orbital-command 1.0.1 → 1.1.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.
- package/README.md +1 -1
- package/bin/orbital.js +3 -3
- package/dist/assets/Landing-B6q9U0Vd.js +11 -0
- package/dist/assets/{PrimitivesConfig-DThSipFy.js → PrimitivesConfig-TlFvypvg.js} +9 -14
- package/dist/assets/{QualityGates-B4kxM5UU.js → QualityGates-D8uvclW4.js} +1 -1
- package/dist/assets/SessionTimeline-QUaJw6Aa.js +1 -0
- package/dist/assets/Settings-CAEnAZAk.js +12 -0
- package/dist/assets/{SourceControl-BMNIz7Lt.js → SourceControl-DPeSBaMV.js} +7 -12
- package/dist/assets/{WorkflowVisualizer-CxuSBOYu.js → WorkflowVisualizer-DHrIjx6W.js} +1 -1
- package/dist/assets/{arrow-down-DVPp6_qp.js → arrow-down-DnfKgF33.js} +1 -1
- package/dist/assets/{bot-NFaJBDn_.js → bot-DUPnHZfM.js} +1 -1
- package/dist/assets/{circle-x-IsFCkBZu.js → circle-x-B4nA79je.js} +1 -1
- package/dist/assets/{file-text-J1cebZXF.js → file-text-PnzRO4pO.js} +1 -1
- package/dist/assets/{globe-WzeyHsUc.js → globe-CIX_GBr0.js} +1 -1
- package/dist/assets/index-B-B-tTjw.css +1 -0
- package/dist/assets/index-DQVAzHMu.js +354 -0
- package/dist/assets/{key-CKR8JJSj.js → key-DpAZM-3d.js} +1 -1
- package/dist/assets/{minus-CHBsJyjp.js → minus-CJlpKNUB.js} +1 -1
- package/dist/assets/{radio-xqZaR-Uk.js → radio-DhynLcSx.js} +1 -1
- package/dist/assets/{rocket-D_xvvNG6.js → rocket-DnuQh3sF.js} +1 -1
- package/dist/assets/{shield-TdB1yv_a.js → shield-DLMQmvFQ.js} +1 -1
- package/dist/assets/{ui-BmsSg9jU.js → ui-QhrRH5wk.js} +6 -6
- package/dist/assets/{useSocketListener-0L5yiN5i.js → useSocketListener-DPrExNDV.js} +1 -1
- package/dist/assets/{useWorkflowEditor-CqeRWVQX.js → useWorkflowEditor-BxN7phfr.js} +2 -2
- package/dist/assets/{workflow-constants-Rw-GmgHZ.js → workflow-constants-ZcCN11vm.js} +1 -1
- package/dist/assets/{zap-C9wqYMpl.js → zap-eJ3z8HRI.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/server/server/index.js +8 -1
- package/dist/server/server/routes/sync-routes.js +175 -0
- package/dist/server/server/wizard/index.js +18 -24
- package/dist/server/server/wizard/phases/setup-wizard.js +4 -34
- package/dist/server/server/wizard/types.js +1 -22
- package/dist/server/shared/workflow-presets.js +22 -0
- package/package.json +1 -1
- package/server/index.ts +7 -1
- package/server/routes/sync-routes.ts +205 -0
- package/server/wizard/index.ts +17 -31
- package/server/wizard/phases/setup-wizard.ts +7 -38
- package/server/wizard/types.ts +2 -28
- package/shared/workflow-presets.ts +28 -0
- package/dist/assets/Landing-CfQdHR0N.js +0 -11
- package/dist/assets/SessionTimeline-Bz1iZnmg.js +0 -1
- package/dist/assets/Settings-DLcZwbCT.js +0 -12
- package/dist/assets/index-BdJ57EhC.css +0 -1
- package/dist/assets/index-o4ScMAuR.js +0 -349
package/dist/index.html
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DQVAzHMu.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/vendor-Bqt8AJn2.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/ui-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/ui-QhrRH5wk.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/charts-LGLb8hyU.js">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B-B-tTjw.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="h-full">
|
|
18
18
|
<div id="root" class="h-full"></div>
|
|
@@ -104,7 +104,14 @@ export async function startCentralServer(overrides) {
|
|
|
104
104
|
app.use('/api/orbital', createAggregateRoutes({ projectManager, io, syncService }));
|
|
105
105
|
// ─── Static File Serving ─────────────────────────────────
|
|
106
106
|
const __selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
107
|
-
|
|
107
|
+
// Find package root — works from both source (server/) and compiled (dist/server/server/)
|
|
108
|
+
let pkgRoot = __selfDir;
|
|
109
|
+
while (pkgRoot !== path.dirname(pkgRoot)) {
|
|
110
|
+
if (fs.existsSync(path.join(pkgRoot, 'package.json')))
|
|
111
|
+
break;
|
|
112
|
+
pkgRoot = path.dirname(pkgRoot);
|
|
113
|
+
}
|
|
114
|
+
const distDir = path.join(pkgRoot, 'dist');
|
|
108
115
|
const hasBuiltFrontend = fs.existsSync(path.join(distDir, 'index.html'));
|
|
109
116
|
const devMode = !hasBuiltFrontend;
|
|
110
117
|
if (hasBuiltFrontend) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execFile } from 'child_process';
|
|
2
5
|
import { isValidRelativePath } from '../utils/route-helpers.js';
|
|
6
|
+
import { runInit } from '../init.js';
|
|
7
|
+
import { loadGlobalConfig } from '../global-config.js';
|
|
8
|
+
import { getPackageVersion } from '../utils/package-info.js';
|
|
3
9
|
export function createSyncRoutes({ syncService, projectManager }) {
|
|
4
10
|
const router = Router();
|
|
5
11
|
// ─── Sync State ─────────────────────────────────────────
|
|
@@ -130,5 +136,174 @@ export function createSyncRoutes({ syncService, projectManager }) {
|
|
|
130
136
|
return res.status(404).json({ error: 'Project not found' });
|
|
131
137
|
res.json(updated);
|
|
132
138
|
});
|
|
139
|
+
// ─── Project Creation (Frontend Setup) ───────────────────
|
|
140
|
+
/** POST /projects/browse — open native folder picker (macOS) */
|
|
141
|
+
router.post('/projects/browse', (_req, res) => {
|
|
142
|
+
if (process.platform !== 'darwin') {
|
|
143
|
+
return res.json({ error: 'not_supported' });
|
|
144
|
+
}
|
|
145
|
+
execFile('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select your project folder")'], { timeout: 60_000 }, (err, stdout) => {
|
|
146
|
+
if (err) {
|
|
147
|
+
// User pressed Cancel — osascript exits with code 1
|
|
148
|
+
if (err.code === 1) {
|
|
149
|
+
return res.json({ cancelled: true });
|
|
150
|
+
}
|
|
151
|
+
return res.json({ error: err.message });
|
|
152
|
+
}
|
|
153
|
+
const selectedPath = stdout.trim();
|
|
154
|
+
if (!selectedPath)
|
|
155
|
+
return res.json({ cancelled: true });
|
|
156
|
+
res.json({ path: selectedPath });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
/** POST /projects/check-path — validate a path and detect git status */
|
|
160
|
+
router.post('/projects/check-path', (req, res) => {
|
|
161
|
+
const { path: rawPath } = req.body;
|
|
162
|
+
if (!rawPath || !rawPath.trim()) {
|
|
163
|
+
return res.json({ valid: false, error: 'Path is required' });
|
|
164
|
+
}
|
|
165
|
+
const absPath = path.resolve(rawPath.trim());
|
|
166
|
+
if (!fs.existsSync(absPath)) {
|
|
167
|
+
return res.json({ valid: false, absPath, error: 'Directory does not exist' });
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const stat = fs.statSync(absPath);
|
|
171
|
+
if (!stat.isDirectory()) {
|
|
172
|
+
return res.json({ valid: false, absPath, error: 'Path must be a directory' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return res.json({ valid: false, absPath, error: 'Cannot access path' });
|
|
177
|
+
}
|
|
178
|
+
const hasGit = fs.existsSync(path.join(absPath, '.git'));
|
|
179
|
+
const suggestedName = path.basename(absPath)
|
|
180
|
+
.replace(/[-_]/g, ' ')
|
|
181
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
182
|
+
// Check if already registered
|
|
183
|
+
const config = loadGlobalConfig();
|
|
184
|
+
const alreadyRegistered = config.projects.some(p => p.path === absPath);
|
|
185
|
+
res.json({
|
|
186
|
+
valid: true,
|
|
187
|
+
absPath,
|
|
188
|
+
hasGit,
|
|
189
|
+
suggestedName,
|
|
190
|
+
alreadyRegistered,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
/** POST /projects/create — full project initialization (init + register + seed) */
|
|
194
|
+
router.post('/projects/create', async (req, res) => {
|
|
195
|
+
const { path: rawPath, name, color, preset, initGit } = req.body;
|
|
196
|
+
if (!rawPath || !name || !color) {
|
|
197
|
+
return res.status(400).json({ error: 'path, name, and color are required' });
|
|
198
|
+
}
|
|
199
|
+
const absPath = path.resolve(rawPath.trim());
|
|
200
|
+
// Validate directory exists
|
|
201
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
|
|
202
|
+
return res.status(400).json({ error: 'Path must be an existing directory' });
|
|
203
|
+
}
|
|
204
|
+
// Check if already registered
|
|
205
|
+
const config = loadGlobalConfig();
|
|
206
|
+
if (config.projects.some(p => p.path === absPath)) {
|
|
207
|
+
return res.status(409).json({ error: 'A project is already registered at this path' });
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
// 1. Optional git init
|
|
211
|
+
if (initGit && !fs.existsSync(path.join(absPath, '.git'))) {
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
execFile('git', ['init'], { cwd: absPath, timeout: 10_000 }, (err) => {
|
|
214
|
+
if (err)
|
|
215
|
+
reject(new Error(`git init failed: ${err.message}`));
|
|
216
|
+
else
|
|
217
|
+
resolve();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// 2. Run full project init (templates, hooks, skills, agents, config, etc.)
|
|
222
|
+
const selectedPreset = preset || 'default';
|
|
223
|
+
runInit(absPath, {
|
|
224
|
+
quiet: true,
|
|
225
|
+
preset: selectedPreset,
|
|
226
|
+
projectName: name,
|
|
227
|
+
});
|
|
228
|
+
// 3. Stamp template version
|
|
229
|
+
const pkgVersion = getPackageVersion();
|
|
230
|
+
const configPath = path.join(absPath, '.claude', 'orbital.config.json');
|
|
231
|
+
if (fs.existsSync(configPath)) {
|
|
232
|
+
try {
|
|
233
|
+
const projConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
234
|
+
projConfig.templateVersion = pkgVersion;
|
|
235
|
+
const tmp = configPath + `.tmp.${process.pid}`;
|
|
236
|
+
fs.writeFileSync(tmp, JSON.stringify(projConfig, null, 2) + '\n', 'utf8');
|
|
237
|
+
fs.renameSync(tmp, configPath);
|
|
238
|
+
}
|
|
239
|
+
catch { /* ignore malformed config */ }
|
|
240
|
+
}
|
|
241
|
+
// 4. Register + initialize context + emit socket event
|
|
242
|
+
const summary = await projectManager.addProject(absPath, { name, color });
|
|
243
|
+
// 5. Seed welcome scope card
|
|
244
|
+
seedWelcomeCard(absPath, selectedPreset);
|
|
245
|
+
res.status(201).json(summary);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
133
251
|
return router;
|
|
134
252
|
}
|
|
253
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
254
|
+
function seedWelcomeCard(projectRoot, preset) {
|
|
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');
|
|
257
|
+
let planningDir = 'planning'; // default fallback
|
|
258
|
+
try {
|
|
259
|
+
const presetPath = path.join(presetsDir, `${preset}.json`);
|
|
260
|
+
if (fs.existsSync(presetPath)) {
|
|
261
|
+
const workflow = JSON.parse(fs.readFileSync(presetPath, 'utf8'));
|
|
262
|
+
// Find the first list that has a directory and isn't an entry point
|
|
263
|
+
const entryId = workflow.entryPoint;
|
|
264
|
+
const entryList = workflow.lists?.find((l) => l.id === entryId);
|
|
265
|
+
// Use the first forward target of the entry point, or the second list
|
|
266
|
+
if (entryList?.forwardTargets?.[0]) {
|
|
267
|
+
planningDir = entryList.forwardTargets[0];
|
|
268
|
+
}
|
|
269
|
+
else if (workflow.lists?.[1]?.hasDirectory) {
|
|
270
|
+
planningDir = workflow.lists[1].id;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch { /* use default */ }
|
|
275
|
+
const scopesDir = path.join(projectRoot, 'scopes', planningDir);
|
|
276
|
+
if (!fs.existsSync(scopesDir)) {
|
|
277
|
+
fs.mkdirSync(scopesDir, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
const cardPath = path.join(scopesDir, '001-welcome.md');
|
|
280
|
+
if (fs.existsSync(cardPath))
|
|
281
|
+
return; // don't overwrite
|
|
282
|
+
const content = `---
|
|
283
|
+
title: Welcome to Orbital Command
|
|
284
|
+
status: ${planningDir}
|
|
285
|
+
priority: low
|
|
286
|
+
tags: [onboarding]
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
# Welcome to Orbital Command
|
|
290
|
+
|
|
291
|
+
Your project is set up and ready to go. Here are some things to try:
|
|
292
|
+
|
|
293
|
+
## Getting Started
|
|
294
|
+
|
|
295
|
+
1. **Create a scope** — Scopes are units of work. Use \`/scope-create\` in Claude Code or create a markdown file in the \`scopes/\` directory.
|
|
296
|
+
2. **Explore the dashboard** — Use the sidebar to navigate between views: Kanban, Primitives, Guards, Repo, Sessions, and Workflow.
|
|
297
|
+
3. **Launch a session** — Start a Claude Code session from the Sessions view to begin working on this scope.
|
|
298
|
+
|
|
299
|
+
## Key Concepts
|
|
300
|
+
|
|
301
|
+
- **Scopes** are markdown files with YAML frontmatter that track units of work
|
|
302
|
+
- **Workflow** defines the columns and transitions on your Kanban board
|
|
303
|
+
- **Guards** enforce quality gates before status transitions
|
|
304
|
+
- **Primitives** are the hooks, skills, and agents that power your setup
|
|
305
|
+
|
|
306
|
+
You can archive this card once you're comfortable with the basics.
|
|
307
|
+
`;
|
|
308
|
+
fs.writeFileSync(cardPath, content, 'utf8');
|
|
309
|
+
}
|
|
@@ -18,7 +18,6 @@ import { phaseWelcome } from './phases/welcome.js';
|
|
|
18
18
|
import { phaseProjectSetup } from './phases/project-setup.js';
|
|
19
19
|
import { phaseWorkflowSetup } from './phases/workflow-setup.js';
|
|
20
20
|
import { phaseConfirm, showPostInstall } from './phases/confirm.js';
|
|
21
|
-
import { NOTES } from './ui.js';
|
|
22
21
|
import { runConfigEditor } from './config-editor.js';
|
|
23
22
|
import { runDoctor } from './doctor.js';
|
|
24
23
|
import { isITerm2Available } from '../adapters/iterm2-adapter.js';
|
|
@@ -26,24 +25,14 @@ import { registerProject } from '../global-config.js';
|
|
|
26
25
|
export { runConfigEditor, runDoctor };
|
|
27
26
|
// ─── Phase 1: Setup Wizard ─────────────────────────────────────
|
|
28
27
|
/**
|
|
29
|
-
* First-time setup. Creates ~/.orbital
|
|
30
|
-
*
|
|
28
|
+
* First-time setup. Creates ~/.orbital/ and seeds primitives.
|
|
29
|
+
* Project setup is now handled by the frontend Add Project modal.
|
|
31
30
|
*/
|
|
32
31
|
export async function runSetupWizard(packageVersion) {
|
|
33
32
|
const state = buildSetupState(packageVersion);
|
|
34
33
|
p.intro(`${pc.bgCyan(pc.black(' Orbital Command '))} ${pc.dim(`v${packageVersion}`)}`);
|
|
35
34
|
await phaseSetupWizard(state);
|
|
36
|
-
|
|
37
|
-
for (const projectRoot of state.linkedProjects) {
|
|
38
|
-
p.log.step(`Setting up ${pc.cyan(path.basename(projectRoot))}...`);
|
|
39
|
-
await runProjectSetupInline(projectRoot, packageVersion);
|
|
40
|
-
}
|
|
41
|
-
if (state.linkedProjects.length === 0) {
|
|
42
|
-
p.note(NOTES.setupComplete, 'Done');
|
|
43
|
-
}
|
|
44
|
-
p.outro(state.linkedProjects.length > 0
|
|
45
|
-
? `Run ${pc.cyan('orbital')} to launch the dashboard.`
|
|
46
|
-
: `Run ${pc.cyan('orbital')} in a project directory to get started.`);
|
|
35
|
+
p.outro(`Run ${pc.cyan('orbital')} to launch the dashboard and add your first project.`);
|
|
47
36
|
}
|
|
48
37
|
// ─── Phase 2: Project Setup ────────────────────────────────────
|
|
49
38
|
/**
|
|
@@ -94,21 +83,26 @@ async function runProjectPhases(state, useForce) {
|
|
|
94
83
|
}
|
|
95
84
|
showPostInstall(state);
|
|
96
85
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
/** Returns true if `a` is older than `b` (semver comparison). */
|
|
87
|
+
function isOlderThan(a, b) {
|
|
88
|
+
const pa = a.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
89
|
+
const pb = b.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
90
|
+
if (!pa || !pb)
|
|
91
|
+
return false;
|
|
92
|
+
for (let i = 1; i <= 3; i++) {
|
|
93
|
+
if (parseInt(pa[i]) < parseInt(pb[i]))
|
|
94
|
+
return true;
|
|
95
|
+
if (parseInt(pa[i]) > parseInt(pb[i]))
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
105
99
|
}
|
|
106
100
|
async function checkForUpdate(currentVersion, cache) {
|
|
107
101
|
// Use cache if checked within 24 hours
|
|
108
102
|
if (cache.lastUpdateCheck && cache.latestVersion) {
|
|
109
103
|
const age = Date.now() - new Date(cache.lastUpdateCheck).getTime();
|
|
110
104
|
if (age < 24 * 60 * 60 * 1000) {
|
|
111
|
-
const isOutdated = cache.latestVersion
|
|
105
|
+
const isOutdated = isOlderThan(currentVersion, cache.latestVersion);
|
|
112
106
|
return {
|
|
113
107
|
info: { current: currentVersion, latest: cache.latestVersion, isOutdated },
|
|
114
108
|
cacheChanged: false,
|
|
@@ -123,7 +117,7 @@ async function checkForUpdate(currentVersion, cache) {
|
|
|
123
117
|
const data = await res.json();
|
|
124
118
|
const latest = data.version;
|
|
125
119
|
return {
|
|
126
|
-
info: { current: currentVersion, latest, isOutdated: latest
|
|
120
|
+
info: { current: currentVersion, latest, isOutdated: isOlderThan(currentVersion, latest) },
|
|
127
121
|
cacheChanged: true,
|
|
128
122
|
};
|
|
129
123
|
}
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
|
-
import pc from 'picocolors';
|
|
10
9
|
import { NOTES } from '../ui.js';
|
|
11
|
-
import {
|
|
12
|
-
export async function phaseSetupWizard(
|
|
10
|
+
import { ORBITAL_HOME } from '../detect.js';
|
|
11
|
+
export async function phaseSetupWizard(_state) {
|
|
13
12
|
// Welcome and core concepts
|
|
14
13
|
p.note(NOTES.setupWelcome, 'Welcome');
|
|
15
14
|
// Create ~/.orbital/ and seed primitives
|
|
@@ -32,35 +31,6 @@ export async function phaseSetupWizard(state) {
|
|
|
32
31
|
p.log.error(err instanceof Error ? err.message : String(err));
|
|
33
32
|
process.exit(1);
|
|
34
33
|
}
|
|
35
|
-
//
|
|
36
|
-
p.note(
|
|
37
|
-
let addMore = true;
|
|
38
|
-
while (addMore) {
|
|
39
|
-
const wantsProject = await p.confirm({
|
|
40
|
-
message: state.linkedProjects.length === 0
|
|
41
|
-
? 'Add a project now?'
|
|
42
|
-
: 'Add another project?',
|
|
43
|
-
initialValue: state.linkedProjects.length === 0,
|
|
44
|
-
});
|
|
45
|
-
if (p.isCancel(wantsProject) || !wantsProject) {
|
|
46
|
-
addMore = false;
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
const projectPath = await p.text({
|
|
50
|
-
message: 'Project path',
|
|
51
|
-
placeholder: '~/Code/my-project',
|
|
52
|
-
validate: (val) => {
|
|
53
|
-
if (!val || !val.trim())
|
|
54
|
-
return 'Path is required';
|
|
55
|
-
return isValidProjectPath(val.trim());
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
if (p.isCancel(projectPath)) {
|
|
59
|
-
addMore = false;
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
const resolved = resolveProjectPath(projectPath.trim());
|
|
63
|
-
state.linkedProjects.push(resolved);
|
|
64
|
-
p.log.success(`Added: ${pc.cyan(resolved)}`);
|
|
65
|
-
}
|
|
34
|
+
// Direct user to the dashboard for project setup
|
|
35
|
+
p.note('Launch the dashboard to add your first project.\nThe setup wizard will guide you through it.', 'Next Steps');
|
|
66
36
|
}
|
|
@@ -5,25 +5,4 @@
|
|
|
5
5
|
* Phase 1 (SetupState) — first-time Orbital setup, ~/.orbital/ creation
|
|
6
6
|
* Phase 2 (ProjectSetupState) — per-project scaffolding into .claude/
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
9
|
-
{
|
|
10
|
-
value: 'default',
|
|
11
|
-
label: 'Default',
|
|
12
|
-
hint: '7 lists, trunk-based — Icebox → Planning → Backlog → Implementing → Review → Completed → Main',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
value: 'minimal',
|
|
16
|
-
label: 'Minimal',
|
|
17
|
-
hint: '3 lists — To Do → In Progress → Done',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
value: 'development',
|
|
21
|
-
label: 'Development',
|
|
22
|
-
hint: '5 lists, dev branch — Backlog → Implementing → Review → Completed → Dev',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
value: 'gitflow',
|
|
26
|
-
label: 'Gitflow',
|
|
27
|
-
hint: '9 lists, multi-branch — Full pipeline with Dev, Staging, and Production',
|
|
28
|
-
},
|
|
29
|
-
];
|
|
8
|
+
export { WORKFLOW_PRESETS } from '../../shared/workflow-presets.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const WORKFLOW_PRESETS = [
|
|
2
|
+
{
|
|
3
|
+
value: 'default',
|
|
4
|
+
label: 'Default',
|
|
5
|
+
hint: '7 lists, trunk-based — Icebox → Planning → Backlog → Implementing → Review → Completed → Main',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
value: 'minimal',
|
|
9
|
+
label: 'Minimal',
|
|
10
|
+
hint: '3 lists — To Do → In Progress → Done',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
value: 'development',
|
|
14
|
+
label: 'Development',
|
|
15
|
+
hint: '5 lists, dev branch — Backlog → Implementing → Review → Completed → Dev',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
value: 'gitflow',
|
|
19
|
+
label: 'Gitflow',
|
|
20
|
+
hint: '9 lists, multi-branch — Full pipeline with Dev, Staging, and Production',
|
|
21
|
+
},
|
|
22
|
+
];
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -148,7 +148,13 @@ export async function startCentralServer(overrides?: CentralServerOverrides): Pr
|
|
|
148
148
|
// ─── Static File Serving ─────────────────────────────────
|
|
149
149
|
|
|
150
150
|
const __selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
151
|
-
|
|
151
|
+
// Find package root — works from both source (server/) and compiled (dist/server/server/)
|
|
152
|
+
let pkgRoot = __selfDir;
|
|
153
|
+
while (pkgRoot !== path.dirname(pkgRoot)) {
|
|
154
|
+
if (fs.existsSync(path.join(pkgRoot, 'package.json'))) break;
|
|
155
|
+
pkgRoot = path.dirname(pkgRoot);
|
|
156
|
+
}
|
|
157
|
+
const distDir = path.join(pkgRoot, 'dist');
|
|
152
158
|
const hasBuiltFrontend = fs.existsSync(path.join(distDir, 'index.html'));
|
|
153
159
|
const devMode = !hasBuiltFrontend;
|
|
154
160
|
if (hasBuiltFrontend) {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execFile } from 'child_process';
|
|
2
5
|
import type { SyncService } from '../services/sync-service.js';
|
|
3
6
|
import type { ProjectManager } from '../project-manager.js';
|
|
4
7
|
import { isValidRelativePath } from '../utils/route-helpers.js';
|
|
8
|
+
import { runInit } from '../init.js';
|
|
9
|
+
import { loadGlobalConfig } from '../global-config.js';
|
|
10
|
+
import { getPackageVersion } from '../utils/package-info.js';
|
|
5
11
|
|
|
6
12
|
interface SyncRouteDeps {
|
|
7
13
|
syncService: SyncService;
|
|
@@ -171,5 +177,204 @@ export function createSyncRoutes({ syncService, projectManager }: SyncRouteDeps)
|
|
|
171
177
|
res.json(updated);
|
|
172
178
|
});
|
|
173
179
|
|
|
180
|
+
// ─── Project Creation (Frontend Setup) ───────────────────
|
|
181
|
+
|
|
182
|
+
/** POST /projects/browse — open native folder picker (macOS) */
|
|
183
|
+
router.post('/projects/browse', (_req, res) => {
|
|
184
|
+
if (process.platform !== 'darwin') {
|
|
185
|
+
return res.json({ error: 'not_supported' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
execFile(
|
|
189
|
+
'osascript',
|
|
190
|
+
['-e', 'POSIX path of (choose folder with prompt "Select your project folder")'],
|
|
191
|
+
{ timeout: 60_000 },
|
|
192
|
+
(err, stdout) => {
|
|
193
|
+
if (err) {
|
|
194
|
+
// User pressed Cancel — osascript exits with code 1
|
|
195
|
+
if (err.code === 1) {
|
|
196
|
+
return res.json({ cancelled: true });
|
|
197
|
+
}
|
|
198
|
+
return res.json({ error: err.message });
|
|
199
|
+
}
|
|
200
|
+
const selectedPath = stdout.trim();
|
|
201
|
+
if (!selectedPath) return res.json({ cancelled: true });
|
|
202
|
+
res.json({ path: selectedPath });
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/** POST /projects/check-path — validate a path and detect git status */
|
|
208
|
+
router.post('/projects/check-path', (req, res) => {
|
|
209
|
+
const { path: rawPath } = req.body as { path: string };
|
|
210
|
+
if (!rawPath || !rawPath.trim()) {
|
|
211
|
+
return res.json({ valid: false, error: 'Path is required' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const absPath = path.resolve(rawPath.trim());
|
|
215
|
+
|
|
216
|
+
if (!fs.existsSync(absPath)) {
|
|
217
|
+
return res.json({ valid: false, absPath, error: 'Directory does not exist' });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const stat = fs.statSync(absPath);
|
|
222
|
+
if (!stat.isDirectory()) {
|
|
223
|
+
return res.json({ valid: false, absPath, error: 'Path must be a directory' });
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
return res.json({ valid: false, absPath, error: 'Cannot access path' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const hasGit = fs.existsSync(path.join(absPath, '.git'));
|
|
230
|
+
const suggestedName = path.basename(absPath)
|
|
231
|
+
.replace(/[-_]/g, ' ')
|
|
232
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
233
|
+
|
|
234
|
+
// Check if already registered
|
|
235
|
+
const config = loadGlobalConfig();
|
|
236
|
+
const alreadyRegistered = config.projects.some(p => p.path === absPath);
|
|
237
|
+
|
|
238
|
+
res.json({
|
|
239
|
+
valid: true,
|
|
240
|
+
absPath,
|
|
241
|
+
hasGit,
|
|
242
|
+
suggestedName,
|
|
243
|
+
alreadyRegistered,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/** POST /projects/create — full project initialization (init + register + seed) */
|
|
248
|
+
router.post('/projects/create', async (req, res) => {
|
|
249
|
+
const { path: rawPath, name, color, preset, initGit } = req.body as {
|
|
250
|
+
path: string;
|
|
251
|
+
name: string;
|
|
252
|
+
color: string;
|
|
253
|
+
preset?: string;
|
|
254
|
+
initGit?: boolean;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (!rawPath || !name || !color) {
|
|
258
|
+
return res.status(400).json({ error: 'path, name, and color are required' });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const absPath = path.resolve(rawPath.trim());
|
|
262
|
+
|
|
263
|
+
// Validate directory exists
|
|
264
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
|
|
265
|
+
return res.status(400).json({ error: 'Path must be an existing directory' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check if already registered
|
|
269
|
+
const config = loadGlobalConfig();
|
|
270
|
+
if (config.projects.some(p => p.path === absPath)) {
|
|
271
|
+
return res.status(409).json({ error: 'A project is already registered at this path' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
// 1. Optional git init
|
|
276
|
+
if (initGit && !fs.existsSync(path.join(absPath, '.git'))) {
|
|
277
|
+
await new Promise<void>((resolve, reject) => {
|
|
278
|
+
execFile('git', ['init'], { cwd: absPath, timeout: 10_000 }, (err) => {
|
|
279
|
+
if (err) reject(new Error(`git init failed: ${err.message}`));
|
|
280
|
+
else resolve();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. Run full project init (templates, hooks, skills, agents, config, etc.)
|
|
286
|
+
const selectedPreset = preset || 'default';
|
|
287
|
+
runInit(absPath, {
|
|
288
|
+
quiet: true,
|
|
289
|
+
preset: selectedPreset,
|
|
290
|
+
projectName: name,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// 3. Stamp template version
|
|
294
|
+
const pkgVersion = getPackageVersion();
|
|
295
|
+
const configPath = path.join(absPath, '.claude', 'orbital.config.json');
|
|
296
|
+
if (fs.existsSync(configPath)) {
|
|
297
|
+
try {
|
|
298
|
+
const projConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
299
|
+
projConfig.templateVersion = pkgVersion;
|
|
300
|
+
const tmp = configPath + `.tmp.${process.pid}`;
|
|
301
|
+
fs.writeFileSync(tmp, JSON.stringify(projConfig, null, 2) + '\n', 'utf8');
|
|
302
|
+
fs.renameSync(tmp, configPath);
|
|
303
|
+
} catch { /* ignore malformed config */ }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 4. Register + initialize context + emit socket event
|
|
307
|
+
const summary = await projectManager.addProject(absPath, { name, color });
|
|
308
|
+
|
|
309
|
+
// 5. Seed welcome scope card
|
|
310
|
+
seedWelcomeCard(absPath, selectedPreset);
|
|
311
|
+
|
|
312
|
+
res.status(201).json(summary);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
174
318
|
return router;
|
|
175
319
|
}
|
|
320
|
+
|
|
321
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
function seedWelcomeCard(projectRoot: string, preset: string): void {
|
|
324
|
+
// Determine the planning directory from the preset
|
|
325
|
+
const presetsDir = path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), 'templates', 'presets');
|
|
326
|
+
let planningDir = 'planning'; // default fallback
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const presetPath = path.join(presetsDir, `${preset}.json`);
|
|
330
|
+
if (fs.existsSync(presetPath)) {
|
|
331
|
+
const workflow = JSON.parse(fs.readFileSync(presetPath, 'utf8'));
|
|
332
|
+
// Find the first list that has a directory and isn't an entry point
|
|
333
|
+
const entryId = workflow.entryPoint;
|
|
334
|
+
const entryList = workflow.lists?.find((l: { id: string }) => l.id === entryId);
|
|
335
|
+
// Use the first forward target of the entry point, or the second list
|
|
336
|
+
if (entryList?.forwardTargets?.[0]) {
|
|
337
|
+
planningDir = entryList.forwardTargets[0];
|
|
338
|
+
} else if (workflow.lists?.[1]?.hasDirectory) {
|
|
339
|
+
planningDir = workflow.lists[1].id;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch { /* use default */ }
|
|
343
|
+
|
|
344
|
+
const scopesDir = path.join(projectRoot, 'scopes', planningDir);
|
|
345
|
+
if (!fs.existsSync(scopesDir)) {
|
|
346
|
+
fs.mkdirSync(scopesDir, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const cardPath = path.join(scopesDir, '001-welcome.md');
|
|
350
|
+
if (fs.existsSync(cardPath)) return; // don't overwrite
|
|
351
|
+
|
|
352
|
+
const content = `---
|
|
353
|
+
title: Welcome to Orbital Command
|
|
354
|
+
status: ${planningDir}
|
|
355
|
+
priority: low
|
|
356
|
+
tags: [onboarding]
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
# Welcome to Orbital Command
|
|
360
|
+
|
|
361
|
+
Your project is set up and ready to go. Here are some things to try:
|
|
362
|
+
|
|
363
|
+
## Getting Started
|
|
364
|
+
|
|
365
|
+
1. **Create a scope** — Scopes are units of work. Use \`/scope-create\` in Claude Code or create a markdown file in the \`scopes/\` directory.
|
|
366
|
+
2. **Explore the dashboard** — Use the sidebar to navigate between views: Kanban, Primitives, Guards, Repo, Sessions, and Workflow.
|
|
367
|
+
3. **Launch a session** — Start a Claude Code session from the Sessions view to begin working on this scope.
|
|
368
|
+
|
|
369
|
+
## Key Concepts
|
|
370
|
+
|
|
371
|
+
- **Scopes** are markdown files with YAML frontmatter that track units of work
|
|
372
|
+
- **Workflow** defines the columns and transitions on your Kanban board
|
|
373
|
+
- **Guards** enforce quality gates before status transitions
|
|
374
|
+
- **Primitives** are the hooks, skills, and agents that power your setup
|
|
375
|
+
|
|
376
|
+
You can archive this card once you're comfortable with the basics.
|
|
377
|
+
`;
|
|
378
|
+
|
|
379
|
+
fs.writeFileSync(cardPath, content, 'utf8');
|
|
380
|
+
}
|