@vibecompass/vibecompass 0.7.0 → 0.9.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/CHANGELOG.md +17 -0
- package/README.md +32 -1
- package/package.json +1 -1
- package/src/cli.js +132 -4
- package/src/docs-review.js +70 -32
- package/src/init.js +171 -4
- package/src/project-memory.js +24 -5
- package/src/read-model.js +8 -4
- package/src/scaffold.js +12 -4
- package/src/setup.js +8 -6
- package/src/status.js +16 -0
- package/src/sync-binding.js +307 -0
- package/src/sync.js +31 -24
- package/src/workflows/registry.js +3 -3
package/src/init.js
CHANGED
|
@@ -6,6 +6,13 @@ import { scaffoldInitFiles } from './scaffold.js';
|
|
|
6
6
|
import { applyPlacementDefaults } from './setup.js';
|
|
7
7
|
import { buildWorkflowMetadata } from './workflow.js';
|
|
8
8
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
9
|
+
import {
|
|
10
|
+
assertValidSyncTargetName,
|
|
11
|
+
buildSyncSectionWithTargets,
|
|
12
|
+
mirrorTargetCursorToFlat,
|
|
13
|
+
readSyncTargets,
|
|
14
|
+
reconcileCursorsAfterConnect,
|
|
15
|
+
} from './sync-binding.js';
|
|
9
16
|
import { PACKAGE_VERSION } from './version.js';
|
|
10
17
|
|
|
11
18
|
const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
|
|
@@ -68,15 +75,146 @@ export async function connectHostedProjectMemory(options) {
|
|
|
68
75
|
throw new Error('connect-hosted requires project.yaml mode to be local-primary or hosted-only.');
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
const targetName = typeof options?.targetName === 'string' && options.targetName.trim() !== ''
|
|
79
|
+
? assertValidSyncTargetName(options.targetName.trim())
|
|
80
|
+
: null;
|
|
81
|
+
const existingTargets = readSyncTargets(projectConfig);
|
|
82
|
+
|
|
83
|
+
if (!targetName && existingTargets) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`This project uses named sync targets (${Object.keys(existingTargets.targets).join(', ')}). Pass --target <name> to add or update one, or use "vibecompass sync-target <name>" to switch the default.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const previousFlatSync = projectConfig.sync && typeof projectConfig.sync === 'object'
|
|
90
|
+
? projectConfig.sync
|
|
91
|
+
: null;
|
|
71
92
|
const sync = normalizeSyncOptions(options?.sync, projectConfig.mode);
|
|
72
|
-
|
|
93
|
+
|
|
94
|
+
let syncTarget = null;
|
|
95
|
+
if (targetName) {
|
|
96
|
+
const targets = { ...(existingTargets?.targets ?? {}) };
|
|
97
|
+
targets[targetName] = {
|
|
98
|
+
api_url: sync.api_url,
|
|
99
|
+
project_id: sync.project_id,
|
|
100
|
+
credential_env_var: sync.credential_env_var,
|
|
101
|
+
};
|
|
102
|
+
const defaultTarget = existingTargets?.defaultTarget ?? targetName;
|
|
103
|
+
projectConfig.sync = buildSyncSectionWithTargets(targets, defaultTarget);
|
|
104
|
+
syncTarget = { name: targetName, defaultTarget };
|
|
105
|
+
} else {
|
|
106
|
+
projectConfig.sync = sync;
|
|
107
|
+
}
|
|
108
|
+
|
|
73
109
|
await writeFile(projectFilePath, serializeProjectConfig(projectConfig), 'utf8');
|
|
74
110
|
|
|
111
|
+
// D-237: reconcile manifest cursor state with the (re)bound target —
|
|
112
|
+
// first-conversion cursor migration, dropping a rebound target's stale
|
|
113
|
+
// cursor, and re-mirroring/clearing the flat cursor when the default
|
|
114
|
+
// target's binding changed (so ≤0.7.0 CLIs can't pair the new flat binding
|
|
115
|
+
// with a previous environment's cursor).
|
|
116
|
+
if (targetName) {
|
|
117
|
+
await updateManifestSyncState(rootDir, (manifestSync) =>
|
|
118
|
+
reconcileCursorsAfterConnect(
|
|
119
|
+
manifestSync,
|
|
120
|
+
targetName,
|
|
121
|
+
{ apiUrl: sync.api_url, projectId: sync.project_id },
|
|
122
|
+
{
|
|
123
|
+
isDefault: syncTarget.defaultTarget === targetName,
|
|
124
|
+
isFirstConversion: !existingTargets,
|
|
125
|
+
flatBindingMatches: Boolean(
|
|
126
|
+
previousFlatSync &&
|
|
127
|
+
previousFlatSync.api_url === sync.api_url &&
|
|
128
|
+
previousFlatSync.project_id === sync.project_id,
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
75
135
|
return {
|
|
76
136
|
rootDir,
|
|
77
137
|
projectFilePath,
|
|
78
138
|
mode: projectConfig.mode,
|
|
79
139
|
syncEnvVar: sync.credential_env_var,
|
|
140
|
+
syncTarget,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function updateManifestSyncState(rootDir, updater) {
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = JSON.parse(
|
|
148
|
+
await readFile(path.join(rootDir, 'state', 'manifest.json'), 'utf8'),
|
|
149
|
+
);
|
|
150
|
+
} catch {
|
|
151
|
+
return; // no manifest yet — nothing to migrate
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nextSync = updater(
|
|
155
|
+
manifest?.sync && typeof manifest.sync === 'object' ? manifest.sync : {},
|
|
156
|
+
);
|
|
157
|
+
if (!nextSync) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await writeStateManifest(rootDir, { sync: nextSync });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function setDefaultSyncTarget(options) {
|
|
165
|
+
const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
166
|
+
const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
|
|
167
|
+
const projectFilePath = path.join(rootDir, 'project.yaml');
|
|
168
|
+
const projectConfig = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
|
|
169
|
+
sourceName: projectFilePath,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const named = readSyncTargets(projectConfig);
|
|
173
|
+
if (!named) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'No named sync targets are defined in project.yaml. Run "vibecompass connect-hosted --target <name>" to add one.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const targetName = typeof options?.targetName === 'string' && options.targetName.trim() !== ''
|
|
180
|
+
? options.targetName.trim()
|
|
181
|
+
: null;
|
|
182
|
+
|
|
183
|
+
if (!targetName) {
|
|
184
|
+
return {
|
|
185
|
+
rootDir,
|
|
186
|
+
projectFilePath,
|
|
187
|
+
changed: false,
|
|
188
|
+
defaultTarget: named.defaultTarget,
|
|
189
|
+
targets: named.targets,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!named.targets[targetName]) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Unknown sync target "${targetName}". Available targets: ${Object.keys(named.targets).join(', ')}.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
projectConfig.sync = buildSyncSectionWithTargets(named.targets, targetName);
|
|
200
|
+
await writeFile(projectFilePath, serializeProjectConfig(projectConfig), 'utf8');
|
|
201
|
+
|
|
202
|
+
// D-237: the flat manifest cursor mirrors the default target so ≤0.7.0 CLIs
|
|
203
|
+
// stay correct; re-mirror it from the new default (clearing it when the new
|
|
204
|
+
// default has no identity-matching cursor yet).
|
|
205
|
+
await updateManifestSyncState(rootDir, (manifestSync) =>
|
|
206
|
+
mirrorTargetCursorToFlat(manifestSync, targetName, {
|
|
207
|
+
apiUrl: named.targets[targetName].api_url,
|
|
208
|
+
projectId: named.targets[targetName].project_id,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
rootDir,
|
|
214
|
+
projectFilePath,
|
|
215
|
+
changed: named.defaultTarget !== targetName,
|
|
216
|
+
defaultTarget: targetName,
|
|
217
|
+
targets: named.targets,
|
|
80
218
|
};
|
|
81
219
|
}
|
|
82
220
|
|
|
@@ -119,13 +257,42 @@ function normalizeInitOptions(options) {
|
|
|
119
257
|
}
|
|
120
258
|
seenRepoIds.add(repo.id);
|
|
121
259
|
|
|
122
|
-
|
|
123
|
-
|
|
260
|
+
const source = typeof repo.source === 'string' && repo.source.trim() !== ''
|
|
261
|
+
? repo.source.trim()
|
|
262
|
+
: null;
|
|
263
|
+
const pathValue = typeof repo.path === 'string' && repo.path.trim() !== ''
|
|
264
|
+
? repo.path.trim()
|
|
265
|
+
: null;
|
|
266
|
+
const remote = typeof repo.remote === 'string' ? repo.remote.trim() : '';
|
|
267
|
+
|
|
268
|
+
if (source && !['git', 'local'].includes(source)) {
|
|
269
|
+
throw new Error(`repo "${repo.id}" source must be git or local.`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (source === 'local' || (!source && !remote && pathValue)) {
|
|
273
|
+
if (!['local-only', 'local-primary'].includes(preparedOptions.mode)) {
|
|
274
|
+
throw new Error(`repo "${repo.id}" local sources are supported only in local-only or local-primary mode.`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!pathValue) {
|
|
278
|
+
throw new Error(`repo "${repo.id}" source local requires a non-empty path.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
id: repo.id,
|
|
283
|
+
source: 'local',
|
|
284
|
+
path: pathValue,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!remote) {
|
|
289
|
+
throw new Error(`repo "${repo.id}" requires a non-empty remote, or use a local repo source with source: local and path.`);
|
|
124
290
|
}
|
|
125
291
|
|
|
126
292
|
return {
|
|
127
293
|
id: repo.id,
|
|
128
|
-
|
|
294
|
+
...(source === 'git' ? { source: 'git' } : {}),
|
|
295
|
+
remote,
|
|
129
296
|
...(repo.defaultBranch ? { default_branch: repo.defaultBranch } : {}),
|
|
130
297
|
};
|
|
131
298
|
});
|
package/src/project-memory.js
CHANGED
|
@@ -34,7 +34,7 @@ const KNOWN_PROJECT_FIELDS = new Set([
|
|
|
34
34
|
'sync',
|
|
35
35
|
'metadata',
|
|
36
36
|
]);
|
|
37
|
-
const KNOWN_REPO_FIELDS = new Set(['id', 'remote', 'default_branch']);
|
|
37
|
+
const KNOWN_REPO_FIELDS = new Set(['id', 'source', 'remote', 'path', 'default_branch']);
|
|
38
38
|
const KNOWN_SYNC_FIELDS = new Set([
|
|
39
39
|
'provider',
|
|
40
40
|
'api_url',
|
|
@@ -278,8 +278,25 @@ function validateProjectData(data, relativePath) {
|
|
|
278
278
|
repoIds.push(repo.id);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
const source = typeof repo.source === 'string' && repo.source.trim() !== ''
|
|
282
|
+
? repo.source.trim()
|
|
283
|
+
: null;
|
|
284
|
+
const remote = typeof repo.remote === 'string' ? repo.remote.trim() : '';
|
|
285
|
+
const sourcePath = typeof repo.path === 'string' ? repo.path.trim() : '';
|
|
286
|
+
|
|
287
|
+
if (source && !['git', 'local'].includes(source)) {
|
|
288
|
+
errors.push(createError(relativePath, 'project-invalid-repo-source', `${prefix}.source must be git or local.`));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (source === 'local' || (!source && !remote && sourcePath)) {
|
|
292
|
+
if (!['local-only', 'local-primary'].includes(data.mode)) {
|
|
293
|
+
errors.push(createError(relativePath, 'project-invalid-repo-source', `${prefix}.source local is supported only when mode is local-only or local-primary.`));
|
|
294
|
+
}
|
|
295
|
+
if (!sourcePath) {
|
|
296
|
+
errors.push(createError(relativePath, 'project-invalid-repo-path', `${prefix}.path must be a non-empty string for local sources.`));
|
|
297
|
+
}
|
|
298
|
+
} else if (!remote) {
|
|
299
|
+
errors.push(createError(relativePath, 'project-invalid-repo-remote', `${prefix}.remote must be a non-empty string for Git-backed sources.`));
|
|
283
300
|
}
|
|
284
301
|
}
|
|
285
302
|
}
|
|
@@ -336,10 +353,12 @@ function validateProjectData(data, relativePath) {
|
|
|
336
353
|
repo_ids: repoIds,
|
|
337
354
|
repos: Array.isArray(data.repos)
|
|
338
355
|
? data.repos
|
|
339
|
-
.filter((repo) => isPlainObject(repo) && typeof repo.id === 'string'
|
|
356
|
+
.filter((repo) => isPlainObject(repo) && typeof repo.id === 'string')
|
|
340
357
|
.map((repo) => ({
|
|
341
358
|
id: repo.id,
|
|
342
|
-
|
|
359
|
+
source: typeof repo.source === 'string' ? repo.source : (typeof repo.remote === 'string' ? 'git' : 'local'),
|
|
360
|
+
remote: typeof repo.remote === 'string' ? repo.remote : null,
|
|
361
|
+
path: typeof repo.path === 'string' ? repo.path : null,
|
|
343
362
|
default_branch: typeof repo.default_branch === 'string' ? repo.default_branch : null,
|
|
344
363
|
}))
|
|
345
364
|
: [],
|
package/src/read-model.js
CHANGED
|
@@ -105,7 +105,9 @@ function buildProjectSummary(scanResult, currentManifest, freshness, existingMan
|
|
|
105
105
|
const extracted = scanResult.project.extracted ?? {};
|
|
106
106
|
const repos = (extracted.repos ?? []).map((repo) => ({
|
|
107
107
|
id: repo.id,
|
|
108
|
+
source: repo.source,
|
|
108
109
|
remote: repo.remote,
|
|
110
|
+
path: repo.path,
|
|
109
111
|
default_branch: repo.default_branch,
|
|
110
112
|
}));
|
|
111
113
|
|
|
@@ -451,10 +453,12 @@ function buildRepoAliasMap(repos) {
|
|
|
451
453
|
aliases.set(repo.id, repo.id);
|
|
452
454
|
aliases.set(repo.id.toLowerCase(), repo.id);
|
|
453
455
|
|
|
454
|
-
const remoteName = repo.remote
|
|
455
|
-
.
|
|
456
|
-
|
|
457
|
-
|
|
456
|
+
const remoteName = typeof repo.remote === 'string'
|
|
457
|
+
? repo.remote
|
|
458
|
+
.replace(/\.git$/, '')
|
|
459
|
+
.split('/')
|
|
460
|
+
.pop()
|
|
461
|
+
: null;
|
|
458
462
|
|
|
459
463
|
if (remoteName) {
|
|
460
464
|
aliases.set(remoteName, repo.id);
|
package/src/scaffold.js
CHANGED
|
@@ -206,7 +206,7 @@ function generateContextMarkdown(options) {
|
|
|
206
206
|
const workflow = resolveWorkflowSettings(options.projectConfig);
|
|
207
207
|
const promptCommands = renderPromptCommandLines({ rootRelativePath }).join('\n');
|
|
208
208
|
const repos = options.projectConfig.repos
|
|
209
|
-
.map((repo) => `- \`${repo.id}\` → ${repo
|
|
209
|
+
.map((repo) => `- \`${repo.id}\` → ${formatRepoDescriptor(repo)}`)
|
|
210
210
|
.join('\n');
|
|
211
211
|
|
|
212
212
|
return `# Project Context — ${projectName}
|
|
@@ -348,7 +348,7 @@ automatically by package commands.
|
|
|
348
348
|
function generateStarterArchitectureDoc(projectConfig) {
|
|
349
349
|
const repos = projectConfig.repos.map((repo) => repo.id);
|
|
350
350
|
const repoLines = projectConfig.repos
|
|
351
|
-
.map((repo) => `- \`${repo.id}\` — ${repo
|
|
351
|
+
.map((repo) => `- \`${repo.id}\` — ${formatRepoDescriptor(repo)}${repo.default_branch ? ` (${repo.default_branch})` : ''}`)
|
|
352
352
|
.join('\n');
|
|
353
353
|
const repoEvidence = projectConfig.repos
|
|
354
354
|
.map((repo) => `- \`${repo.id}:/\` — declared repository root`)
|
|
@@ -379,7 +379,7 @@ ${repoLines}
|
|
|
379
379
|
## Coverage
|
|
380
380
|
Confirmed:
|
|
381
381
|
- Project identity and mode from \`project.yaml\`
|
|
382
|
-
- Declared repository IDs and
|
|
382
|
+
- Declared repository IDs and source descriptors from \`project.yaml\`
|
|
383
383
|
- Project-memory root layout created by \`vibecompass init\`
|
|
384
384
|
|
|
385
385
|
Not yet documented:
|
|
@@ -425,7 +425,7 @@ Do not copy this example as-is. Allocate real decision IDs from the current cano
|
|
|
425
425
|
function generateStarterSessionNote(options) {
|
|
426
426
|
const sessionDate = sessionDateFromGeneratedAt(options.generatedAt);
|
|
427
427
|
const projectConfig = options.projectConfig;
|
|
428
|
-
const repoLines = projectConfig.repos.map((repo) => `- \`${repo.id}\` — ${repo
|
|
428
|
+
const repoLines = projectConfig.repos.map((repo) => `- \`${repo.id}\` — ${formatRepoDescriptor(repo)}`).join('\n');
|
|
429
429
|
|
|
430
430
|
return `# Session — ${sessionDate}-1 — Project Memory Initialized
|
|
431
431
|
|
|
@@ -458,6 +458,14 @@ ${repoLines}
|
|
|
458
458
|
`;
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
function formatRepoDescriptor(repo) {
|
|
462
|
+
if (repo.source === 'local') {
|
|
463
|
+
return `local folder ${repo.path}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return repo.remote;
|
|
467
|
+
}
|
|
468
|
+
|
|
461
469
|
function generateArchitectureGuide(projectConfig) {
|
|
462
470
|
return `# Architecture
|
|
463
471
|
|
package/src/setup.js
CHANGED
|
@@ -139,6 +139,7 @@ export async function resolveConnectHostedCliOptions(options, environment = {})
|
|
|
139
139
|
|
|
140
140
|
return {
|
|
141
141
|
rootDir: resolved.rootDir ? toRelativeIfInsideCwd(resolved.rootDir, cwd) : undefined,
|
|
142
|
+
...(resolved.targetName ? { targetName: resolved.targetName } : {}),
|
|
142
143
|
sync: resolved.sync,
|
|
143
144
|
};
|
|
144
145
|
}
|
|
@@ -881,12 +882,13 @@ async function promptForRepos(prompter, cwd) {
|
|
|
881
882
|
|
|
882
883
|
for (let index = 0; index < repoCount; index += 1) {
|
|
883
884
|
const ordinal = index + 1;
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
885
|
+
const repoId = await askInput(prompter, `Repo ${ordinal} id`, {
|
|
886
|
+
defaultValue: repoCount === 1 ? repoIdDefault : `repo-${ordinal}`,
|
|
887
|
+
});
|
|
888
|
+
const remote = await askOptionalInput(prompter, `Repo ${ordinal} remote (blank for local folder)`);
|
|
889
|
+
const repo = remote
|
|
890
|
+
? { id: repoId, remote }
|
|
891
|
+
: { id: repoId, source: 'local', path: '.' };
|
|
890
892
|
|
|
891
893
|
const defaultBranch = await askOptionalInput(prompter, `Repo ${ordinal} default branch`);
|
|
892
894
|
if (defaultBranch) {
|
package/src/status.js
CHANGED
|
@@ -143,7 +143,9 @@ async function readProjectSummary(projectFilePath) {
|
|
|
143
143
|
.filter((repo) => repo && typeof repo === 'object')
|
|
144
144
|
.map((repo) => ({
|
|
145
145
|
id: typeof repo.id === 'string' ? repo.id : null,
|
|
146
|
+
source: typeof repo.source === 'string' ? repo.source : (typeof repo.remote === 'string' ? 'git' : 'local'),
|
|
146
147
|
remote: typeof repo.remote === 'string' ? repo.remote : null,
|
|
148
|
+
path: typeof repo.path === 'string' ? repo.path : null,
|
|
147
149
|
defaultBranch: typeof repo.default_branch === 'string' ? repo.default_branch : null,
|
|
148
150
|
}))
|
|
149
151
|
.filter((repo) => repo.id)
|
|
@@ -155,6 +157,20 @@ async function readProjectSummary(projectFilePath) {
|
|
|
155
157
|
projectId: typeof config.sync.project_id === 'string' ? config.sync.project_id : null,
|
|
156
158
|
credentialSource: typeof config.sync.credential_source === 'string' ? config.sync.credential_source : null,
|
|
157
159
|
credentialEnvVar: typeof config.sync.credential_env_var === 'string' ? config.sync.credential_env_var : null,
|
|
160
|
+
defaultTarget:
|
|
161
|
+
typeof config.sync.default_target === 'string' ? config.sync.default_target : null,
|
|
162
|
+
targets:
|
|
163
|
+
config.sync.targets && typeof config.sync.targets === 'object'
|
|
164
|
+
? Object.entries(config.sync.targets)
|
|
165
|
+
.filter(([, target]) => target && typeof target === 'object')
|
|
166
|
+
.map(([name, target]) => ({
|
|
167
|
+
name,
|
|
168
|
+
apiUrl: typeof target.api_url === 'string' ? target.api_url : null,
|
|
169
|
+
projectId: typeof target.project_id === 'string' ? target.project_id : null,
|
|
170
|
+
credentialEnvVar:
|
|
171
|
+
typeof target.credential_env_var === 'string' ? target.credential_env_var : null,
|
|
172
|
+
}))
|
|
173
|
+
: [],
|
|
158
174
|
}
|
|
159
175
|
: null,
|
|
160
176
|
};
|