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
@@ -0,0 +1,144 @@
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
+
6
+ // ─── errMsg ─────────────────────────────────────────────────
7
+
8
+ describe('errMsg', () => {
9
+ it('extracts message from Error instances', () => {
10
+ expect(errMsg(new Error('something broke'))).toBe('something broke');
11
+ });
12
+
13
+ it('converts non-Error values to strings', () => {
14
+ expect(errMsg('raw string')).toBe('raw string');
15
+ expect(errMsg(42)).toBe('42');
16
+ expect(errMsg(null)).toBe('null');
17
+ expect(errMsg(undefined)).toBe('undefined');
18
+ });
19
+ });
20
+
21
+ // ─── isValidRelativePath ────────────────────────────────────
22
+
23
+ describe('isValidRelativePath', () => {
24
+ it('accepts normal relative paths', () => {
25
+ expect(isValidRelativePath('hooks/init.sh')).toBe(true);
26
+ expect(isValidRelativePath('agents/attacker/AGENT.md')).toBe(true);
27
+ expect(isValidRelativePath('file.txt')).toBe(true);
28
+ });
29
+
30
+ it('rejects directory traversal', () => {
31
+ expect(isValidRelativePath('../etc/passwd')).toBe(false);
32
+ expect(isValidRelativePath('hooks/../../secret')).toBe(false);
33
+ });
34
+
35
+ it('rejects absolute paths', () => {
36
+ expect(isValidRelativePath('/etc/passwd')).toBe(false);
37
+ });
38
+
39
+ it('rejects null bytes', () => {
40
+ expect(isValidRelativePath('file\0.txt')).toBe(false);
41
+ });
42
+ });
43
+
44
+ // ─── inferErrorStatus ───────────────────────────────────────
45
+
46
+ describe('inferErrorStatus', () => {
47
+ it('returns 403 for traversal errors', () => {
48
+ expect(inferErrorStatus('Path traversal detected')).toBe(403);
49
+ expect(inferErrorStatus('directory traversal not allowed')).toBe(403);
50
+ });
51
+
52
+ it('returns 404 for not-found errors', () => {
53
+ expect(inferErrorStatus('File not found')).toBe(404);
54
+ expect(inferErrorStatus('ENOENT: no such file')).toBe(404);
55
+ });
56
+
57
+ it('returns 409 for already-exists errors', () => {
58
+ expect(inferErrorStatus('File already exists at path')).toBe(409);
59
+ });
60
+
61
+ it('returns 400 for directory errors', () => {
62
+ expect(inferErrorStatus('Cannot delete directory')).toBe(400);
63
+ });
64
+
65
+ it('returns 500 for unrecognized errors', () => {
66
+ expect(inferErrorStatus('something unexpected')).toBe(500);
67
+ expect(inferErrorStatus('')).toBe(500);
68
+ });
69
+
70
+ it('matches the first keyword when multiple are present', () => {
71
+ // "traversal" comes first in the chain, should win
72
+ expect(inferErrorStatus('traversal not found')).toBe(403);
73
+ });
74
+ });
75
+
76
+ // ─── catchRoute ─────────────────────────────────────────────
77
+
78
+ describe('catchRoute', () => {
79
+ function createApp(handler: express.RequestHandler) {
80
+ const app = express();
81
+ app.get('/test', handler);
82
+ return app;
83
+ }
84
+
85
+ it('returns normal response when handler succeeds', async () => {
86
+ const app = createApp(catchRoute((_req, res) => {
87
+ res.json({ ok: true });
88
+ }));
89
+
90
+ const res = await request(app).get('/test');
91
+ expect(res.status).toBe(200);
92
+ expect(res.body).toEqual({ ok: true });
93
+ });
94
+
95
+ it('catches sync throws and returns 500', async () => {
96
+ const app = createApp(catchRoute(() => {
97
+ throw new Error('sync failure');
98
+ }));
99
+
100
+ const res = await request(app).get('/test');
101
+ expect(res.status).toBe(500);
102
+ expect(res.body).toEqual({ success: false, error: 'sync failure' });
103
+ });
104
+
105
+ it('catches async throws and returns 500', async () => {
106
+ const app = createApp(catchRoute(async () => {
107
+ throw new Error('async failure');
108
+ }));
109
+
110
+ const res = await request(app).get('/test');
111
+ expect(res.status).toBe(500);
112
+ expect(res.body).toEqual({ success: false, error: 'async failure' });
113
+ });
114
+
115
+ it('uses custom statusFn for error status codes', async () => {
116
+ const app = createApp(catchRoute(() => {
117
+ throw new Error('File not found at path');
118
+ }, inferErrorStatus));
119
+
120
+ const res = await request(app).get('/test');
121
+ expect(res.status).toBe(404);
122
+ expect(res.body).toEqual({ success: false, error: 'File not found at path' });
123
+ });
124
+
125
+ it('uses custom statusFn for traversal errors', async () => {
126
+ const app = createApp(catchRoute(() => {
127
+ throw new Error('Path traversal detected');
128
+ }, inferErrorStatus));
129
+
130
+ const res = await request(app).get('/test');
131
+ expect(res.status).toBe(403);
132
+ });
133
+
134
+ it('handles non-Error thrown values', async () => {
135
+ const app = createApp(catchRoute(() => {
136
+ // eslint-disable-next-line no-throw-literal
137
+ throw 'raw string error';
138
+ }));
139
+
140
+ const res = await request(app).get('/test');
141
+ expect(res.status).toBe(500);
142
+ expect(res.body).toEqual({ success: false, error: 'raw string error' });
143
+ });
144
+ });
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import type { Request, Response, NextFunction, RequestHandler } from 'express';
2
3
 
3
4
  /** Extract a human-readable message from an unknown error value. */
4
5
  export function errMsg(err: unknown): string {
@@ -10,3 +11,40 @@ export function isValidRelativePath(p: string): boolean {
10
11
  const normalized = path.normalize(p);
11
12
  return !normalized.startsWith('..') && !path.isAbsolute(normalized) && !normalized.includes('\0');
12
13
  }
14
+
15
+ /** Infer an HTTP status code from an error message. */
16
+ export function inferErrorStatus(msg: string): number {
17
+ if (msg.includes('traversal')) return 403;
18
+ if (msg.includes('ENOENT') || msg.includes('not found')) return 404;
19
+ if (msg.includes('already exists')) return 409;
20
+ if (msg.includes('directory')) return 400;
21
+ return 500;
22
+ }
23
+
24
+ /**
25
+ * Wrap an Express route handler to catch thrown errors and send a JSON error response.
26
+ * Works with both sync and async handlers.
27
+ *
28
+ * @param fn — route handler that may throw
29
+ * @param statusFn — optional function to infer status from error message (defaults to 500)
30
+ */
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ export function catchRoute<Req extends Request = any, Res extends Response = any>(
33
+ fn: (req: Req, res: Res, next: NextFunction) => void | Promise<void>,
34
+ statusFn?: (msg: string) => number,
35
+ ): RequestHandler {
36
+ return ((req: Req, res: Res, next: NextFunction) => {
37
+ try {
38
+ const result = fn(req, res, next);
39
+ if (result instanceof Promise) {
40
+ result.catch((err: unknown) => {
41
+ const msg = errMsg(err);
42
+ res.status(statusFn ? statusFn(msg) : 500).json({ success: false, error: msg });
43
+ });
44
+ }
45
+ } catch (err) {
46
+ const msg = errMsg(err);
47
+ res.status(statusFn ? statusFn(msg) : 500).json({ success: false, error: msg });
48
+ }
49
+ }) as unknown as RequestHandler;
50
+ }
@@ -8,6 +8,7 @@ import { createLogger } from '../utils/logger.js';
8
8
  const log = createLogger('event');
9
9
 
10
10
  const ARCHIVE_DIR_NAME = 'processed';
11
+ const processingFiles = new Set<string>();
11
12
 
12
13
  /**
13
14
  * Watch .claude/orbital-events/ for new JSON event files.
@@ -81,20 +82,31 @@ function processEventFile(
81
82
  eventService: EventService,
82
83
  archiveDir: string
83
84
  ): void {
84
- const event = parseEventFile(filePath);
85
- if (!event) return;
85
+ if (processingFiles.has(filePath)) return;
86
+ processingFiles.add(filePath);
87
+ try {
88
+ const event = parseEventFile(filePath);
89
+ if (!event) return;
86
90
 
87
- eventService.ingest(event);
91
+ eventService.ingest(event);
88
92
 
89
- // Move to archive
90
- const fileName = path.basename(filePath);
91
- try {
92
- fs.renameSync(filePath, path.join(archiveDir, fileName));
93
- } catch (err) {
94
- log.warn('Failed to archive event file', { file: filePath, error: (err as Error).message });
95
- // If rename fails (cross-device), just delete the source
96
- try { fs.unlinkSync(filePath); } catch (unlinkErr) {
97
- log.warn('Failed to delete event file', { file: filePath, error: (unlinkErr as Error).message });
93
+ // Move to archive
94
+ const fileName = path.basename(filePath);
95
+ try {
96
+ fs.renameSync(filePath, path.join(archiveDir, fileName));
97
+ } catch (err) {
98
+ const code = (err as NodeJS.ErrnoException).code;
99
+ if (code === 'ENOENT') return; // Already archived by concurrent handler
100
+ log.warn('Failed to archive event file', { file: filePath, error: (err as Error).message });
101
+ // If rename fails (cross-device), just delete the source
102
+ try { fs.unlinkSync(filePath); } catch (unlinkErr) {
103
+ const unlinkCode = (unlinkErr as NodeJS.ErrnoException).code;
104
+ if (unlinkCode !== 'ENOENT') {
105
+ log.warn('Failed to delete event file', { file: filePath, error: (unlinkErr as Error).message });
106
+ }
107
+ }
98
108
  }
109
+ } finally {
110
+ processingFiles.delete(filePath);
99
111
  }
100
112
  }
@@ -15,7 +15,7 @@ export async function runConfigEditor(projectRoot: string, packageVersion: strin
15
15
  if (subcommand === 'show') {
16
16
  const config = loadProjectConfig(projectRoot);
17
17
  if (!config) {
18
- console.error('No config found. Run `orbital init` first.');
18
+ console.error('No config found. Run `orbital` first.');
19
19
  process.exit(1);
20
20
  }
21
21
  console.log(JSON.stringify(config, null, 2));
@@ -37,7 +37,7 @@ export async function runConfigEditor(projectRoot: string, packageVersion: strin
37
37
  // Interactive mode
38
38
  const config = loadProjectConfig(projectRoot);
39
39
  if (!config) {
40
- p.log.error('No config found. Run `orbital init` first.');
40
+ p.log.error('No config found. Run `orbital` first.');
41
41
  process.exit(1);
42
42
  }
43
43
 
@@ -166,7 +166,7 @@ async function editGlobalSection(): Promise<void> {
166
166
  const registryPath = path.join(homedir, '.orbital', 'config.json');
167
167
 
168
168
  if (!fs.existsSync(registryPath)) {
169
- p.log.info('No global registry found. Run `orbital init` in a project first.');
169
+ p.log.info('No global registry found. Run `orbital` in a project first.');
170
170
  return;
171
171
  }
172
172
 
@@ -210,7 +210,7 @@ function saveProjectConfig(projectRoot: string, config: Record<string, unknown>)
210
210
  function setConfigValue(projectRoot: string, key: string, value: string): void {
211
211
  const config = loadProjectConfig(projectRoot);
212
212
  if (!config) {
213
- console.error('No config found. Run `orbital init` first.');
213
+ console.error('No config found. Run `orbital` first.');
214
214
  process.exit(1);
215
215
  }
216
216
 
@@ -49,7 +49,7 @@ export async function runDoctor(projectRoot: string, packageVersion: string): Pr
49
49
  checks.push({ label: 'Global', status: pc.yellow('~/.orbital/ exists (registry unreadable)') });
50
50
  }
51
51
  } else {
52
- checks.push({ label: 'Global', status: pc.dim('~/.orbital/ not found (run `orbital init` to create)') });
52
+ checks.push({ label: 'Global', status: pc.dim('~/.orbital/ not found (run `orbital` to create)') });
53
53
  }
54
54
 
55
55
  // 4. Project initialization
@@ -63,7 +63,7 @@ export async function runDoctor(projectRoot: string, packageVersion: string): Pr
63
63
  checks.push({ label: 'Project', status: pc.yellow('config exists but unreadable') });
64
64
  }
65
65
  } else {
66
- checks.push({ label: 'Project', status: pc.dim('not initialized (run `orbital init`)') });
66
+ checks.push({ label: 'Project', status: pc.dim('not initialized (run `orbital`)') });
67
67
  }
68
68
 
69
69
  // 5. Workflow