Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
# Node 18 reached EOL April 2025. ESLint 10 and Vitest/rolldown require
# util.styleText which was added in Node 20.12. Min supported: Node 20.
node-version: [20, 22]
node-version: [22, 24]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci
Expand All @@ -44,8 +50,8 @@ jobs:
run: npm test

- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.node-version == 22
uses: codecov/codecov-action@v6
if: matrix.node-version == 24
with:
files: ./coverage/lcov.info
fail_ci_if_error: false
54 changes: 28 additions & 26 deletions src/cli/commands/cloud.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* conductor cloud - Cloud sync and account management
*
*
* Commands:
* cloud login - Log in to Conductor Cloud and pair device
* cloud sync - Sync credentials from cloud
Expand Down Expand Up @@ -51,13 +51,13 @@ async function apiRequest(
method?: string;
body?: unknown;
sessionId?: string;
} = {}
} = {},
): Promise<Record<string, unknown>> {
const url = `${serverUrl}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};

if (options.sessionId) {
headers['Authorization'] = `Bearer ${options.sessionId}`;
}
Expand All @@ -68,17 +68,15 @@ async function apiRequest(
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
return await response.json() as Record<string, unknown>;

return (await response.json()) as Record<string, unknown>;
} catch (error) {
return { success: false, error: String(error) };
}
}

export function registerCloudCommands(program: Command) {
const cloud = program
.command('cloud')
.description('Conductor Cloud - sync credentials across devices');
const cloud = program.command('cloud').description('Conductor Cloud - sync credentials across devices');

// cloud login
cloud
Expand All @@ -101,7 +99,7 @@ export function registerCloudCommands(program: Command) {
// Generate device credentials
const deviceId = crypto.randomUUID();
const deviceName = process.env.HOSTNAME || 'My Computer';

// Create pairing request
console.log(chalk.gray(' Creating device pairing request...'));
const pairResult = await apiRequest(serverUrl, '/device/pair', {
Expand All @@ -111,10 +109,10 @@ export function registerCloudCommands(program: Command) {

if (!pairResult.success) {
console.log(chalk.red(` ✗ Failed to create pairing request: ${pairResult.error}`));

// For demo mode, simulate success
console.log(chalk.yellow(' ⚠ Running in demo mode (simulated)'));

const config: CloudConfig = {
connected: true,
deviceId,
Expand All @@ -124,11 +122,13 @@ export function registerCloudCommands(program: Command) {
lastSync: Date.now(),
sessionId: `demo_session_${Date.now()}`,
};

await saveCloudConfig(config);

console.log(chalk.green(' ✓ Connected to Conductor Cloud!'));
console.log(chalk.gray(' Run ') + chalk.white('conductor cloud sync') + chalk.gray(' to download credentials.\n'));
console.log(
chalk.gray(' Run ') + chalk.white('conductor cloud sync') + chalk.gray(' to download credentials.\n'),
);
return;
}

Expand All @@ -151,21 +151,21 @@ export function registerCloudCommands(program: Command) {
// Poll for approval
let attempts = 0;
const maxAttempts = 60;

while (attempts < maxAttempts) {
await new Promise(r => setTimeout(r, 2000));
await new Promise((r) => setTimeout(r, 2000));

const verifyResult = await apiRequest(serverUrl, `/device/pairing?requestId=${requestId}`, {
sessionId: requestId,
});

if (verifyResult.success && (verifyResult.requests as unknown[])?.length === 0) {
// Device approved!
break;
}

attempts++;

if (attempts % 10 === 0) {
console.log(chalk.gray(` Waiting for approval... (${attempts * 2}s)`));
}
Expand All @@ -185,19 +185,21 @@ export function registerCloudCommands(program: Command) {
lastSync: Date.now(),
sessionId: requestId,
};

await saveCloudConfig(config);

console.log(chalk.green(' ✓ Device paired successfully!'));
console.log(chalk.gray(' Run ') + chalk.white('conductor cloud sync') + chalk.gray(' to download credentials.\n'));
console.log(
chalk.gray(' Run ') + chalk.white('conductor cloud sync') + chalk.gray(' to download credentials.\n'),
);
});

// cloud sync
cloud
.command('sync')
.description('Sync credentials from Conductor Cloud')
.option('--force', 'Force full sync')
.action(async (opts: { force?: boolean }) => {
.action(async (_opts: { force?: boolean }) => {
const config = await loadCloudConfig();

if (!config.connected || !config.serverUrl) {
Expand Down Expand Up @@ -283,7 +285,7 @@ export function registerCloudCommands(program: Command) {
console.log(chalk.gray(' Server: ') + chalk.white(config.serverUrl || 'cloud.conductor.sh'));
console.log(chalk.gray(' Email: ') + chalk.white(config.email || 'N/A'));
console.log(chalk.gray(' Device: ') + chalk.white(config.deviceId?.substring(0, 8) || 'N/A'));

if (config.lastSync) {
console.log(chalk.gray(' Last sync: ') + chalk.white(new Date(config.lastSync).toLocaleString()));
}
Expand Down Expand Up @@ -312,7 +314,7 @@ export function registerCloudCommands(program: Command) {
console.log(chalk.gray(' ' + '─'.repeat(50)));

const devices = (result.devices as unknown[]) || [];

if (devices.length === 0) {
console.log(chalk.gray(' Only this device connected.\n'));
return;
Expand Down Expand Up @@ -348,4 +350,4 @@ export function registerCloudCommands(program: Command) {
console.log(chalk.gray(' Server: ') + chalk.white(config.serverUrl || 'N/A'));
console.log('');
});
}
}
1 change: 0 additions & 1 deletion src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ export async function doctor(conductor: Conductor): Promise<void> {
// Disk space
const homeDir = os.homedir();
try {

const si = await import('systeminformation');
const fsInfo = await si.default.fsSize();
const homeFs = fsInfo.find((f: any) => homeDir.startsWith(f.mount));
Expand Down
35 changes: 25 additions & 10 deletions src/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ function stepHeader(n: number, total: number, label: string): void {

function printBanner(): void {
const W = 50;
const top = ' ┌' + '─'.repeat(W) + '┐';
const bot = ' └' + '─'.repeat(W) + '┘';
const blank = ' │' + ' '.repeat(W) + '│';
const top = ' ┌' + '─'.repeat(W) + '┐';
const bot = ' └' + '─'.repeat(W) + '┘';
const blank = ' │' + ' '.repeat(W) + '│';
const line = (text: string) => {
const pad = W - text.length - 1;
return ` │ ${B}${text}${R}` + ' '.repeat(Math.max(0, pad)) + '│';
Expand Down Expand Up @@ -159,8 +159,13 @@ async function setupAIProvider(conductor: Conductor): Promise<void> {
console.log('');
console.log(` ${D}Get your key at: https://console.anthropic.com${R}`);
const { apiKey } = await inquirer.prompt<{ apiKey: string }>([
{ type: 'password', name: 'apiKey', message: 'Anthropic API key:', mask: '*',
validate: (v: string) => v.trim().length > 0 || 'API key is required' },
{
type: 'password',
name: 'apiKey',
message: 'Anthropic API key:',
mask: '*',
validate: (v: string) => v.trim().length > 0 || 'API key is required',
},
]);
await aiManager.setupClaude(apiKey.trim());
console.log(` ${G}✓${R} Claude configured`);
Expand All @@ -170,8 +175,13 @@ async function setupAIProvider(conductor: Conductor): Promise<void> {
console.log('');
console.log(` ${D}Get your key at: https://platform.openai.com/api-keys${R}`);
const { apiKey } = await inquirer.prompt<{ apiKey: string }>([
{ type: 'password', name: 'apiKey', message: 'OpenAI API key:', mask: '*',
validate: (v: string) => v.trim().length > 0 || 'API key is required' },
{
type: 'password',
name: 'apiKey',
message: 'OpenAI API key:',
mask: '*',
validate: (v: string) => v.trim().length > 0 || 'API key is required',
},
]);
await aiManager.setupOpenAI(apiKey.trim());
console.log(` ${G}✓${R} OpenAI configured`);
Expand All @@ -181,8 +191,13 @@ async function setupAIProvider(conductor: Conductor): Promise<void> {
console.log('');
console.log(` ${D}Get your key at: https://aistudio.google.com/app/apikey${R}`);
const { apiKey } = await inquirer.prompt<{ apiKey: string }>([
{ type: 'password', name: 'apiKey', message: 'Gemini API key:', mask: '*',
validate: (v: string) => v.trim().length > 0 || 'API key is required' },
{
type: 'password',
name: 'apiKey',
message: 'Gemini API key:',
mask: '*',
validate: (v: string) => v.trim().length > 0 || 'API key is required',
},
]);
await aiManager.setupGemini(apiKey.trim());
console.log(` ${G}✓${R} Gemini configured`);
Expand Down Expand Up @@ -357,4 +372,4 @@ export async function init(conductor: Conductor): Promise<void> {
await setupMCPClient(conductor);
printFinalInstructions();
}
}
}
Loading
Loading