saasaloy 0.5.1 → 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.
- package/README.md +8 -7
- package/dist/index.js +639 -522
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/schemas/registry-item.schema.json +31 -1
- package/templates/base/AGENTS.md +180 -53
- package/templates/base/README.md +20 -8
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +26 -15
- package/templates/base/_agents/skills/saasaloy-env/SKILL.md +130 -0
- package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +63 -46
- package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +1 -1
- package/templates/base/_gitignore +0 -3
- package/templates/base/_saasaloy-base.json +8 -1
- package/templates/base/{DESIGN.md → apps/web/DESIGN.md} +36 -12
- package/templates/base/apps/web/astro.config.mjs +54 -3
- package/templates/base/apps/web/package.json +13 -11
- package/templates/base/apps/web/src/env.ts +41 -0
- package/templates/base/apps/web/src/layouts/Layout.astro +39 -11
- package/templates/base/apps/web/src/pages/index.astro +33 -24
- package/templates/base/apps/web/src/pages/privacy.astro +10 -2
- package/templates/base/apps/web/src/pages/terms.astro +10 -2
- package/templates/base/apps/web/wrangler.jsonc +7 -0
- package/templates/base/oxlint.config.mjs +91 -4
- package/templates/base/package.json +15 -11
- package/templates/base/packages/env/_env.example +54 -0
- package/templates/base/packages/env/package.json +35 -0
- package/templates/base/packages/env/scripts/env-setup.ts +87 -0
- package/templates/base/packages/env/src/check.test.ts +41 -0
- package/templates/base/packages/env/src/check.ts +50 -0
- package/templates/base/packages/env/src/define.test.ts +206 -0
- package/templates/base/packages/env/src/define.ts +315 -0
- package/templates/base/packages/env/src/deploy.test.ts +438 -0
- package/templates/base/packages/env/src/deploy.ts +518 -0
- package/templates/base/packages/env/src/dotenv.test.ts +179 -0
- package/templates/base/packages/env/src/dotenv.ts +173 -0
- package/templates/base/packages/env/src/env-file.ts +51 -0
- package/templates/base/packages/env/src/error.ts +38 -0
- package/templates/base/packages/env/src/example.test.ts +86 -0
- package/templates/base/packages/env/src/example.ts +90 -0
- package/templates/base/packages/env/src/index.ts +15 -0
- package/templates/base/packages/env/src/schema.ts +63 -0
- package/templates/base/packages/env/src/services.test.ts +83 -0
- package/templates/base/packages/env/src/services.ts +110 -0
- package/templates/base/packages/env/src/setup.test.ts +254 -0
- package/templates/base/packages/env/src/setup.ts +177 -0
- package/templates/base/packages/env/src/source.ts +20 -0
- package/templates/base/packages/env/src/sources.test.ts +137 -0
- package/templates/base/packages/env/src/sources.ts +93 -0
- package/templates/base/packages/env/src/zod.ts +8 -0
- package/templates/base/packages/env/tsconfig.json +13 -0
- package/templates/base/packages/tsconfig/base.json +7 -0
- package/templates/base/packages/ui/components.json +1 -1
- package/templates/base/packages/ui/package.json +5 -4
- package/templates/base/packages/ui/src/blocks/audience.tsx +73 -0
- package/templates/base/packages/ui/src/blocks/cta.tsx +41 -30
- package/templates/base/packages/ui/src/blocks/error-state.tsx +2 -0
- package/templates/base/packages/ui/src/blocks/faq.tsx +64 -36
- package/templates/base/packages/ui/src/blocks/feature-grid.tsx +48 -146
- package/templates/base/packages/ui/src/blocks/footer.tsx +129 -70
- package/templates/base/packages/ui/src/blocks/hero.tsx +183 -26
- package/templates/base/packages/ui/src/blocks/how-it-works.tsx +75 -0
- package/templates/base/packages/ui/src/blocks/logo-cloud.tsx +118 -0
- package/templates/base/packages/ui/src/blocks/navbar.tsx +204 -88
- package/templates/base/packages/ui/src/blocks/pain-points.tsx +131 -0
- package/templates/base/packages/ui/src/blocks/pricing-table.tsx +112 -94
- package/templates/base/packages/ui/src/blocks/showcase.tsx +185 -0
- package/templates/base/packages/ui/src/blocks/testimonials.tsx +85 -0
- package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +16 -12
- package/templates/base/packages/ui/src/components/badge.tsx +1 -1
- package/templates/base/packages/ui/src/components/button.tsx +2 -2
- package/templates/base/packages/ui/src/components/tabs.tsx +1 -1
- package/templates/base/packages/ui/src/content/landing.ts +590 -125
- package/templates/base/packages/ui/src/index.ts +6 -2
- package/templates/base/packages/ui/src/lib/external-link.ts +36 -0
- package/templates/base/packages/ui/src/lib/icons.ts +131 -0
- package/templates/base/packages/ui/src/lib/inline-code.ts +29 -0
- package/templates/base/packages/ui/src/lib/theme.ts +173 -86
- package/templates/base/packages/ui/src/styles/globals.css +47 -125
- package/templates/base/packages/ui/src/styles/web.css +359 -0
- package/templates/base/pnpm-workspace.yaml +0 -6
- package/templates/base/saasaloy.json +1 -0
- package/templates/base/stylelint.config.js +3 -3
- package/templates/base/turbo.json +13 -0
- 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
|
|
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
|
|
27
|
-
import { dirname as
|
|
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
|
|
278
|
-
return
|
|
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
|
|
287
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
434
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2248
|
-
await
|
|
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
|
|
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
|
|
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/
|
|
2744
|
-
import {
|
|
2745
|
-
import {
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
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
|
-
|
|
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
|
|
2769
|
-
|
|
2770
|
-
|
|
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
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
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
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
3232
|
+
import { mkdtemp, readFile as readFile10, rm } from "fs/promises";
|
|
3043
3233
|
import { tmpdir } from "os";
|
|
3044
|
-
import { isAbsolute as isAbsolute2, join as
|
|
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 =
|
|
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
|
|
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
|
-
|
|
3116
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
3422
|
+
const parent = await mkdtemp(join9(tmpdir(), "saasaloy-reg-"));
|
|
3213
3423
|
this.tempDirs.push(parent);
|
|
3214
|
-
const dir =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4257
|
+
log2.warn(`Dependency version conflict \u2014 ${conflict}.`);
|
|
4061
4258
|
}
|
|
4062
4259
|
} else {
|
|
4063
|
-
|
|
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
|
-
|
|
4266
|
+
log2.step(`${ACTION_LABEL[file.action]} ${file.target}`);
|
|
4070
4267
|
}
|
|
4071
4268
|
for (const file of result.refreshed) {
|
|
4072
|
-
|
|
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
|
-
|
|
4273
|
+
log2.step(`${label} ${link.path} ${pc2.dim(`\u2192 ${link.target}`)}`);
|
|
4077
4274
|
}
|
|
4078
4275
|
for (const link of result.linkConflicts) {
|
|
4079
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4169
|
-
import { join as
|
|
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
|
|
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
|
|
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.
|
|
4393
|
+
version: "0.6.0",
|
|
4187
4394
|
description: "Composable SaaS accelerator kit.",
|
|
4188
4395
|
keywords: [
|
|
4189
4396
|
"saas",
|
|
@@ -4222,6 +4429,7 @@ var package_default = {
|
|
|
4222
4429
|
},
|
|
4223
4430
|
scripts: {
|
|
4224
4431
|
build: "tsup",
|
|
4432
|
+
clean: "rimraf dist coverage",
|
|
4225
4433
|
prepack: "pnpm -w run readme:cli && pnpm run build",
|
|
4226
4434
|
dev: "tsup --watch",
|
|
4227
4435
|
start: "node ./dist/index.js",
|
|
@@ -4237,6 +4445,7 @@ var package_default = {
|
|
|
4237
4445
|
"@types/node": "26.4.1",
|
|
4238
4446
|
"@vitest/coverage-v8": "5.0.0",
|
|
4239
4447
|
"release-it": "21.0.2",
|
|
4448
|
+
rimraf: "6.1.3",
|
|
4240
4449
|
tsup: "8.5.1",
|
|
4241
4450
|
vitest: "5.0.0"
|
|
4242
4451
|
},
|
|
@@ -4257,7 +4466,7 @@ var TEMPLATE_DIR_CANDIDATES = ["../templates/base", "../../templates/base"];
|
|
|
4257
4466
|
async function baseTemplateDir() {
|
|
4258
4467
|
for (const candidate of TEMPLATE_DIR_CANDIDATES) {
|
|
4259
4468
|
const dir = fileURLToPath3(new URL(candidate, import.meta.url));
|
|
4260
|
-
if (await pathExists(
|
|
4469
|
+
if (await pathExists(join10(dir, "package.json"))) {
|
|
4261
4470
|
return dir;
|
|
4262
4471
|
}
|
|
4263
4472
|
}
|
|
@@ -4280,11 +4489,11 @@ async function copyTree(srcDir, destDir, vars, options, fromPrefix, targetPrefix
|
|
|
4280
4489
|
if (isRoot && entry.name === BASE_DECLARATION) {
|
|
4281
4490
|
continue;
|
|
4282
4491
|
}
|
|
4283
|
-
const srcPath =
|
|
4492
|
+
const srcPath = join10(srcDir, entry.name);
|
|
4284
4493
|
const outName = renderedName(entry.name);
|
|
4285
|
-
const destPath =
|
|
4286
|
-
const from =
|
|
4287
|
-
const target =
|
|
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);
|
|
4288
4497
|
if (entry.isDirectory()) {
|
|
4289
4498
|
written.push(
|
|
4290
4499
|
...await copyTree(
|
|
@@ -4334,9 +4543,9 @@ function ownedMatcher(declaration) {
|
|
|
4334
4543
|
return (target) => patterns.some((re) => re.test(target));
|
|
4335
4544
|
}
|
|
4336
4545
|
async function readBaseDeclaration(templateDir) {
|
|
4337
|
-
const raw = await readIfPresent(
|
|
4546
|
+
const raw = await readIfPresent(join11(templateDir, BASE_DECLARATION));
|
|
4338
4547
|
if (raw === void 0) {
|
|
4339
|
-
return { seedFiles: [], ownedFiles: [] };
|
|
4548
|
+
return { seedFiles: [], retiredSeedFiles: [], ownedFiles: [] };
|
|
4340
4549
|
}
|
|
4341
4550
|
const parsed = JSON.parse(raw);
|
|
4342
4551
|
const seedFiles = parsed.seedFiles;
|
|
@@ -4345,6 +4554,14 @@ async function readBaseDeclaration(templateDir) {
|
|
|
4345
4554
|
`${BASE_DECLARATION} must declare "seedFiles" as a list of project-relative paths.`
|
|
4346
4555
|
);
|
|
4347
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
|
+
}
|
|
4348
4565
|
const ownedFiles = parsed.ownedFiles ?? [];
|
|
4349
4566
|
if (!Array.isArray(ownedFiles) || !ownedFiles.every((entry) => typeof entry === "string" && entry !== "")) {
|
|
4350
4567
|
throw new Error(
|
|
@@ -4352,7 +4569,8 @@ async function readBaseDeclaration(templateDir) {
|
|
|
4352
4569
|
);
|
|
4353
4570
|
}
|
|
4354
4571
|
return {
|
|
4355
|
-
seedFiles,
|
|
4572
|
+
seedFiles: [...seedFiles, ...retiredSeedFiles],
|
|
4573
|
+
retiredSeedFiles,
|
|
4356
4574
|
ownedFiles
|
|
4357
4575
|
};
|
|
4358
4576
|
}
|
|
@@ -4363,9 +4581,9 @@ async function listTemplateFiles(templateDir) {
|
|
|
4363
4581
|
if (isRoot && entry.name === BASE_DECLARATION) {
|
|
4364
4582
|
continue;
|
|
4365
4583
|
}
|
|
4366
|
-
const from =
|
|
4367
|
-
const target =
|
|
4368
|
-
const abs =
|
|
4584
|
+
const from = posix5.join(fromPrefix, entry.name);
|
|
4585
|
+
const target = posix5.join(targetPrefix, renderedName(entry.name));
|
|
4586
|
+
const abs = join11(dir, entry.name);
|
|
4369
4587
|
if (entry.isDirectory()) {
|
|
4370
4588
|
await walk(abs, from, target, false);
|
|
4371
4589
|
} else if (entry.isFile()) {
|
|
@@ -4439,7 +4657,7 @@ async function missingBaseTargets(root, manifest, templateDir) {
|
|
|
4439
4657
|
return missing.toSorted((a, b) => a < b ? -1 : 1);
|
|
4440
4658
|
}
|
|
4441
4659
|
async function renderTemplate(root, templateDir, projectName) {
|
|
4442
|
-
const dir = await mkdtemp2(
|
|
4660
|
+
const dir = await mkdtemp2(join11(tmpdir2(), "saasaloy-base-render-"));
|
|
4443
4661
|
const files = await copyTemplate(
|
|
4444
4662
|
templateDir,
|
|
4445
4663
|
dir,
|
|
@@ -4555,7 +4773,7 @@ async function baseUpdateInput(args) {
|
|
|
4555
4773
|
import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
|
|
4556
4774
|
import { parseModule as parseModule7 } from "magicast";
|
|
4557
4775
|
import { readFile as readFile13 } from "fs/promises";
|
|
4558
|
-
import { basename, dirname as dirname6, join as
|
|
4776
|
+
import { basename, dirname as dirname6, join as join12, posix as posix6, resolve as resolve3 } from "path";
|
|
4559
4777
|
var SKILL_PREFIX = "saasaloy-";
|
|
4560
4778
|
var PINNED_DEP = /^(@[^/@]+\/)?[^/@]+@\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
|
|
4561
4779
|
function finding(module, where, message) {
|
|
@@ -4570,7 +4788,7 @@ function asRecord(value) {
|
|
|
4570
4788
|
async function registryModuleNames(dir) {
|
|
4571
4789
|
const names = [];
|
|
4572
4790
|
for (const name of await readDirNames(dir)) {
|
|
4573
|
-
if (await pathExists(
|
|
4791
|
+
if (await pathExists(join12(dir, name, "registry-item.json"))) {
|
|
4574
4792
|
names.push(name);
|
|
4575
4793
|
}
|
|
4576
4794
|
}
|
|
@@ -4579,12 +4797,12 @@ async function registryModuleNames(dir) {
|
|
|
4579
4797
|
async function collectAliases(registryDir, names) {
|
|
4580
4798
|
const templateDir = await baseTemplateDir();
|
|
4581
4799
|
let base = {};
|
|
4582
|
-
if (await pathExists(
|
|
4800
|
+
if (await pathExists(join12(templateDir, "saasaloy.json"))) {
|
|
4583
4801
|
base = (await loadConfig(templateDir)).aliases;
|
|
4584
4802
|
}
|
|
4585
4803
|
const fromScaffolds = {};
|
|
4586
4804
|
for (const name of names) {
|
|
4587
|
-
const item = asRecord(await readDescriptor(
|
|
4805
|
+
const item = asRecord(await readDescriptor(join12(registryDir, name)));
|
|
4588
4806
|
for (const scaffold of asArray2(item.scaffolds)) {
|
|
4589
4807
|
const aliases = asRecord(asRecord(scaffold).aliases);
|
|
4590
4808
|
for (const [alias, prefix] of Object.entries(aliases)) {
|
|
@@ -4597,7 +4815,7 @@ async function collectAliases(registryDir, names) {
|
|
|
4597
4815
|
return { base, fromScaffolds };
|
|
4598
4816
|
}
|
|
4599
4817
|
async function readDescriptor(dir) {
|
|
4600
|
-
const file =
|
|
4818
|
+
const file = join12(dir, "registry-item.json");
|
|
4601
4819
|
if (!await pathExists(file)) {
|
|
4602
4820
|
return void 0;
|
|
4603
4821
|
}
|
|
@@ -4612,7 +4830,7 @@ async function checkModule(input) {
|
|
|
4612
4830
|
const module = basename(dir);
|
|
4613
4831
|
const findings = [];
|
|
4614
4832
|
const report = () => ({ dir, findings, module });
|
|
4615
|
-
const file =
|
|
4833
|
+
const file = join12(dir, "registry-item.json");
|
|
4616
4834
|
if (!await pathExists(file)) {
|
|
4617
4835
|
findings.push(
|
|
4618
4836
|
finding(module, "(root)", `no registry-item.json in ${module}/`)
|
|
@@ -4730,6 +4948,28 @@ async function checkModule(input) {
|
|
|
4730
4948
|
);
|
|
4731
4949
|
}
|
|
4732
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
|
+
}
|
|
4733
4973
|
for (const key of Object.keys(asRecord(item.devVars))) {
|
|
4734
4974
|
if (!(key in asRecord(item.envVars))) {
|
|
4735
4975
|
findings.push(
|
|
@@ -4745,7 +4985,7 @@ async function checkModule(input) {
|
|
|
4745
4985
|
if (typeof skill !== "string") {
|
|
4746
4986
|
continue;
|
|
4747
4987
|
}
|
|
4748
|
-
const folder =
|
|
4988
|
+
const folder = posix6.basename(skill);
|
|
4749
4989
|
if (!folder.startsWith(SKILL_PREFIX)) {
|
|
4750
4990
|
findings.push(
|
|
4751
4991
|
finding(
|
|
@@ -4979,7 +5219,7 @@ async function readPolicyState(root, installed) {
|
|
|
4979
5219
|
const source = await readIfPresent(
|
|
4980
5220
|
resolveWithinRoot(
|
|
4981
5221
|
root,
|
|
4982
|
-
|
|
5222
|
+
posix6.join(posix6.dirname(KV_INDEX_FILE), `${specifier}.ts`)
|
|
4983
5223
|
)
|
|
4984
5224
|
);
|
|
4985
5225
|
if (source !== void 0) {
|
|
@@ -5006,7 +5246,7 @@ function relativeImportsIn(index) {
|
|
|
5006
5246
|
}
|
|
5007
5247
|
async function resolveDoctorTarget(path) {
|
|
5008
5248
|
const dir = resolve3(path);
|
|
5009
|
-
if (await pathExists(
|
|
5249
|
+
if (await pathExists(join12(dir, "registry-item.json"))) {
|
|
5010
5250
|
return { names: [basename(dir)], registryDir: dirname6(dir) };
|
|
5011
5251
|
}
|
|
5012
5252
|
return { names: await registryModuleNames(dir), registryDir: dir };
|
|
@@ -5028,6 +5268,54 @@ function checkPartialInstalls(state) {
|
|
|
5028
5268
|
)
|
|
5029
5269
|
);
|
|
5030
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
|
+
}
|
|
5031
5319
|
async function checkTarget(target) {
|
|
5032
5320
|
const siblings = await registryModuleNames(target.registryDir);
|
|
5033
5321
|
const aliases = await collectAliases(target.registryDir, siblings);
|
|
@@ -5036,7 +5324,7 @@ async function checkTarget(target) {
|
|
|
5036
5324
|
reports.push(
|
|
5037
5325
|
await checkModule({
|
|
5038
5326
|
aliases,
|
|
5039
|
-
dir:
|
|
5327
|
+
dir: join12(target.registryDir, name),
|
|
5040
5328
|
siblings
|
|
5041
5329
|
})
|
|
5042
5330
|
);
|
|
@@ -5150,10 +5438,13 @@ async function reportProject(path) {
|
|
|
5150
5438
|
});
|
|
5151
5439
|
note2(wrapForNote(renderBase(base)), "Base");
|
|
5152
5440
|
const policyState = await readPolicyState(path, config.installed);
|
|
5441
|
+
const envState = await readEnvState(path);
|
|
5153
5442
|
const findings = [
|
|
5154
5443
|
...checkProject({ config, manifest }),
|
|
5155
5444
|
...checkPartialInstalls({ installed: config.installed, manifest }),
|
|
5156
|
-
...policyState ? checkPolicyBindings(policyState) : []
|
|
5445
|
+
...policyState ? checkPolicyBindings(policyState) : [],
|
|
5446
|
+
...checkEnv(envState),
|
|
5447
|
+
...checkRetiredInfra(await hasRetiredInfra(path))
|
|
5157
5448
|
];
|
|
5158
5449
|
if (findings.length === 0) {
|
|
5159
5450
|
const count = config.installed.length;
|
|
@@ -5198,7 +5489,7 @@ async function runDoctor(argv) {
|
|
|
5198
5489
|
);
|
|
5199
5490
|
return EXIT_REFUSED;
|
|
5200
5491
|
}
|
|
5201
|
-
if (await pathExists(
|
|
5492
|
+
if (await pathExists(join13(path, CONFIG_FILE))) {
|
|
5202
5493
|
return await reportProject(path);
|
|
5203
5494
|
}
|
|
5204
5495
|
const target = await resolveDoctorTarget(path);
|
|
@@ -5228,7 +5519,7 @@ async function runDoctor(argv) {
|
|
|
5228
5519
|
`${pc3.yellow(report.module)} ${pc3.dim(`(${report.findings.length})`)}`
|
|
5229
5520
|
);
|
|
5230
5521
|
}
|
|
5231
|
-
|
|
5522
|
+
log3.info(
|
|
5232
5523
|
pc3.dim(
|
|
5233
5524
|
`Checked ${reports.length} module${reports.length === 1 ? "" : "s"}; ${reports.length - bad.length} clean.`
|
|
5234
5525
|
)
|
|
@@ -5250,118 +5541,29 @@ import {
|
|
|
5250
5541
|
cancel as cancel3,
|
|
5251
5542
|
intro as intro3,
|
|
5252
5543
|
isCancel as isCancel2,
|
|
5253
|
-
log as
|
|
5544
|
+
log as log4,
|
|
5254
5545
|
note as note3,
|
|
5255
5546
|
outro as outro3,
|
|
5256
|
-
select as select2,
|
|
5257
5547
|
text
|
|
5258
5548
|
} from "@clack/prompts";
|
|
5259
5549
|
import pc4 from "picocolors";
|
|
5260
5550
|
|
|
5261
5551
|
// src/lib/env-vars.ts
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
function isPublicVar(name) {
|
|
5269
|
-
return name.startsWith(PUBLIC_PREFIX);
|
|
5270
|
-
}
|
|
5271
|
-
function targetFileName(name) {
|
|
5272
|
-
return isPublicVar(name) ? ENV_FILE : DEV_VARS_FILE;
|
|
5273
|
-
}
|
|
5274
|
-
function resolved(workspace) {
|
|
5275
|
-
return workspace ? { kind: "resolved", workspace } : void 0;
|
|
5276
|
-
}
|
|
5277
|
-
function routeVariable(args) {
|
|
5278
|
-
const { candidates, wranglerWorkspaces } = args;
|
|
5279
|
-
if (isPublicVar(args.name)) {
|
|
5280
|
-
const apps = candidates.filter(
|
|
5281
|
-
(w) => w.startsWith(APPS_DIR) && w !== args.apiWorkspace
|
|
5282
|
-
);
|
|
5283
|
-
if (apps.length === 1) {
|
|
5284
|
-
return { kind: "resolved", workspace: apps[0] };
|
|
5285
|
-
}
|
|
5286
|
-
if (apps.length > 1) {
|
|
5287
|
-
return { choices: apps.toSorted(), kind: "ambiguous" };
|
|
5288
|
-
}
|
|
5289
|
-
return resolved(args.baseWorkspace) ?? fromCandidatesAlone(candidates);
|
|
5290
|
-
}
|
|
5291
|
-
const wrangler = candidates.filter((w) => wranglerWorkspaces.includes(w));
|
|
5292
|
-
if (wrangler.length === 1) {
|
|
5293
|
-
return { kind: "resolved", workspace: wrangler[0] };
|
|
5294
|
-
}
|
|
5295
|
-
if (wrangler.length > 1) {
|
|
5296
|
-
return (args.apiWorkspace && wrangler.includes(args.apiWorkspace) ? resolved(args.apiWorkspace) : void 0) ?? { choices: wrangler.toSorted(), kind: "ambiguous" };
|
|
5297
|
-
}
|
|
5298
|
-
return resolved(args.apiWorkspace) ?? fromCandidatesAlone(candidates);
|
|
5299
|
-
}
|
|
5300
|
-
function fromCandidatesAlone(candidates) {
|
|
5301
|
-
if (candidates.length === 1) {
|
|
5302
|
-
return { kind: "resolved", workspace: candidates[0] };
|
|
5303
|
-
}
|
|
5304
|
-
return candidates.length === 0 ? { kind: "unknown" } : { choices: candidates.toSorted(), kind: "ambiguous" };
|
|
5305
|
-
}
|
|
5306
|
-
async function discoverWorkspaces(root, aliases) {
|
|
5307
|
-
const found = /* @__PURE__ */ new Set();
|
|
5308
|
-
for (const value of Object.values(aliases)) {
|
|
5309
|
-
const workspace = await walkUpToWorkspace(root, value);
|
|
5310
|
-
if (workspace) {
|
|
5311
|
-
found.add(workspace);
|
|
5312
|
-
}
|
|
5313
|
-
}
|
|
5314
|
-
return [...found].toSorted();
|
|
5315
|
-
}
|
|
5316
|
-
async function walkUpToWorkspace(root, relPosixPath) {
|
|
5317
|
-
let current = relPosixPath.replace(/\/+$/, "");
|
|
5318
|
-
while (current && current !== "." && current !== "/") {
|
|
5319
|
-
if (await pathExists(resolveWithinRoot(root, `${current}/package.json`))) {
|
|
5320
|
-
return current;
|
|
5321
|
-
}
|
|
5322
|
-
const parent = posix8.dirname(current);
|
|
5323
|
-
if (parent === current) {
|
|
5324
|
-
return void 0;
|
|
5325
|
-
}
|
|
5326
|
-
current = parent;
|
|
5327
|
-
}
|
|
5328
|
-
return void 0;
|
|
5329
|
-
}
|
|
5330
|
-
async function findWranglerWorkspaces(root, workspaces) {
|
|
5331
|
-
const found = [];
|
|
5332
|
-
for (const workspace of workspaces) {
|
|
5333
|
-
for (const config of WRANGLER_CONFIGS) {
|
|
5334
|
-
if (await pathExists(resolveWithinRoot(root, `${workspace}/${config}`))) {
|
|
5335
|
-
found.push(workspace);
|
|
5336
|
-
break;
|
|
5337
|
-
}
|
|
5338
|
-
}
|
|
5339
|
-
}
|
|
5340
|
-
return found;
|
|
5341
|
-
}
|
|
5342
|
-
function workspacesByModule(manifest, workspaces) {
|
|
5343
|
-
const byModule = /* @__PURE__ */ new Map();
|
|
5344
|
-
for (const [path, entry] of Object.entries(manifest.managed)) {
|
|
5345
|
-
const workspace = workspaceForPath(path, workspaces);
|
|
5346
|
-
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("#")) {
|
|
5347
5558
|
continue;
|
|
5348
5559
|
}
|
|
5349
|
-
const
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
}
|
|
5353
|
-
return new Map(
|
|
5354
|
-
[...byModule].map(([module, set]) => [module, [...set].toSorted()])
|
|
5355
|
-
);
|
|
5356
|
-
}
|
|
5357
|
-
function workspaceForPath(path, workspaces) {
|
|
5358
|
-
let best;
|
|
5359
|
-
for (const workspace of workspaces) {
|
|
5360
|
-
if (path.startsWith(`${workspace}/`) && (best === void 0 || workspace.length > best.length)) {
|
|
5361
|
-
best = workspace;
|
|
5560
|
+
const eq = trimmed.indexOf("=");
|
|
5561
|
+
if (eq <= 0) {
|
|
5562
|
+
continue;
|
|
5362
5563
|
}
|
|
5564
|
+
values[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1);
|
|
5363
5565
|
}
|
|
5364
|
-
return
|
|
5566
|
+
return values;
|
|
5365
5567
|
}
|
|
5366
5568
|
function isSet(values, name) {
|
|
5367
5569
|
return (values[name] ?? "").trim() !== "";
|
|
@@ -5397,27 +5599,20 @@ function appendVars(existing, additions) {
|
|
|
5397
5599
|
return appended.length === 0 ? head : `${head}${appended.join("\n")}
|
|
5398
5600
|
`;
|
|
5399
5601
|
}
|
|
5400
|
-
function productionSecretCommands(
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
continue;
|
|
5405
|
-
}
|
|
5406
|
-
byWorkspace.set(workspace, [...byWorkspace.get(workspace) ?? [], name]);
|
|
5407
|
-
}
|
|
5408
|
-
const lines = [];
|
|
5409
|
-
for (const workspace of [...byWorkspace.keys()].toSorted()) {
|
|
5410
|
-
lines.push(`# from ${workspace}`);
|
|
5411
|
-
for (const name of (byWorkspace.get(workspace) ?? []).toSorted()) {
|
|
5412
|
-
lines.push(`wrangler secret put ${name}`);
|
|
5413
|
-
}
|
|
5602
|
+
function productionSecretCommands(names) {
|
|
5603
|
+
const secrets = names.filter((name) => !isPublicVar(name)).toSorted();
|
|
5604
|
+
if (secrets.length === 0) {
|
|
5605
|
+
return [];
|
|
5414
5606
|
}
|
|
5415
|
-
return
|
|
5607
|
+
return [
|
|
5608
|
+
"# from the Worker's workspace, e.g. apps/api",
|
|
5609
|
+
...secrets.map((name) => `wrangler secret put ${name}`)
|
|
5610
|
+
];
|
|
5416
5611
|
}
|
|
5417
5612
|
|
|
5418
5613
|
// src/lib/gitignore.ts
|
|
5419
5614
|
import { readFile as readFile14 } from "fs/promises";
|
|
5420
|
-
import { dirname as dirname7, join as
|
|
5615
|
+
import { dirname as dirname7, join as join14, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
|
|
5421
5616
|
var REGEX_SPECIAL = /[.+^${}()|[\]\\]/g;
|
|
5422
5617
|
function globToRegex(glob) {
|
|
5423
5618
|
let out = "";
|
|
@@ -5501,7 +5696,7 @@ function excludes(layers, candidate, isDir) {
|
|
|
5501
5696
|
async function findRepoRoot(start) {
|
|
5502
5697
|
let current = resolve4(start);
|
|
5503
5698
|
for (; ; ) {
|
|
5504
|
-
if (await pathExists(
|
|
5699
|
+
if (await pathExists(join14(current, ".git"))) {
|
|
5505
5700
|
return current;
|
|
5506
5701
|
}
|
|
5507
5702
|
const parent = dirname7(current);
|
|
@@ -5518,7 +5713,7 @@ async function isPathIgnored(root, relPosixPath) {
|
|
|
5518
5713
|
}
|
|
5519
5714
|
const fromRepo = relative3(
|
|
5520
5715
|
repoRoot,
|
|
5521
|
-
|
|
5716
|
+
join14(resolve4(root), ...relPosixPath.split("/"))
|
|
5522
5717
|
);
|
|
5523
5718
|
const segments = fromRepo.split(sep2).filter(Boolean);
|
|
5524
5719
|
if (segments.length === 0 || segments.includes("..")) {
|
|
@@ -5527,7 +5722,7 @@ async function isPathIgnored(root, relPosixPath) {
|
|
|
5527
5722
|
const layers = [];
|
|
5528
5723
|
for (let depth = 0; depth < segments.length; depth += 1) {
|
|
5529
5724
|
const parts = segments.slice(0, depth);
|
|
5530
|
-
const file =
|
|
5725
|
+
const file = join14(repoRoot, ...parts, ".gitignore");
|
|
5531
5726
|
if (await pathExists(file)) {
|
|
5532
5727
|
layers.push({
|
|
5533
5728
|
base: parts.join("/"),
|
|
@@ -5565,16 +5760,9 @@ function parseArgs3(argv) {
|
|
|
5565
5760
|
return { check: argv.includes("--check"), unknown };
|
|
5566
5761
|
}
|
|
5567
5762
|
function renderPending(pending) {
|
|
5568
|
-
return pending.map(
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
return `${head}
|
|
5572
|
-
${pc4.dim(`\u2192 ${entry.file}`)}`;
|
|
5573
|
-
}
|
|
5574
|
-
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";
|
|
5575
|
-
return `${head}
|
|
5576
|
-
${pc4.yellow(`\u2192 ${why}`)}`;
|
|
5577
|
-
});
|
|
5763
|
+
return pending.map(
|
|
5764
|
+
(entry) => `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2014 declared by ${entry.declaration.module}`)}`
|
|
5765
|
+
);
|
|
5578
5766
|
}
|
|
5579
5767
|
async function loadDeclarations(config, lock, sources, onSkip) {
|
|
5580
5768
|
const override = process.env[REGISTRY_ENV];
|
|
@@ -5643,31 +5831,15 @@ async function loadDeclarations(config, lock, sources, onSkip) {
|
|
|
5643
5831
|
return true;
|
|
5644
5832
|
});
|
|
5645
5833
|
}
|
|
5646
|
-
function
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
return void 0;
|
|
5650
|
-
}
|
|
5651
|
-
return workspaceForPath(
|
|
5652
|
-
value.endsWith("/") ? value : `${value}/`,
|
|
5653
|
-
workspaces
|
|
5654
|
-
);
|
|
5655
|
-
}
|
|
5656
|
-
async function writeAnswers(root, answers, contents) {
|
|
5657
|
-
const written = [];
|
|
5658
|
-
for (const [file, additions] of [...answers].toSorted(
|
|
5659
|
-
([a], [b]) => a.localeCompare(b)
|
|
5660
|
-
)) {
|
|
5661
|
-
if (additions.length === 0) {
|
|
5662
|
-
continue;
|
|
5663
|
-
}
|
|
5664
|
-
const abs = resolveWithinRoot(root, file);
|
|
5665
|
-
await assertNoSymlinkPath(root, abs);
|
|
5666
|
-
await mkdir6(dirname8(abs), { recursive: true });
|
|
5667
|
-
await writeFile8(abs, appendVars(contents.get(file), additions), "utf-8");
|
|
5668
|
-
written.push([file, additions.length]);
|
|
5834
|
+
async function writeAnswers(root, additions, before) {
|
|
5835
|
+
if (additions.length === 0) {
|
|
5836
|
+
return 0;
|
|
5669
5837
|
}
|
|
5670
|
-
|
|
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;
|
|
5671
5843
|
}
|
|
5672
5844
|
async function readTarget(root, file) {
|
|
5673
5845
|
const abs = resolveWithinRoot(root, file);
|
|
@@ -5691,62 +5863,34 @@ async function runEnv(argv) {
|
|
|
5691
5863
|
try {
|
|
5692
5864
|
const root = await findProjectRoot();
|
|
5693
5865
|
const config = await loadConfig(root);
|
|
5694
|
-
const manifest = await loadManifest(root);
|
|
5695
5866
|
const lock = await loadLock(root);
|
|
5696
5867
|
const declarations = await loadDeclarations(
|
|
5697
5868
|
config,
|
|
5698
5869
|
lock,
|
|
5699
5870
|
sources,
|
|
5700
5871
|
(msg) => {
|
|
5701
|
-
|
|
5872
|
+
log4.warn(msg);
|
|
5702
5873
|
}
|
|
5703
5874
|
);
|
|
5704
5875
|
if (declarations.length === 0) {
|
|
5705
5876
|
outro3(pc4.dim("No module declares an environment variable."));
|
|
5706
5877
|
return EXIT_OK;
|
|
5707
5878
|
}
|
|
5708
|
-
const
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
const
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
);
|
|
5717
|
-
const contents = /* @__PURE__ */ new Map();
|
|
5718
|
-
const readOnce = async (file) => {
|
|
5719
|
-
if (!contents.has(file)) {
|
|
5720
|
-
contents.set(file, await readTarget(root, file));
|
|
5721
|
-
}
|
|
5722
|
-
return contents.get(file);
|
|
5723
|
-
};
|
|
5724
|
-
const pending = [];
|
|
5725
|
-
const routed = [];
|
|
5726
|
-
for (const declaration of declarations) {
|
|
5727
|
-
const route = routeVariable({
|
|
5728
|
-
candidates: byModule.get(declaration.module) ?? [],
|
|
5729
|
-
name: declaration.name,
|
|
5730
|
-
wranglerWorkspaces,
|
|
5731
|
-
...api ? { apiWorkspace: api } : {},
|
|
5732
|
-
...base ? { baseWorkspace: base } : {}
|
|
5733
|
-
});
|
|
5734
|
-
if (route.kind === "resolved") {
|
|
5735
|
-
const file = `${route.workspace}/${targetFileName(declaration.name)}`;
|
|
5736
|
-
routed.push({ name: declaration.name, workspace: route.workspace });
|
|
5737
|
-
const values = parseDevVars(await readOnce(file) ?? "");
|
|
5738
|
-
if (isSet(values, declaration.name)) {
|
|
5739
|
-
continue;
|
|
5740
|
-
}
|
|
5741
|
-
pending.push({ declaration, file, workspace: route.workspace });
|
|
5742
|
-
continue;
|
|
5743
|
-
}
|
|
5744
|
-
pending.push({
|
|
5745
|
-
declaration,
|
|
5746
|
-
...route.kind === "ambiguous" ? { choices: route.choices } : {}
|
|
5747
|
-
});
|
|
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
|
+
);
|
|
5748
5887
|
}
|
|
5749
|
-
const
|
|
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
|
+
);
|
|
5750
5894
|
const printSecrets = () => {
|
|
5751
5895
|
if (secretCommands.length > 0) {
|
|
5752
5896
|
note3(
|
|
@@ -5764,16 +5908,11 @@ ${pc4.dim("Printed, never run \u2014 copy these when you deploy.")}`
|
|
|
5764
5908
|
outro3(pc4.green("Every declared variable is set."));
|
|
5765
5909
|
return EXIT_OK;
|
|
5766
5910
|
}
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
cancel3(
|
|
5773
|
-
`${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.`
|
|
5774
|
-
);
|
|
5775
|
-
return EXIT_REFUSED;
|
|
5776
|
-
}
|
|
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;
|
|
5777
5916
|
}
|
|
5778
5917
|
if (opts.check || !isInteractive()) {
|
|
5779
5918
|
note3(wrapForNote(renderPending(pending).join("\n")), "Missing");
|
|
@@ -5784,42 +5923,11 @@ ${pc4.dim("Printed, never run \u2014 copy these when you deploy.")}`
|
|
|
5784
5923
|
);
|
|
5785
5924
|
return EXIT_REFUSED;
|
|
5786
5925
|
}
|
|
5787
|
-
const answers =
|
|
5926
|
+
const answers = [];
|
|
5788
5927
|
for (const entry of pending) {
|
|
5789
|
-
let file = entry.file;
|
|
5790
|
-
if (!file) {
|
|
5791
|
-
if (!entry.choices) {
|
|
5792
|
-
log3.warn(
|
|
5793
|
-
`${pc4.cyan(entry.declaration.name)} \u2014 no target workspace found; set it by hand.`
|
|
5794
|
-
);
|
|
5795
|
-
continue;
|
|
5796
|
-
}
|
|
5797
|
-
const picked = await select2({
|
|
5798
|
-
message: `Which workspace reads ${entry.declaration.name}?`,
|
|
5799
|
-
options: entry.choices.map((workspace) => ({
|
|
5800
|
-
label: workspace,
|
|
5801
|
-
value: workspace
|
|
5802
|
-
}))
|
|
5803
|
-
});
|
|
5804
|
-
if (isCancel2(picked)) {
|
|
5805
|
-
cancel3("Cancelled \u2014 nothing written.");
|
|
5806
|
-
return EXIT_REFUSED;
|
|
5807
|
-
}
|
|
5808
|
-
file = `${picked}/${targetFileName(entry.declaration.name)}`;
|
|
5809
|
-
if (!await isPathIgnored(root, file)) {
|
|
5810
|
-
cancel3(
|
|
5811
|
-
`${pc4.cyan(file)} isn't gitignored \u2014 refusing to write a secret into a tracked file.`
|
|
5812
|
-
);
|
|
5813
|
-
return EXIT_REFUSED;
|
|
5814
|
-
}
|
|
5815
|
-
const values = parseDevVars(await readOnce(file) ?? "");
|
|
5816
|
-
if (isSet(values, entry.declaration.name)) {
|
|
5817
|
-
continue;
|
|
5818
|
-
}
|
|
5819
|
-
}
|
|
5820
5928
|
const answer = await text({
|
|
5821
5929
|
defaultValue: entry.declaration.devValue ?? "",
|
|
5822
|
-
message: `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2192 ${
|
|
5930
|
+
message: `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2192 ${LOCAL_VALUES}`)}
|
|
5823
5931
|
${entry.declaration.description}`,
|
|
5824
5932
|
...entry.declaration.devValue === void 0 ? {} : { initialValue: entry.declaration.devValue }
|
|
5825
5933
|
});
|
|
@@ -5827,18 +5935,15 @@ ${entry.declaration.description}`,
|
|
|
5827
5935
|
cancel3("Cancelled \u2014 nothing written.");
|
|
5828
5936
|
return EXIT_REFUSED;
|
|
5829
5937
|
}
|
|
5830
|
-
answers.
|
|
5831
|
-
...answers.get(file) ?? [],
|
|
5832
|
-
[entry.declaration.name, answer]
|
|
5833
|
-
]);
|
|
5938
|
+
answers.push([entry.declaration.name, answer]);
|
|
5834
5939
|
}
|
|
5835
|
-
const written = await writeAnswers(root, answers,
|
|
5836
|
-
if (written
|
|
5940
|
+
const written = await writeAnswers(root, answers, before);
|
|
5941
|
+
if (written > 0) {
|
|
5837
5942
|
note3(
|
|
5838
5943
|
wrapForNote(
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
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.")}`
|
|
5842
5947
|
),
|
|
5843
5948
|
"Written"
|
|
5844
5949
|
);
|
|
@@ -5857,39 +5962,39 @@ ${entry.declaration.description}`,
|
|
|
5857
5962
|
}
|
|
5858
5963
|
|
|
5859
5964
|
// src/commands/init.ts
|
|
5860
|
-
import { spawn } from "child_process";
|
|
5965
|
+
import { spawn as spawn2 } from "child_process";
|
|
5861
5966
|
import { readdir as readdir4, readFile as readFile16 } from "fs/promises";
|
|
5862
|
-
import { basename as basename2, join as
|
|
5967
|
+
import { basename as basename2, join as join15, resolve as resolve6 } from "path";
|
|
5863
5968
|
import {
|
|
5864
5969
|
cancel as cancel4,
|
|
5865
5970
|
intro as intro4,
|
|
5866
5971
|
isCancel as isCancel3,
|
|
5867
5972
|
note as note4,
|
|
5868
5973
|
outro as outro4,
|
|
5869
|
-
select as
|
|
5974
|
+
select as select2,
|
|
5870
5975
|
spinner,
|
|
5871
5976
|
text as text2
|
|
5872
5977
|
} from "@clack/prompts";
|
|
5873
5978
|
import pc6 from "picocolors";
|
|
5874
5979
|
|
|
5875
5980
|
// src/lib/logger.ts
|
|
5876
|
-
import { log as
|
|
5981
|
+
import { log as log5 } from "@clack/prompts";
|
|
5877
5982
|
import pc5 from "picocolors";
|
|
5878
5983
|
var logger = {
|
|
5879
5984
|
error(message) {
|
|
5880
|
-
|
|
5985
|
+
log5.error(message);
|
|
5881
5986
|
},
|
|
5882
5987
|
info(message) {
|
|
5883
|
-
|
|
5988
|
+
log5.info(message);
|
|
5884
5989
|
},
|
|
5885
5990
|
step(message) {
|
|
5886
|
-
|
|
5991
|
+
log5.step(pc5.dim(message));
|
|
5887
5992
|
},
|
|
5888
5993
|
success(message) {
|
|
5889
|
-
|
|
5994
|
+
log5.success(message);
|
|
5890
5995
|
},
|
|
5891
5996
|
warn(message) {
|
|
5892
|
-
|
|
5997
|
+
log5.warn(message);
|
|
5893
5998
|
}
|
|
5894
5999
|
};
|
|
5895
6000
|
|
|
@@ -5936,7 +6041,7 @@ function parseArgs4(argv) {
|
|
|
5936
6041
|
}
|
|
5937
6042
|
function run(command, args, cwd) {
|
|
5938
6043
|
return new Promise((resolvePromise) => {
|
|
5939
|
-
const child =
|
|
6044
|
+
const child = spawn2(command, args, {
|
|
5940
6045
|
cwd,
|
|
5941
6046
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5942
6047
|
shell: process.platform === "win32"
|
|
@@ -5971,7 +6076,10 @@ function runPnpmInstall(cwd) {
|
|
|
5971
6076
|
async function initGitRepo(cwd) {
|
|
5972
6077
|
const inside = await run("git", ["rev-parse", "--is-inside-work-tree"], cwd);
|
|
5973
6078
|
if (inside.ok) {
|
|
5974
|
-
|
|
6079
|
+
const ignored = await run("git", ["check-ignore", "--quiet", "."], cwd);
|
|
6080
|
+
if (!ignored.ok) {
|
|
6081
|
+
return { created: false };
|
|
6082
|
+
}
|
|
5975
6083
|
}
|
|
5976
6084
|
const result = await run("git", ["init"], cwd);
|
|
5977
6085
|
if (!result.ok) {
|
|
@@ -5992,7 +6100,7 @@ function bySkillOrder(a, b) {
|
|
|
5992
6100
|
}
|
|
5993
6101
|
async function linkAgentSkills(target) {
|
|
5994
6102
|
const result = { linked: [], conflicts: [], failures: [] };
|
|
5995
|
-
const skillsDir =
|
|
6103
|
+
const skillsDir = join15(target, ".agents", "skills");
|
|
5996
6104
|
let names;
|
|
5997
6105
|
try {
|
|
5998
6106
|
names = await readDirNames(skillsDir);
|
|
@@ -6001,8 +6109,8 @@ async function linkAgentSkills(target) {
|
|
|
6001
6109
|
return result;
|
|
6002
6110
|
}
|
|
6003
6111
|
for (const name of names.toSorted(bySkillOrder)) {
|
|
6004
|
-
const linkAbs =
|
|
6005
|
-
const targetAbs =
|
|
6112
|
+
const linkAbs = join15(target, ".claude", "skills", name);
|
|
6113
|
+
const targetAbs = join15(skillsDir, name);
|
|
6006
6114
|
let state;
|
|
6007
6115
|
try {
|
|
6008
6116
|
state = await classifyLink(linkAbs, targetAbs);
|
|
@@ -6160,7 +6268,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
|
|
|
6160
6268
|
);
|
|
6161
6269
|
}
|
|
6162
6270
|
let installed = false;
|
|
6163
|
-
const wantsInstall = noInstall ? false : await
|
|
6271
|
+
const wantsInstall = noInstall ? false : await select2({
|
|
6164
6272
|
message: "Install dependencies now?",
|
|
6165
6273
|
options: [
|
|
6166
6274
|
{ value: true, label: `Yes, run ${pc6.cyan("pnpm install")}` },
|
|
@@ -6195,7 +6303,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
|
|
|
6195
6303
|
// owner who never opens AGENTS.md would otherwise not know they exist.
|
|
6196
6304
|
// Pad to the same column the commands above comment at (`pnpm dev` + 21 spaces).
|
|
6197
6305
|
...skills.linked.map(
|
|
6198
|
-
(name) => `${`/${name}`.padEnd(29)}${pc6.dim("# in
|
|
6306
|
+
(name) => `${`/${name}`.padEnd(29)}${pc6.dim("# in your AI coding tool")}`
|
|
6199
6307
|
)
|
|
6200
6308
|
].filter((line) => line !== null).map((line) => pc6.cyan(line)).join("\n");
|
|
6201
6309
|
note4(steps, "Next steps");
|
|
@@ -6205,7 +6313,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
|
|
|
6205
6313
|
|
|
6206
6314
|
// src/commands/list.ts
|
|
6207
6315
|
import { cancel as cancel5, intro as intro5, note as note5, outro as outro5 } from "@clack/prompts";
|
|
6208
|
-
import { join as
|
|
6316
|
+
import { join as join16 } from "path";
|
|
6209
6317
|
import pc7 from "picocolors";
|
|
6210
6318
|
var KNOWN_FLAGS5 = /* @__PURE__ */ new Set(["--installed", "--available", "--help", "-h"]);
|
|
6211
6319
|
var USAGE5 = "saasaloy list [<owner/repo[@ref]>] [--installed] [--available]";
|
|
@@ -6238,7 +6346,7 @@ function parseArgs5(argv) {
|
|
|
6238
6346
|
}
|
|
6239
6347
|
async function localProject() {
|
|
6240
6348
|
const root = await findProjectRoot();
|
|
6241
|
-
if (!await pathExists(
|
|
6349
|
+
if (!await pathExists(join16(root, CONFIG_FILE))) {
|
|
6242
6350
|
return { installed: /* @__PURE__ */ new Set() };
|
|
6243
6351
|
}
|
|
6244
6352
|
const config = await loadConfig(root);
|
|
@@ -6338,21 +6446,21 @@ async function runList(argv) {
|
|
|
6338
6446
|
}
|
|
6339
6447
|
|
|
6340
6448
|
// src/commands/new.ts
|
|
6341
|
-
import { join as
|
|
6449
|
+
import { join as join18, relative as relative4, resolve as resolve7 } from "path";
|
|
6342
6450
|
import {
|
|
6343
6451
|
cancel as cancel6,
|
|
6344
6452
|
intro as intro6,
|
|
6345
6453
|
isCancel as isCancel4,
|
|
6346
6454
|
note as note6,
|
|
6347
6455
|
outro as outro6,
|
|
6348
|
-
select as
|
|
6456
|
+
select as select3,
|
|
6349
6457
|
text as text3
|
|
6350
6458
|
} from "@clack/prompts";
|
|
6351
6459
|
import pc8 from "picocolors";
|
|
6352
6460
|
|
|
6353
6461
|
// src/lib/new-module.ts
|
|
6354
6462
|
import { mkdir as mkdir7, writeFile as writeFile9 } from "fs/promises";
|
|
6355
|
-
import { dirname as dirname9, join as
|
|
6463
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
6356
6464
|
var MODULE_TYPES = [
|
|
6357
6465
|
"saasaloy:capability",
|
|
6358
6466
|
"saasaloy:feature"
|
|
@@ -6456,7 +6564,7 @@ function moduleFiles(spec) {
|
|
|
6456
6564
|
async function writeModule(dir, spec) {
|
|
6457
6565
|
const files = moduleFiles(spec);
|
|
6458
6566
|
for (const file of files) {
|
|
6459
|
-
const abs =
|
|
6567
|
+
const abs = join17(dir, ...file.path.split("/"));
|
|
6460
6568
|
await mkdir7(dirname9(abs), { recursive: true });
|
|
6461
6569
|
await writeFile9(abs, file.content, "utf-8");
|
|
6462
6570
|
}
|
|
@@ -6544,7 +6652,7 @@ async function runNew(argv) {
|
|
|
6544
6652
|
}
|
|
6545
6653
|
try {
|
|
6546
6654
|
const projectRoot = await findProjectRoot();
|
|
6547
|
-
if (await pathExists(
|
|
6655
|
+
if (await pathExists(join18(projectRoot, CONFIG_FILE))) {
|
|
6548
6656
|
cancel6(
|
|
6549
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.`
|
|
6550
6658
|
);
|
|
@@ -6572,7 +6680,7 @@ async function runNew(argv) {
|
|
|
6572
6680
|
return EXIT_REFUSED;
|
|
6573
6681
|
}
|
|
6574
6682
|
const registryDir = resolve7(process.cwd(), REGISTRY_DIR);
|
|
6575
|
-
const dir =
|
|
6683
|
+
const dir = join18(registryDir, name);
|
|
6576
6684
|
if (await pathExists(dir)) {
|
|
6577
6685
|
cancel6(
|
|
6578
6686
|
`${pc8.cyan(`${REGISTRY_DIR}/${name}`)} already exists \u2014 pick another name, or edit it by hand. Nothing was written.`
|
|
@@ -6589,7 +6697,7 @@ async function runNew(argv) {
|
|
|
6589
6697
|
}
|
|
6590
6698
|
type = opts.type;
|
|
6591
6699
|
} else if (isInteractive()) {
|
|
6592
|
-
const picked = await
|
|
6700
|
+
const picked = await select3({
|
|
6593
6701
|
message: "Which tier is this?",
|
|
6594
6702
|
options: MODULE_TYPES.map((value) => ({
|
|
6595
6703
|
hint: TYPE_HINTS[value],
|
|
@@ -6662,16 +6770,16 @@ async function runNew(argv) {
|
|
|
6662
6770
|
}
|
|
6663
6771
|
|
|
6664
6772
|
// src/commands/outdated.ts
|
|
6665
|
-
import { cancel as cancel7, intro as intro7, log as
|
|
6773
|
+
import { cancel as cancel7, intro as intro7, log as log6, note as note7, outro as outro7 } from "@clack/prompts";
|
|
6666
6774
|
import pc9 from "picocolors";
|
|
6667
6775
|
|
|
6668
6776
|
// src/lib/updater.ts
|
|
6669
6777
|
import { mkdir as mkdir8, readFile as readFile18, rm as rmPath2, writeFile as writeFile11 } from "fs/promises";
|
|
6670
|
-
import { dirname as dirname11, posix as
|
|
6778
|
+
import { dirname as dirname11, posix as posix7 } from "path";
|
|
6671
6779
|
|
|
6672
6780
|
// src/lib/remover.ts
|
|
6673
6781
|
import { readdir as readdir5, readFile as readFile17, rm as rmPath, writeFile as writeFile10 } from "fs/promises";
|
|
6674
|
-
import { dirname as dirname10, join as
|
|
6782
|
+
import { dirname as dirname10, join as join19, relative as relative5, sep as sep3 } from "path";
|
|
6675
6783
|
function classifyTrackedFile(content, manifestHash) {
|
|
6676
6784
|
if (content === void 0) {
|
|
6677
6785
|
return "missing";
|
|
@@ -6815,7 +6923,7 @@ async function pruneEmptyDirs(root, deletedAbsPaths) {
|
|
|
6815
6923
|
async function dropDanglingAliases(root, config) {
|
|
6816
6924
|
const dropped = [];
|
|
6817
6925
|
for (const [alias, prefix] of Object.entries(config.aliases)) {
|
|
6818
|
-
const prefixAbs =
|
|
6926
|
+
const prefixAbs = join19(root, ...prefix.split("/"));
|
|
6819
6927
|
if (!await pathExists(prefixAbs)) {
|
|
6820
6928
|
delete config.aliases[alias];
|
|
6821
6929
|
dropped.push(alias);
|
|
@@ -7352,9 +7460,9 @@ function classifyUpdate(base, theirs, mine, managedHash, adopted, projectOwned)
|
|
|
7352
7460
|
async function planLinks(root, theirs) {
|
|
7353
7461
|
const links = [];
|
|
7354
7462
|
for (const skillRel of theirs.item.agent?.skills ?? []) {
|
|
7355
|
-
const folderName =
|
|
7356
|
-
const path =
|
|
7357
|
-
const target =
|
|
7463
|
+
const folderName = posix7.basename(skillRel);
|
|
7464
|
+
const path = posix7.join(".claude/skills", folderName);
|
|
7465
|
+
const target = posix7.join(".agents/skills", folderName);
|
|
7358
7466
|
const pathAbs = resolveWithinRoot(root, path);
|
|
7359
7467
|
const targetAbs = resolveWithinRoot(root, target);
|
|
7360
7468
|
const state = await classifyLink(pathAbs, targetAbs);
|
|
@@ -7708,7 +7816,7 @@ async function runOutdated(argv) {
|
|
|
7708
7816
|
const manifest = await loadManifest(root);
|
|
7709
7817
|
const registryOverride = !!process.env[REGISTRY_ENV];
|
|
7710
7818
|
if (registryOverride && config.installed.length > 0) {
|
|
7711
|
-
|
|
7819
|
+
log6.warn(
|
|
7712
7820
|
`${REGISTRY_ENV} is set, so every module reads as local \u2014 unset it to compare against the registry.`
|
|
7713
7821
|
);
|
|
7714
7822
|
}
|
|
@@ -7789,10 +7897,10 @@ import {
|
|
|
7789
7897
|
confirm as confirm2,
|
|
7790
7898
|
intro as intro8,
|
|
7791
7899
|
isCancel as isCancel5,
|
|
7792
|
-
log as
|
|
7900
|
+
log as log7,
|
|
7793
7901
|
note as note8,
|
|
7794
7902
|
outro as outro8,
|
|
7795
|
-
select as
|
|
7903
|
+
select as select4
|
|
7796
7904
|
} from "@clack/prompts";
|
|
7797
7905
|
import pc10 from "picocolors";
|
|
7798
7906
|
var KNOWN_FLAGS8 = /* @__PURE__ */ new Set([
|
|
@@ -7891,12 +7999,12 @@ function summarizeRemovePlan(plan, name, yes) {
|
|
|
7891
7999
|
note8(wrapForNote(linkLines.join("\n")), "Skill links");
|
|
7892
8000
|
}
|
|
7893
8001
|
if (plan.dependents.length > 0) {
|
|
7894
|
-
|
|
8002
|
+
log7.warn(
|
|
7895
8003
|
`${pc10.cyan(name)} is still depended on by ${plan.dependents.join(", ")} \u2014 proceeding anyway (--force).`
|
|
7896
8004
|
);
|
|
7897
8005
|
}
|
|
7898
8006
|
for (const missingLock of plan.missingLockEntries) {
|
|
7899
|
-
|
|
8007
|
+
log7.warn(
|
|
7900
8008
|
`Installed module ${pc10.cyan(missingLock)} has no lock entry \u2014 dependent detection is incomplete for it.`
|
|
7901
8009
|
);
|
|
7902
8010
|
}
|
|
@@ -7912,7 +8020,7 @@ function summarizeRemovePlan(plan, name, yes) {
|
|
|
7912
8020
|
if (p.action !== "drop") {
|
|
7913
8021
|
continue;
|
|
7914
8022
|
}
|
|
7915
|
-
|
|
8023
|
+
log7.warn(
|
|
7916
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.`
|
|
7917
8025
|
);
|
|
7918
8026
|
}
|
|
@@ -7953,7 +8061,7 @@ async function runRemove(argv) {
|
|
|
7953
8061
|
);
|
|
7954
8062
|
return EXIT_REFUSED;
|
|
7955
8063
|
}
|
|
7956
|
-
const picked = await
|
|
8064
|
+
const picked = await select4({
|
|
7957
8065
|
message: "Pick a module to remove",
|
|
7958
8066
|
options: config.installed.map((n) => ({ value: n, label: n }))
|
|
7959
8067
|
});
|
|
@@ -7978,7 +8086,7 @@ async function runRemove(argv) {
|
|
|
7978
8086
|
}
|
|
7979
8087
|
summarizeRemovePlan(plan, name, opts.yes);
|
|
7980
8088
|
for (const warning of plan.warnings) {
|
|
7981
|
-
|
|
8089
|
+
log7.warn(warning);
|
|
7982
8090
|
}
|
|
7983
8091
|
if (opts.diff) {
|
|
7984
8092
|
for (const file of plan.files) {
|
|
@@ -8053,24 +8161,28 @@ async function runRemove(argv) {
|
|
|
8053
8161
|
throw failedSave.error;
|
|
8054
8162
|
}
|
|
8055
8163
|
for (const file of result.deleted) {
|
|
8056
|
-
|
|
8164
|
+
log7.step(`${pc10.red("delete")} ${file.target}`);
|
|
8057
8165
|
}
|
|
8058
8166
|
for (const file of result.missingUntracked) {
|
|
8059
|
-
|
|
8167
|
+
log7.step(
|
|
8060
8168
|
`${pc10.dim("untrack")} ${file.target} ${pc10.dim("(already gone)")}`
|
|
8061
8169
|
);
|
|
8062
8170
|
}
|
|
8063
8171
|
for (const link of result.linksRemoved) {
|
|
8064
|
-
|
|
8172
|
+
log7.step(`${pc10.red("unlink")} ${link.path}`);
|
|
8065
8173
|
}
|
|
8066
8174
|
for (const link of result.linkConflicts) {
|
|
8067
|
-
|
|
8175
|
+
log7.warn(
|
|
8068
8176
|
`Skill link ${pc10.cyan(link.path)} left untouched \u2014 not ours to remove.`
|
|
8069
8177
|
);
|
|
8070
8178
|
}
|
|
8071
8179
|
for (const p of result.patchesReversed) {
|
|
8072
|
-
|
|
8180
|
+
log7.step(`${pc10.red("revert")} ${p.file} ${pc10.dim(`(${p.patch.kind})`)}`);
|
|
8073
8181
|
}
|
|
8182
|
+
await formatPatchedFiles(
|
|
8183
|
+
root,
|
|
8184
|
+
result.patchesReversed.map((p) => p.file)
|
|
8185
|
+
);
|
|
8074
8186
|
const refusals = new Map(
|
|
8075
8187
|
result.patchRefusals.map((r) => [r.patch, r.reason])
|
|
8076
8188
|
);
|
|
@@ -8079,15 +8191,15 @@ async function runRemove(argv) {
|
|
|
8079
8191
|
continue;
|
|
8080
8192
|
}
|
|
8081
8193
|
const reason = refusals.get(p);
|
|
8082
|
-
|
|
8194
|
+
log7.warn(
|
|
8083
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}.`
|
|
8084
8196
|
);
|
|
8085
8197
|
}
|
|
8086
8198
|
for (const dir of result.prunedDirs) {
|
|
8087
|
-
|
|
8199
|
+
log7.step(`${pc10.dim("prune")} ${dir}`);
|
|
8088
8200
|
}
|
|
8089
8201
|
for (const alias of result.droppedAliases) {
|
|
8090
|
-
|
|
8202
|
+
log7.warn(
|
|
8091
8203
|
`Alias ${pc10.cyan(alias)} dropped \u2014 its target directory is gone.`
|
|
8092
8204
|
);
|
|
8093
8205
|
}
|
|
@@ -8128,13 +8240,13 @@ ${pc10.dim("Hand-edited since \u2014 left on disk, now untracked.")}`
|
|
|
8128
8240
|
|
|
8129
8241
|
// src/commands/update.ts
|
|
8130
8242
|
import { mkdir as mkdir10, realpath, writeFile as writeFile13 } from "fs/promises";
|
|
8131
|
-
import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as
|
|
8243
|
+
import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join21, resolve as resolve8 } from "path";
|
|
8132
8244
|
import {
|
|
8133
8245
|
cancel as cancel9,
|
|
8134
8246
|
confirm as confirm3,
|
|
8135
8247
|
intro as intro9,
|
|
8136
8248
|
isCancel as isCancel6,
|
|
8137
|
-
log as
|
|
8249
|
+
log as log8,
|
|
8138
8250
|
note as note9,
|
|
8139
8251
|
outro as outro9
|
|
8140
8252
|
} from "@clack/prompts";
|
|
@@ -8149,14 +8261,14 @@ import {
|
|
|
8149
8261
|
rm as rmPath3,
|
|
8150
8262
|
writeFile as writeFile12
|
|
8151
8263
|
} from "fs/promises";
|
|
8152
|
-
import { dirname as dirname12, join as
|
|
8153
|
-
var BACKUP_DIR =
|
|
8264
|
+
import { dirname as dirname12, join as join20 } from "path";
|
|
8265
|
+
var BACKUP_DIR = join20(".saasaloy", "backups");
|
|
8154
8266
|
var RECORD_FILE = "backup.json";
|
|
8155
8267
|
var STATE_FILES = [
|
|
8156
8268
|
"saasaloy.json",
|
|
8157
8269
|
"saasaloy-lock.json",
|
|
8158
8270
|
"package.json",
|
|
8159
|
-
|
|
8271
|
+
join20(".saasaloy", "manifest.json")
|
|
8160
8272
|
];
|
|
8161
8273
|
function stampName(now) {
|
|
8162
8274
|
return now.toISOString().replaceAll(/[:.]/g, "-");
|
|
@@ -8164,8 +8276,8 @@ function stampName(now) {
|
|
|
8164
8276
|
async function writeBackup(args) {
|
|
8165
8277
|
const { root, targets, cliVersion } = args;
|
|
8166
8278
|
const createdAt = (args.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8167
|
-
const rel =
|
|
8168
|
-
const dir =
|
|
8279
|
+
const rel = join20(BACKUP_DIR, stampName(args.now ?? new Date(createdAt)));
|
|
8280
|
+
const dir = join20(root, rel);
|
|
8169
8281
|
const files = [];
|
|
8170
8282
|
const absent = [];
|
|
8171
8283
|
for (const target of /* @__PURE__ */ new Set([...targets, ...STATE_FILES])) {
|
|
@@ -8174,7 +8286,7 @@ async function writeBackup(args) {
|
|
|
8174
8286
|
absent.push(target);
|
|
8175
8287
|
continue;
|
|
8176
8288
|
}
|
|
8177
|
-
const destination =
|
|
8289
|
+
const destination = join20(dir, target);
|
|
8178
8290
|
await mkdir9(dirname12(destination), { recursive: true });
|
|
8179
8291
|
await copyFile(source, destination);
|
|
8180
8292
|
files.push(target);
|
|
@@ -8187,7 +8299,7 @@ async function writeBackup(args) {
|
|
|
8187
8299
|
};
|
|
8188
8300
|
await mkdir9(dir, { recursive: true });
|
|
8189
8301
|
await writeFile12(
|
|
8190
|
-
|
|
8302
|
+
join20(dir, RECORD_FILE),
|
|
8191
8303
|
`${JSON.stringify(record, null, 2)}
|
|
8192
8304
|
`,
|
|
8193
8305
|
"utf-8"
|
|
@@ -8195,7 +8307,7 @@ async function writeBackup(args) {
|
|
|
8195
8307
|
return rel;
|
|
8196
8308
|
}
|
|
8197
8309
|
async function listBackups(root) {
|
|
8198
|
-
const dir =
|
|
8310
|
+
const dir = join20(root, BACKUP_DIR);
|
|
8199
8311
|
if (!await pathExists(dir)) {
|
|
8200
8312
|
return [];
|
|
8201
8313
|
}
|
|
@@ -8210,25 +8322,25 @@ async function restoreLatestBackup(root) {
|
|
|
8210
8322
|
`No backup to restore \u2014 nothing under ${BACKUP_DIR}. \`saasaloy update\` writes one before it applies a plan.`
|
|
8211
8323
|
);
|
|
8212
8324
|
}
|
|
8213
|
-
const rel =
|
|
8214
|
-
const dir =
|
|
8215
|
-
const raw = await readIfPresent(
|
|
8325
|
+
const rel = join20(BACKUP_DIR, newest);
|
|
8326
|
+
const dir = join20(root, rel);
|
|
8327
|
+
const raw = await readIfPresent(join20(dir, RECORD_FILE));
|
|
8216
8328
|
if (raw === void 0) {
|
|
8217
8329
|
throw new RefusalError(
|
|
8218
|
-
`${
|
|
8330
|
+
`${join20(rel, RECORD_FILE)} is missing \u2014 that backup can't be restored safely.`
|
|
8219
8331
|
);
|
|
8220
8332
|
}
|
|
8221
8333
|
let record;
|
|
8222
8334
|
try {
|
|
8223
8335
|
record = JSON.parse(raw);
|
|
8224
8336
|
} catch (error) {
|
|
8225
|
-
throw new RefusalError(`${
|
|
8337
|
+
throw new RefusalError(`${join20(rel, RECORD_FILE)} is invalid.`, {
|
|
8226
8338
|
cause: error
|
|
8227
8339
|
});
|
|
8228
8340
|
}
|
|
8229
8341
|
const restored = [];
|
|
8230
8342
|
for (const target of record.files) {
|
|
8231
|
-
const source =
|
|
8343
|
+
const source = join20(dir, target);
|
|
8232
8344
|
const destination = resolveWithinRoot(root, target);
|
|
8233
8345
|
if (!await pathExists(source)) {
|
|
8234
8346
|
continue;
|
|
@@ -8670,18 +8782,18 @@ function summarizePlan2(plan) {
|
|
|
8670
8782
|
lines.push(pc11.dim(`${writing} file(s) to apply, ${merging} needing merge`));
|
|
8671
8783
|
note9(wrapForNote(lines.join("\n")), "Plan", TUI_ON_STDERR);
|
|
8672
8784
|
for (const comparison of plan.skipped) {
|
|
8673
|
-
|
|
8785
|
+
log8.info(
|
|
8674
8786
|
`${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison)}`)}`,
|
|
8675
8787
|
TUI_ON_STDERR
|
|
8676
8788
|
);
|
|
8677
8789
|
}
|
|
8678
8790
|
for (const mod of plan.modules) {
|
|
8679
8791
|
for (const conflict of mod.depConflicts) {
|
|
8680
|
-
|
|
8792
|
+
log8.warn(`Dependency version conflict \u2014 ${conflict}.`, TUI_ON_STDERR);
|
|
8681
8793
|
}
|
|
8682
8794
|
for (const patch of mod.patches) {
|
|
8683
8795
|
if (patch.action === "missing") {
|
|
8684
|
-
|
|
8796
|
+
log8.warn(
|
|
8685
8797
|
`Config patch target ${pc11.cyan(patch.file)} is missing \u2014 ${patch.patch.kind} not applied.`,
|
|
8686
8798
|
TUI_ON_STDERR
|
|
8687
8799
|
);
|
|
@@ -8753,7 +8865,7 @@ async function canonicalPath(path) {
|
|
|
8753
8865
|
return await realpath(path);
|
|
8754
8866
|
} catch {
|
|
8755
8867
|
try {
|
|
8756
|
-
return
|
|
8868
|
+
return join21(await realpath(dirname13(path)), basename4(path));
|
|
8757
8869
|
} catch {
|
|
8758
8870
|
return path;
|
|
8759
8871
|
}
|
|
@@ -8770,7 +8882,7 @@ async function emitMergePlan(document, out) {
|
|
|
8770
8882
|
const target = resolvePlanOut(out);
|
|
8771
8883
|
await mkdir10(dirname13(target), { recursive: true });
|
|
8772
8884
|
await writeFile13(target, document, "utf-8");
|
|
8773
|
-
|
|
8885
|
+
log8.success(`Merge plan written to ${pc11.cyan(out)}`, TUI_ON_STDERR);
|
|
8774
8886
|
}
|
|
8775
8887
|
function backupTargets(plan) {
|
|
8776
8888
|
const targets = /* @__PURE__ */ new Set();
|
|
@@ -8827,10 +8939,10 @@ async function runUpdate(argv) {
|
|
|
8827
8939
|
try {
|
|
8828
8940
|
const restored = await restoreLatestBackup(root);
|
|
8829
8941
|
for (const target of restored.restored) {
|
|
8830
|
-
|
|
8942
|
+
log8.step(`${pc11.green("restore")} ${target}`, TUI_ON_STDERR);
|
|
8831
8943
|
}
|
|
8832
8944
|
for (const target of restored.removed) {
|
|
8833
|
-
|
|
8945
|
+
log8.step(
|
|
8834
8946
|
`${pc11.red("remove")} ${target} ${pc11.dim("(the update created it)")}`,
|
|
8835
8947
|
TUI_ON_STDERR
|
|
8836
8948
|
);
|
|
@@ -8897,7 +9009,7 @@ async function runUpdate(argv) {
|
|
|
8897
9009
|
if (wantsBase) {
|
|
8898
9010
|
templateDir = await baseTemplateDir();
|
|
8899
9011
|
if (project.source === "directory") {
|
|
8900
|
-
|
|
9012
|
+
log8.warn(
|
|
8901
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.`,
|
|
8902
9014
|
TUI_ON_STDERR
|
|
8903
9015
|
);
|
|
@@ -8947,7 +9059,7 @@ async function runUpdate(argv) {
|
|
|
8947
9059
|
return EXIT_REFUSED;
|
|
8948
9060
|
}
|
|
8949
9061
|
if (recorded !== cliVersion) {
|
|
8950
|
-
|
|
9062
|
+
log8.info(
|
|
8951
9063
|
`Base recorded at CLI ${pc11.cyan(recorded)}; ${pc11.cyan(cliVersion)} is running.`,
|
|
8952
9064
|
TUI_ON_STDERR
|
|
8953
9065
|
);
|
|
@@ -8989,7 +9101,7 @@ async function runUpdate(argv) {
|
|
|
8989
9101
|
return entry && entry.source !== "local";
|
|
8990
9102
|
});
|
|
8991
9103
|
if (fromRemote.length > 0) {
|
|
8992
|
-
|
|
9104
|
+
log8.warn(
|
|
8993
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")}.`,
|
|
8994
9106
|
TUI_ON_STDERR
|
|
8995
9107
|
);
|
|
@@ -9014,7 +9126,7 @@ async function runUpdate(argv) {
|
|
|
9014
9126
|
}
|
|
9015
9127
|
}
|
|
9016
9128
|
for (const comparison of skipped) {
|
|
9017
|
-
|
|
9129
|
+
log8.info(
|
|
9018
9130
|
`${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison, preview)}`)}`,
|
|
9019
9131
|
TUI_ON_STDERR
|
|
9020
9132
|
);
|
|
@@ -9137,7 +9249,7 @@ async function runUpdate(argv) {
|
|
|
9137
9249
|
}
|
|
9138
9250
|
}
|
|
9139
9251
|
if (missingLockEntries.size > 0) {
|
|
9140
|
-
|
|
9252
|
+
log8.warn(
|
|
9141
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)")}.`,
|
|
9142
9254
|
TUI_ON_STDERR
|
|
9143
9255
|
);
|
|
@@ -9154,9 +9266,9 @@ async function runUpdate(argv) {
|
|
|
9154
9266
|
for (const comparison of skipped) {
|
|
9155
9267
|
const line = `${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison)}`)}`;
|
|
9156
9268
|
if (comparison.status === "unresolvable") {
|
|
9157
|
-
|
|
9269
|
+
log8.warn(line, TUI_ON_STDERR);
|
|
9158
9270
|
} else {
|
|
9159
|
-
|
|
9271
|
+
log8.info(line, TUI_ON_STDERR);
|
|
9160
9272
|
}
|
|
9161
9273
|
}
|
|
9162
9274
|
outro9(pc11.yellow("Nothing could be updated."), TUI_ON_STDERR);
|
|
@@ -9164,7 +9276,7 @@ async function runUpdate(argv) {
|
|
|
9164
9276
|
}
|
|
9165
9277
|
const pkg = await readRootPackageJson(root);
|
|
9166
9278
|
if (!pkg) {
|
|
9167
|
-
|
|
9279
|
+
log8.warn(
|
|
9168
9280
|
"No package.json at the project root \u2014 dependency pins won't be updated.",
|
|
9169
9281
|
TUI_ON_STDERR
|
|
9170
9282
|
);
|
|
@@ -9224,7 +9336,7 @@ async function runUpdate(argv) {
|
|
|
9224
9336
|
targets: backupTargets(plan),
|
|
9225
9337
|
cliVersion
|
|
9226
9338
|
});
|
|
9227
|
-
|
|
9339
|
+
log8.info(
|
|
9228
9340
|
`Pre-update copies in ${pc11.cyan(backupDir)} ${pc11.dim("\u2014 `saasaloy update --abort` restores them")}`,
|
|
9229
9341
|
TUI_ON_STDERR
|
|
9230
9342
|
);
|
|
@@ -9248,6 +9360,11 @@ async function runUpdate(argv) {
|
|
|
9248
9360
|
}
|
|
9249
9361
|
const result = outcome.value;
|
|
9250
9362
|
reportResult(result);
|
|
9363
|
+
await formatPatchedFiles(
|
|
9364
|
+
root,
|
|
9365
|
+
result.patched.map((p) => p.file),
|
|
9366
|
+
TUI_ON_STDERR
|
|
9367
|
+
);
|
|
9251
9368
|
const document = renderMergePlan(plan);
|
|
9252
9369
|
if (document) {
|
|
9253
9370
|
await emitMergePlan(document, opts.out);
|
|
@@ -9297,37 +9414,37 @@ function summarizeOutcome(plan, result) {
|
|
|
9297
9414
|
}
|
|
9298
9415
|
function reportResult(result) {
|
|
9299
9416
|
for (const name of result.prereqsInstalled) {
|
|
9300
|
-
|
|
9417
|
+
log8.step(
|
|
9301
9418
|
`${pc11.green("install")} ${pc11.cyan(name)} ${pc11.dim("(new prerequisite)")}`,
|
|
9302
9419
|
TUI_ON_STDERR
|
|
9303
9420
|
);
|
|
9304
9421
|
}
|
|
9305
9422
|
for (const file of result.written) {
|
|
9306
|
-
|
|
9423
|
+
log8.step(`${ACTION_LABEL2[file.action]} ${file.target}`, TUI_ON_STDERR);
|
|
9307
9424
|
}
|
|
9308
9425
|
for (const file of result.deleted) {
|
|
9309
|
-
|
|
9426
|
+
log8.step(`${pc11.red("delete")} ${file.target}`, TUI_ON_STDERR);
|
|
9310
9427
|
}
|
|
9311
9428
|
for (const file of result.untracked) {
|
|
9312
|
-
|
|
9429
|
+
log8.step(
|
|
9313
9430
|
`${pc11.dim("untrack")} ${file.target} ${pc11.dim("(already gone)")}`,
|
|
9314
9431
|
TUI_ON_STDERR
|
|
9315
9432
|
);
|
|
9316
9433
|
}
|
|
9317
9434
|
for (const patch of result.patched) {
|
|
9318
|
-
|
|
9435
|
+
log8.step(
|
|
9319
9436
|
`${pc11.green("patch")} ${patch.file} ${pc11.dim(`\u2014 ${patch.patch.kind}`)}`,
|
|
9320
9437
|
TUI_ON_STDERR
|
|
9321
9438
|
);
|
|
9322
9439
|
}
|
|
9323
9440
|
for (const link of result.linkConflicts) {
|
|
9324
|
-
|
|
9441
|
+
log8.warn(
|
|
9325
9442
|
`Skill link ${pc11.cyan(link.path)} left untouched \u2014 not ours to replace.`,
|
|
9326
9443
|
TUI_ON_STDERR
|
|
9327
9444
|
);
|
|
9328
9445
|
}
|
|
9329
9446
|
for (const dep of result.depsWritten) {
|
|
9330
|
-
|
|
9447
|
+
log8.step(`${pc11.cyan("dep")} ${dep.name}@${dep.version}`, TUI_ON_STDERR);
|
|
9331
9448
|
}
|
|
9332
9449
|
if (result.lateDrift.length > 0) {
|
|
9333
9450
|
const lines = result.lateDrift.map((f) => ` ${pc11.yellow("kept")} ${f.target}`).join("\n");
|
|
@@ -9407,8 +9524,8 @@ ${pc12.dim("Set SAASALOY_DEBUG=1 to print the full cause chain behind a failure.
|
|
|
9407
9524
|
);
|
|
9408
9525
|
}
|
|
9409
9526
|
async function pickCommand(deps) {
|
|
9410
|
-
const { registry, select:
|
|
9411
|
-
const picked = await
|
|
9527
|
+
const { registry, select: select5, isCancel: isCancel7 } = deps;
|
|
9528
|
+
const picked = await select5({
|
|
9412
9529
|
message: "What would you like to do?",
|
|
9413
9530
|
options: Object.entries(registry).map(([name, command2]) => ({
|
|
9414
9531
|
value: name,
|
|
@@ -9429,7 +9546,7 @@ async function pickCommand(deps) {
|
|
|
9429
9546
|
async function main(argv, deps = {}) {
|
|
9430
9547
|
const {
|
|
9431
9548
|
registry = COMMANDS,
|
|
9432
|
-
select:
|
|
9549
|
+
select: select5 = clackSelect,
|
|
9433
9550
|
isCancel: isCancel7 = clackIsCancel
|
|
9434
9551
|
} = deps;
|
|
9435
9552
|
const [name, ...rest] = argv;
|
|
@@ -9446,7 +9563,7 @@ async function main(argv, deps = {}) {
|
|
|
9446
9563
|
printHelp(registry);
|
|
9447
9564
|
return EXIT_OK;
|
|
9448
9565
|
}
|
|
9449
|
-
return pickCommand({ registry, select:
|
|
9566
|
+
return pickCommand({ registry, select: select5, isCancel: isCancel7 });
|
|
9450
9567
|
}
|
|
9451
9568
|
const command = Object.hasOwn(registry, name) ? registry[name] : void 0;
|
|
9452
9569
|
if (!command) {
|