orbital-command 0.3.0 → 1.0.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/README.md +67 -42
- package/bin/commands/config.js +19 -0
- package/bin/commands/events.js +40 -0
- package/bin/commands/launch.js +126 -0
- package/bin/commands/manifest.js +283 -0
- package/bin/commands/registry.js +104 -0
- package/bin/commands/update.js +24 -0
- package/bin/lib/helpers.js +229 -0
- package/bin/orbital.js +95 -870
- package/dist/assets/Landing-CfQdHR0N.js +11 -0
- package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
- package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
- package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
- package/dist/assets/Settings-DLcZwbCT.js +12 -0
- package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
- package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
- package/dist/assets/{arrow-down-CPy85_J6.js → arrow-down-DVPp6_qp.js} +1 -1
- package/dist/assets/bot-NFaJBDn_.js +6 -0
- package/dist/assets/{charts-DbDg0Psc.js → charts-LGLb8hyU.js} +1 -1
- package/dist/assets/{circle-x-Cwz6ZQDV.js → circle-x-IsFCkBZu.js} +1 -1
- package/dist/assets/{file-text-C46Xr65c.js → file-text-J1cebZXF.js} +1 -1
- package/dist/assets/{globe-Cn2yNZUD.js → globe-WzeyHsUc.js} +1 -1
- package/dist/assets/index-BdJ57EhC.css +1 -0
- package/dist/assets/index-o4ScMAuR.js +349 -0
- package/dist/assets/{key-OPaNTWJ5.js → key-CKR8JJSj.js} +1 -1
- package/dist/assets/{minus-GMsbpKym.js → minus-CHBsJyjp.js} +1 -1
- package/dist/assets/radio-xqZaR-Uk.js +6 -0
- package/dist/assets/rocket-D_xvvNG6.js +6 -0
- package/dist/assets/{shield-DwAFkDYI.js → shield-TdB1yv_a.js} +1 -1
- package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
- package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
- package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
- package/dist/assets/zap-C9wqYMpl.js +6 -0
- package/dist/index.html +3 -3
- package/dist/server/server/__tests__/data-routes.test.js +2 -0
- package/dist/server/server/__tests__/scope-routes.test.js +1 -0
- package/dist/server/server/config-migrator.js +0 -3
- package/dist/server/server/config.js +35 -6
- package/dist/server/server/database.js +0 -22
- package/dist/server/server/index.js +26 -814
- package/dist/server/server/init.js +32 -399
- package/dist/server/server/launch.js +1 -1
- package/dist/server/server/parsers/event-parser.js +4 -1
- package/dist/server/server/project-context.js +19 -9
- package/dist/server/server/project-manager.js +6 -6
- package/dist/server/server/routes/aggregate-routes.js +871 -0
- package/dist/server/server/routes/config-routes.js +41 -88
- package/dist/server/server/routes/data-routes.js +5 -15
- package/dist/server/server/routes/dispatch-routes.js +24 -8
- package/dist/server/server/routes/manifest-routes.js +1 -1
- package/dist/server/server/routes/scope-routes.js +10 -7
- package/dist/server/server/schema.js +1 -0
- package/dist/server/server/services/batch-orchestrator.js +17 -3
- package/dist/server/server/services/config-service.js +10 -1
- package/dist/server/server/services/scope-service.js +7 -7
- package/dist/server/server/services/sprint-orchestrator.js +24 -11
- package/dist/server/server/services/sprint-service.js +2 -2
- package/dist/server/server/uninstall.js +195 -0
- package/dist/server/server/update.js +212 -0
- package/dist/server/server/utils/dispatch-utils.js +8 -6
- package/dist/server/server/utils/flag-builder.js +54 -0
- package/dist/server/server/utils/json-fields.js +14 -0
- package/dist/server/server/utils/json-fields.test.js +73 -0
- package/dist/server/server/utils/route-helpers.js +37 -0
- package/dist/server/server/utils/route-helpers.test.js +115 -0
- package/dist/server/server/watchers/event-watcher.js +28 -13
- package/dist/server/server/wizard/config-editor.js +4 -4
- package/dist/server/server/wizard/doctor.js +2 -2
- package/dist/server/server/wizard/index.js +224 -39
- package/dist/server/server/wizard/phases/welcome.js +1 -4
- package/dist/server/server/wizard/ui.js +6 -7
- package/dist/server/shared/api-types.js +80 -1
- package/dist/server/shared/workflow-engine.js +1 -1
- package/package.json +20 -20
- package/schemas/orbital.config.schema.json +1 -19
- package/scripts/postinstall.js +6 -42
- package/scripts/release.sh +53 -0
- package/server/__tests__/data-routes.test.ts +2 -0
- package/server/__tests__/scope-routes.test.ts +1 -0
- package/server/config-migrator.ts +0 -3
- package/server/config.ts +39 -11
- package/server/database.ts +0 -26
- package/server/global-config.ts +4 -0
- package/server/index.ts +29 -894
- package/server/init.ts +32 -443
- package/server/launch.ts +1 -1
- package/server/parsers/event-parser.ts +4 -1
- package/server/project-context.ts +26 -10
- package/server/project-manager.ts +5 -6
- package/server/routes/aggregate-routes.ts +968 -0
- package/server/routes/config-routes.ts +41 -81
- package/server/routes/data-routes.ts +7 -16
- package/server/routes/dispatch-routes.ts +29 -8
- package/server/routes/manifest-routes.ts +1 -1
- package/server/routes/scope-routes.ts +12 -7
- package/server/schema.ts +1 -0
- package/server/services/batch-orchestrator.ts +18 -2
- package/server/services/config-service.ts +10 -1
- package/server/services/scope-service.ts +6 -6
- package/server/services/sprint-orchestrator.ts +24 -9
- package/server/services/sprint-service.ts +2 -2
- package/server/uninstall.ts +214 -0
- package/server/update.ts +263 -0
- package/server/utils/dispatch-utils.ts +8 -6
- package/server/utils/flag-builder.ts +56 -0
- package/server/utils/json-fields.test.ts +83 -0
- package/server/utils/json-fields.ts +14 -0
- package/server/utils/route-helpers.test.ts +144 -0
- package/server/utils/route-helpers.ts +38 -0
- package/server/watchers/event-watcher.ts +24 -12
- package/server/wizard/config-editor.ts +4 -4
- package/server/wizard/doctor.ts +2 -2
- package/server/wizard/index.ts +291 -40
- package/server/wizard/phases/welcome.ts +1 -5
- package/server/wizard/ui.ts +6 -7
- package/shared/api-types.ts +106 -0
- package/shared/workflow-engine.ts +1 -1
- package/templates/agents/QUICK-REFERENCE.md +1 -0
- package/templates/agents/README.md +1 -0
- package/templates/agents/SKILL-TRIGGERS.md +11 -0
- package/templates/agents/green-team/deep-dive.md +361 -0
- package/templates/hooks/end-session.sh +1 -0
- package/templates/hooks/init-session.sh +1 -0
- package/templates/hooks/scope-commit-logger.sh +2 -2
- package/templates/hooks/scope-create-gate.sh +2 -4
- package/templates/hooks/scope-gate.sh +4 -6
- package/templates/hooks/scope-helpers.sh +10 -1
- package/templates/hooks/scope-lifecycle-gate.sh +14 -5
- package/templates/hooks/scope-prepare.sh +1 -1
- package/templates/hooks/scope-transition.sh +14 -6
- package/templates/hooks/time-tracker.sh +2 -5
- package/templates/orbital.config.json +1 -4
- package/templates/presets/development.json +4 -4
- package/templates/presets/gitflow.json +7 -0
- package/templates/prompts/README.md +23 -0
- package/templates/prompts/deep-dive-audit.md +94 -0
- package/templates/quick/rules.md +56 -5
- package/templates/skills/git-commit/SKILL.md +21 -6
- package/templates/skills/git-dev/SKILL.md +8 -4
- package/templates/skills/git-main/SKILL.md +8 -4
- package/templates/skills/git-production/SKILL.md +6 -3
- package/templates/skills/git-staging/SKILL.md +6 -3
- package/templates/skills/scope-fix-review/SKILL.md +8 -4
- package/templates/skills/scope-implement/SKILL.md +13 -5
- package/templates/skills/scope-post-review/SKILL.md +16 -4
- package/templates/skills/scope-pre-review/SKILL.md +6 -2
- package/dist/assets/PrimitivesConfig-CrmQXYh4.js +0 -32
- package/dist/assets/QualityGates-BbasOsF3.js +0 -21
- package/dist/assets/SessionTimeline-CGeJsVvy.js +0 -1
- package/dist/assets/Settings-oiM496mc.js +0 -12
- package/dist/assets/SourceControl-B1fP2nJL.js +0 -41
- package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +0 -74
- package/dist/assets/formatDistanceToNow-BMqsSP44.js +0 -1
- package/dist/assets/index-Aj4sV8Al.css +0 -1
- package/dist/assets/index-Bc9dK3MW.js +0 -354
- package/dist/assets/useWorkflowEditor-BJkTX_NR.js +0 -16
- package/dist/assets/zap-DfbUoOty.js +0 -11
- package/dist/server/server/services/telemetry-service.js +0 -143
- package/server/services/telemetry-service.ts +0 -195
- /package/{shared/default-workflow.json → templates/presets/default.json} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import { errMsg, isValidRelativePath, inferErrorStatus, catchRoute } from './route-helpers.js';
|
|
5
|
+
// ─── errMsg ─────────────────────────────────────────────────
|
|
6
|
+
describe('errMsg', () => {
|
|
7
|
+
it('extracts message from Error instances', () => {
|
|
8
|
+
expect(errMsg(new Error('something broke'))).toBe('something broke');
|
|
9
|
+
});
|
|
10
|
+
it('converts non-Error values to strings', () => {
|
|
11
|
+
expect(errMsg('raw string')).toBe('raw string');
|
|
12
|
+
expect(errMsg(42)).toBe('42');
|
|
13
|
+
expect(errMsg(null)).toBe('null');
|
|
14
|
+
expect(errMsg(undefined)).toBe('undefined');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
// ─── isValidRelativePath ────────────────────────────────────
|
|
18
|
+
describe('isValidRelativePath', () => {
|
|
19
|
+
it('accepts normal relative paths', () => {
|
|
20
|
+
expect(isValidRelativePath('hooks/init.sh')).toBe(true);
|
|
21
|
+
expect(isValidRelativePath('agents/attacker/AGENT.md')).toBe(true);
|
|
22
|
+
expect(isValidRelativePath('file.txt')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('rejects directory traversal', () => {
|
|
25
|
+
expect(isValidRelativePath('../etc/passwd')).toBe(false);
|
|
26
|
+
expect(isValidRelativePath('hooks/../../secret')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it('rejects absolute paths', () => {
|
|
29
|
+
expect(isValidRelativePath('/etc/passwd')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it('rejects null bytes', () => {
|
|
32
|
+
expect(isValidRelativePath('file\0.txt')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
// ─── inferErrorStatus ───────────────────────────────────────
|
|
36
|
+
describe('inferErrorStatus', () => {
|
|
37
|
+
it('returns 403 for traversal errors', () => {
|
|
38
|
+
expect(inferErrorStatus('Path traversal detected')).toBe(403);
|
|
39
|
+
expect(inferErrorStatus('directory traversal not allowed')).toBe(403);
|
|
40
|
+
});
|
|
41
|
+
it('returns 404 for not-found errors', () => {
|
|
42
|
+
expect(inferErrorStatus('File not found')).toBe(404);
|
|
43
|
+
expect(inferErrorStatus('ENOENT: no such file')).toBe(404);
|
|
44
|
+
});
|
|
45
|
+
it('returns 409 for already-exists errors', () => {
|
|
46
|
+
expect(inferErrorStatus('File already exists at path')).toBe(409);
|
|
47
|
+
});
|
|
48
|
+
it('returns 400 for directory errors', () => {
|
|
49
|
+
expect(inferErrorStatus('Cannot delete directory')).toBe(400);
|
|
50
|
+
});
|
|
51
|
+
it('returns 500 for unrecognized errors', () => {
|
|
52
|
+
expect(inferErrorStatus('something unexpected')).toBe(500);
|
|
53
|
+
expect(inferErrorStatus('')).toBe(500);
|
|
54
|
+
});
|
|
55
|
+
it('matches the first keyword when multiple are present', () => {
|
|
56
|
+
// "traversal" comes first in the chain, should win
|
|
57
|
+
expect(inferErrorStatus('traversal not found')).toBe(403);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// ─── catchRoute ─────────────────────────────────────────────
|
|
61
|
+
describe('catchRoute', () => {
|
|
62
|
+
function createApp(handler) {
|
|
63
|
+
const app = express();
|
|
64
|
+
app.get('/test', handler);
|
|
65
|
+
return app;
|
|
66
|
+
}
|
|
67
|
+
it('returns normal response when handler succeeds', async () => {
|
|
68
|
+
const app = createApp(catchRoute((_req, res) => {
|
|
69
|
+
res.json({ ok: true });
|
|
70
|
+
}));
|
|
71
|
+
const res = await request(app).get('/test');
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
expect(res.body).toEqual({ ok: true });
|
|
74
|
+
});
|
|
75
|
+
it('catches sync throws and returns 500', async () => {
|
|
76
|
+
const app = createApp(catchRoute(() => {
|
|
77
|
+
throw new Error('sync failure');
|
|
78
|
+
}));
|
|
79
|
+
const res = await request(app).get('/test');
|
|
80
|
+
expect(res.status).toBe(500);
|
|
81
|
+
expect(res.body).toEqual({ success: false, error: 'sync failure' });
|
|
82
|
+
});
|
|
83
|
+
it('catches async throws and returns 500', async () => {
|
|
84
|
+
const app = createApp(catchRoute(async () => {
|
|
85
|
+
throw new Error('async failure');
|
|
86
|
+
}));
|
|
87
|
+
const res = await request(app).get('/test');
|
|
88
|
+
expect(res.status).toBe(500);
|
|
89
|
+
expect(res.body).toEqual({ success: false, error: 'async failure' });
|
|
90
|
+
});
|
|
91
|
+
it('uses custom statusFn for error status codes', async () => {
|
|
92
|
+
const app = createApp(catchRoute(() => {
|
|
93
|
+
throw new Error('File not found at path');
|
|
94
|
+
}, inferErrorStatus));
|
|
95
|
+
const res = await request(app).get('/test');
|
|
96
|
+
expect(res.status).toBe(404);
|
|
97
|
+
expect(res.body).toEqual({ success: false, error: 'File not found at path' });
|
|
98
|
+
});
|
|
99
|
+
it('uses custom statusFn for traversal errors', async () => {
|
|
100
|
+
const app = createApp(catchRoute(() => {
|
|
101
|
+
throw new Error('Path traversal detected');
|
|
102
|
+
}, inferErrorStatus));
|
|
103
|
+
const res = await request(app).get('/test');
|
|
104
|
+
expect(res.status).toBe(403);
|
|
105
|
+
});
|
|
106
|
+
it('handles non-Error thrown values', async () => {
|
|
107
|
+
const app = createApp(catchRoute(() => {
|
|
108
|
+
// eslint-disable-next-line no-throw-literal
|
|
109
|
+
throw 'raw string error';
|
|
110
|
+
}));
|
|
111
|
+
const res = await request(app).get('/test');
|
|
112
|
+
expect(res.status).toBe(500);
|
|
113
|
+
expect(res.body).toEqual({ success: false, error: 'raw string error' });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -5,6 +5,7 @@ import { parseEventFile } from '../parsers/event-parser.js';
|
|
|
5
5
|
import { createLogger } from '../utils/logger.js';
|
|
6
6
|
const log = createLogger('event');
|
|
7
7
|
const ARCHIVE_DIR_NAME = 'processed';
|
|
8
|
+
const processingFiles = new Set();
|
|
8
9
|
/**
|
|
9
10
|
* Watch .claude/orbital-events/ for new JSON event files.
|
|
10
11
|
* On startup, processes any existing unprocessed events.
|
|
@@ -59,23 +60,37 @@ function processExistingEvents(eventsDir, eventService, archiveDir) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
function processEventFile(filePath, eventService, archiveDir) {
|
|
62
|
-
|
|
63
|
-
if (!event)
|
|
63
|
+
if (processingFiles.has(filePath))
|
|
64
64
|
return;
|
|
65
|
-
|
|
66
|
-
// Move to archive
|
|
67
|
-
const fileName = path.basename(filePath);
|
|
65
|
+
processingFiles.add(filePath);
|
|
68
66
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
67
|
+
const event = parseEventFile(filePath);
|
|
68
|
+
if (!event)
|
|
69
|
+
return;
|
|
70
|
+
eventService.ingest(event);
|
|
71
|
+
// Move to archive
|
|
72
|
+
const fileName = path.basename(filePath);
|
|
74
73
|
try {
|
|
75
|
-
fs.
|
|
74
|
+
fs.renameSync(filePath, path.join(archiveDir, fileName));
|
|
76
75
|
}
|
|
77
|
-
catch (
|
|
78
|
-
|
|
76
|
+
catch (err) {
|
|
77
|
+
const code = err.code;
|
|
78
|
+
if (code === 'ENOENT')
|
|
79
|
+
return; // Already archived by concurrent handler
|
|
80
|
+
log.warn('Failed to archive event file', { file: filePath, error: err.message });
|
|
81
|
+
// If rename fails (cross-device), just delete the source
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(filePath);
|
|
84
|
+
}
|
|
85
|
+
catch (unlinkErr) {
|
|
86
|
+
const unlinkCode = unlinkErr.code;
|
|
87
|
+
if (unlinkCode !== 'ENOENT') {
|
|
88
|
+
log.warn('Failed to delete event file', { file: filePath, error: unlinkErr.message });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
79
91
|
}
|
|
80
92
|
}
|
|
93
|
+
finally {
|
|
94
|
+
processingFiles.delete(filePath);
|
|
95
|
+
}
|
|
81
96
|
}
|
|
@@ -12,7 +12,7 @@ export async function runConfigEditor(projectRoot, packageVersion, args) {
|
|
|
12
12
|
if (subcommand === 'show') {
|
|
13
13
|
const config = loadProjectConfig(projectRoot);
|
|
14
14
|
if (!config) {
|
|
15
|
-
console.error('No config found. Run `orbital
|
|
15
|
+
console.error('No config found. Run `orbital` first.');
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
console.log(JSON.stringify(config, null, 2));
|
|
@@ -32,7 +32,7 @@ export async function runConfigEditor(projectRoot, packageVersion, args) {
|
|
|
32
32
|
// Interactive mode
|
|
33
33
|
const config = loadProjectConfig(projectRoot);
|
|
34
34
|
if (!config) {
|
|
35
|
-
p.log.error('No config found. Run `orbital
|
|
35
|
+
p.log.error('No config found. Run `orbital` first.');
|
|
36
36
|
process.exit(1);
|
|
37
37
|
}
|
|
38
38
|
p.intro(`${pc.bgCyan(pc.black(' Orbital Config '))} ${pc.dim(`v${packageVersion}`)}`);
|
|
@@ -158,7 +158,7 @@ async function editGlobalSection() {
|
|
|
158
158
|
const homedir = process.env.HOME || process.env.USERPROFILE || '~';
|
|
159
159
|
const registryPath = path.join(homedir, '.orbital', 'config.json');
|
|
160
160
|
if (!fs.existsSync(registryPath)) {
|
|
161
|
-
p.log.info('No global registry found. Run `orbital
|
|
161
|
+
p.log.info('No global registry found. Run `orbital` in a project first.');
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
164
|
try {
|
|
@@ -198,7 +198,7 @@ function saveProjectConfig(projectRoot, config) {
|
|
|
198
198
|
function setConfigValue(projectRoot, key, value) {
|
|
199
199
|
const config = loadProjectConfig(projectRoot);
|
|
200
200
|
if (!config) {
|
|
201
|
-
console.error('No config found. Run `orbital
|
|
201
|
+
console.error('No config found. Run `orbital` first.');
|
|
202
202
|
process.exit(1);
|
|
203
203
|
}
|
|
204
204
|
// Parse value: try number, then boolean, then string
|
|
@@ -47,7 +47,7 @@ export async function runDoctor(projectRoot, packageVersion) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
|
-
checks.push({ label: 'Global', status: pc.dim('~/.orbital/ not found (run `orbital
|
|
50
|
+
checks.push({ label: 'Global', status: pc.dim('~/.orbital/ not found (run `orbital` to create)') });
|
|
51
51
|
}
|
|
52
52
|
// 4. Project initialization
|
|
53
53
|
const configPath = path.join(projectRoot, '.claude', 'orbital.config.json');
|
|
@@ -62,7 +62,7 @@ export async function runDoctor(projectRoot, packageVersion) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
else {
|
|
65
|
-
checks.push({ label: 'Project', status: pc.dim('not initialized (run `orbital
|
|
65
|
+
checks.push({ label: 'Project', status: pc.dim('not initialized (run `orbital`)') });
|
|
66
66
|
}
|
|
67
67
|
// 5. Workflow
|
|
68
68
|
const workflowPath = path.join(projectRoot, '.claude', 'config', 'workflow.json');
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import path from 'path';
|
|
12
|
-
import
|
|
12
|
+
import { spawn, execFileSync } from 'child_process';
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
14
|
import pc from 'picocolors';
|
|
15
|
-
import { buildSetupState, buildProjectState
|
|
15
|
+
import { buildSetupState, buildProjectState } from './detect.js';
|
|
16
16
|
import { phaseSetupWizard } from './phases/setup-wizard.js';
|
|
17
17
|
import { phaseWelcome } from './phases/welcome.js';
|
|
18
18
|
import { phaseProjectSetup } from './phases/project-setup.js';
|
|
@@ -21,6 +21,8 @@ import { phaseConfirm, showPostInstall } from './phases/confirm.js';
|
|
|
21
21
|
import { NOTES } from './ui.js';
|
|
22
22
|
import { runConfigEditor } from './config-editor.js';
|
|
23
23
|
import { runDoctor } from './doctor.js';
|
|
24
|
+
import { isITerm2Available } from '../adapters/iterm2-adapter.js';
|
|
25
|
+
import { registerProject } from '../global-config.js';
|
|
24
26
|
export { runConfigEditor, runDoctor };
|
|
25
27
|
// ─── Phase 1: Setup Wizard ─────────────────────────────────────
|
|
26
28
|
/**
|
|
@@ -40,8 +42,8 @@ export async function runSetupWizard(packageVersion) {
|
|
|
40
42
|
p.note(NOTES.setupComplete, 'Done');
|
|
41
43
|
}
|
|
42
44
|
p.outro(state.linkedProjects.length > 0
|
|
43
|
-
? `Run ${pc.cyan('orbital
|
|
44
|
-
: `Run ${pc.cyan('orbital
|
|
45
|
+
? `Run ${pc.cyan('orbital')} to launch the dashboard.`
|
|
46
|
+
: `Run ${pc.cyan('orbital')} in a project directory to get started.`);
|
|
45
47
|
}
|
|
46
48
|
// ─── Phase 2: Project Setup ────────────────────────────────────
|
|
47
49
|
/**
|
|
@@ -56,7 +58,7 @@ export async function runProjectSetup(projectRoot, packageVersion, args) {
|
|
|
56
58
|
const forceFromWelcome = await phaseWelcome(state);
|
|
57
59
|
const useForce = force || forceFromWelcome;
|
|
58
60
|
await runProjectPhases(state, useForce);
|
|
59
|
-
p.outro(`Run ${pc.cyan('orbital
|
|
61
|
+
p.outro(`Run ${pc.cyan('orbital')} to launch the dashboard.`);
|
|
60
62
|
}
|
|
61
63
|
// ─── Shared project phases (used by both flows) ────────────────
|
|
62
64
|
/**
|
|
@@ -81,7 +83,7 @@ async function runProjectPhases(state, useForce) {
|
|
|
81
83
|
clientPort: state.clientPort,
|
|
82
84
|
commands: state.selectedCommands,
|
|
83
85
|
});
|
|
84
|
-
registerProject(state.projectRoot, state.projectName);
|
|
86
|
+
registerProject(state.projectRoot, { name: state.projectName });
|
|
85
87
|
stampTemplateVersion(state.projectRoot, state.packageVersion);
|
|
86
88
|
s.stop('Project ready.');
|
|
87
89
|
}
|
|
@@ -101,43 +103,224 @@ async function runProjectSetupInline(projectRoot, packageVersion) {
|
|
|
101
103
|
// Skip welcome gate for inline — this is a fresh project being linked
|
|
102
104
|
await runProjectPhases(state, false);
|
|
103
105
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
async function checkForUpdate(currentVersion, cache) {
|
|
107
|
+
// Use cache if checked within 24 hours
|
|
108
|
+
if (cache.lastUpdateCheck && cache.latestVersion) {
|
|
109
|
+
const age = Date.now() - new Date(cache.lastUpdateCheck).getTime();
|
|
110
|
+
if (age < 24 * 60 * 60 * 1000) {
|
|
111
|
+
const isOutdated = cache.latestVersion !== currentVersion;
|
|
112
|
+
return {
|
|
113
|
+
info: { current: currentVersion, latest: cache.latestVersion, isOutdated },
|
|
114
|
+
cacheChanged: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Fetch from npm registry
|
|
108
119
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
const res = await fetch('https://registry.npmjs.org/orbital-command/latest', {
|
|
121
|
+
signal: AbortSignal.timeout(3000),
|
|
122
|
+
});
|
|
123
|
+
const data = await res.json();
|
|
124
|
+
const latest = data.version;
|
|
125
|
+
return {
|
|
126
|
+
info: { current: currentVersion, latest, isOutdated: latest !== currentVersion },
|
|
127
|
+
cacheChanged: true,
|
|
128
|
+
};
|
|
112
129
|
}
|
|
113
130
|
catch {
|
|
114
|
-
|
|
131
|
+
return { info: null, cacheChanged: false };
|
|
115
132
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Context-aware hub menu — the main entry point for `orbital` (no args).
|
|
136
|
+
* Checks for updates, offers template sync, shows iTerm2 recommendation, then menu.
|
|
137
|
+
*/
|
|
138
|
+
export async function runHub(opts) {
|
|
139
|
+
const result = { action: 'launch' };
|
|
140
|
+
p.intro(`${pc.bgCyan(pc.black(' Orbital Command '))} ${pc.dim(`v${opts.packageVersion}`)}`);
|
|
141
|
+
// ── Update check ──
|
|
142
|
+
const updateCheck = await checkForUpdate(opts.packageVersion, {
|
|
143
|
+
lastUpdateCheck: opts.lastUpdateCheck,
|
|
144
|
+
latestVersion: opts.latestVersion,
|
|
145
|
+
});
|
|
146
|
+
if (updateCheck.cacheChanged) {
|
|
147
|
+
result.updateCache = {
|
|
148
|
+
lastUpdateCheck: new Date().toISOString(),
|
|
149
|
+
latestVersion: updateCheck.info?.latest,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (updateCheck.info?.isOutdated) {
|
|
153
|
+
p.log.info(`Update available: ${pc.dim(`v${updateCheck.info.current}`)} → ${pc.cyan(`v${updateCheck.info.latest}`)}`);
|
|
154
|
+
const updateChoice = await p.select({
|
|
155
|
+
message: 'Update Orbital Command now?',
|
|
156
|
+
options: [
|
|
157
|
+
{ value: 'update', label: 'Yes, update' },
|
|
158
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
if (!p.isCancel(updateChoice) && updateChoice === 'update') {
|
|
162
|
+
const s = p.spinner();
|
|
163
|
+
s.start('Updating Orbital Command...');
|
|
164
|
+
try {
|
|
165
|
+
execFileSync('npm', ['update', '-g', 'orbital-command'], { stdio: 'pipe', timeout: 60000 });
|
|
166
|
+
s.stop(`Updated to v${updateCheck.info.latest}!`);
|
|
167
|
+
p.outro(`Run ${pc.cyan('orbital')} again to use the new version.`);
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
s.stop('Update failed.');
|
|
172
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
+
if (msg.includes('EACCES') || msg.includes('permission denied')) {
|
|
174
|
+
p.log.error('Permission denied. Try running with sudo or ensure npm is installed via nvm.');
|
|
175
|
+
}
|
|
176
|
+
else if (msg.includes('ETIMEDOUT') || msg.includes('timeout')) {
|
|
177
|
+
p.log.error('Update timed out. Check your network connection and try again.');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
p.log.error(msg);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ── Template staleness check ──
|
|
186
|
+
if (opts.projectPaths.length > 0) {
|
|
187
|
+
const mod = await import('../manifest.js');
|
|
188
|
+
const initMod = await import('../init.js');
|
|
189
|
+
const outdatedProjects = [];
|
|
190
|
+
for (const proj of opts.projectPaths) {
|
|
191
|
+
if (!fs.existsSync(proj.path)) {
|
|
192
|
+
p.log.warn(`${proj.name}: project path not found (${proj.path})`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const manifest = mod.loadManifest(proj.path);
|
|
196
|
+
if (!manifest)
|
|
197
|
+
continue;
|
|
198
|
+
const claudeDir = path.join(proj.path, '.claude');
|
|
199
|
+
mod.refreshFileStatuses(manifest, claudeDir);
|
|
200
|
+
const summary = mod.summarizeManifest(manifest);
|
|
201
|
+
const parts = Object.entries(summary.byType)
|
|
202
|
+
.filter(([, counts]) => counts.outdated > 0)
|
|
203
|
+
.map(([type, counts]) => `${counts.outdated} ${type}`);
|
|
204
|
+
if (parts.length > 0) {
|
|
205
|
+
outdatedProjects.push({ name: proj.name, path: proj.path, details: parts });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (outdatedProjects.length > 0) {
|
|
209
|
+
const lines = outdatedProjects.map(proj => ` ${pc.cyan(proj.name.padEnd(16))} ${proj.details.join(', ')} outdated`);
|
|
210
|
+
const count = outdatedProjects.length;
|
|
211
|
+
p.note(lines.join('\n'), `${count} project${count > 1 ? 's have' : ' has'} outdated templates`);
|
|
212
|
+
const syncChoice = await p.select({
|
|
213
|
+
message: 'Update project templates now?',
|
|
214
|
+
options: [
|
|
215
|
+
{ value: 'update', label: 'Yes, update all safe files', hint: 'skips modified and pinned' },
|
|
216
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
if (!p.isCancel(syncChoice) && syncChoice === 'update') {
|
|
220
|
+
for (const proj of outdatedProjects) {
|
|
221
|
+
const s = p.spinner();
|
|
222
|
+
s.start(`Updating ${proj.name}...`);
|
|
223
|
+
try {
|
|
224
|
+
initMod.runUpdate(proj.path, { dryRun: false });
|
|
225
|
+
s.stop(`${proj.name} updated.`);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
s.stop(`${proj.name} failed.`);
|
|
229
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ── iTerm2 recommendation (macOS only, one-time) ──
|
|
236
|
+
if (opts.isMac && !opts.itermPromptShown && !isITerm2Available()) {
|
|
237
|
+
p.note(`Sprint dispatch, batch orchestration, and session management\n` +
|
|
238
|
+
`use iTerm2 tabs to run parallel Claude Code sessions.\n` +
|
|
239
|
+
`Without it, sessions fall back to basic subprocess mode.`, 'iTerm2 Recommended');
|
|
240
|
+
const itermChoice = await p.select({
|
|
241
|
+
message: 'Install iTerm2?',
|
|
242
|
+
options: [
|
|
243
|
+
{ value: 'install', label: 'Open download page', hint: 'https://iterm2.com' },
|
|
244
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
result.setItermPromptShown = true;
|
|
248
|
+
if (!p.isCancel(itermChoice) && itermChoice === 'install') {
|
|
249
|
+
spawn('open', ['https://iterm2.com'], { detached: true, stdio: 'ignore' }).unref();
|
|
250
|
+
p.log.info('Waiting for iTerm2 to install... (press any key to skip)');
|
|
251
|
+
await new Promise((resolve) => {
|
|
252
|
+
let done = false;
|
|
253
|
+
const cleanup = () => {
|
|
254
|
+
if (done)
|
|
255
|
+
return;
|
|
256
|
+
done = true;
|
|
257
|
+
process.stdin.setRawMode?.(false);
|
|
258
|
+
process.stdin.removeListener('data', onKey);
|
|
259
|
+
process.stdin.pause();
|
|
260
|
+
clearInterval(timer);
|
|
261
|
+
resolve();
|
|
262
|
+
};
|
|
263
|
+
const onKey = () => { cleanup(); };
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
const MAX_WAIT = 10 * 60 * 1000; // 10 minutes
|
|
266
|
+
const timer = setInterval(() => {
|
|
267
|
+
if (isITerm2Available()) {
|
|
268
|
+
p.log.success('iTerm2 detected!');
|
|
269
|
+
cleanup();
|
|
270
|
+
}
|
|
271
|
+
else if (Date.now() - startTime > MAX_WAIT) {
|
|
272
|
+
cleanup();
|
|
273
|
+
}
|
|
274
|
+
}, 3000);
|
|
275
|
+
process.stdin.setRawMode?.(true);
|
|
276
|
+
process.stdin.resume();
|
|
277
|
+
process.stdin.on('data', onKey);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ── Build menu options based on project state ──
|
|
282
|
+
const projectHint = opts.projectNames.length > 0
|
|
283
|
+
? pc.dim(` (${opts.projectNames.join(', ')})`)
|
|
284
|
+
: '';
|
|
285
|
+
const options = [];
|
|
286
|
+
if (opts.isProjectInitialized) {
|
|
287
|
+
options.push({ value: 'launch', label: `Launch dashboard${projectHint}` }, { value: 'config', label: 'Config', hint: 'modify project settings' }, { value: 'doctor', label: 'Doctor', hint: 'health check & diagnostics' }, { value: 'update', label: 'Update templates', hint: 'sync to latest' }, { value: 'status', label: 'Status', hint: 'template sync status' }, { value: 'reset', label: 'Reset to defaults', hint: 'force-reset all templates' });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
options.push({ value: 'init', label: 'Initialize this project' }, { value: 'launch', label: `Launch dashboard${projectHint}` });
|
|
291
|
+
}
|
|
292
|
+
const action = await p.select({
|
|
293
|
+
message: 'What would you like to do?',
|
|
294
|
+
options,
|
|
139
295
|
});
|
|
140
|
-
|
|
296
|
+
if (p.isCancel(action)) {
|
|
297
|
+
p.cancel('Cancelled.');
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
300
|
+
// ── Double-confirm for destructive reset ──
|
|
301
|
+
if (action === 'reset') {
|
|
302
|
+
p.note('This will overwrite ALL hooks, skills, agents, and workflow config\n' +
|
|
303
|
+
'with the default templates. Modified and pinned files will be replaced.\n' +
|
|
304
|
+
'Your scopes, database, and orbital.config.json are preserved.', 'Warning');
|
|
305
|
+
const confirmReset = await p.confirm({
|
|
306
|
+
message: 'Are you sure you want to reset all templates?',
|
|
307
|
+
initialValue: false,
|
|
308
|
+
});
|
|
309
|
+
if (p.isCancel(confirmReset) || !confirmReset) {
|
|
310
|
+
p.cancel('Reset cancelled.');
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
const doubleConfirm = await p.confirm({
|
|
314
|
+
message: 'This cannot be undone. Continue?',
|
|
315
|
+
initialValue: false,
|
|
316
|
+
});
|
|
317
|
+
if (p.isCancel(doubleConfirm) || !doubleConfirm) {
|
|
318
|
+
p.cancel('Reset cancelled.');
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
result.action = action;
|
|
323
|
+
return result;
|
|
141
324
|
}
|
|
142
325
|
// ─── Template Version Stamping ─────────────────────────────────
|
|
143
326
|
function stampTemplateVersion(projectRoot, packageVersion) {
|
|
@@ -148,7 +331,9 @@ function stampTemplateVersion(projectRoot, packageVersion) {
|
|
|
148
331
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
149
332
|
if (config.templateVersion !== packageVersion) {
|
|
150
333
|
config.templateVersion = packageVersion;
|
|
151
|
-
|
|
334
|
+
const tmp = configPath + `.tmp.${process.pid}`;
|
|
335
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
336
|
+
fs.renameSync(tmp, configPath);
|
|
152
337
|
}
|
|
153
338
|
}
|
|
154
339
|
catch { /* ignore malformed config */ }
|
|
@@ -14,8 +14,7 @@ export async function phaseWelcome(state) {
|
|
|
14
14
|
const action = await p.select({
|
|
15
15
|
message: 'What would you like to do?',
|
|
16
16
|
options: [
|
|
17
|
-
{ value: '
|
|
18
|
-
{ value: 'configure', label: 'Open config editor', hint: 'modify settings without resetting' },
|
|
17
|
+
{ value: 'configure', label: 'Open config editor', hint: 'modify settings' },
|
|
19
18
|
{ value: 'cancel', label: 'Cancel' },
|
|
20
19
|
],
|
|
21
20
|
});
|
|
@@ -27,8 +26,6 @@ export async function phaseWelcome(state) {
|
|
|
27
26
|
await runConfigEditor(state.projectRoot, state.packageVersion, []);
|
|
28
27
|
process.exit(0);
|
|
29
28
|
}
|
|
30
|
-
// Re-init — continue through the full project setup with force
|
|
31
|
-
return true;
|
|
32
29
|
}
|
|
33
30
|
// Not initialized — continue normally
|
|
34
31
|
return false;
|
|
@@ -15,14 +15,13 @@ Everything is driven by config files and hooks inside your project's
|
|
|
15
15
|
${pc.cyan('.claude/')} directory — no database or external service required.`,
|
|
16
16
|
setupComplete: `${pc.bold('Setup complete.')}
|
|
17
17
|
|
|
18
|
-
${pc.cyan('orbital
|
|
19
|
-
${pc.cyan('orbital launch --open')} Start the dashboard
|
|
18
|
+
${pc.cyan('orbital')} Add a project or launch the dashboard
|
|
20
19
|
${pc.cyan('orbital doctor')} Health check & version info`,
|
|
21
|
-
addProject: `You can add projects now or later
|
|
20
|
+
addProject: `You can add projects now or later by running ${pc.cyan('orbital')} in a project directory.
|
|
22
21
|
Each project gets its own workflow, scopes, and quality gates.`,
|
|
23
22
|
// Phase 2: Project setup (runs per-project)
|
|
24
23
|
reconfigure: `This project is already initialized with Orbital Command.
|
|
25
|
-
You can reconfigure settings or
|
|
24
|
+
You can reconfigure settings or select ${pc.cyan('Reset to defaults')} from the hub menu.`,
|
|
26
25
|
projectConfig: `${pc.bold('Project Config')} ${pc.dim('(.claude/orbital.config.json)')}
|
|
27
26
|
|
|
28
27
|
Each project gets its own config inside ${pc.cyan('.claude/')}. The project
|
|
@@ -45,16 +44,16 @@ ${pc.cyan('Workflow')} Your selected preset defining lists and transitions
|
|
|
45
44
|
${pc.cyan('Quality Gates')} Automated checks (lint, typecheck, tests) before transitions`,
|
|
46
45
|
nextSteps: `${pc.bold('Next Steps')}
|
|
47
46
|
|
|
48
|
-
1. ${pc.cyan('orbital
|
|
47
|
+
1. Run ${pc.cyan('orbital')} and select ${pc.bold('Launch dashboard')}
|
|
49
48
|
2. Create a scope from the board or use ${pc.cyan('/scope-create')}
|
|
50
49
|
3. Use ${pc.cyan('/scope-implement')} to start working on a scope
|
|
51
50
|
|
|
52
51
|
${pc.bold('Useful Commands')}
|
|
53
52
|
|
|
53
|
+
${pc.cyan('orbital')} Hub menu — launch, config, doctor, etc.
|
|
54
54
|
${pc.cyan('orbital status')} See template sync status
|
|
55
55
|
${pc.cyan('orbital config')} Modify project settings
|
|
56
|
-
${pc.cyan('orbital update')} Sync to latest templates
|
|
57
|
-
${pc.cyan('orbital doctor')} Health check & version info`,
|
|
56
|
+
${pc.cyan('orbital update')} Sync to latest templates`,
|
|
58
57
|
};
|
|
59
58
|
// ─── Formatting Helpers ─────────────────────────────────────────
|
|
60
59
|
export function formatDetectedCommands(commands) {
|