create-agentic-monorepo 0.1.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/dist/main.mjs ADDED
@@ -0,0 +1,885 @@
1
+ #!/usr/bin/env node
2
+ import { a as toOverrides, d as validateConfig, l as presets, o as CreatorError, r as parseCliArgs, s as catalog, t as hasSelection, u as resolveOptions } from "./args-D-klo3P0.mjs";
3
+ import { lstat, mkdir, open, readFile, readdir, realpath, rmdir, unlink } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ import { parse, stringify } from "yaml";
7
+ import spawn from "cross-spawn";
8
+ import pc from "picocolors";
9
+ //#region ../../../libs/platform/node/scaffolding/dist/index.mjs
10
+ /** Read and strictly validate a creator configuration file. */
11
+ async function readConfig(filePath) {
12
+ const absolutePath = resolve(process.cwd(), filePath);
13
+ let source;
14
+ try {
15
+ source = await readFile(absolutePath, "utf8");
16
+ } catch (cause) {
17
+ throw new CreatorError("INVALID_CONFIG", `Unable to read configuration file ${absolutePath}`, { cause });
18
+ }
19
+ let value;
20
+ try {
21
+ value = JSON.parse(source);
22
+ } catch (cause) {
23
+ throw new CreatorError("INVALID_CONFIG", `Configuration file ${absolutePath} is not valid JSON`, { cause });
24
+ }
25
+ try {
26
+ return validateConfig(value);
27
+ } catch (cause) {
28
+ if (cause instanceof CreatorError) throw cause;
29
+ throw new CreatorError("INVALID_CONFIG", `Configuration file ${absolutePath} is invalid`, { cause });
30
+ }
31
+ }
32
+ function conflict(message, cause) {
33
+ return new CreatorError("DESTINATION_CONFLICT", message, cause === void 0 ? void 0 : { cause });
34
+ }
35
+ /** Resolve a destination through its existing parent and enforce an empty target. */
36
+ async function inspectDestination(directory) {
37
+ const requested = resolve(process.cwd(), directory);
38
+ let current;
39
+ try {
40
+ current = await lstat(requested);
41
+ } catch (cause) {
42
+ if (cause.code !== "ENOENT") throw conflict(`Unable to inspect destination ${requested}`, cause);
43
+ const parent = dirname(requested);
44
+ let canonicalParent;
45
+ try {
46
+ canonicalParent = await realpath(parent);
47
+ } catch (parentCause) {
48
+ throw conflict(`Destination parent does not exist: ${parent}`, parentCause);
49
+ }
50
+ let parentStat;
51
+ try {
52
+ parentStat = await lstat(canonicalParent);
53
+ } catch (parentCause) {
54
+ throw conflict(`Unable to inspect destination parent ${canonicalParent}`, parentCause);
55
+ }
56
+ if (!parentStat.isDirectory()) throw conflict(`Destination parent is not a directory: ${canonicalParent}`);
57
+ return {
58
+ path: join(canonicalParent, requested.slice(parent.length + 1)),
59
+ existed: false,
60
+ identity: null
61
+ };
62
+ }
63
+ if (current.isSymbolicLink()) throw conflict(`Destination must not be a symbolic link: ${requested}`);
64
+ if (!current.isDirectory()) throw conflict(`Destination is not a directory: ${requested}`);
65
+ let entries;
66
+ try {
67
+ entries = await readdir(requested);
68
+ } catch (cause) {
69
+ throw conflict(`Unable to inspect destination ${requested}`, cause);
70
+ }
71
+ if (entries.length > 0) throw conflict(`Destination is not empty: ${requested}`);
72
+ return {
73
+ path: requested,
74
+ existed: true,
75
+ identity: {
76
+ dev: current.dev,
77
+ ino: current.ino
78
+ }
79
+ };
80
+ }
81
+ /** Canonical JSON used for template digest calculation. */
82
+ function canonicalTemplateValue(snapshot) {
83
+ const files = [...snapshot.files].map((file) => ({
84
+ path: file.path,
85
+ mode: file.mode,
86
+ text: file.text
87
+ })).sort((left, right) => left.path.localeCompare(right.path));
88
+ const variantLockfiles = Object.fromEntries(Object.entries(snapshot.variantLockfiles ?? {}).sort(([left], [right]) => left.localeCompare(right)));
89
+ return JSON.stringify({
90
+ files,
91
+ variantLockfiles
92
+ });
93
+ }
94
+ function calculateTemplateDigest(snapshot) {
95
+ return `sha256:${createHash("sha256").update(canonicalTemplateValue(snapshot), "utf8").digest("hex")}`;
96
+ }
97
+ function validateTemplatePath(value) {
98
+ if (value.length === 0 || value.includes("\\") || value.includes("\0")) return false;
99
+ if (isAbsolute(value) || win32.isAbsolute(value) || win32.parse(value).root.length > 0) return false;
100
+ const normalized = posix.normalize(value);
101
+ return normalized === value && normalized !== "." && !normalized.split("/").includes("..") && !value.startsWith("/");
102
+ }
103
+ function invalid$1(message, cause) {
104
+ return new CreatorError("INVALID_TEMPLATE", message, cause === void 0 ? void 0 : { cause });
105
+ }
106
+ function parseSnapshot(value) {
107
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalid$1("Template asset must contain a JSON object");
108
+ const record = value;
109
+ if (record.formatVersion !== 1) throw invalid$1("Unsupported template format version");
110
+ if (typeof record.creatorVersion !== "string" || record.creatorVersion.length === 0) throw invalid$1("Template creatorVersion must be a non-empty string");
111
+ if (typeof record.digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(record.digest)) throw invalid$1("Template digest must be a sha256 digest");
112
+ if (!Array.isArray(record.files) || record.files.length === 0) throw invalid$1("Template files must be a non-empty array");
113
+ const files = [];
114
+ const paths = /* @__PURE__ */ new Set();
115
+ const caseFoldedPaths = /* @__PURE__ */ new Set();
116
+ for (const item of record.files) {
117
+ if (typeof item !== "object" || item === null || Array.isArray(item)) throw invalid$1("Template file entries must be objects");
118
+ const file = item;
119
+ if (typeof file.path !== "string" || file.mode !== 420 && file.mode !== 493 || typeof file.text !== "string" || !validateTemplatePath(file.path)) throw invalid$1("Template contains an invalid file entry");
120
+ const key = file.path.toLocaleLowerCase("en-US");
121
+ if (paths.has(file.path) || caseFoldedPaths.has(key)) throw invalid$1(`Template contains duplicate file path ${file.path}`);
122
+ paths.add(file.path);
123
+ caseFoldedPaths.add(key);
124
+ files.push({
125
+ path: file.path,
126
+ mode: file.mode,
127
+ text: file.text
128
+ });
129
+ }
130
+ let variantLockfiles;
131
+ if (record.variantLockfiles !== void 0) {
132
+ if (typeof record.variantLockfiles !== "object" || record.variantLockfiles === null || Array.isArray(record.variantLockfiles)) throw invalid$1("Template variantLockfiles must be an object");
133
+ const variants = {};
134
+ for (const [key, text] of Object.entries(record.variantLockfiles)) {
135
+ if (key.length === 0 || typeof text !== "string") throw invalid$1("Template variantLockfiles contains an invalid entry");
136
+ variants[key] = text;
137
+ }
138
+ variantLockfiles = variants;
139
+ }
140
+ const snapshot = {
141
+ formatVersion: 1,
142
+ creatorVersion: record.creatorVersion,
143
+ digest: record.digest,
144
+ files,
145
+ ...variantLockfiles === void 0 ? {} : { variantLockfiles }
146
+ };
147
+ if (calculateTemplateDigest(snapshot) !== snapshot.digest) throw invalid$1("Template digest does not match its contents");
148
+ return snapshot;
149
+ }
150
+ /** Read, validate, and verify an immutable bundled template asset. */
151
+ async function loadTemplate(templateUrl) {
152
+ let text;
153
+ try {
154
+ text = await readFile(templateUrl, "utf8");
155
+ } catch (cause) {
156
+ throw invalid$1(`Unable to read template asset ${templateUrl.href}`, cause);
157
+ }
158
+ let value;
159
+ try {
160
+ value = JSON.parse(text);
161
+ } catch (cause) {
162
+ throw invalid$1("Template asset is not valid JSON", cause);
163
+ }
164
+ return parseSnapshot(value);
165
+ }
166
+ const APP_PATHS = {
167
+ web: "apps/web/example-app",
168
+ cli: "apps/cli/example-app",
169
+ mac: "apps/mac/example-app"
170
+ };
171
+ const IMPORTER_PATHS = {
172
+ web: "apps/web/example-app",
173
+ cli: "apps/cli/example-app"
174
+ };
175
+ function invalid(message, cause) {
176
+ return new CreatorError("INVALID_TEMPLATE", message, cause === void 0 ? void 0 : { cause });
177
+ }
178
+ function descriptors() {
179
+ const source = catalog;
180
+ if (Array.isArray(source)) return source;
181
+ if (typeof source === "object" && source !== null) return Object.values(source);
182
+ throw invalid("Scaffolding catalog is not an object or array");
183
+ }
184
+ function normalizeSource(text) {
185
+ return text.replace(/\r\n?/g, "\n");
186
+ }
187
+ function parseJson(text, path) {
188
+ try {
189
+ const value = JSON.parse(text);
190
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("expected an object");
191
+ return value;
192
+ } catch (cause) {
193
+ throw invalid(`Template member ${path} is not a JSON object`, cause);
194
+ }
195
+ }
196
+ function selectedSourcePaths(options) {
197
+ const selected = new Set(options.components);
198
+ const paths = [];
199
+ for (const descriptor of descriptors()) {
200
+ if (!selected.has(descriptor.id) || descriptor.status !== "available") continue;
201
+ for (const sourcePath of descriptor.sourcePaths) paths.push(sourcePath);
202
+ }
203
+ return paths;
204
+ }
205
+ function pathMatches(path, sourcePath) {
206
+ return path === sourcePath || path.startsWith(`${sourcePath}/`);
207
+ }
208
+ function renderPackage(text, options) {
209
+ const packageJson = parseJson(text, "package.json");
210
+ packageJson.name = options.name;
211
+ packageJson.private = true;
212
+ const roots = options.components.filter((component) => component === "web" || component === "cli" || component === "mac" || component === "search-domain" || component === "mac-filesystem").map((component) => APP_PATHS[component] ?? (component === "search-domain" ? "libs/domains/search" : "libs/platform/mac/filesystem"));
213
+ const lintRoots = roots.length === 0 ? ["tools"] : [...roots, "tools"];
214
+ packageJson.scripts = {
215
+ ...typeof packageJson.scripts === "object" && packageJson.scripts !== null ? packageJson.scripts : {},
216
+ lint: `oxlint ${lintRoots.join(" ")} && node tools/check-boundaries.mjs`
217
+ };
218
+ return `${JSON.stringify(packageJson, null, 2)}\n`;
219
+ }
220
+ function selectedImporters(options) {
221
+ const result = /* @__PURE__ */ new Set(["."]);
222
+ for (const app of options.apps) {
223
+ const importer = IMPORTER_PATHS[app];
224
+ if (importer !== void 0) result.add(importer);
225
+ }
226
+ if (options.apps.some((app) => app === "web" || app === "cli")) result.add("libs/domains/search");
227
+ return result;
228
+ }
229
+ function renderLockfile(text, options, variant) {
230
+ const source = variant ?? text;
231
+ let document;
232
+ try {
233
+ document = parse(source);
234
+ } catch (cause) {
235
+ throw invalid("Template lockfile is not valid YAML", cause);
236
+ }
237
+ if (typeof document !== "object" || document === null || Array.isArray(document)) throw invalid("Template lockfile must be a YAML object");
238
+ const lockfile = document;
239
+ if (lockfile.lockfileVersion !== "9.0" && lockfile.lockfileVersion !== 9) throw invalid("Template lockfile must use lockfileVersion 9.0");
240
+ if (typeof lockfile.importers !== "object" || lockfile.importers === null || Array.isArray(lockfile.importers)) throw invalid("Template lockfile has no importer map");
241
+ const importers = lockfile.importers;
242
+ const keep = selectedImporters(options);
243
+ for (const key of Object.keys(importers)) if (!keep.has(key)) delete importers[key];
244
+ for (const key of keep) if (importers[key] === void 0) throw invalid(`Template lockfile is missing selected importer ${key}`);
245
+ return normalizeSource(stringify(lockfile));
246
+ }
247
+ function renderNx(text) {
248
+ if (typeof parseJson(text, "nx.json").analytics !== "boolean") throw invalid("Template member nx.json[\"analytics\"] must be a boolean");
249
+ const match = text.match(/("analytics"\s*:\s*)(true|false)/);
250
+ if (match === null || match.index === void 0) throw invalid("Template nx.json is missing the \"analytics\" key");
251
+ const rendered = `${text.slice(0, match.index)}${match[1]}false${text.slice(match.index + match[0].length)}`;
252
+ return rendered.endsWith("\n") ? rendered : `${rendered}\n`;
253
+ }
254
+ function renderConfig(options, template) {
255
+ return `{
256
+ "schemaVersion": 1,
257
+ "name": ${JSON.stringify(options.name)},
258
+ "apps": [${options.apps.map((app) => JSON.stringify(app)).join(", ")}],
259
+ "rust": ${JSON.stringify(options.rust)},
260
+ "templateVersion": ${JSON.stringify(template.creatorVersion)},
261
+ "templateDigest": ${JSON.stringify(template.digest)}
262
+ }
263
+ `;
264
+ }
265
+ function renderReadme(options, template) {
266
+ const selected = options.apps.length === 0 ? "an empty workspace" : options.apps.map((app) => `${app} starter`).join(", ");
267
+ const appLines = options.apps.length === 0 ? "- No application is included yet. Add a domain and app following docs/architecture.md." : options.apps.map((app) => `- ${APP_PATHS[app]} (${app})`).join("\n");
268
+ const buildLines = options.apps.length === 0 ? "pnpm nx run-many -t typecheck build test lint" : options.apps.map((app) => `pnpm nx run ${app}-example-app:build`).join("\n");
269
+ const developCommands = {
270
+ web: "pnpm nx run web-example-app:dev",
271
+ cli: "pnpm nx run cli-example-app:start -- demo",
272
+ mac: "pnpm nx run mac-example-app:run"
273
+ };
274
+ const developSection = options.apps.length === 0 ? "" : `## Develop\n\n\`\`\`sh\n${options.apps.map((app) => developCommands[app]).join("\n")}\n\`\`\`\n\nIf you change a library, rebuild it before the running development server picks it up.\n\n`;
275
+ return `# ${options.name}\n\nThis repository contains ${selected}, generated from Agentic Monorepo (template ${template.creatorVersion}). Examples are intentionally small and should be replaced with your product's real domains.\n\n## Included projects\n\n${appLines}\n${options.rust ? "- libs/rust/search-index (independent Rust library)\n" : ""}\n${developSection}## Checks\n\n\`\`\`sh\n${buildLines}\npnpm nx run-many -t typecheck build test lint\npnpm format:check\n\`\`\`\n\nReplay this selection with npx create-agentic-monorepo ${options.name} --config ./agentic.config.json. The generated examples use the shared search domain where applicable; Rust is independent and is not wired into an application.\n`;
276
+ }
277
+ function renderWorkflow(options) {
278
+ const steps = [
279
+ {
280
+ id: "checkout",
281
+ uses: "actions/checkout@v4"
282
+ },
283
+ {
284
+ id: "setup-pnpm",
285
+ uses: "pnpm/action-setup@v4"
286
+ },
287
+ {
288
+ id: "setup-node",
289
+ uses: "actions/setup-node@v4",
290
+ with: {
291
+ "node-version": 22,
292
+ cache: "pnpm"
293
+ }
294
+ }
295
+ ];
296
+ if (options.apps.includes("mac")) steps.push({
297
+ id: "setup-swiftlint",
298
+ run: "brew install swiftlint"
299
+ });
300
+ if (options.rust) steps.push({
301
+ id: "setup-rust",
302
+ run: "rustup toolchain install 1.97.1 --profile minimal --component clippy --component rustfmt"
303
+ });
304
+ steps.push({
305
+ id: "install-dependencies",
306
+ run: "pnpm install --frozen-lockfile"
307
+ }, {
308
+ id: "verify-workspace",
309
+ run: "pnpm nx run-many -t typecheck build test lint"
310
+ }, {
311
+ id: "check-format",
312
+ run: "pnpm format:check"
313
+ });
314
+ return `${stringify({
315
+ name: "ci",
316
+ on: {
317
+ push: { branches: ["main"] },
318
+ pull_request: {}
319
+ },
320
+ jobs: { verify: {
321
+ "runs-on": options.apps.includes("mac") ? "macos-latest" : "ubuntu-latest",
322
+ steps
323
+ } }
324
+ })}`;
325
+ }
326
+ /** Render a complete selected repository before any destination mutation. */
327
+ function renderRepository(template, options) {
328
+ const sourcePaths = selectedSourcePaths(options);
329
+ const selectedFiles = template.files.filter((file) => sourcePaths.some((sourcePath) => pathMatches(file.path, sourcePath)));
330
+ if (!selectedFiles.some((file) => file.path === "package.json")) throw invalid("Selected template does not include package.json");
331
+ const files = /* @__PURE__ */ new Map();
332
+ for (const file of selectedFiles) {
333
+ if (!validateTemplatePath(file.path)) throw invalid(`Invalid selected template path ${file.path}`);
334
+ files.set(file.path, {
335
+ path: file.path,
336
+ mode: file.mode,
337
+ text: normalizeSource(file.text)
338
+ });
339
+ }
340
+ const lockKey = `${options.apps.join("+") || "none"};rust=${options.rust ? "1" : "0"}`;
341
+ const variant = template.variantLockfiles?.[lockKey];
342
+ for (const file of files.values()) if (file.path === "package.json") files.set(file.path, {
343
+ ...file,
344
+ text: renderPackage(file.text, options)
345
+ });
346
+ else if (file.path === "pnpm-lock.yaml") files.set(file.path, {
347
+ ...file,
348
+ text: renderLockfile(file.text, options, variant)
349
+ });
350
+ else if (file.path === "nx.json") files.set(file.path, {
351
+ ...file,
352
+ text: renderNx(file.text)
353
+ });
354
+ else if (file.path === ".github/workflows/ci.yml") files.set(file.path, {
355
+ ...file,
356
+ text: renderWorkflow(options)
357
+ });
358
+ files.set("agentic.config.json", {
359
+ path: "agentic.config.json",
360
+ mode: 420,
361
+ text: renderConfig(options, template)
362
+ });
363
+ files.set("README.md", {
364
+ path: "README.md",
365
+ mode: 420,
366
+ text: renderReadme(options, template)
367
+ });
368
+ const rendered = [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
369
+ const seen = /* @__PURE__ */ new Set();
370
+ for (const file of rendered) {
371
+ if (seen.has(file.path)) throw invalid(`Duplicate rendered path ${file.path}`);
372
+ seen.add(file.path);
373
+ }
374
+ return rendered;
375
+ }
376
+ function error(code, message, cause) {
377
+ return new CreatorError(code, message, cause === void 0 ? void 0 : { cause });
378
+ }
379
+ function digest(text) {
380
+ return createHash("sha256").update(text, "utf8").digest("hex");
381
+ }
382
+ function checkAborted(signal) {
383
+ if (signal.aborted) throw error("CANCELLED", "Repository creation was cancelled");
384
+ }
385
+ function contained(destination, candidate) {
386
+ const root = resolve(destination);
387
+ const target = resolve(candidate);
388
+ return target !== root && target.startsWith(`${root}${sep}`);
389
+ }
390
+ async function rollback(files, directories, destination, destinationOwned) {
391
+ for (const file of [...files].reverse()) try {
392
+ const current = await lstat(file.path);
393
+ if (current.isSymbolicLink() || current.dev !== file.dev || current.ino !== file.ino) continue;
394
+ const text = await (await open(file.path, "r")).readFile({ encoding: "utf8" });
395
+ if (file.digest.length === 0 || digest(text) === file.digest) await unlink(file.path);
396
+ } catch {}
397
+ for (const directory of [...directories].reverse()) try {
398
+ const current = await lstat(directory.path);
399
+ if (current.isDirectory() && current.dev === directory.dev && current.ino === directory.ino) await rmdir(directory.path);
400
+ } catch {}
401
+ if (destinationOwned) try {
402
+ if ((await lstat(destination.path)).isDirectory() && (await readdir(destination.path)).length === 0) await rmdir(destination.path);
403
+ } catch {}
404
+ }
405
+ async function reserveDestination(destination) {
406
+ if (!destination.existed) try {
407
+ await mkdir(destination.path, {
408
+ mode: 493,
409
+ recursive: false
410
+ });
411
+ return true;
412
+ } catch (cause) {
413
+ if (cause.code === "EEXIST") throw error("DESTINATION_CONFLICT", `Destination appeared while creating it: ${destination.path}`, cause);
414
+ throw error("WRITE_FAILED", `Unable to reserve destination ${destination.path}`, cause);
415
+ }
416
+ let current;
417
+ try {
418
+ current = await lstat(destination.path);
419
+ } catch (cause) {
420
+ throw error("DESTINATION_CONFLICT", `Destination changed before creation: ${destination.path}`, cause);
421
+ }
422
+ if (!current.isDirectory() || current.isSymbolicLink()) throw error("DESTINATION_CONFLICT", `Destination changed before creation: ${destination.path}`);
423
+ if (destination.identity === null || current.dev !== destination.identity.dev || current.ino !== destination.identity.ino) throw error("DESTINATION_CONFLICT", `Destination changed before creation: ${destination.path}`);
424
+ try {
425
+ if ((await readdir(destination.path)).length !== 0) throw error("DESTINATION_CONFLICT", `Destination is no longer empty: ${destination.path}`);
426
+ } catch (cause) {
427
+ if (cause instanceof CreatorError) throw cause;
428
+ throw error("DESTINATION_CONFLICT", `Unable to recheck destination ${destination.path}`, cause);
429
+ }
430
+ return false;
431
+ }
432
+ async function ensureParentDirectories(destination, filePath, owned) {
433
+ const parent = dirname(filePath);
434
+ if (parent === destination) return;
435
+ const relativeParent = relative(destination, parent);
436
+ if (!relativeParent || relativeParent.startsWith("..") || relativeParent.includes(`..${sep}`)) throw error("INVALID_TEMPLATE", `Template file escapes destination: ${filePath}`);
437
+ let current = destination;
438
+ for (const segment of relativeParent.split(sep)) {
439
+ current = resolve(current, segment);
440
+ let stat;
441
+ try {
442
+ stat = await lstat(current);
443
+ } catch (cause) {
444
+ if (cause.code !== "ENOENT") throw error("WRITE_FAILED", `Unable to inspect directory ${current}`, cause);
445
+ try {
446
+ await mkdir(current, {
447
+ mode: 493,
448
+ recursive: false
449
+ });
450
+ } catch (mkdirCause) {
451
+ throw error("WRITE_FAILED", `Unable to create directory ${current}`, mkdirCause);
452
+ }
453
+ stat = await lstat(current);
454
+ owned.push({
455
+ path: current,
456
+ dev: stat.dev,
457
+ ino: stat.ino
458
+ });
459
+ continue;
460
+ }
461
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw error("DESTINATION_CONFLICT", `Template parent is not a safe directory: ${current}`);
462
+ if (!owned.some((entry) => entry.path === current)) throw error("DESTINATION_CONFLICT", `Unexpected existing template directory: ${current}`);
463
+ }
464
+ }
465
+ async function writeFileExclusive(file, destination, created, signal) {
466
+ checkAborted(signal);
467
+ const filePath = resolve(destination, file.path);
468
+ if (!contained(destination, filePath)) throw error("INVALID_TEMPLATE", `Template file escapes destination: ${file.path}`);
469
+ let handle;
470
+ try {
471
+ handle = await open(filePath, "wx", file.mode);
472
+ const opened = await handle.stat();
473
+ created.push({
474
+ path: filePath,
475
+ dev: opened.dev,
476
+ ino: opened.ino,
477
+ digest: ""
478
+ });
479
+ await handle.writeFile(file.text, "utf8");
480
+ await handle.chmod(file.mode);
481
+ await handle.close();
482
+ const index = created.length - 1;
483
+ const createdFile = created[index];
484
+ if (createdFile !== void 0) created[index] = {
485
+ ...createdFile,
486
+ digest: digest(file.text)
487
+ };
488
+ } catch (cause) {
489
+ try {
490
+ await handle?.close();
491
+ } catch {}
492
+ if (cause.code === "EEXIST") throw error("DESTINATION_CONFLICT", `Template file already exists: ${file.path}`, cause);
493
+ throw error("WRITE_FAILED", `Unable to create ${file.path}`, cause);
494
+ }
495
+ const stat = await lstat(filePath);
496
+ if (!stat.isFile() || stat.isSymbolicLink()) throw error("WRITE_FAILED", `Created path is not a regular file: ${file.path}`);
497
+ checkAborted(signal);
498
+ }
499
+ /** Render and write a repository using exclusive files and ownership-aware rollback. */
500
+ async function createRepository(template, options, destination, signal) {
501
+ checkAborted(signal);
502
+ const rendered = renderRepository(template, options);
503
+ const destinationOwned = await reserveDestination(destination);
504
+ const files = [];
505
+ const directories = [];
506
+ try {
507
+ for (const file of rendered) {
508
+ checkAborted(signal);
509
+ const filePath = resolve(destination.path, file.path);
510
+ if (!contained(destination.path, filePath)) throw error("INVALID_TEMPLATE", `Template file escapes destination: ${file.path}`);
511
+ await ensureParentDirectories(destination.path, filePath, directories);
512
+ await writeFileExclusive(file, destination.path, files, signal);
513
+ }
514
+ checkAborted(signal);
515
+ } catch (cause) {
516
+ await rollback(files, directories, destination, destinationOwned);
517
+ if (cause instanceof CreatorError) throw cause;
518
+ throw error("WRITE_FAILED", `Unable to create repository in ${destination.path}`, cause);
519
+ }
520
+ }
521
+ function setupError(message, cause) {
522
+ return new CreatorError("SETUP_FAILED", message, cause === void 0 ? void 0 : { cause });
523
+ }
524
+ function cancellation() {
525
+ return new CreatorError("CANCELLED", "Setup was cancelled");
526
+ }
527
+ function terminate(child) {
528
+ const pid = child.pid;
529
+ if (pid === void 0 || pid === null) return;
530
+ if (process.platform === "win32") {
531
+ spawn("taskkill.exe", [
532
+ "/PID",
533
+ String(pid),
534
+ "/T",
535
+ "/F"
536
+ ], {
537
+ stdio: "ignore",
538
+ shell: false
539
+ }).unref();
540
+ return;
541
+ }
542
+ try {
543
+ process.kill(-pid, "SIGTERM");
544
+ } catch {
545
+ try {
546
+ child.kill("SIGTERM");
547
+ } catch {}
548
+ }
549
+ setTimeout(() => {
550
+ try {
551
+ process.kill(-pid, "SIGKILL");
552
+ } catch {}
553
+ }, 5e3).unref();
554
+ }
555
+ function runProcess(executable, args, directory, signal, onOutput) {
556
+ return new Promise((resolve, reject) => {
557
+ let child;
558
+ try {
559
+ child = spawn(executable, [...args], {
560
+ cwd: directory,
561
+ shell: false,
562
+ detached: process.platform !== "win32",
563
+ stdio: [
564
+ "ignore",
565
+ "pipe",
566
+ "pipe"
567
+ ]
568
+ });
569
+ } catch (cause) {
570
+ reject(setupError(`Unable to start ${executable}`, cause));
571
+ return;
572
+ }
573
+ let stdout = "";
574
+ let aborted = signal.aborted;
575
+ const onAbort = () => {
576
+ aborted = true;
577
+ terminate(child);
578
+ };
579
+ signal.addEventListener("abort", onAbort, { once: true });
580
+ child.stdout?.on("data", (chunk) => {
581
+ const text = String(chunk);
582
+ stdout += text;
583
+ onOutput(text);
584
+ });
585
+ child.stderr?.on("data", (chunk) => onOutput(String(chunk)));
586
+ child.once("error", (cause) => {
587
+ signal.removeEventListener("abort", onAbort);
588
+ reject(aborted ? cancellation() : setupError(`Unable to run ${executable}`, cause));
589
+ });
590
+ child.once("close", (code) => {
591
+ signal.removeEventListener("abort", onAbort);
592
+ if (aborted) reject(cancellation());
593
+ else resolve({
594
+ code,
595
+ stdout
596
+ });
597
+ });
598
+ if (aborted) onAbort();
599
+ });
600
+ }
601
+ function packageManagerVersion(value) {
602
+ if (typeof value !== "string" || !value.startsWith("pnpm@") || value.slice(5).length === 0) throw setupError("Generated package.json must pin packageManager to pnpm@<version>");
603
+ return value.slice(5);
604
+ }
605
+ /** Run only explicitly requested setup operations, sequentially and without a shell. */
606
+ async function runSetup(directory, options, signal, onOutput) {
607
+ const result = {
608
+ install: "skipped",
609
+ git: "skipped"
610
+ };
611
+ if (options.install) {
612
+ let packageJson;
613
+ try {
614
+ packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
615
+ } catch (cause) {
616
+ throw setupError("Unable to read generated package.json for setup", cause);
617
+ }
618
+ const expected = packageManagerVersion(packageJson.packageManager);
619
+ const version = await runProcess("pnpm", ["--version"], directory, signal, onOutput);
620
+ if (version.code !== 0 || version.stdout.trim() !== expected) throw setupError(`Expected pnpm ${expected}, but found ${version.stdout.trim() || "an unavailable version"}`);
621
+ const install = await runProcess("pnpm", ["install", "--frozen-lockfile"], directory, signal, onOutput);
622
+ if (install.code !== 0) throw setupError(`pnpm install failed with exit code ${install.code ?? 1}`);
623
+ result.install = "completed";
624
+ }
625
+ if (options.git) {
626
+ const git = await runProcess("git", ["init", "--initial-branch=main"], directory, signal, onOutput);
627
+ if (git.code !== 0) throw setupError(`git init failed with exit code ${git.code ?? 1}`);
628
+ result.git = "completed";
629
+ }
630
+ return result;
631
+ }
632
+ //#endregion
633
+ //#region src/output.ts
634
+ function writeJson(value) {
635
+ process.stdout.write(`${JSON.stringify(value)}\n`);
636
+ }
637
+ function stripAnsi(value) {
638
+ return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, "");
639
+ }
640
+ function cleanDiagnostic(value) {
641
+ return stripAnsi(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
642
+ }
643
+ function printHelp(stream = process.stdout) {
644
+ stream.write(`create-agentic-monorepo [destination]\n\nCreate a repository from the Agentic Monorepo starter.\n\nOptions:\n --name <name> Root package/repository name\n --preset <id> web, cli, mac, web-cli, full, or workspace\n --apps <csv> web, cli, mac, or none\n --rust / --no-rust Include the independent Rust library\n --install / --no-install Install dependencies with pinned pnpm\n --git / --no-git Initialize local Git\n --config <path> Read a strict JSON configuration\n --yes, -y Do not prompt (defaults to web)\n --dry-run Print the plan without writing or running commands\n --json Emit one machine-readable result on stdout\n --plain Use accessible line-oriented prompts\n --list List supported and Coming soon capabilities\n --help, -h Show this help\n --version, -v Show the creator version\n`);
645
+ }
646
+ function printList(presets, capabilities, json) {
647
+ if (json) {
648
+ writeJson({
649
+ schemaVersion: 1,
650
+ status: "catalog",
651
+ presets,
652
+ capabilities
653
+ });
654
+ return;
655
+ }
656
+ const lines = ["Supported starters", ""];
657
+ for (const preset of presets) lines.push(` ${preset.label} — ${preset.hint}`);
658
+ lines.push("", "Capabilities");
659
+ for (const capability of capabilities) {
660
+ const badge = capability.status === "available" ? "Available" : "Coming soon";
661
+ const requirements = capability.requirements.length > 0 ? ` (${capability.requirements.join(", ")})` : "";
662
+ lines.push(` ${badge}: ${capability.label} — ${capability.hint}${requirements}`);
663
+ }
664
+ lines.push("", "Host prerequisites: creator, web, and CLI output run on macOS, Linux, and Windows; Mac code needs macOS 14+ and Swift 6; Rust needs Rust 1.97.1.");
665
+ process.stdout.write(`${lines.join("\n")}\n`);
666
+ }
667
+ function printPlan(directory, name, apps, rust, template, install, git) {
668
+ const actions = ["create files"];
669
+ if (install) actions.push("install dependencies");
670
+ if (git) actions.push("initialize local Git");
671
+ const lines = [
672
+ `Plan for ${name}`,
673
+ ` destination: ${directory}`,
674
+ ` apps: ${apps.length === 0 ? "none" : apps.join(", ")}`,
675
+ ` libraries: ${rust ? "Rust search-index" : "none"}`,
676
+ ` files: ${template.files.length}`,
677
+ ` actions: ${actions.join(", ")}`
678
+ ];
679
+ process.stdout.write(`${lines.join("\n")}\n`);
680
+ return actions;
681
+ }
682
+ function shellQuote(value) {
683
+ if (process.platform === "win32") return `Set-Location -LiteralPath '${value.replaceAll("'", "''")}'`;
684
+ return `cd -- '${value.replaceAll("'", "'\\''")}'`;
685
+ }
686
+ function nextSteps(directory, apps, rust, setup) {
687
+ const result = [shellQuote(directory)];
688
+ if (setup.install !== "completed") result.push("pnpm install --frozen-lockfile");
689
+ const first = apps[0];
690
+ if (first === "web") result.push("pnpm nx run web-example-app:build", "pnpm nx run web-example-app:dev");
691
+ else if (first === "cli") result.push("pnpm nx run cli-example-app:build", "pnpm nx run cli-example-app:start -- demo");
692
+ else if (first === "mac") result.push("pnpm nx run mac-example-app:build", "pnpm nx run mac-example-app:run");
693
+ else if (rust) result.push("pnpm nx run search-index:build", "pnpm nx run search-index:test");
694
+ else result.push("pnpm nx run-many -t typecheck build test lint");
695
+ return result;
696
+ }
697
+ function printSuccess(directory, name, apps, rust, template, setup, json, planned = false) {
698
+ const steps = nextSteps(directory, apps, rust, setup);
699
+ if (json) {
700
+ writeJson({
701
+ schemaVersion: 1,
702
+ status: planned ? "planned" : "created",
703
+ directory,
704
+ name,
705
+ apps,
706
+ rust,
707
+ templateVersion: template.creatorVersion,
708
+ templateDigest: template.digest,
709
+ setup: planned ? {
710
+ install: "skipped",
711
+ git: "skipped"
712
+ } : setup,
713
+ nextSteps: steps
714
+ });
715
+ return;
716
+ }
717
+ const heading = planned ? "Planned" : "Created";
718
+ process.stderr.write(`${pc.cyan(pc.bold(heading))} ${name}\n\n`);
719
+ process.stderr.write(` ${directory}\n`);
720
+ process.stderr.write(` apps: ${apps.length === 0 ? "none" : apps.join(", ")}${rust ? "; Rust search-index" : ""}\n`);
721
+ process.stderr.write(` install: ${setup.install}; git: ${setup.git}\n\n`);
722
+ process.stderr.write(`${pc.bold("Next")}\n${steps.map((step) => ` ${step}`).join("\n")}\n`);
723
+ if (apps.includes("mac") && process.platform !== "darwin") process.stderr.write(`${pc.yellow("Warning")}: Mac commands require macOS 14+ and Swift 6.\n`);
724
+ }
725
+ function printError(result, json) {
726
+ if (json) {
727
+ writeJson(result);
728
+ return;
729
+ }
730
+ process.stderr.write(`${pc.red(pc.bold(result.status === "cancelled" ? "Cancelled" : "Error"))}: ${cleanDiagnostic(result.message)}\n`);
731
+ for (const step of result.nextSteps) process.stderr.write(` Next: ${cleanDiagnostic(step)}\n`);
732
+ }
733
+ //#endregion
734
+ //#region src/main.ts
735
+ const packageMetadataUrl = new URL("../package.json", import.meta.url);
736
+ const templateAssetUrl = new URL("../template.json", import.meta.url);
737
+ function isTruthyCi(value) {
738
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false";
739
+ }
740
+ function defaultSetup() {
741
+ return {
742
+ install: "skipped",
743
+ git: "skipped"
744
+ };
745
+ }
746
+ function errorCode(error) {
747
+ return error instanceof CreatorError ? error.code : "WRITE_FAILED";
748
+ }
749
+ function exitCode(error, signalCode) {
750
+ if (signalCode !== 0) return signalCode;
751
+ if (error instanceof CreatorError) {
752
+ if (error.code === "CANCELLED") return 130;
753
+ if (error.code === "INVALID_ARGUMENT" || error.code === "INVALID_CONFIG" || error.code === "UNSUPPORTED_OPTION" || error.code === "UNSUPPORTED_RUNTIME" || error.code === "TEMPLATE_MISMATCH" || error.code === "DESTINATION_CONFLICT") return 2;
754
+ }
755
+ return 1;
756
+ }
757
+ async function readPackageMetadata() {
758
+ const value = JSON.parse(await readFile(packageMetadataUrl, "utf8"));
759
+ if (typeof value !== "object" || value === null || typeof value.name !== "string" || typeof value.version !== "string") throw new CreatorError("INVALID_TEMPLATE", "The staged package manifest is invalid");
760
+ return value;
761
+ }
762
+ function selectionInConfig(config) {
763
+ return config.preset !== void 0 || config.apps !== void 0;
764
+ }
765
+ function checkTemplatePins(config, template) {
766
+ if (config.templateVersion !== void 0 && config.templateVersion !== template.creatorVersion) throw new CreatorError("TEMPLATE_MISMATCH", `Configuration requires creator version ${config.templateVersion}; use create-agentic-monorepo@${config.templateVersion} or remove the templateVersion pin`);
767
+ if (config.templateDigest !== void 0 && config.templateDigest !== template.digest) throw new CreatorError("TEMPLATE_MISMATCH", `Configuration requires template digest ${config.templateDigest}; use the pinned creator or remove the templateDigest pin`);
768
+ }
769
+ function noninteractive(args) {
770
+ return args.yes || args.json || isTruthyCi(process.env["CI"]) || !process.stdin.isTTY || !process.stdout.isTTY || !process.stderr.isTTY;
771
+ }
772
+ function makeErrorResult(error, directory, repositoryCreated, signalCode) {
773
+ const cancelled = signalCode !== 0 || error instanceof CreatorError && error.code === "CANCELLED";
774
+ const message = error instanceof Error ? error.message : "Creator failed";
775
+ const nextSteps = repositoryCreated ? ["cd to the generated directory", "pnpm install --frozen-lockfile"] : [];
776
+ return {
777
+ schemaVersion: 1,
778
+ status: cancelled ? "cancelled" : "error",
779
+ code: cancelled ? "CANCELLED" : errorCode(error),
780
+ message: cleanDiagnostic(message),
781
+ directory,
782
+ repositoryCreated,
783
+ nextSteps
784
+ };
785
+ }
786
+ async function run(argv) {
787
+ let args;
788
+ try {
789
+ args = parseCliArgs(argv);
790
+ } catch (error) {
791
+ const json = argv.includes("--json");
792
+ printError(makeErrorResult(error, null, false, 0), json);
793
+ return exitCode(error, 0);
794
+ }
795
+ if (args.help) {
796
+ printHelp();
797
+ return 0;
798
+ }
799
+ if (args.version) {
800
+ const metadata = await readPackageMetadata();
801
+ process.stdout.write(`${metadata.version}\n`);
802
+ return 0;
803
+ }
804
+ if (args.list) {
805
+ printList(presets, catalog, args.json);
806
+ return 0;
807
+ }
808
+ let destination = args.destination;
809
+ let repositoryCreated = false;
810
+ let signalCode = 0;
811
+ const controller = new AbortController();
812
+ const onInterrupt = () => {
813
+ signalCode = 130;
814
+ controller.abort();
815
+ };
816
+ const onTerminate = () => {
817
+ signalCode = 143;
818
+ controller.abort();
819
+ };
820
+ process.once("SIGINT", onInterrupt);
821
+ process.once("SIGTERM", onTerminate);
822
+ try {
823
+ const config = args.config === void 0 ? { schemaVersion: 1 } : await readConfig(args.config);
824
+ let overrides = toOverrides(args);
825
+ if (!noninteractive(args)) {
826
+ const { runWizard } = await import("./wizard-BFRKuEqB.mjs");
827
+ const wizardResult = await runWizard({
828
+ args,
829
+ config,
830
+ signal: controller.signal,
831
+ plain: args.plain || process.env["TERM"] === "dumb"
832
+ });
833
+ destination = wizardResult.destination;
834
+ overrides = wizardResult.overrides;
835
+ } else {
836
+ if (destination === void 0) throw new CreatorError("INVALID_ARGUMENT", "A destination is required in noninteractive mode; for example: create-agentic-monorepo my-product --preset web --yes");
837
+ if (!hasSelection(args) && !selectionInConfig(config)) {
838
+ if (args.yes) overrides = {
839
+ ...overrides,
840
+ preset: "web"
841
+ };
842
+ else throw new CreatorError("INVALID_ARGUMENT", "A starter selection is required in noninteractive mode; use --preset web, --apps web, or --yes");
843
+ }
844
+ }
845
+ if (destination === void 0) throw new CreatorError("INVALID_ARGUMENT", "A destination is required");
846
+ const fallbackName = basename(resolve(destination));
847
+ const options = resolveOptions(config, overrides, fallbackName);
848
+ const template = await loadTemplate(templateAssetUrl);
849
+ const packageMetadata = await readPackageMetadata();
850
+ if (template.creatorVersion !== packageMetadata.version) throw new CreatorError("TEMPLATE_MISMATCH", `Bundled template ${template.creatorVersion} does not match creator ${packageMetadata.version}`);
851
+ checkTemplatePins(config, template);
852
+ const inspected = await inspectDestination(destination);
853
+ if (args.dryRun) {
854
+ if (args.json) printSuccess(inspected.path, options.name, options.apps, options.rust, template, defaultSetup(), true, true);
855
+ else printPlan(inspected.path, options.name, options.apps, options.rust, template, options.install, options.git);
856
+ return 0;
857
+ }
858
+ process.stderr.write("Validating destination and template...\n");
859
+ process.stderr.write("Creating files...\n");
860
+ await createRepository(template, options, inspected, controller.signal);
861
+ repositoryCreated = true;
862
+ let setup = defaultSetup();
863
+ if (options.install || options.git) {
864
+ process.stderr.write("Running requested setup...\n");
865
+ setup = await runSetup(destination, {
866
+ install: options.install,
867
+ git: options.git
868
+ }, controller.signal, (text) => {
869
+ process.stderr.write(`${cleanDiagnostic(text)}\n`);
870
+ });
871
+ }
872
+ printSuccess(inspected.path, options.name, options.apps, options.rust, template, setup, args.json);
873
+ return 0;
874
+ } catch (error) {
875
+ printError(makeErrorResult(error, destination === void 0 ? null : resolve(destination), repositoryCreated, signalCode), args.json);
876
+ return exitCode(error, signalCode);
877
+ } finally {
878
+ process.removeListener("SIGINT", onInterrupt);
879
+ process.removeListener("SIGTERM", onTerminate);
880
+ }
881
+ }
882
+ const exitStatus = await run(process.argv.slice(2));
883
+ process.exitCode = exitStatus;
884
+ //#endregion
885
+ export {};