genesis-compiler 1.10.5 → 1.10.6
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
CHANGED
|
@@ -264,8 +264,10 @@ files alone does not refresh its existing context.
|
|
|
264
264
|
|
|
265
265
|
Project-scoped options follow the command, for example
|
|
266
266
|
`genesis check --project-root /path/to/project`. Commands support `--json` when
|
|
267
|
-
a host or script needs the normalized result.
|
|
268
|
-
|
|
267
|
+
a host or script needs the normalized result. A failed command prints its
|
|
268
|
+
diagnostic code and message on standard error; when the failure came from a
|
|
269
|
+
child process such as Git, its bounded output follows, so the actual reason is
|
|
270
|
+
visible without `--json`. Run `genesis --help` for the compact CLI synopsis.
|
|
269
271
|
|
|
270
272
|
## Collaboration approach
|
|
271
273
|
|
package/docs/templates.md
CHANGED
|
@@ -83,7 +83,14 @@ The framework starters live in `seed-django`, `seed-rails`, `seed-spring-boot`,
|
|
|
83
83
|
source, dependency lockfile, setup/run contracts and focused checks.
|
|
84
84
|
|
|
85
85
|
Applying fetches the explicitly named branch once, resolves its commit and reads
|
|
86
|
-
its ordinary Git blobs into a temporary project.
|
|
86
|
+
its ordinary Git blobs into a temporary project. Both scratch repositories, the
|
|
87
|
+
fetched source and the prepared project, live inside the destination's Git-local
|
|
88
|
+
`genesis-template.lock` directory (`git rev-parse --git-path genesis-template.lock`),
|
|
89
|
+
never in the system temporary directory or the work tree. Every Git command
|
|
90
|
+
therefore runs inside the destination repository, which hosts that confine Git
|
|
91
|
+
to the project accept, and the scratch is removed with the lock when the
|
|
92
|
+
operation succeeds or fails. A linked worktree keeps its lock and scratch in its
|
|
93
|
+
own private Git directory. Genesis rejects arbitrary links,
|
|
87
94
|
submodules, unsafe paths, dependencies and private dotenv files in the template.
|
|
88
95
|
The only link exception is the exact generated Claude skill discovery link
|
|
89
96
|
`.claude/skills/<name>` → `../../.agents/skills/<name>`. Incoming discovery links
|
|
@@ -94,9 +101,11 @@ generated links without following links supplied by a template.
|
|
|
94
101
|
It validates the prepared project before changing the destination. A project
|
|
95
102
|
lock serializes competing imports; the source and authored bootstrap files are
|
|
96
103
|
checked again immediately before the copy. An ordinary copy failure rolls back
|
|
97
|
-
written files.
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
written files. `SIGINT` and `SIGTERM` let the CLI finish that cleanup. If a process
|
|
105
|
+
is forcibly killed, inspect any partial work and the Git-local
|
|
106
|
+
`genesis-template.lock` before removing the abandoned lock, and the scratch inside
|
|
107
|
+
it, and retrying; Genesis never treats partial application code as an empty
|
|
108
|
+
destination.
|
|
100
109
|
|
|
101
110
|
The destination keeps its Git history, remotes, branch, meaningful Blueprint,
|
|
102
111
|
collaboration/engineering choices, and authored Stack operation sections.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -233,6 +233,14 @@ function line(stream, value = '') {
|
|
|
233
233
|
stream.write(`${String(value).replace(/\n$/u, '')}\n`);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// A failed command's bounded output usually carries its actual reason.
|
|
237
|
+
function diagnosticOutput(stream, item) {
|
|
238
|
+
for (const field of ['stdout', 'stderr']) {
|
|
239
|
+
const output = item.details?.[field];
|
|
240
|
+
if (typeof output === 'string' && output.trim()) line(stream, output.trim());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
236
244
|
function namedItems(label, values) {
|
|
237
245
|
if (!Array.isArray(values) || values.length === 0) return;
|
|
238
246
|
line(process.stdout, `${label}:`);
|
|
@@ -413,10 +421,7 @@ function writeResult(command, result) {
|
|
|
413
421
|
namedItems('Verification commands', result.commands?.map(({ label, argv }) => `${label}: ${argv.join(' ')}`));
|
|
414
422
|
for (const item of result.diagnostics || []) {
|
|
415
423
|
line(process.stdout, `${item.code}: ${item.message}`);
|
|
416
|
-
|
|
417
|
-
const output = item.details?.[field]?.trim();
|
|
418
|
-
if (output) line(process.stdout, output);
|
|
419
|
-
}
|
|
424
|
+
diagnosticOutput(process.stdout, item);
|
|
420
425
|
}
|
|
421
426
|
if (result.guidance) line(process.stdout, result.guidance);
|
|
422
427
|
if (result.prompt) {
|
|
@@ -711,7 +716,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
711
716
|
} catch (error) {
|
|
712
717
|
const diagnostic = asDiagnostic(error);
|
|
713
718
|
if (options.json) line(process.stderr, JSON.stringify(diagnostic));
|
|
714
|
-
else
|
|
719
|
+
else {
|
|
720
|
+
line(process.stderr, `${diagnostic.code}: ${diagnostic.message}`);
|
|
721
|
+
diagnosticOutput(process.stderr, diagnostic);
|
|
722
|
+
}
|
|
715
723
|
return 1;
|
|
716
724
|
} finally {
|
|
717
725
|
process.removeListener('SIGINT', abort);
|
|
@@ -64,7 +64,7 @@ async function installedCatalogs(stackPackages, projectRoot) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Catalogues are explicit data sources. A Stack or Skill never implicitly installs a template. */
|
|
67
|
-
export async function listTemplates({ projectRoot = process.cwd(), stackPackages = [], templateSources = [] } = {}) {
|
|
67
|
+
export async function listTemplates({ projectRoot = process.cwd(), stackPackages = [], templateSources = [], scratchRoot } = {}) {
|
|
68
68
|
let recorded = [];
|
|
69
69
|
try { recorded = (await readStack(projectRoot, { stackPackages })).stackPackages; } catch { /* Explicit sources also work before init. */ }
|
|
70
70
|
stackPackages = [...new Set([...recorded, ...stackPackages])];
|
|
@@ -78,7 +78,7 @@ export async function listTemplates({ projectRoot = process.cwd(), stackPackages
|
|
|
78
78
|
namespaces.add(source.namespace);
|
|
79
79
|
let catalog = source.catalog;
|
|
80
80
|
if (!catalog) {
|
|
81
|
-
const snapshot = await readGitSnapshot({ repository: source.repository, branch: source.branch || 'main' });
|
|
81
|
+
const snapshot = await readGitSnapshot({ repository: source.repository, branch: source.branch || 'main', scratchRoot });
|
|
82
82
|
const file = snapshot.files.find(({ path: name }) => name === 'genesis.templates.json');
|
|
83
83
|
if (!file) throw templateCatalogError(`${source.namespace} does not contain genesis.templates.json.`);
|
|
84
84
|
try { catalog = JSON.parse(file.contents.toString('utf8')); }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm,
|
|
2
|
-
import os from 'node:os';
|
|
1
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { parseBlueprintSource } from './blueprint.js';
|
|
5
4
|
import { buildProjectIndex } from './code-index.js';
|
|
@@ -79,22 +78,23 @@ async function assertSeedable(root, stackPackages) {
|
|
|
79
78
|
/** Apply one complete source tree without changing Git history or running application commands. */
|
|
80
79
|
export async function applyTemplate({ projectRoot, templateId, stackPackages = [], templateSources = [] } = {}) {
|
|
81
80
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
81
|
+
// The lock directory also holds the scratch repositories, so a host that
|
|
82
|
+
// confines Git to the project accepts them and they never outlive the lock.
|
|
82
83
|
const lockPath = path.resolve(root, await runGitText(root, ['rev-parse', '--git-path', 'genesis-template.lock']));
|
|
83
84
|
try { await mkdir(lockPath); }
|
|
84
85
|
catch (error) {
|
|
85
86
|
if (error.code === 'EEXIST') throw new GenesisError('TEMPLATE_PROJECT_BUSY', 'Another template operation holds this project lock.');
|
|
86
87
|
throw error;
|
|
87
88
|
}
|
|
88
|
-
let stagingRoot;
|
|
89
89
|
try {
|
|
90
90
|
await assertSeedable(root, stackPackages);
|
|
91
|
-
const { templates } = await listTemplates({ projectRoot: root, stackPackages, templateSources });
|
|
91
|
+
const { templates } = await listTemplates({ projectRoot: root, stackPackages, templateSources, scratchRoot: lockPath });
|
|
92
92
|
const template = resolveTemplate(templates, String(templateId || ''));
|
|
93
93
|
const preserved = new Map();
|
|
94
94
|
// A seedable project's .gitignore can only be the one init wrote for the Cities.
|
|
95
95
|
for (const name of [...GENESIS_BOOTSTRAP_PATHS, '.gitignore']) preserved.set(name, await existingFile(root, name));
|
|
96
|
-
const snapshot = await readGitSnapshot(template);
|
|
97
|
-
stagingRoot = await mkdtemp(path.join(
|
|
96
|
+
const snapshot = await readGitSnapshot({ ...template, scratchRoot: lockPath });
|
|
97
|
+
const stagingRoot = await mkdtemp(path.join(lockPath, 'genesis-template-project-'));
|
|
98
98
|
await runGit(stagingRoot, ['init', '--quiet']);
|
|
99
99
|
for (const file of snapshot.files) {
|
|
100
100
|
if (file.path.split('/').includes('node_modules') || file.path.split('/').some((part) => part === '.env' || part.startsWith('.env.') && part !== '.env.example')) {
|
|
@@ -168,7 +168,6 @@ export async function applyTemplate({ projectRoot, templateId, stackPackages = [
|
|
|
168
168
|
guidance: 'The starting application is ready for its declared workspace setup. No application commands or Git commits were run.',
|
|
169
169
|
};
|
|
170
170
|
} finally {
|
|
171
|
-
|
|
172
|
-
await rmdir(lockPath);
|
|
171
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
173
172
|
}
|
|
174
173
|
}
|
|
@@ -17,13 +17,16 @@ function validateRepository(repository) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Fetch a branch once and read ordinary blobs; never execute or check out remote hooks/filters.
|
|
22
|
+
* The scratch repository is created below scratchRoot (the system temporary directory by default).
|
|
23
|
+
*/
|
|
24
|
+
export async function readGitSnapshot({ repository, branch, scratchRoot = os.tmpdir() }) {
|
|
22
25
|
validateRepository(repository);
|
|
23
26
|
if (typeof branch !== 'string' || !branch || branch.startsWith('-')) {
|
|
24
27
|
throw new GenesisError('TEMPLATE_SOURCE_INVALID', 'A template branch is required.');
|
|
25
28
|
}
|
|
26
|
-
const temporaryRoot = await mkdtemp(path.join(
|
|
29
|
+
const temporaryRoot = await mkdtemp(path.join(scratchRoot, 'genesis-template-source-'));
|
|
27
30
|
try {
|
|
28
31
|
await runGit(temporaryRoot, ['init', '--bare', '--quiet']);
|
|
29
32
|
await runGit(temporaryRoot, ['check-ref-format', `refs/heads/${branch}`]);
|