@vibecompass/vibecompass 0.3.1 → 0.3.3
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 +8 -5
- package/package.json +1 -1
- package/src/setup.js +150 -17
package/README.md
CHANGED
|
@@ -327,11 +327,14 @@ Guided init can also record workflow defaults in
|
|
|
327
327
|
`project.yaml.metadata.workflow`, including whether close-session should
|
|
328
328
|
include a Git publish step and which remote name to use for that step.
|
|
329
329
|
|
|
330
|
-
Guided init is inference-first for
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
330
|
+
Guided init is inference-first for common repo layouts: it reads `package.json`,
|
|
331
|
+
detects the current Git `origin` when you run it inside a repo, or scans a
|
|
332
|
+
parent workspace for immediate child repos when the parent is not itself a Git
|
|
333
|
+
checkout. It asks once before using detected repos, maps a friendly setup goal
|
|
334
|
+
to the stored project mode, places single-child repo memory under that child
|
|
335
|
+
repo, and uses workspace-root placement for multi-repo parent folders. Hosted
|
|
336
|
+
credentials are deferred by default; run `vibecompass connect-hosted` when you
|
|
337
|
+
are ready to connect the local root to `vibecompass.dev`.
|
|
335
338
|
|
|
336
339
|
### `vibecompass connect-hosted`
|
|
337
340
|
|
package/package.json
CHANGED
package/src/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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';
|
|
@@ -220,14 +220,18 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
220
220
|
resolved.mode = setupGoal === 'share-later' ? 'local-primary' : setupGoal;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
const detectedRepos = inferredProject.repos ?? [];
|
|
224
|
+
|
|
223
225
|
if (!Array.isArray(resolved.repos) || resolved.repos.length === 0) {
|
|
224
|
-
if (
|
|
225
|
-
const
|
|
226
|
+
if (detectedRepos.length > 0) {
|
|
227
|
+
const useDetectedRepos = await askConfirm(
|
|
226
228
|
prompter,
|
|
227
|
-
|
|
229
|
+
formatDetectedReposPrompt(detectedRepos),
|
|
228
230
|
true,
|
|
229
231
|
);
|
|
230
|
-
resolved.repos =
|
|
232
|
+
resolved.repos = useDetectedRepos
|
|
233
|
+
? detectedRepos.map((repo) => toProjectRepo(repo))
|
|
234
|
+
: await promptForRepos(prompter, cwd);
|
|
231
235
|
} else {
|
|
232
236
|
resolved.repos = await promptForRepos(prompter, cwd);
|
|
233
237
|
}
|
|
@@ -235,7 +239,15 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
235
239
|
|
|
236
240
|
let recommendation = null;
|
|
237
241
|
if (!resolved.placementPattern) {
|
|
238
|
-
if (inferredProject.
|
|
242
|
+
if (inferredProject.source === 'child-repos' && detectedRepos.length > 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
243
|
+
recommendation = recommendPlacementPattern({
|
|
244
|
+
repoCount: detectedRepos.length,
|
|
245
|
+
usesSharedWorkspace: true,
|
|
246
|
+
});
|
|
247
|
+
resolved.placementPattern = recommendation.pattern;
|
|
248
|
+
prompter.write(`Recommended placement: ${recommendation.pattern}\n`);
|
|
249
|
+
prompter.write(`${recommendation.reason}\n`);
|
|
250
|
+
} else if (detectedRepos.length === 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
239
251
|
recommendation = recommendPlacementPattern({ repoCount: 1 });
|
|
240
252
|
resolved.placementPattern = recommendation.pattern;
|
|
241
253
|
} else {
|
|
@@ -248,7 +260,9 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
248
260
|
}
|
|
249
261
|
|
|
250
262
|
if (!resolved.toolingRootDir) {
|
|
251
|
-
if (
|
|
263
|
+
if (resolved.placementPattern === PLACEMENT_PATTERNS.PRIMARY_REPO && detectedRepos.length === 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
264
|
+
resolved.toolingRootDir = detectedRepos[0].directory;
|
|
265
|
+
} else if (resolved.placementPattern === PLACEMENT_PATTERNS.WORKSPACE_ROOT && inferredProject.source === 'child-repos' && detectedRepos.length > 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
|
|
252
266
|
resolved.toolingRootDir = '.';
|
|
253
267
|
} else {
|
|
254
268
|
const ownerLabel = describePlacementOwner(resolved.placementPattern);
|
|
@@ -381,26 +395,43 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
381
395
|
}
|
|
382
396
|
|
|
383
397
|
async function inferProjectFromCwd(cwd) {
|
|
384
|
-
const [packageName, gitInfo] = await Promise.all([
|
|
398
|
+
const [packageName, gitInfo, childRepos] = await Promise.all([
|
|
385
399
|
readPackageName(cwd),
|
|
386
400
|
readGitInfo(cwd),
|
|
401
|
+
discoverChildGitRepos(cwd),
|
|
387
402
|
]);
|
|
388
403
|
const projectName = packageName ?? path.basename(cwd);
|
|
389
404
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
405
|
+
if (gitInfo?.remote) {
|
|
406
|
+
const remoteSlug = extractRemoteSlug(gitInfo.remote);
|
|
407
|
+
const repoId = suggestRepoId(remoteSlug ?? packageName ?? path.basename(gitInfo.root ?? cwd));
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
projectName,
|
|
411
|
+
source: 'current-repo',
|
|
412
|
+
repos: [
|
|
413
|
+
{
|
|
414
|
+
id: repoId,
|
|
415
|
+
remote: gitInfo.remote,
|
|
416
|
+
directory: '.',
|
|
417
|
+
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
392
421
|
}
|
|
393
422
|
|
|
394
|
-
|
|
395
|
-
|
|
423
|
+
if (childRepos.length > 0) {
|
|
424
|
+
return {
|
|
425
|
+
projectName,
|
|
426
|
+
source: 'child-repos',
|
|
427
|
+
repos: childRepos,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
396
430
|
|
|
397
431
|
return {
|
|
398
432
|
projectName,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
remote: gitInfo.remote,
|
|
402
|
-
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
403
|
-
},
|
|
433
|
+
source: 'none',
|
|
434
|
+
repos: [],
|
|
404
435
|
};
|
|
405
436
|
}
|
|
406
437
|
|
|
@@ -437,6 +468,96 @@ async function readGitInfo(cwd) {
|
|
|
437
468
|
}
|
|
438
469
|
}
|
|
439
470
|
|
|
471
|
+
async function discoverChildGitRepos(cwd) {
|
|
472
|
+
let entries = [];
|
|
473
|
+
try {
|
|
474
|
+
entries = await readdir(cwd, { withFileTypes: true });
|
|
475
|
+
} catch {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const candidates = entries
|
|
480
|
+
.filter((entry) => entry.isDirectory() && isScannableWorkspaceChild(entry.name))
|
|
481
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
482
|
+
|
|
483
|
+
const discovered = await Promise.all(candidates.map(async (entry) => {
|
|
484
|
+
const directory = entry.name;
|
|
485
|
+
const absoluteDirectory = path.join(cwd, directory);
|
|
486
|
+
const [gitInfo, packageName] = await Promise.all([
|
|
487
|
+
readGitInfo(absoluteDirectory),
|
|
488
|
+
readPackageName(absoluteDirectory),
|
|
489
|
+
]);
|
|
490
|
+
|
|
491
|
+
if (!gitInfo?.remote) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (gitInfo.root && !(await isSameDirectory(gitInfo.root, absoluteDirectory))) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const remoteSlug = extractRemoteSlug(gitInfo.remote);
|
|
500
|
+
return {
|
|
501
|
+
id: suggestRepoId(remoteSlug ?? packageName ?? directory),
|
|
502
|
+
remote: gitInfo.remote,
|
|
503
|
+
directory,
|
|
504
|
+
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
505
|
+
};
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
const reposByDirectory = new Map();
|
|
509
|
+
for (const repo of discovered) {
|
|
510
|
+
if (repo && !reposByDirectory.has(repo.directory)) {
|
|
511
|
+
reposByDirectory.set(repo.directory, repo);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return [...reposByDirectory.values()].sort((left, right) => left.directory.localeCompare(right.directory));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function isScannableWorkspaceChild(name) {
|
|
519
|
+
if (!name || name.startsWith('.')) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return !new Set([
|
|
524
|
+
'build',
|
|
525
|
+
'coverage',
|
|
526
|
+
'dist',
|
|
527
|
+
'node_modules',
|
|
528
|
+
'out',
|
|
529
|
+
'target',
|
|
530
|
+
'vendor',
|
|
531
|
+
]).has(name);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function formatDetectedReposPrompt(repos) {
|
|
535
|
+
if (repos.length === 1) {
|
|
536
|
+
const repo = repos[0];
|
|
537
|
+
const location = repo.directory && repo.directory !== '.' ? ` in ${repo.directory}` : '';
|
|
538
|
+
return `Use detected Git repo${location} (${repo.remote})?`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return `Use ${repos.length} detected Git repos (${repos.map((repo) => repo.directory).join(', ')})?`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function toProjectRepo(repo) {
|
|
545
|
+
return {
|
|
546
|
+
id: repo.id,
|
|
547
|
+
remote: repo.remote,
|
|
548
|
+
...(repo.defaultBranch ? { defaultBranch: repo.defaultBranch } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function reposMatchDetected(repos, detectedRepos) {
|
|
553
|
+
if (!Array.isArray(repos) || repos.length !== detectedRepos.length) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const detectedKeys = new Set(detectedRepos.map((repo) => `${repo.id}\n${repo.remote}`));
|
|
558
|
+
return repos.every((repo) => detectedKeys.has(`${repo.id}\n${repo.remote}`));
|
|
559
|
+
}
|
|
560
|
+
|
|
440
561
|
async function promptForRepos(prompter, cwd) {
|
|
441
562
|
const repoCount = await askInteger(prompter, 'How many repos belong to this logical project?', {
|
|
442
563
|
defaultValue: 1,
|
|
@@ -872,6 +993,18 @@ async function directoryExists(directoryPath) {
|
|
|
872
993
|
}
|
|
873
994
|
}
|
|
874
995
|
|
|
996
|
+
async function isSameDirectory(left, right) {
|
|
997
|
+
try {
|
|
998
|
+
const [leftRealPath, rightRealPath] = await Promise.all([
|
|
999
|
+
realpath(left),
|
|
1000
|
+
realpath(right),
|
|
1001
|
+
]);
|
|
1002
|
+
return leftRealPath === rightRealPath;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return path.resolve(left) === path.resolve(right);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
875
1008
|
function toRelativeIfInsideCwd(targetPath, cwd) {
|
|
876
1009
|
const absolute = path.resolve(cwd, targetPath);
|
|
877
1010
|
const relative = path.relative(cwd, absolute);
|