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. Run `genesis --help` for the
268
- compact CLI synopsis.
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. Genesis rejects arbitrary links,
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. If a process is forcibly killed, inspect any partial work and the
98
- Git-local `genesis-template.lock` before removing the abandoned lock and retrying;
99
- Genesis never treats partial application code as an empty destination.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.10.5",
3
+ "version": "1.10.6",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with project agent guidance.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.10.5",
3
+ "version": "1.10.6",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
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
- for (const field of ['stdout', 'stderr']) {
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 line(process.stderr, `${diagnostic.code}: ${diagnostic.message}`);
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, rmdir, symlink, writeFile } from 'node:fs/promises';
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(os.tmpdir(), 'genesis-template-project-'));
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
- if (stagingRoot) await rm(stagingRoot, { recursive: true, force: true });
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
- /** Fetch a branch once and read ordinary blobs; never execute or check out remote hooks/filters. */
21
- export async function readGitSnapshot({ repository, branch }) {
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(os.tmpdir(), 'genesis-template-source-'));
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}`]);