saasaloy 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/index.js +637 -522
- package/dist/index.js.map +1 -1
- package/package.json +1 -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 +36 -6
- package/templates/base/apps/web/package.json +2 -1
- 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/oxlint.config.mjs +91 -4
- package/templates/base/package.json +7 -3
- 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 +1 -0
- 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 +140 -56
- 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",
|
|
@@ -4259,7 +4466,7 @@ var TEMPLATE_DIR_CANDIDATES = ["../templates/base", "../../templates/base"];
|
|
|
4259
4466
|
async function baseTemplateDir() {
|
|
4260
4467
|
for (const candidate of TEMPLATE_DIR_CANDIDATES) {
|
|
4261
4468
|
const dir = fileURLToPath3(new URL(candidate, import.meta.url));
|
|
4262
|
-
if (await pathExists(
|
|
4469
|
+
if (await pathExists(join10(dir, "package.json"))) {
|
|
4263
4470
|
return dir;
|
|
4264
4471
|
}
|
|
4265
4472
|
}
|
|
@@ -4282,11 +4489,11 @@ async function copyTree(srcDir, destDir, vars, options, fromPrefix, targetPrefix
|
|
|
4282
4489
|
if (isRoot && entry.name === BASE_DECLARATION) {
|
|
4283
4490
|
continue;
|
|
4284
4491
|
}
|
|
4285
|
-
const srcPath =
|
|
4492
|
+
const srcPath = join10(srcDir, entry.name);
|
|
4286
4493
|
const outName = renderedName(entry.name);
|
|
4287
|
-
const destPath =
|
|
4288
|
-
const from =
|
|
4289
|
-
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);
|
|
4290
4497
|
if (entry.isDirectory()) {
|
|
4291
4498
|
written.push(
|
|
4292
4499
|
...await copyTree(
|
|
@@ -4336,9 +4543,9 @@ function ownedMatcher(declaration) {
|
|
|
4336
4543
|
return (target) => patterns.some((re) => re.test(target));
|
|
4337
4544
|
}
|
|
4338
4545
|
async function readBaseDeclaration(templateDir) {
|
|
4339
|
-
const raw = await readIfPresent(
|
|
4546
|
+
const raw = await readIfPresent(join11(templateDir, BASE_DECLARATION));
|
|
4340
4547
|
if (raw === void 0) {
|
|
4341
|
-
return { seedFiles: [], ownedFiles: [] };
|
|
4548
|
+
return { seedFiles: [], retiredSeedFiles: [], ownedFiles: [] };
|
|
4342
4549
|
}
|
|
4343
4550
|
const parsed = JSON.parse(raw);
|
|
4344
4551
|
const seedFiles = parsed.seedFiles;
|
|
@@ -4347,6 +4554,14 @@ async function readBaseDeclaration(templateDir) {
|
|
|
4347
4554
|
`${BASE_DECLARATION} must declare "seedFiles" as a list of project-relative paths.`
|
|
4348
4555
|
);
|
|
4349
4556
|
}
|
|
4557
|
+
const retiredSeedFiles = parsed.retiredSeedFiles ?? [];
|
|
4558
|
+
if (!Array.isArray(retiredSeedFiles) || !retiredSeedFiles.every(
|
|
4559
|
+
(entry) => typeof entry === "string" && entry !== ""
|
|
4560
|
+
)) {
|
|
4561
|
+
throw new Error(
|
|
4562
|
+
`${BASE_DECLARATION} must declare "retiredSeedFiles" as a list of project-relative paths.`
|
|
4563
|
+
);
|
|
4564
|
+
}
|
|
4350
4565
|
const ownedFiles = parsed.ownedFiles ?? [];
|
|
4351
4566
|
if (!Array.isArray(ownedFiles) || !ownedFiles.every((entry) => typeof entry === "string" && entry !== "")) {
|
|
4352
4567
|
throw new Error(
|
|
@@ -4354,7 +4569,8 @@ async function readBaseDeclaration(templateDir) {
|
|
|
4354
4569
|
);
|
|
4355
4570
|
}
|
|
4356
4571
|
return {
|
|
4357
|
-
seedFiles,
|
|
4572
|
+
seedFiles: [...seedFiles, ...retiredSeedFiles],
|
|
4573
|
+
retiredSeedFiles,
|
|
4358
4574
|
ownedFiles
|
|
4359
4575
|
};
|
|
4360
4576
|
}
|
|
@@ -4365,9 +4581,9 @@ async function listTemplateFiles(templateDir) {
|
|
|
4365
4581
|
if (isRoot && entry.name === BASE_DECLARATION) {
|
|
4366
4582
|
continue;
|
|
4367
4583
|
}
|
|
4368
|
-
const from =
|
|
4369
|
-
const target =
|
|
4370
|
-
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);
|
|
4371
4587
|
if (entry.isDirectory()) {
|
|
4372
4588
|
await walk(abs, from, target, false);
|
|
4373
4589
|
} else if (entry.isFile()) {
|
|
@@ -4441,7 +4657,7 @@ async function missingBaseTargets(root, manifest, templateDir) {
|
|
|
4441
4657
|
return missing.toSorted((a, b) => a < b ? -1 : 1);
|
|
4442
4658
|
}
|
|
4443
4659
|
async function renderTemplate(root, templateDir, projectName) {
|
|
4444
|
-
const dir = await mkdtemp2(
|
|
4660
|
+
const dir = await mkdtemp2(join11(tmpdir2(), "saasaloy-base-render-"));
|
|
4445
4661
|
const files = await copyTemplate(
|
|
4446
4662
|
templateDir,
|
|
4447
4663
|
dir,
|
|
@@ -4557,7 +4773,7 @@ async function baseUpdateInput(args) {
|
|
|
4557
4773
|
import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
|
|
4558
4774
|
import { parseModule as parseModule7 } from "magicast";
|
|
4559
4775
|
import { readFile as readFile13 } from "fs/promises";
|
|
4560
|
-
import { basename, dirname as dirname6, join as
|
|
4776
|
+
import { basename, dirname as dirname6, join as join12, posix as posix6, resolve as resolve3 } from "path";
|
|
4561
4777
|
var SKILL_PREFIX = "saasaloy-";
|
|
4562
4778
|
var PINNED_DEP = /^(@[^/@]+\/)?[^/@]+@\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
|
|
4563
4779
|
function finding(module, where, message) {
|
|
@@ -4572,7 +4788,7 @@ function asRecord(value) {
|
|
|
4572
4788
|
async function registryModuleNames(dir) {
|
|
4573
4789
|
const names = [];
|
|
4574
4790
|
for (const name of await readDirNames(dir)) {
|
|
4575
|
-
if (await pathExists(
|
|
4791
|
+
if (await pathExists(join12(dir, name, "registry-item.json"))) {
|
|
4576
4792
|
names.push(name);
|
|
4577
4793
|
}
|
|
4578
4794
|
}
|
|
@@ -4581,12 +4797,12 @@ async function registryModuleNames(dir) {
|
|
|
4581
4797
|
async function collectAliases(registryDir, names) {
|
|
4582
4798
|
const templateDir = await baseTemplateDir();
|
|
4583
4799
|
let base = {};
|
|
4584
|
-
if (await pathExists(
|
|
4800
|
+
if (await pathExists(join12(templateDir, "saasaloy.json"))) {
|
|
4585
4801
|
base = (await loadConfig(templateDir)).aliases;
|
|
4586
4802
|
}
|
|
4587
4803
|
const fromScaffolds = {};
|
|
4588
4804
|
for (const name of names) {
|
|
4589
|
-
const item = asRecord(await readDescriptor(
|
|
4805
|
+
const item = asRecord(await readDescriptor(join12(registryDir, name)));
|
|
4590
4806
|
for (const scaffold of asArray2(item.scaffolds)) {
|
|
4591
4807
|
const aliases = asRecord(asRecord(scaffold).aliases);
|
|
4592
4808
|
for (const [alias, prefix] of Object.entries(aliases)) {
|
|
@@ -4599,7 +4815,7 @@ async function collectAliases(registryDir, names) {
|
|
|
4599
4815
|
return { base, fromScaffolds };
|
|
4600
4816
|
}
|
|
4601
4817
|
async function readDescriptor(dir) {
|
|
4602
|
-
const file =
|
|
4818
|
+
const file = join12(dir, "registry-item.json");
|
|
4603
4819
|
if (!await pathExists(file)) {
|
|
4604
4820
|
return void 0;
|
|
4605
4821
|
}
|
|
@@ -4614,7 +4830,7 @@ async function checkModule(input) {
|
|
|
4614
4830
|
const module = basename(dir);
|
|
4615
4831
|
const findings = [];
|
|
4616
4832
|
const report = () => ({ dir, findings, module });
|
|
4617
|
-
const file =
|
|
4833
|
+
const file = join12(dir, "registry-item.json");
|
|
4618
4834
|
if (!await pathExists(file)) {
|
|
4619
4835
|
findings.push(
|
|
4620
4836
|
finding(module, "(root)", `no registry-item.json in ${module}/`)
|
|
@@ -4732,6 +4948,28 @@ async function checkModule(input) {
|
|
|
4732
4948
|
);
|
|
4733
4949
|
}
|
|
4734
4950
|
}
|
|
4951
|
+
for (const key of Object.keys(asRecord(item.envServices))) {
|
|
4952
|
+
if (key !== "default" && !(key in asRecord(item.envVars))) {
|
|
4953
|
+
findings.push(
|
|
4954
|
+
finding(
|
|
4955
|
+
module,
|
|
4956
|
+
`/envServices/${key}`,
|
|
4957
|
+
"has no matching entry in envVars, so it routes nothing"
|
|
4958
|
+
)
|
|
4959
|
+
);
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
for (const key of asArray2(item.envOptional)) {
|
|
4963
|
+
if (typeof key === "string" && !(key in asRecord(item.envVars))) {
|
|
4964
|
+
findings.push(
|
|
4965
|
+
finding(
|
|
4966
|
+
module,
|
|
4967
|
+
`/envOptional/${key}`,
|
|
4968
|
+
"has no matching entry in envVars, so nothing marks it blank on purpose"
|
|
4969
|
+
)
|
|
4970
|
+
);
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4735
4973
|
for (const key of Object.keys(asRecord(item.devVars))) {
|
|
4736
4974
|
if (!(key in asRecord(item.envVars))) {
|
|
4737
4975
|
findings.push(
|
|
@@ -4747,7 +4985,7 @@ async function checkModule(input) {
|
|
|
4747
4985
|
if (typeof skill !== "string") {
|
|
4748
4986
|
continue;
|
|
4749
4987
|
}
|
|
4750
|
-
const folder =
|
|
4988
|
+
const folder = posix6.basename(skill);
|
|
4751
4989
|
if (!folder.startsWith(SKILL_PREFIX)) {
|
|
4752
4990
|
findings.push(
|
|
4753
4991
|
finding(
|
|
@@ -4981,7 +5219,7 @@ async function readPolicyState(root, installed) {
|
|
|
4981
5219
|
const source = await readIfPresent(
|
|
4982
5220
|
resolveWithinRoot(
|
|
4983
5221
|
root,
|
|
4984
|
-
|
|
5222
|
+
posix6.join(posix6.dirname(KV_INDEX_FILE), `${specifier}.ts`)
|
|
4985
5223
|
)
|
|
4986
5224
|
);
|
|
4987
5225
|
if (source !== void 0) {
|
|
@@ -5008,7 +5246,7 @@ function relativeImportsIn(index) {
|
|
|
5008
5246
|
}
|
|
5009
5247
|
async function resolveDoctorTarget(path) {
|
|
5010
5248
|
const dir = resolve3(path);
|
|
5011
|
-
if (await pathExists(
|
|
5249
|
+
if (await pathExists(join12(dir, "registry-item.json"))) {
|
|
5012
5250
|
return { names: [basename(dir)], registryDir: dirname6(dir) };
|
|
5013
5251
|
}
|
|
5014
5252
|
return { names: await registryModuleNames(dir), registryDir: dir };
|
|
@@ -5030,6 +5268,54 @@ function checkPartialInstalls(state) {
|
|
|
5030
5268
|
)
|
|
5031
5269
|
);
|
|
5032
5270
|
}
|
|
5271
|
+
var LEGACY_ENV_FILES = [".dev.vars", ".dev.vars.example"];
|
|
5272
|
+
function checkEnv(args) {
|
|
5273
|
+
const findings = args.legacy.toSorted().map(
|
|
5274
|
+
(path) => finding(
|
|
5275
|
+
"env",
|
|
5276
|
+
`/${path}`,
|
|
5277
|
+
`${path} still exists, and nothing reads it. wrangler loads .env only while no .dev.vars sits beside it, so every value pnpm env:setup writes is ignored. Run \`pnpm env:setup\` to carry its values into .env and delete it.`
|
|
5278
|
+
)
|
|
5279
|
+
);
|
|
5280
|
+
if (args.legacy.length > 0 && !args.hasKeyList) {
|
|
5281
|
+
findings.push(
|
|
5282
|
+
finding(
|
|
5283
|
+
"env",
|
|
5284
|
+
`/${ENV_EXAMPLE}`,
|
|
5285
|
+
`${ENV_EXAMPLE} is missing, so there is no key list to migrate those values into. Re-run \`saasaloy add\` for an installed module to regenerate it.`
|
|
5286
|
+
)
|
|
5287
|
+
);
|
|
5288
|
+
}
|
|
5289
|
+
return findings;
|
|
5290
|
+
}
|
|
5291
|
+
var INFRA_RUNBOOK = "https://github.com/mimukit/saasaloy/blob/main/docs/wiki/runbooks/move-off-the-infra-module.md";
|
|
5292
|
+
function checkRetiredInfra(hasInfra) {
|
|
5293
|
+
return hasInfra ? [
|
|
5294
|
+
finding(
|
|
5295
|
+
"deploy",
|
|
5296
|
+
"/infra",
|
|
5297
|
+
`infra/ is the Pulumi deploy that pnpm deploy:production replaced, and nothing runs it any more. Never run pulumi destroy: it deletes the live Workers and their data. Follow ${INFRA_RUNBOOK} to move the resource ids into wrangler.jsonc, then delete infra/.`
|
|
5298
|
+
)
|
|
5299
|
+
] : [];
|
|
5300
|
+
}
|
|
5301
|
+
function hasRetiredInfra(root) {
|
|
5302
|
+
return pathExists(resolveWithinRoot(root, "infra"));
|
|
5303
|
+
}
|
|
5304
|
+
async function readEnvState(root) {
|
|
5305
|
+
const legacy = [];
|
|
5306
|
+
for (const workspace of ["apps/api", "apps/web", "apps/admin"]) {
|
|
5307
|
+
for (const name of LEGACY_ENV_FILES) {
|
|
5308
|
+
const path = posix6.join(workspace, name);
|
|
5309
|
+
if (await pathExists(resolveWithinRoot(root, path))) {
|
|
5310
|
+
legacy.push(path);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
return {
|
|
5315
|
+
legacy,
|
|
5316
|
+
hasKeyList: await pathExists(resolveWithinRoot(root, ENV_EXAMPLE))
|
|
5317
|
+
};
|
|
5318
|
+
}
|
|
5033
5319
|
async function checkTarget(target) {
|
|
5034
5320
|
const siblings = await registryModuleNames(target.registryDir);
|
|
5035
5321
|
const aliases = await collectAliases(target.registryDir, siblings);
|
|
@@ -5038,7 +5324,7 @@ async function checkTarget(target) {
|
|
|
5038
5324
|
reports.push(
|
|
5039
5325
|
await checkModule({
|
|
5040
5326
|
aliases,
|
|
5041
|
-
dir:
|
|
5327
|
+
dir: join12(target.registryDir, name),
|
|
5042
5328
|
siblings
|
|
5043
5329
|
})
|
|
5044
5330
|
);
|
|
@@ -5152,10 +5438,13 @@ async function reportProject(path) {
|
|
|
5152
5438
|
});
|
|
5153
5439
|
note2(wrapForNote(renderBase(base)), "Base");
|
|
5154
5440
|
const policyState = await readPolicyState(path, config.installed);
|
|
5441
|
+
const envState = await readEnvState(path);
|
|
5155
5442
|
const findings = [
|
|
5156
5443
|
...checkProject({ config, manifest }),
|
|
5157
5444
|
...checkPartialInstalls({ installed: config.installed, manifest }),
|
|
5158
|
-
...policyState ? checkPolicyBindings(policyState) : []
|
|
5445
|
+
...policyState ? checkPolicyBindings(policyState) : [],
|
|
5446
|
+
...checkEnv(envState),
|
|
5447
|
+
...checkRetiredInfra(await hasRetiredInfra(path))
|
|
5159
5448
|
];
|
|
5160
5449
|
if (findings.length === 0) {
|
|
5161
5450
|
const count = config.installed.length;
|
|
@@ -5200,7 +5489,7 @@ async function runDoctor(argv) {
|
|
|
5200
5489
|
);
|
|
5201
5490
|
return EXIT_REFUSED;
|
|
5202
5491
|
}
|
|
5203
|
-
if (await pathExists(
|
|
5492
|
+
if (await pathExists(join13(path, CONFIG_FILE))) {
|
|
5204
5493
|
return await reportProject(path);
|
|
5205
5494
|
}
|
|
5206
5495
|
const target = await resolveDoctorTarget(path);
|
|
@@ -5230,7 +5519,7 @@ async function runDoctor(argv) {
|
|
|
5230
5519
|
`${pc3.yellow(report.module)} ${pc3.dim(`(${report.findings.length})`)}`
|
|
5231
5520
|
);
|
|
5232
5521
|
}
|
|
5233
|
-
|
|
5522
|
+
log3.info(
|
|
5234
5523
|
pc3.dim(
|
|
5235
5524
|
`Checked ${reports.length} module${reports.length === 1 ? "" : "s"}; ${reports.length - bad.length} clean.`
|
|
5236
5525
|
)
|
|
@@ -5252,118 +5541,29 @@ import {
|
|
|
5252
5541
|
cancel as cancel3,
|
|
5253
5542
|
intro as intro3,
|
|
5254
5543
|
isCancel as isCancel2,
|
|
5255
|
-
log as
|
|
5544
|
+
log as log4,
|
|
5256
5545
|
note as note3,
|
|
5257
5546
|
outro as outro3,
|
|
5258
|
-
select as select2,
|
|
5259
5547
|
text
|
|
5260
5548
|
} from "@clack/prompts";
|
|
5261
5549
|
import pc4 from "picocolors";
|
|
5262
5550
|
|
|
5263
5551
|
// src/lib/env-vars.ts
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
function isPublicVar(name) {
|
|
5271
|
-
return name.startsWith(PUBLIC_PREFIX);
|
|
5272
|
-
}
|
|
5273
|
-
function targetFileName(name) {
|
|
5274
|
-
return isPublicVar(name) ? ENV_FILE : DEV_VARS_FILE;
|
|
5275
|
-
}
|
|
5276
|
-
function resolved(workspace) {
|
|
5277
|
-
return workspace ? { kind: "resolved", workspace } : void 0;
|
|
5278
|
-
}
|
|
5279
|
-
function routeVariable(args) {
|
|
5280
|
-
const { candidates, wranglerWorkspaces } = args;
|
|
5281
|
-
if (isPublicVar(args.name)) {
|
|
5282
|
-
const apps = candidates.filter(
|
|
5283
|
-
(w) => w.startsWith(APPS_DIR) && w !== args.apiWorkspace
|
|
5284
|
-
);
|
|
5285
|
-
if (apps.length === 1) {
|
|
5286
|
-
return { kind: "resolved", workspace: apps[0] };
|
|
5287
|
-
}
|
|
5288
|
-
if (apps.length > 1) {
|
|
5289
|
-
return { choices: apps.toSorted(), kind: "ambiguous" };
|
|
5290
|
-
}
|
|
5291
|
-
return resolved(args.baseWorkspace) ?? fromCandidatesAlone(candidates);
|
|
5292
|
-
}
|
|
5293
|
-
const wrangler = candidates.filter((w) => wranglerWorkspaces.includes(w));
|
|
5294
|
-
if (wrangler.length === 1) {
|
|
5295
|
-
return { kind: "resolved", workspace: wrangler[0] };
|
|
5296
|
-
}
|
|
5297
|
-
if (wrangler.length > 1) {
|
|
5298
|
-
return (args.apiWorkspace && wrangler.includes(args.apiWorkspace) ? resolved(args.apiWorkspace) : void 0) ?? { choices: wrangler.toSorted(), kind: "ambiguous" };
|
|
5299
|
-
}
|
|
5300
|
-
return resolved(args.apiWorkspace) ?? fromCandidatesAlone(candidates);
|
|
5301
|
-
}
|
|
5302
|
-
function fromCandidatesAlone(candidates) {
|
|
5303
|
-
if (candidates.length === 1) {
|
|
5304
|
-
return { kind: "resolved", workspace: candidates[0] };
|
|
5305
|
-
}
|
|
5306
|
-
return candidates.length === 0 ? { kind: "unknown" } : { choices: candidates.toSorted(), kind: "ambiguous" };
|
|
5307
|
-
}
|
|
5308
|
-
async function discoverWorkspaces(root, aliases) {
|
|
5309
|
-
const found = /* @__PURE__ */ new Set();
|
|
5310
|
-
for (const value of Object.values(aliases)) {
|
|
5311
|
-
const workspace = await walkUpToWorkspace(root, value);
|
|
5312
|
-
if (workspace) {
|
|
5313
|
-
found.add(workspace);
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
return [...found].toSorted();
|
|
5317
|
-
}
|
|
5318
|
-
async function walkUpToWorkspace(root, relPosixPath) {
|
|
5319
|
-
let current = relPosixPath.replace(/\/+$/, "");
|
|
5320
|
-
while (current && current !== "." && current !== "/") {
|
|
5321
|
-
if (await pathExists(resolveWithinRoot(root, `${current}/package.json`))) {
|
|
5322
|
-
return current;
|
|
5323
|
-
}
|
|
5324
|
-
const parent = posix8.dirname(current);
|
|
5325
|
-
if (parent === current) {
|
|
5326
|
-
return void 0;
|
|
5327
|
-
}
|
|
5328
|
-
current = parent;
|
|
5329
|
-
}
|
|
5330
|
-
return void 0;
|
|
5331
|
-
}
|
|
5332
|
-
async function findWranglerWorkspaces(root, workspaces) {
|
|
5333
|
-
const found = [];
|
|
5334
|
-
for (const workspace of workspaces) {
|
|
5335
|
-
for (const config of WRANGLER_CONFIGS) {
|
|
5336
|
-
if (await pathExists(resolveWithinRoot(root, `${workspace}/${config}`))) {
|
|
5337
|
-
found.push(workspace);
|
|
5338
|
-
break;
|
|
5339
|
-
}
|
|
5340
|
-
}
|
|
5341
|
-
}
|
|
5342
|
-
return found;
|
|
5343
|
-
}
|
|
5344
|
-
function workspacesByModule(manifest, workspaces) {
|
|
5345
|
-
const byModule = /* @__PURE__ */ new Map();
|
|
5346
|
-
for (const [path, entry] of Object.entries(manifest.managed)) {
|
|
5347
|
-
const workspace = workspaceForPath(path, workspaces);
|
|
5348
|
-
if (!workspace) {
|
|
5552
|
+
var LOCAL_VALUES = "packages/env/.env";
|
|
5553
|
+
function parseEnvValues(text4) {
|
|
5554
|
+
const values = {};
|
|
5555
|
+
for (const line of text4.split("\n")) {
|
|
5556
|
+
const trimmed = line.trim();
|
|
5557
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
5349
5558
|
continue;
|
|
5350
5559
|
}
|
|
5351
|
-
const
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
}
|
|
5355
|
-
return new Map(
|
|
5356
|
-
[...byModule].map(([module, set]) => [module, [...set].toSorted()])
|
|
5357
|
-
);
|
|
5358
|
-
}
|
|
5359
|
-
function workspaceForPath(path, workspaces) {
|
|
5360
|
-
let best;
|
|
5361
|
-
for (const workspace of workspaces) {
|
|
5362
|
-
if (path.startsWith(`${workspace}/`) && (best === void 0 || workspace.length > best.length)) {
|
|
5363
|
-
best = workspace;
|
|
5560
|
+
const eq = trimmed.indexOf("=");
|
|
5561
|
+
if (eq <= 0) {
|
|
5562
|
+
continue;
|
|
5364
5563
|
}
|
|
5564
|
+
values[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1);
|
|
5365
5565
|
}
|
|
5366
|
-
return
|
|
5566
|
+
return values;
|
|
5367
5567
|
}
|
|
5368
5568
|
function isSet(values, name) {
|
|
5369
5569
|
return (values[name] ?? "").trim() !== "";
|
|
@@ -5399,27 +5599,20 @@ function appendVars(existing, additions) {
|
|
|
5399
5599
|
return appended.length === 0 ? head : `${head}${appended.join("\n")}
|
|
5400
5600
|
`;
|
|
5401
5601
|
}
|
|
5402
|
-
function productionSecretCommands(
|
|
5403
|
-
const
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
continue;
|
|
5407
|
-
}
|
|
5408
|
-
byWorkspace.set(workspace, [...byWorkspace.get(workspace) ?? [], name]);
|
|
5409
|
-
}
|
|
5410
|
-
const lines = [];
|
|
5411
|
-
for (const workspace of [...byWorkspace.keys()].toSorted()) {
|
|
5412
|
-
lines.push(`# from ${workspace}`);
|
|
5413
|
-
for (const name of (byWorkspace.get(workspace) ?? []).toSorted()) {
|
|
5414
|
-
lines.push(`wrangler secret put ${name}`);
|
|
5415
|
-
}
|
|
5602
|
+
function productionSecretCommands(names) {
|
|
5603
|
+
const secrets = names.filter((name) => !isPublicVar(name)).toSorted();
|
|
5604
|
+
if (secrets.length === 0) {
|
|
5605
|
+
return [];
|
|
5416
5606
|
}
|
|
5417
|
-
return
|
|
5607
|
+
return [
|
|
5608
|
+
"# from the Worker's workspace, e.g. apps/api",
|
|
5609
|
+
...secrets.map((name) => `wrangler secret put ${name}`)
|
|
5610
|
+
];
|
|
5418
5611
|
}
|
|
5419
5612
|
|
|
5420
5613
|
// src/lib/gitignore.ts
|
|
5421
5614
|
import { readFile as readFile14 } from "fs/promises";
|
|
5422
|
-
import { dirname as dirname7, join as
|
|
5615
|
+
import { dirname as dirname7, join as join14, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
|
|
5423
5616
|
var REGEX_SPECIAL = /[.+^${}()|[\]\\]/g;
|
|
5424
5617
|
function globToRegex(glob) {
|
|
5425
5618
|
let out = "";
|
|
@@ -5503,7 +5696,7 @@ function excludes(layers, candidate, isDir) {
|
|
|
5503
5696
|
async function findRepoRoot(start) {
|
|
5504
5697
|
let current = resolve4(start);
|
|
5505
5698
|
for (; ; ) {
|
|
5506
|
-
if (await pathExists(
|
|
5699
|
+
if (await pathExists(join14(current, ".git"))) {
|
|
5507
5700
|
return current;
|
|
5508
5701
|
}
|
|
5509
5702
|
const parent = dirname7(current);
|
|
@@ -5520,7 +5713,7 @@ async function isPathIgnored(root, relPosixPath) {
|
|
|
5520
5713
|
}
|
|
5521
5714
|
const fromRepo = relative3(
|
|
5522
5715
|
repoRoot,
|
|
5523
|
-
|
|
5716
|
+
join14(resolve4(root), ...relPosixPath.split("/"))
|
|
5524
5717
|
);
|
|
5525
5718
|
const segments = fromRepo.split(sep2).filter(Boolean);
|
|
5526
5719
|
if (segments.length === 0 || segments.includes("..")) {
|
|
@@ -5529,7 +5722,7 @@ async function isPathIgnored(root, relPosixPath) {
|
|
|
5529
5722
|
const layers = [];
|
|
5530
5723
|
for (let depth = 0; depth < segments.length; depth += 1) {
|
|
5531
5724
|
const parts = segments.slice(0, depth);
|
|
5532
|
-
const file =
|
|
5725
|
+
const file = join14(repoRoot, ...parts, ".gitignore");
|
|
5533
5726
|
if (await pathExists(file)) {
|
|
5534
5727
|
layers.push({
|
|
5535
5728
|
base: parts.join("/"),
|
|
@@ -5567,16 +5760,9 @@ function parseArgs3(argv) {
|
|
|
5567
5760
|
return { check: argv.includes("--check"), unknown };
|
|
5568
5761
|
}
|
|
5569
5762
|
function renderPending(pending) {
|
|
5570
|
-
return pending.map(
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
return `${head}
|
|
5574
|
-
${pc4.dim(`\u2192 ${entry.file}`)}`;
|
|
5575
|
-
}
|
|
5576
|
-
const why = entry.choices ? `several workspaces fit (${entry.choices.join(", ")}) \u2014 run \`saasaloy env\` to pick one` : "no target workspace found \u2014 this module wrote no files under a known alias";
|
|
5577
|
-
return `${head}
|
|
5578
|
-
${pc4.yellow(`\u2192 ${why}`)}`;
|
|
5579
|
-
});
|
|
5763
|
+
return pending.map(
|
|
5764
|
+
(entry) => `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2014 declared by ${entry.declaration.module}`)}`
|
|
5765
|
+
);
|
|
5580
5766
|
}
|
|
5581
5767
|
async function loadDeclarations(config, lock, sources, onSkip) {
|
|
5582
5768
|
const override = process.env[REGISTRY_ENV];
|
|
@@ -5645,31 +5831,15 @@ async function loadDeclarations(config, lock, sources, onSkip) {
|
|
|
5645
5831
|
return true;
|
|
5646
5832
|
});
|
|
5647
5833
|
}
|
|
5648
|
-
function
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
return void 0;
|
|
5652
|
-
}
|
|
5653
|
-
return workspaceForPath(
|
|
5654
|
-
value.endsWith("/") ? value : `${value}/`,
|
|
5655
|
-
workspaces
|
|
5656
|
-
);
|
|
5657
|
-
}
|
|
5658
|
-
async function writeAnswers(root, answers, contents) {
|
|
5659
|
-
const written = [];
|
|
5660
|
-
for (const [file, additions] of [...answers].toSorted(
|
|
5661
|
-
([a], [b]) => a.localeCompare(b)
|
|
5662
|
-
)) {
|
|
5663
|
-
if (additions.length === 0) {
|
|
5664
|
-
continue;
|
|
5665
|
-
}
|
|
5666
|
-
const abs = resolveWithinRoot(root, file);
|
|
5667
|
-
await assertNoSymlinkPath(root, abs);
|
|
5668
|
-
await mkdir6(dirname8(abs), { recursive: true });
|
|
5669
|
-
await writeFile8(abs, appendVars(contents.get(file), additions), "utf-8");
|
|
5670
|
-
written.push([file, additions.length]);
|
|
5834
|
+
async function writeAnswers(root, additions, before) {
|
|
5835
|
+
if (additions.length === 0) {
|
|
5836
|
+
return 0;
|
|
5671
5837
|
}
|
|
5672
|
-
|
|
5838
|
+
const abs = resolveWithinRoot(root, LOCAL_VALUES);
|
|
5839
|
+
await assertNoSymlinkPath(root, abs);
|
|
5840
|
+
await mkdir6(dirname8(abs), { recursive: true });
|
|
5841
|
+
await writeFile8(abs, appendVars(before, additions), "utf-8");
|
|
5842
|
+
return additions.length;
|
|
5673
5843
|
}
|
|
5674
5844
|
async function readTarget(root, file) {
|
|
5675
5845
|
const abs = resolveWithinRoot(root, file);
|
|
@@ -5693,62 +5863,34 @@ async function runEnv(argv) {
|
|
|
5693
5863
|
try {
|
|
5694
5864
|
const root = await findProjectRoot();
|
|
5695
5865
|
const config = await loadConfig(root);
|
|
5696
|
-
const manifest = await loadManifest(root);
|
|
5697
5866
|
const lock = await loadLock(root);
|
|
5698
5867
|
const declarations = await loadDeclarations(
|
|
5699
5868
|
config,
|
|
5700
5869
|
lock,
|
|
5701
5870
|
sources,
|
|
5702
5871
|
(msg) => {
|
|
5703
|
-
|
|
5872
|
+
log4.warn(msg);
|
|
5704
5873
|
}
|
|
5705
5874
|
);
|
|
5706
5875
|
if (declarations.length === 0) {
|
|
5707
5876
|
outro3(pc4.dim("No module declares an environment variable."));
|
|
5708
5877
|
return EXIT_OK;
|
|
5709
5878
|
}
|
|
5710
|
-
const
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
const
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
);
|
|
5719
|
-
const contents = /* @__PURE__ */ new Map();
|
|
5720
|
-
const readOnce = async (file) => {
|
|
5721
|
-
if (!contents.has(file)) {
|
|
5722
|
-
contents.set(file, await readTarget(root, file));
|
|
5723
|
-
}
|
|
5724
|
-
return contents.get(file);
|
|
5725
|
-
};
|
|
5726
|
-
const pending = [];
|
|
5727
|
-
const routed = [];
|
|
5728
|
-
for (const declaration of declarations) {
|
|
5729
|
-
const route = routeVariable({
|
|
5730
|
-
candidates: byModule.get(declaration.module) ?? [],
|
|
5731
|
-
name: declaration.name,
|
|
5732
|
-
wranglerWorkspaces,
|
|
5733
|
-
...api ? { apiWorkspace: api } : {},
|
|
5734
|
-
...base ? { baseWorkspace: base } : {}
|
|
5735
|
-
});
|
|
5736
|
-
if (route.kind === "resolved") {
|
|
5737
|
-
const file = `${route.workspace}/${targetFileName(declaration.name)}`;
|
|
5738
|
-
routed.push({ name: declaration.name, workspace: route.workspace });
|
|
5739
|
-
const values = parseDevVars(await readOnce(file) ?? "");
|
|
5740
|
-
if (isSet(values, declaration.name)) {
|
|
5741
|
-
continue;
|
|
5742
|
-
}
|
|
5743
|
-
pending.push({ declaration, file, workspace: route.workspace });
|
|
5744
|
-
continue;
|
|
5745
|
-
}
|
|
5746
|
-
pending.push({
|
|
5747
|
-
declaration,
|
|
5748
|
-
...route.kind === "ambiguous" ? { choices: route.choices } : {}
|
|
5749
|
-
});
|
|
5879
|
+
const listed = new Set(
|
|
5880
|
+
Object.keys(parseEnvExample(await readTarget(root, ENV_EXAMPLE) ?? ""))
|
|
5881
|
+
);
|
|
5882
|
+
const undeclared = declarations.filter((declaration) => !listed.has(declaration.name)).map((declaration) => declaration.name);
|
|
5883
|
+
if (undeclared.length > 0) {
|
|
5884
|
+
log4.warn(
|
|
5885
|
+
`${ENV_EXAMPLE} does not list ${undeclared.join(", ")}, so no service reads ${undeclared.length === 1 ? "it" : "them"}. Re-run \`saasaloy add\` for the declaring module to regenerate the key list.`
|
|
5886
|
+
);
|
|
5750
5887
|
}
|
|
5751
|
-
const
|
|
5888
|
+
const before = await readTarget(root, LOCAL_VALUES);
|
|
5889
|
+
const values = parseEnvValues(before ?? "");
|
|
5890
|
+
const pending = declarations.filter((declaration) => !isSet(values, declaration.name)).map((declaration) => ({ declaration }));
|
|
5891
|
+
const secretCommands = productionSecretCommands(
|
|
5892
|
+
declarations.map((declaration) => declaration.name)
|
|
5893
|
+
);
|
|
5752
5894
|
const printSecrets = () => {
|
|
5753
5895
|
if (secretCommands.length > 0) {
|
|
5754
5896
|
note3(
|
|
@@ -5766,16 +5908,11 @@ ${pc4.dim("Printed, never run \u2014 copy these when you deploy.")}`
|
|
|
5766
5908
|
outro3(pc4.green("Every declared variable is set."));
|
|
5767
5909
|
return EXIT_OK;
|
|
5768
5910
|
}
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
cancel3(
|
|
5775
|
-
`${pc4.cyan(file)} isn't gitignored \u2014 refusing to write a secret into a tracked file. Add it to .gitignore (the base template already does) and run again.`
|
|
5776
|
-
);
|
|
5777
|
-
return EXIT_REFUSED;
|
|
5778
|
-
}
|
|
5911
|
+
if (!await isPathIgnored(root, LOCAL_VALUES)) {
|
|
5912
|
+
cancel3(
|
|
5913
|
+
`${pc4.cyan(LOCAL_VALUES)} isn't gitignored \u2014 refusing to write a secret into a tracked file. Add it to .gitignore (the base template already does) and run again.`
|
|
5914
|
+
);
|
|
5915
|
+
return EXIT_REFUSED;
|
|
5779
5916
|
}
|
|
5780
5917
|
if (opts.check || !isInteractive()) {
|
|
5781
5918
|
note3(wrapForNote(renderPending(pending).join("\n")), "Missing");
|
|
@@ -5786,42 +5923,11 @@ ${pc4.dim("Printed, never run \u2014 copy these when you deploy.")}`
|
|
|
5786
5923
|
);
|
|
5787
5924
|
return EXIT_REFUSED;
|
|
5788
5925
|
}
|
|
5789
|
-
const answers =
|
|
5926
|
+
const answers = [];
|
|
5790
5927
|
for (const entry of pending) {
|
|
5791
|
-
let file = entry.file;
|
|
5792
|
-
if (!file) {
|
|
5793
|
-
if (!entry.choices) {
|
|
5794
|
-
log3.warn(
|
|
5795
|
-
`${pc4.cyan(entry.declaration.name)} \u2014 no target workspace found; set it by hand.`
|
|
5796
|
-
);
|
|
5797
|
-
continue;
|
|
5798
|
-
}
|
|
5799
|
-
const picked = await select2({
|
|
5800
|
-
message: `Which workspace reads ${entry.declaration.name}?`,
|
|
5801
|
-
options: entry.choices.map((workspace) => ({
|
|
5802
|
-
label: workspace,
|
|
5803
|
-
value: workspace
|
|
5804
|
-
}))
|
|
5805
|
-
});
|
|
5806
|
-
if (isCancel2(picked)) {
|
|
5807
|
-
cancel3("Cancelled \u2014 nothing written.");
|
|
5808
|
-
return EXIT_REFUSED;
|
|
5809
|
-
}
|
|
5810
|
-
file = `${picked}/${targetFileName(entry.declaration.name)}`;
|
|
5811
|
-
if (!await isPathIgnored(root, file)) {
|
|
5812
|
-
cancel3(
|
|
5813
|
-
`${pc4.cyan(file)} isn't gitignored \u2014 refusing to write a secret into a tracked file.`
|
|
5814
|
-
);
|
|
5815
|
-
return EXIT_REFUSED;
|
|
5816
|
-
}
|
|
5817
|
-
const values = parseDevVars(await readOnce(file) ?? "");
|
|
5818
|
-
if (isSet(values, entry.declaration.name)) {
|
|
5819
|
-
continue;
|
|
5820
|
-
}
|
|
5821
|
-
}
|
|
5822
5928
|
const answer = await text({
|
|
5823
5929
|
defaultValue: entry.declaration.devValue ?? "",
|
|
5824
|
-
message: `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2192 ${
|
|
5930
|
+
message: `${pc4.cyan(entry.declaration.name)} ${pc4.dim(`\u2192 ${LOCAL_VALUES}`)}
|
|
5825
5931
|
${entry.declaration.description}`,
|
|
5826
5932
|
...entry.declaration.devValue === void 0 ? {} : { initialValue: entry.declaration.devValue }
|
|
5827
5933
|
});
|
|
@@ -5829,18 +5935,15 @@ ${entry.declaration.description}`,
|
|
|
5829
5935
|
cancel3("Cancelled \u2014 nothing written.");
|
|
5830
5936
|
return EXIT_REFUSED;
|
|
5831
5937
|
}
|
|
5832
|
-
answers.
|
|
5833
|
-
...answers.get(file) ?? [],
|
|
5834
|
-
[entry.declaration.name, answer]
|
|
5835
|
-
]);
|
|
5938
|
+
answers.push([entry.declaration.name, answer]);
|
|
5836
5939
|
}
|
|
5837
|
-
const written = await writeAnswers(root, answers,
|
|
5838
|
-
if (written
|
|
5940
|
+
const written = await writeAnswers(root, answers, before);
|
|
5941
|
+
if (written > 0) {
|
|
5839
5942
|
note3(
|
|
5840
5943
|
wrapForNote(
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5944
|
+
`${pc4.cyan(LOCAL_VALUES)} ${pc4.dim(`\u2014 ${written} added`)}
|
|
5945
|
+
|
|
5946
|
+
${pc4.dim("Run `pnpm env:setup` to write each service's own .env from it.")}`
|
|
5844
5947
|
),
|
|
5845
5948
|
"Written"
|
|
5846
5949
|
);
|
|
@@ -5859,39 +5962,39 @@ ${entry.declaration.description}`,
|
|
|
5859
5962
|
}
|
|
5860
5963
|
|
|
5861
5964
|
// src/commands/init.ts
|
|
5862
|
-
import { spawn } from "child_process";
|
|
5965
|
+
import { spawn as spawn2 } from "child_process";
|
|
5863
5966
|
import { readdir as readdir4, readFile as readFile16 } from "fs/promises";
|
|
5864
|
-
import { basename as basename2, join as
|
|
5967
|
+
import { basename as basename2, join as join15, resolve as resolve6 } from "path";
|
|
5865
5968
|
import {
|
|
5866
5969
|
cancel as cancel4,
|
|
5867
5970
|
intro as intro4,
|
|
5868
5971
|
isCancel as isCancel3,
|
|
5869
5972
|
note as note4,
|
|
5870
5973
|
outro as outro4,
|
|
5871
|
-
select as
|
|
5974
|
+
select as select2,
|
|
5872
5975
|
spinner,
|
|
5873
5976
|
text as text2
|
|
5874
5977
|
} from "@clack/prompts";
|
|
5875
5978
|
import pc6 from "picocolors";
|
|
5876
5979
|
|
|
5877
5980
|
// src/lib/logger.ts
|
|
5878
|
-
import { log as
|
|
5981
|
+
import { log as log5 } from "@clack/prompts";
|
|
5879
5982
|
import pc5 from "picocolors";
|
|
5880
5983
|
var logger = {
|
|
5881
5984
|
error(message) {
|
|
5882
|
-
|
|
5985
|
+
log5.error(message);
|
|
5883
5986
|
},
|
|
5884
5987
|
info(message) {
|
|
5885
|
-
|
|
5988
|
+
log5.info(message);
|
|
5886
5989
|
},
|
|
5887
5990
|
step(message) {
|
|
5888
|
-
|
|
5991
|
+
log5.step(pc5.dim(message));
|
|
5889
5992
|
},
|
|
5890
5993
|
success(message) {
|
|
5891
|
-
|
|
5994
|
+
log5.success(message);
|
|
5892
5995
|
},
|
|
5893
5996
|
warn(message) {
|
|
5894
|
-
|
|
5997
|
+
log5.warn(message);
|
|
5895
5998
|
}
|
|
5896
5999
|
};
|
|
5897
6000
|
|
|
@@ -5938,7 +6041,7 @@ function parseArgs4(argv) {
|
|
|
5938
6041
|
}
|
|
5939
6042
|
function run(command, args, cwd) {
|
|
5940
6043
|
return new Promise((resolvePromise) => {
|
|
5941
|
-
const child =
|
|
6044
|
+
const child = spawn2(command, args, {
|
|
5942
6045
|
cwd,
|
|
5943
6046
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5944
6047
|
shell: process.platform === "win32"
|
|
@@ -5973,7 +6076,10 @@ function runPnpmInstall(cwd) {
|
|
|
5973
6076
|
async function initGitRepo(cwd) {
|
|
5974
6077
|
const inside = await run("git", ["rev-parse", "--is-inside-work-tree"], cwd);
|
|
5975
6078
|
if (inside.ok) {
|
|
5976
|
-
|
|
6079
|
+
const ignored = await run("git", ["check-ignore", "--quiet", "."], cwd);
|
|
6080
|
+
if (!ignored.ok) {
|
|
6081
|
+
return { created: false };
|
|
6082
|
+
}
|
|
5977
6083
|
}
|
|
5978
6084
|
const result = await run("git", ["init"], cwd);
|
|
5979
6085
|
if (!result.ok) {
|
|
@@ -5994,7 +6100,7 @@ function bySkillOrder(a, b) {
|
|
|
5994
6100
|
}
|
|
5995
6101
|
async function linkAgentSkills(target) {
|
|
5996
6102
|
const result = { linked: [], conflicts: [], failures: [] };
|
|
5997
|
-
const skillsDir =
|
|
6103
|
+
const skillsDir = join15(target, ".agents", "skills");
|
|
5998
6104
|
let names;
|
|
5999
6105
|
try {
|
|
6000
6106
|
names = await readDirNames(skillsDir);
|
|
@@ -6003,8 +6109,8 @@ async function linkAgentSkills(target) {
|
|
|
6003
6109
|
return result;
|
|
6004
6110
|
}
|
|
6005
6111
|
for (const name of names.toSorted(bySkillOrder)) {
|
|
6006
|
-
const linkAbs =
|
|
6007
|
-
const targetAbs =
|
|
6112
|
+
const linkAbs = join15(target, ".claude", "skills", name);
|
|
6113
|
+
const targetAbs = join15(skillsDir, name);
|
|
6008
6114
|
let state;
|
|
6009
6115
|
try {
|
|
6010
6116
|
state = await classifyLink(linkAbs, targetAbs);
|
|
@@ -6162,7 +6268,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
|
|
|
6162
6268
|
);
|
|
6163
6269
|
}
|
|
6164
6270
|
let installed = false;
|
|
6165
|
-
const wantsInstall = noInstall ? false : await
|
|
6271
|
+
const wantsInstall = noInstall ? false : await select2({
|
|
6166
6272
|
message: "Install dependencies now?",
|
|
6167
6273
|
options: [
|
|
6168
6274
|
{ value: true, label: `Yes, run ${pc6.cyan("pnpm install")}` },
|
|
@@ -6197,7 +6303,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
|
|
|
6197
6303
|
// owner who never opens AGENTS.md would otherwise not know they exist.
|
|
6198
6304
|
// Pad to the same column the commands above comment at (`pnpm dev` + 21 spaces).
|
|
6199
6305
|
...skills.linked.map(
|
|
6200
|
-
(name) => `${`/${name}`.padEnd(29)}${pc6.dim("# in
|
|
6306
|
+
(name) => `${`/${name}`.padEnd(29)}${pc6.dim("# in your AI coding tool")}`
|
|
6201
6307
|
)
|
|
6202
6308
|
].filter((line) => line !== null).map((line) => pc6.cyan(line)).join("\n");
|
|
6203
6309
|
note4(steps, "Next steps");
|
|
@@ -6207,7 +6313,7 @@ ${pc6.dim("Symlinked for Claude Code \u2014 the skill files live in `.agents/ski
|
|
|
6207
6313
|
|
|
6208
6314
|
// src/commands/list.ts
|
|
6209
6315
|
import { cancel as cancel5, intro as intro5, note as note5, outro as outro5 } from "@clack/prompts";
|
|
6210
|
-
import { join as
|
|
6316
|
+
import { join as join16 } from "path";
|
|
6211
6317
|
import pc7 from "picocolors";
|
|
6212
6318
|
var KNOWN_FLAGS5 = /* @__PURE__ */ new Set(["--installed", "--available", "--help", "-h"]);
|
|
6213
6319
|
var USAGE5 = "saasaloy list [<owner/repo[@ref]>] [--installed] [--available]";
|
|
@@ -6240,7 +6346,7 @@ function parseArgs5(argv) {
|
|
|
6240
6346
|
}
|
|
6241
6347
|
async function localProject() {
|
|
6242
6348
|
const root = await findProjectRoot();
|
|
6243
|
-
if (!await pathExists(
|
|
6349
|
+
if (!await pathExists(join16(root, CONFIG_FILE))) {
|
|
6244
6350
|
return { installed: /* @__PURE__ */ new Set() };
|
|
6245
6351
|
}
|
|
6246
6352
|
const config = await loadConfig(root);
|
|
@@ -6340,21 +6446,21 @@ async function runList(argv) {
|
|
|
6340
6446
|
}
|
|
6341
6447
|
|
|
6342
6448
|
// src/commands/new.ts
|
|
6343
|
-
import { join as
|
|
6449
|
+
import { join as join18, relative as relative4, resolve as resolve7 } from "path";
|
|
6344
6450
|
import {
|
|
6345
6451
|
cancel as cancel6,
|
|
6346
6452
|
intro as intro6,
|
|
6347
6453
|
isCancel as isCancel4,
|
|
6348
6454
|
note as note6,
|
|
6349
6455
|
outro as outro6,
|
|
6350
|
-
select as
|
|
6456
|
+
select as select3,
|
|
6351
6457
|
text as text3
|
|
6352
6458
|
} from "@clack/prompts";
|
|
6353
6459
|
import pc8 from "picocolors";
|
|
6354
6460
|
|
|
6355
6461
|
// src/lib/new-module.ts
|
|
6356
6462
|
import { mkdir as mkdir7, writeFile as writeFile9 } from "fs/promises";
|
|
6357
|
-
import { dirname as dirname9, join as
|
|
6463
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
6358
6464
|
var MODULE_TYPES = [
|
|
6359
6465
|
"saasaloy:capability",
|
|
6360
6466
|
"saasaloy:feature"
|
|
@@ -6458,7 +6564,7 @@ function moduleFiles(spec) {
|
|
|
6458
6564
|
async function writeModule(dir, spec) {
|
|
6459
6565
|
const files = moduleFiles(spec);
|
|
6460
6566
|
for (const file of files) {
|
|
6461
|
-
const abs =
|
|
6567
|
+
const abs = join17(dir, ...file.path.split("/"));
|
|
6462
6568
|
await mkdir7(dirname9(abs), { recursive: true });
|
|
6463
6569
|
await writeFile9(abs, file.content, "utf-8");
|
|
6464
6570
|
}
|
|
@@ -6546,7 +6652,7 @@ async function runNew(argv) {
|
|
|
6546
6652
|
}
|
|
6547
6653
|
try {
|
|
6548
6654
|
const projectRoot = await findProjectRoot();
|
|
6549
|
-
if (await pathExists(
|
|
6655
|
+
if (await pathExists(join18(projectRoot, CONFIG_FILE))) {
|
|
6550
6656
|
cancel6(
|
|
6551
6657
|
`${CONFIG_FILE} is present at ${pc8.cyan(projectRoot)} \u2014 this is a generated project, and \`new module\` authors a module for a registry. Run it in the registry repo instead. Nothing was written.`
|
|
6552
6658
|
);
|
|
@@ -6574,7 +6680,7 @@ async function runNew(argv) {
|
|
|
6574
6680
|
return EXIT_REFUSED;
|
|
6575
6681
|
}
|
|
6576
6682
|
const registryDir = resolve7(process.cwd(), REGISTRY_DIR);
|
|
6577
|
-
const dir =
|
|
6683
|
+
const dir = join18(registryDir, name);
|
|
6578
6684
|
if (await pathExists(dir)) {
|
|
6579
6685
|
cancel6(
|
|
6580
6686
|
`${pc8.cyan(`${REGISTRY_DIR}/${name}`)} already exists \u2014 pick another name, or edit it by hand. Nothing was written.`
|
|
@@ -6591,7 +6697,7 @@ async function runNew(argv) {
|
|
|
6591
6697
|
}
|
|
6592
6698
|
type = opts.type;
|
|
6593
6699
|
} else if (isInteractive()) {
|
|
6594
|
-
const picked = await
|
|
6700
|
+
const picked = await select3({
|
|
6595
6701
|
message: "Which tier is this?",
|
|
6596
6702
|
options: MODULE_TYPES.map((value) => ({
|
|
6597
6703
|
hint: TYPE_HINTS[value],
|
|
@@ -6664,16 +6770,16 @@ async function runNew(argv) {
|
|
|
6664
6770
|
}
|
|
6665
6771
|
|
|
6666
6772
|
// src/commands/outdated.ts
|
|
6667
|
-
import { cancel as cancel7, intro as intro7, log as
|
|
6773
|
+
import { cancel as cancel7, intro as intro7, log as log6, note as note7, outro as outro7 } from "@clack/prompts";
|
|
6668
6774
|
import pc9 from "picocolors";
|
|
6669
6775
|
|
|
6670
6776
|
// src/lib/updater.ts
|
|
6671
6777
|
import { mkdir as mkdir8, readFile as readFile18, rm as rmPath2, writeFile as writeFile11 } from "fs/promises";
|
|
6672
|
-
import { dirname as dirname11, posix as
|
|
6778
|
+
import { dirname as dirname11, posix as posix7 } from "path";
|
|
6673
6779
|
|
|
6674
6780
|
// src/lib/remover.ts
|
|
6675
6781
|
import { readdir as readdir5, readFile as readFile17, rm as rmPath, writeFile as writeFile10 } from "fs/promises";
|
|
6676
|
-
import { dirname as dirname10, join as
|
|
6782
|
+
import { dirname as dirname10, join as join19, relative as relative5, sep as sep3 } from "path";
|
|
6677
6783
|
function classifyTrackedFile(content, manifestHash) {
|
|
6678
6784
|
if (content === void 0) {
|
|
6679
6785
|
return "missing";
|
|
@@ -6817,7 +6923,7 @@ async function pruneEmptyDirs(root, deletedAbsPaths) {
|
|
|
6817
6923
|
async function dropDanglingAliases(root, config) {
|
|
6818
6924
|
const dropped = [];
|
|
6819
6925
|
for (const [alias, prefix] of Object.entries(config.aliases)) {
|
|
6820
|
-
const prefixAbs =
|
|
6926
|
+
const prefixAbs = join19(root, ...prefix.split("/"));
|
|
6821
6927
|
if (!await pathExists(prefixAbs)) {
|
|
6822
6928
|
delete config.aliases[alias];
|
|
6823
6929
|
dropped.push(alias);
|
|
@@ -7354,9 +7460,9 @@ function classifyUpdate(base, theirs, mine, managedHash, adopted, projectOwned)
|
|
|
7354
7460
|
async function planLinks(root, theirs) {
|
|
7355
7461
|
const links = [];
|
|
7356
7462
|
for (const skillRel of theirs.item.agent?.skills ?? []) {
|
|
7357
|
-
const folderName =
|
|
7358
|
-
const path =
|
|
7359
|
-
const target =
|
|
7463
|
+
const folderName = posix7.basename(skillRel);
|
|
7464
|
+
const path = posix7.join(".claude/skills", folderName);
|
|
7465
|
+
const target = posix7.join(".agents/skills", folderName);
|
|
7360
7466
|
const pathAbs = resolveWithinRoot(root, path);
|
|
7361
7467
|
const targetAbs = resolveWithinRoot(root, target);
|
|
7362
7468
|
const state = await classifyLink(pathAbs, targetAbs);
|
|
@@ -7710,7 +7816,7 @@ async function runOutdated(argv) {
|
|
|
7710
7816
|
const manifest = await loadManifest(root);
|
|
7711
7817
|
const registryOverride = !!process.env[REGISTRY_ENV];
|
|
7712
7818
|
if (registryOverride && config.installed.length > 0) {
|
|
7713
|
-
|
|
7819
|
+
log6.warn(
|
|
7714
7820
|
`${REGISTRY_ENV} is set, so every module reads as local \u2014 unset it to compare against the registry.`
|
|
7715
7821
|
);
|
|
7716
7822
|
}
|
|
@@ -7791,10 +7897,10 @@ import {
|
|
|
7791
7897
|
confirm as confirm2,
|
|
7792
7898
|
intro as intro8,
|
|
7793
7899
|
isCancel as isCancel5,
|
|
7794
|
-
log as
|
|
7900
|
+
log as log7,
|
|
7795
7901
|
note as note8,
|
|
7796
7902
|
outro as outro8,
|
|
7797
|
-
select as
|
|
7903
|
+
select as select4
|
|
7798
7904
|
} from "@clack/prompts";
|
|
7799
7905
|
import pc10 from "picocolors";
|
|
7800
7906
|
var KNOWN_FLAGS8 = /* @__PURE__ */ new Set([
|
|
@@ -7893,12 +7999,12 @@ function summarizeRemovePlan(plan, name, yes) {
|
|
|
7893
7999
|
note8(wrapForNote(linkLines.join("\n")), "Skill links");
|
|
7894
8000
|
}
|
|
7895
8001
|
if (plan.dependents.length > 0) {
|
|
7896
|
-
|
|
8002
|
+
log7.warn(
|
|
7897
8003
|
`${pc10.cyan(name)} is still depended on by ${plan.dependents.join(", ")} \u2014 proceeding anyway (--force).`
|
|
7898
8004
|
);
|
|
7899
8005
|
}
|
|
7900
8006
|
for (const missingLock of plan.missingLockEntries) {
|
|
7901
|
-
|
|
8007
|
+
log7.warn(
|
|
7902
8008
|
`Installed module ${pc10.cyan(missingLock)} has no lock entry \u2014 dependent detection is incomplete for it.`
|
|
7903
8009
|
);
|
|
7904
8010
|
}
|
|
@@ -7914,7 +8020,7 @@ function summarizeRemovePlan(plan, name, yes) {
|
|
|
7914
8020
|
if (p.action !== "drop") {
|
|
7915
8021
|
continue;
|
|
7916
8022
|
}
|
|
7917
|
-
|
|
8023
|
+
log7.warn(
|
|
7918
8024
|
`Config patch on ${pc10.cyan(p.entry.file)} ${pc10.dim(`(${p.entry.patch.kind})`)} is not reversed by \`remove\` \u2014 hand-revert it if needed.`
|
|
7919
8025
|
);
|
|
7920
8026
|
}
|
|
@@ -7955,7 +8061,7 @@ async function runRemove(argv) {
|
|
|
7955
8061
|
);
|
|
7956
8062
|
return EXIT_REFUSED;
|
|
7957
8063
|
}
|
|
7958
|
-
const picked = await
|
|
8064
|
+
const picked = await select4({
|
|
7959
8065
|
message: "Pick a module to remove",
|
|
7960
8066
|
options: config.installed.map((n) => ({ value: n, label: n }))
|
|
7961
8067
|
});
|
|
@@ -7980,7 +8086,7 @@ async function runRemove(argv) {
|
|
|
7980
8086
|
}
|
|
7981
8087
|
summarizeRemovePlan(plan, name, opts.yes);
|
|
7982
8088
|
for (const warning of plan.warnings) {
|
|
7983
|
-
|
|
8089
|
+
log7.warn(warning);
|
|
7984
8090
|
}
|
|
7985
8091
|
if (opts.diff) {
|
|
7986
8092
|
for (const file of plan.files) {
|
|
@@ -8055,24 +8161,28 @@ async function runRemove(argv) {
|
|
|
8055
8161
|
throw failedSave.error;
|
|
8056
8162
|
}
|
|
8057
8163
|
for (const file of result.deleted) {
|
|
8058
|
-
|
|
8164
|
+
log7.step(`${pc10.red("delete")} ${file.target}`);
|
|
8059
8165
|
}
|
|
8060
8166
|
for (const file of result.missingUntracked) {
|
|
8061
|
-
|
|
8167
|
+
log7.step(
|
|
8062
8168
|
`${pc10.dim("untrack")} ${file.target} ${pc10.dim("(already gone)")}`
|
|
8063
8169
|
);
|
|
8064
8170
|
}
|
|
8065
8171
|
for (const link of result.linksRemoved) {
|
|
8066
|
-
|
|
8172
|
+
log7.step(`${pc10.red("unlink")} ${link.path}`);
|
|
8067
8173
|
}
|
|
8068
8174
|
for (const link of result.linkConflicts) {
|
|
8069
|
-
|
|
8175
|
+
log7.warn(
|
|
8070
8176
|
`Skill link ${pc10.cyan(link.path)} left untouched \u2014 not ours to remove.`
|
|
8071
8177
|
);
|
|
8072
8178
|
}
|
|
8073
8179
|
for (const p of result.patchesReversed) {
|
|
8074
|
-
|
|
8180
|
+
log7.step(`${pc10.red("revert")} ${p.file} ${pc10.dim(`(${p.patch.kind})`)}`);
|
|
8075
8181
|
}
|
|
8182
|
+
await formatPatchedFiles(
|
|
8183
|
+
root,
|
|
8184
|
+
result.patchesReversed.map((p) => p.file)
|
|
8185
|
+
);
|
|
8076
8186
|
const refusals = new Map(
|
|
8077
8187
|
result.patchRefusals.map((r) => [r.patch, r.reason])
|
|
8078
8188
|
);
|
|
@@ -8081,15 +8191,15 @@ async function runRemove(argv) {
|
|
|
8081
8191
|
continue;
|
|
8082
8192
|
}
|
|
8083
8193
|
const reason = refusals.get(p);
|
|
8084
|
-
|
|
8194
|
+
log7.warn(
|
|
8085
8195
|
reason === void 0 ? `Config patch on ${pc10.cyan(p.file)} ${pc10.dim(`(${p.patch.kind})`)} was already gone \u2014 nothing to revert.` : `Config patch on ${pc10.cyan(p.file)} ${pc10.dim(`(${p.patch.kind})`)} left untouched \u2014 ${reason}.`
|
|
8086
8196
|
);
|
|
8087
8197
|
}
|
|
8088
8198
|
for (const dir of result.prunedDirs) {
|
|
8089
|
-
|
|
8199
|
+
log7.step(`${pc10.dim("prune")} ${dir}`);
|
|
8090
8200
|
}
|
|
8091
8201
|
for (const alias of result.droppedAliases) {
|
|
8092
|
-
|
|
8202
|
+
log7.warn(
|
|
8093
8203
|
`Alias ${pc10.cyan(alias)} dropped \u2014 its target directory is gone.`
|
|
8094
8204
|
);
|
|
8095
8205
|
}
|
|
@@ -8130,13 +8240,13 @@ ${pc10.dim("Hand-edited since \u2014 left on disk, now untracked.")}`
|
|
|
8130
8240
|
|
|
8131
8241
|
// src/commands/update.ts
|
|
8132
8242
|
import { mkdir as mkdir10, realpath, writeFile as writeFile13 } from "fs/promises";
|
|
8133
|
-
import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as
|
|
8243
|
+
import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join21, resolve as resolve8 } from "path";
|
|
8134
8244
|
import {
|
|
8135
8245
|
cancel as cancel9,
|
|
8136
8246
|
confirm as confirm3,
|
|
8137
8247
|
intro as intro9,
|
|
8138
8248
|
isCancel as isCancel6,
|
|
8139
|
-
log as
|
|
8249
|
+
log as log8,
|
|
8140
8250
|
note as note9,
|
|
8141
8251
|
outro as outro9
|
|
8142
8252
|
} from "@clack/prompts";
|
|
@@ -8151,14 +8261,14 @@ import {
|
|
|
8151
8261
|
rm as rmPath3,
|
|
8152
8262
|
writeFile as writeFile12
|
|
8153
8263
|
} from "fs/promises";
|
|
8154
|
-
import { dirname as dirname12, join as
|
|
8155
|
-
var BACKUP_DIR =
|
|
8264
|
+
import { dirname as dirname12, join as join20 } from "path";
|
|
8265
|
+
var BACKUP_DIR = join20(".saasaloy", "backups");
|
|
8156
8266
|
var RECORD_FILE = "backup.json";
|
|
8157
8267
|
var STATE_FILES = [
|
|
8158
8268
|
"saasaloy.json",
|
|
8159
8269
|
"saasaloy-lock.json",
|
|
8160
8270
|
"package.json",
|
|
8161
|
-
|
|
8271
|
+
join20(".saasaloy", "manifest.json")
|
|
8162
8272
|
];
|
|
8163
8273
|
function stampName(now) {
|
|
8164
8274
|
return now.toISOString().replaceAll(/[:.]/g, "-");
|
|
@@ -8166,8 +8276,8 @@ function stampName(now) {
|
|
|
8166
8276
|
async function writeBackup(args) {
|
|
8167
8277
|
const { root, targets, cliVersion } = args;
|
|
8168
8278
|
const createdAt = (args.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8169
|
-
const rel =
|
|
8170
|
-
const dir =
|
|
8279
|
+
const rel = join20(BACKUP_DIR, stampName(args.now ?? new Date(createdAt)));
|
|
8280
|
+
const dir = join20(root, rel);
|
|
8171
8281
|
const files = [];
|
|
8172
8282
|
const absent = [];
|
|
8173
8283
|
for (const target of /* @__PURE__ */ new Set([...targets, ...STATE_FILES])) {
|
|
@@ -8176,7 +8286,7 @@ async function writeBackup(args) {
|
|
|
8176
8286
|
absent.push(target);
|
|
8177
8287
|
continue;
|
|
8178
8288
|
}
|
|
8179
|
-
const destination =
|
|
8289
|
+
const destination = join20(dir, target);
|
|
8180
8290
|
await mkdir9(dirname12(destination), { recursive: true });
|
|
8181
8291
|
await copyFile(source, destination);
|
|
8182
8292
|
files.push(target);
|
|
@@ -8189,7 +8299,7 @@ async function writeBackup(args) {
|
|
|
8189
8299
|
};
|
|
8190
8300
|
await mkdir9(dir, { recursive: true });
|
|
8191
8301
|
await writeFile12(
|
|
8192
|
-
|
|
8302
|
+
join20(dir, RECORD_FILE),
|
|
8193
8303
|
`${JSON.stringify(record, null, 2)}
|
|
8194
8304
|
`,
|
|
8195
8305
|
"utf-8"
|
|
@@ -8197,7 +8307,7 @@ async function writeBackup(args) {
|
|
|
8197
8307
|
return rel;
|
|
8198
8308
|
}
|
|
8199
8309
|
async function listBackups(root) {
|
|
8200
|
-
const dir =
|
|
8310
|
+
const dir = join20(root, BACKUP_DIR);
|
|
8201
8311
|
if (!await pathExists(dir)) {
|
|
8202
8312
|
return [];
|
|
8203
8313
|
}
|
|
@@ -8212,25 +8322,25 @@ async function restoreLatestBackup(root) {
|
|
|
8212
8322
|
`No backup to restore \u2014 nothing under ${BACKUP_DIR}. \`saasaloy update\` writes one before it applies a plan.`
|
|
8213
8323
|
);
|
|
8214
8324
|
}
|
|
8215
|
-
const rel =
|
|
8216
|
-
const dir =
|
|
8217
|
-
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));
|
|
8218
8328
|
if (raw === void 0) {
|
|
8219
8329
|
throw new RefusalError(
|
|
8220
|
-
`${
|
|
8330
|
+
`${join20(rel, RECORD_FILE)} is missing \u2014 that backup can't be restored safely.`
|
|
8221
8331
|
);
|
|
8222
8332
|
}
|
|
8223
8333
|
let record;
|
|
8224
8334
|
try {
|
|
8225
8335
|
record = JSON.parse(raw);
|
|
8226
8336
|
} catch (error) {
|
|
8227
|
-
throw new RefusalError(`${
|
|
8337
|
+
throw new RefusalError(`${join20(rel, RECORD_FILE)} is invalid.`, {
|
|
8228
8338
|
cause: error
|
|
8229
8339
|
});
|
|
8230
8340
|
}
|
|
8231
8341
|
const restored = [];
|
|
8232
8342
|
for (const target of record.files) {
|
|
8233
|
-
const source =
|
|
8343
|
+
const source = join20(dir, target);
|
|
8234
8344
|
const destination = resolveWithinRoot(root, target);
|
|
8235
8345
|
if (!await pathExists(source)) {
|
|
8236
8346
|
continue;
|
|
@@ -8672,18 +8782,18 @@ function summarizePlan2(plan) {
|
|
|
8672
8782
|
lines.push(pc11.dim(`${writing} file(s) to apply, ${merging} needing merge`));
|
|
8673
8783
|
note9(wrapForNote(lines.join("\n")), "Plan", TUI_ON_STDERR);
|
|
8674
8784
|
for (const comparison of plan.skipped) {
|
|
8675
|
-
|
|
8785
|
+
log8.info(
|
|
8676
8786
|
`${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison)}`)}`,
|
|
8677
8787
|
TUI_ON_STDERR
|
|
8678
8788
|
);
|
|
8679
8789
|
}
|
|
8680
8790
|
for (const mod of plan.modules) {
|
|
8681
8791
|
for (const conflict of mod.depConflicts) {
|
|
8682
|
-
|
|
8792
|
+
log8.warn(`Dependency version conflict \u2014 ${conflict}.`, TUI_ON_STDERR);
|
|
8683
8793
|
}
|
|
8684
8794
|
for (const patch of mod.patches) {
|
|
8685
8795
|
if (patch.action === "missing") {
|
|
8686
|
-
|
|
8796
|
+
log8.warn(
|
|
8687
8797
|
`Config patch target ${pc11.cyan(patch.file)} is missing \u2014 ${patch.patch.kind} not applied.`,
|
|
8688
8798
|
TUI_ON_STDERR
|
|
8689
8799
|
);
|
|
@@ -8755,7 +8865,7 @@ async function canonicalPath(path) {
|
|
|
8755
8865
|
return await realpath(path);
|
|
8756
8866
|
} catch {
|
|
8757
8867
|
try {
|
|
8758
|
-
return
|
|
8868
|
+
return join21(await realpath(dirname13(path)), basename4(path));
|
|
8759
8869
|
} catch {
|
|
8760
8870
|
return path;
|
|
8761
8871
|
}
|
|
@@ -8772,7 +8882,7 @@ async function emitMergePlan(document, out) {
|
|
|
8772
8882
|
const target = resolvePlanOut(out);
|
|
8773
8883
|
await mkdir10(dirname13(target), { recursive: true });
|
|
8774
8884
|
await writeFile13(target, document, "utf-8");
|
|
8775
|
-
|
|
8885
|
+
log8.success(`Merge plan written to ${pc11.cyan(out)}`, TUI_ON_STDERR);
|
|
8776
8886
|
}
|
|
8777
8887
|
function backupTargets(plan) {
|
|
8778
8888
|
const targets = /* @__PURE__ */ new Set();
|
|
@@ -8829,10 +8939,10 @@ async function runUpdate(argv) {
|
|
|
8829
8939
|
try {
|
|
8830
8940
|
const restored = await restoreLatestBackup(root);
|
|
8831
8941
|
for (const target of restored.restored) {
|
|
8832
|
-
|
|
8942
|
+
log8.step(`${pc11.green("restore")} ${target}`, TUI_ON_STDERR);
|
|
8833
8943
|
}
|
|
8834
8944
|
for (const target of restored.removed) {
|
|
8835
|
-
|
|
8945
|
+
log8.step(
|
|
8836
8946
|
`${pc11.red("remove")} ${target} ${pc11.dim("(the update created it)")}`,
|
|
8837
8947
|
TUI_ON_STDERR
|
|
8838
8948
|
);
|
|
@@ -8899,7 +9009,7 @@ async function runUpdate(argv) {
|
|
|
8899
9009
|
if (wantsBase) {
|
|
8900
9010
|
templateDir = await baseTemplateDir();
|
|
8901
9011
|
if (project.source === "directory") {
|
|
8902
|
-
|
|
9012
|
+
log8.warn(
|
|
8903
9013
|
`No \`name\` in ${CONFIG_FILE} and no root package.json name \u2014 falling back to the directory name ${pc11.cyan(project.name)}. Set \`name\` in ${CONFIG_FILE} if that isn't this project's name.`,
|
|
8904
9014
|
TUI_ON_STDERR
|
|
8905
9015
|
);
|
|
@@ -8949,7 +9059,7 @@ async function runUpdate(argv) {
|
|
|
8949
9059
|
return EXIT_REFUSED;
|
|
8950
9060
|
}
|
|
8951
9061
|
if (recorded !== cliVersion) {
|
|
8952
|
-
|
|
9062
|
+
log8.info(
|
|
8953
9063
|
`Base recorded at CLI ${pc11.cyan(recorded)}; ${pc11.cyan(cliVersion)} is running.`,
|
|
8954
9064
|
TUI_ON_STDERR
|
|
8955
9065
|
);
|
|
@@ -8991,7 +9101,7 @@ async function runUpdate(argv) {
|
|
|
8991
9101
|
return entry && entry.source !== "local";
|
|
8992
9102
|
});
|
|
8993
9103
|
if (fromRemote.length > 0) {
|
|
8994
|
-
|
|
9104
|
+
log8.warn(
|
|
8995
9105
|
`${REGISTRY_ENV} is set, so ${fromRemote.join(", ")} will be updated from that working copy rather than ${pc11.cyan(lock.modules[fromRemote[0]]?.source ?? "the registry")} \u2014 ${pc11.dim("there is no merge base for a working copy")}.`,
|
|
8996
9106
|
TUI_ON_STDERR
|
|
8997
9107
|
);
|
|
@@ -9016,7 +9126,7 @@ async function runUpdate(argv) {
|
|
|
9016
9126
|
}
|
|
9017
9127
|
}
|
|
9018
9128
|
for (const comparison of skipped) {
|
|
9019
|
-
|
|
9129
|
+
log8.info(
|
|
9020
9130
|
`${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison, preview)}`)}`,
|
|
9021
9131
|
TUI_ON_STDERR
|
|
9022
9132
|
);
|
|
@@ -9139,7 +9249,7 @@ async function runUpdate(argv) {
|
|
|
9139
9249
|
}
|
|
9140
9250
|
}
|
|
9141
9251
|
if (missingLockEntries.size > 0) {
|
|
9142
|
-
|
|
9252
|
+
log8.warn(
|
|
9143
9253
|
`No lock entry for ${[...missingLockEntries].map((m) => pc11.cyan(m)).join(", ")} \u2014 any conflict they declare can't be checked ${pc11.dim("(re-add them to record it)")}.`,
|
|
9144
9254
|
TUI_ON_STDERR
|
|
9145
9255
|
);
|
|
@@ -9156,9 +9266,9 @@ async function runUpdate(argv) {
|
|
|
9156
9266
|
for (const comparison of skipped) {
|
|
9157
9267
|
const line = `${pc11.cyan(comparison.name)} ${pc11.dim(`\u2014 ${skipReason(comparison)}`)}`;
|
|
9158
9268
|
if (comparison.status === "unresolvable") {
|
|
9159
|
-
|
|
9269
|
+
log8.warn(line, TUI_ON_STDERR);
|
|
9160
9270
|
} else {
|
|
9161
|
-
|
|
9271
|
+
log8.info(line, TUI_ON_STDERR);
|
|
9162
9272
|
}
|
|
9163
9273
|
}
|
|
9164
9274
|
outro9(pc11.yellow("Nothing could be updated."), TUI_ON_STDERR);
|
|
@@ -9166,7 +9276,7 @@ async function runUpdate(argv) {
|
|
|
9166
9276
|
}
|
|
9167
9277
|
const pkg = await readRootPackageJson(root);
|
|
9168
9278
|
if (!pkg) {
|
|
9169
|
-
|
|
9279
|
+
log8.warn(
|
|
9170
9280
|
"No package.json at the project root \u2014 dependency pins won't be updated.",
|
|
9171
9281
|
TUI_ON_STDERR
|
|
9172
9282
|
);
|
|
@@ -9226,7 +9336,7 @@ async function runUpdate(argv) {
|
|
|
9226
9336
|
targets: backupTargets(plan),
|
|
9227
9337
|
cliVersion
|
|
9228
9338
|
});
|
|
9229
|
-
|
|
9339
|
+
log8.info(
|
|
9230
9340
|
`Pre-update copies in ${pc11.cyan(backupDir)} ${pc11.dim("\u2014 `saasaloy update --abort` restores them")}`,
|
|
9231
9341
|
TUI_ON_STDERR
|
|
9232
9342
|
);
|
|
@@ -9250,6 +9360,11 @@ async function runUpdate(argv) {
|
|
|
9250
9360
|
}
|
|
9251
9361
|
const result = outcome.value;
|
|
9252
9362
|
reportResult(result);
|
|
9363
|
+
await formatPatchedFiles(
|
|
9364
|
+
root,
|
|
9365
|
+
result.patched.map((p) => p.file),
|
|
9366
|
+
TUI_ON_STDERR
|
|
9367
|
+
);
|
|
9253
9368
|
const document = renderMergePlan(plan);
|
|
9254
9369
|
if (document) {
|
|
9255
9370
|
await emitMergePlan(document, opts.out);
|
|
@@ -9299,37 +9414,37 @@ function summarizeOutcome(plan, result) {
|
|
|
9299
9414
|
}
|
|
9300
9415
|
function reportResult(result) {
|
|
9301
9416
|
for (const name of result.prereqsInstalled) {
|
|
9302
|
-
|
|
9417
|
+
log8.step(
|
|
9303
9418
|
`${pc11.green("install")} ${pc11.cyan(name)} ${pc11.dim("(new prerequisite)")}`,
|
|
9304
9419
|
TUI_ON_STDERR
|
|
9305
9420
|
);
|
|
9306
9421
|
}
|
|
9307
9422
|
for (const file of result.written) {
|
|
9308
|
-
|
|
9423
|
+
log8.step(`${ACTION_LABEL2[file.action]} ${file.target}`, TUI_ON_STDERR);
|
|
9309
9424
|
}
|
|
9310
9425
|
for (const file of result.deleted) {
|
|
9311
|
-
|
|
9426
|
+
log8.step(`${pc11.red("delete")} ${file.target}`, TUI_ON_STDERR);
|
|
9312
9427
|
}
|
|
9313
9428
|
for (const file of result.untracked) {
|
|
9314
|
-
|
|
9429
|
+
log8.step(
|
|
9315
9430
|
`${pc11.dim("untrack")} ${file.target} ${pc11.dim("(already gone)")}`,
|
|
9316
9431
|
TUI_ON_STDERR
|
|
9317
9432
|
);
|
|
9318
9433
|
}
|
|
9319
9434
|
for (const patch of result.patched) {
|
|
9320
|
-
|
|
9435
|
+
log8.step(
|
|
9321
9436
|
`${pc11.green("patch")} ${patch.file} ${pc11.dim(`\u2014 ${patch.patch.kind}`)}`,
|
|
9322
9437
|
TUI_ON_STDERR
|
|
9323
9438
|
);
|
|
9324
9439
|
}
|
|
9325
9440
|
for (const link of result.linkConflicts) {
|
|
9326
|
-
|
|
9441
|
+
log8.warn(
|
|
9327
9442
|
`Skill link ${pc11.cyan(link.path)} left untouched \u2014 not ours to replace.`,
|
|
9328
9443
|
TUI_ON_STDERR
|
|
9329
9444
|
);
|
|
9330
9445
|
}
|
|
9331
9446
|
for (const dep of result.depsWritten) {
|
|
9332
|
-
|
|
9447
|
+
log8.step(`${pc11.cyan("dep")} ${dep.name}@${dep.version}`, TUI_ON_STDERR);
|
|
9333
9448
|
}
|
|
9334
9449
|
if (result.lateDrift.length > 0) {
|
|
9335
9450
|
const lines = result.lateDrift.map((f) => ` ${pc11.yellow("kept")} ${f.target}`).join("\n");
|
|
@@ -9409,8 +9524,8 @@ ${pc12.dim("Set SAASALOY_DEBUG=1 to print the full cause chain behind a failure.
|
|
|
9409
9524
|
);
|
|
9410
9525
|
}
|
|
9411
9526
|
async function pickCommand(deps) {
|
|
9412
|
-
const { registry, select:
|
|
9413
|
-
const picked = await
|
|
9527
|
+
const { registry, select: select5, isCancel: isCancel7 } = deps;
|
|
9528
|
+
const picked = await select5({
|
|
9414
9529
|
message: "What would you like to do?",
|
|
9415
9530
|
options: Object.entries(registry).map(([name, command2]) => ({
|
|
9416
9531
|
value: name,
|
|
@@ -9431,7 +9546,7 @@ async function pickCommand(deps) {
|
|
|
9431
9546
|
async function main(argv, deps = {}) {
|
|
9432
9547
|
const {
|
|
9433
9548
|
registry = COMMANDS,
|
|
9434
|
-
select:
|
|
9549
|
+
select: select5 = clackSelect,
|
|
9435
9550
|
isCancel: isCancel7 = clackIsCancel
|
|
9436
9551
|
} = deps;
|
|
9437
9552
|
const [name, ...rest] = argv;
|
|
@@ -9448,7 +9563,7 @@ async function main(argv, deps = {}) {
|
|
|
9448
9563
|
printHelp(registry);
|
|
9449
9564
|
return EXIT_OK;
|
|
9450
9565
|
}
|
|
9451
|
-
return pickCommand({ registry, select:
|
|
9566
|
+
return pickCommand({ registry, select: select5, isCancel: isCancel7 });
|
|
9452
9567
|
}
|
|
9453
9568
|
const command = Object.hasOwn(registry, name) ? registry[name] : void 0;
|
|
9454
9569
|
if (!command) {
|