@vibecompass/vibecompass 0.2.1 → 0.3.1
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 +202 -19
- package/package.json +1 -1
- package/src/cli.js +548 -3
- package/src/docs-review.js +1220 -0
- package/src/generators/agent-files/template.js +2 -2
- package/src/index.js +7 -0
- package/src/init.js +67 -25
- package/src/manifest.js +52 -2
- package/src/project-memory.js +22 -3
- package/src/read-model.js +1 -1
- package/src/scaffold.js +218 -12
- package/src/session.js +180 -6
- package/src/setup.js +149 -34
- package/src/sync.js +395 -0
package/src/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { scanProjectMemory } from './project-memory.js';
|
|
3
4
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
4
5
|
import { buildCloseSessionGuidance, resolveWorkflowSettings } from './workflow.js';
|
|
5
6
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
@@ -36,6 +37,18 @@ export async function startProjectSession(options) {
|
|
|
36
37
|
const claude = await readClaudeFile(normalized.claudePath);
|
|
37
38
|
const sessionId = await resolveStartSessionId(normalized, options);
|
|
38
39
|
const lanePaths = getLanePaths(normalized, sessionId);
|
|
40
|
+
const features = normalizeStringArray(options?.features);
|
|
41
|
+
const repos = normalizeStringArray(options?.repos);
|
|
42
|
+
const claims = normalizeStringArray(options?.claims);
|
|
43
|
+
const existingLanes = await listActiveSessionLanes(normalized);
|
|
44
|
+
const overlapWarnings = buildOverlapWarnings({
|
|
45
|
+
sessionId,
|
|
46
|
+
features,
|
|
47
|
+
repos,
|
|
48
|
+
claims,
|
|
49
|
+
existingLanes,
|
|
50
|
+
});
|
|
51
|
+
const highestDecisionId = await readHighestDecisionId(normalized.rootDir);
|
|
39
52
|
|
|
40
53
|
const nextSessionNumber = await getNextSessionNumber(normalized.sessionsDir, sessionDate);
|
|
41
54
|
const currentSession = parseCurrentSessionBlock(claude.content);
|
|
@@ -68,9 +81,10 @@ export async function startProjectSession(options) {
|
|
|
68
81
|
sessionDate,
|
|
69
82
|
sessionNumber: nextSessionNumber,
|
|
70
83
|
workingOn,
|
|
71
|
-
features
|
|
72
|
-
repos
|
|
73
|
-
claims
|
|
84
|
+
features,
|
|
85
|
+
repos,
|
|
86
|
+
claims,
|
|
87
|
+
highestDecisionId,
|
|
74
88
|
}),
|
|
75
89
|
'utf8',
|
|
76
90
|
);
|
|
@@ -99,6 +113,8 @@ export async function startProjectSession(options) {
|
|
|
99
113
|
status: 'active',
|
|
100
114
|
workingOn,
|
|
101
115
|
}, { current: sessionId });
|
|
116
|
+
const auditWarnings = await buildDocsReviewWarnings(normalized);
|
|
117
|
+
const metadataWarnings = collectLaneMetadataWarnings(existingLanes);
|
|
102
118
|
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
103
119
|
rootDir: normalized.rootDir,
|
|
104
120
|
toolingRootDir: normalized.toolingRootDir,
|
|
@@ -114,6 +130,11 @@ export async function startProjectSession(options) {
|
|
|
114
130
|
handoffFilePath: lanePaths.handoffFilePath,
|
|
115
131
|
sessionDate,
|
|
116
132
|
sessionNumber: nextSessionNumber,
|
|
133
|
+
warnings: [
|
|
134
|
+
...metadataWarnings,
|
|
135
|
+
...overlapWarnings,
|
|
136
|
+
...auditWarnings,
|
|
137
|
+
],
|
|
117
138
|
agentFileSync,
|
|
118
139
|
};
|
|
119
140
|
}
|
|
@@ -314,6 +335,26 @@ async function syncAgentInstructionFilesSafely(options) {
|
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
337
|
|
|
338
|
+
async function buildDocsReviewWarnings(normalized) {
|
|
339
|
+
const markerPath = path.join(normalized.rootDir, 'state', 'docs-review.json');
|
|
340
|
+
try {
|
|
341
|
+
const marker = JSON.parse(await readFile(markerPath, 'utf8'));
|
|
342
|
+
if (marker && marker.status === 'completed') {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')) {
|
|
347
|
+
return [
|
|
348
|
+
`Docs-review marker at ${markerPath} is unreadable. Starter docs are not a comprehensive architecture review; run "vibecompass docs-review --guided" before risky implementation work.`,
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return [
|
|
354
|
+
`No docs-review marker found at ${markerPath}. Starter docs are not a comprehensive architecture review; run "vibecompass docs-review --guided" before risky implementation work.`,
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
|
|
317
358
|
function normalizeSessionPaths(options) {
|
|
318
359
|
const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
319
360
|
const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
|
|
@@ -511,6 +552,9 @@ async function listActiveSessionLanes(normalized) {
|
|
|
511
552
|
features: metadata.features,
|
|
512
553
|
repos: metadata.repos,
|
|
513
554
|
claims: metadata.claims,
|
|
555
|
+
startedAt: metadata.startedAt,
|
|
556
|
+
decisionSnapshot: metadata.decisionSnapshot,
|
|
557
|
+
warnings: metadata.warnings,
|
|
514
558
|
sessionDate: metadata.sessionDate ?? wipSession?.sessionDate ?? null,
|
|
515
559
|
sessionNumber: metadata.sessionNumber ?? wipSession?.sessionNumber ?? 0,
|
|
516
560
|
});
|
|
@@ -544,18 +588,30 @@ async function readLaneMetadata(sessionFilePath) {
|
|
|
544
588
|
features: normalizeStringArray(data.feature_slugs),
|
|
545
589
|
repos: normalizeStringArray(data.repos),
|
|
546
590
|
claims: normalizeStringArray(data.claimed_paths),
|
|
591
|
+
startedAt: normalizeOptionalString(data.started_at),
|
|
592
|
+
decisionSnapshot: {
|
|
593
|
+
highestDecisionId: parseNullableNumber(data.decision_snapshot?.highest_decision_id),
|
|
594
|
+
},
|
|
547
595
|
sessionDate: normalizeOptionalString(data.session_date),
|
|
548
596
|
sessionNumber: typeof data.session_number === 'number' ? data.session_number : Number(data.session_number) || null,
|
|
597
|
+
warnings: [],
|
|
549
598
|
};
|
|
550
|
-
} catch {
|
|
599
|
+
} catch (error) {
|
|
551
600
|
return {
|
|
552
601
|
status: null,
|
|
553
602
|
workingOn: null,
|
|
554
603
|
features: [],
|
|
555
604
|
repos: [],
|
|
556
605
|
claims: [],
|
|
606
|
+
startedAt: null,
|
|
607
|
+
decisionSnapshot: {
|
|
608
|
+
highestDecisionId: null,
|
|
609
|
+
},
|
|
557
610
|
sessionDate: null,
|
|
558
611
|
sessionNumber: null,
|
|
612
|
+
warnings: [
|
|
613
|
+
`Could not parse ${sessionFilePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
614
|
+
],
|
|
559
615
|
};
|
|
560
616
|
}
|
|
561
617
|
}
|
|
@@ -778,9 +834,9 @@ function renderLaneMetadata(options) {
|
|
|
778
834
|
renderYamlArray('feature_slugs', options.features),
|
|
779
835
|
renderYamlArray('repos', options.repos),
|
|
780
836
|
renderYamlArray('claimed_paths', options.claims),
|
|
781
|
-
`started_at: ${new Date()
|
|
837
|
+
`started_at: ${formatLocalDateTime(new Date())}`,
|
|
782
838
|
'decision_snapshot:',
|
|
783
|
-
|
|
839
|
+
` highest_decision_id: ${options.highestDecisionId ?? 'null'}`,
|
|
784
840
|
'',
|
|
785
841
|
].join('\n');
|
|
786
842
|
}
|
|
@@ -892,6 +948,124 @@ function normalizeSessionDate(value) {
|
|
|
892
948
|
].join('-');
|
|
893
949
|
}
|
|
894
950
|
|
|
951
|
+
async function readHighestDecisionId(rootDir) {
|
|
952
|
+
try {
|
|
953
|
+
const scanResult = await scanProjectMemory(rootDir);
|
|
954
|
+
const decisionIds = scanResult.documents.flatMap((document) => document.extracted?.decision_ids ?? []);
|
|
955
|
+
return decisionIds.reduce((max, id) => Math.max(max, id), 0) || null;
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function buildOverlapWarnings(options) {
|
|
962
|
+
const warnings = [];
|
|
963
|
+
for (const lane of options.existingLanes) {
|
|
964
|
+
const sharedFeatures = intersect(options.features, lane.features ?? []);
|
|
965
|
+
if (sharedFeatures.length > 0) {
|
|
966
|
+
warnings.push(
|
|
967
|
+
`Active session lane "${options.sessionId}" overlaps "${lane.id}" on feature(s): ${sharedFeatures.join(', ')}.`,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const claimOverlaps = findClaimOverlaps(options.claims, lane.claims ?? []);
|
|
972
|
+
for (const overlap of claimOverlaps) {
|
|
973
|
+
warnings.push(
|
|
974
|
+
`Active session lane "${options.sessionId}" overlaps "${lane.id}" on claimed path ${overlap.repo}:${overlap.path}.`,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const sharedRepos = intersect(options.repos, lane.repos ?? []);
|
|
979
|
+
if (sharedRepos.length > 0 && options.claims.length === 0 && (lane.claims ?? []).length === 0) {
|
|
980
|
+
warnings.push(
|
|
981
|
+
`Active session lane "${options.sessionId}" shares repo(s) ${sharedRepos.join(', ')} with "${lane.id}" without path claims; add --claim values to clarify ownership.`,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return warnings;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function collectLaneMetadataWarnings(lanes) {
|
|
990
|
+
return lanes.flatMap((lane) => lane.warnings ?? []);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function intersect(left, right) {
|
|
994
|
+
const rightSet = new Set(right);
|
|
995
|
+
return left.filter((item) => rightSet.has(item));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function findClaimOverlaps(leftClaims, rightClaims) {
|
|
999
|
+
const overlaps = [];
|
|
1000
|
+
for (const left of leftClaims.map(parseClaim).filter(Boolean)) {
|
|
1001
|
+
for (const right of rightClaims.map(parseClaim).filter(Boolean)) {
|
|
1002
|
+
if (left.repo !== right.repo) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (pathPrefixesOverlap(left.path, right.path)) {
|
|
1007
|
+
overlaps.push({
|
|
1008
|
+
repo: left.repo,
|
|
1009
|
+
path: left.path.length <= right.path.length ? left.path : right.path,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return overlaps;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function parseClaim(value) {
|
|
1019
|
+
const separatorIndex = value.indexOf(':');
|
|
1020
|
+
if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
repo: value.slice(0, separatorIndex),
|
|
1026
|
+
path: normalizeClaimPath(value.slice(separatorIndex + 1)),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function normalizeClaimPath(value) {
|
|
1031
|
+
return value.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function pathPrefixesOverlap(left, right) {
|
|
1035
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function parseNullableNumber(value) {
|
|
1039
|
+
if (value === null || value === undefined) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
1044
|
+
return Number.isFinite(number) ? number : null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function formatLocalDateTime(date) {
|
|
1048
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
1049
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
1050
|
+
const absOffset = Math.abs(offsetMinutes);
|
|
1051
|
+
const offset = `${sign}${String(Math.floor(absOffset / 60)).padStart(2, '0')}:${String(absOffset % 60).padStart(2, '0')}`;
|
|
1052
|
+
|
|
1053
|
+
return [
|
|
1054
|
+
date.getFullYear(),
|
|
1055
|
+
'-',
|
|
1056
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
1057
|
+
'-',
|
|
1058
|
+
String(date.getDate()).padStart(2, '0'),
|
|
1059
|
+
'T',
|
|
1060
|
+
String(date.getHours()).padStart(2, '0'),
|
|
1061
|
+
':',
|
|
1062
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
1063
|
+
':',
|
|
1064
|
+
String(date.getSeconds()).padStart(2, '0'),
|
|
1065
|
+
offset,
|
|
1066
|
+
].join('');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
895
1069
|
function requireNonEmptyString(value, message) {
|
|
896
1070
|
const normalized = normalizeOptionalString(value);
|
|
897
1071
|
if (!normalized) {
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
4
8
|
|
|
5
9
|
export const PLACEMENT_PATTERNS = {
|
|
6
10
|
WORKSPACE_ROOT: 'workspace-root',
|
|
@@ -73,6 +77,40 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
export async function resolveConnectHostedCliOptions(options, environment = {}) {
|
|
81
|
+
const cwd = environment.cwd ? path.resolve(environment.cwd) : process.cwd();
|
|
82
|
+
let resolved = {
|
|
83
|
+
...options,
|
|
84
|
+
sync: isPlainObject(options?.sync) ? { ...options.sync } : {},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!resolved.sync.apiUrl || !resolved.sync.projectId || !resolved.sync.credentialEnvVar) {
|
|
88
|
+
const prompter = createPrompter(environment.io, environment.runtime);
|
|
89
|
+
try {
|
|
90
|
+
if (!resolved.sync.apiUrl) {
|
|
91
|
+
resolved.sync.apiUrl = await askInput(prompter, 'Hosted sync API URL', {
|
|
92
|
+
defaultValue: 'https://vibecompass.dev',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (!resolved.sync.projectId) {
|
|
96
|
+
resolved.sync.projectId = await askInput(prompter, 'Hosted project id');
|
|
97
|
+
}
|
|
98
|
+
if (!resolved.sync.credentialEnvVar) {
|
|
99
|
+
resolved.sync.credentialEnvVar = await askInput(prompter, 'Credential env var', {
|
|
100
|
+
defaultValue: 'VIBECOMPASS_SYNC_TOKEN',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
await prompter.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
rootDir: resolved.rootDir ? toRelativeIfInsideCwd(resolved.rootDir, cwd) : undefined,
|
|
110
|
+
sync: resolved.sync,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
76
114
|
export function applyPlacementDefaults(options, environment = {}) {
|
|
77
115
|
const cwd = environment.cwd ? path.resolve(environment.cwd) : process.cwd();
|
|
78
116
|
const resolved = cloneInitOptions(options);
|
|
@@ -153,7 +191,8 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
153
191
|
let resolved = cloneInitOptions(options);
|
|
154
192
|
const cwd = environment.cwd;
|
|
155
193
|
const prompter = environment.prompter;
|
|
156
|
-
const
|
|
194
|
+
const inferredProject = await inferProjectFromCwd(cwd);
|
|
195
|
+
const projectNameDefault = inferredProject.projectName ?? path.basename(cwd);
|
|
157
196
|
|
|
158
197
|
if (!resolved.name) {
|
|
159
198
|
resolved.name = await askInput(prompter, 'Project name', {
|
|
@@ -162,63 +201,82 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
162
201
|
}
|
|
163
202
|
|
|
164
203
|
if (!resolved.mode) {
|
|
165
|
-
|
|
204
|
+
const setupGoal = await askChoice(prompter, 'What should VibeCompass set up?', [
|
|
166
205
|
{
|
|
167
|
-
value: '
|
|
168
|
-
description: 'Local
|
|
206
|
+
value: 'share-later',
|
|
207
|
+
description: 'Recommended. Local notes, with the option to share or collaborate later.',
|
|
169
208
|
},
|
|
170
209
|
{
|
|
171
|
-
value: 'local-
|
|
172
|
-
description: 'Local files
|
|
210
|
+
value: 'local-only',
|
|
211
|
+
description: 'Local files only. No hosted sync or hosted docs-review prompts.',
|
|
173
212
|
},
|
|
174
213
|
{
|
|
175
214
|
value: 'hosted-only',
|
|
176
|
-
description: '
|
|
215
|
+
description: 'Hosted VibeCompass is already the source of truth for this project.',
|
|
177
216
|
},
|
|
178
217
|
], {
|
|
179
|
-
defaultValue: '
|
|
218
|
+
defaultValue: 'share-later',
|
|
180
219
|
});
|
|
220
|
+
resolved.mode = setupGoal === 'share-later' ? 'local-primary' : setupGoal;
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
if (!Array.isArray(resolved.repos) || resolved.repos.length === 0) {
|
|
184
|
-
|
|
224
|
+
if (inferredProject.repo) {
|
|
225
|
+
const useDetectedRepo = await askConfirm(
|
|
226
|
+
prompter,
|
|
227
|
+
`Use detected Git repo ${inferredProject.repo.remote}?`,
|
|
228
|
+
true,
|
|
229
|
+
);
|
|
230
|
+
resolved.repos = useDetectedRepo ? [inferredProject.repo] : await promptForRepos(prompter, cwd);
|
|
231
|
+
} else {
|
|
232
|
+
resolved.repos = await promptForRepos(prompter, cwd);
|
|
233
|
+
}
|
|
185
234
|
}
|
|
186
235
|
|
|
187
236
|
let recommendation = null;
|
|
188
237
|
if (!resolved.placementPattern) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
238
|
+
if (inferredProject.repo && resolved.repos.length === 1) {
|
|
239
|
+
recommendation = recommendPlacementPattern({ repoCount: 1 });
|
|
240
|
+
resolved.placementPattern = recommendation.pattern;
|
|
241
|
+
} else {
|
|
242
|
+
recommendation = await promptForPlacement(prompter, {
|
|
243
|
+
cwd,
|
|
244
|
+
repoCount: resolved.repos.length,
|
|
245
|
+
});
|
|
246
|
+
resolved.placementPattern = recommendation.pattern;
|
|
247
|
+
}
|
|
194
248
|
}
|
|
195
249
|
|
|
196
250
|
if (!resolved.toolingRootDir) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
251
|
+
if (inferredProject.repo && resolved.repos.length === 1 && resolved.placementPattern === PLACEMENT_PATTERNS.PRIMARY_REPO) {
|
|
252
|
+
resolved.toolingRootDir = '.';
|
|
253
|
+
} else {
|
|
254
|
+
const ownerLabel = describePlacementOwner(resolved.placementPattern);
|
|
255
|
+
const defaultOwnerDir = await suggestOwnerDirectory({
|
|
256
|
+
cwd,
|
|
257
|
+
placementPattern: resolved.placementPattern,
|
|
258
|
+
repos: resolved.repos,
|
|
259
|
+
});
|
|
260
|
+
resolved.toolingRootDir = await askInput(prompter, ownerLabel, {
|
|
261
|
+
defaultValue: defaultOwnerDir,
|
|
262
|
+
validate: async (value) => {
|
|
263
|
+
if (!(await directoryExists(path.resolve(cwd, value)))) {
|
|
264
|
+
return `${ownerLabel} must already exist relative to the current working directory.`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return null;
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
213
271
|
}
|
|
214
272
|
|
|
215
273
|
resolved = applyPlacementDefaults(resolved, { cwd });
|
|
216
274
|
|
|
217
|
-
if (!resolved.sync && resolved.mode === '
|
|
275
|
+
if (!resolved.sync && resolved.mode === 'hosted-only') {
|
|
218
276
|
const configureSync = await askConfirm(
|
|
219
277
|
prompter,
|
|
220
|
-
'
|
|
221
|
-
|
|
278
|
+
'Connect hosted VibeCompass now?',
|
|
279
|
+
resolved.mode === 'hosted-only',
|
|
222
280
|
);
|
|
223
281
|
|
|
224
282
|
if (configureSync) {
|
|
@@ -322,6 +380,63 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
322
380
|
};
|
|
323
381
|
}
|
|
324
382
|
|
|
383
|
+
async function inferProjectFromCwd(cwd) {
|
|
384
|
+
const [packageName, gitInfo] = await Promise.all([
|
|
385
|
+
readPackageName(cwd),
|
|
386
|
+
readGitInfo(cwd),
|
|
387
|
+
]);
|
|
388
|
+
const projectName = packageName ?? path.basename(cwd);
|
|
389
|
+
|
|
390
|
+
if (!gitInfo?.remote) {
|
|
391
|
+
return { projectName, repo: null };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const remoteSlug = extractRemoteSlug(gitInfo.remote);
|
|
395
|
+
const repoId = suggestRepoId(remoteSlug ?? packageName ?? path.basename(gitInfo.root ?? cwd));
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
projectName,
|
|
399
|
+
repo: {
|
|
400
|
+
id: repoId,
|
|
401
|
+
remote: gitInfo.remote,
|
|
402
|
+
...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function readPackageName(cwd) {
|
|
408
|
+
try {
|
|
409
|
+
const packageJson = JSON.parse(await readFile(path.join(cwd, 'package.json'), 'utf8'));
|
|
410
|
+
return typeof packageJson.name === 'string' && packageJson.name.trim()
|
|
411
|
+
? packageJson.name.trim()
|
|
412
|
+
: null;
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function readGitInfo(cwd) {
|
|
419
|
+
try {
|
|
420
|
+
const [{ stdout: remote }, { stdout: root }, branchResult] = await Promise.all([
|
|
421
|
+
execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd }),
|
|
422
|
+
execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd }),
|
|
423
|
+
execFileAsync('git', ['branch', '--show-current'], { cwd }).catch(() => ({ stdout: '' })),
|
|
424
|
+
]);
|
|
425
|
+
const normalizedRemote = remote.trim();
|
|
426
|
+
if (!normalizedRemote) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
remote: normalizedRemote,
|
|
432
|
+
root: root.trim() || null,
|
|
433
|
+
branch: branchResult.stdout.trim() || null,
|
|
434
|
+
};
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
325
440
|
async function promptForRepos(prompter, cwd) {
|
|
326
441
|
const repoCount = await askInteger(prompter, 'How many repos belong to this logical project?', {
|
|
327
442
|
defaultValue: 1,
|