@vibecompass/vibecompass 0.3.4 → 0.5.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/src/cli.js CHANGED
@@ -7,6 +7,9 @@ import { preflightDocsReview } from './docs-review.js';
7
7
  import { resolveConnectHostedCliOptions, resolveInitCliOptions } from './setup.js';
8
8
  import { closeProjectSession, listProjectSessions, startProjectSession, switchProjectSession } from './session.js';
9
9
  import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
10
+ import { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
11
+ import { refreshWorkflow } from './refresh-workflow.js';
12
+ import { inspectProjectCompatibility, formatCompatibilityWarnings } from './compatibility.js';
10
13
  import {
11
14
  applyPullExport,
12
15
  pullExportProjectMemory,
@@ -28,6 +31,10 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
28
31
  io,
29
32
  runtime,
30
33
  });
34
+ if (initPlan.existingProject) {
35
+ return writeExistingInitProjectResult(io, initPlan.existingProject);
36
+ }
37
+
31
38
  const result = await initializeProjectMemory({
32
39
  ...initPlan.initOptions,
33
40
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -106,6 +113,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
106
113
  }
107
114
 
108
115
  if (parsed.command === 'start-session') {
116
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
109
117
  const result = await startProjectSession({
110
118
  ...parsed.options,
111
119
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -120,6 +128,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
120
128
  }
121
129
 
122
130
  if (parsed.command === 'connect-hosted') {
131
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
123
132
  const connectPlan = await resolveConnectHostedCliOptions(parsed.options, {
124
133
  cwd: runtime.cwd,
125
134
  io,
@@ -141,7 +150,30 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
141
150
  return 0;
142
151
  }
143
152
 
153
+ if (parsed.command === 'status') {
154
+ const result = await getProjectStatus({
155
+ ...parsed.options,
156
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
157
+ });
158
+ if (parsed.options.json) {
159
+ io.stdout.write(`${JSON.stringify(toStatusJson(result), null, 2)}\n`);
160
+ } else {
161
+ io.stdout.write(renderStatusText(result));
162
+ }
163
+ return 0;
164
+ }
165
+
166
+ if (parsed.command === 'refresh-workflow') {
167
+ const result = await refreshWorkflow({
168
+ ...parsed.options,
169
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
170
+ });
171
+ writeRefreshWorkflowResult(io, result);
172
+ return 0;
173
+ }
174
+
144
175
  if (parsed.command === 'close-session' || parsed.command === 'end-session') {
176
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
145
177
  const result = await closeProjectSession({
146
178
  ...parsed.options,
147
179
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -160,6 +192,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
160
192
  }
161
193
 
162
194
  if (parsed.command === 'list-sessions') {
195
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
163
196
  const result = await listProjectSessions({
164
197
  ...parsed.options,
165
198
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -177,6 +210,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
177
210
  }
178
211
 
179
212
  if (parsed.command === 'switch-session') {
213
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
180
214
  const result = await switchProjectSession({
181
215
  ...parsed.options,
182
216
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -188,6 +222,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
188
222
  }
189
223
 
190
224
  if (parsed.command === 'sync-agents') {
225
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
191
226
  const result = await syncAgentInstructionFiles({
192
227
  ...parsed.options,
193
228
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -197,6 +232,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
197
232
  }
198
233
 
199
234
  if (parsed.command === 'docs-review') {
235
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
200
236
  const result = await preflightDocsReview({
201
237
  ...parsed.options,
202
238
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -246,6 +282,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
246
282
  }
247
283
 
248
284
  if (parsed.command === 'push') {
285
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
249
286
  const result = await pushProjectMemory(parsed.options, {
250
287
  cwd: runtime.cwd,
251
288
  env: runtime.env,
@@ -266,6 +303,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
266
303
  }
267
304
 
268
305
  if (parsed.command === 'pull-preview') {
306
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
269
307
  const result = await pullPreviewProjectMemory(parsed.options, {
270
308
  cwd: runtime.cwd,
271
309
  env: runtime.env,
@@ -290,6 +328,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
290
328
  }
291
329
 
292
330
  if (parsed.command === 'pull-export') {
331
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
293
332
  const result = await pullExportProjectMemory(parsed.options, {
294
333
  cwd: runtime.cwd,
295
334
  env: runtime.env,
@@ -305,6 +344,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
305
344
  }
306
345
 
307
346
  if (parsed.command === 'apply-export') {
347
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
308
348
  const result = await applyPullExport(parsed.options, {
309
349
  cwd: runtime.cwd,
310
350
  env: runtime.env,
@@ -338,6 +378,14 @@ export function parseCliArgs(argv) {
338
378
  return parseConnectHostedArgs(rest);
339
379
  }
340
380
 
381
+ if (command === 'status') {
382
+ return parseStatusArgs(rest);
383
+ }
384
+
385
+ if (command === 'refresh-workflow') {
386
+ return parseRefreshWorkflowArgs(rest);
387
+ }
388
+
341
389
  if (command === 'close-session' || command === 'end-session') {
342
390
  return parseCloseSessionArgs(rest);
343
391
  }
@@ -390,6 +438,11 @@ export function parseCliArgs(argv) {
390
438
  continue;
391
439
  }
392
440
 
441
+ if (token === '--replace-active-lanes') {
442
+ parsed.replaceActiveLanes = true;
443
+ continue;
444
+ }
445
+
393
446
  if (token === '--guided') {
394
447
  parsed.guided = true;
395
448
  continue;
@@ -516,6 +569,7 @@ export function parseCliArgs(argv) {
516
569
  mode: parsed.mode,
517
570
  repos: parsed.repos,
518
571
  force: parsed.force,
572
+ replaceActiveLanes: parsed.replaceActiveLanes,
519
573
  startSession: parsed.startSession,
520
574
  sessionWorkingOn: parsed.sessionWorkingOn,
521
575
  sessionId: parsed.sessionId,
@@ -662,6 +716,99 @@ function parseConnectHostedArgs(argv) {
662
716
  };
663
717
  }
664
718
 
719
+ function parseStatusArgs(argv) {
720
+ const parsed = {};
721
+
722
+ for (let index = 0; index < argv.length; index += 1) {
723
+ const token = argv[index];
724
+
725
+ if (token === '--json') {
726
+ parsed.json = true;
727
+ continue;
728
+ }
729
+
730
+ if (!token.startsWith('--')) {
731
+ throw new Error(`Unexpected argument "${token}".`);
732
+ }
733
+
734
+ const value = argv[index + 1];
735
+ if (value === undefined) {
736
+ throw new Error(`Flag "${token}" requires a value.`);
737
+ }
738
+ index += 1;
739
+
740
+ switch (token) {
741
+ case '--root':
742
+ parsed.rootDir = value;
743
+ break;
744
+ case '--tooling-root':
745
+ parsed.toolingRootDir = value;
746
+ break;
747
+ default:
748
+ throw new Error(`Unknown flag "${token}".`);
749
+ }
750
+ }
751
+
752
+ return {
753
+ command: 'status',
754
+ options: parsed,
755
+ };
756
+ }
757
+
758
+ function parseRefreshWorkflowArgs(argv) {
759
+ const parsed = {};
760
+
761
+ for (let index = 0; index < argv.length; index += 1) {
762
+ const token = argv[index];
763
+
764
+ if (token === '--dry-run') {
765
+ parsed.apply = false;
766
+ continue;
767
+ }
768
+
769
+ if (token === '--apply') {
770
+ parsed.apply = true;
771
+ continue;
772
+ }
773
+
774
+ if (token === '--update-package-stamp') {
775
+ parsed.updatePackageStamp = true;
776
+ continue;
777
+ }
778
+
779
+ if (token === '--allow-downgrade-templates') {
780
+ parsed.allowDowngradeTemplates = true;
781
+ continue;
782
+ }
783
+
784
+ if (!token.startsWith('--')) {
785
+ throw new Error(`Unexpected argument "${token}".`);
786
+ }
787
+
788
+ const value = argv[index + 1];
789
+ if (value === undefined) {
790
+ throw new Error(`Flag "${token}" requires a value.`);
791
+ }
792
+ index += 1;
793
+
794
+ switch (token) {
795
+ case '--root':
796
+ parsed.rootDir = value;
797
+ break;
798
+ case '--tooling-root':
799
+ parsed.toolingRootDir = value;
800
+ break;
801
+ default:
802
+ throw new Error(`Unknown flag "${token}".`);
803
+ }
804
+ }
805
+
806
+ return {
807
+ command: 'refresh-workflow',
808
+ options: parsed,
809
+ };
810
+ }
811
+
665
812
  function parseCloseSessionArgs(argv) {
666
813
  const parsed = {
667
814
  completed: [],
@@ -1113,6 +1260,46 @@ function writeAgentFileSyncResult(io, result) {
1113
1260
  }
1114
1261
  }
1115
1262
 
1263
+ function writeRefreshWorkflowResult(io, result) {
1264
+ io.stdout.write(`${result.dryRun ? 'Planned' : 'Applied'} workflow refresh for ${result.rootDir}\n`);
1265
+ writeCompatibilityWarnings(io, result.compatibility);
1266
+ io.stdout.write('Project package stamp:\n');
1267
+ const projectSuffix = result.projectFile.warning ? ` — ${result.projectFile.warning}` : '';
1268
+ const projectStatus = result.dryRun && result.projectFile.changed
1269
+ ? `dry-run-${result.projectFile.status}`
1270
+ : result.projectFile.status;
1271
+ io.stdout.write(`- project.yaml: ${projectStatus}${projectSuffix}\n`);
1272
+
1273
+ io.stdout.write('Workflow files:\n');
1274
+ for (const file of result.workflowFiles) {
1275
+ const suffix = file.warning ? ` — ${file.warning}` : '';
1276
+ io.stdout.write(`- ${file.relativePath}: ${result.dryRun && file.changed ? `dry-run-${file.status}` : file.status}${suffix}\n`);
1277
+ }
1278
+
1279
+ io.stdout.write('State manifest:\n');
1280
+ io.stdout.write(`- ${result.manifest.path}: ${result.manifest.status} (${result.manifest.documentCount} canonical docs, ${result.manifest.warningCount} warnings)\n`);
1281
+ writeAgentFileSyncResult(io, result.agentFileSync);
1282
+ }
1283
+
1284
+ async function writeCompatibilityPreflightWarnings(io, options = {}, runtime = {}) {
1285
+ const result = await inspectProjectCompatibility({
1286
+ ...options,
1287
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
1288
+ });
1289
+ writeCompatibilityWarnings(io, result);
1290
+ }
1291
+
1292
+ function writeCompatibilityWarnings(io, result) {
1293
+ const warnings = formatCompatibilityWarnings(result);
1294
+ if (warnings.length === 0) {
1295
+ return;
1296
+ }
1297
+
1298
+ io.stderr.write('Compatibility warnings:\n');
1299
+ writeWarnings(io, warnings);
1300
+ io.stderr.write('Run `vibecompass status` for detail.\n');
1301
+ }
1302
+
1116
1303
  function writeWarnings(io, warnings = []) {
1117
1304
  for (const warning of warnings) {
1118
1305
  io.stderr.write(`Warning: ${warning}\n`);
@@ -1133,6 +1320,8 @@ function usageText() {
1133
1320
  'Usage:',
1134
1321
  ' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
1135
1322
  ' vibecompass connect-hosted [options]',
1323
+ ' vibecompass status [options]',
1324
+ ' vibecompass refresh-workflow [--dry-run|--apply] [options]',
1136
1325
  ' vibecompass start-session --id <lane-id> --working-on <text> [options]',
1137
1326
  ' vibecompass close-session --title <text> --completed <text> --next-step <text> [options]',
1138
1327
  ' vibecompass end-session --title <text> --completed <text> --next-step <text> [options] # alias',
@@ -1168,6 +1357,7 @@ function usageText() {
1168
1357
  ' --close-session-git-publish Include a Git publish step in the stored close-session workflow',
1169
1358
  ' --close-session-git-remote <name> Default Git remote name for that close-session publish step',
1170
1359
  ' --force Overwrite an existing project.yaml',
1360
+ ' --replace-active-lanes Allow --force to replace a root that has active session lanes',
1171
1361
  '',
1172
1362
  'Connect-hosted options:',
1173
1363
  ' --root <path> Project-memory root. Defaults to .compass',
@@ -1176,6 +1366,19 @@ function usageText() {
1176
1366
  ' --sync-credential-env-var <name> Hosted sync env var reference',
1177
1367
  ' Replaces any existing project.yaml sync binding',
1178
1368
  '',
1369
+ 'Status options:',
1370
+ ' --root <path> Project-memory root. Defaults to .compass',
1371
+ ' --tooling-root <path> Directory where agent files are inspected. Defaults to cwd',
1372
+ ' --json Print the typed status model as JSON',
1373
+ '',
1374
+ 'Refresh-workflow options:',
1375
+ ' --root <path> Project-memory root. Defaults to .compass',
1376
+ ' --tooling-root <path> Directory where agent files are refreshed. Defaults to cwd',
1377
+ ' --dry-run Show planned refresh without changing files (default)',
1378
+ ' --apply Apply the planned workflow refresh',
1379
+ ' --update-package-stamp Update an existing project.yaml package_version stamp',
1380
+ ' --allow-downgrade-templates Allow applying templates with a CLI older than the root stamp',
1381
+ '',
1179
1382
  'Start-session options:',
1180
1383
  ' --root <path> Project-memory root. Defaults to .compass',
1181
1384
  ' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
@@ -1260,6 +1463,66 @@ function formatOptionalRootFlag(rootDir) {
1260
1463
  return ` --root ${rootDir}`;
1261
1464
  }
1262
1465
 
1466
+ function writeExistingInitProjectResult(io, existingProject) {
1467
+ const rootFlag = formatOptionalRootFlag(existingProject.displayPath);
1468
+ const active = existingProject.activeSummary ?? { count: 0, current: null };
1469
+
1470
+ if (existingProject.status === 'ambiguous') {
1471
+ (io.stderr ?? process.stderr).write(
1472
+ [
1473
+ 'Multiple VibeCompass project memory roots found:',
1474
+ ...existingProject.candidates.map((candidate) => `- ${candidate.displayPath}`),
1475
+ 'Pass --root explicitly to choose one.',
1476
+ '',
1477
+ ].join('\n'),
1478
+ );
1479
+ return 1;
1480
+ }
1481
+
1482
+ if (existingProject.status === 'unreadable') {
1483
+ (io.stderr ?? process.stderr).write('VibeCompass project memory already exists, but project.yaml is unreadable.\n');
1484
+ (io.stderr ?? process.stderr).write(`Root: ${existingProject.displayPath}\n`);
1485
+ (io.stderr ?? process.stderr).write(`Project file: ${existingProject.projectFilePath}\n`);
1486
+ (io.stderr ?? process.stderr).write(`Error: ${existingProject.errorMessage}\n`);
1487
+ if (active.count > 0) {
1488
+ (io.stderr ?? process.stderr).write(`Active lanes: ${formatActiveLaneSummary(active)}\n`);
1489
+ (io.stderr ?? process.stderr).write('Close active lanes before replacing this root, or pass --force --replace-active-lanes if replacement is intentional.\n');
1490
+ } else {
1491
+ (io.stderr ?? process.stderr).write('Fix project.yaml manually, or rerun with --force if replacement is intentional.\n');
1492
+ }
1493
+ return 1;
1494
+ }
1495
+
1496
+ const config = existingProject.projectConfig;
1497
+ const repos = Array.isArray(config.repos)
1498
+ ? config.repos.map((repo) => repo.id).filter(Boolean)
1499
+ : [];
1500
+ io.stdout.write('VibeCompass is already initialized — here is how to continue.\n');
1501
+ io.stdout.write(`Project: ${config.name ?? 'Unknown'}\n`);
1502
+ io.stdout.write(`Mode: ${config.mode ?? 'Unknown'}\n`);
1503
+ io.stdout.write(`Root: ${existingProject.displayPath}\n`);
1504
+ io.stdout.write(`Repos: ${repos.length > 0 ? repos.join(', ') : 'None recorded'}\n`);
1505
+ io.stdout.write(`Active lanes: ${formatActiveLaneSummary(active)}\n`);
1506
+ io.stdout.write('Next commands:\n');
1507
+ io.stdout.write(`- vibecompass status${rootFlag}\n`);
1508
+ io.stdout.write(`- vibecompass list-sessions${rootFlag}\n`);
1509
+ io.stdout.write(`- vibecompass start-session${rootFlag} --id LANE_ID --working-on "TASK"\n`);
1510
+ io.stdout.write(`- vibecompass docs-review${rootFlag} --guided\n`);
1511
+ io.stdout.write(`- vibecompass sync-agents${rootFlag}\n`);
1512
+ if (['local-primary', 'hosted-only'].includes(config.mode)) {
1513
+ io.stdout.write(`- vibecompass connect-hosted${rootFlag}\n`);
1514
+ }
1515
+ return 0;
1516
+ }
1517
+
1518
+ function formatActiveLaneSummary(activeSummary) {
1519
+ if (!activeSummary || activeSummary.count === 0) {
1520
+ return 'none';
1521
+ }
1522
+
1523
+ return `${activeSummary.count}${activeSummary.current ? ` (current: ${activeSummary.current})` : ''}`;
1524
+ }
1525
+
1263
1526
  async function main() {
1264
1527
  try {
1265
1528
  const exitCode = await runCli(process.argv.slice(2));