@nboard-dev/octus 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/bin/octus.js +5 -0
- package/package.json +68 -0
- package/src/commands/config.js +29 -0
- package/src/commands/doctor.js +254 -0
- package/src/commands/generate-profile.js +46 -0
- package/src/commands/init.js +311 -0
- package/src/commands/login.js +60 -0
- package/src/commands/logout.js +21 -0
- package/src/commands/ping.js +72 -0
- package/src/commands/setup.js +439 -0
- package/src/index.js +62 -0
- package/src/lib/api.js +210 -0
- package/src/lib/config.js +254 -0
- package/src/lib/octus-contract.js +231 -0
- package/src/lib/profile-generator.js +443 -0
- package/src/lib/ui.js +186 -0
- package/src/lib/version.js +1 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import { config, runLock, runState } from '../lib/config.js';
|
|
10
|
+
import { api } from '../lib/api.js';
|
|
11
|
+
import ui from '../lib/ui.js';
|
|
12
|
+
import {
|
|
13
|
+
buildEnvironmentSnapshot,
|
|
14
|
+
buildLogEntries,
|
|
15
|
+
buildRunContext,
|
|
16
|
+
buildRunPayload,
|
|
17
|
+
computeProfileHash,
|
|
18
|
+
normalizeInitFilePayload,
|
|
19
|
+
normalizeProfile,
|
|
20
|
+
scanRepository,
|
|
21
|
+
} from '../lib/octus-contract.js';
|
|
22
|
+
|
|
23
|
+
export const setupCommand = new Command('setup')
|
|
24
|
+
.description('Execute onboarding setup run')
|
|
25
|
+
.argument('[path]', 'Repository path', '.')
|
|
26
|
+
.option('--profile <key>', 'Profile key (auto|demo-node|demo-python|...)', 'auto')
|
|
27
|
+
.option('--ignore-repo-profile', 'Ignore repo-local .octus/profile.yaml')
|
|
28
|
+
.option('--timeout <seconds>', 'Per-step timeout in seconds', '300')
|
|
29
|
+
.option('--force-lock', 'Break stale run lock')
|
|
30
|
+
.option('--resume', 'Resume last local run')
|
|
31
|
+
.option('--force-resume', 'Allow resume from different repo path')
|
|
32
|
+
.option('--step-delay-ms <ms>', 'Delay before each step (testing)', '0')
|
|
33
|
+
.option('--turbo', 'Skip confirmations and run fast')
|
|
34
|
+
.option('--verbose', 'Show detailed output')
|
|
35
|
+
.action(async (pathArg, options) => {
|
|
36
|
+
await runSetup(pathArg, options);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function normalizeRunSteps(steps = []) {
|
|
40
|
+
return [...steps].sort((a, b) => {
|
|
41
|
+
const aIndex = a.step_index ?? a.stepIndex ?? 0;
|
|
42
|
+
const bIndex = b.step_index ?? b.stepIndex ?? 0;
|
|
43
|
+
return aIndex - bIndex;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function ensureLoggedIn(turbo) {
|
|
48
|
+
if (config.isLoggedIn()) return;
|
|
49
|
+
if (turbo) {
|
|
50
|
+
ui.error('Not logged in. Run `octus login` first.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ui.warning('Not logged in');
|
|
55
|
+
const { doLogin } = await inquirer.prompt([
|
|
56
|
+
{ type: 'confirm', name: 'doLogin', message: 'Login now?', default: true },
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
if (!doLogin) {
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { loginCommand } = await import('./login.js');
|
|
64
|
+
await loginCommand.parseAsync(['login'], { from: 'user' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runSetup(pathArg, options = {}) {
|
|
68
|
+
const repoPath = resolve(pathArg || '.');
|
|
69
|
+
const timeoutMs = parseInt(options.timeout || '300', 10) * 1000;
|
|
70
|
+
const stepDelayMs = parseInt(options.stepDelayMs || '0', 10);
|
|
71
|
+
const turbo = Boolean(options.turbo);
|
|
72
|
+
const verbose = Boolean(options.verbose);
|
|
73
|
+
|
|
74
|
+
await ensureLoggedIn(turbo);
|
|
75
|
+
|
|
76
|
+
const octusYamlPath = join(repoPath, '.octus.yaml');
|
|
77
|
+
if (!existsSync(octusYamlPath)) {
|
|
78
|
+
ui.error('.octus.yaml not found. Run `octus init` first.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const lockResult = runLock.acquire(options.forceLock);
|
|
83
|
+
if (!lockResult.success) {
|
|
84
|
+
ui.error(`Another setup is running (PID: ${lockResult.lock.pid})`);
|
|
85
|
+
ui.info('Use --force-lock to override if the other process is dead');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cleanup = () => runLock.release();
|
|
90
|
+
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
91
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
ui.header('Octus Setup');
|
|
95
|
+
ui.info(`Repository: ${repoPath}`);
|
|
96
|
+
|
|
97
|
+
let initPayload;
|
|
98
|
+
try {
|
|
99
|
+
initPayload = normalizeInitFilePayload(
|
|
100
|
+
YAML.parse(readFileSync(octusYamlPath, 'utf-8'))
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
ui.error(error.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const verifySpin = ui.spinner('Verifying init file...').start();
|
|
108
|
+
let verification;
|
|
109
|
+
try {
|
|
110
|
+
verification = await api.verifyInitFile(initPayload);
|
|
111
|
+
verifySpin.succeed('Init file verified');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
verifySpin.fail(`Verification failed: ${error.message}`);
|
|
114
|
+
ui.info('Run `octus init` to regenerate');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const scanResult = scanRepository(repoPath);
|
|
119
|
+
const registerSpin = ui.spinner('Registering repository...').start();
|
|
120
|
+
let repoResponse;
|
|
121
|
+
let currentUser;
|
|
122
|
+
try {
|
|
123
|
+
repoResponse = await api.registerRepository({
|
|
124
|
+
name: scanResult.name,
|
|
125
|
+
local_path: repoPath,
|
|
126
|
+
git_url: null,
|
|
127
|
+
default_branch: 'main',
|
|
128
|
+
project_type: scanResult.project_type,
|
|
129
|
+
metadata: scanResult.metadata,
|
|
130
|
+
has_readme: scanResult.has_readme,
|
|
131
|
+
has_dockerfile: scanResult.has_dockerfile,
|
|
132
|
+
has_docker_compose: scanResult.has_docker_compose,
|
|
133
|
+
});
|
|
134
|
+
currentUser = await api.getMe();
|
|
135
|
+
registerSpin.succeed('Repository registered');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
registerSpin.fail(`Failed to register repository: ${error.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (String(repoResponse.organization_id) !== initPayload.org_id) {
|
|
142
|
+
ui.error('Invalid .octus.yaml — organization mismatch. Run `octus init` again.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (String(repoResponse.id) !== initPayload.repo_id) {
|
|
147
|
+
ui.error('Invalid .octus.yaml — repository mismatch. Run `octus init` again.');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const localProfilePath = join(repoPath, '.octus', 'profile.json');
|
|
152
|
+
let profile;
|
|
153
|
+
let selectedProfileKey = null;
|
|
154
|
+
|
|
155
|
+
if (!options.ignoreRepoProfile && existsSync(localProfilePath)) {
|
|
156
|
+
profile = normalizeProfile(
|
|
157
|
+
JSON.parse(readFileSync(localProfilePath, 'utf-8')),
|
|
158
|
+
scanResult.name
|
|
159
|
+
);
|
|
160
|
+
ui.info(`Using local profile: ${profile.name}`);
|
|
161
|
+
} else {
|
|
162
|
+
const backendProfileKey = options.profile && options.profile !== 'auto'
|
|
163
|
+
? options.profile
|
|
164
|
+
: verification?.profile_key || initPayload.profile_ref;
|
|
165
|
+
const profileSpin = ui.spinner('Fetching profile from backend...').start();
|
|
166
|
+
try {
|
|
167
|
+
profile = normalizeProfile(
|
|
168
|
+
await api.getProfile(backendProfileKey),
|
|
169
|
+
backendProfileKey
|
|
170
|
+
);
|
|
171
|
+
selectedProfileKey = profile.key || backendProfileKey;
|
|
172
|
+
profileSpin.succeed(`Using profile: ${profile.name}`);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
profileSpin.fail(`Failed to fetch profile: ${error.message}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!Array.isArray(profile.steps) || profile.steps.length === 0) {
|
|
180
|
+
ui.warning('No steps in profile');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const profileHash = computeProfileHash(profile.name, profile.steps);
|
|
185
|
+
|
|
186
|
+
let runResponse;
|
|
187
|
+
const existingState = runState.get();
|
|
188
|
+
let localState = {
|
|
189
|
+
run_id: null,
|
|
190
|
+
profile_hash: profileHash,
|
|
191
|
+
repo_path: repoPath,
|
|
192
|
+
steps: {},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (options.resume && existingState.run_id) {
|
|
196
|
+
if (existingState.repo_path && existingState.repo_path !== repoPath && !options.forceResume) {
|
|
197
|
+
ui.error('Saved run is for a different repo path');
|
|
198
|
+
ui.info(`Saved: ${existingState.repo_path}`);
|
|
199
|
+
ui.info(`Current: ${repoPath}`);
|
|
200
|
+
ui.info('Use --force-resume to override');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const runSpin = ui.spinner(`Loading run ${existingState.run_id.slice(0, 8)}...`).start();
|
|
204
|
+
try {
|
|
205
|
+
runResponse = await api.getRun(existingState.run_id);
|
|
206
|
+
localState = {
|
|
207
|
+
...existingState,
|
|
208
|
+
profile_hash: existingState.profile_hash || profileHash,
|
|
209
|
+
repo_path: repoPath,
|
|
210
|
+
steps: existingState.steps || {},
|
|
211
|
+
};
|
|
212
|
+
runSpin.succeed('Resume state loaded');
|
|
213
|
+
} catch (error) {
|
|
214
|
+
runSpin.fail(`Failed to load saved run: ${error.message}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
const runSpin = ui.spinner('Creating onboarding run...').start();
|
|
219
|
+
try {
|
|
220
|
+
runResponse = await api.createRun(
|
|
221
|
+
buildRunPayload({
|
|
222
|
+
repoResponse,
|
|
223
|
+
currentUser,
|
|
224
|
+
scanResult,
|
|
225
|
+
selectedProfile: profile.name,
|
|
226
|
+
selectedProfileKey,
|
|
227
|
+
selectedProfileVersion: profile.version,
|
|
228
|
+
profileHash,
|
|
229
|
+
stepDefs: profile.steps,
|
|
230
|
+
runContext: buildRunContext({
|
|
231
|
+
scanResult,
|
|
232
|
+
repoPath,
|
|
233
|
+
profileName: profile.name,
|
|
234
|
+
profileVersion: profile.version,
|
|
235
|
+
resume: false,
|
|
236
|
+
gitRemoteUrl: null,
|
|
237
|
+
}),
|
|
238
|
+
environmentSnapshot: buildEnvironmentSnapshot({
|
|
239
|
+
scanResult,
|
|
240
|
+
profileName: profile.name,
|
|
241
|
+
profileVersion: profile.version,
|
|
242
|
+
captureSource: 'cli_setup_start',
|
|
243
|
+
}),
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
runSpin.succeed(`Run created: ${runResponse.id.slice(0, 8)}...`);
|
|
247
|
+
localState.run_id = runResponse.id;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
runSpin.fail(`Failed to create run: ${error.message}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
runState.set(localState);
|
|
255
|
+
|
|
256
|
+
ui.divider();
|
|
257
|
+
ui.header('Running Onboarding Steps');
|
|
258
|
+
|
|
259
|
+
const runSteps = normalizeRunSteps(runResponse?.steps || []);
|
|
260
|
+
let allPassed = true;
|
|
261
|
+
let failedStep = null;
|
|
262
|
+
|
|
263
|
+
for (let index = 0; index < runSteps.length; index += 1) {
|
|
264
|
+
const backendStep = runSteps[index];
|
|
265
|
+
const localStep = profile.steps[index] || backendStep;
|
|
266
|
+
const stepId = backendStep.id;
|
|
267
|
+
const stepTitle = localStep.title || backendStep.label || backendStep.title || localStep.step_key;
|
|
268
|
+
const existingStateForStep = localState.steps[stepId];
|
|
269
|
+
const knownStatus = existingStateForStep?.status || backendStep.status;
|
|
270
|
+
|
|
271
|
+
if (knownStatus === 'success' || knownStatus === 'skipped') {
|
|
272
|
+
ui.step(index + 1, runSteps.length, stepTitle, 'success');
|
|
273
|
+
console.log(chalk.dim(' (already completed)'));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (stepDelayMs > 0) {
|
|
278
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, stepDelayMs));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
ui.step(index + 1, runSteps.length, stepTitle, 'running');
|
|
282
|
+
|
|
283
|
+
let exitCode = 0;
|
|
284
|
+
let stdout = '';
|
|
285
|
+
let stderr = '';
|
|
286
|
+
const startedAt = new Date().toISOString();
|
|
287
|
+
const startedMs = Date.now();
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
if (backendStep.status !== 'running') {
|
|
291
|
+
await api.startStep(stepId, {
|
|
292
|
+
event_id: randomUUID(),
|
|
293
|
+
sequence: 1,
|
|
294
|
+
started_at: startedAt,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (localStep.command) {
|
|
299
|
+
if (verbose) {
|
|
300
|
+
console.log(chalk.dim(` $ ${localStep.command}`));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const result = await execa(localStep.command, {
|
|
304
|
+
cwd: repoPath,
|
|
305
|
+
timeout: timeoutMs,
|
|
306
|
+
shell: true,
|
|
307
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
308
|
+
reject: false,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
exitCode = result.exitCode;
|
|
312
|
+
stdout = result.stdout || '';
|
|
313
|
+
stderr = result.stderr || '';
|
|
314
|
+
|
|
315
|
+
if (verbose && stdout) {
|
|
316
|
+
stdout.split('\n').slice(0, 10).forEach((line) => {
|
|
317
|
+
if (line.trim()) console.log(chalk.dim(` ${line}`));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const durationMs = Date.now() - startedMs;
|
|
323
|
+
localState.steps[stepId] = {
|
|
324
|
+
status: exitCode === 0 ? 'success' : 'failed',
|
|
325
|
+
metadata: {
|
|
326
|
+
step_key: localStep.step_key,
|
|
327
|
+
command: localStep.command,
|
|
328
|
+
step_index: index,
|
|
329
|
+
exit_code: exitCode,
|
|
330
|
+
duration_ms: durationMs,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
runState.set(localState);
|
|
334
|
+
|
|
335
|
+
await api.finishStep(stepId, {
|
|
336
|
+
event_id: randomUUID(),
|
|
337
|
+
sequence: 2,
|
|
338
|
+
status: exitCode === 0 ? 'success' : 'failed',
|
|
339
|
+
exit_code: exitCode,
|
|
340
|
+
duration_ms: durationMs,
|
|
341
|
+
stdout_tail: stdout.slice(-2000),
|
|
342
|
+
stderr_tail: stderr.slice(-2000),
|
|
343
|
+
finished_at: new Date().toISOString(),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (stdout || stderr) {
|
|
347
|
+
const { entries } = buildLogEntries(stdout, stderr, 1);
|
|
348
|
+
if (entries.length > 0) {
|
|
349
|
+
await api.appendStepLogsBatch(stepId, { entries }).catch(() => {});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
process.stdout.write('\x1B[1A\x1B[2K');
|
|
354
|
+
if (exitCode === 0) {
|
|
355
|
+
ui.step(index + 1, runSteps.length, stepTitle, 'success');
|
|
356
|
+
console.log(chalk.dim(` (${durationMs}ms)`));
|
|
357
|
+
} else {
|
|
358
|
+
ui.step(index + 1, runSteps.length, stepTitle, 'failed');
|
|
359
|
+
console.log(chalk.red(` Exit code: ${exitCode}`));
|
|
360
|
+
if (stderr) {
|
|
361
|
+
stderr.split('\n').slice(0, 3).forEach((line) => {
|
|
362
|
+
if (line.trim()) console.log(chalk.red(` ${line}`));
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
allPassed = false;
|
|
366
|
+
failedStep = localStep;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
process.stdout.write('\x1B[1A\x1B[2K');
|
|
371
|
+
ui.step(index + 1, runSteps.length, stepTitle, 'failed');
|
|
372
|
+
console.log(chalk.red(` Error: ${error.message}`));
|
|
373
|
+
|
|
374
|
+
localState.steps[stepId] = {
|
|
375
|
+
status: 'failed',
|
|
376
|
+
metadata: {
|
|
377
|
+
step_key: localStep.step_key,
|
|
378
|
+
command: localStep.command,
|
|
379
|
+
step_index: index,
|
|
380
|
+
error: error.message,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
runState.set(localState);
|
|
384
|
+
|
|
385
|
+
allPassed = false;
|
|
386
|
+
failedStep = localStep;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
ui.divider();
|
|
392
|
+
|
|
393
|
+
if (allPassed) {
|
|
394
|
+
runState.clear();
|
|
395
|
+
ui.successPanel('Setup Complete', `
|
|
396
|
+
All ${runSteps.length} steps completed successfully!
|
|
397
|
+
|
|
398
|
+
${chalk.bold('Next steps:')}
|
|
399
|
+
• Start coding
|
|
400
|
+
• Check ${chalk.cyan('octus doctor')} for environment status
|
|
401
|
+
• View your dashboard at ${chalk.cyan('https://demo.nboard.tech')}
|
|
402
|
+
`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
ui.failurePanel('Setup Failed', `
|
|
407
|
+
Step "${failedStep?.title || failedStep?.step_key || 'unknown'}" failed.
|
|
408
|
+
|
|
409
|
+
${chalk.bold('To retry:')}
|
|
410
|
+
${chalk.cyan('octus setup --resume')}
|
|
411
|
+
|
|
412
|
+
${chalk.bold('To debug:')}
|
|
413
|
+
${chalk.cyan('octus doctor')}
|
|
414
|
+
|
|
415
|
+
${chalk.bold('To start fresh:')}
|
|
416
|
+
${chalk.cyan('octus setup --force-lock')}
|
|
417
|
+
`);
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const patterns = await api.getBlockerPatterns({
|
|
421
|
+
step_key: failedStep?.step_key,
|
|
422
|
+
});
|
|
423
|
+
if (Array.isArray(patterns) && patterns.length > 0) {
|
|
424
|
+
ui.info('Similar issues resolved before:');
|
|
425
|
+
patterns.slice(0, 3).forEach((pattern) => {
|
|
426
|
+
console.log(chalk.dim(` • ${pattern.resolution_summary || pattern.reason}`));
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
// ignore blocker hint failures
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
process.exit(1);
|
|
434
|
+
} finally {
|
|
435
|
+
cleanup();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export default setupCommand;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { version } from './lib/version.js';
|
|
4
|
+
|
|
5
|
+
// Import commands
|
|
6
|
+
import { loginCommand } from './commands/login.js';
|
|
7
|
+
import { logoutCommand } from './commands/logout.js';
|
|
8
|
+
import { pingCommand } from './commands/ping.js';
|
|
9
|
+
import { configCommand } from './commands/config.js';
|
|
10
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
11
|
+
import { initCommand } from './commands/init.js';
|
|
12
|
+
import { setupCommand } from './commands/setup.js';
|
|
13
|
+
import { generateProfileCommand } from './commands/generate-profile.js';
|
|
14
|
+
|
|
15
|
+
export const program = new Command();
|
|
16
|
+
|
|
17
|
+
// ASCII art banner
|
|
18
|
+
const banner = chalk.cyan(`
|
|
19
|
+
____ __
|
|
20
|
+
/ __ \\____/ /___ _______
|
|
21
|
+
/ / / / __/ __/ / / / ___/
|
|
22
|
+
/ /_/ / /_/ /_/ /_/ (__ )
|
|
23
|
+
\\____/\\__/\\__/\\__,_/____/
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('octus')
|
|
28
|
+
.description(`${banner}\n ${chalk.dim('AI-powered onboarding for engineering teams')}`)
|
|
29
|
+
.version(version, '-V, --version', 'Show version')
|
|
30
|
+
.addHelpText('after', `
|
|
31
|
+
${chalk.bold('Examples:')}
|
|
32
|
+
${chalk.dim('# First time setup')}
|
|
33
|
+
$ octus login
|
|
34
|
+
$ octus init .
|
|
35
|
+
$ octus setup
|
|
36
|
+
|
|
37
|
+
${chalk.dim('# Check everything is working')}
|
|
38
|
+
$ octus doctor
|
|
39
|
+
|
|
40
|
+
${chalk.dim('# Quick start (login + init + setup)')}
|
|
41
|
+
$ octus setup --turbo
|
|
42
|
+
|
|
43
|
+
${chalk.bold('Documentation:')}
|
|
44
|
+
https://docs.nboard.tech/cli
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
// Register all commands
|
|
48
|
+
program.addCommand(loginCommand);
|
|
49
|
+
program.addCommand(logoutCommand);
|
|
50
|
+
program.addCommand(pingCommand);
|
|
51
|
+
program.addCommand(configCommand);
|
|
52
|
+
program.addCommand(doctorCommand);
|
|
53
|
+
program.addCommand(initCommand);
|
|
54
|
+
program.addCommand(setupCommand);
|
|
55
|
+
program.addCommand(generateProfileCommand);
|
|
56
|
+
|
|
57
|
+
// Default action when no command provided
|
|
58
|
+
program.action(() => {
|
|
59
|
+
program.outputHelp();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export default program;
|