genesis-compiler 1.2.21 → 1.2.23
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 +22 -1
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/src/cli.js +52 -2
- package/src/index/check.js +38 -4
- package/src/index/codex-hooks.js +70 -13
- package/src/index/init.js +20 -2
- package/src/index/migration.js +85 -0
- package/src/index/paths.js +1 -0
- package/src/index/project-format.js +138 -0
- package/src/index.js +17 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ interaction.
|
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
14
|
genesis/
|
|
15
|
+
version project-file format version used for deterministic migrations
|
|
15
16
|
blueprint.md non-technical product intent
|
|
16
17
|
engineering.md selected engineering profile and project-specific requirements
|
|
17
18
|
stack.md selected technology guidance, verification, declarations, and opaque extensions
|
|
@@ -106,7 +107,8 @@ type a command. It never runs adoption without approval and does nothing in an
|
|
|
106
107
|
empty or already-adopted project.
|
|
107
108
|
|
|
108
109
|
`genesis init` creates an empty Blueprint, a focused engineering approach, an
|
|
109
|
-
empty optional Stack, three project Agent Skills,
|
|
110
|
+
empty optional Stack, the current `genesis/version`, three project Agent Skills,
|
|
111
|
+
and project-local Codex hooks. The skills are ordinary
|
|
110
112
|
[Agent Skills](https://agentskills.io): `genesis-project`, `genesis-program`,
|
|
111
113
|
and `genesis-deslop`, each with standard `SKILL.md` and `agents/openai.yaml`
|
|
112
114
|
metadata. Open `/hooks` once in Codex to review and trust the hooks. After
|
|
@@ -473,6 +475,8 @@ genesis check
|
|
|
473
475
|
|
|
474
476
|
`check` is read-only. It reports only structural or recorded facts:
|
|
475
477
|
|
|
478
|
+
- whether the project format is current, outdated, unversioned, newer than the
|
|
479
|
+
running CLI, or invalid;
|
|
476
480
|
- Blueprint, engineering approach, and Stack validity;
|
|
477
481
|
- selected Agent Skill presence and structural validity;
|
|
478
482
|
- Program presence and structural validity;
|
|
@@ -491,6 +495,20 @@ hashes match. It does not mean that the product behavior makes sense. Generate
|
|
|
491
495
|
Genesis does not label Program semantically current. Program is ordinary
|
|
492
496
|
Markdown reviewed through Git.
|
|
493
497
|
|
|
498
|
+
When `check` reports an outdated or unversioned project, migrate its Genesis
|
|
499
|
+
files to the running CLI's project format:
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
genesis migrate
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Migration is deterministic and forward-only. It updates only recognized
|
|
506
|
+
Genesis formats, synchronizes managed skills and hooks, regenerates derived
|
|
507
|
+
indexes, and then returns a fresh `check` result. A newer project is never
|
|
508
|
+
downgraded. Every project-scoped CLI invocation warns when the recorded format
|
|
509
|
+
does not match the running CLI, and ordinary project operations stop until the
|
|
510
|
+
migration is resolved.
|
|
511
|
+
|
|
494
512
|
## Public API
|
|
495
513
|
|
|
496
514
|
```js
|
|
@@ -508,6 +526,7 @@ import {
|
|
|
508
526
|
installCodex,
|
|
509
527
|
listEngineeringProfiles,
|
|
510
528
|
listStackPieces,
|
|
529
|
+
migrate,
|
|
511
530
|
setEngineeringProfile,
|
|
512
531
|
verify,
|
|
513
532
|
} from 'genesis-compiler';
|
|
@@ -515,6 +534,8 @@ import {
|
|
|
515
534
|
|
|
516
535
|
`initialize()` installs the project files, Genesis workflow skills, selected
|
|
517
536
|
Stack skills, and local Codex hooks.
|
|
537
|
+
`migrate()` updates recognized older Genesis project-file formats and returns
|
|
538
|
+
the resulting structural check for hosts such as Vibe64.
|
|
518
539
|
`adoptProject()` also returns the initial `adopt` prompt for an existing
|
|
519
540
|
codebase. `installCodex()` installs the optional global discovery plugin.
|
|
520
541
|
`listEngineeringProfiles()`, `inspectEngineering()`, and
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
installCodex,
|
|
16
16
|
listEngineeringProfiles,
|
|
17
17
|
listStackPieces,
|
|
18
|
+
migrate,
|
|
18
19
|
setEngineeringProfile,
|
|
19
20
|
verify,
|
|
20
21
|
} from './index.js';
|
|
@@ -28,6 +29,10 @@ import {
|
|
|
28
29
|
} from './index/codex-hooks.js';
|
|
29
30
|
import { engineeringProfile, readEngineeringBaseline } from './index/engineering.js';
|
|
30
31
|
import { asDiagnostic, fail } from './index/errors.js';
|
|
32
|
+
import {
|
|
33
|
+
inspectProjectFormat,
|
|
34
|
+
projectFormatDiagnostic,
|
|
35
|
+
} from './index/project-format.js';
|
|
31
36
|
|
|
32
37
|
const USAGE = `Usage:
|
|
33
38
|
genesis init
|
|
@@ -40,6 +45,7 @@ const USAGE = `Usage:
|
|
|
40
45
|
genesis stack add <piece...>
|
|
41
46
|
genesis context <path...>
|
|
42
47
|
genesis index [function-or-path...]
|
|
48
|
+
genesis migrate
|
|
43
49
|
genesis inspect environment
|
|
44
50
|
genesis inspect section <name>
|
|
45
51
|
genesis prompt [request...]
|
|
@@ -59,7 +65,7 @@ prompt to the agent you already use. Review all edits through the ordinary Git
|
|
|
59
65
|
diff, then run genesis verify for the Stack's concrete checks.
|
|
60
66
|
`;
|
|
61
67
|
|
|
62
|
-
const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'prompt', 'stack', 'verify']);
|
|
68
|
+
const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'stack', 'verify']);
|
|
63
69
|
|
|
64
70
|
function parseCommand(argv) {
|
|
65
71
|
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
|
|
@@ -151,6 +157,9 @@ function namedItems(label, values) {
|
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
function writeCheck(result) {
|
|
160
|
+
const format = result.projectFormat;
|
|
161
|
+
const version = format.projectVersion === null ? format.status : format.projectVersion;
|
|
162
|
+
line(process.stdout, `Project format: ${version} (${format.status}; CLI supports ${format.supportedVersion})`);
|
|
154
163
|
line(process.stdout, `Blueprint: ${result.blueprint}`);
|
|
155
164
|
line(process.stdout, `Engineering approach: ${result.engineering}`);
|
|
156
165
|
line(process.stdout, `Stack: ${result.stack}`);
|
|
@@ -301,6 +310,7 @@ async function execute({ command, operands, options }, { signal } = {}) {
|
|
|
301
310
|
const projectRoot = options.projectRoot || process.cwd();
|
|
302
311
|
const stackPackages = options.stackPackages || [];
|
|
303
312
|
if (command === 'init') return initialize({ projectRoot, stackPackages });
|
|
313
|
+
if (command === 'migrate') return migrate({ projectRoot, stackPackages });
|
|
304
314
|
if (command === 'adopt') {
|
|
305
315
|
return adoptProject({ projectRoot, request: operands.join(' '), stackPackages });
|
|
306
316
|
}
|
|
@@ -401,6 +411,34 @@ async function execute({ command, operands, options }, { signal } = {}) {
|
|
|
401
411
|
fail('CLI_UNKNOWN_COMMAND', `Unknown command: ${command}`);
|
|
402
412
|
}
|
|
403
413
|
|
|
414
|
+
async function optionalProjectFormat(projectRoot) {
|
|
415
|
+
try {
|
|
416
|
+
return await inspectProjectFormat({ projectRoot });
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error?.code === 'GIT_REPOSITORY_REQUIRED') return null;
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function requiresCurrentProjectFormat(command, operands) {
|
|
424
|
+
if (['check', 'codex', 'hook', 'migrate'].includes(command)) return false;
|
|
425
|
+
if (command === 'engineering') {
|
|
426
|
+
return operands[0] === 'set' || (operands[0] === 'show' && operands.length === 1);
|
|
427
|
+
}
|
|
428
|
+
return command !== 'help';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function withProjectFormatWarning(result, projectFormat, diagnostic) {
|
|
432
|
+
if (!diagnostic) return result;
|
|
433
|
+
const warnings = [...(result.warnings || [])];
|
|
434
|
+
if (!warnings.some((warning) => warning.code === diagnostic.code)) warnings.unshift(diagnostic);
|
|
435
|
+
return {
|
|
436
|
+
...result,
|
|
437
|
+
projectFormat: result.projectFormat || projectFormat,
|
|
438
|
+
warnings,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
404
442
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
405
443
|
let options = {};
|
|
406
444
|
const controller = new AbortController();
|
|
@@ -412,12 +450,24 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
412
450
|
return 0;
|
|
413
451
|
}
|
|
414
452
|
options = parsed.options;
|
|
453
|
+
const projectFormat = await optionalProjectFormat(options.projectRoot || process.cwd());
|
|
454
|
+
const formatDiagnostic = projectFormatDiagnostic(projectFormat);
|
|
455
|
+
if (formatDiagnostic && !options.json) {
|
|
456
|
+
line(process.stderr, `WARNING: ${formatDiagnostic.message}`);
|
|
457
|
+
}
|
|
458
|
+
if (formatDiagnostic && requiresCurrentProjectFormat(parsed.command, parsed.operands)) {
|
|
459
|
+
fail(formatDiagnostic.code, formatDiagnostic.message, formatDiagnostic.details);
|
|
460
|
+
}
|
|
415
461
|
const finiteCommand = parsed.command === 'verify';
|
|
416
462
|
if (finiteCommand) {
|
|
417
463
|
process.once('SIGINT', abort);
|
|
418
464
|
process.once('SIGTERM', abort);
|
|
419
465
|
}
|
|
420
|
-
const result =
|
|
466
|
+
const result = withProjectFormatWarning(
|
|
467
|
+
await execute(parsed, { signal: controller.signal }),
|
|
468
|
+
projectFormat,
|
|
469
|
+
formatDiagnostic,
|
|
470
|
+
);
|
|
421
471
|
if (options.json) line(process.stdout, JSON.stringify(result));
|
|
422
472
|
else writeResult(parsed.command, result);
|
|
423
473
|
return ['blocked', 'failed', 'invalid'].includes(result?.status) ? 2 : 0;
|
package/src/index/check.js
CHANGED
|
@@ -9,10 +9,15 @@ import { missingStackResources } from './stack-preflight.js';
|
|
|
9
9
|
import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
|
|
10
10
|
import { inspectProjectEnvironment } from './environment-files.js';
|
|
11
11
|
import { readEngineering } from './engineering.js';
|
|
12
|
+
import {
|
|
13
|
+
inspectProjectFormatAtRoot,
|
|
14
|
+
projectFormatDiagnostic,
|
|
15
|
+
} from './project-format.js';
|
|
12
16
|
|
|
13
|
-
function invalidResult(area, error) {
|
|
17
|
+
function invalidResult(area, error, projectFormat) {
|
|
14
18
|
return {
|
|
15
19
|
status: 'invalid',
|
|
20
|
+
projectFormat,
|
|
16
21
|
blueprint: area === 'blueprint' ? 'invalid' : 'valid',
|
|
17
22
|
engineering: area === 'engineering' ? 'invalid' : 'unknown',
|
|
18
23
|
stack: area === 'stack' ? 'invalid' : 'unknown',
|
|
@@ -29,16 +34,44 @@ function invalidResult(area, error) {
|
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
function formatMismatchResult(projectFormat) {
|
|
38
|
+
const diagnostic = projectFormatDiagnostic(projectFormat);
|
|
39
|
+
const status = ['unversioned', 'outdated'].includes(projectFormat.status)
|
|
40
|
+
? 'attention'
|
|
41
|
+
: projectFormat.status === 'newer' ? 'blocked' : 'invalid';
|
|
42
|
+
return {
|
|
43
|
+
status,
|
|
44
|
+
projectFormat,
|
|
45
|
+
blueprint: 'unknown',
|
|
46
|
+
engineering: 'unknown',
|
|
47
|
+
stack: 'unknown',
|
|
48
|
+
skills: 'unknown',
|
|
49
|
+
program: 'unknown',
|
|
50
|
+
environment: 'unknown',
|
|
51
|
+
extensions: 'unknown',
|
|
52
|
+
resources: 'unknown',
|
|
53
|
+
verification: 'unknown',
|
|
54
|
+
programFiles: [],
|
|
55
|
+
subsystems: [],
|
|
56
|
+
diagnostics: [diagnostic],
|
|
57
|
+
guidance: diagnostic.message,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
export async function checkProject({
|
|
33
62
|
environment = process.env,
|
|
34
63
|
projectRoot,
|
|
35
64
|
stackPackages = [],
|
|
36
65
|
} = {}) {
|
|
37
66
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
67
|
+
const projectFormat = await inspectProjectFormatAtRoot(root);
|
|
68
|
+
if (!['current', 'uninitialized'].includes(projectFormat.status)) {
|
|
69
|
+
return formatMismatchResult(projectFormat);
|
|
70
|
+
}
|
|
38
71
|
try {
|
|
39
72
|
await readBlueprint(root, { required: true, requireDescription: true });
|
|
40
73
|
} catch (error) {
|
|
41
|
-
return invalidResult('blueprint', error);
|
|
74
|
+
return invalidResult('blueprint', error, projectFormat);
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
let stack;
|
|
@@ -46,13 +79,13 @@ export async function checkProject({
|
|
|
46
79
|
try {
|
|
47
80
|
engineering = await readEngineering(root);
|
|
48
81
|
} catch (error) {
|
|
49
|
-
return invalidResult('engineering', error);
|
|
82
|
+
return invalidResult('engineering', error, projectFormat);
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
try {
|
|
53
86
|
stack = await readStack(root, { stackPackages });
|
|
54
87
|
} catch (error) {
|
|
55
|
-
return invalidResult('stack', error);
|
|
88
|
+
return invalidResult('stack', error, projectFormat);
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
let program;
|
|
@@ -119,6 +152,7 @@ export async function checkProject({
|
|
|
119
152
|
status: program.status === 'invalid' || skills.status === 'invalid' || verification.status === 'invalid'
|
|
120
153
|
? 'invalid'
|
|
121
154
|
: needsAttention ? 'attention' : 'ok',
|
|
155
|
+
projectFormat,
|
|
122
156
|
blueprint: 'valid',
|
|
123
157
|
engineering: engineering.status,
|
|
124
158
|
stack: 'valid',
|
package/src/index/codex-hooks.js
CHANGED
|
@@ -214,6 +214,13 @@ function zeroPaths(buffer) {
|
|
|
214
214
|
return normalizedPaths(buffer.toString('utf8').split('\0'));
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
function projectPathStates(projectRoot, files) {
|
|
218
|
+
return Promise.all(files.map(async (file) => ({
|
|
219
|
+
path: file,
|
|
220
|
+
state: await pathState(path.join(projectRoot, file)),
|
|
221
|
+
})));
|
|
222
|
+
}
|
|
223
|
+
|
|
217
224
|
async function repositorySnapshot(projectRoot) {
|
|
218
225
|
const head = await runGitText(projectRoot, ['rev-parse', '--verify', 'HEAD']).catch(() => null);
|
|
219
226
|
const results = await Promise.all([
|
|
@@ -227,10 +234,7 @@ async function repositorySnapshot(projectRoot) {
|
|
|
227
234
|
.sort();
|
|
228
235
|
return {
|
|
229
236
|
head,
|
|
230
|
-
dirty: await
|
|
231
|
-
path: file,
|
|
232
|
-
state: await pathState(path.join(projectRoot, file)),
|
|
233
|
-
}))),
|
|
237
|
+
dirty: await projectPathStates(projectRoot, dirty),
|
|
234
238
|
};
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -423,19 +427,27 @@ function changedPathLines(changedPaths) {
|
|
|
423
427
|
}
|
|
424
428
|
|
|
425
429
|
function changedRepositoryLines(repositories) {
|
|
426
|
-
return repositories.flatMap(({ projectRoot, changedPaths }) => [
|
|
430
|
+
return repositories.flatMap(({ projectRoot, changedPaths, driftedPaths = [] }) => [
|
|
427
431
|
`## Repository: \`${projectRoot}\``,
|
|
428
432
|
'',
|
|
429
433
|
...changedPathLines(changedPaths),
|
|
434
|
+
...(driftedPaths.length > 0 ? [
|
|
435
|
+
'',
|
|
436
|
+
'### Concurrent changes excluded from cleanup',
|
|
437
|
+
'',
|
|
438
|
+
'These paths changed after the implementation scope was frozen. Report them, but do not edit them:',
|
|
439
|
+
'',
|
|
440
|
+
...changedPathLines(driftedPaths),
|
|
441
|
+
] : []),
|
|
430
442
|
'',
|
|
431
443
|
]);
|
|
432
444
|
}
|
|
433
445
|
|
|
434
|
-
async function changedRepositories(repositories) {
|
|
446
|
+
async function changedRepositories(repositories, { snapshotField = 'snapshot' } = {}) {
|
|
435
447
|
return (await Promise.all(repositories.map(async (repository) => {
|
|
436
448
|
const currentPaths = await changedProjectPaths(
|
|
437
449
|
repository.projectRoot,
|
|
438
|
-
repository.snapshot,
|
|
450
|
+
repository[snapshotField] || repository.snapshot,
|
|
439
451
|
await repositorySnapshot(repository.projectRoot),
|
|
440
452
|
);
|
|
441
453
|
const changedPaths = normalizedPaths([
|
|
@@ -446,6 +458,47 @@ async function changedRepositories(repositories) {
|
|
|
446
458
|
}))).filter(Boolean);
|
|
447
459
|
}
|
|
448
460
|
|
|
461
|
+
async function freezeChangedRepositories(repositories, { startPostChange = false } = {}) {
|
|
462
|
+
return Promise.all(repositories.map(async (repository) => {
|
|
463
|
+
const changedPaths = normalizedPaths(repository.changedPaths || []).sort();
|
|
464
|
+
const [pathStates, phaseSnapshot] = await Promise.all([
|
|
465
|
+
projectPathStates(repository.projectRoot, changedPaths),
|
|
466
|
+
startPostChange ? repositorySnapshot(repository.projectRoot) : null,
|
|
467
|
+
]);
|
|
468
|
+
const frozen = {
|
|
469
|
+
...repository,
|
|
470
|
+
changedPaths,
|
|
471
|
+
pathStates,
|
|
472
|
+
};
|
|
473
|
+
delete frozen.driftedPaths;
|
|
474
|
+
delete frozen.phaseSnapshot;
|
|
475
|
+
if (phaseSnapshot) frozen.phaseSnapshot = phaseSnapshot;
|
|
476
|
+
return frozen;
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function cleanupScopeRepositories(repositories) {
|
|
481
|
+
return (await Promise.all(repositories.map(async (repository) => {
|
|
482
|
+
const ownedPaths = normalizedPaths(repository.changedPaths || []).sort();
|
|
483
|
+
if (ownedPaths.length === 0) return null;
|
|
484
|
+
const expected = new Map((repository.pathStates || [])
|
|
485
|
+
.map(({ path: file, state }) => [file, state]));
|
|
486
|
+
const changedPaths = [];
|
|
487
|
+
const driftedPaths = [];
|
|
488
|
+
for (const { path: file, state } of await projectPathStates(repository.projectRoot, ownedPaths)) {
|
|
489
|
+
const destination = expected.has(file) && stableJson(expected.get(file)) !== stableJson(state)
|
|
490
|
+
? driftedPaths
|
|
491
|
+
: changedPaths;
|
|
492
|
+
destination.push(file);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
...repository,
|
|
496
|
+
changedPaths,
|
|
497
|
+
driftedPaths,
|
|
498
|
+
};
|
|
499
|
+
}))).filter(Boolean);
|
|
500
|
+
}
|
|
501
|
+
|
|
449
502
|
async function repositoryContexts(repositories) {
|
|
450
503
|
return Promise.all(repositories.map(async (repository) => {
|
|
451
504
|
const [stack, engineering] = await Promise.all([
|
|
@@ -572,13 +625,14 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
|
|
|
572
625
|
await removeTurnState(statePath, input);
|
|
573
626
|
return {};
|
|
574
627
|
}
|
|
575
|
-
const
|
|
576
|
-
if (
|
|
628
|
+
const changed = await changedRepositories(registeredRepositories(state, root));
|
|
629
|
+
if (changed.length === 0) {
|
|
577
630
|
await removeTurnState(statePath, input);
|
|
578
631
|
return {};
|
|
579
632
|
}
|
|
580
|
-
const contexts = await repositoryContexts(
|
|
633
|
+
const contexts = await repositoryContexts(changed);
|
|
581
634
|
const postChange = contexts.some(({ stack }) => Boolean(stack?.postChange?.trim()));
|
|
635
|
+
const repositories = await freezeChangedRepositories(changed, { startPostChange: postChange });
|
|
582
636
|
await writeTurnState(statePath, {
|
|
583
637
|
...state,
|
|
584
638
|
phase: postChange ? 'post-change' : 'reconcile',
|
|
@@ -598,11 +652,14 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
|
|
|
598
652
|
}
|
|
599
653
|
|
|
600
654
|
if (state.phase === 'post-change') {
|
|
601
|
-
const
|
|
602
|
-
|
|
655
|
+
const changed = await changedRepositories(registeredRepositories(state, root), {
|
|
656
|
+
snapshotField: 'phaseSnapshot',
|
|
657
|
+
});
|
|
658
|
+
if (changed.length === 0) {
|
|
603
659
|
await removeTurnState(statePath, input);
|
|
604
660
|
return {};
|
|
605
661
|
}
|
|
662
|
+
const repositories = await freezeChangedRepositories(changed);
|
|
606
663
|
await writeTurnState(statePath, {
|
|
607
664
|
...state,
|
|
608
665
|
phase: 'reconcile',
|
|
@@ -616,7 +673,7 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
|
|
|
616
673
|
|
|
617
674
|
if (state.phase === 'reconcile') {
|
|
618
675
|
const registered = registeredRepositories(state, root);
|
|
619
|
-
const repositories = await
|
|
676
|
+
const repositories = await cleanupScopeRepositories(registered);
|
|
620
677
|
if (repositories.length === 0) {
|
|
621
678
|
await refreshProjectIndexes(registered);
|
|
622
679
|
await removeTurnState(statePath, input);
|
package/src/index/init.js
CHANGED
|
@@ -6,7 +6,18 @@ import { BLUEPRINT_SKELETON_SOURCE } from './blueprint.js';
|
|
|
6
6
|
import { installCodexHooks } from './codex-hooks.js';
|
|
7
7
|
import { ENGINEERING_SKELETON_SOURCE } from './engineering.js';
|
|
8
8
|
import { gitContext } from './git.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
BLUEPRINT_PATH,
|
|
11
|
+
ENGINEERING_PATH,
|
|
12
|
+
PROGRAM_ROOT,
|
|
13
|
+
PROJECT_VERSION_PATH,
|
|
14
|
+
STACK_PATH,
|
|
15
|
+
} from './paths.js';
|
|
16
|
+
import {
|
|
17
|
+
CURRENT_PROJECT_FORMAT_SOURCE,
|
|
18
|
+
inspectProjectFormatAtRoot,
|
|
19
|
+
requireCurrentProjectFormat,
|
|
20
|
+
} from './project-format.js';
|
|
10
21
|
import { EMPTY_STACK_SOURCE, readStack } from './stack.js';
|
|
11
22
|
|
|
12
23
|
async function createIfMissing(projectRoot, relativePath, source) {
|
|
@@ -23,6 +34,8 @@ async function createIfMissing(projectRoot, relativePath, source) {
|
|
|
23
34
|
|
|
24
35
|
export async function initializeProject({ projectRoot, stackPackages = [] } = {}) {
|
|
25
36
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
37
|
+
const projectFormat = await inspectProjectFormatAtRoot(root);
|
|
38
|
+
requireCurrentProjectFormat(projectFormat, { allowUninitialized: true });
|
|
26
39
|
const created = (await Promise.all([
|
|
27
40
|
createIfMissing(root, BLUEPRINT_PATH, BLUEPRINT_SKELETON_SOURCE),
|
|
28
41
|
createIfMissing(root, ENGINEERING_PATH, ENGINEERING_SKELETON_SOURCE),
|
|
@@ -32,7 +45,12 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
|
|
|
32
45
|
const stack = await readStack(root, { stackPackages });
|
|
33
46
|
const skills = await syncProjectSkills({ projectRoot: root, stack });
|
|
34
47
|
const hooks = await installCodexHooks({ projectRoot: root });
|
|
35
|
-
const
|
|
48
|
+
const version = projectFormat.status === 'uninitialized'
|
|
49
|
+
? await createIfMissing(root, PROJECT_VERSION_PATH, CURRENT_PROJECT_FORMAT_SOURCE)
|
|
50
|
+
: null;
|
|
51
|
+
const changedFiles = [...created, ...hooks.changedFiles, ...skills.changedFiles, version]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.sort();
|
|
36
54
|
return {
|
|
37
55
|
status: changedFiles.length > 0 ? 'updated' : 'unchanged',
|
|
38
56
|
summary: changedFiles.length > 0
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readBlueprint } from './blueprint.js';
|
|
2
|
+
import { readEngineering } from './engineering.js';
|
|
3
|
+
import { asDiagnostic, GenesisError } from './errors.js';
|
|
4
|
+
import { gitContext } from './git.js';
|
|
5
|
+
import { initializeProject } from './init.js';
|
|
6
|
+
import { inspectProgram } from './program.js';
|
|
7
|
+
import {
|
|
8
|
+
CURRENT_PROJECT_FORMAT_VERSION,
|
|
9
|
+
inspectProjectFormatAtRoot,
|
|
10
|
+
projectFormatDiagnostic,
|
|
11
|
+
writeProjectFormatVersion,
|
|
12
|
+
} from './project-format.js';
|
|
13
|
+
import { PROJECT_VERSION_PATH } from './paths.js';
|
|
14
|
+
import { readStack } from './stack.js';
|
|
15
|
+
import { uniqueSorted } from './utils.js';
|
|
16
|
+
|
|
17
|
+
async function validateLegacyProject({ projectRoot, stackPackages }) {
|
|
18
|
+
try {
|
|
19
|
+
await readBlueprint(projectRoot, { required: true });
|
|
20
|
+
await readEngineering(projectRoot);
|
|
21
|
+
await readStack(projectRoot, { stackPackages });
|
|
22
|
+
try {
|
|
23
|
+
await inspectProgram(projectRoot);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error?.code !== 'PROGRAM_SOURCE_MISSING') throw error;
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new GenesisError(
|
|
29
|
+
'PROJECT_FORMAT_MIGRATION_UNAVAILABLE',
|
|
30
|
+
`Genesis cannot automatically migrate these legacy project files: ${error.message}`,
|
|
31
|
+
{ cause: asDiagnostic(error) },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MIGRATIONS = new Map([
|
|
37
|
+
[0, validateLegacyProject],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export async function migrateProject({ projectRoot, stackPackages = [] } = {}) {
|
|
41
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
42
|
+
const initial = await inspectProjectFormatAtRoot(root);
|
|
43
|
+
if (initial.status === 'uninitialized') {
|
|
44
|
+
throw new GenesisError(
|
|
45
|
+
'PROJECT_INITIALIZATION_REQUIRED',
|
|
46
|
+
'No Genesis project files exist. Run `genesis init` first.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (['invalid', 'newer'].includes(initial.status)) {
|
|
50
|
+
const diagnostic = projectFormatDiagnostic(initial);
|
|
51
|
+
throw new GenesisError(diagnostic.code, diagnostic.message, diagnostic.details);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let version = initial.status === 'unversioned' ? 0 : initial.projectVersion;
|
|
55
|
+
const migratedFiles = [];
|
|
56
|
+
while (version < CURRENT_PROJECT_FORMAT_VERSION) {
|
|
57
|
+
const migration = MIGRATIONS.get(version);
|
|
58
|
+
if (!migration) {
|
|
59
|
+
throw new GenesisError(
|
|
60
|
+
'PROJECT_FORMAT_MIGRATION_UNAVAILABLE',
|
|
61
|
+
`No migration from project format ${version} is installed.`,
|
|
62
|
+
{ projectVersion: version, supportedVersion: CURRENT_PROJECT_FORMAT_VERSION },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
await migration({ projectRoot: root, stackPackages });
|
|
66
|
+
version += 1;
|
|
67
|
+
await writeProjectFormatVersion(root, version);
|
|
68
|
+
migratedFiles.push(PROJECT_VERSION_PATH);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const initialized = await initializeProject({ projectRoot: root, stackPackages });
|
|
72
|
+
const changedFiles = uniqueSorted([...migratedFiles, ...initialized.changedFiles]);
|
|
73
|
+
const migrated = initial.status !== 'current';
|
|
74
|
+
return {
|
|
75
|
+
status: changedFiles.length > 0 ? 'updated' : 'unchanged',
|
|
76
|
+
summary: migrated
|
|
77
|
+
? `Migrated Genesis project format from ${initial.status === 'unversioned' ? 'unversioned' : initial.projectVersion} to ${CURRENT_PROJECT_FORMAT_VERSION}.`
|
|
78
|
+
: `Genesis project format ${CURRENT_PROJECT_FORMAT_VERSION} is current.`,
|
|
79
|
+
fromVersion: initial.projectVersion,
|
|
80
|
+
toVersion: CURRENT_PROJECT_FORMAT_VERSION,
|
|
81
|
+
projectFormat: await inspectProjectFormatAtRoot(root),
|
|
82
|
+
changedFiles,
|
|
83
|
+
diagnostics: initialized.diagnostics,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/index/paths.js
CHANGED
|
@@ -2,6 +2,7 @@ export const BLUEPRINT_PATH = 'genesis/blueprint.md';
|
|
|
2
2
|
export const ENGINEERING_PATH = 'genesis/engineering.md';
|
|
3
3
|
export const STACK_PATH = 'genesis/stack.md';
|
|
4
4
|
export const PROGRAM_ROOT = 'genesis/program';
|
|
5
|
+
export const PROJECT_VERSION_PATH = 'genesis/version';
|
|
5
6
|
export const VERIFICATION_PATH = '.genesis/verification.json';
|
|
6
7
|
|
|
7
8
|
export function isProjectContentPath(file) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { asDiagnostic, GenesisError } from './errors.js';
|
|
5
|
+
import { gitContext } from './git.js';
|
|
6
|
+
import {
|
|
7
|
+
BLUEPRINT_PATH,
|
|
8
|
+
ENGINEERING_PATH,
|
|
9
|
+
PROGRAM_ROOT,
|
|
10
|
+
PROJECT_VERSION_PATH,
|
|
11
|
+
STACK_PATH,
|
|
12
|
+
} from './paths.js';
|
|
13
|
+
import { normalizeSource, pathState, writeFileAtomic } from './utils.js';
|
|
14
|
+
|
|
15
|
+
export const CURRENT_PROJECT_FORMAT_VERSION = 1;
|
|
16
|
+
export const CURRENT_PROJECT_FORMAT_SOURCE = `${CURRENT_PROJECT_FORMAT_VERSION}\n`;
|
|
17
|
+
|
|
18
|
+
const VERSION_PATTERN = /^(0|[1-9][0-9]*)\n?$/u;
|
|
19
|
+
|
|
20
|
+
function projectFormat(status, projectVersion, action, diagnostic = null) {
|
|
21
|
+
return {
|
|
22
|
+
path: PROJECT_VERSION_PATH,
|
|
23
|
+
status,
|
|
24
|
+
projectVersion,
|
|
25
|
+
supportedVersion: CURRENT_PROJECT_FORMAT_VERSION,
|
|
26
|
+
action,
|
|
27
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseProjectFormatVersion(source) {
|
|
32
|
+
const normalized = normalizeSource(source);
|
|
33
|
+
const match = normalized.match(VERSION_PATTERN);
|
|
34
|
+
const version = match ? Number(match[1]) : NaN;
|
|
35
|
+
if (!Number.isSafeInteger(version)) {
|
|
36
|
+
throw new GenesisError(
|
|
37
|
+
'PROJECT_FORMAT_INVALID',
|
|
38
|
+
`${PROJECT_VERSION_PATH} must contain exactly one non-negative integer.`,
|
|
39
|
+
{ path: PROJECT_VERSION_PATH },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return version;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function hasGenesisSource(projectRoot) {
|
|
46
|
+
const states = await Promise.all([
|
|
47
|
+
BLUEPRINT_PATH,
|
|
48
|
+
ENGINEERING_PATH,
|
|
49
|
+
STACK_PATH,
|
|
50
|
+
PROGRAM_ROOT,
|
|
51
|
+
].map((relative) => pathState(path.join(projectRoot, relative))));
|
|
52
|
+
return states.some(({ exists }) => exists);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function inspectProjectFormatAtRoot(projectRoot) {
|
|
56
|
+
let source;
|
|
57
|
+
try {
|
|
58
|
+
source = await readFile(path.join(projectRoot, PROJECT_VERSION_PATH), 'utf8');
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
|
|
61
|
+
const genesisSource = await hasGenesisSource(projectRoot);
|
|
62
|
+
return projectFormat(
|
|
63
|
+
genesisSource ? 'unversioned' : 'uninitialized',
|
|
64
|
+
null,
|
|
65
|
+
genesisSource ? 'migrate' : 'init',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const diagnostic = asDiagnostic(new GenesisError(
|
|
69
|
+
'PROJECT_FORMAT_INVALID',
|
|
70
|
+
`${PROJECT_VERSION_PATH} could not be read: ${error.message}.`,
|
|
71
|
+
{ path: PROJECT_VERSION_PATH, cause: error.code || error.message },
|
|
72
|
+
));
|
|
73
|
+
return projectFormat('invalid', null, 'repair', diagnostic);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let version;
|
|
77
|
+
try {
|
|
78
|
+
version = parseProjectFormatVersion(source);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return projectFormat('invalid', null, 'repair', asDiagnostic(error));
|
|
81
|
+
}
|
|
82
|
+
if (version < CURRENT_PROJECT_FORMAT_VERSION) {
|
|
83
|
+
return projectFormat('outdated', version, 'migrate');
|
|
84
|
+
}
|
|
85
|
+
if (version > CURRENT_PROJECT_FORMAT_VERSION) {
|
|
86
|
+
return projectFormat('newer', version, 'update-genesis');
|
|
87
|
+
}
|
|
88
|
+
return projectFormat('current', version, null);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function inspectProjectFormat({ projectRoot } = {}) {
|
|
92
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
93
|
+
return inspectProjectFormatAtRoot(root);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function projectFormatDiagnostic(format) {
|
|
97
|
+
if (!format || ['current', 'uninitialized'].includes(format.status)) return null;
|
|
98
|
+
if (format.status === 'invalid') return format.diagnostic;
|
|
99
|
+
if (format.status === 'unversioned') {
|
|
100
|
+
return {
|
|
101
|
+
code: 'PROJECT_FORMAT_UNVERSIONED',
|
|
102
|
+
message: `Genesis project files are unversioned; this CLI uses project format ${format.supportedVersion}. Run \`genesis migrate\`.`,
|
|
103
|
+
details: { path: format.path, supportedVersion: format.supportedVersion },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (format.status === 'outdated') {
|
|
107
|
+
return {
|
|
108
|
+
code: 'PROJECT_FORMAT_OUTDATED',
|
|
109
|
+
message: `Project format ${format.projectVersion} is older than this CLI's format ${format.supportedVersion}. Run \`genesis migrate\`.`,
|
|
110
|
+
details: {
|
|
111
|
+
path: format.path,
|
|
112
|
+
projectVersion: format.projectVersion,
|
|
113
|
+
supportedVersion: format.supportedVersion,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
code: 'PROJECT_FORMAT_NEWER',
|
|
119
|
+
message: `Project format ${format.projectVersion} is newer than this CLI's format ${format.supportedVersion}. Update Genesis before using this project.`,
|
|
120
|
+
details: {
|
|
121
|
+
path: format.path,
|
|
122
|
+
projectVersion: format.projectVersion,
|
|
123
|
+
supportedVersion: format.supportedVersion,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function requireCurrentProjectFormat(format, { allowUninitialized = false } = {}) {
|
|
129
|
+
if (format.status === 'current' || (allowUninitialized && format.status === 'uninitialized')) return;
|
|
130
|
+
const diagnostic = projectFormatDiagnostic(format);
|
|
131
|
+
throw new GenesisError(diagnostic.code, diagnostic.message, diagnostic.details);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function writeProjectFormatVersion(projectRoot, version) {
|
|
135
|
+
const source = `${version}\n`;
|
|
136
|
+
parseProjectFormatVersion(source);
|
|
137
|
+
return writeFileAtomic(path.join(projectRoot, PROJECT_VERSION_PATH), source);
|
|
138
|
+
}
|
package/src/index.js
CHANGED
|
@@ -11,17 +11,19 @@ import {
|
|
|
11
11
|
selectEngineeringProfile,
|
|
12
12
|
} from './index/engineering.js';
|
|
13
13
|
import { initializeProject } from './index/init.js';
|
|
14
|
+
import { migrateProject } from './index/migration.js';
|
|
14
15
|
import { installCodexPlugin } from './index/codex-plugin.js';
|
|
15
16
|
import { inspectProjectEnvironment } from './index/environment-files.js';
|
|
16
17
|
import { inspectProjectStackSection } from './index/stack-section-inspection.js';
|
|
17
18
|
import { listStackCatalogPieces } from './index/stack-catalog.js';
|
|
18
19
|
import { addStackPieces, readStack } from './index/stack.js';
|
|
20
|
+
import { uniqueSorted } from './index/utils.js';
|
|
19
21
|
import { verifyProject } from './index/verification.js';
|
|
20
22
|
|
|
21
23
|
export { GENESIS_CONTRACTS };
|
|
22
24
|
|
|
23
25
|
function withIndexResult(result, index) {
|
|
24
|
-
const changedFiles =
|
|
26
|
+
const changedFiles = uniqueSorted([...result.changedFiles, ...index.changedFiles]);
|
|
25
27
|
const refreshedOnly = result.status === 'unchanged' && index.changedFiles.length > 0;
|
|
26
28
|
return {
|
|
27
29
|
...result,
|
|
@@ -42,6 +44,20 @@ export function initialize({ projectRoot = process.cwd(), stackPackages = [] } =
|
|
|
42
44
|
return initializeWithIndex(projectRoot, stackPackages);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
export async function migrate({ projectRoot = process.cwd(), stackPackages = [] } = {}) {
|
|
48
|
+
const migrated = await migrateProject({ projectRoot, stackPackages });
|
|
49
|
+
const index = await buildProjectIndex({ projectRoot, stackPackages });
|
|
50
|
+
const result = withIndexResult(migrated, index);
|
|
51
|
+
const checked = await checkProject({ projectRoot, stackPackages });
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
projectFormat: checked.projectFormat,
|
|
55
|
+
check: checked,
|
|
56
|
+
diagnostics: [...result.diagnostics, ...checked.diagnostics],
|
|
57
|
+
guidance: checked.guidance,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
export async function adoptProject({
|
|
46
62
|
projectRoot = process.cwd(),
|
|
47
63
|
request = '',
|