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.
Files changed (160) hide show
  1. package/README.md +67 -42
  2. package/bin/commands/config.js +19 -0
  3. package/bin/commands/events.js +40 -0
  4. package/bin/commands/launch.js +126 -0
  5. package/bin/commands/manifest.js +283 -0
  6. package/bin/commands/registry.js +104 -0
  7. package/bin/commands/update.js +24 -0
  8. package/bin/lib/helpers.js +229 -0
  9. package/bin/orbital.js +95 -870
  10. package/dist/assets/Landing-CfQdHR0N.js +11 -0
  11. package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
  12. package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
  13. package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
  14. package/dist/assets/Settings-DLcZwbCT.js +12 -0
  15. package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
  16. package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
  17. package/dist/assets/{arrow-down-CPy85_J6.js → arrow-down-DVPp6_qp.js} +1 -1
  18. package/dist/assets/bot-NFaJBDn_.js +6 -0
  19. package/dist/assets/{charts-DbDg0Psc.js → charts-LGLb8hyU.js} +1 -1
  20. package/dist/assets/{circle-x-Cwz6ZQDV.js → circle-x-IsFCkBZu.js} +1 -1
  21. package/dist/assets/{file-text-C46Xr65c.js → file-text-J1cebZXF.js} +1 -1
  22. package/dist/assets/{globe-Cn2yNZUD.js → globe-WzeyHsUc.js} +1 -1
  23. package/dist/assets/index-BdJ57EhC.css +1 -0
  24. package/dist/assets/index-o4ScMAuR.js +349 -0
  25. package/dist/assets/{key-OPaNTWJ5.js → key-CKR8JJSj.js} +1 -1
  26. package/dist/assets/{minus-GMsbpKym.js → minus-CHBsJyjp.js} +1 -1
  27. package/dist/assets/radio-xqZaR-Uk.js +6 -0
  28. package/dist/assets/rocket-D_xvvNG6.js +6 -0
  29. package/dist/assets/{shield-DwAFkDYI.js → shield-TdB1yv_a.js} +1 -1
  30. package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
  31. package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
  32. package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
  33. package/dist/assets/zap-C9wqYMpl.js +6 -0
  34. package/dist/index.html +3 -3
  35. package/dist/server/server/__tests__/data-routes.test.js +2 -0
  36. package/dist/server/server/__tests__/scope-routes.test.js +1 -0
  37. package/dist/server/server/config-migrator.js +0 -3
  38. package/dist/server/server/config.js +35 -6
  39. package/dist/server/server/database.js +0 -22
  40. package/dist/server/server/index.js +26 -814
  41. package/dist/server/server/init.js +32 -399
  42. package/dist/server/server/launch.js +1 -1
  43. package/dist/server/server/parsers/event-parser.js +4 -1
  44. package/dist/server/server/project-context.js +19 -9
  45. package/dist/server/server/project-manager.js +6 -6
  46. package/dist/server/server/routes/aggregate-routes.js +871 -0
  47. package/dist/server/server/routes/config-routes.js +41 -88
  48. package/dist/server/server/routes/data-routes.js +5 -15
  49. package/dist/server/server/routes/dispatch-routes.js +24 -8
  50. package/dist/server/server/routes/manifest-routes.js +1 -1
  51. package/dist/server/server/routes/scope-routes.js +10 -7
  52. package/dist/server/server/schema.js +1 -0
  53. package/dist/server/server/services/batch-orchestrator.js +17 -3
  54. package/dist/server/server/services/config-service.js +10 -1
  55. package/dist/server/server/services/scope-service.js +7 -7
  56. package/dist/server/server/services/sprint-orchestrator.js +24 -11
  57. package/dist/server/server/services/sprint-service.js +2 -2
  58. package/dist/server/server/uninstall.js +195 -0
  59. package/dist/server/server/update.js +212 -0
  60. package/dist/server/server/utils/dispatch-utils.js +8 -6
  61. package/dist/server/server/utils/flag-builder.js +54 -0
  62. package/dist/server/server/utils/json-fields.js +14 -0
  63. package/dist/server/server/utils/json-fields.test.js +73 -0
  64. package/dist/server/server/utils/route-helpers.js +37 -0
  65. package/dist/server/server/utils/route-helpers.test.js +115 -0
  66. package/dist/server/server/watchers/event-watcher.js +28 -13
  67. package/dist/server/server/wizard/config-editor.js +4 -4
  68. package/dist/server/server/wizard/doctor.js +2 -2
  69. package/dist/server/server/wizard/index.js +224 -39
  70. package/dist/server/server/wizard/phases/welcome.js +1 -4
  71. package/dist/server/server/wizard/ui.js +6 -7
  72. package/dist/server/shared/api-types.js +80 -1
  73. package/dist/server/shared/workflow-engine.js +1 -1
  74. package/package.json +20 -20
  75. package/schemas/orbital.config.schema.json +1 -19
  76. package/scripts/postinstall.js +6 -42
  77. package/scripts/release.sh +53 -0
  78. package/server/__tests__/data-routes.test.ts +2 -0
  79. package/server/__tests__/scope-routes.test.ts +1 -0
  80. package/server/config-migrator.ts +0 -3
  81. package/server/config.ts +39 -11
  82. package/server/database.ts +0 -26
  83. package/server/global-config.ts +4 -0
  84. package/server/index.ts +29 -894
  85. package/server/init.ts +32 -443
  86. package/server/launch.ts +1 -1
  87. package/server/parsers/event-parser.ts +4 -1
  88. package/server/project-context.ts +26 -10
  89. package/server/project-manager.ts +5 -6
  90. package/server/routes/aggregate-routes.ts +968 -0
  91. package/server/routes/config-routes.ts +41 -81
  92. package/server/routes/data-routes.ts +7 -16
  93. package/server/routes/dispatch-routes.ts +29 -8
  94. package/server/routes/manifest-routes.ts +1 -1
  95. package/server/routes/scope-routes.ts +12 -7
  96. package/server/schema.ts +1 -0
  97. package/server/services/batch-orchestrator.ts +18 -2
  98. package/server/services/config-service.ts +10 -1
  99. package/server/services/scope-service.ts +6 -6
  100. package/server/services/sprint-orchestrator.ts +24 -9
  101. package/server/services/sprint-service.ts +2 -2
  102. package/server/uninstall.ts +214 -0
  103. package/server/update.ts +263 -0
  104. package/server/utils/dispatch-utils.ts +8 -6
  105. package/server/utils/flag-builder.ts +56 -0
  106. package/server/utils/json-fields.test.ts +83 -0
  107. package/server/utils/json-fields.ts +14 -0
  108. package/server/utils/route-helpers.test.ts +144 -0
  109. package/server/utils/route-helpers.ts +38 -0
  110. package/server/watchers/event-watcher.ts +24 -12
  111. package/server/wizard/config-editor.ts +4 -4
  112. package/server/wizard/doctor.ts +2 -2
  113. package/server/wizard/index.ts +291 -40
  114. package/server/wizard/phases/welcome.ts +1 -5
  115. package/server/wizard/ui.ts +6 -7
  116. package/shared/api-types.ts +106 -0
  117. package/shared/workflow-engine.ts +1 -1
  118. package/templates/agents/QUICK-REFERENCE.md +1 -0
  119. package/templates/agents/README.md +1 -0
  120. package/templates/agents/SKILL-TRIGGERS.md +11 -0
  121. package/templates/agents/green-team/deep-dive.md +361 -0
  122. package/templates/hooks/end-session.sh +1 -0
  123. package/templates/hooks/init-session.sh +1 -0
  124. package/templates/hooks/scope-commit-logger.sh +2 -2
  125. package/templates/hooks/scope-create-gate.sh +2 -4
  126. package/templates/hooks/scope-gate.sh +4 -6
  127. package/templates/hooks/scope-helpers.sh +10 -1
  128. package/templates/hooks/scope-lifecycle-gate.sh +14 -5
  129. package/templates/hooks/scope-prepare.sh +1 -1
  130. package/templates/hooks/scope-transition.sh +14 -6
  131. package/templates/hooks/time-tracker.sh +2 -5
  132. package/templates/orbital.config.json +1 -4
  133. package/templates/presets/development.json +4 -4
  134. package/templates/presets/gitflow.json +7 -0
  135. package/templates/prompts/README.md +23 -0
  136. package/templates/prompts/deep-dive-audit.md +94 -0
  137. package/templates/quick/rules.md +56 -5
  138. package/templates/skills/git-commit/SKILL.md +21 -6
  139. package/templates/skills/git-dev/SKILL.md +8 -4
  140. package/templates/skills/git-main/SKILL.md +8 -4
  141. package/templates/skills/git-production/SKILL.md +6 -3
  142. package/templates/skills/git-staging/SKILL.md +6 -3
  143. package/templates/skills/scope-fix-review/SKILL.md +8 -4
  144. package/templates/skills/scope-implement/SKILL.md +13 -5
  145. package/templates/skills/scope-post-review/SKILL.md +16 -4
  146. package/templates/skills/scope-pre-review/SKILL.md +6 -2
  147. package/dist/assets/PrimitivesConfig-CrmQXYh4.js +0 -32
  148. package/dist/assets/QualityGates-BbasOsF3.js +0 -21
  149. package/dist/assets/SessionTimeline-CGeJsVvy.js +0 -1
  150. package/dist/assets/Settings-oiM496mc.js +0 -12
  151. package/dist/assets/SourceControl-B1fP2nJL.js +0 -41
  152. package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +0 -74
  153. package/dist/assets/formatDistanceToNow-BMqsSP44.js +0 -1
  154. package/dist/assets/index-Aj4sV8Al.css +0 -1
  155. package/dist/assets/index-Bc9dK3MW.js +0 -354
  156. package/dist/assets/useWorkflowEditor-BJkTX_NR.js +0 -16
  157. package/dist/assets/zap-DfbUoOty.js +0 -11
  158. package/dist/server/server/services/telemetry-service.js +0 -143
  159. package/server/services/telemetry-service.ts +0 -195
  160. /package/{shared/default-workflow.json → templates/presets/default.json} +0 -0
package/README.md CHANGED
@@ -24,52 +24,32 @@ Think of it as the control tower that turns a collection of AI-assisted coding s
24
24
 
25
25
  - **Node.js >= 18**
26
26
  - **Claude Code** installed and available as `claude` on your PATH
27
+ - **iTerm2** (macOS, recommended) — sprint dispatch and batch orchestration use iTerm2 tabs to run parallel Claude Code sessions. Without it, sessions fall back to basic subprocess mode. The setup wizard will prompt you to install it.
27
28
  - **C++ compiler** for the `better-sqlite3` native module:
28
29
  - macOS: Xcode Command Line Tools (`xcode-select --install`)
29
30
  - Linux: `build-essential` (`apt install build-essential`)
30
31
 
31
- ## Installation
32
-
33
- **Global install (recommended for regular use):**
34
-
35
- ```bash
36
- npm install -g github:SakaraLabs/orbital-command
37
- ```
38
-
39
- This registers the `orbital` command globally. Then in any project:
40
-
41
- ```bash
42
- orbital init
43
- orbital dev
44
- ```
45
-
46
- **Without global install:**
47
-
48
- ```bash
49
- npx github:SakaraLabs/orbital-command init
50
- npx github:SakaraLabs/orbital-command dev
51
- ```
52
-
53
32
  ## Quick Start
54
33
 
55
34
  ```bash
56
- # 1. Navigate to your project
35
+ npm install orbital-command
57
36
  cd my-project
37
+ orbital
38
+ ```
58
39
 
59
- # 2. Scaffold Orbital Command (hooks, skills, agents, config)
60
- orbital init
40
+ That's it. The `orbital` command detects your context and guides you through everything:
61
41
 
62
- # 3. Launch the dashboard
63
- orbital dev
64
- ```
42
+ 1. **First run** — setup wizard configures Orbital globally
43
+ 2. **New project** — project setup wizard scaffolds hooks, skills, agents, and workflow config
44
+ 3. **Existing project** — hub menu lets you launch the dashboard, edit config, run diagnostics, or update templates
65
45
 
66
- Open [http://localhost:4445](http://localhost:4445) in your browser. The API server runs on port 4444.
46
+ On macOS, the wizard will recommend installing [iTerm2](https://iterm2.com) for the full dispatch experience it polls for the install automatically so you can continue once it's ready.
67
47
 
68
- The `init` command is non-destructive — it skips files that already exist. Pass `--force` to overwrite everything with the latest templates.
48
+ Open [http://localhost:4444](http://localhost:4444) after launching.
69
49
 
70
50
  ## What Gets Installed
71
51
 
72
- After `orbital init`, your project receives:
52
+ After setup, your project receives:
73
53
 
74
54
  ```
75
55
  .claude/
@@ -111,15 +91,26 @@ scopes/
111
91
 
112
92
  ## CLI Reference
113
93
 
94
+ The bare `orbital` command is the primary entry point — it detects context and shows the right options. All subcommands below are also available directly for scripting or when you know what you want.
95
+
114
96
  | Command | Description |
115
97
  |---------|-------------|
116
- | `orbital init` | Scaffold hooks, skills, agents, and config into the current project |
117
- | `orbital init --force` | Re-scaffold, overwriting all existing files with latest templates |
118
- | `orbital dev` | Start the API server and Vite dev server concurrently |
119
- | `orbital build` | Production build of the dashboard frontend |
98
+ | `orbital` | Context-aware hub menu (setup, init, launch, config, etc.) |
99
+ | `orbital` | Context-aware hub menu (setup, launch, config, doctor, etc.) |
100
+ | `orbital config` | Modify project settings interactively |
101
+ | `orbital doctor` | Health check and version diagnostics |
102
+ | `orbital update` | Sync templates and apply migrations |
103
+ | `orbital status` | Show template sync status |
120
104
  | `orbital emit <TYPE> [JSON]` | Emit an event to the file-based event bus |
121
- | `orbital update` | Re-copy hooks, skills, agents, and presets (overwrites). Pass `--include-examples` to include domain-specific example agents |
122
- | `orbital uninstall` | Remove all Orbital artifacts from the project. Preserves `scopes/` and event history |
105
+ | `orbital validate` | Check cross-references and consistency |
106
+ | `orbital register [path]` | Register a project with the dashboard |
107
+ | `orbital unregister <id>` | Remove a project from the dashboard |
108
+ | `orbital projects` | List all registered projects |
109
+ | `orbital pin <path>` | Lock a file from template updates |
110
+ | `orbital unpin <path>` | Unlock a pinned file |
111
+ | `orbital diff <path>` | Show diff between template and local file |
112
+ | `orbital reset <path>` | Restore a file from the current template |
113
+ | `orbital uninstall` | Remove all Orbital artifacts from the project |
123
114
 
124
115
  ## Workflow Presets
125
116
 
@@ -351,7 +342,7 @@ When you dispatch a scope from the dashboard, Orbital:
351
342
  No. Orbital Command is purpose-built for Claude Code. The hooks, skills, agents, and session tracking all depend on Claude Code's lifecycle events and CLI.
352
343
 
353
344
  ### Do I need to keep the dashboard running?
354
- No. The file-based event bus means hooks write events as JSON files regardless of whether the server is running. Events queue up and get ingested when you next run `orbital dev`.
345
+ No. The file-based event bus means hooks write events as JSON files regardless of whether the server is running. Events queue up and get ingested when you next launch the dashboard.
355
346
 
356
347
  ### How do I customize the workflow columns?
357
348
  Open the Workflow Visualizer (`/workflow` in the dashboard). You can add/remove columns, create transitions, attach hooks, and set confirmation levels. Or edit `.claude/config/workflow.json` directly. Changes take effect immediately.
@@ -370,7 +361,7 @@ Sprints group multiple scopes for batch execution. The orchestrator resolves dep
370
361
  ### How do I reset everything?
371
362
  ```bash
372
363
  orbital uninstall # Remove all Orbital artifacts
373
- orbital init --force # Re-scaffold from scratch
364
+ # Then run `orbital` and select "Reset to defaults"
374
365
  ```
375
366
 
376
367
  This preserves your `scopes/` directory and event history.
@@ -382,14 +373,48 @@ Yes. Set `terminal.adapter` in your config:
382
373
  - `"subprocess"` — Generic subprocess spawning (works everywhere)
383
374
  - `"none"` — Disable terminal dispatch
384
375
 
376
+ ## Development
377
+
378
+ ```bash
379
+ # Install dependencies (includes all build/frontend packages as devDependencies)
380
+ npm install
381
+
382
+ # Start dev server with hot-reload
383
+ npm run dev:local
384
+
385
+ # Run the full validation pipeline (typecheck → test → build → build:server)
386
+ npm run validate
387
+ ```
388
+
389
+ | Script | Purpose |
390
+ |--------|---------|
391
+ | `npm run dev:local` | Express API + Vite dev server with hot-reload |
392
+ | `npm run dev:server` | Express API only (tsx watch) |
393
+ | `npm run dev:client` | Vite dev server only |
394
+ | `npm run typecheck` | Type check client + server tsconfigs |
395
+ | `npm run test` | Run all tests |
396
+ | `npm run build` | Vite production build (frontend) |
397
+ | `npm run build:server` | TypeScript compile server to dist/server |
398
+ | `npm run validate` | Full pipeline: typecheck → test → build → build:server |
399
+
400
+ ### Releasing
401
+
402
+ ```bash
403
+ npm run release # patch bump (0.3.0 → 0.3.1)
404
+ npm run release -- minor # minor bump (0.3.0 → 0.4.0)
405
+ npm run release -- major # major bump (0.3.0 → 1.0.0)
406
+ ```
407
+
408
+ This validates the full pipeline, bumps the version, creates a git tag, and pushes. The tag push triggers the publish workflow which validates again and publishes to npm with provenance.
409
+
385
410
  ## Contributing
386
411
 
412
+ Open an issue first for large changes or new features.
413
+
387
414
  1. Fork the repository and clone locally
388
415
  2. Install dependencies: `npm install`
389
416
  3. Start development: `npm run dev:local`
390
- 4. Typecheck: `npm run typecheck`
391
-
392
- Open an issue first for large changes or new features.
417
+ 4. Validate before submitting: `npm run validate`
393
418
 
394
419
  ## License
395
420
 
@@ -0,0 +1,19 @@
1
+ import {
2
+ detectProjectRoot,
3
+ getPackageVersion,
4
+ loadWizardModule,
5
+ } from '../lib/helpers.js';
6
+
7
+ export async function cmdConfig(args) {
8
+ const { runConfigEditor } = await loadWizardModule();
9
+ const projectRoot = detectProjectRoot();
10
+ const version = getPackageVersion();
11
+ await runConfigEditor(projectRoot, version, args);
12
+ }
13
+
14
+ export async function cmdDoctor() {
15
+ const { runDoctor } = await loadWizardModule();
16
+ const projectRoot = detectProjectRoot();
17
+ const version = getPackageVersion();
18
+ await runDoctor(projectRoot, version);
19
+ }
@@ -0,0 +1,40 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { detectProjectRoot } from '../lib/helpers.js';
5
+
6
+ export function cmdEmit(args) {
7
+ const type = args[0];
8
+ const jsonStr = args.slice(1).join(' ');
9
+
10
+ if (!type) {
11
+ console.error('Usage: orbital emit <TYPE> <JSON>');
12
+ process.exit(1);
13
+ }
14
+
15
+ const projectRoot = detectProjectRoot();
16
+ const eventsDir = path.join(projectRoot, '.claude', 'orbital-events');
17
+ if (!fs.existsSync(eventsDir)) fs.mkdirSync(eventsDir, { recursive: true });
18
+
19
+ let payload;
20
+ try {
21
+ payload = jsonStr ? JSON.parse(jsonStr) : {};
22
+ } catch (err) {
23
+ console.error(`Invalid JSON: ${err.message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const eventId = crypto.randomUUID();
28
+ const event = {
29
+ ...payload,
30
+ id: eventId,
31
+ type,
32
+ timestamp: new Date().toISOString(),
33
+ };
34
+
35
+ const filePath = path.join(eventsDir, `${eventId}.json`);
36
+ fs.writeFileSync(filePath, JSON.stringify(event, null, 2) + '\n', 'utf8');
37
+
38
+ console.log(`Event emitted: ${type} (${eventId})`);
39
+ console.log(` File: ${path.relative(projectRoot, filePath)}`);
40
+ }
@@ -0,0 +1,126 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import {
5
+ PACKAGE_ROOT,
6
+ resolveBin,
7
+ detectProjectRoot,
8
+ loadConfig,
9
+ openBrowser,
10
+ } from '../lib/helpers.js';
11
+
12
+ export function cmdLaunchOrDev(forceViteFlag) {
13
+ const shouldOpen = process.argv.includes('--open');
14
+ const forceVite = forceViteFlag || process.argv.includes('--vite');
15
+ const projectRoot = detectProjectRoot();
16
+ const config = loadConfig(projectRoot);
17
+ const serverPort = config.serverPort || 4444;
18
+ const clientPort = config.clientPort || 4445;
19
+
20
+ // Detect packaged mode: dist/index.html exists -> serve pre-built frontend
21
+ const hasPrebuiltFrontend = fs.existsSync(path.join(PACKAGE_ROOT, 'dist', 'index.html'));
22
+ const useVite = forceVite || !hasPrebuiltFrontend;
23
+
24
+ // Detect compiled server: dist/server/server/launch.js exists -> run with node
25
+ const compiledServer = path.join(PACKAGE_ROOT, 'dist', 'server', 'server', 'launch.js');
26
+ const hasCompiledServer = fs.existsSync(compiledServer);
27
+ const useCompiledServer = hasCompiledServer && !useVite;
28
+
29
+ console.log(`\nOrbital Command — ${useVite ? 'dev' : 'launch'}`);
30
+ console.log(`Project root: ${projectRoot}`);
31
+ if (useVite) {
32
+ console.log(`Server: http://localhost:${serverPort}`);
33
+ console.log(`Client: http://localhost:${clientPort} (Vite dev server)\n`);
34
+ } else {
35
+ console.log(`Dashboard: http://localhost:${serverPort}\n`);
36
+ }
37
+
38
+ const env = {
39
+ ...process.env,
40
+ ORBITAL_LAUNCH_MODE: 'central',
41
+ ORBITAL_AUTO_REGISTER: projectRoot,
42
+ ORBITAL_SERVER_PORT: String(serverPort),
43
+ };
44
+
45
+ let serverProcess;
46
+
47
+ if (useCompiledServer) {
48
+ serverProcess = spawn(process.execPath, [compiledServer],
49
+ { stdio: 'inherit', env, cwd: PACKAGE_ROOT });
50
+ } else {
51
+ const tsxBin = resolveBin('tsx');
52
+ const serverScript = path.join(PACKAGE_ROOT, 'server', 'launch.ts');
53
+ if (tsxBin) {
54
+ serverProcess = spawn(tsxBin, ['watch', serverScript],
55
+ { stdio: 'inherit', env, cwd: PACKAGE_ROOT });
56
+ } else {
57
+ console.error('Error: tsx not found. Install it with: npm install tsx');
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ let viteProcess = null;
63
+
64
+ if (useVite) {
65
+ const viteBin = resolveBin('vite');
66
+ if (!viteBin) {
67
+ console.error('Error: vite not found. Install it with: npm install vite');
68
+ process.exit(1);
69
+ }
70
+ viteProcess = spawn(viteBin, ['--config', path.join(PACKAGE_ROOT, 'vite.config.ts'), '--port', String(clientPort)],
71
+ { stdio: 'inherit', env, cwd: PACKAGE_ROOT });
72
+ }
73
+
74
+ const dashboardUrl = useVite
75
+ ? `http://localhost:${clientPort}`
76
+ : `http://localhost:${serverPort}`;
77
+
78
+ if (shouldOpen) {
79
+ setTimeout(() => openBrowser(dashboardUrl), 2000);
80
+ }
81
+
82
+ let exiting = false;
83
+
84
+ function cleanup() {
85
+ if (exiting) return;
86
+ exiting = true;
87
+ serverProcess.kill();
88
+ if (viteProcess) viteProcess.kill();
89
+ process.exit(0);
90
+ }
91
+ process.on('SIGINT', cleanup);
92
+ process.on('SIGTERM', cleanup);
93
+
94
+ serverProcess.on('exit', (code) => {
95
+ if (exiting) return;
96
+ exiting = true;
97
+ console.log(`Server exited with code ${code}`);
98
+ if (viteProcess) viteProcess.kill();
99
+ process.exit(code || 0);
100
+ });
101
+ if (viteProcess) {
102
+ viteProcess.on('exit', (code) => {
103
+ if (exiting) return;
104
+ exiting = true;
105
+ console.log(`Vite exited with code ${code}`);
106
+ serverProcess.kill();
107
+ process.exit(code || 0);
108
+ });
109
+ }
110
+ }
111
+
112
+ export function cmdBuild() {
113
+ console.log(`\nOrbital Command — build\n`);
114
+
115
+ const viteBin = resolveBin('vite');
116
+ if (!viteBin) {
117
+ console.error('Error: vite not found. Install it with: npm install vite');
118
+ process.exit(1);
119
+ }
120
+ const buildProcess = spawn(viteBin, ['build', '--config', path.join(PACKAGE_ROOT, 'vite.config.ts')],
121
+ { stdio: 'inherit', cwd: PACKAGE_ROOT });
122
+
123
+ buildProcess.on('exit', (code) => {
124
+ process.exit(code || 0);
125
+ });
126
+ }
@@ -0,0 +1,283 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execFileSync } from 'child_process';
4
+ import {
5
+ PACKAGE_ROOT,
6
+ detectProjectRoot,
7
+ getPackageVersion,
8
+ loadSharedModule,
9
+ } from '../lib/helpers.js';
10
+
11
+ export async function cmdStatus() {
12
+ const projectRoot = detectProjectRoot();
13
+
14
+ const mod = await loadSharedModule();
15
+ const manifest = mod.loadManifest(projectRoot);
16
+
17
+ if (!manifest) {
18
+ console.log('\nNo manifest found. Run `orbital` to set up this project.\n');
19
+ return;
20
+ }
21
+
22
+ const claudeDir = path.join(projectRoot, '.claude');
23
+ mod.refreshFileStatuses(manifest, claudeDir);
24
+
25
+ const summary = mod.summarizeManifest(manifest);
26
+ const packageVersion = getPackageVersion();
27
+ const needsUpdate = manifest.packageVersion !== packageVersion;
28
+
29
+ console.log(`\nOrbital Command v${packageVersion}${needsUpdate ? ` (installed: ${manifest.packageVersion} → needs update)` : ''}\n`);
30
+
31
+ for (const [type, counts] of Object.entries(summary.byType)) {
32
+ const parts = [];
33
+ if (counts.synced) parts.push(`${counts.synced} synced`);
34
+ if (counts.outdated) parts.push(`${counts.outdated} outdated`);
35
+ if (counts.modified) parts.push(`${counts.modified} modified`);
36
+ if (counts.pinned) parts.push(`${counts.pinned} pinned`);
37
+ if (counts.userOwned) parts.push(`${counts.userOwned} user-owned`);
38
+ console.log(` ${type.padEnd(16)} ${parts.join(', ')}`);
39
+ }
40
+
41
+ const outdated = Object.entries(manifest.files)
42
+ .filter(([, r]) => r.status === 'outdated');
43
+ if (outdated.length > 0) {
44
+ console.log('\n Outdated files (safe to update):');
45
+ for (const [file] of outdated) {
46
+ console.log(` ${file}`);
47
+ }
48
+ }
49
+
50
+ const modified = Object.entries(manifest.files)
51
+ .filter(([, r]) => r.status === 'modified');
52
+ if (modified.length > 0) {
53
+ console.log('\n Modified files (user edited):');
54
+ for (const [file] of modified) {
55
+ console.log(` ${file} (run 'orbital diff ${file}')`);
56
+ }
57
+ }
58
+
59
+ const pinned = Object.entries(manifest.files)
60
+ .filter(([, r]) => r.status === 'pinned');
61
+ if (pinned.length > 0) {
62
+ console.log('\n Pinned files:');
63
+ for (const [file, record] of pinned) {
64
+ const reason = record.pinnedReason ? `"${record.pinnedReason}"` : '';
65
+ console.log(` ${file} ${reason}`);
66
+ }
67
+ }
68
+
69
+ console.log();
70
+ }
71
+
72
+ export async function cmdValidate() {
73
+ const projectRoot = detectProjectRoot();
74
+
75
+ const mod = await loadSharedModule();
76
+ const report = mod.validate(projectRoot, getPackageVersion());
77
+ console.log(mod.formatValidationReport(report));
78
+ process.exit(report.errors > 0 ? 1 : 0);
79
+ }
80
+
81
+ export async function cmdPin(args) {
82
+ const projectRoot = detectProjectRoot();
83
+ const filePath = args.find(a => !a.startsWith('--'));
84
+ const reasonIdx = args.indexOf('--reason');
85
+ const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : undefined;
86
+
87
+ if (!filePath) {
88
+ console.error('Usage: orbital pin <relative-path> [--reason "..."]');
89
+ process.exit(1);
90
+ }
91
+
92
+ const mod = await loadSharedModule();
93
+ const manifest = mod.loadManifest(projectRoot);
94
+ if (!manifest) {
95
+ console.error('No manifest found. Run `orbital` first.');
96
+ process.exit(1);
97
+ }
98
+
99
+ const record = manifest.files[filePath];
100
+ if (!record) {
101
+ console.error(`File not tracked: ${filePath}`);
102
+ process.exit(1);
103
+ }
104
+ if (record.origin === 'user') {
105
+ console.error(`Cannot pin user-owned file: ${filePath}`);
106
+ process.exit(1);
107
+ }
108
+
109
+ record.status = 'pinned';
110
+ record.pinnedAt = new Date().toISOString();
111
+ if (reason) record.pinnedReason = reason;
112
+
113
+ mod.saveManifest(projectRoot, manifest);
114
+ console.log(`Pinned: ${filePath}${reason ? ` (${reason})` : ''}`);
115
+ }
116
+
117
+ export async function cmdUnpin(args) {
118
+ const projectRoot = detectProjectRoot();
119
+ const filePath = args[0];
120
+
121
+ if (!filePath) {
122
+ console.error('Usage: orbital unpin <relative-path>');
123
+ process.exit(1);
124
+ }
125
+
126
+ const mod = await loadSharedModule();
127
+ const manifest = mod.loadManifest(projectRoot);
128
+ if (!manifest) {
129
+ console.error('No manifest found. Run `orbital` first.');
130
+ process.exit(1);
131
+ }
132
+
133
+ const record = manifest.files[filePath];
134
+ if (!record || record.status !== 'pinned') {
135
+ console.error(`File is not pinned: ${filePath}`);
136
+ process.exit(1);
137
+ }
138
+
139
+ record.status = 'synced';
140
+ delete record.pinnedAt;
141
+ delete record.pinnedReason;
142
+
143
+ const absPath = path.join(projectRoot, '.claude', filePath);
144
+ if (fs.existsSync(absPath)) {
145
+ const currentHash = mod.hashFile(absPath);
146
+ record.status = mod.computeFileStatus(record, currentHash);
147
+ } else {
148
+ record.status = 'synced';
149
+ }
150
+
151
+ mod.saveManifest(projectRoot, manifest);
152
+ console.log(`Unpinned: ${filePath} (now ${record.status})`);
153
+ }
154
+
155
+ export async function cmdPins() {
156
+ const projectRoot = detectProjectRoot();
157
+
158
+ const mod = await loadSharedModule();
159
+ const manifest = mod.loadManifest(projectRoot);
160
+ if (!manifest) {
161
+ console.error('No manifest found. Run `orbital` first.');
162
+ process.exit(1);
163
+ }
164
+
165
+ const pinned = Object.entries(manifest.files)
166
+ .filter(([, r]) => r.status === 'pinned');
167
+
168
+ if (pinned.length === 0) {
169
+ console.log('No pinned files.');
170
+ return;
171
+ }
172
+
173
+ console.log(`\n Pinned files:\n`);
174
+ for (const [file, record] of pinned) {
175
+ const reason = record.pinnedReason || '(no reason)';
176
+ const date = record.pinnedAt ? new Date(record.pinnedAt).toLocaleDateString() : '';
177
+ console.log(` ${file}`);
178
+ console.log(` Reason: ${reason} Pinned: ${date}`);
179
+ if (record.templateHash !== record.installedHash) {
180
+ console.log(` Template has changed since pin — run 'orbital diff ${file}' to compare`);
181
+ }
182
+ }
183
+ console.log();
184
+ }
185
+
186
+ export async function cmdDiff(args) {
187
+ const projectRoot = detectProjectRoot();
188
+ const filePath = args[0];
189
+
190
+ if (!filePath) {
191
+ console.error('Usage: orbital diff <relative-path>');
192
+ process.exit(1);
193
+ }
194
+
195
+ const mod = await loadSharedModule();
196
+ const manifest = mod.loadManifest(projectRoot);
197
+ if (!manifest) {
198
+ console.error('No manifest found. Run `orbital` first.');
199
+ process.exit(1);
200
+ }
201
+
202
+ const record = manifest.files[filePath];
203
+ if (!record || record.origin !== 'template') {
204
+ console.error(`Not a template file: ${filePath}`);
205
+ process.exit(1);
206
+ }
207
+
208
+ let templateRelPath = filePath;
209
+ if (filePath.startsWith('config/workflows/')) {
210
+ templateRelPath = filePath.replace('config/workflows/', 'presets/');
211
+ }
212
+
213
+ const templatePath = path.join(PACKAGE_ROOT, 'templates', templateRelPath);
214
+ const localPath = path.join(projectRoot, '.claude', filePath);
215
+
216
+ if (!fs.existsSync(templatePath)) {
217
+ console.error(`Template file not found: ${templateRelPath}`);
218
+ process.exit(1);
219
+ }
220
+ if (!fs.existsSync(localPath)) {
221
+ console.log('Local file does not exist. Template content:');
222
+ console.log(fs.readFileSync(templatePath, 'utf-8'));
223
+ return;
224
+ }
225
+
226
+ try {
227
+ const output = execFileSync(
228
+ 'git', ['diff', '--no-index', '--color', '--', templatePath, localPath],
229
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
230
+ );
231
+ console.log(output);
232
+ } catch (e) {
233
+ if (e.stdout) console.log(e.stdout);
234
+ else console.log('Files differ but git diff is unavailable.');
235
+ }
236
+ }
237
+
238
+ export async function cmdReset(args) {
239
+ const projectRoot = detectProjectRoot();
240
+ const filePath = args[0];
241
+
242
+ if (!filePath) {
243
+ console.error('Usage: orbital reset <relative-path>');
244
+ process.exit(1);
245
+ }
246
+
247
+ const mod = await loadSharedModule();
248
+ const manifest = mod.loadManifest(projectRoot);
249
+ if (!manifest) {
250
+ console.error('No manifest found. Run `orbital` first.');
251
+ process.exit(1);
252
+ }
253
+
254
+ const record = manifest.files[filePath];
255
+ if (!record || record.origin !== 'template') {
256
+ console.error(`Not a template file: ${filePath}`);
257
+ process.exit(1);
258
+ }
259
+
260
+ let templateRelPath = filePath;
261
+ if (filePath.startsWith('config/workflows/')) {
262
+ templateRelPath = filePath.replace('config/workflows/', 'presets/');
263
+ }
264
+
265
+ const templatePath = path.join(PACKAGE_ROOT, 'templates', templateRelPath);
266
+ const localPath = path.join(projectRoot, '.claude', filePath);
267
+
268
+ if (!fs.existsSync(templatePath)) {
269
+ console.error(`Template file not found: ${templateRelPath}`);
270
+ process.exit(1);
271
+ }
272
+
273
+ fs.copyFileSync(templatePath, localPath);
274
+ const newHash = mod.hashFile(localPath);
275
+ record.status = 'synced';
276
+ record.templateHash = newHash;
277
+ record.installedHash = newHash;
278
+ delete record.pinnedAt;
279
+ delete record.pinnedReason;
280
+
281
+ mod.saveManifest(projectRoot, manifest);
282
+ console.log(`Reset: ${filePath} → synced with template`);
283
+ }