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.
- package/bin/commands/launch.js +0 -1
- package/bin/lib/helpers.js +1 -31
- package/bin/orbital.js +32 -30
- package/dist/server/server/index.js +1 -9
- package/dist/server/server/launch.js +0 -3
- package/dist/server/server/routes/sync-routes.js +2 -2
- package/dist/server/server/services/telemetry-service.js +143 -0
- package/dist/server/server/wizard/detect.js +0 -79
- package/dist/server/server/wizard/index.js +27 -91
- package/dist/server/server/wizard/types.js +2 -3
- package/package.json +1 -1
- package/server/index.ts +0 -13
- package/server/launch.ts +0 -3
- package/server/routes/sync-routes.ts +2 -2
- package/server/wizard/detect.ts +1 -81
- package/server/wizard/index.ts +29 -116
- package/server/wizard/types.ts +2 -17
- package/templates/hooks/block-push.sh +16 -2
- package/templates/hooks/git-commit-guard.sh +3 -2
- package/dist/server/server/wizard/phases/confirm.js +0 -39
- package/dist/server/server/wizard/phases/project-setup.js +0 -90
- package/dist/server/server/wizard/phases/welcome.js +0 -32
- package/dist/server/server/wizard/phases/workflow-setup.js +0 -22
- package/server/wizard/phases/confirm.ts +0 -45
- package/server/wizard/phases/project-setup.ts +0 -106
- package/server/wizard/phases/welcome.ts +0 -39
- package/server/wizard/phases/workflow-setup.ts +0 -28
package/bin/commands/launch.js
CHANGED
package/bin/lib/helpers.js
CHANGED
|
@@ -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 —
|
|
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
|
-
//
|
|
49
|
+
// Not an initialized project — launch dashboard directly.
|
|
51
50
|
// The frontend Add Project modal handles project setup.
|
|
52
|
-
if (!isInitialized
|
|
51
|
+
if (!isInitialized) {
|
|
53
52
|
cmdLaunchOrDev(false);
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// Show hub menu
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
break;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,
|
|
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(
|
|
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() —
|
|
6
|
-
*
|
|
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
|
|
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(
|
|
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
|
-
// ──
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
+
return action;
|
|
334
270
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the interactive CLI wizard.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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';
|