@vibecompass/vibecompass 0.14.0 → 0.16.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/CHANGELOG.md +183 -0
- package/README.md +111 -8
- package/package.json +1 -1
- package/src/brief-keywords.js +203 -0
- package/src/brief-lifecycle.js +638 -0
- package/src/brief-render.js +341 -0
- package/src/brief-settings.js +160 -0
- package/src/brief.js +1557 -0
- package/src/cli.js +731 -7
- package/src/credential-store.js +856 -0
- package/src/decision-lineage.js +1717 -0
- package/src/docs-review.js +28 -4
- package/src/generators/agent-files/template.js +2 -1
- package/src/index.js +44 -0
- package/src/login.js +257 -0
- package/src/mode-transition.js +8 -2
- package/src/project-memory.js +30 -4
- package/src/read-model.js +92 -2
- package/src/scaffold.js +10 -2
- package/src/secret-prompt.js +64 -0
- package/src/session.js +30 -0
- package/src/status.js +54 -9
- package/src/sync.js +12 -4
package/src/cli.js
CHANGED
|
@@ -5,6 +5,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
5
5
|
import { connectHostedProjectMemory, initializeProjectMemory, setDefaultSyncTarget } from './init.js';
|
|
6
6
|
import { preflightDocsReview } from './docs-review.js';
|
|
7
7
|
import { planDocsUpdate, renderDocsUpdatePlan } from './docs-update.js';
|
|
8
|
+
import { buildSessionBrief, resolveBriefRequest } from './brief.js';
|
|
9
|
+
import { generateLaneBrief, inspectLaneBrief } from './brief-lifecycle.js';
|
|
10
|
+
import { renderBrief } from './brief-render.js';
|
|
8
11
|
import { resolveConnectHostedCliOptions, resolveInitCliOptions } from './setup.js';
|
|
9
12
|
import { readFile } from 'node:fs/promises';
|
|
10
13
|
import path from 'node:path';
|
|
@@ -18,6 +21,17 @@ import { refreshWorkflow } from './refresh-workflow.js';
|
|
|
18
21
|
import { inspectProjectCompatibility, formatCompatibilityWarnings } from './compatibility.js';
|
|
19
22
|
import { PACKAGE_VERSION } from './version.js';
|
|
20
23
|
import { demoteHosted, promoteHosted } from './mode-transition.js';
|
|
24
|
+
import { loginHosted } from './login.js';
|
|
25
|
+
import {
|
|
26
|
+
credentialStoreKey,
|
|
27
|
+
credentialStoreOptionsFrom,
|
|
28
|
+
listSyncCredentials,
|
|
29
|
+
removeSyncCredential,
|
|
30
|
+
storeSyncCredential,
|
|
31
|
+
} from './credential-store.js';
|
|
32
|
+
import { canPromptSecret, promptSecret, readSecretFromStdin } from './secret-prompt.js';
|
|
33
|
+
import { resolveSyncBinding, readSyncTargets } from './sync-binding.js';
|
|
34
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
21
35
|
import {
|
|
22
36
|
adoptRemoteHead,
|
|
23
37
|
applyPullExport,
|
|
@@ -71,7 +85,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
71
85
|
if (result.syncEnvVar) {
|
|
72
86
|
const rootFlag = formatOptionalRootFlag(initPlan.initOptions.rootDir);
|
|
73
87
|
io.stdout.write(`Hosted binding: configured for ${initPlan.initOptions.mode}\n`);
|
|
74
|
-
io.stdout.write(`Next step:
|
|
88
|
+
io.stdout.write(`Next step: sign in with "vibecompass login${rootFlag}" (opens the dashboard and stores the sync token locally).\n`);
|
|
75
89
|
if (initPlan.initOptions.mode === 'local-primary') {
|
|
76
90
|
io.stdout.write(`Then run: vibecompass push${rootFlag}\n`);
|
|
77
91
|
io.stdout.write(`Hosted docs-review: vibecompass docs-review --submit-hosted${rootFlag}\n`);
|
|
@@ -156,6 +170,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
156
170
|
io.stdout.write(`Runtime: port ${result.runtime.port}, temp dir ${result.runtime.tmpDir}\n`);
|
|
157
171
|
io.stdout.write('Export into a shell with: eval "$(vibecompass lane-env)" (D-282; pass --session <lane-id> when multiple lanes are active; use --worktree for independently runnable same-repo lanes).\n');
|
|
158
172
|
}
|
|
173
|
+
writeLifecycleBriefResult(io, result.brief);
|
|
159
174
|
writeWarnings(io, result.warnings);
|
|
160
175
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
161
176
|
return 0;
|
|
@@ -204,7 +219,22 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
204
219
|
io.stdout.write(`Sync target: ${result.syncTarget.name} (default: ${result.syncTarget.defaultTarget})\n`);
|
|
205
220
|
io.stdout.write(`Select per command with --sync-target ${result.syncTarget.name}, or switch the default with: vibecompass sync-target ${result.syncTarget.name}\n`);
|
|
206
221
|
}
|
|
207
|
-
|
|
222
|
+
const capture = await captureSyncTokenAfterConnect(io, runtime, {
|
|
223
|
+
binding: {
|
|
224
|
+
apiUrl: connectPlan.sync.apiUrl,
|
|
225
|
+
projectId: connectPlan.sync.projectId,
|
|
226
|
+
credentialEnvVar: result.syncEnvVar,
|
|
227
|
+
target: result.syncTarget?.name ?? null,
|
|
228
|
+
},
|
|
229
|
+
rootDir: parsed.options.rootDir,
|
|
230
|
+
tokenStdin: Boolean(parsed.options.tokenStdin),
|
|
231
|
+
noStore: Boolean(parsed.options.noStore),
|
|
232
|
+
credentialStore: parsed.options.credentialStore,
|
|
233
|
+
rootFlag: formatOptionalRootFlag(parsed.options.rootDir),
|
|
234
|
+
});
|
|
235
|
+
if (capture?.viaLogin) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
208
238
|
if (result.mode === 'local-primary') {
|
|
209
239
|
io.stdout.write('Then run: vibecompass push\n');
|
|
210
240
|
io.stdout.write('Hosted docs-review: vibecompass docs-review --submit-hosted\n');
|
|
@@ -214,6 +244,22 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
214
244
|
return 0;
|
|
215
245
|
}
|
|
216
246
|
|
|
247
|
+
if (parsed.command === 'login') {
|
|
248
|
+
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
249
|
+
const result = await loginHosted(parsed.options, {
|
|
250
|
+
cwd: runtime.cwd,
|
|
251
|
+
env: runtime.env,
|
|
252
|
+
io,
|
|
253
|
+
runtime,
|
|
254
|
+
});
|
|
255
|
+
writeLoginResult(io, result);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (parsed.command === 'sync-credential') {
|
|
260
|
+
return runSyncCredentialCommand(io, runtime, parsed.options);
|
|
261
|
+
}
|
|
262
|
+
|
|
217
263
|
if (parsed.command === 'sync-target') {
|
|
218
264
|
const result = await setDefaultSyncTarget({
|
|
219
265
|
...parsed.options,
|
|
@@ -272,6 +318,94 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
272
318
|
return 0;
|
|
273
319
|
}
|
|
274
320
|
|
|
321
|
+
if (parsed.command === 'brief' && (parsed.options.write || parsed.options.check)) {
|
|
322
|
+
// Lane brief (D-364): --write regenerates the lane's brief.md under the
|
|
323
|
+
// lane root's lock; --check reports its freshness and writes nothing. A
|
|
324
|
+
// --source-root is only ever read.
|
|
325
|
+
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
326
|
+
const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
|
|
327
|
+
const request = await resolveBriefRequest({
|
|
328
|
+
rootDir: parsed.options.rootDir,
|
|
329
|
+
sessionId: parsed.options.sessionId,
|
|
330
|
+
cwd,
|
|
331
|
+
});
|
|
332
|
+
if (!request.laneId) {
|
|
333
|
+
throw new Error(`brief ${parsed.options.write ? '--write' : '--check'} needs an active lane: pass --session <lane-id>, or run it from a lane worktree.`);
|
|
334
|
+
}
|
|
335
|
+
const sourceRootDir = parsed.options.sourceRootDir ? path.resolve(cwd, parsed.options.sourceRootDir) : undefined;
|
|
336
|
+
if (parsed.options.check) {
|
|
337
|
+
const inspection = await inspectLaneBrief({
|
|
338
|
+
rootDir: request.rootDir,
|
|
339
|
+
laneId: request.laneId,
|
|
340
|
+
sourceRootDir,
|
|
341
|
+
sourceSessionId: parsed.options.sourceSessionId,
|
|
342
|
+
});
|
|
343
|
+
if (parsed.options.json) {
|
|
344
|
+
io.stdout.write(`${JSON.stringify({
|
|
345
|
+
lane_id: request.laneId,
|
|
346
|
+
path: inspection.path,
|
|
347
|
+
exists: inspection.exists,
|
|
348
|
+
stale: inspection.stale,
|
|
349
|
+
reasons: inspection.reasons,
|
|
350
|
+
status: inspection.status,
|
|
351
|
+
generated_at: inspection.generatedAt,
|
|
352
|
+
}, null, 2)}\n`);
|
|
353
|
+
} else {
|
|
354
|
+
const state = !inspection.exists ? 'missing' : inspection.stale ? 'stale' : 'current';
|
|
355
|
+
io.stdout.write(`Brief: ${state}${inspection.status ? ` (status ${inspection.status}${inspection.generatedAt ? `, generated ${inspection.generatedAt}` : ''})` : ''} — ${inspection.path}\n`);
|
|
356
|
+
for (const reason of inspection.exists ? inspection.reasons : []) {
|
|
357
|
+
io.stdout.write(`- ${reason}\n`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
writeWarnings(io, request.warnings);
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const generated = await generateLaneBrief({
|
|
365
|
+
rootDir: request.rootDir,
|
|
366
|
+
laneId: request.laneId,
|
|
367
|
+
sourceRootDir,
|
|
368
|
+
sourceSessionId: parsed.options.sourceSessionId,
|
|
369
|
+
...(parsed.options.budget !== undefined ? { budget: parsed.options.budget } : {}),
|
|
370
|
+
});
|
|
371
|
+
if (parsed.options.json) {
|
|
372
|
+
io.stdout.write(`${JSON.stringify({ lane_id: request.laneId, ...generated }, null, 2)}\n`);
|
|
373
|
+
} else {
|
|
374
|
+
const from = generated.source?.adapter ? ` from ${generated.source.root_dir}` : '';
|
|
375
|
+
io.stdout.write(`Brief: ${generated.status}${generated.state === 'written' ? '' : ` (${generated.state}: ${generated.reason})`} — wrote ${generated.path}${from}\n`);
|
|
376
|
+
}
|
|
377
|
+
writeWarnings(io, [...request.warnings, ...generated.warnings]);
|
|
378
|
+
return generated.state === 'written' ? 0 : 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (parsed.command === 'brief') {
|
|
382
|
+
// Strictly read-only (plan task A3): no manifest refresh, state write,
|
|
383
|
+
// lane write, or agent-file sync, so it is safe against a legacy root.
|
|
384
|
+
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
385
|
+
const request = await resolveBriefRequest({
|
|
386
|
+
rootDir: parsed.options.rootDir,
|
|
387
|
+
sessionId: parsed.options.sessionId,
|
|
388
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
389
|
+
});
|
|
390
|
+
const result = await buildSessionBrief({
|
|
391
|
+
rootDir: request.rootDir,
|
|
392
|
+
laneId: request.laneId,
|
|
393
|
+
task: parsed.options.task,
|
|
394
|
+
files: parsed.options.files,
|
|
395
|
+
featureSlugs: parsed.options.featureSlugs,
|
|
396
|
+
claims: parsed.options.claims,
|
|
397
|
+
budget: parsed.options.budget,
|
|
398
|
+
});
|
|
399
|
+
const markdown = renderBrief(result);
|
|
400
|
+
if (parsed.options.json) {
|
|
401
|
+
io.stdout.write(`${JSON.stringify({ ...result, lane_source: request.laneSource, cli_warnings: request.warnings, markdown }, null, 2)}\n`);
|
|
402
|
+
} else {
|
|
403
|
+
io.stdout.write(markdown);
|
|
404
|
+
}
|
|
405
|
+
writeWarnings(io, request.warnings);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
275
409
|
if (parsed.command === 'close-session' || parsed.command === 'end-session') {
|
|
276
410
|
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
277
411
|
const result = await closeProjectSession({
|
|
@@ -318,6 +452,9 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
318
452
|
io.stdout.write('Read next:\n');
|
|
319
453
|
io.stdout.write(`- ${result.wipFilePath}\n`);
|
|
320
454
|
io.stdout.write(`- ${result.handoffFilePath}\n`);
|
|
455
|
+
if (result.brief?.path && result.brief.state !== 'off' && !(result.brief.state === 'failed' && !result.brief.status)) {
|
|
456
|
+
io.stdout.write(`- ${result.brief.path} (session brief — read before planning)\n`);
|
|
457
|
+
}
|
|
321
458
|
if (result.latestFinalizedSessionPath) {
|
|
322
459
|
io.stdout.write(`- ${result.latestFinalizedSessionPath} (latest finalized note)\n`);
|
|
323
460
|
}
|
|
@@ -335,6 +472,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
335
472
|
io.stdout.write(`- ${lane.id}: ${lane.workingOn ?? 'No summary recorded'}\n`);
|
|
336
473
|
}
|
|
337
474
|
}
|
|
475
|
+
writeLifecycleBriefResult(io, result.brief);
|
|
338
476
|
}
|
|
339
477
|
writeWarnings(io, result.warnings);
|
|
340
478
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
@@ -762,6 +900,14 @@ export function parseCliArgs(argv) {
|
|
|
762
900
|
return parseSyncTargetArgs(rest);
|
|
763
901
|
}
|
|
764
902
|
|
|
903
|
+
if (command === 'login') {
|
|
904
|
+
return parseLoginArgs(rest);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (command === 'sync-credential') {
|
|
908
|
+
return parseSyncCredentialArgs(rest);
|
|
909
|
+
}
|
|
910
|
+
|
|
765
911
|
if (command === 'status') {
|
|
766
912
|
return parseStatusArgs(rest);
|
|
767
913
|
}
|
|
@@ -774,6 +920,10 @@ export function parseCliArgs(argv) {
|
|
|
774
920
|
return parseDocsUpdateArgs(rest);
|
|
775
921
|
}
|
|
776
922
|
|
|
923
|
+
if (command === 'brief') {
|
|
924
|
+
return parseBriefArgs(rest);
|
|
925
|
+
}
|
|
926
|
+
|
|
777
927
|
if (command === 'close-session' || command === 'end-session') {
|
|
778
928
|
return parseCloseSessionArgs(rest);
|
|
779
929
|
}
|
|
@@ -1050,6 +1200,10 @@ function parseStartSessionArgs(argv) {
|
|
|
1050
1200
|
if (token.startsWith('--worktree=')) {
|
|
1051
1201
|
throw new Error('Flag "--worktree" takes no value; the worktree container is fixed to <workspace>/worktrees/<lane-id> (D-279).');
|
|
1052
1202
|
}
|
|
1203
|
+
if (token === '--no-brief') {
|
|
1204
|
+
parsed.brief = false;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1053
1207
|
|
|
1054
1208
|
const value = argv[index + 1];
|
|
1055
1209
|
if (value === undefined) {
|
|
@@ -1162,6 +1316,16 @@ function parseConnectHostedArgs(argv) {
|
|
|
1162
1316
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1163
1317
|
const token = argv[index];
|
|
1164
1318
|
|
|
1319
|
+
if (token === '--token-stdin') {
|
|
1320
|
+
parsed.tokenStdin = true;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (token === '--no-store') {
|
|
1325
|
+
parsed.noStore = true;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1165
1329
|
if (!token.startsWith('--')) {
|
|
1166
1330
|
throw new Error(`Unexpected argument "${token}".`);
|
|
1167
1331
|
}
|
|
@@ -1188,6 +1352,9 @@ function parseConnectHostedArgs(argv) {
|
|
|
1188
1352
|
case '--sync-credential-env-var':
|
|
1189
1353
|
parsed.syncCredentialEnvVar = value;
|
|
1190
1354
|
break;
|
|
1355
|
+
case '--credential-store':
|
|
1356
|
+
parsed.credentialStore = value;
|
|
1357
|
+
break;
|
|
1191
1358
|
default:
|
|
1192
1359
|
throw new Error(`Unknown flag "${token}".`);
|
|
1193
1360
|
}
|
|
@@ -1203,6 +1370,9 @@ function parseConnectHostedArgs(argv) {
|
|
|
1203
1370
|
command: 'connect-hosted',
|
|
1204
1371
|
options: {
|
|
1205
1372
|
rootDir: parsed.rootDir,
|
|
1373
|
+
...(parsed.tokenStdin ? { tokenStdin: true } : {}),
|
|
1374
|
+
...(parsed.noStore ? { noStore: true } : {}),
|
|
1375
|
+
...(parsed.credentialStore ? { credentialStore: parsed.credentialStore } : {}),
|
|
1206
1376
|
...(parsed.targetName ? { targetName: parsed.targetName } : {}),
|
|
1207
1377
|
...(syncValues.length > 0
|
|
1208
1378
|
? {
|
|
@@ -1217,6 +1387,363 @@ function parseConnectHostedArgs(argv) {
|
|
|
1217
1387
|
};
|
|
1218
1388
|
}
|
|
1219
1389
|
|
|
1390
|
+
function parseLoginArgs(argv) {
|
|
1391
|
+
const parsed = {};
|
|
1392
|
+
|
|
1393
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1394
|
+
const token = argv[index];
|
|
1395
|
+
|
|
1396
|
+
if (token === '--no-browser') {
|
|
1397
|
+
parsed.noBrowser = true;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (!token.startsWith('--')) {
|
|
1402
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const value = argv[index + 1];
|
|
1406
|
+
if (value === undefined) {
|
|
1407
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1408
|
+
}
|
|
1409
|
+
index += 1;
|
|
1410
|
+
|
|
1411
|
+
switch (token) {
|
|
1412
|
+
case '--root':
|
|
1413
|
+
parsed.rootDir = value;
|
|
1414
|
+
break;
|
|
1415
|
+
case '--sync-target':
|
|
1416
|
+
parsed.syncTarget = value;
|
|
1417
|
+
break;
|
|
1418
|
+
case '--api-url':
|
|
1419
|
+
parsed.apiUrl = value;
|
|
1420
|
+
break;
|
|
1421
|
+
case '--project-id':
|
|
1422
|
+
parsed.projectId = value;
|
|
1423
|
+
break;
|
|
1424
|
+
case '--sync-credential-env-var':
|
|
1425
|
+
parsed.credentialEnvVar = value;
|
|
1426
|
+
break;
|
|
1427
|
+
case '--label':
|
|
1428
|
+
parsed.label = value;
|
|
1429
|
+
break;
|
|
1430
|
+
case '--credential-store':
|
|
1431
|
+
parsed.credentialStore = value;
|
|
1432
|
+
break;
|
|
1433
|
+
case '--poll-timeout': {
|
|
1434
|
+
const seconds = Number(value);
|
|
1435
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
1436
|
+
throw new Error('--poll-timeout requires a positive number of seconds.');
|
|
1437
|
+
}
|
|
1438
|
+
parsed.pollTimeoutSeconds = seconds;
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
default:
|
|
1442
|
+
throw new Error(`Unknown flag "${token}" for login.`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
return { command: 'login', options: parsed };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const SYNC_CREDENTIAL_ACTIONS = new Set(['set', 'remove', 'list']);
|
|
1450
|
+
|
|
1451
|
+
function parseSyncCredentialArgs(argv) {
|
|
1452
|
+
const parsed = { action: null };
|
|
1453
|
+
|
|
1454
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1455
|
+
const token = argv[index];
|
|
1456
|
+
|
|
1457
|
+
if (token === '--token-stdin') {
|
|
1458
|
+
parsed.tokenStdin = true;
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (token === '--from-env') {
|
|
1462
|
+
parsed.fromEnv = true;
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (token === '--json') {
|
|
1466
|
+
parsed.json = true;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (!token.startsWith('--')) {
|
|
1471
|
+
if (parsed.action !== null) {
|
|
1472
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
1473
|
+
}
|
|
1474
|
+
if (!SYNC_CREDENTIAL_ACTIONS.has(token)) {
|
|
1475
|
+
throw new Error(`Unknown sync-credential action "${token}". Use set, remove, or list.`);
|
|
1476
|
+
}
|
|
1477
|
+
parsed.action = token;
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const value = argv[index + 1];
|
|
1482
|
+
if (value === undefined) {
|
|
1483
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1484
|
+
}
|
|
1485
|
+
index += 1;
|
|
1486
|
+
|
|
1487
|
+
switch (token) {
|
|
1488
|
+
case '--root':
|
|
1489
|
+
parsed.rootDir = value;
|
|
1490
|
+
break;
|
|
1491
|
+
case '--sync-target':
|
|
1492
|
+
parsed.syncTarget = value;
|
|
1493
|
+
break;
|
|
1494
|
+
case '--api-url':
|
|
1495
|
+
parsed.apiUrl = value;
|
|
1496
|
+
break;
|
|
1497
|
+
case '--project-id':
|
|
1498
|
+
parsed.projectId = value;
|
|
1499
|
+
break;
|
|
1500
|
+
case '--label':
|
|
1501
|
+
parsed.label = value;
|
|
1502
|
+
break;
|
|
1503
|
+
case '--credential-store':
|
|
1504
|
+
parsed.credentialStore = value;
|
|
1505
|
+
break;
|
|
1506
|
+
default:
|
|
1507
|
+
throw new Error(`Unknown flag "${token}" for sync-credential.`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (!parsed.action) {
|
|
1512
|
+
throw new Error('sync-credential requires an action: set, remove, or list.');
|
|
1513
|
+
}
|
|
1514
|
+
if ((parsed.apiUrl && !parsed.projectId) || (!parsed.apiUrl && parsed.projectId)) {
|
|
1515
|
+
throw new Error('--api-url and --project-id must be given together.');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return { command: 'sync-credential', options: parsed };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
async function readRootSyncBindings(cwd, rootDirOption) {
|
|
1522
|
+
const rootDir = path.resolve(cwd, rootDirOption ?? '.compass');
|
|
1523
|
+
const projectFilePath = path.join(rootDir, 'project.yaml');
|
|
1524
|
+
let project;
|
|
1525
|
+
try {
|
|
1526
|
+
project = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), { sourceName: projectFilePath });
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
1529
|
+
return { rootDir, project: null, bindings: [] };
|
|
1530
|
+
}
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
const named = readSyncTargets(project);
|
|
1534
|
+
const bindings = [];
|
|
1535
|
+
if (named) {
|
|
1536
|
+
for (const name of Object.keys(named.targets)) {
|
|
1537
|
+
try {
|
|
1538
|
+
bindings.push(resolveSyncBinding(project, name));
|
|
1539
|
+
} catch {
|
|
1540
|
+
// incomplete target; skip in listings
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
} else {
|
|
1544
|
+
const flat = resolveSyncBinding(project, null);
|
|
1545
|
+
if (flat) bindings.push(flat);
|
|
1546
|
+
}
|
|
1547
|
+
return { rootDir, project, bindings };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
async function resolveSyncCredentialBinding(runtime, options) {
|
|
1551
|
+
if (options.apiUrl && options.projectId) {
|
|
1552
|
+
return {
|
|
1553
|
+
target: null,
|
|
1554
|
+
apiUrl: options.apiUrl,
|
|
1555
|
+
projectId: options.projectId,
|
|
1556
|
+
credentialEnvVar: 'VIBECOMPASS_SYNC_TOKEN',
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
|
|
1560
|
+
const { rootDir, project } = await readRootSyncBindings(cwd, options.rootDir);
|
|
1561
|
+
if (!project) {
|
|
1562
|
+
throw new Error(`No project.yaml found in ${rootDir}. Run "vibecompass login" from an initialized root, or pass --api-url and --project-id.`);
|
|
1563
|
+
}
|
|
1564
|
+
const binding = resolveSyncBinding(project, options.syncTarget ?? null);
|
|
1565
|
+
if (!binding) {
|
|
1566
|
+
throw new Error('This root has no hosted sync binding yet. Run "vibecompass login" (recommended) or "vibecompass connect-hosted", or pass --api-url and --project-id.');
|
|
1567
|
+
}
|
|
1568
|
+
return binding;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function writeLoginResult(io, result) {
|
|
1572
|
+
io.stdout.write(`Signed in to ${result.apiUrl} for project ${result.project.name} (${result.project.id}).\n`);
|
|
1573
|
+
writeStoredCredentialLines(io, result.stored, {
|
|
1574
|
+
apiUrl: result.apiUrl,
|
|
1575
|
+
projectId: result.project.id,
|
|
1576
|
+
credentialEnvVar: result.binding?.syncEnvVar ?? result.previousBinding?.credentialEnvVar ?? 'VIBECOMPASS_SYNC_TOKEN',
|
|
1577
|
+
});
|
|
1578
|
+
if (result.binding) {
|
|
1579
|
+
io.stdout.write(`Connected hosted VibeCompass for ${result.binding.mode}\n`);
|
|
1580
|
+
if (result.binding.modeChanged) {
|
|
1581
|
+
io.stdout.write(`Project mode: ${result.binding.previousMode} -> ${result.binding.mode}\n`);
|
|
1582
|
+
}
|
|
1583
|
+
io.stdout.write(`Updated ${result.binding.projectFilePath}\n`);
|
|
1584
|
+
if (result.binding.syncTarget) {
|
|
1585
|
+
io.stdout.write(`Sync target: ${result.binding.syncTarget.name} (default: ${result.binding.syncTarget.defaultTarget})\n`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
for (const warning of result.warnings) {
|
|
1589
|
+
io.stdout.write(`WARNING: ${warning}\n`);
|
|
1590
|
+
}
|
|
1591
|
+
const targetFlag = result.previousBinding?.target ?? result.binding?.syncTarget?.name;
|
|
1592
|
+
const targetSuffix = targetFlag ? ` --sync-target ${targetFlag}` : '';
|
|
1593
|
+
if (result.mode === 'local-primary') {
|
|
1594
|
+
io.stdout.write(`Then run: vibecompass push${targetSuffix}\n`);
|
|
1595
|
+
}
|
|
1596
|
+
io.stdout.write(`Hosted docs-review: vibecompass docs-review --submit-hosted${targetSuffix}\n`);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function writeStoredCredentialLines(io, stored, binding) {
|
|
1600
|
+
for (const warning of stored.warnings ?? []) {
|
|
1601
|
+
io.stdout.write(`WARNING: ${warning}\n`);
|
|
1602
|
+
}
|
|
1603
|
+
const where = stored.backend === 'keychain' ? `the ${stored.backendDescription}` : stored.storePath;
|
|
1604
|
+
io.stdout.write(`Stored the sync token for ${binding.apiUrl} (project ${binding.projectId}) in ${where}${stored.replaced ? ', replacing the previous entry' : ''}.\n`);
|
|
1605
|
+
io.stdout.write(`Hosted commands find it automatically in every terminal; ${binding.credentialEnvVar} still overrides it per shell.\n`);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
async function captureSyncTokenAfterConnect(io, runtime, spec) {
|
|
1609
|
+
const { binding } = spec;
|
|
1610
|
+
const env = runtime.env ?? process.env;
|
|
1611
|
+
const targetFlag = binding.target ? ` --sync-target ${binding.target}` : '';
|
|
1612
|
+
const loginHint = `vibecompass login${spec.rootFlag}${targetFlag}`;
|
|
1613
|
+
|
|
1614
|
+
if (spec.noStore) {
|
|
1615
|
+
io.stdout.write(`Token not stored (--no-store). Set ${binding.credentialEnvVar} in every shell that runs hosted commands, or later run: ${loginHint}\n`);
|
|
1616
|
+
return null;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
let token = null;
|
|
1620
|
+
if (spec.tokenStdin) {
|
|
1621
|
+
token = await readSecretFromStdin(io, runtime);
|
|
1622
|
+
if (!token) {
|
|
1623
|
+
throw new Error('--token-stdin was given but no token arrived on stdin.');
|
|
1624
|
+
}
|
|
1625
|
+
} else if (typeof env[binding.credentialEnvVar] === 'string' && env[binding.credentialEnvVar].trim() !== '') {
|
|
1626
|
+
io.stdout.write(`${binding.credentialEnvVar} is set in this shell and takes precedence for hosted commands. Persist it for new terminals with: vibecompass sync-credential set${spec.rootFlag}${targetFlag} --from-env\n`);
|
|
1627
|
+
return null;
|
|
1628
|
+
} else if (canPromptSecret(io, runtime)) {
|
|
1629
|
+
// D-356: the recommended path is the browser sign-in. A pasted dashboard
|
|
1630
|
+
// token is the advanced alternative; Enter falls through to login.
|
|
1631
|
+
token = await promptSecret('Press Enter to sign in with your browser, or paste a dashboard token (input hidden)', io, runtime);
|
|
1632
|
+
if (!token) {
|
|
1633
|
+
const result = await loginHosted(
|
|
1634
|
+
{
|
|
1635
|
+
rootDir: spec.rootDir,
|
|
1636
|
+
...(binding.target ? { syncTarget: binding.target } : {}),
|
|
1637
|
+
...(spec.credentialStore ? { credentialStore: spec.credentialStore } : {}),
|
|
1638
|
+
},
|
|
1639
|
+
{ cwd: runtime.cwd, env, io, runtime },
|
|
1640
|
+
);
|
|
1641
|
+
writeLoginResult(io, result);
|
|
1642
|
+
return { viaLogin: true, stored: result.stored };
|
|
1643
|
+
}
|
|
1644
|
+
} else {
|
|
1645
|
+
io.stdout.write(`Next step: sign in with "${loginHint}" (opens the dashboard and stores the sync token locally). CI or scripts: set ${binding.credentialEnvVar}, or pipe a dashboard token into "vibecompass connect-hosted ... --token-stdin".\n`);
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const stored = await storeSyncCredential(
|
|
1650
|
+
{
|
|
1651
|
+
apiUrl: binding.apiUrl,
|
|
1652
|
+
projectId: binding.projectId,
|
|
1653
|
+
token,
|
|
1654
|
+
label: `VibeCompass sync token (${binding.projectId})`,
|
|
1655
|
+
source: 'connect-hosted',
|
|
1656
|
+
backend: spec.credentialStore,
|
|
1657
|
+
},
|
|
1658
|
+
credentialStoreOptionsFrom({ env, runtime }),
|
|
1659
|
+
);
|
|
1660
|
+
writeStoredCredentialLines(io, stored, binding);
|
|
1661
|
+
return stored;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async function runSyncCredentialCommand(io, runtime, options) {
|
|
1665
|
+
const env = runtime.env ?? process.env;
|
|
1666
|
+
const storeOptions = credentialStoreOptionsFrom({ env, runtime });
|
|
1667
|
+
|
|
1668
|
+
if (options.action === 'list') {
|
|
1669
|
+
const listing = await listSyncCredentials(storeOptions);
|
|
1670
|
+
const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
|
|
1671
|
+
const { bindings } = await readRootSyncBindings(cwd, options.rootDir).catch(() => ({ bindings: [] }));
|
|
1672
|
+
const boundKeys = new Map();
|
|
1673
|
+
for (const binding of bindings) {
|
|
1674
|
+
try {
|
|
1675
|
+
boundKeys.set(credentialStoreKey(binding.apiUrl, binding.projectId), binding);
|
|
1676
|
+
} catch {
|
|
1677
|
+
// an unparseable binding URL cannot be annotated; the entry still lists
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (options.json) {
|
|
1681
|
+
io.stdout.write(`${JSON.stringify({ storePath: listing.storePath, entries: listing.entries }, null, 2)}\n`);
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
io.stdout.write(`Credential store: ${listing.storePath} (${listing.entries.length} ${listing.entries.length === 1 ? 'entry' : 'entries'})\n`);
|
|
1685
|
+
if (listing.entries.length === 0) {
|
|
1686
|
+
io.stdout.write('No stored sync tokens. Run "vibecompass login" to sign in from this terminal.\n');
|
|
1687
|
+
return 0;
|
|
1688
|
+
}
|
|
1689
|
+
for (const entry of listing.entries) {
|
|
1690
|
+
const bound = boundKeys.get(entry.key);
|
|
1691
|
+
const boundNote = bound ? (bound.target ? ` — bound here as target ${bound.target}` : ' — bound here') : '';
|
|
1692
|
+
io.stdout.write(
|
|
1693
|
+
`- ${entry.apiUrl} project ${entry.projectId}: ${entry.backend}, prefix ${entry.tokenPrefix ?? '?'}, stored ${entry.storedAt ?? '?'} via ${entry.source ?? '?'}${boundNote}\n`,
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
return 0;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const binding = await resolveSyncCredentialBinding(runtime, options);
|
|
1700
|
+
|
|
1701
|
+
if (options.action === 'remove') {
|
|
1702
|
+
const result = await removeSyncCredential(binding, storeOptions);
|
|
1703
|
+
io.stdout.write(
|
|
1704
|
+
result.removed
|
|
1705
|
+
? `Removed the stored sync token for ${binding.apiUrl} (project ${binding.projectId}) from the ${result.backend === 'keychain' ? 'OS keychain' : 'credential store file'}.\n`
|
|
1706
|
+
: `No stored sync token for ${binding.apiUrl} (project ${binding.projectId}).\n`,
|
|
1707
|
+
);
|
|
1708
|
+
io.stdout.write('Revoke the credential on the hosted dashboard under Setup -> Hosted sync if it should stop working everywhere.\n');
|
|
1709
|
+
return 0;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
let token = null;
|
|
1713
|
+
if (options.tokenStdin) {
|
|
1714
|
+
token = await readSecretFromStdin(io, runtime);
|
|
1715
|
+
if (!token) {
|
|
1716
|
+
throw new Error('--token-stdin was given but no token arrived on stdin.');
|
|
1717
|
+
}
|
|
1718
|
+
} else if (options.fromEnv) {
|
|
1719
|
+
token = typeof env[binding.credentialEnvVar] === 'string' ? env[binding.credentialEnvVar].trim() : '';
|
|
1720
|
+
if (!token) {
|
|
1721
|
+
throw new Error(`--from-env requires ${binding.credentialEnvVar} to be set in this shell.`);
|
|
1722
|
+
}
|
|
1723
|
+
} else if (canPromptSecret(io, runtime)) {
|
|
1724
|
+
token = await promptSecret('Paste the sync token from the dashboard (input hidden)', io, runtime);
|
|
1725
|
+
if (!token) {
|
|
1726
|
+
throw new Error('No token entered.');
|
|
1727
|
+
}
|
|
1728
|
+
} else {
|
|
1729
|
+
throw new Error('sync-credential set needs a token: run it in an interactive terminal, pipe one with --token-stdin, or use --from-env.');
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const stored = await storeSyncCredential(
|
|
1733
|
+
{
|
|
1734
|
+
apiUrl: binding.apiUrl,
|
|
1735
|
+
projectId: binding.projectId,
|
|
1736
|
+
token,
|
|
1737
|
+
label: options.label ?? `VibeCompass sync token (${binding.projectId})`,
|
|
1738
|
+
source: 'sync-credential',
|
|
1739
|
+
backend: options.credentialStore,
|
|
1740
|
+
},
|
|
1741
|
+
storeOptions,
|
|
1742
|
+
);
|
|
1743
|
+
writeStoredCredentialLines(io, stored, binding);
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1220
1747
|
function parseStatusArgs(argv) {
|
|
1221
1748
|
const parsed = {};
|
|
1222
1749
|
|
|
@@ -1434,6 +1961,105 @@ function parseDocsUpdateArgs(argv) {
|
|
|
1434
1961
|
};
|
|
1435
1962
|
}
|
|
1436
1963
|
|
|
1964
|
+
function parseBriefArgs(argv) {
|
|
1965
|
+
const parsed = {
|
|
1966
|
+
files: [],
|
|
1967
|
+
featureSlugs: [],
|
|
1968
|
+
claims: [],
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1972
|
+
const token = argv[index];
|
|
1973
|
+
if (token === '--json') {
|
|
1974
|
+
parsed.json = true;
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (token === '--write' || token === '--check') {
|
|
1978
|
+
parsed[token.slice(2)] = true;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
if (!token.startsWith('--')) {
|
|
1983
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const value = argv[index + 1];
|
|
1987
|
+
if (value === undefined || value.startsWith('--')) {
|
|
1988
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1989
|
+
}
|
|
1990
|
+
index += 1;
|
|
1991
|
+
|
|
1992
|
+
switch (token) {
|
|
1993
|
+
case '--root':
|
|
1994
|
+
parsed.rootDir = value;
|
|
1995
|
+
break;
|
|
1996
|
+
case '--session':
|
|
1997
|
+
parsed.sessionId = value;
|
|
1998
|
+
break;
|
|
1999
|
+
case '--source-root':
|
|
2000
|
+
parsed.sourceRootDir = value;
|
|
2001
|
+
break;
|
|
2002
|
+
case '--source-session':
|
|
2003
|
+
parsed.sourceSessionId = value;
|
|
2004
|
+
break;
|
|
2005
|
+
case '--task':
|
|
2006
|
+
parsed.task = value;
|
|
2007
|
+
break;
|
|
2008
|
+
case '--files':
|
|
2009
|
+
case '--file':
|
|
2010
|
+
// `--files a b c` and repeated `--files a --files b` both work.
|
|
2011
|
+
parsed.files.push(value);
|
|
2012
|
+
while (argv[index + 1] !== undefined && !argv[index + 1].startsWith('--')) {
|
|
2013
|
+
index += 1;
|
|
2014
|
+
parsed.files.push(argv[index]);
|
|
2015
|
+
}
|
|
2016
|
+
break;
|
|
2017
|
+
case '--feature':
|
|
2018
|
+
parsed.featureSlugs.push(value);
|
|
2019
|
+
break;
|
|
2020
|
+
case '--claim':
|
|
2021
|
+
parsed.claims.push(value);
|
|
2022
|
+
break;
|
|
2023
|
+
case '--budget':
|
|
2024
|
+
if (!/^\d+$/.test(value)) {
|
|
2025
|
+
throw new Error(`--budget must be a whole number of estimated tokens (got "${value}").`);
|
|
2026
|
+
}
|
|
2027
|
+
parsed.budget = Number(value);
|
|
2028
|
+
break;
|
|
2029
|
+
default:
|
|
2030
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
if (parsed.write && parsed.check) {
|
|
2035
|
+
throw new Error('Choose one of --write (regenerate the lane brief) or --check (report its freshness).');
|
|
2036
|
+
}
|
|
2037
|
+
if (parsed.write || parsed.check) {
|
|
2038
|
+
const laneDerived = [
|
|
2039
|
+
parsed.task !== undefined && '--task',
|
|
2040
|
+
parsed.files.length > 0 && '--files',
|
|
2041
|
+
parsed.featureSlugs.length > 0 && '--feature',
|
|
2042
|
+
parsed.claims.length > 0 && '--claim',
|
|
2043
|
+
parsed.check && parsed.budget !== undefined && '--budget',
|
|
2044
|
+
].filter(Boolean);
|
|
2045
|
+
if (laneDerived.length > 0) {
|
|
2046
|
+
throw new Error(
|
|
2047
|
+
`${laneDerived.join(', ')} cannot be combined with ${parsed.write ? '--write' : '--check'}: a lane brief takes its task from the lane's working-on, its files from the lane's claims, and its features from the lane's feature slugs (update them with continue-session --working-on or the lane metadata).`,
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
} else if (parsed.sourceRootDir || parsed.sourceSessionId) {
|
|
2051
|
+
throw new Error('--source-root and --source-session apply only to --write or --check; to print a brief of another root, pass --root.');
|
|
2052
|
+
}
|
|
2053
|
+
if (parsed.sourceSessionId && !parsed.sourceRootDir) {
|
|
2054
|
+
throw new Error('--source-session requires --source-root.');
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
return {
|
|
2058
|
+
command: 'brief',
|
|
2059
|
+
options: parsed,
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1437
2063
|
function parseListSessionsArgs(argv) {
|
|
1438
2064
|
const parsed = {};
|
|
1439
2065
|
|
|
@@ -1707,6 +2333,10 @@ function parseContinueSessionArgs(argv) {
|
|
|
1707
2333
|
parsed.json = true;
|
|
1708
2334
|
continue;
|
|
1709
2335
|
}
|
|
2336
|
+
if (token === '--no-brief') {
|
|
2337
|
+
parsed.brief = false;
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
1710
2340
|
|
|
1711
2341
|
const value = argv[index + 1];
|
|
1712
2342
|
if (value === undefined) {
|
|
@@ -1724,6 +2354,12 @@ function parseContinueSessionArgs(argv) {
|
|
|
1724
2354
|
case '--session':
|
|
1725
2355
|
parsed.sessionId = value;
|
|
1726
2356
|
break;
|
|
2357
|
+
case '--source-root':
|
|
2358
|
+
parsed.sourceRootDir = value;
|
|
2359
|
+
break;
|
|
2360
|
+
case '--source-session':
|
|
2361
|
+
parsed.sourceSessionId = value;
|
|
2362
|
+
break;
|
|
1727
2363
|
case '--working-on':
|
|
1728
2364
|
parsed.workingOn = value;
|
|
1729
2365
|
break;
|
|
@@ -1756,6 +2392,13 @@ function parseContinueSessionArgs(argv) {
|
|
|
1756
2392
|
parsed.sessionId = positional[0];
|
|
1757
2393
|
}
|
|
1758
2394
|
|
|
2395
|
+
if (parsed.brief === false && (parsed.sourceRootDir || parsed.sourceSessionId)) {
|
|
2396
|
+
throw new Error('--no-brief cannot be combined with --source-root or --source-session.');
|
|
2397
|
+
}
|
|
2398
|
+
if (parsed.sourceSessionId && !parsed.sourceRootDir) {
|
|
2399
|
+
throw new Error('--source-session requires --source-root.');
|
|
2400
|
+
}
|
|
2401
|
+
|
|
1759
2402
|
return {
|
|
1760
2403
|
command: 'continue-session',
|
|
1761
2404
|
options: parsed,
|
|
@@ -2467,6 +3110,31 @@ function writeWarnings(io, warnings = []) {
|
|
|
2467
3110
|
}
|
|
2468
3111
|
}
|
|
2469
3112
|
|
|
3113
|
+
/** One status line for the lifecycle brief (D-364); failures also arrive as warnings. */
|
|
3114
|
+
function writeLifecycleBriefResult(io, brief) {
|
|
3115
|
+
if (!brief) return;
|
|
3116
|
+
const location = brief.path ? ` — ${brief.path}` : '';
|
|
3117
|
+
const from = brief.source?.adapter ? ` (from ${brief.source.root_dir})` : '';
|
|
3118
|
+
switch (brief.state) {
|
|
3119
|
+
case 'off':
|
|
3120
|
+
io.stdout.write(`Brief: off (${brief.reason})\n`);
|
|
3121
|
+
return;
|
|
3122
|
+
case 'kept':
|
|
3123
|
+
io.stdout.write(`Brief: current, kept (status ${brief.status ?? 'unknown'}${brief.generatedAt ? `, generated ${brief.generatedAt}` : ''})${location}\n`);
|
|
3124
|
+
return;
|
|
3125
|
+
case 'regenerated':
|
|
3126
|
+
io.stdout.write(`Brief: regenerated, ${brief.status}${from} (was stale: ${(brief.staleReasons ?? []).join('; ') || 'unknown'})${location}\n`);
|
|
3127
|
+
return;
|
|
3128
|
+
case 'written':
|
|
3129
|
+
io.stdout.write(`Brief: ${brief.status}${from}${location}\n`);
|
|
3130
|
+
return;
|
|
3131
|
+
default:
|
|
3132
|
+
io.stdout.write(brief.status
|
|
3133
|
+
? `Brief: incomplete (${brief.state}: ${brief.reason})${location}\n`
|
|
3134
|
+
: 'Brief: not written (see warning)\n');
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
2470
3138
|
function splitAssignment(value, flagName) {
|
|
2471
3139
|
const separatorIndex = value.indexOf('=');
|
|
2472
3140
|
if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
|
|
@@ -2487,15 +3155,23 @@ function usageText() {
|
|
|
2487
3155
|
' vibecompass --version',
|
|
2488
3156
|
' vibecompass version',
|
|
2489
3157
|
' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
|
|
2490
|
-
' vibecompass
|
|
3158
|
+
' vibecompass login [--sync-target <name>] [--api-url <url>] [--no-browser] [options]',
|
|
3159
|
+
' Recommended: connect this root to hosted VibeCompass from your browser',
|
|
2491
3160
|
' vibecompass sync-target [<name>] [options]',
|
|
3161
|
+
' vibecompass connect-hosted [options] Advanced: bind with a dashboard token (login does this for you)',
|
|
3162
|
+
' vibecompass sync-credential <set|remove|list> [--sync-target <name>] [options]',
|
|
3163
|
+
' Advanced: manage stored tokens for CI or scripts',
|
|
2492
3164
|
' vibecompass status [options]',
|
|
2493
3165
|
' vibecompass refresh-workflow [--dry-run|--apply] [options]',
|
|
2494
3166
|
' vibecompass docs-update [--session <lane-id>] [options]',
|
|
2495
|
-
' vibecompass
|
|
3167
|
+
' vibecompass brief --task <text> [--files <repo:path>...] [--feature <slug>] [--claim <path>] [--session <lane-id>] [--budget <n>] [--json] [options]',
|
|
3168
|
+
' Read-only session brief: relevant memory, packed into an estimated token budget',
|
|
3169
|
+
' vibecompass brief --write|--check [--session <lane-id>] [--source-root <path> [--source-session <lane-id>]] [--budget <n>] [--json] [options]',
|
|
3170
|
+
' Regenerate the lane\'s sessions/active/<lane-id>/brief.md, or report whether it is stale (D-364)',
|
|
3171
|
+
' vibecompass start-session --id <lane-id> --working-on <text> [--branch <name> [--worktree]] [--no-brief] [options]',
|
|
2496
3172
|
' vibecompass close-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options]',
|
|
2497
3173
|
' vibecompass end-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options] # alias',
|
|
2498
|
-
' vibecompass continue-session [<lane-id>] [--session <lane-id>] [--working-on <text>] [--json] [options]',
|
|
3174
|
+
' vibecompass continue-session [<lane-id>] [--session <lane-id>] [--working-on <text>] [--no-brief | --source-root <path>] [--json] [options]',
|
|
2499
3175
|
' vibecompass list-sessions [--json] [options]',
|
|
2500
3176
|
' vibecompass switch-session <id> [options]',
|
|
2501
3177
|
' vibecompass rebuild-active-index [--current <lane-id>] [options]',
|
|
@@ -2541,13 +3217,17 @@ function usageText() {
|
|
|
2541
3217
|
' --force Overwrite an existing project.yaml',
|
|
2542
3218
|
' --replace-active-lanes Allow --force to replace a root that has active session lanes',
|
|
2543
3219
|
'',
|
|
2544
|
-
'Connect-hosted options:',
|
|
3220
|
+
'Connect-hosted options (advanced; on a terminal, Enter at the token prompt starts the browser sign-in):',
|
|
2545
3221
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
2546
3222
|
' --target <name> Add or update a named sync target (e.g. dev, prod)',
|
|
2547
3223
|
' First named target becomes the default; flat sync fields mirror the default target',
|
|
2548
3224
|
' --sync-api-url <url> Hosted sync api_url',
|
|
2549
3225
|
' --sync-project-id <id> Hosted sync project_id',
|
|
2550
3226
|
' --sync-credential-env-var <name> Hosted sync env var reference',
|
|
3227
|
+
' --token-stdin Store the sync token read from stdin (first non-empty line)',
|
|
3228
|
+
' --no-store Do not store a token; rely on the env var only',
|
|
3229
|
+
' --credential-store <auto|keychain|file>',
|
|
3230
|
+
' Where to keep the token (D-355). auto prefers the OS keychain',
|
|
2551
3231
|
' Without --target, replaces any existing flat project.yaml sync binding',
|
|
2552
3232
|
'',
|
|
2553
3233
|
'Sync-target options:',
|
|
@@ -2555,6 +3235,33 @@ function usageText() {
|
|
|
2555
3235
|
' vibecompass sync-target <name> Switch the default sync target (re-mirrors flat sync fields)',
|
|
2556
3236
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
2557
3237
|
'',
|
|
3238
|
+
'Login options (recommended path, D-355/D-356):',
|
|
3239
|
+
' vibecompass login Sign in from the terminal: opens the dashboard, you approve a short code, the sync token is stored locally',
|
|
3240
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
3241
|
+
' --sync-target <name> Sign in for a named sync target (add --api-url to create a new one)',
|
|
3242
|
+
' --api-url <url> Hosted URL for an unbound root or a new target. Defaults to https://vibecompass.dev',
|
|
3243
|
+
' --project-id <id> Pre-select a hosted project on the approval page',
|
|
3244
|
+
' --sync-credential-env-var <name> Override env-var name recorded in a new binding. Defaults to VIBECOMPASS_SYNC_TOKEN',
|
|
3245
|
+
' --label <text> Device label shown in the dashboard. Defaults to the hostname',
|
|
3246
|
+
' --credential-store <auto|keychain|file>',
|
|
3247
|
+
' Where to keep the token. auto prefers the OS keychain',
|
|
3248
|
+
' --poll-timeout <seconds> Stop waiting for approval after this long. Defaults to the code lifetime',
|
|
3249
|
+
' --no-browser Print the link without opening a browser',
|
|
3250
|
+
'',
|
|
3251
|
+
'Sync-credential options (advanced, D-355):',
|
|
3252
|
+
' vibecompass sync-credential set Store a dashboard token for the bound project (hidden prompt, --token-stdin, or --from-env)',
|
|
3253
|
+
' vibecompass sync-credential remove Forget the stored token for the bound project',
|
|
3254
|
+
' vibecompass sync-credential list List stored tokens (metadata only, never values)',
|
|
3255
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
3256
|
+
' --sync-target <name> Address a named sync target',
|
|
3257
|
+
' --api-url <url> --project-id <id> Address a hosted project without a bound root',
|
|
3258
|
+
' --token-stdin Read the token from stdin (first non-empty line)',
|
|
3259
|
+
' --from-env Store the value currently in the binding env var',
|
|
3260
|
+
' --credential-store <auto|keychain|file>',
|
|
3261
|
+
' Where to keep the token. auto prefers the OS keychain',
|
|
3262
|
+
' --label <text> Label recorded with the stored token',
|
|
3263
|
+
' --json list: print entries as JSON',
|
|
3264
|
+
'',
|
|
2558
3265
|
'Status options:',
|
|
2559
3266
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
2560
3267
|
' --tooling-root <path> Directory where agent files are inspected. Defaults to cwd',
|
|
@@ -2590,6 +3297,7 @@ function usageText() {
|
|
|
2590
3297
|
' --last-thing-completed <text> Optional override for the CLAUDE.md current-session block',
|
|
2591
3298
|
' --blockers <text> Optional current blockers summary',
|
|
2592
3299
|
' --next-session-should <text> Optional current-session handoff summary',
|
|
3300
|
+
' --no-brief Skip the lane brief for this command (sessions/active/<lane-id>/brief.md; D-359, D-364); lifecycle commands write it only when project.yaml sets brief.enabled: true (opt-in, D-368)',
|
|
2593
3301
|
'',
|
|
2594
3302
|
'Close-session options (also accepted by end-session):',
|
|
2595
3303
|
' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
|
|
@@ -2617,7 +3325,23 @@ function usageText() {
|
|
|
2617
3325
|
' --last-thing-completed <text> Optional override for the CLAUDE.md current-session block',
|
|
2618
3326
|
' --blockers <text> Optional current blockers summary',
|
|
2619
3327
|
' --next-session-should <text> Optional current-session handoff summary',
|
|
2620
|
-
' --json Print the resume result (lane, paths, runtime, other lanes, warnings) as JSON',
|
|
3328
|
+
' --json Print the resume result (lane, paths, runtime, other lanes, brief, warnings) as JSON',
|
|
3329
|
+
' --no-brief Leave the lane brief as it is; with project.yaml brief.enabled: true (opt-in, D-368) resume keeps a current brief and regenerates a missing or stale one',
|
|
3330
|
+
' --source-root <path> D-255 dual-root dogfood: build the brief from this memory root\'s matching lane (same id, session date, and number) and write it only into this lane; the source root is only read',
|
|
3331
|
+
' --source-session <lane-id> With --source-root: the source lane id, which must equal the resumed lane',
|
|
3332
|
+
'',
|
|
3333
|
+
'Brief options:',
|
|
3334
|
+
' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
|
|
3335
|
+
' --session <lane-id> Lane to brief. Omitted: nearest worktree lane marker, else the single active lane; 2+ active lanes require --session or a marker (D-277)',
|
|
3336
|
+
' --task <text> Task text (print mode). Omitted: the lane\'s working-on',
|
|
3337
|
+
' --files <repo:path>... Files the task touches (print mode)',
|
|
3338
|
+
' --feature <slug>, --claim <path> Repeatable feature slug and path claim (print mode)',
|
|
3339
|
+
' --budget <n> Estimated-token budget (default 6000, minimum 800)',
|
|
3340
|
+
' --write Regenerate the lane\'s brief.md from its working-on, claims, and feature slugs, under the root lock',
|
|
3341
|
+
' --check Report whether the lane\'s brief.md is current or stale against its header bindings; writes nothing',
|
|
3342
|
+
' --source-root <path> With --write/--check: read this memory root\'s matching lane instead (D-255 dogfood); never written',
|
|
3343
|
+
' --source-session <lane-id> With --source-root: the source lane id, which must equal the lane being briefed',
|
|
3344
|
+
' --json Print the result as JSON',
|
|
2621
3345
|
'',
|
|
2622
3346
|
'List/switch-session options:',
|
|
2623
3347
|
' --root <path> Project-memory root. Defaults to .compass',
|