@vibecompass/vibecompass 0.3.3 → 0.4.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 } : {}),
@@ -78,6 +85,14 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
78
85
  io.stdout.write(`Left existing ${filePath} untouched\n`);
79
86
  }
80
87
 
88
+ if (initPlan.agentFileSyncPlan) {
89
+ const agentFileSync = await syncAgentInstructionFiles({
90
+ ...initPlan.agentFileSyncPlan,
91
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
92
+ });
93
+ writeAgentFileSyncResult(io, agentFileSync);
94
+ }
95
+
81
96
  if (initPlan.sessionPlan) {
82
97
  const sessionResult = await startProjectSession({
83
98
  rootDir: initPlan.initOptions.rootDir,
@@ -98,6 +113,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
98
113
  }
99
114
 
100
115
  if (parsed.command === 'start-session') {
116
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
101
117
  const result = await startProjectSession({
102
118
  ...parsed.options,
103
119
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -112,6 +128,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
112
128
  }
113
129
 
114
130
  if (parsed.command === 'connect-hosted') {
131
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
115
132
  const connectPlan = await resolveConnectHostedCliOptions(parsed.options, {
116
133
  cwd: runtime.cwd,
117
134
  io,
@@ -133,7 +150,30 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
133
150
  return 0;
134
151
  }
135
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
+
136
175
  if (parsed.command === 'close-session' || parsed.command === 'end-session') {
176
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
137
177
  const result = await closeProjectSession({
138
178
  ...parsed.options,
139
179
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -152,6 +192,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
152
192
  }
153
193
 
154
194
  if (parsed.command === 'list-sessions') {
195
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
155
196
  const result = await listProjectSessions({
156
197
  ...parsed.options,
157
198
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -169,6 +210,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
169
210
  }
170
211
 
171
212
  if (parsed.command === 'switch-session') {
213
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
172
214
  const result = await switchProjectSession({
173
215
  ...parsed.options,
174
216
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -180,6 +222,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
180
222
  }
181
223
 
182
224
  if (parsed.command === 'sync-agents') {
225
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
183
226
  const result = await syncAgentInstructionFiles({
184
227
  ...parsed.options,
185
228
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -189,6 +232,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
189
232
  }
190
233
 
191
234
  if (parsed.command === 'docs-review') {
235
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
192
236
  const result = await preflightDocsReview({
193
237
  ...parsed.options,
194
238
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
@@ -238,6 +282,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
238
282
  }
239
283
 
240
284
  if (parsed.command === 'push') {
285
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
241
286
  const result = await pushProjectMemory(parsed.options, {
242
287
  cwd: runtime.cwd,
243
288
  env: runtime.env,
@@ -258,6 +303,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
258
303
  }
259
304
 
260
305
  if (parsed.command === 'pull-preview') {
306
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
261
307
  const result = await pullPreviewProjectMemory(parsed.options, {
262
308
  cwd: runtime.cwd,
263
309
  env: runtime.env,
@@ -282,6 +328,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
282
328
  }
283
329
 
284
330
  if (parsed.command === 'pull-export') {
331
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
285
332
  const result = await pullExportProjectMemory(parsed.options, {
286
333
  cwd: runtime.cwd,
287
334
  env: runtime.env,
@@ -297,6 +344,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
297
344
  }
298
345
 
299
346
  if (parsed.command === 'apply-export') {
347
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
300
348
  const result = await applyPullExport(parsed.options, {
301
349
  cwd: runtime.cwd,
302
350
  env: runtime.env,
@@ -330,6 +378,14 @@ export function parseCliArgs(argv) {
330
378
  return parseConnectHostedArgs(rest);
331
379
  }
332
380
 
381
+ if (command === 'status') {
382
+ return parseStatusArgs(rest);
383
+ }
384
+
385
+ if (command === 'refresh-workflow') {
386
+ return parseRefreshWorkflowArgs(rest);
387
+ }
388
+
333
389
  if (command === 'close-session' || command === 'end-session') {
334
390
  return parseCloseSessionArgs(rest);
335
391
  }
@@ -382,6 +438,11 @@ export function parseCliArgs(argv) {
382
438
  continue;
383
439
  }
384
440
 
441
+ if (token === '--replace-active-lanes') {
442
+ parsed.replaceActiveLanes = true;
443
+ continue;
444
+ }
445
+
385
446
  if (token === '--guided') {
386
447
  parsed.guided = true;
387
448
  continue;
@@ -402,6 +463,11 @@ export function parseCliArgs(argv) {
402
463
  continue;
403
464
  }
404
465
 
466
+ if (token === '--adopt-existing-agent-files') {
467
+ parsed.adoptExistingAgentFiles = true;
468
+ continue;
469
+ }
470
+
405
471
  if (token === '--start-session') {
406
472
  parsed.startSession = true;
407
473
  continue;
@@ -503,11 +569,13 @@ export function parseCliArgs(argv) {
503
569
  mode: parsed.mode,
504
570
  repos: parsed.repos,
505
571
  force: parsed.force,
572
+ replaceActiveLanes: parsed.replaceActiveLanes,
506
573
  startSession: parsed.startSession,
507
574
  sessionWorkingOn: parsed.sessionWorkingOn,
508
575
  sessionId: parsed.sessionId,
509
576
  closeSessionGitPublish: parsed.closeSessionGitPublish,
510
577
  closeSessionGitRemote: parsed.closeSessionGitRemote,
578
+ adoptExistingAgentFiles: parsed.adoptExistingAgentFiles,
511
579
  ...(parsed.withWorkflow || parsed.withClaude || parsed.withAgents
512
580
  ? {
513
581
  bootstrap: {
@@ -648,6 +716,99 @@ function parseConnectHostedArgs(argv) {
648
716
  };
649
717
  }
650
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
+
651
812
  function parseCloseSessionArgs(argv) {
652
813
  const parsed = {
653
814
  completed: [],
@@ -804,6 +965,11 @@ function parseSyncAgentsArgs(argv) {
804
965
  continue;
805
966
  }
806
967
 
968
+ if (token === '--adopt-existing') {
969
+ parsed.adoptExisting = true;
970
+ continue;
971
+ }
972
+
807
973
  if (!token.startsWith('--')) {
808
974
  throw new Error(`Unexpected argument "${token}".`);
809
975
  }
@@ -1087,7 +1253,51 @@ function writeAgentFileSyncResult(io, result) {
1087
1253
  const suffix = item.warning ? ` — ${item.warning}` : '';
1088
1254
  const stream = item.warning ? io.stderr : io.stdout;
1089
1255
  stream.write(`- ${item.relativePath}: ${item.status}${suffix}\n`);
1256
+ if (item.conflicts?.length > 0) {
1257
+ const lineNumbers = item.conflicts.map((conflict) => conflict.line).join(', ');
1258
+ io.stderr.write(` possible workflow overlap on lines ${lineNumbers}; review existing instructions if behavior conflicts.\n`);
1259
+ }
1260
+ }
1261
+ }
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`);
1090
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');
1091
1301
  }
1092
1302
 
1093
1303
  function writeWarnings(io, warnings = []) {
@@ -1110,6 +1320,8 @@ function usageText() {
1110
1320
  'Usage:',
1111
1321
  ' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
1112
1322
  ' vibecompass connect-hosted [options]',
1323
+ ' vibecompass status [options]',
1324
+ ' vibecompass refresh-workflow [--dry-run|--apply] [options]',
1113
1325
  ' vibecompass start-session --id <lane-id> --working-on <text> [options]',
1114
1326
  ' vibecompass close-session --title <text> --completed <text> --next-step <text> [options]',
1115
1327
  ' vibecompass end-session --title <text> --completed <text> --next-step <text> [options] # alias',
@@ -1138,12 +1350,14 @@ function usageText() {
1138
1350
  ' --with-workflow Scaffold context.md and workflow guide files',
1139
1351
  ' --with-claude Create a starter CLAUDE.md if missing',
1140
1352
  ' --with-agents Create a starter AGENTS.md if missing',
1353
+ ' --adopt-existing-agent-files Adopt existing unmarked agent files after init',
1141
1354
  ' --start-session Open the first builder session after init',
1142
1355
  ' --session-working-on <text> Required with --start-session outside guided mode',
1143
1356
  ' --session-id <lane-id> Required with --start-session; names the first builder lane',
1144
1357
  ' --close-session-git-publish Include a Git publish step in the stored close-session workflow',
1145
1358
  ' --close-session-git-remote <name> Default Git remote name for that close-session publish step',
1146
1359
  ' --force Overwrite an existing project.yaml',
1360
+ ' --replace-active-lanes Allow --force to replace a root that has active session lanes',
1147
1361
  '',
1148
1362
  'Connect-hosted options:',
1149
1363
  ' --root <path> Project-memory root. Defaults to .compass',
@@ -1152,6 +1366,19 @@ function usageText() {
1152
1366
  ' --sync-credential-env-var <name> Hosted sync env var reference',
1153
1367
  ' Replaces any existing project.yaml sync binding',
1154
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
+ '',
1155
1382
  'Start-session options:',
1156
1383
  ' --root <path> Project-memory root. Defaults to .compass',
1157
1384
  ' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
@@ -1187,6 +1414,7 @@ function usageText() {
1187
1414
  ' --root <path> Project-memory root. Defaults to .compass',
1188
1415
  ' --tooling-root <path> Directory where agent files are written. Defaults to cwd',
1189
1416
  ' --format <name> Optional format: claude_md, agents_md, cursor_rules, copilot_instructions',
1417
+ ' --adopt-existing Append managed blocks to existing unmarked files',
1190
1418
  ' --dry-run Show planned writes without changing files',
1191
1419
  '',
1192
1420
  'Sync transport options:',
@@ -1235,6 +1463,66 @@ function formatOptionalRootFlag(rootDir) {
1235
1463
  return ` --root ${rootDir}`;
1236
1464
  }
1237
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
+
1238
1526
  async function main() {
1239
1527
  try {
1240
1528
  const exitCode = await runCli(process.argv.slice(2));