@vibecompass/vibecompass 0.3.1 → 0.3.4
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 +27 -7
- package/package.json +1 -1
- package/src/cli.js +25 -0
- package/src/generators/agent-files/index.js +57 -1
- package/src/generators/agent-files/markers.js +10 -1
- package/src/generators/agent-files/template.js +4 -0
- package/src/setup.js +235 -17
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://vibecompass.dev">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/jack-whimvy/vibecompass/main/brand/logo-a4.png" alt="VibeCompass" height="96" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# @vibecompass/vibecompass
|
|
2
8
|
|
|
3
9
|
Local-first project-memory core for [VibeCompass](https://vibecompass.dev).
|
|
@@ -243,7 +249,16 @@ npx -y @vibecompass/vibecompass sync-agents --root .compass
|
|
|
243
249
|
```
|
|
244
250
|
|
|
245
251
|
Content outside those markers is preserved. Existing files without markers are
|
|
246
|
-
reported as warnings and left untouched
|
|
252
|
+
reported as warnings and left untouched unless you explicitly adopt them:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npx -y @vibecompass/vibecompass sync-agents --root .compass --adopt-existing
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Adoption appends a managed VibeCompass block to the end of existing unmarked
|
|
259
|
+
agent files, preserves everything outside the markers, and reports possible
|
|
260
|
+
workflow-overlap lines before you rely on the result. Guided init may offer the
|
|
261
|
+
same adoption step for existing files, but the prompt defaults to no.
|
|
247
262
|
|
|
248
263
|
Connect hosted VibeCompass later when you want browser views, team sync, or hosted docs-review:
|
|
249
264
|
|
|
@@ -314,6 +329,7 @@ Options:
|
|
|
314
329
|
- `--with-workflow`: generate `context.md` plus workflow guide files
|
|
315
330
|
- `--with-claude`: create a starter `CLAUDE.md` if it does not already exist
|
|
316
331
|
- `--with-agents`: create a starter `AGENTS.md` if it does not already exist
|
|
332
|
+
- `--adopt-existing-agent-files`: append managed blocks to existing unmarked agent files
|
|
317
333
|
- `--start-session`: open the first builder session after init
|
|
318
334
|
- `--session-working-on <text>`: required with `--start-session` outside guided mode
|
|
319
335
|
- `--session-id <lane-id>`: required with `--start-session`; names the first builder lane
|
|
@@ -327,11 +343,14 @@ Guided init can also record workflow defaults in
|
|
|
327
343
|
`project.yaml.metadata.workflow`, including whether close-session should
|
|
328
344
|
include a Git publish step and which remote name to use for that step.
|
|
329
345
|
|
|
330
|
-
Guided init is inference-first for
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
346
|
+
Guided init is inference-first for common repo layouts: it reads `package.json`,
|
|
347
|
+
detects the current Git `origin` when you run it inside a repo, or scans a
|
|
348
|
+
parent workspace for immediate child repos when the parent is not itself a Git
|
|
349
|
+
checkout. It asks once before using detected repos, maps a friendly setup goal
|
|
350
|
+
to the stored project mode, places single-child repo memory under that child
|
|
351
|
+
repo, and uses workspace-root placement for multi-repo parent folders. Hosted
|
|
352
|
+
credentials are deferred by default; run `vibecompass connect-hosted` when you
|
|
353
|
+
are ready to connect the local root to `vibecompass.dev`.
|
|
335
354
|
|
|
336
355
|
### `vibecompass connect-hosted`
|
|
337
356
|
|
|
@@ -437,12 +456,13 @@ Options:
|
|
|
437
456
|
- `--root <path>`: project-memory root; defaults to `.compass`
|
|
438
457
|
- `--tooling-root <path>`: directory where agent files are written; defaults to cwd
|
|
439
458
|
- `--format <name>`: optional format filter: `claude_md`, `agents_md`, `cursor_rules`, or `copilot_instructions`
|
|
459
|
+
- `--adopt-existing`: append managed blocks to existing unmarked files
|
|
440
460
|
- `--dry-run`: show planned writes without changing files
|
|
441
461
|
|
|
442
462
|
This command generates enabled agent-instruction files from canonical project
|
|
443
463
|
memory. It creates missing files with managed markers, replaces existing
|
|
444
464
|
managed regions, preserves content outside markers, and warns instead of
|
|
445
|
-
overwriting existing unmarked files.
|
|
465
|
+
overwriting existing unmarked files unless `--adopt-existing` is supplied.
|
|
446
466
|
|
|
447
467
|
### `vibecompass docs-review`
|
|
448
468
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -78,6 +78,14 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
78
78
|
io.stdout.write(`Left existing ${filePath} untouched\n`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (initPlan.agentFileSyncPlan) {
|
|
82
|
+
const agentFileSync = await syncAgentInstructionFiles({
|
|
83
|
+
...initPlan.agentFileSyncPlan,
|
|
84
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
85
|
+
});
|
|
86
|
+
writeAgentFileSyncResult(io, agentFileSync);
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
if (initPlan.sessionPlan) {
|
|
82
90
|
const sessionResult = await startProjectSession({
|
|
83
91
|
rootDir: initPlan.initOptions.rootDir,
|
|
@@ -402,6 +410,11 @@ export function parseCliArgs(argv) {
|
|
|
402
410
|
continue;
|
|
403
411
|
}
|
|
404
412
|
|
|
413
|
+
if (token === '--adopt-existing-agent-files') {
|
|
414
|
+
parsed.adoptExistingAgentFiles = true;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
405
418
|
if (token === '--start-session') {
|
|
406
419
|
parsed.startSession = true;
|
|
407
420
|
continue;
|
|
@@ -508,6 +521,7 @@ export function parseCliArgs(argv) {
|
|
|
508
521
|
sessionId: parsed.sessionId,
|
|
509
522
|
closeSessionGitPublish: parsed.closeSessionGitPublish,
|
|
510
523
|
closeSessionGitRemote: parsed.closeSessionGitRemote,
|
|
524
|
+
adoptExistingAgentFiles: parsed.adoptExistingAgentFiles,
|
|
511
525
|
...(parsed.withWorkflow || parsed.withClaude || parsed.withAgents
|
|
512
526
|
? {
|
|
513
527
|
bootstrap: {
|
|
@@ -804,6 +818,11 @@ function parseSyncAgentsArgs(argv) {
|
|
|
804
818
|
continue;
|
|
805
819
|
}
|
|
806
820
|
|
|
821
|
+
if (token === '--adopt-existing') {
|
|
822
|
+
parsed.adoptExisting = true;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
|
|
807
826
|
if (!token.startsWith('--')) {
|
|
808
827
|
throw new Error(`Unexpected argument "${token}".`);
|
|
809
828
|
}
|
|
@@ -1087,6 +1106,10 @@ function writeAgentFileSyncResult(io, result) {
|
|
|
1087
1106
|
const suffix = item.warning ? ` — ${item.warning}` : '';
|
|
1088
1107
|
const stream = item.warning ? io.stderr : io.stdout;
|
|
1089
1108
|
stream.write(`- ${item.relativePath}: ${item.status}${suffix}\n`);
|
|
1109
|
+
if (item.conflicts?.length > 0) {
|
|
1110
|
+
const lineNumbers = item.conflicts.map((conflict) => conflict.line).join(', ');
|
|
1111
|
+
io.stderr.write(` possible workflow overlap on lines ${lineNumbers}; review existing instructions if behavior conflicts.\n`);
|
|
1112
|
+
}
|
|
1090
1113
|
}
|
|
1091
1114
|
}
|
|
1092
1115
|
|
|
@@ -1138,6 +1161,7 @@ function usageText() {
|
|
|
1138
1161
|
' --with-workflow Scaffold context.md and workflow guide files',
|
|
1139
1162
|
' --with-claude Create a starter CLAUDE.md if missing',
|
|
1140
1163
|
' --with-agents Create a starter AGENTS.md if missing',
|
|
1164
|
+
' --adopt-existing-agent-files Adopt existing unmarked agent files after init',
|
|
1141
1165
|
' --start-session Open the first builder session after init',
|
|
1142
1166
|
' --session-working-on <text> Required with --start-session outside guided mode',
|
|
1143
1167
|
' --session-id <lane-id> Required with --start-session; names the first builder lane',
|
|
@@ -1187,6 +1211,7 @@ function usageText() {
|
|
|
1187
1211
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
1188
1212
|
' --tooling-root <path> Directory where agent files are written. Defaults to cwd',
|
|
1189
1213
|
' --format <name> Optional format: claude_md, agents_md, cursor_rules, copilot_instructions',
|
|
1214
|
+
' --adopt-existing Append managed blocks to existing unmarked files',
|
|
1190
1215
|
' --dry-run Show planned writes without changing files',
|
|
1191
1216
|
'',
|
|
1192
1217
|
'Sync transport options:',
|
|
@@ -17,6 +17,17 @@ const FORMATS = [
|
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
const FORMAT_MAP = new Map(FORMATS.map((format) => [format.name, format]));
|
|
20
|
+
const CONFLICT_SCAN_PATTERNS = [
|
|
21
|
+
'session',
|
|
22
|
+
'handoff',
|
|
23
|
+
'decisions',
|
|
24
|
+
'wip.md',
|
|
25
|
+
'review handoff',
|
|
26
|
+
'close-session',
|
|
27
|
+
'address review',
|
|
28
|
+
'builder',
|
|
29
|
+
'reviewer',
|
|
30
|
+
];
|
|
20
31
|
|
|
21
32
|
export async function syncAgentInstructionFiles(options = {}) {
|
|
22
33
|
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
@@ -26,6 +37,8 @@ export async function syncAgentInstructionFiles(options = {}) {
|
|
|
26
37
|
: cwd;
|
|
27
38
|
const requestedFormat = normalizeOptionalString(options.format);
|
|
28
39
|
const dryRun = Boolean(options.dryRun);
|
|
40
|
+
const adoptExisting = Boolean(options.adoptExisting);
|
|
41
|
+
const existingOnly = Boolean(options.existingOnly);
|
|
29
42
|
|
|
30
43
|
const projectConfig = await readProjectConfig(rootDir);
|
|
31
44
|
const selection = resolveEnabledFormats(projectConfig, requestedFormat);
|
|
@@ -56,8 +69,27 @@ export async function syncAgentInstructionFiles(options = {}) {
|
|
|
56
69
|
for (const format of selection.formats) {
|
|
57
70
|
const outputPath = path.join(toolingRootDir, format.path);
|
|
58
71
|
const existingContent = await readExistingFile(outputPath);
|
|
72
|
+
|
|
73
|
+
if (existingOnly && existingContent === null) {
|
|
74
|
+
results.push({
|
|
75
|
+
format: format.name,
|
|
76
|
+
path: outputPath,
|
|
77
|
+
relativePath: toPosix(path.relative(toolingRootDir, outputPath)),
|
|
78
|
+
status: 'skipped-missing',
|
|
79
|
+
warning: null,
|
|
80
|
+
changed: false,
|
|
81
|
+
conflicts: [],
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
const generatedContent = format.render(context);
|
|
60
|
-
const applied = applyManagedBlock(existingContent, generatedContent
|
|
87
|
+
const applied = applyManagedBlock(existingContent, generatedContent, {
|
|
88
|
+
adoptExisting,
|
|
89
|
+
});
|
|
90
|
+
const conflicts = applied.status === 'adopt'
|
|
91
|
+
? scanPotentialWorkflowConflicts(existingContent)
|
|
92
|
+
: [];
|
|
61
93
|
|
|
62
94
|
if (!dryRun && applied.warning === null && applied.content !== existingContent) {
|
|
63
95
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -71,6 +103,7 @@ export async function syncAgentInstructionFiles(options = {}) {
|
|
|
71
103
|
status: dryRun && applied.warning === null ? `dry-run-${applied.status}` : applied.status,
|
|
72
104
|
warning: applied.warning,
|
|
73
105
|
changed: applied.warning === null && applied.content !== existingContent,
|
|
106
|
+
conflicts,
|
|
74
107
|
});
|
|
75
108
|
}
|
|
76
109
|
|
|
@@ -86,6 +119,10 @@ export function getSupportedAgentFormats() {
|
|
|
86
119
|
return FORMATS.map((format) => format.name);
|
|
87
120
|
}
|
|
88
121
|
|
|
122
|
+
export function getSupportedAgentFilePaths() {
|
|
123
|
+
return FORMATS.map((format) => format.path);
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
async function readProjectConfig(rootDir) {
|
|
90
127
|
const source = await readFile(path.join(rootDir, 'project.yaml'), 'utf8');
|
|
91
128
|
return parseSimpleYaml(source, { sourceName: 'project.yaml' });
|
|
@@ -145,3 +182,22 @@ function normalizeOptionalString(value) {
|
|
|
145
182
|
function toPosix(value) {
|
|
146
183
|
return value.split(path.sep).join('/');
|
|
147
184
|
}
|
|
185
|
+
|
|
186
|
+
function scanPotentialWorkflowConflicts(content) {
|
|
187
|
+
if (typeof content !== 'string' || content.trim() === '') {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return content
|
|
192
|
+
.split(/\r?\n/)
|
|
193
|
+
.map((line, index) => ({
|
|
194
|
+
line: index + 1,
|
|
195
|
+
text: line,
|
|
196
|
+
normalized: line.toLowerCase(),
|
|
197
|
+
}))
|
|
198
|
+
.filter((entry) => CONFLICT_SCAN_PATTERNS.some((pattern) => entry.normalized.includes(pattern)))
|
|
199
|
+
.map(({ line, text }) => ({
|
|
200
|
+
line,
|
|
201
|
+
text: text.trim(),
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
@@ -6,7 +6,7 @@ export function renderManagedBlock(content) {
|
|
|
6
6
|
return `${START_MARKER}\n${trimmed}\n${END_MARKER}\n`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function applyManagedBlock(existingContent, generatedContent) {
|
|
9
|
+
export function applyManagedBlock(existingContent, generatedContent, options = {}) {
|
|
10
10
|
const block = renderManagedBlock(generatedContent);
|
|
11
11
|
|
|
12
12
|
if (existingContent === null) {
|
|
@@ -21,6 +21,15 @@ export function applyManagedBlock(existingContent, generatedContent) {
|
|
|
21
21
|
const endIndex = existingContent.indexOf(END_MARKER);
|
|
22
22
|
|
|
23
23
|
if (startIndex === -1 && endIndex === -1) {
|
|
24
|
+
if (options.adoptExisting) {
|
|
25
|
+
const separator = existingContent === '' || existingContent.endsWith('\n') ? '' : '\n';
|
|
26
|
+
return {
|
|
27
|
+
status: 'adopt',
|
|
28
|
+
content: `${existingContent}${separator}${block}`,
|
|
29
|
+
warning: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
return {
|
|
25
34
|
status: 'warning',
|
|
26
35
|
content: existingContent,
|
|
@@ -22,6 +22,10 @@ export function renderSharedInstructionBody(context, options = {}) {
|
|
|
22
22
|
'',
|
|
23
23
|
intro,
|
|
24
24
|
'',
|
|
25
|
+
`Read \`${context.rootDir}/context.md\` for the full VibeCompass workflow protocol when it exists.`,
|
|
26
|
+
'',
|
|
27
|
+
'Existing project-specific coding, style, framework, and safety instructions outside this managed block remain authoritative. This block owns only VibeCompass project-memory workflow: sessions, handoffs, decisions, docs-review, and close-out.',
|
|
28
|
+
'',
|
|
25
29
|
'## Read First',
|
|
26
30
|
`- Project memory root: \`${context.rootDir}\``,
|
|
27
31
|
'- Read `project.yaml` for project identity, repos, mode, and workflow defaults.',
|
package/src/setup.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { access, readFile, readdir, realpath } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { getSupportedAgentFilePaths } from './generators/agent-files/index.js';
|
|
7
|
+
import { END_MARKER, START_MARKER } from './generators/agent-files/markers.js';
|
|
6
8
|
|
|
7
9
|
const execFileAsync = promisify(execFile);
|
|
8
10
|
|
|
@@ -68,6 +70,14 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
68
70
|
},
|
|
69
71
|
placementPattern: resolved.placementPattern ?? null,
|
|
70
72
|
guidedSummary,
|
|
73
|
+
agentFileSyncPlan: resolved.adoptExistingAgentFiles
|
|
74
|
+
? {
|
|
75
|
+
rootDir: resolved.rootDir,
|
|
76
|
+
toolingRootDir: resolved.toolingRootDir,
|
|
77
|
+
adoptExisting: true,
|
|
78
|
+
existingOnly: true,
|
|
79
|
+
}
|
|
80
|
+
: null,
|
|
71
81
|
sessionPlan: resolved.startSession
|
|
72
82
|
? {
|
|
73
83
|
sessionId: resolved.sessionId,
|
|
@@ -220,14 +230,18 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
220
230
|
resolved.mode = setupGoal === 'share-later' ? 'local-primary' : setupGoal;
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
const detectedRepos = inferredProject.repos ?? [];
|
|
234
|
+
|
|
223
235
|
if (!Array.isArray(resolved.repos) || resolved.repos.length === 0) {
|
|
224
|
-
if (
|
|
225
|
-
const
|
|
236
|
+
if (detectedRepos.length > 0) {
|
|
237
|
+
const useDetectedRepos = await askConfirm(
|
|
226
238
|
prompter,
|
|
227
|
-
|
|
239
|
+
formatDetectedReposPrompt(detectedRepos),
|
|
228
240
|
true,
|
|
229
241
|
);
|
|
230
|
-
resolved.repos =
|
|
242
|
+
resolved.repos = useDetectedRepos
|
|
243
|
+
? detectedRepos.map((repo) => toProjectRepo(repo))
|
|
244
|
+
: await promptForRepos(prompter, cwd);
|
|
231
245
|
} else {
|
|
232
246
|
resolved.repos = await promptForRepos(prompter, cwd);
|
|
233
247
|
}
|
|
@@ -235,7 +249,15 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
235
249
|
|
|
236
250
|
let recommendation = null;
|
|
237
251
|
if (!resolved.placementPattern) {
|
|
238
|
-
if (inferredProject.
|
|
252
|
+
if (inferredProject.source === 'child-repos' && detectedRepos.length > 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
253
|
+
recommendation = recommendPlacementPattern({
|
|
254
|
+
repoCount: detectedRepos.length,
|
|
255
|
+
usesSharedWorkspace: true,
|
|
256
|
+
});
|
|
257
|
+
resolved.placementPattern = recommendation.pattern;
|
|
258
|
+
prompter.write(`Recommended placement: ${recommendation.pattern}\n`);
|
|
259
|
+
prompter.write(`${recommendation.reason}\n`);
|
|
260
|
+
} else if (detectedRepos.length === 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
239
261
|
recommendation = recommendPlacementPattern({ repoCount: 1 });
|
|
240
262
|
resolved.placementPattern = recommendation.pattern;
|
|
241
263
|
} else {
|
|
@@ -248,7 +270,9 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
if (!resolved.toolingRootDir) {
|
|
251
|
-
if (
|
|
273
|
+
if (resolved.placementPattern === PLACEMENT_PATTERNS.PRIMARY_REPO && detectedRepos.length === 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
274
|
+
resolved.toolingRootDir = detectedRepos[0].directory;
|
|
275
|
+
} else if (resolved.placementPattern === PLACEMENT_PATTERNS.WORKSPACE_ROOT && inferredProject.source === 'child-repos' && detectedRepos.length > 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
252
276
|
resolved.toolingRootDir = '.';
|
|
253
277
|
} else {
|
|
254
278
|
const ownerLabel = describePlacementOwner(resolved.placementPattern);
|
|
@@ -322,6 +346,22 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
322
346
|
}
|
|
323
347
|
}
|
|
324
348
|
|
|
349
|
+
if (resolved.adoptExistingAgentFiles === undefined) {
|
|
350
|
+
const adoptionCandidates = await findExistingUnmarkedAgentFiles({
|
|
351
|
+
cwd,
|
|
352
|
+
toolingRootDir: resolved.toolingRootDir,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (adoptionCandidates.length > 0) {
|
|
356
|
+
writeAgentAdoptionPreview(prompter, adoptionCandidates);
|
|
357
|
+
resolved.adoptExistingAgentFiles = await askConfirm(
|
|
358
|
+
prompter,
|
|
359
|
+
'Append VibeCompass managed sections to existing agent instruction files?',
|
|
360
|
+
false,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
325
365
|
if (!resolved.startSession) {
|
|
326
366
|
resolved.startSession = await askConfirm(
|
|
327
367
|
prompter,
|
|
@@ -381,26 +421,43 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
381
421
|
}
|
|
382
422
|
|
|
383
423
|
async function inferProjectFromCwd(cwd) {
|
|
384
|
-
const [packageName, gitInfo] = await Promise.all([
|
|
424
|
+
const [packageName, gitInfo, childRepos] = await Promise.all([
|
|
385
425
|
readPackageName(cwd),
|
|
386
426
|
readGitInfo(cwd),
|
|
427
|
+
discoverChildGitRepos(cwd),
|
|
387
428
|
]);
|
|
388
429
|
const projectName = packageName ?? path.basename(cwd);
|
|
389
430
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
431
|
+
if (gitInfo?.remote) {
|
|
432
|
+
const remoteSlug = extractRemoteSlug(gitInfo.remote);
|
|
433
|
+
const repoId = suggestRepoId(remoteSlug ?? packageName ?? path.basename(gitInfo.root ?? cwd));
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
projectName,
|
|
437
|
+
source: 'current-repo',
|
|
438
|
+
repos: [
|
|
439
|
+
{
|
|
440
|
+
id: repoId,
|
|
441
|
+
remote: gitInfo.remote,
|
|
442
|
+
directory: '.',
|
|
443
|
+
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
};
|
|
392
447
|
}
|
|
393
448
|
|
|
394
|
-
|
|
395
|
-
|
|
449
|
+
if (childRepos.length > 0) {
|
|
450
|
+
return {
|
|
451
|
+
projectName,
|
|
452
|
+
source: 'child-repos',
|
|
453
|
+
repos: childRepos,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
396
456
|
|
|
397
457
|
return {
|
|
398
458
|
projectName,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
remote: gitInfo.remote,
|
|
402
|
-
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
403
|
-
},
|
|
459
|
+
source: 'none',
|
|
460
|
+
repos: [],
|
|
404
461
|
};
|
|
405
462
|
}
|
|
406
463
|
|
|
@@ -437,6 +494,155 @@ async function readGitInfo(cwd) {
|
|
|
437
494
|
}
|
|
438
495
|
}
|
|
439
496
|
|
|
497
|
+
async function discoverChildGitRepos(cwd) {
|
|
498
|
+
let entries = [];
|
|
499
|
+
try {
|
|
500
|
+
entries = await readdir(cwd, { withFileTypes: true });
|
|
501
|
+
} catch {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const candidates = entries
|
|
506
|
+
.filter((entry) => entry.isDirectory() && isScannableWorkspaceChild(entry.name))
|
|
507
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
508
|
+
|
|
509
|
+
const discovered = await Promise.all(candidates.map(async (entry) => {
|
|
510
|
+
const directory = entry.name;
|
|
511
|
+
const absoluteDirectory = path.join(cwd, directory);
|
|
512
|
+
const [gitInfo, packageName] = await Promise.all([
|
|
513
|
+
readGitInfo(absoluteDirectory),
|
|
514
|
+
readPackageName(absoluteDirectory),
|
|
515
|
+
]);
|
|
516
|
+
|
|
517
|
+
if (!gitInfo?.remote) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (gitInfo.root && !(await isSameDirectory(gitInfo.root, absoluteDirectory))) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const remoteSlug = extractRemoteSlug(gitInfo.remote);
|
|
526
|
+
return {
|
|
527
|
+
id: suggestRepoId(remoteSlug ?? packageName ?? directory),
|
|
528
|
+
remote: gitInfo.remote,
|
|
529
|
+
directory,
|
|
530
|
+
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
531
|
+
};
|
|
532
|
+
}));
|
|
533
|
+
|
|
534
|
+
const reposByDirectory = new Map();
|
|
535
|
+
for (const repo of discovered) {
|
|
536
|
+
if (repo && !reposByDirectory.has(repo.directory)) {
|
|
537
|
+
reposByDirectory.set(repo.directory, repo);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return [...reposByDirectory.values()].sort((left, right) => left.directory.localeCompare(right.directory));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isScannableWorkspaceChild(name) {
|
|
545
|
+
if (!name || name.startsWith('.')) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return !new Set([
|
|
550
|
+
'build',
|
|
551
|
+
'coverage',
|
|
552
|
+
'dist',
|
|
553
|
+
'node_modules',
|
|
554
|
+
'out',
|
|
555
|
+
'target',
|
|
556
|
+
'vendor',
|
|
557
|
+
]).has(name);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function formatDetectedReposPrompt(repos) {
|
|
561
|
+
if (repos.length === 1) {
|
|
562
|
+
const repo = repos[0];
|
|
563
|
+
const location = repo.directory && repo.directory !== '.' ? ` in ${repo.directory}` : '';
|
|
564
|
+
return `Use detected Git repo${location} (${repo.remote})?`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return `Use ${repos.length} detected Git repos (${repos.map((repo) => repo.directory).join(', ')})?`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function toProjectRepo(repo) {
|
|
571
|
+
return {
|
|
572
|
+
id: repo.id,
|
|
573
|
+
remote: repo.remote,
|
|
574
|
+
...(repo.defaultBranch ? { defaultBranch: repo.defaultBranch } : {}),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function reposMatchDetected(repos, detectedRepos) {
|
|
579
|
+
if (!Array.isArray(repos) || repos.length !== detectedRepos.length) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const detectedKeys = new Set(detectedRepos.map((repo) => `${repo.id}\n${repo.remote}`));
|
|
584
|
+
return repos.every((repo) => detectedKeys.has(`${repo.id}\n${repo.remote}`));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function findExistingUnmarkedAgentFiles(context) {
|
|
588
|
+
const toolingRootDir = path.resolve(context.cwd, context.toolingRootDir ?? '.');
|
|
589
|
+
const candidates = [];
|
|
590
|
+
|
|
591
|
+
for (const relativePath of getSupportedAgentFilePaths()) {
|
|
592
|
+
const filePath = path.join(toolingRootDir, relativePath);
|
|
593
|
+
let content = null;
|
|
594
|
+
try {
|
|
595
|
+
content = await readFile(filePath, 'utf8');
|
|
596
|
+
} catch {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (content.includes(START_MARKER) || content.includes(END_MARKER)) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
candidates.push({
|
|
605
|
+
relativePath,
|
|
606
|
+
conflicts: scanPotentialAgentWorkflowConflicts(content),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return candidates;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function writeAgentAdoptionPreview(prompter, candidates) {
|
|
614
|
+
prompter.write('Existing agent instruction files without VibeCompass markers:\n');
|
|
615
|
+
for (const candidate of candidates) {
|
|
616
|
+
prompter.write(`- ${candidate.relativePath}: VibeCompass can append a managed block at the end of this file.\n`);
|
|
617
|
+
if (candidate.conflicts.length > 0) {
|
|
618
|
+
prompter.write(` possible workflow overlap on lines ${candidate.conflicts.map((conflict) => conflict.line).join(', ')}\n`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
prompter.write('Content outside the managed block stays user-owned and will not be rewritten.\n');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function scanPotentialAgentWorkflowConflicts(content) {
|
|
625
|
+
const patterns = [
|
|
626
|
+
'session',
|
|
627
|
+
'handoff',
|
|
628
|
+
'decisions',
|
|
629
|
+
'wip.md',
|
|
630
|
+
'review handoff',
|
|
631
|
+
'close-session',
|
|
632
|
+
'address review',
|
|
633
|
+
'builder',
|
|
634
|
+
'reviewer',
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
return content
|
|
638
|
+
.split(/\r?\n/)
|
|
639
|
+
.map((line, index) => ({
|
|
640
|
+
line: index + 1,
|
|
641
|
+
normalized: line.toLowerCase(),
|
|
642
|
+
}))
|
|
643
|
+
.filter((entry) => patterns.some((pattern) => entry.normalized.includes(pattern)));
|
|
644
|
+
}
|
|
645
|
+
|
|
440
646
|
async function promptForRepos(prompter, cwd) {
|
|
441
647
|
const repoCount = await askInteger(prompter, 'How many repos belong to this logical project?', {
|
|
442
648
|
defaultValue: 1,
|
|
@@ -872,6 +1078,18 @@ async function directoryExists(directoryPath) {
|
|
|
872
1078
|
}
|
|
873
1079
|
}
|
|
874
1080
|
|
|
1081
|
+
async function isSameDirectory(left, right) {
|
|
1082
|
+
try {
|
|
1083
|
+
const [leftRealPath, rightRealPath] = await Promise.all([
|
|
1084
|
+
realpath(left),
|
|
1085
|
+
realpath(right),
|
|
1086
|
+
]);
|
|
1087
|
+
return leftRealPath === rightRealPath;
|
|
1088
|
+
} catch {
|
|
1089
|
+
return path.resolve(left) === path.resolve(right);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
875
1093
|
function toRelativeIfInsideCwd(targetPath, cwd) {
|
|
876
1094
|
const absolute = path.resolve(cwd, targetPath);
|
|
877
1095
|
const relative = path.relative(cwd, absolute);
|