genesis-compiler 1.2.22 → 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/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/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 = '',
|