saasaloy 0.5.2 → 0.6.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.
Files changed (83) hide show
  1. package/README.md +7 -7
  2. package/dist/index.js +637 -522
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/schemas/registry-item.schema.json +31 -1
  6. package/templates/base/AGENTS.md +180 -53
  7. package/templates/base/README.md +20 -8
  8. package/templates/base/_agents/skills/saasaloy-design/SKILL.md +26 -15
  9. package/templates/base/_agents/skills/saasaloy-env/SKILL.md +130 -0
  10. package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +63 -46
  11. package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +1 -1
  12. package/templates/base/_gitignore +0 -3
  13. package/templates/base/_saasaloy-base.json +8 -1
  14. package/templates/base/{DESIGN.md → apps/web/DESIGN.md} +36 -12
  15. package/templates/base/apps/web/astro.config.mjs +36 -6
  16. package/templates/base/apps/web/package.json +2 -1
  17. package/templates/base/apps/web/src/env.ts +41 -0
  18. package/templates/base/apps/web/src/layouts/Layout.astro +39 -11
  19. package/templates/base/apps/web/src/pages/index.astro +33 -24
  20. package/templates/base/apps/web/src/pages/privacy.astro +10 -2
  21. package/templates/base/apps/web/src/pages/terms.astro +10 -2
  22. package/templates/base/oxlint.config.mjs +91 -4
  23. package/templates/base/package.json +7 -3
  24. package/templates/base/packages/env/_env.example +54 -0
  25. package/templates/base/packages/env/package.json +35 -0
  26. package/templates/base/packages/env/scripts/env-setup.ts +87 -0
  27. package/templates/base/packages/env/src/check.test.ts +41 -0
  28. package/templates/base/packages/env/src/check.ts +50 -0
  29. package/templates/base/packages/env/src/define.test.ts +206 -0
  30. package/templates/base/packages/env/src/define.ts +315 -0
  31. package/templates/base/packages/env/src/deploy.test.ts +438 -0
  32. package/templates/base/packages/env/src/deploy.ts +518 -0
  33. package/templates/base/packages/env/src/dotenv.test.ts +179 -0
  34. package/templates/base/packages/env/src/dotenv.ts +173 -0
  35. package/templates/base/packages/env/src/env-file.ts +51 -0
  36. package/templates/base/packages/env/src/error.ts +38 -0
  37. package/templates/base/packages/env/src/example.test.ts +86 -0
  38. package/templates/base/packages/env/src/example.ts +90 -0
  39. package/templates/base/packages/env/src/index.ts +15 -0
  40. package/templates/base/packages/env/src/schema.ts +63 -0
  41. package/templates/base/packages/env/src/services.test.ts +83 -0
  42. package/templates/base/packages/env/src/services.ts +110 -0
  43. package/templates/base/packages/env/src/setup.test.ts +254 -0
  44. package/templates/base/packages/env/src/setup.ts +177 -0
  45. package/templates/base/packages/env/src/source.ts +20 -0
  46. package/templates/base/packages/env/src/sources.test.ts +137 -0
  47. package/templates/base/packages/env/src/sources.ts +93 -0
  48. package/templates/base/packages/env/src/zod.ts +8 -0
  49. package/templates/base/packages/env/tsconfig.json +13 -0
  50. package/templates/base/packages/tsconfig/base.json +7 -0
  51. package/templates/base/packages/ui/components.json +1 -1
  52. package/templates/base/packages/ui/package.json +1 -0
  53. package/templates/base/packages/ui/src/blocks/audience.tsx +73 -0
  54. package/templates/base/packages/ui/src/blocks/cta.tsx +41 -30
  55. package/templates/base/packages/ui/src/blocks/error-state.tsx +2 -0
  56. package/templates/base/packages/ui/src/blocks/faq.tsx +64 -36
  57. package/templates/base/packages/ui/src/blocks/feature-grid.tsx +48 -146
  58. package/templates/base/packages/ui/src/blocks/footer.tsx +129 -70
  59. package/templates/base/packages/ui/src/blocks/hero.tsx +183 -26
  60. package/templates/base/packages/ui/src/blocks/how-it-works.tsx +75 -0
  61. package/templates/base/packages/ui/src/blocks/logo-cloud.tsx +118 -0
  62. package/templates/base/packages/ui/src/blocks/navbar.tsx +204 -88
  63. package/templates/base/packages/ui/src/blocks/pain-points.tsx +131 -0
  64. package/templates/base/packages/ui/src/blocks/pricing-table.tsx +112 -94
  65. package/templates/base/packages/ui/src/blocks/showcase.tsx +185 -0
  66. package/templates/base/packages/ui/src/blocks/testimonials.tsx +85 -0
  67. package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +16 -12
  68. package/templates/base/packages/ui/src/components/badge.tsx +1 -1
  69. package/templates/base/packages/ui/src/components/button.tsx +2 -2
  70. package/templates/base/packages/ui/src/components/tabs.tsx +1 -1
  71. package/templates/base/packages/ui/src/content/landing.ts +590 -125
  72. package/templates/base/packages/ui/src/index.ts +6 -2
  73. package/templates/base/packages/ui/src/lib/external-link.ts +36 -0
  74. package/templates/base/packages/ui/src/lib/icons.ts +131 -0
  75. package/templates/base/packages/ui/src/lib/inline-code.ts +29 -0
  76. package/templates/base/packages/ui/src/lib/theme.ts +140 -56
  77. package/templates/base/packages/ui/src/styles/globals.css +47 -125
  78. package/templates/base/packages/ui/src/styles/web.css +359 -0
  79. package/templates/base/pnpm-workspace.yaml +0 -6
  80. package/templates/base/saasaloy.json +1 -0
  81. package/templates/base/stylelint.config.js +3 -3
  82. package/templates/base/turbo.json +13 -0
  83. package/templates/base/vitest.config.ts +21 -0
package/dist/index.js CHANGED
@@ -14,17 +14,17 @@ import {
14
14
  confirm,
15
15
  intro,
16
16
  isCancel,
17
- log,
17
+ log as log2,
18
18
  note,
19
19
  outro,
20
20
  select
21
21
  } from "@clack/prompts";
22
- import { posix as posix4 } from "path";
22
+ import { posix as posix3 } from "path";
23
23
  import pc2 from "picocolors";
24
24
 
25
25
  // src/lib/applier.ts
26
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
27
- import { dirname as dirname3, posix as posix2 } from "path";
26
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
27
+ import { dirname as dirname4, posix as posix2 } from "path";
28
28
 
29
29
  // src/lib/exit.ts
30
30
  var EXIT_OK = 0;
@@ -152,6 +152,10 @@ function formatOwnedCollisions(collisions, requested = "these modules") {
152
152
  );
153
153
  }
154
154
 
155
+ // src/lib/env-example.ts
156
+ import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
157
+ import { dirname as dirname2 } from "path";
158
+
155
159
  // src/lib/fs-utils.ts
156
160
  import { createHash } from "crypto";
157
161
  import {
@@ -274,20 +278,145 @@ async function classifyLink(linkAbs, targetAbs) {
274
278
  return "conflict";
275
279
  }
276
280
  const dest = await readlink(linkAbs);
277
- const resolved2 = resolve(dirname(linkAbs), dest);
278
- return resolved2 === resolve(targetAbs) ? "correct" : "conflict";
281
+ const resolved = resolve(dirname(linkAbs), dest);
282
+ return resolved === resolve(targetAbs) ? "correct" : "conflict";
279
283
  }
280
284
  async function createDirLink(linkAbs, targetAbs) {
281
285
  await mkdir(dirname(linkAbs), { recursive: true });
282
286
  await (process.platform === "win32" ? symlink(resolve(targetAbs), linkAbs, "junction") : symlink(relative(dirname(linkAbs), targetAbs), linkAbs, "dir"));
283
287
  }
284
288
 
289
+ // src/lib/env-example.ts
290
+ var ENV_EXAMPLE = "packages/env/.env.example";
291
+ var PUBLIC_PREFIX = "PUBLIC_";
292
+ var SERVICE_NAMES = ["api", "web", "admin", "deploy"];
293
+ var CLIENT_SERVICES = ["web", "admin"];
294
+ function isPublicVar(name) {
295
+ return name.startsWith(PUBLIC_PREFIX);
296
+ }
297
+ var HEADER = [
298
+ "# The one key list for this repository. Every `.env` that `pnpm env:setup` writes is",
299
+ "# written from this file. There is no second example file, and no `.dev.vars.example`.",
300
+ "#",
301
+ "# Generated by `saasaloy add` from the installed modules' `envVars`. A value you type",
302
+ "# here survives that, and nothing is ever removed \u2014 `saasaloy remove` owns deletions.",
303
+ "#",
304
+ "# `.env` is gitignored; this file is tracked. Keep real secrets out of it.",
305
+ "#",
306
+ "# A `# @services <names>` line sets which services take every key below it, until the",
307
+ "# next such line. A key with no section above it fails `pnpm env:setup`."
308
+ ];
309
+ function servicesFor(name, envServices) {
310
+ const declared = envServices?.[name] ?? envServices?.default;
311
+ if (declared && declared.length > 0) {
312
+ return SERVICE_NAMES.filter((service) => declared.includes(service));
313
+ }
314
+ return isPublicVar(name) ? [...CLIENT_SERVICES] : ["api"];
315
+ }
316
+ var SECTION = /^\s*#\s*@services\b(.*)$/;
317
+ function parseEnvExample(text4) {
318
+ const entries = {};
319
+ let comment = [];
320
+ let services;
321
+ for (const line of text4.split("\n")) {
322
+ const trimmed = line.trim();
323
+ const section = SECTION.exec(trimmed);
324
+ if (section) {
325
+ const named = new Set((section[1] ?? "").split(/[\s,]+/).filter(Boolean));
326
+ services = SERVICE_NAMES.filter((name) => named.has(name));
327
+ comment = [];
328
+ continue;
329
+ }
330
+ if (trimmed.startsWith("#")) {
331
+ comment.push(trimmed);
332
+ continue;
333
+ }
334
+ if (trimmed === "") {
335
+ comment = [];
336
+ continue;
337
+ }
338
+ const eq = trimmed.indexOf("=");
339
+ if (eq <= 0) {
340
+ comment = [];
341
+ continue;
342
+ }
343
+ entries[trimmed.slice(0, eq).trim()] = {
344
+ value: trimmed.slice(eq + 1),
345
+ comment,
346
+ services: services && services.length > 0 ? services : void 0
347
+ };
348
+ comment = [];
349
+ }
350
+ return entries;
351
+ }
352
+ function sectionKey(services) {
353
+ return SERVICE_NAMES.filter((name) => services.includes(name)).join(" ");
354
+ }
355
+ var BLANK_ON_PURPOSE = "# Blank on purpose: this key may stay empty.";
356
+ function renderEnvExample(args) {
357
+ const { envVars, devVars, existing } = args;
358
+ const optional = new Set(args.optional);
359
+ const names = [
360
+ .../* @__PURE__ */ new Set([...Object.keys(envVars), ...Object.keys(existing)])
361
+ ].toSorted();
362
+ const bySection = /* @__PURE__ */ new Map();
363
+ for (const name of names) {
364
+ const declared = args.services[name];
365
+ const kept = existing[name]?.services;
366
+ const section = sectionKey(
367
+ declared || kept ? [...kept ?? [], ...declared ?? []] : servicesFor(name)
368
+ );
369
+ bySection.set(section, [...bySection.get(section) ?? [], name]);
370
+ }
371
+ const blocks = [];
372
+ for (const section of [...bySection.keys()].toSorted()) {
373
+ blocks.push(`# @services ${section}`);
374
+ for (const name of bySection.get(section) ?? []) {
375
+ const entry = existing[name];
376
+ const value = entry?.value ?? devVars[name] ?? "";
377
+ const description = envVars[name];
378
+ const kept = entry?.comment ?? [];
379
+ const lines = description ? description.split("\n").map((line) => `# ${line}`) : kept.length > 0 ? [...kept] : ["# Declared outside saasaloy \u2014 no module describes this one."];
380
+ if (optional.has(name)) {
381
+ lines.unshift(BLANK_ON_PURPOSE);
382
+ }
383
+ lines.push(`${name}=${value}`);
384
+ blocks.push(lines.join("\n"));
385
+ }
386
+ }
387
+ return `${[HEADER.join("\n"), ...blocks].join("\n\n")}
388
+ `;
389
+ }
390
+ async function writeEnvExample(args) {
391
+ const { root, envVars, devVars, services, optional } = args;
392
+ const abs = resolveWithinRoot(root, ENV_EXAMPLE);
393
+ await assertNoSymlinkPath(root, abs);
394
+ const before = await pathExists(abs) ? await readFile2(abs, "utf-8") : "";
395
+ const existing = parseEnvExample(before);
396
+ if (Object.keys(envVars).length === 0 && Object.keys(existing).length === 0) {
397
+ return void 0;
398
+ }
399
+ const content = renderEnvExample({
400
+ devVars,
401
+ envVars,
402
+ existing,
403
+ optional,
404
+ services
405
+ });
406
+ if (before === content) {
407
+ return void 0;
408
+ }
409
+ await mkdir2(dirname2(abs), { recursive: true });
410
+ await writeFile(abs, content, "utf-8");
411
+ return ENV_EXAMPLE;
412
+ }
413
+
285
414
  // src/lib/manifest.ts
286
- import { mkdir as mkdir2, readFile as readFile3, writeFile } from "fs/promises";
287
- import { dirname as dirname2, join as join3 } from "path";
415
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
416
+ import { dirname as dirname3, join as join3 } from "path";
288
417
 
289
418
  // src/lib/schema.ts
290
- import { readFile as readFile2 } from "fs/promises";
419
+ import { readFile as readFile3 } from "fs/promises";
291
420
  import { join as join2 } from "path";
292
421
  import { fileURLToPath } from "url";
293
422
  import AjvDefault from "ajv/dist/2020.js";
@@ -320,7 +449,7 @@ async function getValidator(name) {
320
449
  return cached;
321
450
  }
322
451
  const schema = JSON.parse(
323
- await readFile2(join2(await schemaDir(), SCHEMA_FILES[name]), "utf-8")
452
+ await readFile3(join2(await schemaDir(), SCHEMA_FILES[name]), "utf-8")
324
453
  );
325
454
  const validator = ajv.compile(schema);
326
455
  validators.set(name, validator);
@@ -405,7 +534,7 @@ async function loadManifest(root) {
405
534
  }
406
535
  let raw;
407
536
  try {
408
- raw = JSON.parse(await readFile3(file, "utf-8"));
537
+ raw = JSON.parse(await readFile4(file, "utf-8"));
409
538
  } catch (error) {
410
539
  throw new RefusalError(
411
540
  `${MANIFEST_FILE} is invalid:
@@ -430,8 +559,8 @@ async function loadManifest(root) {
430
559
  }
431
560
  async function saveManifest(root, manifest) {
432
561
  const file = join3(root, MANIFEST_FILE);
433
- await mkdir2(dirname2(file), { recursive: true });
434
- await writeFile(file, `${JSON.stringify(manifest, null, 2)}
562
+ await mkdir3(dirname3(file), { recursive: true });
563
+ await writeFile2(file, `${JSON.stringify(manifest, null, 2)}
435
564
  `, "utf-8");
436
565
  }
437
566
 
@@ -1828,7 +1957,7 @@ function refusalReason(table, source, patch) {
1828
1957
  }
1829
1958
 
1830
1959
  // src/lib/saasaloy-config.ts
1831
- import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
1960
+ import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
1832
1961
  import { join as join4 } from "path";
1833
1962
  var CONFIG_FILE = "saasaloy.json";
1834
1963
  async function loadConfig(root) {
@@ -1838,7 +1967,7 @@ async function loadConfig(root) {
1838
1967
  `No ${CONFIG_FILE} found in ${root}. Run \`saasaloy init\` first, or cd into a Saasaloy project.`
1839
1968
  );
1840
1969
  }
1841
- const parsed = JSON.parse(await readFile4(file, "utf-8"));
1970
+ const parsed = JSON.parse(await readFile5(file, "utf-8"));
1842
1971
  const result = await validateSaasaloyConfig(parsed);
1843
1972
  if (!result.valid) {
1844
1973
  throw new RefusalError(
@@ -1861,7 +1990,7 @@ function migrateBase(config) {
1861
1990
  }
1862
1991
  async function saveConfig(root, config) {
1863
1992
  const file = join4(root, CONFIG_FILE);
1864
- await writeFile2(file, `${JSON.stringify(config, null, 2)}
1993
+ await writeFile3(file, `${JSON.stringify(config, null, 2)}
1865
1994
  `, "utf-8");
1866
1995
  }
1867
1996
  function resolveTarget(aliases, target) {
@@ -1881,6 +2010,9 @@ function resolveTarget(aliases, target) {
1881
2010
  );
1882
2011
  }
1883
2012
  const trimmed = base.endsWith("/") ? base.slice(0, -1) : base;
2013
+ if (trimmed === "." || trimmed === "") {
2014
+ return rest;
2015
+ }
1884
2016
  return `${trimmed}/${rest}`;
1885
2017
  }
1886
2018
 
@@ -2004,7 +2136,7 @@ async function classify(targetAbs, target, newHash, manifest, module) {
2004
2136
  const stale = manifest.managed[target];
2005
2137
  return stale && stale.module !== module ? { action: "create", staleOwner: stale.module } : { action: "create" };
2006
2138
  }
2007
- const oldContent = await readFile5(targetAbs, "utf-8");
2139
+ const oldContent = await readFile6(targetAbs, "utf-8");
2008
2140
  const oldHash = hashContent(oldContent);
2009
2141
  const managed = manifest.managed[target];
2010
2142
  const ownedBy = managed && managed.module !== module ? { ownedBy: managed.module } : {};
@@ -2021,7 +2153,7 @@ async function classify(targetAbs, target, newHash, manifest, module) {
2021
2153
  return { action: "conflict", oldContent };
2022
2154
  }
2023
2155
  async function planModuleFile(module, ref, root, manifest) {
2024
- const content = await readFile5(ref.abs, "utf-8");
2156
+ const content = await readFile6(ref.abs, "utf-8");
2025
2157
  const newHash = hashContent(content);
2026
2158
  const targetAbs = resolveWithinRoot(root, ref.target);
2027
2159
  const { action, oldContent, ownedBy, staleOwner } = await classify(
@@ -2067,6 +2199,9 @@ async function buildPlan(args) {
2067
2199
  const devDependencies = [];
2068
2200
  const envVars = {};
2069
2201
  const devVars = {};
2202
+ const envServices = {};
2203
+ const envServicesGuessed = /* @__PURE__ */ new Set();
2204
+ const envOptional = /* @__PURE__ */ new Set();
2070
2205
  const removeWarnings = {};
2071
2206
  const aliases = {};
2072
2207
  for (const name of install) {
@@ -2164,8 +2299,20 @@ async function buildPlan(args) {
2164
2299
  for (const [key, value] of Object.entries(item.devVars ?? {})) {
2165
2300
  devVars[key] = value;
2166
2301
  }
2302
+ for (const key of item.envOptional ?? []) {
2303
+ envOptional.add(key);
2304
+ }
2167
2305
  for (const [key, value] of Object.entries(item.envVars ?? {})) {
2168
2306
  envVars[key] = value;
2307
+ if (!item.envServices) {
2308
+ envServicesGuessed.add(name);
2309
+ }
2310
+ envServices[key] = [
2311
+ .../* @__PURE__ */ new Set([
2312
+ ...envServices[key] ?? [],
2313
+ ...servicesFor(key, item.envServices)
2314
+ ])
2315
+ ];
2169
2316
  }
2170
2317
  if (item.removeWarnings && item.removeWarnings.length > 0) {
2171
2318
  removeWarnings[name] = [...item.removeWarnings];
@@ -2212,6 +2359,9 @@ async function buildPlan(args) {
2212
2359
  dependencies,
2213
2360
  devDependencies,
2214
2361
  devVars,
2362
+ envOptional: [...envOptional].toSorted(),
2363
+ envServices,
2364
+ envServicesGuessed: [...envServicesGuessed].toSorted(),
2215
2365
  envVars,
2216
2366
  aliases,
2217
2367
  aliasConflicts,
@@ -2244,8 +2394,8 @@ async function executePlan(plan, root, config, manifest) {
2244
2394
  }
2245
2395
  if (WRITABLE.has(file.action)) {
2246
2396
  await assertNoSymlinkPath(root, file.targetAbs);
2247
- await mkdir3(dirname3(file.targetAbs), { recursive: true });
2248
- await writeFile3(file.targetAbs, file.content, "utf-8");
2397
+ await mkdir4(dirname4(file.targetAbs), { recursive: true });
2398
+ await writeFile4(file.targetAbs, file.content, "utf-8");
2249
2399
  written.push(file);
2250
2400
  } else {
2251
2401
  refreshed.push(file);
@@ -2263,10 +2413,10 @@ async function executePlan(plan, root, config, manifest) {
2263
2413
  continue;
2264
2414
  }
2265
2415
  await assertNoSymlinkPath(root, p.fileAbs);
2266
- const source = await readFile5(p.fileAbs, "utf-8");
2416
+ const source = await readFile6(p.fileAbs, "utf-8");
2267
2417
  const { content, changed, reason } = applyPatch(source, p.patch, p.file);
2268
2418
  if (changed) {
2269
- await writeFile3(p.fileAbs, content, "utf-8");
2419
+ await writeFile4(p.fileAbs, content, "utf-8");
2270
2420
  patched.push(p);
2271
2421
  recordPatch(manifest, {
2272
2422
  file: p.file,
@@ -2652,6 +2802,39 @@ function formatCliMismatches(mismatches, requested, cliVersion) {
2652
2802
  `Nothing was written.`
2653
2803
  ].join("\n");
2654
2804
  }
2805
+ function describeInvalidDescriptor(args) {
2806
+ const { name, raw, errors, cliVersion } = args;
2807
+ const range = rawRequiresRange(raw);
2808
+ if (range !== void 0 && isValidRange(range)) {
2809
+ if (cliVersion === UNKNOWN_VERSION) {
2810
+ return `Module "${name}" needs saasaloy ${range}, and this CLI's own version could not be read. Upgrade with \`${UPGRADE_COMMAND}\`.`;
2811
+ }
2812
+ if (!satisfies(cliVersion, range)) {
2813
+ return `Module "${name}" needs saasaloy ${range}, and ${cliVersion} is installed. Upgrade with \`${UPGRADE_COMMAND}\`.`;
2814
+ }
2815
+ }
2816
+ const lines = [
2817
+ `Module "${name}" has an invalid descriptor:`,
2818
+ ...errors.map((e) => ` ${e}`)
2819
+ ];
2820
+ if (errors.some((e) => e.includes("unexpected property"))) {
2821
+ lines.push(
2822
+ `It may use a field newer than saasaloy ${cliVersion}. Upgrade with \`${UPGRADE_COMMAND}\` and retry.`
2823
+ );
2824
+ }
2825
+ return lines.join("\n");
2826
+ }
2827
+ function rawRequiresRange(raw) {
2828
+ if (typeof raw !== "object" || raw === null) {
2829
+ return void 0;
2830
+ }
2831
+ const { requires } = raw;
2832
+ if (typeof requires !== "object" || requires === null) {
2833
+ return void 0;
2834
+ }
2835
+ const { saasaloy } = requires;
2836
+ return typeof saasaloy === "string" ? saasaloy : void 0;
2837
+ }
2655
2838
 
2656
2839
  // src/lib/conflicts.ts
2657
2840
  function pairKey(a, b) {
@@ -2740,81 +2923,88 @@ function planWritesUi(plan) {
2740
2923
  );
2741
2924
  }
2742
2925
 
2743
- // src/lib/dev-vars.ts
2744
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
2745
- import { dirname as dirname4, posix as posix3 } from "path";
2746
- var DEV_VARS_EXAMPLE = ".dev.vars.example";
2747
- var API_ALIAS = "@api";
2748
- function apiWorkspace(aliases) {
2749
- const src = aliases[API_ALIAS];
2750
- if (!src) {
2751
- return void 0;
2926
+ // src/lib/format.ts
2927
+ import { spawn } from "child_process";
2928
+ import { join as join5 } from "path";
2929
+ import { log } from "@clack/prompts";
2930
+ function prettierBin(root) {
2931
+ const name = process.platform === "win32" ? "prettier.cmd" : "prettier";
2932
+ return join5(root, "node_modules", ".bin", name);
2933
+ }
2934
+ async function formatFiles(root, files, run2 = runChild) {
2935
+ const unique = [...new Set(files)];
2936
+ if (unique.length === 0) {
2937
+ return { kind: "nothing" };
2938
+ }
2939
+ const bin = prettierBin(root);
2940
+ if (!await pathExists(bin)) {
2941
+ return { files: unique, kind: "no-prettier" };
2942
+ }
2943
+ const result = await run2(
2944
+ bin,
2945
+ ["--write", "--ignore-unknown", "--log-level", "warn", "--", ...unique],
2946
+ root
2947
+ );
2948
+ if (!result.ok) {
2949
+ return {
2950
+ files: unique,
2951
+ kind: "failed",
2952
+ message: result.message ?? "prettier failed"
2953
+ };
2752
2954
  }
2753
- const workspace = posix3.dirname(src.replace(/\/+$/, ""));
2754
- return workspace === "." || workspace === "/" ? void 0 : workspace;
2755
- }
2756
- var HEADER = [
2757
- "# Environment variables this project's modules declare.",
2758
- "# Generated by `saasaloy add`, one entry per declared variable, comments and all.",
2759
- "#",
2760
- "# Copy this file to `.dev.vars` and fill in the blanks: `.dev.vars` is what",
2761
- "# `wrangler dev` reads, and it is gitignored. This example is not \u2014 keep it",
2762
- "# checked in, and keep real secrets out of it."
2763
- ];
2764
- function devVarsExamplePath(aliases) {
2765
- const workspace = apiWorkspace(aliases);
2766
- return workspace ? posix3.join(workspace, DEV_VARS_EXAMPLE) : void 0;
2955
+ return { files: unique, kind: "formatted" };
2767
2956
  }
2768
- function parseDevVars(text4) {
2769
- const values = {};
2770
- for (const line of text4.split("\n")) {
2771
- const trimmed = line.trim();
2772
- if (!trimmed || trimmed.startsWith("#")) {
2773
- continue;
2774
- }
2775
- const eq = trimmed.indexOf("=");
2776
- if (eq <= 0) {
2777
- continue;
2778
- }
2779
- values[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1);
2957
+ function formatReport(outcome) {
2958
+ if (outcome.kind === "nothing") {
2959
+ return null;
2780
2960
  }
2781
- return values;
2782
- }
2783
- function renderDevVarsExample(args) {
2784
- const { envVars, devVars, existing } = args;
2785
- const names = [
2786
- .../* @__PURE__ */ new Set([...Object.keys(envVars), ...Object.keys(existing)])
2787
- ].toSorted();
2788
- const blocks = names.map((name) => {
2789
- const value = existing[name] ?? devVars[name] ?? "";
2790
- const description = envVars[name];
2791
- const lines = description ? description.split("\n").map((line) => `# ${line}`) : ["# Declared outside saasaloy \u2014 no module describes this one."];
2792
- lines.push(`${name}=${value}`);
2793
- return lines.join("\n");
2794
- });
2795
- return `${[HEADER.join("\n"), ...blocks].join("\n\n")}
2796
- `;
2797
- }
2798
- async function writeDevVarsExample(args) {
2799
- const { root, aliases, envVars, devVars } = args;
2800
- const target = devVarsExamplePath(aliases);
2801
- if (!target) {
2802
- return void 0;
2961
+ const count = `${String(outcome.files.length)} patched file${outcome.files.length === 1 ? "" : "s"}`;
2962
+ if (outcome.kind === "formatted") {
2963
+ return { level: "step", message: `format ${count} with Prettier` };
2803
2964
  }
2804
- const abs = resolveWithinRoot(root, target);
2805
- await assertNoSymlinkPath(root, abs);
2806
- const before = await pathExists(abs) ? await readFile6(abs, "utf-8") : void 0;
2807
- const existing = before === void 0 ? {} : parseDevVars(before);
2808
- if (Object.keys(envVars).length === 0 && Object.keys(existing).length === 0) {
2809
- return void 0;
2965
+ if (outcome.kind === "no-prettier") {
2966
+ return {
2967
+ level: "info",
2968
+ message: `Prettier isn't installed yet, so ${count} kept the patch layout. Run \`pnpm install\`, then \`pnpm format\`.`
2969
+ };
2810
2970
  }
2811
- const content = renderDevVarsExample({ devVars, envVars, existing });
2812
- if (before === content) {
2813
- return void 0;
2971
+ return {
2972
+ level: "warn",
2973
+ message: `Couldn't format ${count} \u2014 ${outcome.message}. Run \`pnpm format\`.`
2974
+ };
2975
+ }
2976
+ async function formatPatchedFiles(root, files, options) {
2977
+ const report = formatReport(await formatFiles(root, files));
2978
+ if (report !== null) {
2979
+ log[report.level](report.message, options);
2814
2980
  }
2815
- await mkdir4(dirname4(abs), { recursive: true });
2816
- await writeFile4(abs, content, "utf-8");
2817
- return target;
2981
+ }
2982
+ function runChild(command, args, cwd) {
2983
+ return new Promise((resolvePromise) => {
2984
+ const child = spawn(command, args, {
2985
+ cwd,
2986
+ shell: process.platform === "win32",
2987
+ stdio: ["ignore", "pipe", "pipe"]
2988
+ });
2989
+ let output = "";
2990
+ child.stdout?.on("data", (chunk) => {
2991
+ output += chunk.toString();
2992
+ });
2993
+ child.stderr?.on("data", (chunk) => {
2994
+ output += chunk.toString();
2995
+ });
2996
+ child.on("error", (error) => {
2997
+ resolvePromise({ message: error.message, ok: false });
2998
+ });
2999
+ child.on("close", (code) => {
3000
+ resolvePromise(
3001
+ code === 0 ? { ok: true } : {
3002
+ message: output.trim() || `prettier exited with code ${String(code)}`,
3003
+ ok: false
3004
+ }
3005
+ );
3006
+ });
3007
+ });
2818
3008
  }
2819
3009
 
2820
3010
  // src/lib/diff.ts
@@ -2865,7 +3055,7 @@ function lineDiff(oldText, newText) {
2865
3055
 
2866
3056
  // src/lib/lock.ts
2867
3057
  import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
2868
- import { join as join5 } from "path";
3058
+ import { join as join6 } from "path";
2869
3059
  var LOCK_FILE = "saasaloy-lock.json";
2870
3060
  var LOCKFILE_VERSION = 1;
2871
3061
  var LOCK_SCHEMA_URL = "https://saasaloy.dev/schemas/saasaloy-lock.schema.json";
@@ -2877,7 +3067,7 @@ function emptyLock() {
2877
3067
  };
2878
3068
  }
2879
3069
  async function loadLock(root) {
2880
- const file = join5(root, LOCK_FILE);
3070
+ const file = join6(root, LOCK_FILE);
2881
3071
  if (!await pathExists(file)) {
2882
3072
  return emptyLock();
2883
3073
  }
@@ -2907,7 +3097,7 @@ async function loadLock(root) {
2907
3097
  };
2908
3098
  }
2909
3099
  async function saveLock(root, lock) {
2910
- const file = join5(root, LOCK_FILE);
3100
+ const file = join6(root, LOCK_FILE);
2911
3101
  await writeFile5(file, `${JSON.stringify(lock, null, 2)}
2912
3102
  `, "utf-8");
2913
3103
  }
@@ -2944,7 +3134,7 @@ async function persistLedger(input) {
2944
3134
 
2945
3135
  // src/lib/pkg-json.ts
2946
3136
  import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
2947
- import { join as join6 } from "path";
3137
+ import { join as join7 } from "path";
2948
3138
  function parseDep(spec) {
2949
3139
  const at = spec.lastIndexOf("@");
2950
3140
  if (at > 0) {
@@ -2987,7 +3177,7 @@ function planDeps(pkg, deps, devDeps = []) {
2987
3177
  return { added, conflicts, devAdded, skipped };
2988
3178
  }
2989
3179
  async function readRootPackageJson(root) {
2990
- const file = join6(root, "package.json");
3180
+ const file = join7(root, "package.json");
2991
3181
  if (!await pathExists(file)) {
2992
3182
  return null;
2993
3183
  }
@@ -3014,20 +3204,20 @@ async function writeDeps(root, pkg, added, devAdded = []) {
3014
3204
  if (devAdded.length > 0) {
3015
3205
  pkg.devDependencies = mergeSorted(pkg.devDependencies, devAdded);
3016
3206
  }
3017
- const file = join6(root, "package.json");
3207
+ const file = join7(root, "package.json");
3018
3208
  await writeFile6(file, `${JSON.stringify(pkg, null, 2)}
3019
3209
  `, "utf-8");
3020
3210
  }
3021
3211
 
3022
3212
  // src/lib/project.ts
3023
- import { dirname as dirname5, join as join7, parse } from "path";
3213
+ import { dirname as dirname5, join as join8, parse } from "path";
3024
3214
  var MARKERS = ["saasaloy.json"];
3025
3215
  async function findProjectRoot(start = process.cwd()) {
3026
3216
  let dir = start;
3027
3217
  const { root } = parse(dir);
3028
3218
  for (; ; ) {
3029
3219
  for (const marker of MARKERS) {
3030
- if (await pathExists(join7(dir, marker))) {
3220
+ if (await pathExists(join8(dir, marker))) {
3031
3221
  return dir;
3032
3222
  }
3033
3223
  }
@@ -3039,10 +3229,26 @@ async function findProjectRoot(start = process.cwd()) {
3039
3229
  }
3040
3230
 
3041
3231
  // src/lib/registry.ts
3042
- import { mkdtemp, readFile as readFile9, rm } from "fs/promises";
3232
+ import { mkdtemp, readFile as readFile10, rm } from "fs/promises";
3043
3233
  import { tmpdir } from "os";
3044
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "path";
3234
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve2 } from "path";
3045
3235
  import { downloadTemplate } from "giget";
3236
+
3237
+ // src/version.ts
3238
+ import { readFile as readFile9 } from "fs/promises";
3239
+ import { fileURLToPath as fileURLToPath2 } from "url";
3240
+ var UNKNOWN_VERSION2 = "unknown";
3241
+ async function readVersion() {
3242
+ try {
3243
+ const file = fileURLToPath2(new URL("../package.json", import.meta.url));
3244
+ const parsed = JSON.parse(await readFile9(file, "utf-8"));
3245
+ return typeof parsed.version === "string" ? parsed.version : UNKNOWN_VERSION2;
3246
+ } catch {
3247
+ return UNKNOWN_VERSION2;
3248
+ }
3249
+ }
3250
+
3251
+ // src/lib/registry.ts
3046
3252
  var REGISTRY_ENV = "SAASALOY_REGISTRY_DIR";
3047
3253
  var DEFAULT_OWNER = "mimukit";
3048
3254
  var DEFAULT_REPO = "saasaloy";
@@ -3093,7 +3299,7 @@ function createRegistrySource(coord) {
3093
3299
  );
3094
3300
  }
3095
3301
  async function loadModuleFolder(dir, name, requiredBy) {
3096
- const file = join8(dir, "registry-item.json");
3302
+ const file = join9(dir, "registry-item.json");
3097
3303
  if (!await pathExists(file)) {
3098
3304
  const because = requiredBy ? ` (required by ${requiredBy})` : "";
3099
3305
  throw new RefusalError(
@@ -3102,7 +3308,7 @@ async function loadModuleFolder(dir, name, requiredBy) {
3102
3308
  }
3103
3309
  let parsed;
3104
3310
  try {
3105
- parsed = JSON.parse(await readFile9(file, "utf-8"));
3311
+ parsed = JSON.parse(await readFile10(file, "utf-8"));
3106
3312
  } catch (error) {
3107
3313
  throw new RefusalError(
3108
3314
  `Module "${name}" has an unreadable descriptor: ${file} is not valid JSON.`,
@@ -3112,8 +3318,12 @@ async function loadModuleFolder(dir, name, requiredBy) {
3112
3318
  const result = await validateRegistryItem(parsed);
3113
3319
  if (!result.valid) {
3114
3320
  throw new RefusalError(
3115
- `Module "${name}" has an invalid descriptor:
3116
- ${result.errors.join("\n ")}`
3321
+ describeInvalidDescriptor({
3322
+ cliVersion: await readVersion(),
3323
+ errors: result.errors,
3324
+ name,
3325
+ raw: parsed
3326
+ })
3117
3327
  );
3118
3328
  }
3119
3329
  const item = parsed;
@@ -3134,13 +3344,13 @@ var LocalRegistrySource = class {
3134
3344
  }
3135
3345
  async readModule(name, requiredBy) {
3136
3346
  await this.assertExists();
3137
- return loadModuleFolder(join8(this.dir, name), name, requiredBy);
3347
+ return loadModuleFolder(join9(this.dir, name), name, requiredBy);
3138
3348
  }
3139
3349
  async listModules() {
3140
3350
  await this.assertExists();
3141
3351
  const names = [];
3142
3352
  for (const name of await readDirNames(this.dir)) {
3143
- if (await pathExists(join8(this.dir, name, "registry-item.json"))) {
3353
+ if (await pathExists(join9(this.dir, name, "registry-item.json"))) {
3144
3354
  names.push(name);
3145
3355
  }
3146
3356
  }
@@ -3209,9 +3419,9 @@ var RemoteRegistrySource = class {
3209
3419
  }
3210
3420
  async readModule(name, requiredBy) {
3211
3421
  const { sha } = await this.resolve();
3212
- const parent = await mkdtemp(join8(tmpdir(), "saasaloy-reg-"));
3422
+ const parent = await mkdtemp(join9(tmpdir(), "saasaloy-reg-"));
3213
3423
  this.tempDirs.push(parent);
3214
- const dir = join8(parent, name);
3424
+ const dir = join9(parent, name);
3215
3425
  try {
3216
3426
  await withDeadline(
3217
3427
  downloadTemplate(
@@ -3513,20 +3723,6 @@ function wantsHelp(argv) {
3513
3723
  return argv.includes("--help") || argv.includes("-h");
3514
3724
  }
3515
3725
 
3516
- // src/version.ts
3517
- import { readFile as readFile10 } from "fs/promises";
3518
- import { fileURLToPath as fileURLToPath2 } from "url";
3519
- var UNKNOWN_VERSION2 = "unknown";
3520
- async function readVersion() {
3521
- try {
3522
- const file = fileURLToPath2(new URL("../package.json", import.meta.url));
3523
- const parsed = JSON.parse(await readFile10(file, "utf-8"));
3524
- return typeof parsed.version === "string" ? parsed.version : UNKNOWN_VERSION2;
3525
- } catch {
3526
- return UNKNOWN_VERSION2;
3527
- }
3528
- }
3529
-
3530
3726
  // src/commands/descriptions.ts
3531
3727
  var DESCRIPTIONS = {
3532
3728
  init: "scaffold a new Saasaloy project (base: Astro landing + ui + config)",
@@ -3675,12 +3871,12 @@ ${pc2.dim("New workspace(s) \u2014 run `pnpm install` to link them.")}`
3675
3871
  );
3676
3872
  }
3677
3873
  for (const conflict of plan.aliasConflicts) {
3678
- log.warn(
3874
+ log2.warn(
3679
3875
  `Alias redefinition: ${conflict} ${pc2.dim("(scaffold overrides the existing alias)")}.`
3680
3876
  );
3681
3877
  }
3682
3878
  for (const stale of plan.staleOwners) {
3683
- log.warn(describeStaleOwner({ ...stale, target: pc2.cyan(stale.target) }));
3879
+ log2.warn(describeStaleOwner({ ...stale, target: pc2.cyan(stale.target) }));
3684
3880
  }
3685
3881
  const newLinks = plan.links.filter((l) => l.action !== "conflict");
3686
3882
  if (newLinks.length > 0) {
@@ -3698,7 +3894,7 @@ ${pc2.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
3698
3894
  }
3699
3895
  for (const link of plan.links) {
3700
3896
  if (link.action === "conflict") {
3701
- log.warn(
3897
+ log2.warn(
3702
3898
  `Skill link ${pc2.cyan(link.path)} already exists and isn't ours \u2014 left untouched ${pc2.dim("(remove it to let `add` link the skill)")}.`
3703
3899
  );
3704
3900
  }
@@ -3712,7 +3908,7 @@ ${pc2.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
3712
3908
  }
3713
3909
  for (const p of plan.patches) {
3714
3910
  if (p.action === "missing") {
3715
- log.warn(
3911
+ log2.warn(
3716
3912
  `Config patch target ${pc2.cyan(p.file)} is missing \u2014 ${p.patch.kind} not applied ${pc2.dim(`(is ${p.module}'s prerequisite installed?)`)}.`
3717
3913
  );
3718
3914
  }
@@ -3727,7 +3923,7 @@ function wireUpSteps(result, aliases) {
3727
3923
  return [];
3728
3924
  }
3729
3925
  const skillByModule = new Map(
3730
- result.links.map((link) => [link.module, posix4.basename(link.path)])
3926
+ result.links.map((link) => [link.module, posix3.basename(link.path)])
3731
3927
  );
3732
3928
  const byModule = /* @__PURE__ */ new Map();
3733
3929
  for (const block of blocks) {
@@ -3743,13 +3939,13 @@ function wireUpSteps(result, aliases) {
3743
3939
  const verb = moduleTargets.length === 1 ? "is" : "are";
3744
3940
  const skill = skillByModule.get(module);
3745
3941
  if (skill === void 0) {
3746
- log.warn(
3942
+ log2.warn(
3747
3943
  `${pc2.cyan(module)} wrote ${targets} but ships no skill \u2014 its wire-up steps are missing. Import the block yourself ${pc2.dim("(a module that ships a block should ship a skill with a Wire-up section)")}.`
3748
3944
  );
3749
3945
  continue;
3750
3946
  }
3751
3947
  lines.push(
3752
- `Manual wire-up needed \u2014 ${targets} ${verb} on disk but on no page yet. In Claude Code, run ${pc2.cyan(`/${skill}`)} and follow its Wire-up section.`
3948
+ `Manual wire-up needed \u2014 ${targets} ${verb} on disk but on no page yet. In your AI coding tool, run ${pc2.cyan(`/${skill}`)} and follow its Wire-up section.`
3753
3949
  );
3754
3950
  }
3755
3951
  return lines;
@@ -3762,7 +3958,8 @@ function envSteps(names, devVarsPath) {
3762
3958
  const where = devVarsPath ? ` ${pc2.dim(`${devVarsPath} keeps the checked-in description of each.`)}` : "";
3763
3959
  return [
3764
3960
  `Set ${keys.map((key) => pc2.cyan(key)).join(", ")}.`,
3765
- `Run ${pc2.cyan("saasaloy env")} \u2014 it prompts for each unset one and writes it to the ${pc2.cyan(".dev.vars")} or ${pc2.cyan(".env")} that reads it.${where}`
3961
+ `Run ${pc2.cyan("saasaloy env")} \u2014 it prompts for each unset one and writes it to ${pc2.cyan("packages/env/.env")}.${where}`,
3962
+ `Then run ${pc2.cyan("pnpm env:setup")} to write each service's own ${pc2.cyan(".env")} from it.`
3766
3963
  ];
3767
3964
  }
3768
3965
  function formatIncomplete(incomplete, requested) {
@@ -3785,11 +3982,11 @@ async function persistState(input) {
3785
3982
  );
3786
3983
  } catch (error) {
3787
3984
  failures.push(error);
3788
- log.warn(`Couldn't pin ${LOCK_FILE} \u2014 ${formatFailure(error)}.`);
3985
+ log2.warn(`Couldn't pin ${LOCK_FILE} \u2014 ${formatFailure(error)}.`);
3789
3986
  }
3790
3987
  for (const failure of await persistLedger(input)) {
3791
3988
  failures.push(failure.error);
3792
- log.warn(
3989
+ log2.warn(
3793
3990
  `Couldn't write ${failure.file} \u2014 ${formatFailure(failure.error)}.`
3794
3991
  );
3795
3992
  }
@@ -3820,10 +4017,10 @@ async function applyAndPersist(input) {
3820
4017
  }
3821
4018
  function printNextSteps(plan, result, devVarsPath, aliases) {
3822
4019
  const lines = [...wireUpSteps(result, aliases)];
3823
- const skills = result.links.map((link) => posix4.basename(link.path)).toSorted();
4020
+ const skills = result.links.map((link) => posix3.basename(link.path)).toSorted();
3824
4021
  if (skills.length > 0) {
3825
4022
  lines.push(
3826
- `Read the module's own procedure \u2014 in Claude Code, run ${skills.map((name) => pc2.cyan(`/${name}`)).join(" or ")}.`
4023
+ `Read the module's own procedure \u2014 in your AI coding tool, run ${skills.map((name) => pc2.cyan(`/${name}`)).join(" or ")}.`
3827
4024
  );
3828
4025
  }
3829
4026
  lines.push(...envSteps(Object.keys(plan.envVars), devVarsPath));
@@ -3876,7 +4073,7 @@ async function runAdd(argv) {
3876
4073
  coord = pinToLock(coord, lock);
3877
4074
  source = createRegistrySource(coord);
3878
4075
  if (process.env[REGISTRY_ENV] && (coord.owner || coord.repo)) {
3879
- log.warn(
4076
+ log2.warn(
3880
4077
  `Ignoring source "${coord.owner}/${coord.repo}" \u2014 ${REGISTRY_ENV} override is set.`
3881
4078
  );
3882
4079
  }
@@ -3958,7 +4155,7 @@ async function runAdd(argv) {
3958
4155
  const manifest = await loadManifest(root);
3959
4156
  const conflicts = detectConflicts({ graph, config, lock });
3960
4157
  if (conflicts.missingLockEntries.length > 0) {
3961
- log.warn(
4158
+ log2.warn(
3962
4159
  `No lock entry for ${conflicts.missingLockEntries.map((m) => pc2.cyan(m)).join(", ")} \u2014 any conflict they declare can't be checked ${pc2.dim("(re-add them to record it)")}.`
3963
4160
  );
3964
4161
  }
@@ -3991,7 +4188,7 @@ async function runAdd(argv) {
3991
4188
  });
3992
4189
  summarizePlan(plan, requested, prereqs);
3993
4190
  if (planWritesUi(plan)) {
3994
- log.info(
4191
+ log2.info(
3995
4192
  `This module writes ${pc2.cyan("packages/ui/")}. Run ${pc2.cyan("/saasaloy-design update")} after it applies.`
3996
4193
  );
3997
4194
  }
@@ -4057,41 +4254,45 @@ async function runAdd(argv) {
4057
4254
  await writeDeps(root, pkg, added, devAdded);
4058
4255
  depsAdded = [...added, ...devAdded].map((d) => d.name);
4059
4256
  for (const conflict of depConflicts) {
4060
- log.warn(`Dependency version conflict \u2014 ${conflict}.`);
4257
+ log2.warn(`Dependency version conflict \u2014 ${conflict}.`);
4061
4258
  }
4062
4259
  } else {
4063
- log.warn(
4260
+ log2.warn(
4064
4261
  `No package.json at the project root \u2014 add ${allDeps.join(", ")} yourself.`
4065
4262
  );
4066
4263
  }
4067
4264
  }
4068
4265
  for (const file of result.written) {
4069
- log.step(`${ACTION_LABEL[file.action]} ${file.target}`);
4266
+ log2.step(`${ACTION_LABEL[file.action]} ${file.target}`);
4070
4267
  }
4071
4268
  for (const file of result.refreshed) {
4072
- log.step(`${ACTION_LABEL[file.action]} ${file.target}`);
4269
+ log2.step(`${ACTION_LABEL[file.action]} ${file.target}`);
4073
4270
  }
4074
4271
  for (const link of result.links) {
4075
4272
  const label = link.action === "create" ? pc2.green("link") : pc2.dim("link");
4076
- log.step(`${label} ${link.path} ${pc2.dim(`\u2192 ${link.target}`)}`);
4273
+ log2.step(`${label} ${link.path} ${pc2.dim(`\u2192 ${link.target}`)}`);
4077
4274
  }
4078
4275
  for (const link of result.linkConflicts) {
4079
- log.warn(
4276
+ log2.warn(
4080
4277
  `Skill link ${pc2.cyan(link.path)} left untouched \u2014 a non-saasaloy path already occupies it.`
4081
4278
  );
4082
4279
  }
4083
4280
  for (const p of result.patched) {
4084
- log.step(
4281
+ log2.step(
4085
4282
  `${pc2.green("patch")} ${p.file} ${pc2.dim(`\u2014 ${p.patch.kind}`)}`
4086
4283
  );
4087
4284
  }
4285
+ await formatPatchedFiles(
4286
+ root,
4287
+ result.patched.map((p) => p.file)
4288
+ );
4088
4289
  for (const p of result.patchConflicts) {
4089
- log.warn(
4290
+ log2.warn(
4090
4291
  `Config patch target ${pc2.cyan(p.file)} missing \u2014 ${p.patch.kind} skipped.`
4091
4292
  );
4092
4293
  }
4093
4294
  for (const r of result.patchRefusals) {
4094
- log.warn(
4295
+ log2.warn(
4095
4296
  `Config patch on ${pc2.cyan(r.patch.file)} skipped \u2014 ${r.reason}. Wire it by hand.`
4096
4297
  );
4097
4298
  }
@@ -4110,7 +4311,7 @@ ${pc2.dim("Changed while the plan was open \u2014 left alone rather than overwri
4110
4311
  (name) => !result.completed.includes(name)
4111
4312
  );
4112
4313
  if (incomplete.length > 0) {
4113
- log.warn(formatIncomplete(incomplete, requested));
4314
+ log2.warn(formatIncomplete(incomplete, requested));
4114
4315
  }
4115
4316
  if (result.heldBack.length > 0) {
4116
4317
  const merges = result.heldBack.map((f) => ` ${ACTION_LABEL[f.action]} ${f.target}`).join("\n");
@@ -4133,16 +4334,22 @@ ${pc2.dim("Run `pnpm install` to fetch them.")}`
4133
4334
  "Dependencies added"
4134
4335
  );
4135
4336
  }
4337
+ if (plan.envServicesGuessed.length > 0) {
4338
+ log2.warn(
4339
+ `${plan.envServicesGuessed.join(", ")} declare env vars without \`envServices\`, so the PUBLIC_ prefix decided which service reads each one. Check ${ENV_EXAMPLE}.`
4340
+ );
4341
+ }
4136
4342
  let devVarsPath;
4137
4343
  try {
4138
- devVarsPath = await writeDevVarsExample({
4139
- aliases: config.aliases,
4344
+ devVarsPath = await writeEnvExample({
4140
4345
  devVars: plan.devVars,
4141
4346
  envVars: plan.envVars,
4347
+ optional: plan.envOptional,
4348
+ services: plan.envServices,
4142
4349
  root
4143
4350
  });
4144
4351
  } catch (error) {
4145
- log.warn(`Couldn't write ${DEV_VARS_EXAMPLE} \u2014 ${formatFailure(error)}.`);
4352
+ log2.warn(`Couldn't write ${ENV_EXAMPLE} \u2014 ${formatFailure(error)}.`);
4146
4353
  }
4147
4354
  printNextSteps(plan, result, devVarsPath, config.aliases);
4148
4355
  outro(
@@ -4155,7 +4362,7 @@ ${pc2.dim("Run `pnpm install` to fetch them.")}`
4155
4362
  return EXIT_OK;
4156
4363
  } catch (error) {
4157
4364
  if (partial) {
4158
- log.warn(formatRecovery(partial));
4365
+ log2.warn(formatRecovery(partial));
4159
4366
  }
4160
4367
  cancel(formatFailure(error));
4161
4368
  return exitCodeFor(error);
@@ -4165,25 +4372,25 @@ ${pc2.dim("Run `pnpm install` to fetch them.")}`
4165
4372
  }
4166
4373
 
4167
4374
  // src/commands/doctor.ts
4168
- import { cancel as cancel2, intro as intro2, log as log2, note as note2, outro as outro2 } from "@clack/prompts";
4169
- import { join as join12, relative as relative2 } from "path";
4375
+ import { cancel as cancel2, intro as intro2, log as log3, note as note2, outro as outro2 } from "@clack/prompts";
4376
+ import { join as join13, relative as relative2 } from "path";
4170
4377
  import pc3 from "picocolors";
4171
4378
 
4172
4379
  // src/lib/base.ts
4173
4380
  import { createHash as createHash2 } from "crypto";
4174
4381
  import { mkdtemp as mkdtemp2, readdir as readdir3, readFile as readFile12, rm as rm2 } from "fs/promises";
4175
4382
  import { tmpdir as tmpdir2 } from "os";
4176
- import { join as join10, posix as posix6 } from "path";
4383
+ import { join as join11, posix as posix5 } from "path";
4177
4384
 
4178
4385
  // src/lib/scaffold.ts
4179
4386
  import { mkdir as mkdir5, readdir as readdir2, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
4180
- import { join as join9, posix as posix5 } from "path";
4387
+ import { join as join10, posix as posix4 } from "path";
4181
4388
  import { fileURLToPath as fileURLToPath3 } from "url";
4182
4389
 
4183
4390
  // package.json
4184
4391
  var package_default = {
4185
4392
  name: "saasaloy",
4186
- version: "0.5.2",
4393
+ version: "0.6.0",
4187
4394
  description: "Composable SaaS accelerator kit.",
4188
4395
  keywords: [
4189
4396
  "saas",
@@ -4259,7 +4466,7 @@ var TEMPLATE_DIR_CANDIDATES = ["../templates/base", "../../templates/base"];
4259
4466
  async function baseTemplateDir() {
4260
4467
  for (const candidate of TEMPLATE_DIR_CANDIDATES) {
4261
4468
  const dir = fileURLToPath3(new URL(candidate, import.meta.url));
4262
- if (await pathExists(join9(dir, "package.json"))) {
4469
+ if (await pathExists(join10(dir, "package.json"))) {
4263
4470
  return dir;
4264
4471
  }
4265
4472
  }
@@ -4282,11 +4489,11 @@ async function copyTree(srcDir, destDir, vars, options, fromPrefix, targetPrefix
4282
4489
  if (isRoot && entry.name === BASE_DECLARATION) {
4283
4490
  continue;
4284
4491
  }
4285
- const srcPath = join9(srcDir, entry.name);
4492
+ const srcPath = join10(srcDir, entry.name);
4286
4493
  const outName = renderedName(entry.name);
4287
- const destPath = join9(destDir, options.keepNames ? entry.name : outName);
4288
- const from = posix5.join(fromPrefix, entry.name);
4289
- const target = posix5.join(targetPrefix, outName);
4494
+ const destPath = join10(destDir, options.keepNames ? entry.name : outName);
4495
+ const from = posix4.join(fromPrefix, entry.name);
4496
+ const target = posix4.join(targetPrefix, outName);
4290
4497
  if (entry.isDirectory()) {
4291
4498
  written.push(
4292
4499
  ...await copyTree(
@@ -4336,9 +4543,9 @@ function ownedMatcher(declaration) {
4336
4543
  return (target) => patterns.some((re) => re.test(target));
4337
4544
  }
4338
4545
  async function readBaseDeclaration(templateDir) {
4339
- const raw = await readIfPresent(join10(templateDir, BASE_DECLARATION));
4546
+ const raw = await readIfPresent(join11(templateDir, BASE_DECLARATION));
4340
4547
  if (raw === void 0) {
4341
- return { seedFiles: [], ownedFiles: [] };
4548
+ return { seedFiles: [], retiredSeedFiles: [], ownedFiles: [] };
4342
4549
  }
4343
4550
  const parsed = JSON.parse(raw);
4344
4551
  const seedFiles = parsed.seedFiles;
@@ -4347,6 +4554,14 @@ async function readBaseDeclaration(templateDir) {
4347
4554
  `${BASE_DECLARATION} must declare "seedFiles" as a list of project-relative paths.`
4348
4555
  );
4349
4556
  }
4557
+ const retiredSeedFiles = parsed.retiredSeedFiles ?? [];
4558
+ if (!Array.isArray(retiredSeedFiles) || !retiredSeedFiles.every(
4559
+ (entry) => typeof entry === "string" && entry !== ""
4560
+ )) {
4561
+ throw new Error(
4562
+ `${BASE_DECLARATION} must declare "retiredSeedFiles" as a list of project-relative paths.`
4563
+ );
4564
+ }
4350
4565
  const ownedFiles = parsed.ownedFiles ?? [];
4351
4566
  if (!Array.isArray(ownedFiles) || !ownedFiles.every((entry) => typeof entry === "string" && entry !== "")) {
4352
4567
  throw new Error(
@@ -4354,7 +4569,8 @@ async function readBaseDeclaration(templateDir) {
4354
4569
  );
4355
4570
  }
4356
4571
  return {
4357
- seedFiles,
4572
+ seedFiles: [...seedFiles, ...retiredSeedFiles],
4573
+ retiredSeedFiles,
4358
4574
  ownedFiles
4359
4575
  };
4360
4576
  }
@@ -4365,9 +4581,9 @@ async function listTemplateFiles(templateDir) {
4365
4581
  if (isRoot && entry.name === BASE_DECLARATION) {
4366
4582
  continue;
4367
4583
  }
4368
- const from = posix6.join(fromPrefix, entry.name);
4369
- const target = posix6.join(targetPrefix, renderedName(entry.name));
4370
- const abs = join10(dir, entry.name);
4584
+ const from = posix5.join(fromPrefix, entry.name);
4585
+ const target = posix5.join(targetPrefix, renderedName(entry.name));
4586
+ const abs = join11(dir, entry.name);
4371
4587
  if (entry.isDirectory()) {
4372
4588
  await walk(abs, from, target, false);
4373
4589
  } else if (entry.isFile()) {
@@ -4441,7 +4657,7 @@ async function missingBaseTargets(root, manifest, templateDir) {
4441
4657
  return missing.toSorted((a, b) => a < b ? -1 : 1);
4442
4658
  }
4443
4659
  async function renderTemplate(root, templateDir, projectName) {
4444
- const dir = await mkdtemp2(join10(tmpdir2(), "saasaloy-base-render-"));
4660
+ const dir = await mkdtemp2(join11(tmpdir2(), "saasaloy-base-render-"));
4445
4661
  const files = await copyTemplate(
4446
4662
  templateDir,
4447
4663
  dir,
@@ -4557,7 +4773,7 @@ async function baseUpdateInput(args) {
4557
4773
  import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
4558
4774
  import { parseModule as parseModule7 } from "magicast";
4559
4775
  import { readFile as readFile13 } from "fs/promises";
4560
- import { basename, dirname as dirname6, join as join11, posix as posix7, resolve as resolve3 } from "path";
4776
+ import { basename, dirname as dirname6, join as join12, posix as posix6, resolve as resolve3 } from "path";
4561
4777
  var SKILL_PREFIX = "saasaloy-";
4562
4778
  var PINNED_DEP = /^(@[^/@]+\/)?[^/@]+@\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
4563
4779
  function finding(module, where, message) {
@@ -4572,7 +4788,7 @@ function asRecord(value) {
4572
4788
  async function registryModuleNames(dir) {
4573
4789
  const names = [];
4574
4790
  for (const name of await readDirNames(dir)) {
4575
- if (await pathExists(join11(dir, name, "registry-item.json"))) {
4791
+ if (await pathExists(join12(dir, name, "registry-item.json"))) {
4576
4792
  names.push(name);
4577
4793
  }
4578
4794
  }
@@ -4581,12 +4797,12 @@ async function registryModuleNames(dir) {
4581
4797
  async function collectAliases(registryDir, names) {
4582
4798
  const templateDir = await baseTemplateDir();
4583
4799
  let base = {};
4584
- if (await pathExists(join11(templateDir, "saasaloy.json"))) {
4800
+ if (await pathExists(join12(templateDir, "saasaloy.json"))) {
4585
4801
  base = (await loadConfig(templateDir)).aliases;
4586
4802
  }
4587
4803
  const fromScaffolds = {};
4588
4804
  for (const name of names) {
4589
- const item = asRecord(await readDescriptor(join11(registryDir, name)));
4805
+ const item = asRecord(await readDescriptor(join12(registryDir, name)));
4590
4806
  for (const scaffold of asArray2(item.scaffolds)) {
4591
4807
  const aliases = asRecord(asRecord(scaffold).aliases);
4592
4808
  for (const [alias, prefix] of Object.entries(aliases)) {
@@ -4599,7 +4815,7 @@ async function collectAliases(registryDir, names) {
4599
4815
  return { base, fromScaffolds };
4600
4816
  }
4601
4817
  async function readDescriptor(dir) {
4602
- const file = join11(dir, "registry-item.json");
4818
+ const file = join12(dir, "registry-item.json");
4603
4819
  if (!await pathExists(file)) {
4604
4820
  return void 0;
4605
4821
  }
@@ -4614,7 +4830,7 @@ async function checkModule(input) {
4614
4830
  const module = basename(dir);
4615
4831
  const findings = [];
4616
4832
  const report = () => ({ dir, findings, module });
4617
- const file = join11(dir, "registry-item.json");
4833
+ const file = join12(dir, "registry-item.json");
4618
4834
  if (!await pathExists(file)) {
4619
4835
  findings.push(
4620
4836
  finding(module, "(root)", `no registry-item.json in ${module}/`)
@@ -4732,6 +4948,28 @@ async function checkModule(input) {
4732
4948
  );
4733
4949
  }
4734
4950
  }
4951
+ for (const key of Object.keys(asRecord(item.envServices))) {
4952
+ if (key !== "default" && !(key in asRecord(item.envVars))) {
4953
+ findings.push(
4954
+ finding(
4955
+ module,
4956
+ `/envServices/${key}`,
4957
+ "has no matching entry in envVars, so it routes nothing"
4958
+ )
4959
+ );
4960
+ }
4961
+ }
4962
+ for (const key of asArray2(item.envOptional)) {
4963
+ if (typeof key === "string" && !(key in asRecord(item.envVars))) {
4964
+ findings.push(
4965
+ finding(
4966
+ module,
4967
+ `/envOptional/${key}`,
4968
+ "has no matching entry in envVars, so nothing marks it blank on purpose"
4969
+ )
4970
+ );
4971
+ }
4972
+ }
4735
4973
  for (const key of Object.keys(asRecord(item.devVars))) {
4736
4974
  if (!(key in asRecord(item.envVars))) {
4737
4975
  findings.push(
@@ -4747,7 +4985,7 @@ async function checkModule(input) {
4747
4985
  if (typeof skill !== "string") {
4748
4986
  continue;
4749
4987
  }
4750
- const folder = posix7.basename(skill);
4988
+ const folder = posix6.basename(skill);
4751
4989
  if (!folder.startsWith(SKILL_PREFIX)) {
4752
4990
  findings.push(
4753
4991
  finding(
@@ -4981,7 +5219,7 @@ async function readPolicyState(root, installed) {
4981
5219
  const source = await readIfPresent(
4982
5220
  resolveWithinRoot(
4983
5221
  root,
4984
- posix7.join(posix7.dirname(KV_INDEX_FILE), `${specifier}.ts`)
5222
+ posix6.join(posix6.dirname(KV_INDEX_FILE), `${specifier}.ts`)
4985
5223
  )
4986
5224
  );
4987
5225
  if (source !== void 0) {
@@ -5008,7 +5246,7 @@ function relativeImportsIn(index) {
5008
5246
  }
5009
5247
  async function resolveDoctorTarget(path) {
5010
5248
  const dir = resolve3(path);
5011
- if (await pathExists(join11(dir, "registry-item.json"))) {
5249
+ if (await pathExists(join12(dir, "registry-item.json"))) {
5012
5250
  return { names: [basename(dir)], registryDir: dirname6(dir) };
5013
5251
  }
5014
5252
  return { names: await registryModuleNames(dir), registryDir: dir };
@@ -5030,6 +5268,54 @@ function checkPartialInstalls(state) {
5030
5268
  )
5031
5269
  );
5032
5270
  }
5271
+ var LEGACY_ENV_FILES = [".dev.vars", ".dev.vars.example"];
5272
+ function checkEnv(args) {
5273
+ const findings = args.legacy.toSorted().map(
5274
+ (path) => finding(
5275
+ "env",
5276
+ `/${path}`,
5277
+ `${path} still exists, and nothing reads it. wrangler loads .env only while no .dev.vars sits beside it, so every value pnpm env:setup writes is ignored. Run \`pnpm env:setup\` to carry its values into .env and delete it.`
5278
+ )
5279
+ );
5280
+ if (args.legacy.length > 0 && !args.hasKeyList) {
5281
+ findings.push(
5282
+ finding(
5283
+ "env",
5284
+ `/${ENV_EXAMPLE}`,
5285
+ `${ENV_EXAMPLE} is missing, so there is no key list to migrate those values into. Re-run \`saasaloy add\` for an installed module to regenerate it.`
5286
+ )
5287
+ );
5288
+ }
5289
+ return findings;
5290
+ }
5291
+ var INFRA_RUNBOOK = "https://github.com/mimukit/saasaloy/blob/main/docs/wiki/runbooks/move-off-the-infra-module.md";
5292
+ function checkRetiredInfra(hasInfra) {
5293
+ return hasInfra ? [
5294
+ finding(
5295
+ "deploy",
5296
+ "/infra",
5297
+ `infra/ is the Pulumi deploy that pnpm deploy:production replaced, and nothing runs it any more. Never run pulumi destroy: it deletes the live Workers and their data. Follow ${INFRA_RUNBOOK} to move the resource ids into wrangler.jsonc, then delete infra/.`
5298
+ )
5299
+ ] : [];
5300
+ }
5301
+ function hasRetiredInfra(root) {
5302
+ return pathExists(resolveWithinRoot(root, "infra"));
5303
+ }
5304
+ async function readEnvState(root) {
5305
+ const legacy = [];
5306
+ for (const workspace of ["apps/api", "apps/web", "apps/admin"]) {
5307
+ for (const name of LEGACY_ENV_FILES) {
5308
+ const path = posix6.join(workspace, name);
5309
+ if (await pathExists(resolveWithinRoot(root, path))) {
5310
+ legacy.push(path);
5311
+ }
5312
+ }
5313
+ }
5314
+ return {
5315
+ legacy,
5316
+ hasKeyList: await pathExists(resolveWithinRoot(root, ENV_EXAMPLE))
5317
+ };
5318
+ }
5033
5319
  async function checkTarget(target) {
5034
5320
  const siblings = await registryModuleNames(target.registryDir);
5035
5321
  const aliases = await collectAliases(target.registryDir, siblings);
@@ -5038,7 +5324,7 @@ async function checkTarget(target) {
5038
5324
  reports.push(
5039
5325
  await checkModule({
5040
5326
  aliases,
5041
- dir: join11(target.registryDir, name),
5327
+ dir: join12(target.registryDir, name),
5042
5328
  siblings
5043
5329
  })
5044
5330
  );
@@ -5152,10 +5438,13 @@ async function reportProject(path) {
5152
5438
  });
5153
5439
  note2(wrapForNote(renderBase(base)), "Base");
5154
5440
  const policyState = await readPolicyState(path, config.installed);
5441
+ const envState = await readEnvState(path);
5155
5442
  const findings = [
5156
5443
  ...checkProject({ config, manifest }),
5157
5444
  ...checkPartialInstalls({ installed: config.installed, manifest }),
5158
- ...policyState ? checkPolicyBindings(policyState) : []
5445
+ ...policyState ? checkPolicyBindings(policyState) : [],
5446
+ ...checkEnv(envState),
5447
+ ...checkRetiredInfra(await hasRetiredInfra(path))
5159
5448
  ];
5160
5449
  if (findings.length === 0) {
5161
5450
  const count = config.installed.length;
@@ -5200,7 +5489,7 @@ async function runDoctor(argv) {
5200
5489
  );
5201
5490
  return EXIT_REFUSED;
5202
5491
  }
5203
- if (await pathExists(join12(path, CONFIG_FILE))) {
5492
+ if (await pathExists(join13(path, CONFIG_FILE))) {
5204
5493
  return await reportProject(path);
5205
5494
  }
5206
5495
  const target = await resolveDoctorTarget(path);
@@ -5230,7 +5519,7 @@ async function runDoctor(argv) {
5230
5519
  `${pc3.yellow(report.module)} ${pc3.dim(`(${report.findings.length})`)}`
5231
5520
  );
5232
5521
  }
5233
- log2.info(
5522
+ log3.info(
5234
5523
  pc3.dim(
5235
5524
  `Checked ${reports.length} module${reports.length === 1 ? "" : "s"}; ${reports.length - bad.length} clean.`
5236
5525
  )
@@ -5252,118 +5541,29 @@ import {
5252
5541
  cancel as cancel3,
5253
5542
  intro as intro3,
5254
5543
  isCancel as isCancel2,
5255
- log as log3,
5544
+ log as log4,
5256
5545
  note as note3,
5257
5546
  outro as outro3,
5258
- select as select2,
5259
5547
  text
5260
5548
  } from "@clack/prompts";
5261
5549
  import pc4 from "picocolors";
5262
5550
 
5263
5551
  // src/lib/env-vars.ts
5264
- import { posix as posix8 } from "path";
5265
- var ENV_FILE = ".env";
5266
- var DEV_VARS_FILE = ".dev.vars";
5267
- var PUBLIC_PREFIX = "PUBLIC_";
5268
- var APPS_DIR = "apps/";
5269
- var WRANGLER_CONFIGS = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
5270
- function isPublicVar(name) {
5271
- return name.startsWith(PUBLIC_PREFIX);
5272
- }
5273
- function targetFileName(name) {
5274
- return isPublicVar(name) ? ENV_FILE : DEV_VARS_FILE;
5275
- }
5276
- function resolved(workspace) {
5277
- return workspace ? { kind: "resolved", workspace } : void 0;
5278
- }
5279
- function routeVariable(args) {
5280
- const { candidates, wranglerWorkspaces } = args;
5281
- if (isPublicVar(args.name)) {
5282
- const apps = candidates.filter(
5283
- (w) => w.startsWith(APPS_DIR) && w !== args.apiWorkspace
5284
- );
5285
- if (apps.length === 1) {
5286
- return { kind: "resolved", workspace: apps[0] };
5287
- }
5288
- if (apps.length > 1) {
5289
- return { choices: apps.toSorted(), kind: "ambiguous" };
5290
- }
5291
- return resolved(args.baseWorkspace) ?? fromCandidatesAlone(candidates);
5292
- }
5293
- const wrangler = candidates.filter((w) => wranglerWorkspaces.includes(w));
5294
- if (wrangler.length === 1) {
5295
- return { kind: "resolved", workspace: wrangler[0] };
5296
- }
5297
- if (wrangler.length > 1) {
5298
- return (args.apiWorkspace && wrangler.includes(args.apiWorkspace) ? resolved(args.apiWorkspace) : void 0) ?? { choices: wrangler.toSorted(), kind: "ambiguous" };
5299
- }
5300
- return resolved(args.apiWorkspace) ?? fromCandidatesAlone(candidates);
5301
- }
5302
- function fromCandidatesAlone(candidates) {
5303
- if (candidates.length === 1) {
5304
- return { kind: "resolved", workspace: candidates[0] };
5305
- }
5306
- return candidates.length === 0 ? { kind: "unknown" } : { choices: candidates.toSorted(), kind: "ambiguous" };
5307
- }
5308
- async function discoverWorkspaces(root, aliases) {
5309
- const found = /* @__PURE__ */ new Set();
5310
- for (const value of Object.values(aliases)) {
5311
- const workspace = await walkUpToWorkspace(root, value);
5312
- if (workspace) {
5313
- found.add(workspace);
5314
- }
5315
- }
5316
- return [...found].toSorted();
5317
- }
5318
- async function walkUpToWorkspace(root, relPosixPath) {
5319
- let current = relPosixPath.replace(/\/+$/, "");
5320
- while (current && current !== "." && current !== "/") {
5321
- if (await pathExists(resolveWithinRoot(root, `${current}/package.json`))) {
5322
- return current;
5323
- }
5324
- const parent = posix8.dirname(current);
5325
- if (parent === current) {
5326
- return void 0;
5327
- }
5328
- current = parent;
5329
- }
5330
- return void 0;
5331
- }
5332
- async function findWranglerWorkspaces(root, workspaces) {
5333
- const found = [];
5334
- for (const workspace of workspaces) {
5335
- for (const config of WRANGLER_CONFIGS) {
5336
- if (await pathExists(resolveWithinRoot(root, `${workspace}/${config}`))) {
5337
- found.push(workspace);
5338
- break;
5339
- }
5340
- }
5341
- }
5342
- return found;
5343
- }
5344
- function workspacesByModule(manifest, workspaces) {
5345
- const byModule = /* @__PURE__ */ new Map();
5346
- for (const [path, entry] of Object.entries(manifest.managed)) {
5347
- const workspace = workspaceForPath(path, workspaces);
5348
- if (!workspace) {
5552
+ var LOCAL_VALUES = "packages/env/.env";
5553
+ function parseEnvValues(text4) {
5554
+ const values = {};
5555
+ for (const line of text4.split("\n")) {
5556
+ const trimmed = line.trim();
5557
+ if (!trimmed || trimmed.startsWith("#")) {
5349
5558
  continue;
5350
5559
  }
5351
- const set = byModule.get(entry.module) ?? /* @__PURE__ */ new Set();
5352
- set.add(workspace);
5353
- byModule.set(entry.module, set);
5354
- }
5355
- return new Map(
5356
- [...byModule].map(([module, set]) => [module, [...set].toSorted()])
5357
- );
5358
- }
5359
- function workspaceForPath(path, workspaces) {
5360
- let best;
5361
- for (const workspace of workspaces) {
5362
- if (path.startsWith(`${workspace}/`) && (best === void 0 || workspace.length > best.length)) {
5363
- best = workspace;
5560
+ const eq = trimmed.indexOf("=");
5561
+ if (eq <= 0) {
5562
+ continue;
5364
5563
  }
5564
+ values[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1);
5365
5565
  }
5366
- return best;
5566
+ return values;
5367
5567
  }
5368
5568
  function isSet(values, name) {
5369
5569
  return (values[name] ?? "").trim() !== "";
@@ -5399,27 +5599,20 @@ function appendVars(existing, additions) {
5399
5599
  return appended.length === 0 ? head : `${head}${appended.join("\n")}
5400
5600
  `;
5401
5601
  }
5402
- function productionSecretCommands(targets) {
5403
- const byWorkspace = /* @__PURE__ */ new Map();
5404
- for (const { name, workspace } of targets) {
5405
- if (isPublicVar(name)) {
5406
- continue;
5407
- }
5408
- byWorkspace.set(workspace, [...byWorkspace.get(workspace) ?? [], name]);
5409
- }
5410
- const lines = [];
5411
- for (const workspace of [...byWorkspace.keys()].toSorted()) {
5412
- lines.push(`# from ${workspace}`);
5413
- for (const name of (byWorkspace.get(workspace) ?? []).toSorted()) {
5414
- lines.push(`wrangler secret put ${name}`);
5415
- }
5602
+ function productionSecretCommands(names) {
5603
+ const secrets = names.filter((name) => !isPublicVar(name)).toSorted();
5604
+ if (secrets.length === 0) {
5605
+ return [];
5416
5606
  }
5417
- return lines;
5607
+ return [
5608
+ "# from the Worker's workspace, e.g. apps/api",
5609
+ ...secrets.map((name) => `wrangler secret put ${name}`)
5610
+ ];
5418
5611
  }
5419
5612
 
5420
5613
  // src/lib/gitignore.ts
5421
5614
  import { readFile as readFile14 } from "fs/promises";
5422
- import { dirname as dirname7, join as join13, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
5615
+ import { dirname as dirname7, join as join14, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
5423
5616
  var REGEX_SPECIAL = /[.+^${}()|[\]\\]/g;
5424
5617
  function globToRegex(glob) {
5425
5618
  let out = "";
@@ -5503,7 +5696,7 @@ function excludes(layers, candidate, isDir) {
5503
5696
  async function findRepoRoot(start) {
5504
5697
  let current = resolve4(start);
5505
5698
  for (; ; ) {
5506
- if (await pathExists(join13(current, ".git"))) {
5699
+ if (await pathExists(join14(current, ".git"))) {
5507
5700
  return current;
5508
5701
  }
5509
5702
  const parent = dirname7(current);
@@ -5520,7 +5713,7 @@ async function isPathIgnored(root, relPosixPath) {
5520
5713
  }
5521
5714
  const fromRepo = relative3(
5522
5715
  repoRoot,
5523
- join13(resolve4(root), ...relPosixPath.split("/"))
5716
+ join14(resolve4(root), ...relPosixPath.split("/"))
5524
5717
  );
5525
5718
  const segments = fromRepo.split(sep2).filter(Boolean);
5526
5719
  if (segments.length === 0 || segments.includes("..")) {
@@ -5529,7 +5722,7 @@ async function isPathIgnored(root, relPosixPath) {
5529
5722
  const layers = [];
5530
5723
  for (let depth = 0; depth < segments.length; depth += 1) {
5531
5724
  const parts = segments.slice(0, depth);
5532
- const file = join13(repoRoot, ...parts, ".gitignore");
5725
+ const file = join14(repoRoot, ...parts, ".gitignore");
5533
5726
  if (await pathExists(file)) {
5534
5727
  layers.push({
5535
5728
  base: parts.join("/"),
@@ -5567,16 +5760,9 @@ function parseArgs3(argv) {
5567
5760
  return { check: argv.includes("--check"), unknown };
5568
5761
  }
5569
5762
  function renderPending(pending) {
5570
- return pending.map((entry) => {
5571
- const head = `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2014 declared by ${entry.declaration.module}`)}`;
5572
- if (entry.file) {
5573
- return `${head}
5574
- ${pc4.dim(`\u2192 ${entry.file}`)}`;
5575
- }
5576
- const why = entry.choices ? `several workspaces fit (${entry.choices.join(", ")}) \u2014 run \`saasaloy env\` to pick one` : "no target workspace found \u2014 this module wrote no files under a known alias";
5577
- return `${head}
5578
- ${pc4.yellow(`\u2192 ${why}`)}`;
5579
- });
5763
+ return pending.map(
5764
+ (entry) => `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2014 declared by ${entry.declaration.module}`)}`
5765
+ );
5580
5766
  }
5581
5767
  async function loadDeclarations(config, lock, sources, onSkip) {
5582
5768
  const override = process.env[REGISTRY_ENV];
@@ -5645,31 +5831,15 @@ async function loadDeclarations(config, lock, sources, onSkip) {
5645
5831
  return true;
5646
5832
  });
5647
5833
  }
5648
- function workspaceForAlias(aliases, alias, workspaces) {
5649
- const value = aliases[alias];
5650
- if (!value) {
5651
- return void 0;
5652
- }
5653
- return workspaceForPath(
5654
- value.endsWith("/") ? value : `${value}/`,
5655
- workspaces
5656
- );
5657
- }
5658
- async function writeAnswers(root, answers, contents) {
5659
- const written = [];
5660
- for (const [file, additions] of [...answers].toSorted(
5661
- ([a], [b]) => a.localeCompare(b)
5662
- )) {
5663
- if (additions.length === 0) {
5664
- continue;
5665
- }
5666
- const abs = resolveWithinRoot(root, file);
5667
- await assertNoSymlinkPath(root, abs);
5668
- await mkdir6(dirname8(abs), { recursive: true });
5669
- await writeFile8(abs, appendVars(contents.get(file), additions), "utf-8");
5670
- written.push([file, additions.length]);
5834
+ async function writeAnswers(root, additions, before) {
5835
+ if (additions.length === 0) {
5836
+ return 0;
5671
5837
  }
5672
- return written;
5838
+ const abs = resolveWithinRoot(root, LOCAL_VALUES);
5839
+ await assertNoSymlinkPath(root, abs);
5840
+ await mkdir6(dirname8(abs), { recursive: true });
5841
+ await writeFile8(abs, appendVars(before, additions), "utf-8");
5842
+ return additions.length;
5673
5843
  }
5674
5844
  async function readTarget(root, file) {
5675
5845
  const abs = resolveWithinRoot(root, file);
@@ -5693,62 +5863,34 @@ async function runEnv(argv) {
5693
5863
  try {
5694
5864
  const root = await findProjectRoot();
5695
5865
  const config = await loadConfig(root);
5696
- const manifest = await loadManifest(root);
5697
5866
  const lock = await loadLock(root);
5698
5867
  const declarations = await loadDeclarations(
5699
5868
  config,
5700
5869
  lock,
5701
5870
  sources,
5702
5871
  (msg) => {
5703
- log3.warn(msg);
5872
+ log4.warn(msg);
5704
5873
  }
5705
5874
  );
5706
5875
  if (declarations.length === 0) {
5707
5876
  outro3(pc4.dim("No module declares an environment variable."));
5708
5877
  return EXIT_OK;
5709
5878
  }
5710
- const workspaces = await discoverWorkspaces(root, config.aliases);
5711
- const wranglerWorkspaces = await findWranglerWorkspaces(root, workspaces);
5712
- const byModule = workspacesByModule(manifest, workspaces);
5713
- const api = apiWorkspace(config.aliases);
5714
- const base = workspaceForAlias(
5715
- config.aliases,
5716
- `@${config.base ?? "web"}`,
5717
- workspaces
5718
- );
5719
- const contents = /* @__PURE__ */ new Map();
5720
- const readOnce = async (file) => {
5721
- if (!contents.has(file)) {
5722
- contents.set(file, await readTarget(root, file));
5723
- }
5724
- return contents.get(file);
5725
- };
5726
- const pending = [];
5727
- const routed = [];
5728
- for (const declaration of declarations) {
5729
- const route = routeVariable({
5730
- candidates: byModule.get(declaration.module) ?? [],
5731
- name: declaration.name,
5732
- wranglerWorkspaces,
5733
- ...api ? { apiWorkspace: api } : {},
5734
- ...base ? { baseWorkspace: base } : {}
5735
- });
5736
- if (route.kind === "resolved") {
5737
- const file = `${route.workspace}/${targetFileName(declaration.name)}`;
5738
- routed.push({ name: declaration.name, workspace: route.workspace });
5739
- const values = parseDevVars(await readOnce(file) ?? "");
5740
- if (isSet(values, declaration.name)) {
5741
- continue;
5742
- }
5743
- pending.push({ declaration, file, workspace: route.workspace });
5744
- continue;
5745
- }
5746
- pending.push({
5747
- declaration,
5748
- ...route.kind === "ambiguous" ? { choices: route.choices } : {}
5749
- });
5879
+ const listed = new Set(
5880
+ Object.keys(parseEnvExample(await readTarget(root, ENV_EXAMPLE) ?? ""))
5881
+ );
5882
+ const undeclared = declarations.filter((declaration) => !listed.has(declaration.name)).map((declaration) => declaration.name);
5883
+ if (undeclared.length > 0) {
5884
+ log4.warn(
5885
+ `${ENV_EXAMPLE} does not list ${undeclared.join(", ")}, so no service reads ${undeclared.length === 1 ? "it" : "them"}. Re-run \`saasaloy add\` for the declaring module to regenerate the key list.`
5886
+ );
5750
5887
  }
5751
- const secretCommands = productionSecretCommands(routed);
5888
+ const before = await readTarget(root, LOCAL_VALUES);
5889
+ const values = parseEnvValues(before ?? "");
5890
+ const pending = declarations.filter((declaration) => !isSet(values, declaration.name)).map((declaration) => ({ declaration }));
5891
+ const secretCommands = productionSecretCommands(
5892
+ declarations.map((declaration) => declaration.name)
5893
+ );
5752
5894
  const printSecrets = () => {
5753
5895
  if (secretCommands.length > 0) {
5754
5896
  note3(
@@ -5766,16 +5908,11 @@ ${pc4.dim("Printed, never run \u2014 copy these when you deploy.")}`
5766
5908
  outro3(pc4.green("Every declared variable is set."));
5767
5909
  return EXIT_OK;
5768
5910
  }
5769
- const files = [
5770
- ...new Set(pending.flatMap((entry) => entry.file ? [entry.file] : []))
5771
- ].toSorted();
5772
- for (const file of files) {
5773
- if (!await isPathIgnored(root, file)) {
5774
- cancel3(
5775
- `${pc4.cyan(file)} isn't gitignored \u2014 refusing to write a secret into a tracked file. Add it to .gitignore (the base template already does) and run again.`
5776
- );
5777
- return EXIT_REFUSED;
5778
- }
5911
+ if (!await isPathIgnored(root, LOCAL_VALUES)) {
5912
+ cancel3(
5913
+ `${pc4.cyan(LOCAL_VALUES)} isn't gitignored \u2014 refusing to write a secret into a tracked file. Add it to .gitignore (the base template already does) and run again.`
5914
+ );
5915
+ return EXIT_REFUSED;
5779
5916
  }
5780
5917
  if (opts.check || !isInteractive()) {
5781
5918
  note3(wrapForNote(renderPending(pending).join("\n")), "Missing");
@@ -5786,42 +5923,11 @@ ${pc4.dim("Printed, never run \u2014 copy these when you deploy.")}`
5786
5923
  );
5787
5924
  return EXIT_REFUSED;
5788
5925
  }
5789
- const answers = /* @__PURE__ */ new Map();
5926
+ const answers = [];
5790
5927
  for (const entry of pending) {
5791
- let file = entry.file;
5792
- if (!file) {
5793
- if (!entry.choices) {
5794
- log3.warn(
5795
- `${pc4.cyan(entry.declaration.name)} \u2014 no target workspace found; set it by hand.`
5796
- );
5797
- continue;
5798
- }
5799
- const picked = await select2({
5800
- message: `Which workspace reads ${entry.declaration.name}?`,
5801
- options: entry.choices.map((workspace) => ({
5802
- label: workspace,
5803
- value: workspace
5804
- }))
5805
- });
5806
- if (isCancel2(picked)) {
5807
- cancel3("Cancelled \u2014 nothing written.");
5808
- return EXIT_REFUSED;
5809
- }
5810
- file = `${picked}/${targetFileName(entry.declaration.name)}`;
5811
- if (!await isPathIgnored(root, file)) {
5812
- cancel3(
5813
- `${pc4.cyan(file)} isn't gitignored \u2014 refusing to write a secret into a tracked file.`
5814
- );
5815
- return EXIT_REFUSED;
5816
- }
5817
- const values = parseDevVars(await readOnce(file) ?? "");
5818
- if (isSet(values, entry.declaration.name)) {
5819
- continue;
5820
- }
5821
- }
5822
5928
  const answer = await text({
5823
5929
  defaultValue: entry.declaration.devValue ?? "",
5824
- message: `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2192 ${file}`)}
5930
+ message: `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2192 ${LOCAL_VALUES}`)}
5825
5931
  ${entry.declaration.description}`,
5826
5932
  ...entry.declaration.devValue === void 0 ? {} : { initialValue: entry.declaration.devValue }
5827
5933
  });
@@ -5829,18 +5935,15 @@ ${entry.declaration.description}`,
5829
5935
  cancel3("Cancelled \u2014 nothing written.");
5830
5936
  return EXIT_REFUSED;
5831
5937
  }
5832
- answers.set(file, [
5833
- ...answers.get(file) ?? [],
5834
- [entry.declaration.name, answer]
5835
- ]);
5938
+ answers.push([entry.declaration.name, answer]);
5836
5939
  }
5837
- const written = await writeAnswers(root, answers, contents);
5838
- if (written.length > 0) {
5940
+ const written = await writeAnswers(root, answers, before);
5941
+ if (written > 0) {
5839
5942
  note3(
5840
5943
  wrapForNote(
5841
- written.map(
5842
- ([file, count]) => `${pc4.cyan(file)} ${pc4.dim(`\u2014 ${count} added`)}`
5843
- ).join("\n")
5944
+ `${pc4.cyan(LOCAL_VALUES)} ${pc4.dim(`\u2014 ${written} added`)}
5945
+
5946
+ ${pc4.dim("Run `pnpm env:setup` to write each service's own .env from it.")}`
5844
5947
  ),
5845
5948
  "Written"
5846
5949
  );
@@ -5859,39 +5962,39 @@ ${entry.declaration.description}`,
5859
5962
  }
5860
5963
 
5861
5964
  // src/commands/init.ts
5862
- import { spawn } from "child_process";
5965
+ import { spawn as spawn2 } from "child_process";
5863
5966
  import { readdir as readdir4, readFile as readFile16 } from "fs/promises";
5864
- import { basename as basename2, join as join14, resolve as resolve6 } from "path";
5967
+ import { basename as basename2, join as join15, resolve as resolve6 } from "path";
5865
5968
  import {
5866
5969
  cancel as cancel4,
5867
5970
  intro as intro4,
5868
5971
  isCancel as isCancel3,
5869
5972
  note as note4,
5870
5973
  outro as outro4,
5871
- select as select3,
5974
+ select as select2,
5872
5975
  spinner,
5873
5976
  text as text2
5874
5977
  } from "@clack/prompts";
5875
5978
  import pc6 from "picocolors";
5876
5979
 
5877
5980
  // src/lib/logger.ts
5878
- import { log as log4 } from "@clack/prompts";
5981
+ import { log as log5 } from "@clack/prompts";
5879
5982
  import pc5 from "picocolors";
5880
5983
  var logger = {
5881
5984
  error(message) {
5882
- log4.error(message);
5985
+ log5.error(message);
5883
5986
  },
5884
5987
  info(message) {
5885
- log4.info(message);
5988
+ log5.info(message);
5886
5989
  },
5887
5990
  step(message) {
5888
- log4.step(pc5.dim(message));
5991
+ log5.step(pc5.dim(message));
5889
5992
  },
5890
5993
  success(message) {
5891
- log4.success(message);
5994
+ log5.success(message);
5892
5995
  },
5893
5996
  warn(message) {
5894
- log4.warn(message);
5997
+ log5.warn(message);
5895
5998
  }
5896
5999
  };
5897
6000
 
@@ -5938,7 +6041,7 @@ function parseArgs4(argv) {
5938
6041
  }
5939
6042
  function run(command, args, cwd) {
5940
6043
  return new Promise((resolvePromise) => {
5941
- const child = spawn(command, args, {
6044
+ const child = spawn2(command, args, {
5942
6045
  cwd,
5943
6046
  stdio: ["ignore", "pipe", "pipe"],
5944
6047
  shell: process.platform === "win32"
@@ -5973,7 +6076,10 @@ function runPnpmInstall(cwd) {
5973
6076
  async function initGitRepo(cwd) {
5974
6077
  const inside = await run("git", ["rev-parse", "--is-inside-work-tree"], cwd);
5975
6078
  if (inside.ok) {
5976
- return { created: false };
6079
+ const ignored = await run("git", ["check-ignore", "--quiet", "."], cwd);
6080
+ if (!ignored.ok) {
6081
+ return { created: false };
6082
+ }
5977
6083
  }
5978
6084
  const result = await run("git", ["init"], cwd);
5979
6085
  if (!result.ok) {
@@ -5994,7 +6100,7 @@ function bySkillOrder(a, b) {
5994
6100
  }
5995
6101
  async function linkAgentSkills(target) {
5996
6102
  const result = { linked: [], conflicts: [], failures: [] };
5997
- const skillsDir = join14(target, ".agents", "skills");
6103
+ const skillsDir = join15(target, ".agents", "skills");
5998
6104
  let names;
5999
6105
  try {
6000
6106
  names = await readDirNames(skillsDir);
@@ -6003,8 +6109,8 @@ async function linkAgentSkills(target) {
6003
6109
  return result;
6004
6110
  }
6005
6111
  for (const name of names.toSorted(bySkillOrder)) {
6006
- const linkAbs = join14(target, ".claude", "skills", name);
6007
- const targetAbs = join14(skillsDir, name);
6112
+ const linkAbs = join15(target, ".claude", "skills", name);
6113
+ const targetAbs = join15(skillsDir, name);
6008
6114
  let state;
6009
6115
  try {
6010
6116
  state = await classifyLink(linkAbs, targetAbs);
@@ -6162,7 +6268,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
6162
6268
  );
6163
6269
  }
6164
6270
  let installed = false;
6165
- const wantsInstall = noInstall ? false : await select3({
6271
+ const wantsInstall = noInstall ? false : await select2({
6166
6272
  message: "Install dependencies now?",
6167
6273
  options: [
6168
6274
  { value: true, label: `Yes, run ${pc6.cyan("pnpm install")}` },
@@ -6197,7 +6303,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
6197
6303
  // owner who never opens AGENTS.md would otherwise not know they exist.
6198
6304
  // Pad to the same column the commands above comment at (`pnpm dev` + 21 spaces).
6199
6305
  ...skills.linked.map(
6200
- (name) => `${`/${name}`.padEnd(29)}${pc6.dim("# in Claude Code")}`
6306
+ (name) => `${`/${name}`.padEnd(29)}${pc6.dim("# in your AI coding tool")}`
6201
6307
  )
6202
6308
  ].filter((line) => line !== null).map((line) => pc6.cyan(line)).join("\n");
6203
6309
  note4(steps, "Next steps");
@@ -6207,7 +6313,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
6207
6313
 
6208
6314
  // src/commands/list.ts
6209
6315
  import { cancel as cancel5, intro as intro5, note as note5, outro as outro5 } from "@clack/prompts";
6210
- import { join as join15 } from "path";
6316
+ import { join as join16 } from "path";
6211
6317
  import pc7 from "picocolors";
6212
6318
  var KNOWN_FLAGS5 = /* @__PURE__ */ new Set(["--installed", "--available", "--help", "-h"]);
6213
6319
  var USAGE5 = "saasaloy list [<owner/repo[@ref]>] [--installed] [--available]";
@@ -6240,7 +6346,7 @@ function parseArgs5(argv) {
6240
6346
  }
6241
6347
  async function localProject() {
6242
6348
  const root = await findProjectRoot();
6243
- if (!await pathExists(join15(root, CONFIG_FILE))) {
6349
+ if (!await pathExists(join16(root, CONFIG_FILE))) {
6244
6350
  return { installed: /* @__PURE__ */ new Set() };
6245
6351
  }
6246
6352
  const config = await loadConfig(root);
@@ -6340,21 +6446,21 @@ async function runList(argv) {
6340
6446
  }
6341
6447
 
6342
6448
  // src/commands/new.ts
6343
- import { join as join17, relative as relative4, resolve as resolve7 } from "path";
6449
+ import { join as join18, relative as relative4, resolve as resolve7 } from "path";
6344
6450
  import {
6345
6451
  cancel as cancel6,
6346
6452
  intro as intro6,
6347
6453
  isCancel as isCancel4,
6348
6454
  note as note6,
6349
6455
  outro as outro6,
6350
- select as select4,
6456
+ select as select3,
6351
6457
  text as text3
6352
6458
  } from "@clack/prompts";
6353
6459
  import pc8 from "picocolors";
6354
6460
 
6355
6461
  // src/lib/new-module.ts
6356
6462
  import { mkdir as mkdir7, writeFile as writeFile9 } from "fs/promises";
6357
- import { dirname as dirname9, join as join16 } from "path";
6463
+ import { dirname as dirname9, join as join17 } from "path";
6358
6464
  var MODULE_TYPES = [
6359
6465
  "saasaloy:capability",
6360
6466
  "saasaloy:feature"
@@ -6458,7 +6564,7 @@ function moduleFiles(spec) {
6458
6564
  async function writeModule(dir, spec) {
6459
6565
  const files = moduleFiles(spec);
6460
6566
  for (const file of files) {
6461
- const abs = join16(dir, ...file.path.split("/"));
6567
+ const abs = join17(dir, ...file.path.split("/"));
6462
6568
  await mkdir7(dirname9(abs), { recursive: true });
6463
6569
  await writeFile9(abs, file.content, "utf-8");
6464
6570
  }
@@ -6546,7 +6652,7 @@ async function runNew(argv) {
6546
6652
  }
6547
6653
  try {
6548
6654
  const projectRoot = await findProjectRoot();
6549
- if (await pathExists(join17(projectRoot, CONFIG_FILE))) {
6655
+ if (await pathExists(join18(projectRoot, CONFIG_FILE))) {
6550
6656
  cancel6(
6551
6657
  `${CONFIG_FILE} is present at ${pc8.cyan(projectRoot)} \u2014 this is a generated project, and \`new module\` authors a module for a registry. Run it in the registry repo instead. Nothing was written.`
6552
6658
  );
@@ -6574,7 +6680,7 @@ async function runNew(argv) {
6574
6680
  return EXIT_REFUSED;
6575
6681
  }
6576
6682
  const registryDir = resolve7(process.cwd(), REGISTRY_DIR);
6577
- const dir = join17(registryDir, name);
6683
+ const dir = join18(registryDir, name);
6578
6684
  if (await pathExists(dir)) {
6579
6685
  cancel6(
6580
6686
  `${pc8.cyan(`${REGISTRY_DIR}/${name}`)} already exists \u2014 pick another name, or edit it by hand. Nothing was written.`
@@ -6591,7 +6697,7 @@ async function runNew(argv) {
6591
6697
  }
6592
6698
  type = opts.type;
6593
6699
  } else if (isInteractive()) {
6594
- const picked = await select4({
6700
+ const picked = await select3({
6595
6701
  message: "Which tier is this?",
6596
6702
  options: MODULE_TYPES.map((value) => ({
6597
6703
  hint: TYPE_HINTS[value],
@@ -6664,16 +6770,16 @@ async function runNew(argv) {
6664
6770
  }
6665
6771
 
6666
6772
  // src/commands/outdated.ts
6667
- import { cancel as cancel7, intro as intro7, log as log5, note as note7, outro as outro7 } from "@clack/prompts";
6773
+ import { cancel as cancel7, intro as intro7, log as log6, note as note7, outro as outro7 } from "@clack/prompts";
6668
6774
  import pc9 from "picocolors";
6669
6775
 
6670
6776
  // src/lib/updater.ts
6671
6777
  import { mkdir as mkdir8, readFile as readFile18, rm as rmPath2, writeFile as writeFile11 } from "fs/promises";
6672
- import { dirname as dirname11, posix as posix9 } from "path";
6778
+ import { dirname as dirname11, posix as posix7 } from "path";
6673
6779
 
6674
6780
  // src/lib/remover.ts
6675
6781
  import { readdir as readdir5, readFile as readFile17, rm as rmPath, writeFile as writeFile10 } from "fs/promises";
6676
- import { dirname as dirname10, join as join18, relative as relative5, sep as sep3 } from "path";
6782
+ import { dirname as dirname10, join as join19, relative as relative5, sep as sep3 } from "path";
6677
6783
  function classifyTrackedFile(content, manifestHash) {
6678
6784
  if (content === void 0) {
6679
6785
  return "missing";
@@ -6817,7 +6923,7 @@ async function pruneEmptyDirs(root, deletedAbsPaths) {
6817
6923
  async function dropDanglingAliases(root, config) {
6818
6924
  const dropped = [];
6819
6925
  for (const [alias, prefix] of Object.entries(config.aliases)) {
6820
- const prefixAbs = join18(root, ...prefix.split("/"));
6926
+ const prefixAbs = join19(root, ...prefix.split("/"));
6821
6927
  if (!await pathExists(prefixAbs)) {
6822
6928
  delete config.aliases[alias];
6823
6929
  dropped.push(alias);
@@ -7354,9 +7460,9 @@ function classifyUpdate(base, theirs, mine, managedHash, adopted, projectOwned)
7354
7460
  async function planLinks(root, theirs) {
7355
7461
  const links = [];
7356
7462
  for (const skillRel of theirs.item.agent?.skills ?? []) {
7357
- const folderName = posix9.basename(skillRel);
7358
- const path = posix9.join(".claude/skills", folderName);
7359
- const target = posix9.join(".agents/skills", folderName);
7463
+ const folderName = posix7.basename(skillRel);
7464
+ const path = posix7.join(".claude/skills", folderName);
7465
+ const target = posix7.join(".agents/skills", folderName);
7360
7466
  const pathAbs = resolveWithinRoot(root, path);
7361
7467
  const targetAbs = resolveWithinRoot(root, target);
7362
7468
  const state = await classifyLink(pathAbs, targetAbs);
@@ -7710,7 +7816,7 @@ async function runOutdated(argv) {
7710
7816
  const manifest = await loadManifest(root);
7711
7817
  const registryOverride = !!process.env[REGISTRY_ENV];
7712
7818
  if (registryOverride && config.installed.length > 0) {
7713
- log5.warn(
7819
+ log6.warn(
7714
7820
  `${REGISTRY_ENV} is set, so every module reads as local \u2014 unset it to compare against the registry.`
7715
7821
  );
7716
7822
  }
@@ -7791,10 +7897,10 @@ import {
7791
7897
  confirm as confirm2,
7792
7898
  intro as intro8,
7793
7899
  isCancel as isCancel5,
7794
- log as log6,
7900
+ log as log7,
7795
7901
  note as note8,
7796
7902
  outro as outro8,
7797
- select as select5
7903
+ select as select4
7798
7904
  } from "@clack/prompts";
7799
7905
  import pc10 from "picocolors";
7800
7906
  var KNOWN_FLAGS8 = /* @__PURE__ */ new Set([
@@ -7893,12 +7999,12 @@ function summarizeRemovePlan(plan, name, yes) {
7893
7999
  note8(wrapForNote(linkLines.join("\n")), "Skill links");
7894
8000
  }
7895
8001
  if (plan.dependents.length > 0) {
7896
- log6.warn(
8002
+ log7.warn(
7897
8003
  `${pc10.cyan(name)} is still depended on by ${plan.dependents.join(", ")} \u2014 proceeding anyway (--force).`
7898
8004
  );
7899
8005
  }
7900
8006
  for (const missingLock of plan.missingLockEntries) {
7901
- log6.warn(
8007
+ log7.warn(
7902
8008
  `Installed module ${pc10.cyan(missingLock)} has no lock entry \u2014 dependent detection is incomplete for it.`
7903
8009
  );
7904
8010
  }
@@ -7914,7 +8020,7 @@ function summarizeRemovePlan(plan, name, yes) {
7914
8020
  if (p.action !== "drop") {
7915
8021
  continue;
7916
8022
  }
7917
- log6.warn(
8023
+ log7.warn(
7918
8024
  `Config patch on ${pc10.cyan(p.entry.file)} ${pc10.dim(`(${p.entry.patch.kind})`)} is not reversed by \`remove\` \u2014 hand-revert it if needed.`
7919
8025
  );
7920
8026
  }
@@ -7955,7 +8061,7 @@ async function runRemove(argv) {
7955
8061
  );
7956
8062
  return EXIT_REFUSED;
7957
8063
  }
7958
- const picked = await select5({
8064
+ const picked = await select4({
7959
8065
  message: "Pick a module to remove",
7960
8066
  options: config.installed.map((n) => ({ value: n, label: n }))
7961
8067
  });
@@ -7980,7 +8086,7 @@ async function runRemove(argv) {
7980
8086
  }
7981
8087
  summarizeRemovePlan(plan, name, opts.yes);
7982
8088
  for (const warning of plan.warnings) {
7983
- log6.warn(warning);
8089
+ log7.warn(warning);
7984
8090
  }
7985
8091
  if (opts.diff) {
7986
8092
  for (const file of plan.files) {
@@ -8055,24 +8161,28 @@ async function runRemove(argv) {
8055
8161
  throw failedSave.error;
8056
8162
  }
8057
8163
  for (const file of result.deleted) {
8058
- log6.step(`${pc10.red("delete")} ${file.target}`);
8164
+ log7.step(`${pc10.red("delete")} ${file.target}`);
8059
8165
  }
8060
8166
  for (const file of result.missingUntracked) {
8061
- log6.step(
8167
+ log7.step(
8062
8168
  `${pc10.dim("untrack")} ${file.target} ${pc10.dim("(already gone)")}`
8063
8169
  );
8064
8170
  }
8065
8171
  for (const link of result.linksRemoved) {
8066
- log6.step(`${pc10.red("unlink")} ${link.path}`);
8172
+ log7.step(`${pc10.red("unlink")} ${link.path}`);
8067
8173
  }
8068
8174
  for (const link of result.linkConflicts) {
8069
- log6.warn(
8175
+ log7.warn(
8070
8176
  `Skill link ${pc10.cyan(link.path)} left untouched \u2014 not ours to remove.`
8071
8177
  );
8072
8178
  }
8073
8179
  for (const p of result.patchesReversed) {
8074
- log6.step(`${pc10.red("revert")} ${p.file} ${pc10.dim(`(${p.patch.kind})`)}`);
8180
+ log7.step(`${pc10.red("revert")} ${p.file} ${pc10.dim(`(${p.patch.kind})`)}`);
8075
8181
  }
8182
+ await formatPatchedFiles(
8183
+ root,
8184
+ result.patchesReversed.map((p) => p.file)
8185
+ );
8076
8186
  const refusals = new Map(
8077
8187
  result.patchRefusals.map((r) => [r.patch, r.reason])
8078
8188
  );
@@ -8081,15 +8191,15 @@ async function runRemove(argv) {
8081
8191
  continue;
8082
8192
  }
8083
8193
  const reason = refusals.get(p);
8084
- log6.warn(
8194
+ log7.warn(
8085
8195
  reason === void 0 ? `Config patch on ${pc10.cyan(p.file)} ${pc10.dim(`(${p.patch.kind})`)} was already gone \u2014 nothing to revert.` : `Config patch on ${pc10.cyan(p.file)} ${pc10.dim(`(${p.patch.kind})`)} left untouched \u2014 ${reason}.`
8086
8196
  );
8087
8197
  }
8088
8198
  for (const dir of result.prunedDirs) {
8089
- log6.step(`${pc10.dim("prune")} ${dir}`);
8199
+ log7.step(`${pc10.dim("prune")} ${dir}`);
8090
8200
  }
8091
8201
  for (const alias of result.droppedAliases) {
8092
- log6.warn(
8202
+ log7.warn(
8093
8203
  `Alias ${pc10.cyan(alias)} dropped \u2014 its target directory is gone.`
8094
8204
  );
8095
8205
  }
@@ -8130,13 +8240,13 @@ ${pc10.dim("Hand-edited since \u2014 left on disk, now untracked.")}`
8130
8240
 
8131
8241
  // src/commands/update.ts
8132
8242
  import { mkdir as mkdir10, realpath, writeFile as writeFile13 } from "fs/promises";
8133
- import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join20, resolve as resolve8 } from "path";
8243
+ import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join21, resolve as resolve8 } from "path";
8134
8244
  import {
8135
8245
  cancel as cancel9,
8136
8246
  confirm as confirm3,
8137
8247
  intro as intro9,
8138
8248
  isCancel as isCancel6,
8139
- log as log7,
8249
+ log as log8,
8140
8250
  note as note9,
8141
8251
  outro as outro9
8142
8252
  } from "@clack/prompts";
@@ -8151,14 +8261,14 @@ import {
8151
8261
  rm as rmPath3,
8152
8262
  writeFile as writeFile12
8153
8263
  } from "fs/promises";
8154
- import { dirname as dirname12, join as join19 } from "path";
8155
- var BACKUP_DIR = join19(".saasaloy", "backups");
8264
+ import { dirname as dirname12, join as join20 } from "path";
8265
+ var BACKUP_DIR = join20(".saasaloy", "backups");
8156
8266
  var RECORD_FILE = "backup.json";
8157
8267
  var STATE_FILES = [
8158
8268
  "saasaloy.json",
8159
8269
  "saasaloy-lock.json",
8160
8270
  "package.json",
8161
- join19(".saasaloy", "manifest.json")
8271
+ join20(".saasaloy", "manifest.json")
8162
8272
  ];
8163
8273
  function stampName(now) {
8164
8274
  return now.toISOString().replaceAll(/[:.]/g, "-");
@@ -8166,8 +8276,8 @@ function stampName(now) {
8166
8276
  async function writeBackup(args) {
8167
8277
  const { root, targets, cliVersion } = args;
8168
8278
  const createdAt = (args.now ?? /* @__PURE__ */ new Date()).toISOString();
8169
- const rel = join19(BACKUP_DIR, stampName(args.now ?? new Date(createdAt)));
8170
- const dir = join19(root, rel);
8279
+ const rel = join20(BACKUP_DIR, stampName(args.now ?? new Date(createdAt)));
8280
+ const dir = join20(root, rel);
8171
8281
  const files = [];
8172
8282
  const absent = [];
8173
8283
  for (const target of /* @__PURE__ */ new Set([...targets, ...STATE_FILES])) {
@@ -8176,7 +8286,7 @@ async function writeBackup(args) {
8176
8286
  absent.push(target);
8177
8287
  continue;
8178
8288
  }
8179
- const destination = join19(dir, target);
8289
+ const destination = join20(dir, target);
8180
8290
  await mkdir9(dirname12(destination), { recursive: true });
8181
8291
  await copyFile(source, destination);
8182
8292
  files.push(target);
@@ -8189,7 +8299,7 @@ async function writeBackup(args) {
8189
8299
  };
8190
8300
  await mkdir9(dir, { recursive: true });
8191
8301
  await writeFile12(
8192
- join19(dir, RECORD_FILE),
8302
+ join20(dir, RECORD_FILE),
8193
8303
  `${JSON.stringify(record, null, 2)}
8194
8304
  `,
8195
8305
  "utf-8"
@@ -8197,7 +8307,7 @@ async function writeBackup(args) {
8197
8307
  return rel;
8198
8308
  }
8199
8309
  async function listBackups(root) {
8200
- const dir = join19(root, BACKUP_DIR);
8310
+ const dir = join20(root, BACKUP_DIR);
8201
8311
  if (!await pathExists(dir)) {
8202
8312
  return [];
8203
8313
  }
@@ -8212,25 +8322,25 @@ async function restoreLatestBackup(root) {
8212
8322
  `No backup to restore \u2014 nothing under ${BACKUP_DIR}. \`saasaloy update\` writes one before it applies a plan.`
8213
8323
  );
8214
8324
  }
8215
- const rel = join19(BACKUP_DIR, newest);
8216
- const dir = join19(root, rel);
8217
- const raw = await readIfPresent(join19(dir, RECORD_FILE));
8325
+ const rel = join20(BACKUP_DIR, newest);
8326
+ const dir = join20(root, rel);
8327
+ const raw = await readIfPresent(join20(dir, RECORD_FILE));
8218
8328
  if (raw === void 0) {
8219
8329
  throw new RefusalError(
8220
- `${join19(rel, RECORD_FILE)} is missing \u2014 that backup can't be restored safely.`
8330
+ `${join20(rel, RECORD_FILE)} is missing \u2014 that backup can't be restored safely.`
8221
8331
  );
8222
8332
  }
8223
8333
  let record;
8224
8334
  try {
8225
8335
  record = JSON.parse(raw);
8226
8336
  } catch (error) {
8227
- throw new RefusalError(`${join19(rel, RECORD_FILE)} is invalid.`, {
8337
+ throw new RefusalError(`${join20(rel, RECORD_FILE)} is invalid.`, {
8228
8338
  cause: error
8229
8339
  });
8230
8340
  }
8231
8341
  const restored = [];
8232
8342
  for (const target of record.files) {
8233
- const source = join19(dir, target);
8343
+ const source = join20(dir, target);
8234
8344
  const destination = resolveWithinRoot(root, target);
8235
8345
  if (!await pathExists(source)) {
8236
8346
  continue;
@@ -8672,18 +8782,18 @@ function summarizePlan2(plan) {
8672
8782
  lines.push(pc11.dim(`${writing} file(s) to apply, ${merging} needing merge`));
8673
8783
  note9(wrapForNote(lines.join("\n")), "Plan", TUI_ON_STDERR);
8674
8784
  for (const comparison of plan.skipped) {
8675
- log7.info(
8785
+ log8.info(
8676
8786
  `${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison)}`)}`,
8677
8787
  TUI_ON_STDERR
8678
8788
  );
8679
8789
  }
8680
8790
  for (const mod of plan.modules) {
8681
8791
  for (const conflict of mod.depConflicts) {
8682
- log7.warn(`Dependency version conflict \u2014 ${conflict}.`, TUI_ON_STDERR);
8792
+ log8.warn(`Dependency version conflict \u2014 ${conflict}.`, TUI_ON_STDERR);
8683
8793
  }
8684
8794
  for (const patch of mod.patches) {
8685
8795
  if (patch.action === "missing") {
8686
- log7.warn(
8796
+ log8.warn(
8687
8797
  `Config patch target ${pc11.cyan(patch.file)} is missing \u2014 ${patch.patch.kind} not applied.`,
8688
8798
  TUI_ON_STDERR
8689
8799
  );
@@ -8755,7 +8865,7 @@ async function canonicalPath(path) {
8755
8865
  return await realpath(path);
8756
8866
  } catch {
8757
8867
  try {
8758
- return join20(await realpath(dirname13(path)), basename4(path));
8868
+ return join21(await realpath(dirname13(path)), basename4(path));
8759
8869
  } catch {
8760
8870
  return path;
8761
8871
  }
@@ -8772,7 +8882,7 @@ async function emitMergePlan(document, out) {
8772
8882
  const target = resolvePlanOut(out);
8773
8883
  await mkdir10(dirname13(target), { recursive: true });
8774
8884
  await writeFile13(target, document, "utf-8");
8775
- log7.success(`Merge plan written to ${pc11.cyan(out)}`, TUI_ON_STDERR);
8885
+ log8.success(`Merge plan written to ${pc11.cyan(out)}`, TUI_ON_STDERR);
8776
8886
  }
8777
8887
  function backupTargets(plan) {
8778
8888
  const targets = /* @__PURE__ */ new Set();
@@ -8829,10 +8939,10 @@ async function runUpdate(argv) {
8829
8939
  try {
8830
8940
  const restored = await restoreLatestBackup(root);
8831
8941
  for (const target of restored.restored) {
8832
- log7.step(`${pc11.green("restore")} ${target}`, TUI_ON_STDERR);
8942
+ log8.step(`${pc11.green("restore")} ${target}`, TUI_ON_STDERR);
8833
8943
  }
8834
8944
  for (const target of restored.removed) {
8835
- log7.step(
8945
+ log8.step(
8836
8946
  `${pc11.red("remove")} ${target} ${pc11.dim("(the update created it)")}`,
8837
8947
  TUI_ON_STDERR
8838
8948
  );
@@ -8899,7 +9009,7 @@ async function runUpdate(argv) {
8899
9009
  if (wantsBase) {
8900
9010
  templateDir = await baseTemplateDir();
8901
9011
  if (project.source === "directory") {
8902
- log7.warn(
9012
+ log8.warn(
8903
9013
  `No \`name\` in ${CONFIG_FILE} and no root package.json name \u2014 falling back to the directory name ${pc11.cyan(project.name)}. Set \`name\` in ${CONFIG_FILE} if that isn't this project's name.`,
8904
9014
  TUI_ON_STDERR
8905
9015
  );
@@ -8949,7 +9059,7 @@ async function runUpdate(argv) {
8949
9059
  return EXIT_REFUSED;
8950
9060
  }
8951
9061
  if (recorded !== cliVersion) {
8952
- log7.info(
9062
+ log8.info(
8953
9063
  `Base recorded at CLI ${pc11.cyan(recorded)}; ${pc11.cyan(cliVersion)} is running.`,
8954
9064
  TUI_ON_STDERR
8955
9065
  );
@@ -8991,7 +9101,7 @@ async function runUpdate(argv) {
8991
9101
  return entry && entry.source !== "local";
8992
9102
  });
8993
9103
  if (fromRemote.length > 0) {
8994
- log7.warn(
9104
+ log8.warn(
8995
9105
  `${REGISTRY_ENV} is set, so ${fromRemote.join(", ")} will be updated from that working copy rather than ${pc11.cyan(lock.modules[fromRemote[0]]?.source ?? "the registry")} \u2014 ${pc11.dim("there is no merge base for a working copy")}.`,
8996
9106
  TUI_ON_STDERR
8997
9107
  );
@@ -9016,7 +9126,7 @@ async function runUpdate(argv) {
9016
9126
  }
9017
9127
  }
9018
9128
  for (const comparison of skipped) {
9019
- log7.info(
9129
+ log8.info(
9020
9130
  `${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison, preview)}`)}`,
9021
9131
  TUI_ON_STDERR
9022
9132
  );
@@ -9139,7 +9249,7 @@ async function runUpdate(argv) {
9139
9249
  }
9140
9250
  }
9141
9251
  if (missingLockEntries.size > 0) {
9142
- log7.warn(
9252
+ log8.warn(
9143
9253
  `No lock entry for ${[...missingLockEntries].map((m) => pc11.cyan(m)).join(", ")} \u2014 any conflict they declare can't be checked ${pc11.dim("(re-add them to record it)")}.`,
9144
9254
  TUI_ON_STDERR
9145
9255
  );
@@ -9156,9 +9266,9 @@ async function runUpdate(argv) {
9156
9266
  for (const comparison of skipped) {
9157
9267
  const line = `${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison)}`)}`;
9158
9268
  if (comparison.status === "unresolvable") {
9159
- log7.warn(line, TUI_ON_STDERR);
9269
+ log8.warn(line, TUI_ON_STDERR);
9160
9270
  } else {
9161
- log7.info(line, TUI_ON_STDERR);
9271
+ log8.info(line, TUI_ON_STDERR);
9162
9272
  }
9163
9273
  }
9164
9274
  outro9(pc11.yellow("Nothing could be updated."), TUI_ON_STDERR);
@@ -9166,7 +9276,7 @@ async function runUpdate(argv) {
9166
9276
  }
9167
9277
  const pkg = await readRootPackageJson(root);
9168
9278
  if (!pkg) {
9169
- log7.warn(
9279
+ log8.warn(
9170
9280
  "No package.json at the project root \u2014 dependency pins won't be updated.",
9171
9281
  TUI_ON_STDERR
9172
9282
  );
@@ -9226,7 +9336,7 @@ async function runUpdate(argv) {
9226
9336
  targets: backupTargets(plan),
9227
9337
  cliVersion
9228
9338
  });
9229
- log7.info(
9339
+ log8.info(
9230
9340
  `Pre-update copies in ${pc11.cyan(backupDir)} ${pc11.dim("\u2014 `saasaloy update --abort` restores them")}`,
9231
9341
  TUI_ON_STDERR
9232
9342
  );
@@ -9250,6 +9360,11 @@ async function runUpdate(argv) {
9250
9360
  }
9251
9361
  const result = outcome.value;
9252
9362
  reportResult(result);
9363
+ await formatPatchedFiles(
9364
+ root,
9365
+ result.patched.map((p) => p.file),
9366
+ TUI_ON_STDERR
9367
+ );
9253
9368
  const document = renderMergePlan(plan);
9254
9369
  if (document) {
9255
9370
  await emitMergePlan(document, opts.out);
@@ -9299,37 +9414,37 @@ function summarizeOutcome(plan, result) {
9299
9414
  }
9300
9415
  function reportResult(result) {
9301
9416
  for (const name of result.prereqsInstalled) {
9302
- log7.step(
9417
+ log8.step(
9303
9418
  `${pc11.green("install")} ${pc11.cyan(name)} ${pc11.dim("(new prerequisite)")}`,
9304
9419
  TUI_ON_STDERR
9305
9420
  );
9306
9421
  }
9307
9422
  for (const file of result.written) {
9308
- log7.step(`${ACTION_LABEL2[file.action]} ${file.target}`, TUI_ON_STDERR);
9423
+ log8.step(`${ACTION_LABEL2[file.action]} ${file.target}`, TUI_ON_STDERR);
9309
9424
  }
9310
9425
  for (const file of result.deleted) {
9311
- log7.step(`${pc11.red("delete")} ${file.target}`, TUI_ON_STDERR);
9426
+ log8.step(`${pc11.red("delete")} ${file.target}`, TUI_ON_STDERR);
9312
9427
  }
9313
9428
  for (const file of result.untracked) {
9314
- log7.step(
9429
+ log8.step(
9315
9430
  `${pc11.dim("untrack")} ${file.target} ${pc11.dim("(already gone)")}`,
9316
9431
  TUI_ON_STDERR
9317
9432
  );
9318
9433
  }
9319
9434
  for (const patch of result.patched) {
9320
- log7.step(
9435
+ log8.step(
9321
9436
  `${pc11.green("patch")} ${patch.file} ${pc11.dim(`\u2014 ${patch.patch.kind}`)}`,
9322
9437
  TUI_ON_STDERR
9323
9438
  );
9324
9439
  }
9325
9440
  for (const link of result.linkConflicts) {
9326
- log7.warn(
9441
+ log8.warn(
9327
9442
  `Skill link ${pc11.cyan(link.path)} left untouched \u2014 not ours to replace.`,
9328
9443
  TUI_ON_STDERR
9329
9444
  );
9330
9445
  }
9331
9446
  for (const dep of result.depsWritten) {
9332
- log7.step(`${pc11.cyan("dep")} ${dep.name}@${dep.version}`, TUI_ON_STDERR);
9447
+ log8.step(`${pc11.cyan("dep")} ${dep.name}@${dep.version}`, TUI_ON_STDERR);
9333
9448
  }
9334
9449
  if (result.lateDrift.length > 0) {
9335
9450
  const lines = result.lateDrift.map((f) => ` ${pc11.yellow("kept")} ${f.target}`).join("\n");
@@ -9409,8 +9524,8 @@ ${pc12.dim("Set SAASALOY_DEBUG=1 to print the full cause chain behind a failure.
9409
9524
  );
9410
9525
  }
9411
9526
  async function pickCommand(deps) {
9412
- const { registry, select: select6, isCancel: isCancel7 } = deps;
9413
- const picked = await select6({
9527
+ const { registry, select: select5, isCancel: isCancel7 } = deps;
9528
+ const picked = await select5({
9414
9529
  message: "What would you like to do?",
9415
9530
  options: Object.entries(registry).map(([name, command2]) => ({
9416
9531
  value: name,
@@ -9431,7 +9546,7 @@ async function pickCommand(deps) {
9431
9546
  async function main(argv, deps = {}) {
9432
9547
  const {
9433
9548
  registry = COMMANDS,
9434
- select: select6 = clackSelect,
9549
+ select: select5 = clackSelect,
9435
9550
  isCancel: isCancel7 = clackIsCancel
9436
9551
  } = deps;
9437
9552
  const [name, ...rest] = argv;
@@ -9448,7 +9563,7 @@ async function main(argv, deps = {}) {
9448
9563
  printHelp(registry);
9449
9564
  return EXIT_OK;
9450
9565
  }
9451
- return pickCommand({ registry, select: select6, isCancel: isCancel7 });
9566
+ return pickCommand({ registry, select: select5, isCancel: isCancel7 });
9452
9567
  }
9453
9568
  const command = Object.hasOwn(registry, name) ? registry[name] : void 0;
9454
9569
  if (!command) {