orbital-command 1.1.0 → 1.1.1

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.
@@ -38,7 +38,6 @@ export function cmdLaunchOrDev(forceViteFlag) {
38
38
  const env = {
39
39
  ...process.env,
40
40
  ORBITAL_LAUNCH_MODE: 'central',
41
- ORBITAL_AUTO_REGISTER: projectRoot,
42
41
  ORBITAL_SERVER_PORT: String(serverPort),
43
42
  };
44
43
 
@@ -39,15 +39,6 @@ export function resolveBin(name) {
39
39
  return null;
40
40
  }
41
41
 
42
- export function isGitRepo() {
43
- try {
44
- execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: 'pipe' });
45
- return true;
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
42
  export function detectProjectRoot() {
52
43
  try {
53
44
  return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: 'pipe' }).trim();
@@ -56,13 +47,6 @@ export function detectProjectRoot() {
56
47
  }
57
48
  }
58
49
 
59
- export function requireGitRepo() {
60
- if (!isGitRepo()) {
61
- console.error('Not a git repository. Run `orbital` from inside a project directory.');
62
- process.exit(1);
63
- }
64
- }
65
-
66
50
  export function loadConfig(projectRoot) {
67
51
  const configPath = path.join(projectRoot, '.claude', 'orbital.config.json');
68
52
  if (fs.existsSync(configPath)) {
@@ -84,21 +68,6 @@ export function getPackageVersion() {
84
68
  }
85
69
  }
86
70
 
87
- export function stampTemplateVersion(projectRoot) {
88
- const configPath = path.join(projectRoot, '.claude', 'orbital.config.json');
89
- if (!fs.existsSync(configPath)) return;
90
-
91
- try {
92
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
93
- const version = getPackageVersion();
94
- if (config.templateVersion !== version) {
95
- config.templateVersion = version;
96
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
97
- console.log(` Stamped templateVersion: ${version}`);
98
- }
99
- } catch { /* ignore malformed config */ }
100
- }
101
-
102
71
  export function openBrowser(url) {
103
72
  const platform = process.platform;
104
73
  if (platform === 'darwin') {
@@ -188,6 +157,7 @@ Usage:
188
157
  orbital <command> Run a specific command directly
189
158
 
190
159
  Commands:
160
+ launch Launch the dashboard directly
191
161
  config Modify project settings interactively
192
162
  doctor Health check and version diagnostics
193
163
  update Sync templates and apply migrations
package/bin/orbital.js CHANGED
@@ -10,7 +10,6 @@ import {
10
10
  loadSharedModule,
11
11
  loadWizardModule,
12
12
  orbitalSetupDone,
13
- stampTemplateVersion,
14
13
  printHelp,
15
14
  } from './lib/helpers.js';
16
15
 
@@ -34,9 +33,10 @@ async function runHubFlow() {
34
33
  const wiz = await loadWizardModule();
35
34
  const hubVersion = getPackageVersion();
36
35
 
37
- // First-time global setup — no menu, just run the wizard
36
+ // First-time global setup — run wizard then launch dashboard
38
37
  if (!orbitalSetupDone()) {
39
38
  await wiz.runSetupWizard(hubVersion);
39
+ cmdLaunchOrDev(false);
40
40
  return;
41
41
  }
42
42
 
@@ -45,23 +45,22 @@ async function runHubFlow() {
45
45
  path.join(hubRoot, '.claude', 'orbital.config.json')
46
46
  );
47
47
  const hubRegistry = loadRegistry();
48
- const projectNames = (hubRegistry.projects || []).map(p => p.name);
49
48
 
50
- // No registered projects — launch dashboard directly.
49
+ // Not an initialized project — launch dashboard directly.
51
50
  // The frontend Add Project modal handles project setup.
52
- if (!isInitialized && projectNames.length === 0) {
51
+ if (!isInitialized) {
53
52
  cmdLaunchOrDev(false);
54
53
  return;
55
54
  }
56
55
 
57
- // Show hub menu (initialized OR has registered projects)
56
+ // Show hub menu for initialized projects
57
+ const projectNames = (hubRegistry.projects || []).map(p => p.name);
58
58
  const projects = (hubRegistry.projects || [])
59
59
  .filter(p => p.enabled !== false)
60
60
  .map(p => ({ name: p.name, path: p.path }));
61
61
 
62
62
  const hubResult = await wiz.runHub({
63
63
  packageVersion: hubVersion,
64
- isProjectInitialized: isInitialized,
65
64
  projectNames,
66
65
  itermPromptShown: hubRegistry.itermPromptShown === true,
67
66
  isMac: process.platform === 'darwin',
@@ -85,26 +84,29 @@ async function runHubFlow() {
85
84
  writeRegistryAtomic(hubRegistry);
86
85
  }
87
86
 
88
- // Route the chosen action
89
- switch (hubResult.action) {
90
- case 'launch': cmdLaunchOrDev(false); break;
91
- case 'init':
92
- await wiz.runProjectSetup(hubRoot, hubVersion, []);
93
- stampTemplateVersion(hubRoot);
94
- break;
95
- case 'config': await cmdConfig([]); break;
96
- case 'doctor': await cmdDoctor(); break;
97
- case 'update': await cmdUpdate([]); break;
98
- case 'status': await cmdStatus(); break;
99
- case 'reset': {
100
- const { runInit } = await loadSharedModule();
101
- runInit(hubRoot, { force: true });
102
- stampTemplateVersion(hubRoot);
103
- break;
87
+ // Route the chosen action, then loop back to menu
88
+ let action = hubResult.action;
89
+
90
+ while (true) {
91
+ switch (action) {
92
+ case 'launch': cmdLaunchOrDev(false); return;
93
+ case 'config': await cmdConfig([]); break;
94
+ case 'doctor': await cmdDoctor(); break;
95
+ case 'update': await cmdUpdate([]); break;
96
+ case 'status': await cmdStatus(); break;
97
+ case 'reset': {
98
+ const { runInit } = await loadSharedModule();
99
+ runInit(hubRoot, { force: true });
100
+ break;
101
+ }
102
+ default:
103
+ console.error(`Unknown action: ${action}`);
104
+ process.exit(1);
104
105
  }
105
- default:
106
- console.error(`Unknown action: ${hubResult.action}`);
107
- process.exit(1);
106
+
107
+ // Show menu again after completing an action
108
+ console.log('');
109
+ action = await wiz.promptHubAction(projectNames);
108
110
  }
109
111
  }
110
112
 
@@ -116,14 +118,14 @@ const [command, ...args] = process.argv.slice(2);
116
118
 
117
119
  async function main() {
118
120
  switch (command) {
119
- // Deprecated commands — silently redirect to hub
120
- case 'init':
121
- case 'setup':
122
- case 'launch':
123
121
  case undefined:
124
122
  await runHubFlow();
125
123
  break;
126
124
 
125
+ case 'launch':
126
+ cmdLaunchOrDev(false);
127
+ break;
128
+
127
129
  // Active commands
128
130
  case 'config':
129
131
  await cmdConfig(args);
@@ -13,7 +13,7 @@ import { SyncService } from './services/sync-service.js';
13
13
  import { startGlobalWatcher } from './watchers/global-watcher.js';
14
14
  import { createSyncRoutes } from './routes/sync-routes.js';
15
15
  import { seedGlobalPrimitives } from './init.js';
16
- import { ensureOrbitalHome, loadGlobalConfig, registerProject as registerProjectGlobal, GLOBAL_PRIMITIVES_DIR, ORBITAL_HOME, } from './global-config.js';
16
+ import { ensureOrbitalHome, GLOBAL_PRIMITIVES_DIR, ORBITAL_HOME, } from './global-config.js';
17
17
  export async function startCentralServer(overrides) {
18
18
  ensureOrbitalHome();
19
19
  const envLevel = process.env.ORBITAL_LOG_LEVEL;
@@ -23,12 +23,6 @@ export async function startCentralServer(overrides) {
23
23
  const log = createLogger('central');
24
24
  const port = overrides?.port ?? (Number(process.env.ORBITAL_SERVER_PORT) || 4444);
25
25
  const clientPort = overrides?.clientPort ?? (Number(process.env.ORBITAL_CLIENT_PORT) || 4445);
26
- // Auto-register current project if registry is empty
27
- const globalConfig = loadGlobalConfig();
28
- if (globalConfig.projects.length === 0 && overrides?.autoRegisterPath) {
29
- registerProjectGlobal(overrides.autoRegisterPath);
30
- log.info('Auto-registered current project', { path: overrides.autoRegisterPath });
31
- }
32
26
  const app = express();
33
27
  const httpServer = createServer(app);
34
28
  const io = new Server(httpServer, {
@@ -201,10 +195,8 @@ const isDirectRun = process.argv[1] && (process.argv[1].endsWith('server/index.t
201
195
  process.argv[1].endsWith('server/index.js') ||
202
196
  process.argv[1].endsWith('server'));
203
197
  if (isDirectRun) {
204
- const projectRoot = process.env.ORBITAL_PROJECT_ROOT || process.cwd();
205
198
  startCentralServer({
206
199
  port: Number(process.env.ORBITAL_SERVER_PORT) || 4444,
207
- autoRegisterPath: projectRoot,
208
200
  }).then(({ shutdown }) => {
209
201
  process.on('SIGINT', async () => {
210
202
  await shutdown();
@@ -3,17 +3,14 @@
3
3
  *
4
4
  * Reads environment variables set by bin/orbital.js:
5
5
  * ORBITAL_LAUNCH_MODE=central
6
- * ORBITAL_AUTO_REGISTER=<path> (if no projects registered yet)
7
6
  * ORBITAL_SERVER_PORT=<port>
8
7
  */
9
8
  import { startCentralServer } from './index.js';
10
9
  import { createLogger } from './utils/logger.js';
11
10
  const log = createLogger('launch');
12
11
  const port = Number(process.env.ORBITAL_SERVER_PORT) || 4444;
13
- const autoRegisterPath = process.env.ORBITAL_AUTO_REGISTER || undefined;
14
12
  startCentralServer({
15
13
  port,
16
- autoRegisterPath: autoRegisterPath || undefined,
17
14
  }).then(({ shutdown }) => {
18
15
  process.on('SIGINT', async () => {
19
16
  await shutdown();
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { execFile } from 'child_process';
5
5
  import { isValidRelativePath } from '../utils/route-helpers.js';
6
- import { runInit } from '../init.js';
6
+ import { runInit, TEMPLATES_DIR } from '../init.js';
7
7
  import { loadGlobalConfig } from '../global-config.js';
8
8
  import { getPackageVersion } from '../utils/package-info.js';
9
9
  export function createSyncRoutes({ syncService, projectManager }) {
@@ -253,7 +253,7 @@ export function createSyncRoutes({ syncService, projectManager }) {
253
253
  // ─── Helpers ──────────────────────────────────────────────
254
254
  function seedWelcomeCard(projectRoot, preset) {
255
255
  // Determine the planning directory from the preset
256
- const presetsDir = path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), 'templates', 'presets');
256
+ const presetsDir = path.join(TEMPLATES_DIR, 'presets');
257
257
  let planningDir = 'planning'; // default fallback
258
258
  try {
259
259
  const presetPath = path.join(presetsDir, `${preset}.json`);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Session Telemetry — uploads raw Claude session JSONL files to a remote
3
+ * Cloudflare Worker + R2 endpoint. This entire feature lives in this single
4
+ * file for easy removal.
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { Router } from 'express';
9
+ import { getClaudeSessionsDir } from '../config.js';
10
+ // ─── Service ───────────────────────────────────────────────
11
+ export class TelemetryService {
12
+ db;
13
+ config;
14
+ projectName;
15
+ projectRoot;
16
+ lastResult = null;
17
+ constructor(db, config, projectName, projectRoot) {
18
+ this.db = db;
19
+ this.config = config;
20
+ this.projectName = projectName;
21
+ this.projectRoot = projectRoot;
22
+ }
23
+ get enabled() {
24
+ return this.config.enabled && this.config.url.length > 0;
25
+ }
26
+ /** Upload sessions that have changed since last telemetry send. */
27
+ async uploadChangedSessions() {
28
+ if (!this.enabled)
29
+ return { ok: true, uploaded: 0, errors: 0 };
30
+ const rows = this.db.prepare(`SELECT id, claude_session_id, ended_at, telemetry_sent_at
31
+ FROM sessions
32
+ WHERE claude_session_id IS NOT NULL
33
+ AND (telemetry_sent_at IS NULL OR ended_at > telemetry_sent_at)`).all();
34
+ return this.uploadRows(rows);
35
+ }
36
+ /** Force re-upload all sessions regardless of telemetry_sent_at. */
37
+ async uploadAllSessions() {
38
+ if (!this.enabled)
39
+ return { ok: true, uploaded: 0, errors: 0 };
40
+ const rows = this.db.prepare(`SELECT id, claude_session_id, ended_at, telemetry_sent_at
41
+ FROM sessions
42
+ WHERE claude_session_id IS NOT NULL`).all();
43
+ return this.uploadRows(rows);
44
+ }
45
+ /** Ping the remote health endpoint. */
46
+ async testConnection() {
47
+ try {
48
+ const res = await fetch(`${this.config.url}/health`, {
49
+ method: 'GET',
50
+ headers: this.config.headers,
51
+ signal: AbortSignal.timeout(10_000),
52
+ });
53
+ const body = await res.text();
54
+ return { ok: res.ok, status: res.status, body };
55
+ }
56
+ catch (err) {
57
+ return { ok: false, status: 0, body: err.message };
58
+ }
59
+ }
60
+ getStatus() {
61
+ return {
62
+ enabled: this.enabled,
63
+ url: this.config.url || null,
64
+ lastResult: this.lastResult,
65
+ };
66
+ }
67
+ // ─── Internal ──────────────────────────────────────────────
68
+ async uploadRows(rows) {
69
+ if (rows.length === 0) {
70
+ this.lastResult = { ok: true, uploaded: 0, errors: 0 };
71
+ return this.lastResult;
72
+ }
73
+ const sessionsDir = getClaudeSessionsDir(this.projectRoot);
74
+ const now = new Date().toISOString();
75
+ let uploaded = 0;
76
+ let errors = 0;
77
+ const updateStmt = this.db.prepare('UPDATE sessions SET telemetry_sent_at = ? WHERE id = ?');
78
+ // Deduplicate by claude_session_id (multiple rows can share the same JSONL file)
79
+ const seen = new Set();
80
+ const unique = [];
81
+ for (const row of rows) {
82
+ if (row.claude_session_id && !seen.has(row.claude_session_id)) {
83
+ seen.add(row.claude_session_id);
84
+ unique.push(row);
85
+ }
86
+ }
87
+ for (const row of unique) {
88
+ const sessionId = row.claude_session_id;
89
+ const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
90
+ if (!fs.existsSync(filePath)) {
91
+ continue;
92
+ }
93
+ try {
94
+ const body = fs.readFileSync(filePath);
95
+ const encodedProject = encodeURIComponent(this.projectName);
96
+ const url = `${this.config.url}/upload/${encodedProject}/${sessionId}.jsonl`;
97
+ const res = await fetch(url, {
98
+ method: 'PUT',
99
+ body,
100
+ headers: {
101
+ 'Content-Type': 'application/x-ndjson',
102
+ ...this.config.headers,
103
+ },
104
+ signal: AbortSignal.timeout(30_000),
105
+ });
106
+ if (res.ok) {
107
+ uploaded++;
108
+ // Update telemetry_sent_at for ALL rows with this claude_session_id
109
+ const matching = rows.filter((r) => r.claude_session_id === sessionId);
110
+ for (const m of matching) {
111
+ updateStmt.run(now, m.id);
112
+ }
113
+ }
114
+ else {
115
+ errors++;
116
+ }
117
+ }
118
+ catch {
119
+ errors++;
120
+ }
121
+ }
122
+ this.lastResult = { ok: errors === 0, uploaded, errors };
123
+ return this.lastResult;
124
+ }
125
+ }
126
+ export function createTelemetryRoutes({ telemetryService }) {
127
+ const router = Router();
128
+ router.post('/telemetry/trigger', async (req, res) => {
129
+ const force = req.query.force === 'true';
130
+ const result = force
131
+ ? await telemetryService.uploadAllSessions()
132
+ : await telemetryService.uploadChangedSessions();
133
+ res.json(result);
134
+ });
135
+ router.post('/telemetry/test', async (_req, res) => {
136
+ const result = await telemetryService.testConnection();
137
+ res.json(result);
138
+ });
139
+ router.get('/telemetry/status', (_req, res) => {
140
+ res.json(telemetryService.getStatus());
141
+ });
142
+ return router;
143
+ }
@@ -5,9 +5,6 @@ import fs from 'fs';
5
5
  import path from 'path';
6
6
  const ORBITAL_HOME = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.orbital');
7
7
  export { ORBITAL_HOME };
8
- export function isInteractiveTerminal() {
9
- return !!(process.stdout.isTTY && !process.env.CI);
10
- }
11
8
  export function isOrbitalSetupDone() {
12
9
  return fs.existsSync(path.join(ORBITAL_HOME, 'config.json'));
13
10
  }
@@ -18,79 +15,3 @@ export function buildSetupState(packageVersion) {
18
15
  linkedProjects: [],
19
16
  };
20
17
  }
21
- export function buildProjectState(projectRoot, packageVersion) {
22
- const projectConfigExists = fs.existsSync(path.join(projectRoot, '.claude', 'orbital.config.json'));
23
- return {
24
- projectRoot,
25
- isProjectInitialized: projectConfigExists,
26
- packageVersion,
27
- };
28
- }
29
- export function detectProjectName(projectRoot) {
30
- return path.basename(projectRoot)
31
- .replace(/[-_]+/g, ' ')
32
- .replace(/\b\w/g, c => c.toUpperCase());
33
- }
34
- export function detectCommands(projectRoot) {
35
- const commands = {
36
- typeCheck: null,
37
- lint: null,
38
- build: null,
39
- test: null,
40
- };
41
- const pkgJsonPath = path.join(projectRoot, 'package.json');
42
- if (!fs.existsSync(pkgJsonPath))
43
- return commands;
44
- try {
45
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
46
- const scripts = pkg.scripts || {};
47
- if (scripts.typecheck || scripts['type-check']) {
48
- commands.typeCheck = `npm run ${scripts.typecheck ? 'typecheck' : 'type-check'}`;
49
- }
50
- if (scripts.lint)
51
- commands.lint = 'npm run lint';
52
- if (scripts.build)
53
- commands.build = 'npm run build';
54
- if (scripts.test)
55
- commands.test = 'npm run test';
56
- }
57
- catch { /* ignore malformed package.json */ }
58
- return commands;
59
- }
60
- export function detectPortConflict(serverPort) {
61
- const registryPath = path.join(ORBITAL_HOME, 'config.json');
62
- if (!fs.existsSync(registryPath))
63
- return null;
64
- try {
65
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
66
- for (const project of registry.projects || []) {
67
- const configPath = path.join(project.path, '.claude', 'orbital.config.json');
68
- if (!fs.existsSync(configPath))
69
- continue;
70
- try {
71
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
72
- if (config.serverPort === serverPort)
73
- return project.name;
74
- }
75
- catch { /* skip unreadable configs */ }
76
- }
77
- }
78
- catch { /* skip unreadable registry */ }
79
- return null;
80
- }
81
- export function isValidProjectPath(p) {
82
- const resolved = p.startsWith('~')
83
- ? path.join(process.env.HOME || process.env.USERPROFILE || '~', p.slice(1))
84
- : path.resolve(p);
85
- if (!fs.existsSync(resolved))
86
- return 'Directory does not exist';
87
- if (!fs.statSync(resolved).isDirectory())
88
- return 'Not a directory';
89
- return undefined;
90
- }
91
- export function resolveProjectPath(p) {
92
- if (p.startsWith('~')) {
93
- return path.join(process.env.HOME || process.env.USERPROFILE || '~', p.slice(1));
94
- }
95
- return path.resolve(p);
96
- }
@@ -2,8 +2,8 @@
2
2
  * Interactive CLI wizard — main orchestrator.
3
3
  *
4
4
  * Entry points:
5
- * runSetupWizard() — Phase 1: first-time Orbital setup (~/.orbital/)
6
- * runProjectSetup() Phase 2: per-project scaffolding (.claude/)
5
+ * runSetupWizard() — First-time Orbital setup (~/.orbital/)
6
+ * runHub() Context-aware hub menu (orbital)
7
7
  * runConfigEditor() — interactive config editor (orbital config)
8
8
  * runDoctor() — health diagnostics (orbital doctor)
9
9
  */
@@ -12,16 +12,11 @@ import path from 'path';
12
12
  import { spawn, execFileSync } from 'child_process';
13
13
  import * as p from '@clack/prompts';
14
14
  import pc from 'picocolors';
15
- import { buildSetupState, buildProjectState } from './detect.js';
15
+ import { buildSetupState } from './detect.js';
16
16
  import { phaseSetupWizard } from './phases/setup-wizard.js';
17
- import { phaseWelcome } from './phases/welcome.js';
18
- import { phaseProjectSetup } from './phases/project-setup.js';
19
- import { phaseWorkflowSetup } from './phases/workflow-setup.js';
20
- import { phaseConfirm, showPostInstall } from './phases/confirm.js';
21
17
  import { runConfigEditor } from './config-editor.js';
22
18
  import { runDoctor } from './doctor.js';
23
19
  import { isITerm2Available } from '../adapters/iterm2-adapter.js';
24
- import { registerProject } from '../global-config.js';
25
20
  export { runConfigEditor, runDoctor };
26
21
  // ─── Phase 1: Setup Wizard ─────────────────────────────────────
27
22
  /**
@@ -32,56 +27,7 @@ export async function runSetupWizard(packageVersion) {
32
27
  const state = buildSetupState(packageVersion);
33
28
  p.intro(`${pc.bgCyan(pc.black(' Orbital Command '))} ${pc.dim(`v${packageVersion}`)}`);
34
29
  await phaseSetupWizard(state);
35
- p.outro(`Run ${pc.cyan('orbital')} to launch the dashboard and add your first project.`);
36
- }
37
- // ─── Phase 2: Project Setup ────────────────────────────────────
38
- /**
39
- * Per-project setup. Walks through name, commands, workflow, then
40
- * calls runInit() to scaffold files into .claude/.
41
- */
42
- export async function runProjectSetup(projectRoot, packageVersion, args) {
43
- const state = buildProjectState(projectRoot, packageVersion);
44
- const force = args.includes('--force');
45
- p.intro(`${pc.bgCyan(pc.black(' Orbital Command '))} ${pc.dim(`v${packageVersion}`)}`);
46
- // Welcome gate: detect re-init / reconfigure
47
- const forceFromWelcome = await phaseWelcome(state);
48
- const useForce = force || forceFromWelcome;
49
- await runProjectPhases(state, useForce);
50
- p.outro(`Run ${pc.cyan('orbital')} to launch the dashboard.`);
51
- }
52
- // ─── Shared project phases (used by both flows) ────────────────
53
- /**
54
- * Run the project setup phases and install. Used by both
55
- * standalone runProjectSetup() and inline from runSetupWizard().
56
- */
57
- async function runProjectPhases(state, useForce) {
58
- await phaseProjectSetup(state);
59
- await phaseWorkflowSetup(state);
60
- await phaseConfirm(state);
61
- // Install
62
- const s = p.spinner();
63
- s.start('Installing into project...');
64
- try {
65
- const { runInit } = await import('../init.js');
66
- runInit(state.projectRoot, {
67
- force: useForce,
68
- quiet: true,
69
- preset: state.workflowPreset,
70
- projectName: state.projectName,
71
- serverPort: state.serverPort,
72
- clientPort: state.clientPort,
73
- commands: state.selectedCommands,
74
- });
75
- registerProject(state.projectRoot, { name: state.projectName });
76
- stampTemplateVersion(state.projectRoot, state.packageVersion);
77
- s.stop('Project ready.');
78
- }
79
- catch (err) {
80
- s.stop('Installation failed.');
81
- p.log.error(err instanceof Error ? err.message : String(err));
82
- process.exit(1);
83
- }
84
- showPostInstall(state);
30
+ p.outro('Launching dashboard...');
85
31
  }
86
32
  /** Returns true if `a` is older than `b` (semver comparison). */
87
33
  function isOlderThan(a, b) {
@@ -272,17 +218,26 @@ export async function runHub(opts) {
272
218
  });
273
219
  }
274
220
  }
275
- // ── Build menu options based on project state ──
276
- const projectHint = opts.projectNames.length > 0
277
- ? pc.dim(` (${opts.projectNames.join(', ')})`)
221
+ // ── Show menu and pick action ──
222
+ result.action = await promptHubAction(opts.projectNames);
223
+ return result;
224
+ }
225
+ /**
226
+ * Show the hub menu and return the chosen action.
227
+ * Exported separately so the CLI can loop back after executing an action.
228
+ */
229
+ export async function promptHubAction(projectNames) {
230
+ const projectHint = projectNames.length > 0
231
+ ? pc.dim(` (${projectNames.join(', ')})`)
278
232
  : '';
279
- const options = [];
280
- if (opts.isProjectInitialized) {
281
- options.push({ value: 'launch', label: `Launch dashboard${projectHint}` }, { value: 'config', label: 'Config', hint: 'modify project settings' }, { value: 'doctor', label: 'Doctor', hint: 'health check & diagnostics' }, { value: 'update', label: 'Update templates', hint: 'sync to latest' }, { value: 'status', label: 'Status', hint: 'template sync status' }, { value: 'reset', label: 'Reset to defaults', hint: 'force-reset all templates' });
282
- }
283
- else {
284
- options.push({ value: 'init', label: 'Initialize this project' }, { value: 'launch', label: `Launch dashboard${projectHint}` });
285
- }
233
+ const options = [
234
+ { value: 'launch', label: `Launch dashboard${projectHint}` },
235
+ { value: 'config', label: 'Config', hint: 'modify project settings' },
236
+ { value: 'doctor', label: 'Doctor', hint: 'health check & diagnostics' },
237
+ { value: 'update', label: 'Update templates', hint: 'sync to latest' },
238
+ { value: 'status', label: 'Status', hint: 'template sync status' },
239
+ { value: 'reset', label: 'Reset to defaults', hint: 'force-reset all templates' },
240
+ ];
286
241
  const action = await p.select({
287
242
  message: 'What would you like to do?',
288
243
  options,
@@ -291,7 +246,7 @@ export async function runHub(opts) {
291
246
  p.cancel('Cancelled.');
292
247
  process.exit(0);
293
248
  }
294
- // ── Double-confirm for destructive reset ──
249
+ // Double-confirm for destructive reset
295
250
  if (action === 'reset') {
296
251
  p.note('This will overwrite ALL hooks, skills, agents, and workflow config\n' +
297
252
  'with the default templates. Modified and pinned files will be replaced.\n' +
@@ -301,34 +256,15 @@ export async function runHub(opts) {
301
256
  initialValue: false,
302
257
  });
303
258
  if (p.isCancel(confirmReset) || !confirmReset) {
304
- p.cancel('Reset cancelled.');
305
- process.exit(0);
259
+ return promptHubAction(projectNames);
306
260
  }
307
261
  const doubleConfirm = await p.confirm({
308
262
  message: 'This cannot be undone. Continue?',
309
263
  initialValue: false,
310
264
  });
311
265
  if (p.isCancel(doubleConfirm) || !doubleConfirm) {
312
- p.cancel('Reset cancelled.');
313
- process.exit(0);
314
- }
315
- }
316
- result.action = action;
317
- return result;
318
- }
319
- // ─── Template Version Stamping ─────────────────────────────────
320
- function stampTemplateVersion(projectRoot, packageVersion) {
321
- const configPath = path.join(projectRoot, '.claude', 'orbital.config.json');
322
- if (!fs.existsSync(configPath))
323
- return;
324
- try {
325
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
326
- if (config.templateVersion !== packageVersion) {
327
- config.templateVersion = packageVersion;
328
- const tmp = configPath + `.tmp.${process.pid}`;
329
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', 'utf8');
330
- fs.renameSync(tmp, configPath);
266
+ return promptHubAction(projectNames);
331
267
  }
332
268
  }
333
- catch { /* ignore malformed config */ }
269
+ return action;
334
270
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Types for the interactive CLI wizard.
3
3
  *
4
- * Two wizard flows:
5
- * Phase 1 (SetupState) — first-time Orbital setup, ~/.orbital/ creation
6
- * Phase 2 (ProjectSetupState) — per-project scaffolding into .claude/
4
+ * Phase 1 (SetupState) — first-time Orbital setup, ~/.orbital/ creation.
5
+ * Project setup is handled by the frontend Add Project modal.
7
6
  */
8
7
  export { WORKFLOW_PRESETS } from '../../shared/workflow-presets.js';