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/server/init.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Shared init logic — used by both the CLI (`orbital init`) and
3
- * programmatic callers (e.g. tests).
2
+ * Shared init logic — used by the wizard and
3
+ * programmatic callers (e.g. tests, API routes).
4
4
  */
5
5
 
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
- import { GLOBAL_PRIMITIVES_DIR, ensureOrbitalHome, unregisterProject } from './global-config.js';
9
+ import { GLOBAL_PRIMITIVES_DIR, ensureOrbitalHome } from './global-config.js';
10
10
  import {
11
- loadManifest,
12
11
  saveManifest,
13
12
  createManifest,
14
13
  hashFile,
@@ -17,18 +16,9 @@ import {
17
16
  userFileRecord,
18
17
  isSelfHosting,
19
18
  getSymlinkTarget,
20
- createBackup,
21
- refreshFileStatuses,
22
- reverseRemapPath,
23
- safeBackupFile,
24
- safeCopyTemplate,
25
19
  } from './manifest.js';
26
20
  import type { OrbitalManifest } from './manifest-types.js';
27
- import { needsLegacyMigration, migrateFromLegacy } from './migrate-legacy.js';
28
- import { computeUpdatePlan, loadRenameMap, formatPlan, getFilesToBackup } from './update-planner.js';
29
- import { syncSettingsHooks, removeAllOrbitalHooks, getTemplateChecksum } from './settings-sync.js';
30
- import { migrateConfig } from './config-migrator.js';
31
- import { validate, formatValidationReport } from './validator.js';
21
+ import { getTemplateChecksum } from './settings-sync.js';
32
22
  import { getPackageVersion as _getPackageVersionUtil } from './utils/package-info.js';
33
23
 
34
24
  const __filename = fileURLToPath(import.meta.url);
@@ -252,7 +242,8 @@ function generateManifest(config: Record<string, unknown>): string {
252
242
  lines.push('');
253
243
 
254
244
  lines.push('# ─── Entry point status ───');
255
- lines.push(`WORKFLOW_ENTRY_STATUS="${(config.entryPoint as string) || (lists[0]?.id as string) || 'todo'}"`);
245
+ const entryPointId = (lists.find((l) => l.isEntryPoint)?.id as string) || (lists[0]?.id as string) || 'todo';
246
+ lines.push(`WORKFLOW_ENTRY_STATUS="${entryPointId}"`);
256
247
  lines.push('');
257
248
 
258
249
  const listMap = new Map(lists.map((l) => [l.id, l]));
@@ -279,6 +270,25 @@ function generateManifest(config: Record<string, unknown>): string {
279
270
  lines.push(')');
280
271
  lines.push('');
281
272
 
273
+ lines.push('# ─── Commit session branch patterns (regex) ───');
274
+ lines.push(`WORKFLOW_COMMIT_BRANCHES="${(config.commitBranchPatterns as string) ?? ''}"`);
275
+ lines.push('');
276
+
277
+ lines.push('# ─── Backward-compat direction aliases (alias:from:to:sessionKey) ───');
278
+ lines.push('WORKFLOW_DIRECTION_ALIASES=(');
279
+ for (const edge of (config.edges as Array<Record<string, unknown>>) || []) {
280
+ if (edge.direction !== 'forward' || !edge.dispatchOnly) continue;
281
+ const targetList = listMap.get(edge.to as string);
282
+ if (!targetList) continue;
283
+ const group = targetList.group as string | undefined;
284
+ if (group?.startsWith('deploy')) {
285
+ const sessionKey = (targetList.sessionKey as string) ?? '';
286
+ lines.push(` "to-${edge.to as string}:${edge.from as string}:${edge.to as string}:${sessionKey}"`);
287
+ }
288
+ }
289
+ lines.push(')');
290
+ lines.push('');
291
+
282
292
  lines.push('# ─── Helper functions ──────────────────────────────');
283
293
  lines.push('');
284
294
  lines.push('status_to_dir() {');
@@ -625,12 +635,16 @@ export function seedGlobalPrimitives(): void {
625
635
  }
626
636
  }
627
637
 
628
- export { TEMPLATES_DIR, ensureDir };
638
+ export { TEMPLATES_DIR, ensureDir, cleanEmptyDirs, chmodScripts, listTemplateFiles, writeManifest, generateIndexMd, getPackageVersion };
629
639
 
630
640
  // Re-export manifest utilities for CLI access via loadSharedModule()
631
641
  export { loadManifest, saveManifest, hashFile, buildTemplateInventory, refreshFileStatuses, summarizeManifest } from './manifest.js';
632
642
  export { validate, formatValidationReport } from './validator.js';
633
643
 
644
+ // Re-export update and uninstall from their new homes (backward compat for loadSharedModule)
645
+ export { runUpdate, type UpdateOptions } from './update.js';
646
+ export { runUninstall, type UninstallOptions } from './uninstall.js';
647
+
634
648
  export interface InitOptions {
635
649
  force?: boolean;
636
650
  quiet?: boolean; // suppress console output (used by wizard)
@@ -905,430 +919,5 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
905
919
  log(`\nDone. ${totalCreated} files installed, ${totalSkipped} skipped (use --force to overwrite).`);
906
920
  }
907
921
 
908
- export interface UpdateOptions {
909
- dryRun?: boolean;
910
- force?: boolean;
911
- }
912
-
913
- export function runUpdate(projectRoot: string, options: UpdateOptions = {}): void {
914
- const { dryRun = false } = options;
915
- const claudeDir = path.join(projectRoot, '.claude');
916
- const newVersion = getPackageVersion();
917
-
918
- console.log(`\nOrbital Command — update${dryRun ? ' (dry run)' : ''}`);
919
- console.log(`Project root: ${projectRoot}\n`);
920
-
921
- // 1. Load or create manifest (auto-migrate legacy installs)
922
- let manifest = loadManifest(projectRoot);
923
- if (!manifest) {
924
- if (needsLegacyMigration(projectRoot)) {
925
- console.log(' Migrating from legacy install...');
926
- const result = migrateFromLegacy(projectRoot, TEMPLATES_DIR, newVersion);
927
- console.log(` Migrated ${result.synced} synced, ${result.modified} modified, ${result.userOwned} user-owned files`);
928
- if (result.importedPins > 0) console.log(` Imported ${result.importedPins} pinned files from orbital-sync.json`);
929
- manifest = loadManifest(projectRoot);
930
- }
931
- if (!manifest) {
932
- console.log(' No manifest found. Run `orbital init` first.');
933
- return;
934
- }
935
- }
936
-
937
- const oldVersion = manifest.packageVersion;
938
-
939
- // 1b. Refresh file statuses so outdated vs modified is accurate
940
- refreshFileStatuses(manifest, claudeDir);
941
-
942
- // 2. Compute update plan
943
- const renameMap = loadRenameMap(TEMPLATES_DIR, oldVersion, newVersion);
944
- const plan = computeUpdatePlan({
945
- templatesDir: TEMPLATES_DIR,
946
- claudeDir,
947
- manifest,
948
- newVersion,
949
- renameMap,
950
- });
951
-
952
- // 3. Dry-run mode — print plan and exit
953
- if (dryRun) {
954
- console.log(formatPlan(plan, oldVersion, newVersion));
955
- return;
956
- }
957
-
958
- if (plan.isEmpty && oldVersion === newVersion) {
959
- console.log(' Everything up to date. No changes needed.');
960
- }
961
-
962
- // 4. Create backup of files that will be modified
963
- const filesToBackup = getFilesToBackup(plan);
964
- if (filesToBackup.length > 0) {
965
- const backupDir = createBackup(claudeDir, filesToBackup);
966
- if (backupDir) {
967
- console.log(` Backup ${filesToBackup.length} files → ${path.relative(claudeDir, backupDir)}/`);
968
- }
969
- }
970
-
971
- // 5. Execute plan
972
- const templateInventory = buildTemplateInventory(TEMPLATES_DIR);
973
-
974
- // 5a. Handle renames
975
- for (const { from, to } of plan.toRename) {
976
- const fromPath = path.join(claudeDir, from);
977
- const toPath = path.join(claudeDir, to);
978
- const toDir = path.dirname(toPath);
979
- if (!fs.existsSync(toDir)) fs.mkdirSync(toDir, { recursive: true });
980
-
981
- if (fs.existsSync(fromPath)) {
982
- safeBackupFile(fromPath);
983
- const stat = fs.lstatSync(fromPath);
984
- if (stat.isSymbolicLink()) {
985
- // Recreate symlink at new path pointing to remapped template
986
- const target = fs.readlinkSync(fromPath);
987
- fs.unlinkSync(fromPath);
988
- fs.symlinkSync(target, toPath);
989
- } else {
990
- fs.renameSync(fromPath, toPath);
991
- }
992
- }
993
-
994
- // Transfer manifest record
995
- const record = manifest.files[from];
996
- if (record) {
997
- delete manifest.files[from];
998
- const newHash = templateInventory.get(to);
999
- manifest.files[to] = { ...record, templateHash: newHash, installedHash: newHash || record.installedHash };
1000
- }
1001
- console.log(` RENAME ${from} → ${to}`);
1002
- }
1003
-
1004
- // 5b. Add new files
1005
- for (const filePath of plan.toAdd) {
1006
- const templateHash = templateInventory.get(filePath);
1007
- if (!templateHash) continue;
1008
-
1009
- const destPath = path.join(claudeDir, filePath);
1010
- const destDir = path.dirname(destPath);
1011
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
1012
-
1013
- copyTemplateFile(TEMPLATES_DIR, filePath, destPath);
1014
- manifest.files[filePath] = templateFileRecord(templateHash);
1015
- console.log(` ADD ${filePath}`);
1016
- }
1017
-
1018
- // 5c. Update changed synced/outdated files
1019
- for (const filePath of plan.toUpdate) {
1020
- const templateHash = templateInventory.get(filePath);
1021
- if (!templateHash) continue;
1022
-
1023
- const destPath = path.join(claudeDir, filePath);
1024
- safeBackupFile(destPath);
1025
- copyTemplateFile(TEMPLATES_DIR, filePath, destPath);
1026
- manifest.files[filePath] = templateFileRecord(templateHash);
1027
- console.log(` UPDATE ${filePath}`);
1028
- }
1029
-
1030
- // 5d. Remove deleted files
1031
- for (const filePath of plan.toRemove) {
1032
- const absPath = path.join(claudeDir, filePath);
1033
- if (fs.existsSync(absPath)) {
1034
- safeBackupFile(absPath);
1035
- fs.unlinkSync(absPath);
1036
- }
1037
- delete manifest.files[filePath];
1038
- console.log(` REMOVE ${filePath}`);
1039
- }
1040
-
1041
- // 5e. Update pinned/modified file records (record new template hash without touching file)
1042
- for (const { file, reason, newTemplateHash } of plan.toSkip) {
1043
- if (manifest.files[file]) {
1044
- manifest.files[file].templateHash = newTemplateHash;
1045
- }
1046
- if (reason === 'modified') {
1047
- console.log(` SKIP ${file} (user modified)`);
1048
- } else {
1049
- console.log(` SKIP ${file} (pinned)`);
1050
- }
1051
- }
1052
-
1053
- // 5f. Clean up empty directories
1054
- for (const dir of ['hooks', 'skills', 'agents', 'config/workflows']) {
1055
- const dirPath = path.join(claudeDir, dir);
1056
- if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1057
- }
1058
-
1059
- // 6. Bidirectional settings hook sync
1060
- const settingsTarget = path.join(claudeDir, 'settings.local.json');
1061
- const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
1062
- const syncResult = syncSettingsHooks(settingsTarget, settingsSrc, manifest.settingsHooksChecksum, renameMap);
1063
- if (!syncResult.skipped) {
1064
- console.log(` Settings +${syncResult.added} -${syncResult.removed} hooks (${syncResult.updated} renamed)`);
1065
- manifest.settingsHooksChecksum = getTemplateChecksum(settingsSrc);
1066
- }
1067
-
1068
- // 7. Config migrations
1069
- const configPath = path.join(claudeDir, 'orbital.config.json');
1070
- const migrationResult = migrateConfig(configPath, manifest.appliedMigrations);
1071
- if (migrationResult.applied.length > 0) {
1072
- manifest.appliedMigrations.push(...migrationResult.applied);
1073
- console.log(` Config ${migrationResult.applied.length} migration(s) applied`);
1074
- }
1075
- if (migrationResult.defaultsFilled.length > 0) {
1076
- console.log(` Config ${migrationResult.defaultsFilled.length} default(s) filled`);
1077
- }
1078
-
1079
- // 8. Regenerate derived artifacts (always)
1080
- const workflowManifestOk = writeManifest(claudeDir);
1081
- console.log(` ${workflowManifestOk ? 'Updated' : 'Skipped'} .claude/config/workflow-manifest.sh`);
1082
-
1083
- const indexContent = generateIndexMd(projectRoot, claudeDir);
1084
- fs.writeFileSync(path.join(claudeDir, 'INDEX.md'), indexContent, 'utf8');
1085
- console.log(` Updated .claude/INDEX.md`);
1086
-
1087
- // 9. Update agent-triggers.json (template-managed)
1088
- const triggersSrc = path.join(TEMPLATES_DIR, 'config', 'agent-triggers.json');
1089
- const triggersDest = path.join(claudeDir, 'config', 'agent-triggers.json');
1090
- if (fs.existsSync(triggersSrc)) {
1091
- fs.copyFileSync(triggersSrc, triggersDest);
1092
- console.log(` Updated .claude/config/agent-triggers.json`);
1093
- }
1094
-
1095
- // 10. Update scope template
1096
- const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
1097
- const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
1098
- if (fs.existsSync(scopeTemplateSrc)) {
1099
- ensureDir(path.join(projectRoot, 'scopes'));
1100
- fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
1101
- }
1102
-
1103
- // 11. Make hook scripts executable
1104
- chmodScripts(path.join(claudeDir, 'hooks'));
1105
-
1106
- // 12. Refresh global primitives
1107
- seedGlobalPrimitives();
1108
-
1109
- // 13. Update manifest metadata
1110
- manifest.previousPackageVersion = oldVersion;
1111
- manifest.packageVersion = newVersion;
1112
- manifest.updatedAt = new Date().toISOString();
1113
- saveManifest(projectRoot, manifest);
1114
-
1115
- // 14. Validate
1116
- const report = validate(projectRoot, newVersion);
1117
- if (report.errors > 0) {
1118
- console.log(`\n Validation: ${report.errors} errors found`);
1119
- console.log(formatValidationReport(report));
1120
- }
1121
-
1122
- const totalChanges = plan.toAdd.length + plan.toUpdate.length + plan.toRemove.length + plan.toRename.length;
1123
- console.log(`\nUpdate complete. ${totalChanges} file changes, ${plan.toSkip.length} skipped.\n`);
1124
- }
1125
-
1126
- export interface UninstallOptions {
1127
- dryRun?: boolean;
1128
- keepConfig?: boolean;
1129
- }
1130
-
1131
- export function runUninstall(projectRoot: string, options: UninstallOptions = {}): void {
1132
- const { dryRun = false, keepConfig = false } = options;
1133
- const claudeDir = path.join(projectRoot, '.claude');
1134
-
1135
- console.log(`\nOrbital Command — uninstall${dryRun ? ' (dry run)' : ''}`);
1136
- console.log(`Project root: ${projectRoot}\n`);
1137
-
1138
- const manifest = loadManifest(projectRoot);
1139
-
1140
- // Fall back to legacy uninstall if no manifest
1141
- if (!manifest) {
1142
- console.log(' No manifest found — falling back to legacy uninstall.');
1143
- runLegacyUninstall(projectRoot);
1144
- return;
1145
- }
1146
-
1147
- // Compute what to remove vs preserve
1148
- const toRemove: string[] = [];
1149
- const toPreserve: string[] = [];
1150
-
1151
- for (const [filePath, record] of Object.entries(manifest.files)) {
1152
- if (record.origin === 'user') {
1153
- toPreserve.push(filePath);
1154
- } else if (record.status === 'modified' || record.status === 'outdated') {
1155
- toPreserve.push(filePath);
1156
- } else {
1157
- toRemove.push(filePath);
1158
- }
1159
- }
1160
-
1161
- if (dryRun) {
1162
- console.log(' Files to REMOVE:');
1163
- for (const f of toRemove) console.log(` ${f}`);
1164
- if (toPreserve.length > 0) {
1165
- console.log(' Files to PRESERVE:');
1166
- for (const f of toPreserve) console.log(` ${f} (${manifest.files[f].origin}/${manifest.files[f].status})`);
1167
- }
1168
- console.log(`\n Would also remove: settings hooks, generated artifacts, config files, gitignore entries`);
1169
- console.log(` No changes made. Run without --dry-run to apply.`);
1170
- return;
1171
- }
1172
-
1173
- // 1. Remove _orbital hooks from settings.local.json
1174
- const settingsPath = path.join(claudeDir, 'settings.local.json');
1175
- const removedHooks = removeAllOrbitalHooks(settingsPath);
1176
- console.log(` Removed ${removedHooks} orbital hook registrations`);
1177
-
1178
- // 2. Delete template files (synced + pinned, not modified or user-owned)
1179
- let filesRemoved = 0;
1180
- for (const filePath of toRemove) {
1181
- const absPath = path.join(claudeDir, filePath);
1182
- if (fs.existsSync(absPath)) {
1183
- fs.unlinkSync(absPath);
1184
- filesRemoved++;
1185
- }
1186
- }
1187
- console.log(` Removed ${filesRemoved} template files`);
1188
- if (toPreserve.length > 0) {
1189
- console.log(` Preserved ${toPreserve.length} user/modified files`);
1190
- }
1191
-
1192
- // 3. Clean up empty directories
1193
- for (const dir of ['hooks', 'skills', 'agents', 'config/workflows', 'quick', 'anti-patterns']) {
1194
- const dirPath = path.join(claudeDir, dir);
1195
- if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1196
- }
1197
-
1198
- // 4. Remove generated artifacts
1199
- for (const artifact of manifest.generatedArtifacts) {
1200
- const artifactPath = path.join(claudeDir, artifact);
1201
- if (fs.existsSync(artifactPath)) {
1202
- fs.unlinkSync(artifactPath);
1203
- console.log(` Removed .claude/${artifact}`);
1204
- }
1205
- }
1206
-
1207
- // 5. Remove template-sourced config files
1208
- const configFiles = [
1209
- 'config/agent-triggers.json',
1210
- 'config/workflow.json',
1211
- 'lessons-learned.md',
1212
- ];
1213
- for (const file of configFiles) {
1214
- const filePath = path.join(claudeDir, file);
1215
- if (fs.existsSync(filePath)) {
1216
- fs.unlinkSync(filePath);
1217
- console.log(` Removed .claude/${file}`);
1218
- }
1219
- }
1220
-
1221
- // Remove config/workflows/ directory entirely
1222
- const workflowsDir = path.join(claudeDir, 'config', 'workflows');
1223
- if (fs.existsSync(workflowsDir)) {
1224
- fs.rmSync(workflowsDir, { recursive: true, force: true });
1225
- console.log(` Removed .claude/config/workflows/`);
1226
- }
1227
-
1228
- // 6. Remove gitignore entries
1229
- removeGitignoreEntries(projectRoot, manifest.gitignoreEntries);
1230
- console.log(` Cleaned .gitignore`);
1231
-
1232
- // 7. Deregister from global registry
1233
- if (unregisterProject(projectRoot)) {
1234
- console.log(` Removed project from ~/.orbital/config.json`);
1235
- }
1236
-
1237
- // 8. Remove orbital config and manifest (unless --keep-config)
1238
- if (!keepConfig) {
1239
- const toClean = ['orbital.config.json', 'orbital-manifest.json', 'orbital-sync.json'];
1240
- for (const file of toClean) {
1241
- const filePath = path.join(claudeDir, file);
1242
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
1243
- }
1244
-
1245
- // Remove backups directory
1246
- const backupsDir = path.join(claudeDir, '.orbital-backups');
1247
- if (fs.existsSync(backupsDir)) fs.rmSync(backupsDir, { recursive: true, force: true });
1248
-
1249
- console.log(` Removed orbital config and manifest`);
1250
- } else {
1251
- // Still remove the manifest — it's invalid after uninstall
1252
- const manifestPath = path.join(claudeDir, 'orbital-manifest.json');
1253
- if (fs.existsSync(manifestPath)) fs.unlinkSync(manifestPath);
1254
- console.log(` Kept orbital.config.json (--keep-config)`);
1255
- }
1256
-
1257
- // Clean up remaining empty directories
1258
- for (const dir of ['config', 'quick', 'anti-patterns', 'review-verdicts']) {
1259
- const dirPath = path.join(claudeDir, dir);
1260
- if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1261
- }
1262
-
1263
- const total = removedHooks + filesRemoved;
1264
- console.log(`\nUninstall complete. ${total} items removed.`);
1265
- if (toPreserve.length > 0) {
1266
- console.log(`Note: ${toPreserve.length} user/modified files were preserved.`);
1267
- }
1268
- console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
1269
- }
1270
-
1271
- /** Legacy uninstall for projects without a manifest (backward compat). */
1272
- function runLegacyUninstall(projectRoot: string): void {
1273
- const claudeDir = path.join(projectRoot, '.claude');
1274
-
1275
- // Remove orbital hooks from settings.local.json
1276
- const settingsPath = path.join(claudeDir, 'settings.local.json');
1277
- const removedHooks = removeAllOrbitalHooks(settingsPath);
1278
- console.log(` Removed ${removedHooks} orbital hook registrations`);
1279
-
1280
- // Delete hooks/skills/agents that match template files
1281
- for (const dir of ['hooks', 'skills', 'agents']) {
1282
- const templateFiles = listTemplateFiles(path.join(TEMPLATES_DIR, dir), path.join(claudeDir, dir));
1283
- let removed = 0;
1284
- for (const f of templateFiles) {
1285
- if (fs.existsSync(f)) { fs.unlinkSync(f); removed++; }
1286
- }
1287
- const dirPath = path.join(claudeDir, dir);
1288
- if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1289
- console.log(` Removed ${removed} ${dir} files`);
1290
- }
1291
-
1292
- console.log(`\nLegacy uninstall complete.`);
1293
- console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
1294
- }
1295
-
1296
- /** Remove Orbital-added entries from .gitignore. */
1297
- function removeGitignoreEntries(projectRoot: string, entries: string[]): void {
1298
- const gitignorePath = path.join(projectRoot, '.gitignore');
1299
- if (!fs.existsSync(gitignorePath)) return;
1300
-
1301
- let content = fs.readFileSync(gitignorePath, 'utf-8');
1302
- const marker = '# Orbital Command';
1303
-
1304
- // Try to remove the entire block
1305
- const markerIdx = content.indexOf(marker);
1306
- if (markerIdx !== -1) {
1307
- // Find the block boundaries: from the marker to the next non-empty/non-orbital line
1308
- const before = content.slice(0, markerIdx).replace(/\n+$/, '');
1309
- const after = content.slice(markerIdx);
1310
- const lines = after.split('\n');
1311
- let endIdx = 0;
1312
- for (let i = 0; i < lines.length; i++) {
1313
- const line = lines[i].trim();
1314
- if (i === 0) { endIdx = i + 1; continue; } // skip the marker line
1315
- if (line === '' || entries.includes(line)) { endIdx = i + 1; continue; }
1316
- break;
1317
- }
1318
- const remaining = lines.slice(endIdx).join('\n');
1319
- content = before + (remaining ? '\n' + remaining : '') + '\n';
1320
- fs.writeFileSync(gitignorePath, content, 'utf-8');
1321
- }
1322
- }
1323
-
1324
- /** Copy a single template file to a destination, resolving the template path. */
1325
- function copyTemplateFile(templatesDir: string, claudeRelPath: string, destPath: string): void {
1326
- const templateRelPath = reverseRemapPath(claudeRelPath);
1327
- const srcPath = path.join(templatesDir, templateRelPath);
1328
- if (!fs.existsSync(srcPath)) {
1329
- throw new Error(`Template file not found: ${templateRelPath}`);
1330
- }
1331
- const destDir = path.dirname(destPath);
1332
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
1333
- safeCopyTemplate(srcPath, destPath);
1334
- }
922
+ // runUpdate and runUninstall have been extracted to update.ts and uninstall.ts
923
+ // and are re-exported above for backward compatibility.
package/server/launch.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Entry point for `orbital launch` — starts the central multi-project server.
2
+ * Entry point for the central multi-project server.
3
3
  *
4
4
  * Reads environment variables set by bin/orbital.js:
5
5
  * ORBITAL_LAUNCH_MODE=central
@@ -46,7 +46,10 @@ export function parseEventFile(filePath: string): RawEvent | null {
46
46
  timestamp: String(parsed.timestamp),
47
47
  };
48
48
  } catch (err) {
49
- log.warn('Failed to parse event file', { file: filePath, error: (err as Error).message });
49
+ const code = (err as NodeJS.ErrnoException).code;
50
+ if (code !== 'ENOENT') {
51
+ log.warn('Failed to parse event file', { file: filePath, error: (err as Error).message });
52
+ }
50
53
  return null;
51
54
  }
52
55
  }
@@ -26,7 +26,6 @@ import { startScopeWatcher } from './watchers/scope-watcher.js';
26
26
  import { startEventWatcher } from './watchers/event-watcher.js';
27
27
  import { resolveStaleDispatches, resolveActiveDispatchesForScope, resolveDispatchesByPid, resolveDispatchesByDispatchId, linkPidToDispatch, tryAutoRevertAndClear } from './utils/dispatch-utils.js';
28
28
  import { syncClaudeSessionsToDB } from './services/claude-session-service.js';
29
- import { TelemetryService } from './services/telemetry-service.js';
30
29
  import { ensureDynamicProfiles } from './utils/terminal-launcher.js';
31
30
  import { createLogger } from './utils/logger.js';
32
31
 
@@ -36,6 +35,11 @@ const log = createLogger('project-context');
36
35
 
37
36
  export type ProjectStatus = 'active' | 'error' | 'offline';
38
37
 
38
+ interface TelemetryEnabled {
39
+ enabled: boolean;
40
+ uploadChangedSessions(): Promise<unknown>;
41
+ }
42
+
39
43
  export interface ProjectContext {
40
44
  /** Project slug ID (derived from directory name) */
41
45
  id: string;
@@ -61,7 +65,8 @@ export interface ProjectContext {
61
65
  workflowService: WorkflowService;
62
66
  gitService: GitService;
63
67
  githubService: GitHubService;
64
- telemetryService: TelemetryService;
68
+ telemetryService: TelemetryEnabled | null;
69
+ telemetryRouter: import('express').Router | null;
65
70
 
66
71
  // Watchers
67
72
  scopeWatcher: FSWatcher;
@@ -122,8 +127,8 @@ export async function createProjectContext(
122
127
  const gateService = new GateService(db, emitter);
123
128
  const deployService = new DeployService(db, emitter);
124
129
  const sprintService = new SprintService(db, emitter, scopeService);
125
- const sprintOrchestrator = new SprintOrchestrator(db, emitter, sprintService, scopeService, workflowEngine, config.projectRoot);
126
- const batchOrchestrator = new BatchOrchestrator(db, emitter, sprintService, scopeService, workflowEngine, config.projectRoot);
130
+ const sprintOrchestrator = new SprintOrchestrator(db, emitter, sprintService, scopeService, workflowEngine, config.projectRoot, config);
131
+ const batchOrchestrator = new BatchOrchestrator(db, emitter, sprintService, scopeService, workflowEngine, config.projectRoot, config);
127
132
  const readinessService = new ReadinessService(scopeService, gateService, workflowEngine, config.projectRoot);
128
133
  const workflowService = new WorkflowService(config.configDir, workflowEngine, config.scopesDir, getDefaultConfigPath());
129
134
  workflowService.setSocketServer(emitter);
@@ -132,7 +137,17 @@ export async function createProjectContext(
132
137
  workflowEngine.reload(workflowService.getActive());
133
138
  const gitService = new GitService(config.projectRoot, scopeCache);
134
139
  const githubService = new GitHubService(config.projectRoot);
135
- const telemetryService = new TelemetryService(db, config.telemetry, config.projectName, config.projectRoot);
140
+
141
+ let telemetryService: TelemetryEnabled | null = null;
142
+ let telemetryRouter: import('express').Router | null = null;
143
+ const telemetryMod = './services/telemetry-service.js';
144
+ try {
145
+ const mod = await import(telemetryMod);
146
+ telemetryService = new mod.TelemetryService(db, config.telemetry, config.projectName, config.projectRoot);
147
+ if (telemetryService?.enabled) {
148
+ telemetryRouter = mod.createTelemetryRoutes({ telemetryService });
149
+ }
150
+ } catch { /* telemetry service not installed */ }
136
151
 
137
152
  // Wire active-group guard
138
153
  scopeService.setActiveGroupCheck((scopeId) => sprintService.getActiveGroupForScope(scopeId));
@@ -190,7 +205,7 @@ export async function createProjectContext(
190
205
 
191
206
  // Wire status change callbacks
192
207
  scopeService.onStatusChange((scopeId, newStatus) => {
193
- if (newStatus === 'dev') sprintOrchestrator.onScopeReachedDev(scopeId);
208
+ if (workflowEngine.isTerminalStatus(newStatus)) sprintOrchestrator.onScopeReachedDev(scopeId);
194
209
  batchOrchestrator.onScopeStatusChanged(scopeId, newStatus);
195
210
  });
196
211
  scopeService.onStatusChange((scopeId, newStatus) => {
@@ -220,14 +235,14 @@ export async function createProjectContext(
220
235
  if (staleBatchesResolved > 0) log.info('Resolved stale batches', { count: staleBatchesResolved });
221
236
 
222
237
  // Resolve stale dispatches
223
- resolveStaleDispatches(db, emitter, scopeService, workflowEngine);
238
+ resolveStaleDispatches(db, emitter, scopeService, workflowEngine, config.dispatch.staleTimeoutMinutes);
224
239
 
225
240
  // Initial session sync + legacy purge (Fix 7)
226
241
  syncClaudeSessionsToDB(db, scopeService, config.projectRoot).then((count) => {
227
242
  if (count > 0) log.info('Synced sessions', { id: projectId, count });
228
243
  const purged = db.prepare("DELETE FROM sessions WHERE action IS NULL AND id LIKE 'claude-%'").run();
229
244
  if (purged.changes > 0) log.info('Purged legacy session rows', { count: purged.changes });
230
- if (telemetryService.enabled) {
245
+ if (telemetryService?.enabled) {
231
246
  telemetryService.uploadChangedSessions().catch(() => {});
232
247
  }
233
248
  }).catch(err => log.error('Session sync failed', { error: err.message }));
@@ -245,13 +260,13 @@ export async function createProjectContext(
245
260
  }, 30_000));
246
261
 
247
262
  intervals.push(setInterval(() => {
248
- resolveStaleDispatches(db, emitter, scopeService, workflowEngine);
263
+ resolveStaleDispatches(db, emitter, scopeService, workflowEngine, config.dispatch.staleTimeoutMinutes);
249
264
  }, 30_000));
250
265
 
251
266
  intervals.push(setInterval(async () => {
252
267
  const count = await syncClaudeSessionsToDB(db, scopeService, config.projectRoot);
253
268
  if (count > 0) emitter.emit('session:updated', { type: 'resync', count });
254
- if (telemetryService.enabled) {
269
+ if (telemetryService?.enabled) {
255
270
  telemetryService.uploadChangedSessions().catch(() => {});
256
271
  }
257
272
  }, 5 * 60_000));
@@ -289,6 +304,7 @@ export async function createProjectContext(
289
304
  gitService,
290
305
  githubService,
291
306
  telemetryService,
307
+ telemetryRouter,
292
308
  scopeWatcher,
293
309
  eventWatcher,
294
310
  intervals,