@vibecompass/vibecompass 0.3.4 → 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/README.md +166 -9
- package/package.json +1 -1
- package/src/cli.js +263 -0
- package/src/compatibility.js +308 -0
- package/src/index.js +5 -1
- package/src/init.js +42 -2
- package/src/manifest.js +16 -4
- package/src/read-model.js +17 -1
- package/src/refresh-workflow.js +233 -0
- package/src/scaffold.js +38 -18
- package/src/setup.js +251 -0
- package/src/status.js +350 -0
- package/src/version.js +6 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { STATE_VERSION } from './manifest.js';
|
|
4
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
5
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inspect root compatibility without writing files.
|
|
9
|
+
* `packageVersion` is a test override; production callers should omit it.
|
|
10
|
+
*/
|
|
11
|
+
export async function inspectProjectCompatibility(options = {}) {
|
|
12
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
13
|
+
const rootDir = path.resolve(cwd, options.rootDir ?? '.compass');
|
|
14
|
+
const cliPackageVersion = normalizePackageVersion(options.packageVersion ?? PACKAGE_VERSION);
|
|
15
|
+
const projectFilePath = path.join(rootDir, 'project.yaml');
|
|
16
|
+
const manifestPath = path.join(rootDir, 'state', 'manifest.json');
|
|
17
|
+
const warnings = [];
|
|
18
|
+
|
|
19
|
+
const project = await readProjectStamp(projectFilePath);
|
|
20
|
+
const manifest = await readManifestState(manifestPath);
|
|
21
|
+
const packageStatus = project.status === 'unreadable'
|
|
22
|
+
? {
|
|
23
|
+
status: 'unknown',
|
|
24
|
+
rootVersion: null,
|
|
25
|
+
cliVersion: cliPackageVersion,
|
|
26
|
+
errorMessage: project.errorMessage,
|
|
27
|
+
}
|
|
28
|
+
: buildPackageStatus(project.packageVersion, cliPackageVersion);
|
|
29
|
+
const stateStatus = buildStateStatus(manifest);
|
|
30
|
+
// Package and state statuses intentionally use different vocabularies:
|
|
31
|
+
// canonical package-stamp drift is distinct from derived-state drift.
|
|
32
|
+
|
|
33
|
+
if (packageStatus.status === 'unknown') {
|
|
34
|
+
warnings.push(warning({
|
|
35
|
+
code: 'project-yaml-unreadable',
|
|
36
|
+
message: `project.yaml is unreadable. ${packageStatus.errorMessage}`,
|
|
37
|
+
errorMessage: packageStatus.errorMessage,
|
|
38
|
+
}));
|
|
39
|
+
} else if (packageStatus.status === 'legacy') {
|
|
40
|
+
warnings.push(warning({
|
|
41
|
+
code: 'package-version-legacy-root',
|
|
42
|
+
message: 'project.yaml has no metadata.package_version stamp; treating the root as legacy/unknown.',
|
|
43
|
+
cliVersion: cliPackageVersion,
|
|
44
|
+
}));
|
|
45
|
+
} else if (packageStatus.status === 'behind') {
|
|
46
|
+
warnings.push(warning({
|
|
47
|
+
code: 'package-version-behind',
|
|
48
|
+
message: `project.yaml metadata.package_version ${packageStatus.rootVersion} is older than CLI package ${cliPackageVersion}.`,
|
|
49
|
+
rootVersion: packageStatus.rootVersion,
|
|
50
|
+
cliVersion: cliPackageVersion,
|
|
51
|
+
}));
|
|
52
|
+
} else if (packageStatus.status === 'ahead') {
|
|
53
|
+
warnings.push(warning({
|
|
54
|
+
code: 'package-version-ahead',
|
|
55
|
+
message: `project.yaml metadata.package_version ${packageStatus.rootVersion} is newer than CLI package ${cliPackageVersion}.`,
|
|
56
|
+
rootVersion: packageStatus.rootVersion,
|
|
57
|
+
cliVersion: cliPackageVersion,
|
|
58
|
+
}));
|
|
59
|
+
} else if (packageStatus.status === 'invalid') {
|
|
60
|
+
warnings.push(warning({
|
|
61
|
+
code: 'package-version-invalid',
|
|
62
|
+
message: 'project.yaml metadata.package_version is present but is not a non-empty semver string.',
|
|
63
|
+
rootVersion: packageStatus.rootVersion,
|
|
64
|
+
cliVersion: cliPackageVersion,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (stateStatus.status === 'missing') {
|
|
69
|
+
warnings.push(warning({
|
|
70
|
+
code: 'state-manifest-missing',
|
|
71
|
+
message: 'state/manifest.json is missing; commands that need derived state can regenerate it.',
|
|
72
|
+
}));
|
|
73
|
+
} else if (stateStatus.status === 'unreadable') {
|
|
74
|
+
warnings.push(warning({
|
|
75
|
+
code: 'state-manifest-unreadable',
|
|
76
|
+
message: `state/manifest.json is unreadable; commands that need derived state can discard and regenerate it. ${stateStatus.errorMessage}`,
|
|
77
|
+
errorMessage: stateStatus.errorMessage,
|
|
78
|
+
}));
|
|
79
|
+
} else if (stateStatus.status === 'unsupported') {
|
|
80
|
+
warnings.push(warning({
|
|
81
|
+
code: 'state-version-unsupported',
|
|
82
|
+
message: `state/manifest.json state_version ${stateStatus.observedVersion} is unsupported by this CLI (expected ${STATE_VERSION}); commands that need derived state can regenerate it.`,
|
|
83
|
+
observedVersion: stateStatus.observedVersion,
|
|
84
|
+
expectedVersion: STATE_VERSION,
|
|
85
|
+
packageObservedVersion: stateStatus.packageObservedVersion,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
rootDir,
|
|
91
|
+
projectFilePath,
|
|
92
|
+
manifestPath,
|
|
93
|
+
cliPackageVersion,
|
|
94
|
+
package: packageStatus,
|
|
95
|
+
state: stateStatus,
|
|
96
|
+
warnings,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatCompatibilityWarnings(result) {
|
|
101
|
+
return (result?.warnings ?? []).map((warning) => warning.message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readProjectStamp(projectFilePath) {
|
|
105
|
+
try {
|
|
106
|
+
const config = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
|
|
107
|
+
sourceName: projectFilePath,
|
|
108
|
+
});
|
|
109
|
+
const metadata = isPlainObject(config.metadata) ? config.metadata : {};
|
|
110
|
+
return {
|
|
111
|
+
status: 'ok',
|
|
112
|
+
packageVersion: Object.hasOwn(metadata, 'package_version')
|
|
113
|
+
? metadata.package_version
|
|
114
|
+
: undefined,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
status: 'unreadable',
|
|
119
|
+
packageVersion: undefined,
|
|
120
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read project.yaml.',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readManifestState(manifestPath) {
|
|
126
|
+
try {
|
|
127
|
+
return {
|
|
128
|
+
status: 'ok',
|
|
129
|
+
manifest: JSON.parse(await readFile(manifestPath, 'utf8')),
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
133
|
+
return { status: 'missing' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
status: 'unreadable',
|
|
138
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read state/manifest.json.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildPackageStatus(rootVersion, cliPackageVersion) {
|
|
144
|
+
if (rootVersion === undefined) {
|
|
145
|
+
return {
|
|
146
|
+
status: 'legacy',
|
|
147
|
+
rootVersion: null,
|
|
148
|
+
cliVersion: cliPackageVersion,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof rootVersion !== 'string' || rootVersion.trim() === '') {
|
|
153
|
+
return {
|
|
154
|
+
status: 'invalid',
|
|
155
|
+
rootVersion,
|
|
156
|
+
cliVersion: cliPackageVersion,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const normalizedRootVersion = normalizePackageVersion(rootVersion);
|
|
161
|
+
if (!parseSemver(normalizedRootVersion)) {
|
|
162
|
+
return {
|
|
163
|
+
status: 'invalid',
|
|
164
|
+
rootVersion,
|
|
165
|
+
cliVersion: cliPackageVersion,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const comparison = comparePackageVersions(normalizedRootVersion, cliPackageVersion);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
status: comparison === 0 ? 'current' : comparison < 0 ? 'behind' : 'ahead',
|
|
173
|
+
rootVersion: normalizedRootVersion,
|
|
174
|
+
cliVersion: cliPackageVersion,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildStateStatus(manifestResult) {
|
|
179
|
+
if (manifestResult.status === 'missing') {
|
|
180
|
+
return { status: 'missing' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (manifestResult.status === 'unreadable') {
|
|
184
|
+
return {
|
|
185
|
+
status: 'unreadable',
|
|
186
|
+
errorMessage: manifestResult.errorMessage,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const manifest = manifestResult.manifest;
|
|
191
|
+
if (manifest?.state_version !== STATE_VERSION) {
|
|
192
|
+
return {
|
|
193
|
+
status: 'unsupported',
|
|
194
|
+
observedVersion: manifest?.state_version ?? null,
|
|
195
|
+
expectedVersion: STATE_VERSION,
|
|
196
|
+
packageObservedVersion:
|
|
197
|
+
typeof manifest?.package?.observed_version === 'string'
|
|
198
|
+
? manifest.package.observed_version
|
|
199
|
+
: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status: 'current',
|
|
205
|
+
observedVersion: manifest.state_version,
|
|
206
|
+
expectedVersion: STATE_VERSION,
|
|
207
|
+
packageObservedVersion:
|
|
208
|
+
typeof manifest?.package?.observed_version === 'string'
|
|
209
|
+
? manifest.package.observed_version
|
|
210
|
+
: null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function comparePackageVersions(left, right) {
|
|
215
|
+
const leftSemver = parseSemver(left);
|
|
216
|
+
const rightSemver = parseSemver(right);
|
|
217
|
+
if (!leftSemver || !rightSemver) {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const leftParts = [leftSemver.major, leftSemver.minor, leftSemver.patch];
|
|
222
|
+
const rightParts = [rightSemver.major, rightSemver.minor, rightSemver.patch];
|
|
223
|
+
for (let index = 0; index < 3; index += 1) {
|
|
224
|
+
if (leftParts[index] < rightParts[index]) {
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
if (leftParts[index] > rightParts[index]) {
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return comparePrerelease(leftSemver.prerelease, rightSemver.prerelease);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseSemver(version) {
|
|
236
|
+
const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
237
|
+
if (!match) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
major: Number(match[1]),
|
|
243
|
+
minor: Number(match[2]),
|
|
244
|
+
patch: Number(match[3]),
|
|
245
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function comparePrerelease(left, right) {
|
|
250
|
+
if (left.length === 0 && right.length === 0) {
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (left.length === 0) {
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (right.length === 0) {
|
|
259
|
+
return -1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const length = Math.max(left.length, right.length);
|
|
263
|
+
for (let index = 0; index < length; index += 1) {
|
|
264
|
+
const leftPart = left[index];
|
|
265
|
+
const rightPart = right[index];
|
|
266
|
+
if (leftPart === undefined) {
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
if (rightPart === undefined) {
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
274
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
275
|
+
if (leftNumeric && rightNumeric) {
|
|
276
|
+
const difference = Number(leftPart) - Number(rightPart);
|
|
277
|
+
if (difference !== 0) {
|
|
278
|
+
return difference < 0 ? -1 : 1;
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (leftNumeric !== rightNumeric) {
|
|
284
|
+
return leftNumeric ? -1 : 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const comparison = leftPart.localeCompare(rightPart);
|
|
288
|
+
if (comparison !== 0) {
|
|
289
|
+
return comparison < 0 ? -1 : 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizePackageVersion(version) {
|
|
297
|
+
return String(version).trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isPlainObject(value) {
|
|
301
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function warning(fields) {
|
|
305
|
+
return Object.fromEntries(
|
|
306
|
+
Object.entries(fields).filter(([, value]) => value !== undefined),
|
|
307
|
+
);
|
|
308
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export { STATE_VERSION, generateStateManifest, writeStateManifest } from './manifest.js';
|
|
1
|
+
export { STATE_VERSION, generateStateManifest, prepareStateManifest, writeStateManifest } from './manifest.js';
|
|
2
|
+
export { inspectProjectCompatibility, formatCompatibilityWarnings } from './compatibility.js';
|
|
3
|
+
export { PACKAGE_VERSION } from './version.js';
|
|
4
|
+
export { refreshWorkflow } from './refresh-workflow.js';
|
|
5
|
+
export { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
|
|
2
6
|
export { initializeProjectMemory } from './init.js';
|
|
3
7
|
export { preflightDocsReview } from './docs-review.js';
|
|
4
8
|
export {
|
package/src/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { serializeProjectConfig } from './project-yaml.js';
|
|
4
4
|
import { writeStateManifest } from './manifest.js';
|
|
@@ -6,12 +6,17 @@ 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 { PACKAGE_VERSION } from './version.js';
|
|
9
10
|
|
|
10
11
|
const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
|
|
11
12
|
|
|
12
13
|
export async function initializeProjectMemory(options) {
|
|
13
14
|
const normalized = normalizeInitOptions(options);
|
|
14
15
|
|
|
16
|
+
if (normalized.force && !normalized.replaceActiveLanes) {
|
|
17
|
+
await assertNoActiveSessionLanes(normalized.rootDir);
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
await mkdir(normalized.rootDir, { recursive: true });
|
|
16
21
|
await mkdir(path.join(normalized.rootDir, 'architecture'), { recursive: true });
|
|
17
22
|
await mkdir(path.join(normalized.rootDir, 'decisions'), { recursive: true });
|
|
@@ -137,7 +142,10 @@ function normalizeInitOptions(options) {
|
|
|
137
142
|
mode: preparedOptions.mode,
|
|
138
143
|
repos,
|
|
139
144
|
...(sync ? { sync } : {}),
|
|
140
|
-
metadata: buildAgentFilesMetadata(buildWorkflowMetadata(
|
|
145
|
+
metadata: buildAgentFilesMetadata(buildWorkflowMetadata({
|
|
146
|
+
...(preparedOptions.metadata && typeof preparedOptions.metadata === 'object' ? preparedOptions.metadata : {}),
|
|
147
|
+
package_version: PACKAGE_VERSION,
|
|
148
|
+
}, {
|
|
141
149
|
gitPublish:
|
|
142
150
|
typeof preparedOptions.closeSessionGitPublish === 'boolean'
|
|
143
151
|
? preparedOptions.closeSessionGitPublish
|
|
@@ -156,12 +164,44 @@ function normalizeInitOptions(options) {
|
|
|
156
164
|
return {
|
|
157
165
|
rootDir,
|
|
158
166
|
force: Boolean(preparedOptions.force),
|
|
167
|
+
replaceActiveLanes: Boolean(preparedOptions.replaceActiveLanes),
|
|
159
168
|
generatedAt: preparedOptions.generatedAt,
|
|
160
169
|
projectConfig,
|
|
161
170
|
bootstrap: bootstrapOptions,
|
|
162
171
|
};
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
async function assertNoActiveSessionLanes(rootDir) {
|
|
175
|
+
const activeDir = path.join(rootDir, 'sessions', 'active');
|
|
176
|
+
let entries = [];
|
|
177
|
+
try {
|
|
178
|
+
entries = await readdir(activeDir, { withFileTypes: true });
|
|
179
|
+
} catch {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const activeLaneIds = [];
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isDirectory()) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const laneDir = path.join(activeDir, entry.name);
|
|
190
|
+
if (
|
|
191
|
+
(await fileExists(path.join(laneDir, 'session.yaml'))) ||
|
|
192
|
+
(await fileExists(path.join(laneDir, 'wip.md')))
|
|
193
|
+
) {
|
|
194
|
+
activeLaneIds.push(entry.name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (activeLaneIds.length > 0) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Cannot overwrite project memory while active session lanes exist: ${activeLaneIds.join(', ')}. Close the lanes first or pass --replace-active-lanes.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
function normalizeSyncOptions(syncOptions, mode) {
|
|
166
206
|
if (!syncOptions || typeof syncOptions !== 'object') {
|
|
167
207
|
throw new Error('sync configuration must be an object.');
|
package/src/manifest.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { localRevisionFromManifestHash, stableHash } from './hash.js';
|
|
4
4
|
import { scanProjectMemory } from './project-memory.js';
|
|
5
5
|
import { listProjectSessions } from './session.js';
|
|
6
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
6
7
|
|
|
7
8
|
export const STATE_VERSION = 1;
|
|
8
9
|
|
|
@@ -50,13 +51,17 @@ export function generateStateManifest(scanResult, options = {}) {
|
|
|
50
51
|
document_count: manifestInventory.length,
|
|
51
52
|
warning_count: warningCount,
|
|
52
53
|
},
|
|
54
|
+
package: {
|
|
55
|
+
observed_version: options.packageVersion ?? PACKAGE_VERSION,
|
|
56
|
+
observed_at: generatedAt,
|
|
57
|
+
},
|
|
53
58
|
documents: manifestDocuments,
|
|
54
59
|
...(options.activeSessions ? { active_sessions: options.activeSessions } : {}),
|
|
55
60
|
...(options.sync ? { sync: options.sync } : {}),
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
export async function
|
|
64
|
+
export async function prepareStateManifest(rootDir, options = {}) {
|
|
60
65
|
const scanResult = await scanProjectMemory(rootDir);
|
|
61
66
|
const activeSessions = await readActiveSessionsForManifest(rootDir);
|
|
62
67
|
const existingSync = Object.hasOwn(options, 'sync')
|
|
@@ -70,9 +75,6 @@ export async function writeStateManifest(rootDir, options = {}) {
|
|
|
70
75
|
const stateDir = path.join(rootDir, 'state');
|
|
71
76
|
const manifestPath = path.join(stateDir, 'manifest.json');
|
|
72
77
|
|
|
73
|
-
await mkdir(stateDir, { recursive: true });
|
|
74
|
-
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
75
|
-
|
|
76
78
|
return {
|
|
77
79
|
manifest,
|
|
78
80
|
manifestPath,
|
|
@@ -80,6 +82,16 @@ export async function writeStateManifest(rootDir, options = {}) {
|
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
export async function writeStateManifest(rootDir, options = {}) {
|
|
86
|
+
const prepared = await prepareStateManifest(rootDir, options);
|
|
87
|
+
const stateDir = path.dirname(prepared.manifestPath);
|
|
88
|
+
|
|
89
|
+
await mkdir(stateDir, { recursive: true });
|
|
90
|
+
await writeFile(prepared.manifestPath, `${JSON.stringify(prepared.manifest, null, 2)}\n`, 'utf8');
|
|
91
|
+
|
|
92
|
+
return prepared;
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
async function readExistingSyncState(rootDir) {
|
|
84
96
|
try {
|
|
85
97
|
const existing = JSON.parse(
|
package/src/read-model.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function loadProjectReadModel(rootDir) {
|
|
|
14
14
|
throw new Error(`Cannot build read model with canonical parse errors.\n${details}`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const existingManifest = await readManifestIfPresent(rootDir);
|
|
17
|
+
const existingManifest = normalizeReadableManifest(await readManifestIfPresent(rootDir));
|
|
18
18
|
const currentManifest = generateStateManifest(scanResult, {
|
|
19
19
|
generatedAt: existingManifest?.generated_at ?? new Date(),
|
|
20
20
|
sync: existingManifest?.sync,
|
|
@@ -555,3 +555,19 @@ async function readManifestIfPresent(rootDir) {
|
|
|
555
555
|
return null;
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
|
+
|
|
559
|
+
function normalizeReadableManifest(manifest) {
|
|
560
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!manifest.canonical || typeof manifest.canonical !== 'object') {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (typeof manifest.canonical.manifest_hash !== 'string') {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return manifest;
|
|
573
|
+
}
|