nymbal 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +927 -62
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { defineCommand as
|
|
2
|
+
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
3
3
|
|
|
4
4
|
// src/utils/version.ts
|
|
5
5
|
import { readFileSync } from "fs";
|
|
@@ -26,8 +26,10 @@ function getCliVersion() {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// src/commands/dev.ts
|
|
29
|
+
import { resolve as resolve4 } from "path";
|
|
29
30
|
import { defineCommand as defineCommand2 } from "citty";
|
|
30
31
|
import pc from "picocolors";
|
|
32
|
+
import { KNOWN_PROVIDERS } from "@nymbalhq/config";
|
|
31
33
|
|
|
32
34
|
// src/utils/project.ts
|
|
33
35
|
import { existsSync } from "fs";
|
|
@@ -43,22 +45,22 @@ async function loadProject(startDir = process.cwd()) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// src/commands/dev.ts
|
|
46
|
-
import { createApp, runSeed } from "@nymbalhq/platform";
|
|
48
|
+
import { createApp, createLogger, runSeed, sqliteFile } from "@nymbalhq/platform";
|
|
47
49
|
import { createHttpServer } from "@nymbalhq/http";
|
|
48
50
|
|
|
49
51
|
// src/utils/spawn.ts
|
|
50
52
|
import { spawn } from "child_process";
|
|
51
53
|
function run(command, args, options = {}) {
|
|
52
|
-
const
|
|
54
|
+
const child2 = spawn(command, args, {
|
|
53
55
|
stdio: "inherit",
|
|
54
56
|
shell: false,
|
|
55
57
|
...options
|
|
56
58
|
});
|
|
57
|
-
const done = new Promise((
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const done = new Promise((resolve6, reject) => {
|
|
60
|
+
child2.on("error", reject);
|
|
61
|
+
child2.on("exit", (code) => resolve6(code ?? 0));
|
|
60
62
|
});
|
|
61
|
-
return { child, done };
|
|
63
|
+
return { child: child2, done };
|
|
62
64
|
}
|
|
63
65
|
async function runOrFail(command, args, options = {}) {
|
|
64
66
|
const { done } = run(command, args, options);
|
|
@@ -71,16 +73,16 @@ async function runOrFail(command, args, options = {}) {
|
|
|
71
73
|
// src/utils/port.ts
|
|
72
74
|
import { createServer } from "net";
|
|
73
75
|
async function isPortFree(port, host) {
|
|
74
|
-
return new Promise((
|
|
76
|
+
return new Promise((resolve6) => {
|
|
75
77
|
const server = createServer();
|
|
76
|
-
server.once("error", (
|
|
77
|
-
if (
|
|
78
|
-
console.error(`[dev] unexpected error probing port ${port}:`,
|
|
78
|
+
server.once("error", (err2) => {
|
|
79
|
+
if (err2.code !== "EADDRINUSE" && err2.code !== "EACCES") {
|
|
80
|
+
console.error(`[dev] unexpected error probing port ${port}:`, err2.message);
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
resolve6(false);
|
|
81
83
|
});
|
|
82
84
|
server.once("listening", () => {
|
|
83
|
-
server.close(() =>
|
|
85
|
+
server.close(() => resolve6(true));
|
|
84
86
|
});
|
|
85
87
|
if (host) {
|
|
86
88
|
server.listen(port, host);
|
|
@@ -233,16 +235,16 @@ async function runShutdown(steps, opts) {
|
|
|
233
235
|
pending = step.name;
|
|
234
236
|
try {
|
|
235
237
|
await step.run();
|
|
236
|
-
} catch (
|
|
238
|
+
} catch (err2) {
|
|
237
239
|
failed = true;
|
|
238
|
-
opts.logger.error({ err, step: step.name }, `Shutdown step "${step.name}" failed`);
|
|
240
|
+
opts.logger.error({ err: err2, step: step.name }, `Shutdown step "${step.name}" failed`);
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
pending = void 0;
|
|
242
244
|
})();
|
|
243
245
|
let timer;
|
|
244
|
-
const bound = new Promise((
|
|
245
|
-
timer = setTimeout(() =>
|
|
246
|
+
const bound = new Promise((resolve6) => {
|
|
247
|
+
timer = setTimeout(() => resolve6("bound"), opts.boundMs);
|
|
246
248
|
});
|
|
247
249
|
const outcome = await Promise.race([work.then(() => "done"), bound]);
|
|
248
250
|
clearTimeout(timer);
|
|
@@ -256,6 +258,63 @@ async function runShutdown(steps, opts) {
|
|
|
256
258
|
return opts.exit(failed ? 1 : 0);
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
// src/commands/dev-summary.ts
|
|
262
|
+
import { displayUrl } from "@nymbalhq/types";
|
|
263
|
+
var WIDTH = 100;
|
|
264
|
+
var LABEL = 14;
|
|
265
|
+
var NAME_COLUMN = 20;
|
|
266
|
+
var columns = (text2) => [...text2].length;
|
|
267
|
+
function fit(text2, width = WIDTH) {
|
|
268
|
+
return columns(text2) <= width ? text2 : `${[...text2].slice(0, width - 1).join("")}\u2026`;
|
|
269
|
+
}
|
|
270
|
+
function row(label, value) {
|
|
271
|
+
return fit(` ${label.padEnd(LABEL)} ${value}`);
|
|
272
|
+
}
|
|
273
|
+
function capabilityList(capabilities, width) {
|
|
274
|
+
if (capabilities.length === 0) return "\u2014";
|
|
275
|
+
const all = capabilities.join(", ");
|
|
276
|
+
if (columns(all) <= width) return all;
|
|
277
|
+
for (let shown = capabilities.length - 1; shown > 0; shown--) {
|
|
278
|
+
const text2 = `${capabilities.slice(0, shown).join(", ")}, \u2026 (+${capabilities.length - shown})`;
|
|
279
|
+
if (columns(text2) <= width) return text2;
|
|
280
|
+
}
|
|
281
|
+
return `\u2026 (+${capabilities.length})`;
|
|
282
|
+
}
|
|
283
|
+
function renderDevSummary(info) {
|
|
284
|
+
const slotWidth = Math.min(NAME_COLUMN, Math.max(...info.slots.map((s) => columns(s.slot)), 0));
|
|
285
|
+
const providerWidth = Math.min(
|
|
286
|
+
NAME_COLUMN,
|
|
287
|
+
Math.max(...info.slots.map((s) => columns(s.provider)), 0)
|
|
288
|
+
);
|
|
289
|
+
const slotLines = info.slots.map(({ slot, provider, capabilities }) => {
|
|
290
|
+
const lead = ` ${slot.padEnd(slotWidth)} ${provider.padEnd(providerWidth)} `;
|
|
291
|
+
return fit(`${lead}${capabilityList(capabilities, WIDTH - columns(lead))}`);
|
|
292
|
+
});
|
|
293
|
+
const commandStore = info.commandStore.kind === "sqlite" ? `sqlite ${info.commandStore.path}` : `postgres ${displayUrl(info.commandStore.url)}`;
|
|
294
|
+
const lines = [
|
|
295
|
+
fit(` ${info.store}`),
|
|
296
|
+
"",
|
|
297
|
+
row("API", info.apiUrl),
|
|
298
|
+
row(
|
|
299
|
+
"storefront",
|
|
300
|
+
info.storefront === null ? "not started (--api-only)" : `${info.storefront.url} (${info.storefront.template})`
|
|
301
|
+
),
|
|
302
|
+
row(
|
|
303
|
+
"admin",
|
|
304
|
+
info.admin === null ? "not built \u2014 run pnpm --filter @nymbalhq/admin build" : info.admin.url
|
|
305
|
+
),
|
|
306
|
+
row("command store", commandStore),
|
|
307
|
+
"",
|
|
308
|
+
...slotLines,
|
|
309
|
+
"",
|
|
310
|
+
...info.primitives.map(({ name, provider }) => row(name, provider)),
|
|
311
|
+
"",
|
|
312
|
+
" Debug logs: run with --verbose (or LOG_LEVEL=debug)."
|
|
313
|
+
];
|
|
314
|
+
return `${lines.join("\n")}
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
259
318
|
// src/commands/dev.ts
|
|
260
319
|
function defaultStorefrontPort(template) {
|
|
261
320
|
return template === "astro" ? 4321 : 3e3;
|
|
@@ -269,13 +328,7 @@ function preferredStorefrontPort(template, flagPort, env) {
|
|
|
269
328
|
}
|
|
270
329
|
return defaultStorefrontPort(template);
|
|
271
330
|
}
|
|
272
|
-
function storefrontEnv(
|
|
273
|
-
const stubPayments = config.commerce.payments.provider === "stripe" ? {} : {
|
|
274
|
-
PUBLIC_NYMBAL_PAYMENTS_PROVIDER: "native-stub",
|
|
275
|
-
// Astro
|
|
276
|
-
NEXT_PUBLIC_NYMBAL_PAYMENTS_PROVIDER: "native-stub"
|
|
277
|
-
// Next.js
|
|
278
|
-
};
|
|
331
|
+
function storefrontEnv(_config, apiUrl, storefrontPort, baseEnv = process.env) {
|
|
279
332
|
return {
|
|
280
333
|
...baseEnv,
|
|
281
334
|
NYMBAL_API_URL: apiUrl,
|
|
@@ -285,8 +338,31 @@ function storefrontEnv(config, apiUrl, storefrontPort, baseEnv = process.env) {
|
|
|
285
338
|
// Next.js
|
|
286
339
|
// The framework also reads the port flag; the env var keeps the scaffold
|
|
287
340
|
// config (astro.config.mjs) in sync when it honours NYMBAL_STOREFRONT_PORT.
|
|
288
|
-
NYMBAL_STOREFRONT_PORT: String(storefrontPort)
|
|
289
|
-
|
|
341
|
+
NYMBAL_STOREFRONT_PORT: String(storefrontPort)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function devLogLevel(verbose, env) {
|
|
345
|
+
const fromEnv = env["LOG_LEVEL"];
|
|
346
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
347
|
+
return verbose ? "debug" : "info";
|
|
348
|
+
}
|
|
349
|
+
function summaryInfo(config, app, apiUrl, storefront, adminBuilt) {
|
|
350
|
+
const report = app.capabilities();
|
|
351
|
+
return {
|
|
352
|
+
store: config.store.name,
|
|
353
|
+
apiUrl,
|
|
354
|
+
storefront,
|
|
355
|
+
admin: adminBuilt ? { url: `${apiUrl}/admin/` } : null,
|
|
356
|
+
commandStore: config.infrastructure.commandStore === "sqlite" ? { kind: "sqlite", path: resolve4(sqliteFile()) } : { kind: "postgres", url: process.env["DATABASE_URL"] ?? "" },
|
|
357
|
+
slots: Object.keys(KNOWN_PROVIDERS).map((slot) => ({
|
|
358
|
+
slot,
|
|
359
|
+
provider: report[slot].provider,
|
|
360
|
+
capabilities: report[slot].capabilities
|
|
361
|
+
})),
|
|
362
|
+
primitives: ["documentStore", "eventBus", "media"].map((name) => ({
|
|
363
|
+
name,
|
|
364
|
+
provider: report[name].provider
|
|
365
|
+
}))
|
|
290
366
|
};
|
|
291
367
|
}
|
|
292
368
|
function apiPort(config, flagPort) {
|
|
@@ -328,6 +404,11 @@ var devCommand = defineCommand2({
|
|
|
328
404
|
type: "string",
|
|
329
405
|
alias: "storefront-port",
|
|
330
406
|
description: "Port for the storefront dev server (default: 4321 Astro / 3000 Next.js). If busy, the next free port is used. Also settable via NYMBAL_STOREFRONT_PORT."
|
|
407
|
+
},
|
|
408
|
+
verbose: {
|
|
409
|
+
type: "boolean",
|
|
410
|
+
description: "Log at debug (LOG_LEVEL, when set, wins)",
|
|
411
|
+
default: false
|
|
331
412
|
}
|
|
332
413
|
},
|
|
333
414
|
async run({ args }) {
|
|
@@ -337,7 +418,15 @@ var devCommand = defineCommand2({
|
|
|
337
418
|
const config = { ...loaded.config, http: { ...loaded.config.http, port } };
|
|
338
419
|
const version = getCliVersion();
|
|
339
420
|
await migrateProject(config, projectRoot);
|
|
340
|
-
const
|
|
421
|
+
const logger = createLogger({
|
|
422
|
+
base: { store: config.store.name },
|
|
423
|
+
level: devLogLevel(args.verbose, process.env)
|
|
424
|
+
});
|
|
425
|
+
const app = await createApp(config, {
|
|
426
|
+
version,
|
|
427
|
+
logger,
|
|
428
|
+
...loaded.adapters !== void 0 && { adapters: loaded.adapters }
|
|
429
|
+
});
|
|
341
430
|
const { platform } = app;
|
|
342
431
|
if (args.seed) {
|
|
343
432
|
const existing = await platform.documentStore.query("products", {
|
|
@@ -363,15 +452,12 @@ var devCommand = defineCommand2({
|
|
|
363
452
|
version
|
|
364
453
|
});
|
|
365
454
|
const adminRoot = resolveAdminRoot();
|
|
366
|
-
if (adminRoot === null) {
|
|
367
|
-
console.log(
|
|
368
|
-
pc.yellow(
|
|
369
|
-
"\u2192 The admin is not built, so /admin/ answers 404: run `pnpm --filter @nymbalhq/admin build`."
|
|
370
|
-
)
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
455
|
await app.attachHttp(http.adapter, { adminRoot });
|
|
374
456
|
await http.start();
|
|
457
|
+
const apiUrl = `http://localhost:${config.http.port}`;
|
|
458
|
+
const printSummary = (storefront) => {
|
|
459
|
+
console.log(renderDevSummary(summaryInfo(config, app, apiUrl, storefront, adminRoot !== null)));
|
|
460
|
+
};
|
|
375
461
|
const stopApi = [
|
|
376
462
|
{ name: "http.stop", run: () => http.stop() },
|
|
377
463
|
{ name: "app.stop", run: () => app.stop() }
|
|
@@ -382,6 +468,7 @@ var devCommand = defineCommand2({
|
|
|
382
468
|
exit: (code) => process.exit(code)
|
|
383
469
|
});
|
|
384
470
|
if (args.apiOnly) {
|
|
471
|
+
printSummary(null);
|
|
385
472
|
const shutdown2 = (signal) => {
|
|
386
473
|
platform.logger.info({ signal }, "Shutting down API");
|
|
387
474
|
return shutdownWith(stopApi);
|
|
@@ -402,18 +489,12 @@ var devCommand = defineCommand2({
|
|
|
402
489
|
);
|
|
403
490
|
}
|
|
404
491
|
const launch = planTemplateLaunch(projectRoot, config.template, storefrontPort);
|
|
405
|
-
const apiUrl = `http://localhost:${config.http.port}`;
|
|
406
492
|
const storefrontUrl = `http://localhost:${storefrontPort}`;
|
|
407
|
-
console.log(
|
|
408
|
-
pc.cyan(
|
|
409
|
-
`\u2192 Starting storefront (${pc.bold(config.template)}, ${launch.mode}) on ${pc.bold(storefrontUrl)}`
|
|
410
|
-
)
|
|
411
|
-
);
|
|
412
|
-
console.log(pc.dim(` API: ${apiUrl}`));
|
|
413
493
|
const templateProc = run(launch.command, launch.args, {
|
|
414
494
|
cwd: launch.cwd,
|
|
415
495
|
env: storefrontEnv(config, apiUrl, storefrontPort)
|
|
416
496
|
});
|
|
497
|
+
printSummary({ template: `${config.template}, ${launch.mode}`, url: storefrontUrl });
|
|
417
498
|
let shuttingDown = false;
|
|
418
499
|
const shutdown = (signal) => {
|
|
419
500
|
shuttingDown = true;
|
|
@@ -462,13 +543,13 @@ var buildCommand = defineCommand3({
|
|
|
462
543
|
|
|
463
544
|
// src/commands/seed.ts
|
|
464
545
|
import { defineCommand as defineCommand4 } from "citty";
|
|
465
|
-
import { createPlatform, runSeed as runSeed2 } from "@nymbalhq/platform";
|
|
546
|
+
import { createPlatform, resolvePrimitives, runSeed as runSeed2 } from "@nymbalhq/platform";
|
|
466
547
|
var seedCommand = defineCommand4({
|
|
467
548
|
meta: { name: "seed", description: "Re-seed demo content (products, categories)" },
|
|
468
549
|
async run() {
|
|
469
550
|
const { config, projectRoot } = await loadProject();
|
|
470
551
|
await migrateProject(config, projectRoot);
|
|
471
|
-
const platform = createPlatform(config);
|
|
552
|
+
const platform = createPlatform(config, await resolvePrimitives(config));
|
|
472
553
|
try {
|
|
473
554
|
const result = await runSeed2({
|
|
474
555
|
config,
|
|
@@ -490,7 +571,7 @@ var seedCommand = defineCommand4({
|
|
|
490
571
|
// src/commands/import.ts
|
|
491
572
|
import { defineCommand as defineCommand5 } from "citty";
|
|
492
573
|
import { intro, outro, text, select, spinner, isCancel, cancel } from "@clack/prompts";
|
|
493
|
-
import
|
|
574
|
+
import p from "picocolors";
|
|
494
575
|
import { createApp as createApp2 } from "@nymbalhq/platform";
|
|
495
576
|
import { runWooCommerceImport, writeReport } from "@nymbalhq/importers";
|
|
496
577
|
import { join as join3 } from "path";
|
|
@@ -635,6 +716,7 @@ var importCommand = defineCommand5({
|
|
|
635
716
|
services: app.services,
|
|
636
717
|
repos: app.repos,
|
|
637
718
|
adapters: { ai: app.adapters.ai, reviews: app.adapters.reviews },
|
|
719
|
+
media: app.platform.media,
|
|
638
720
|
onProgress(event) {
|
|
639
721
|
if (event.kind === "phase") {
|
|
640
722
|
console.log(p.cyan(` \u2192 ${event.phase}`));
|
|
@@ -680,34 +762,815 @@ var configValidateCommand = defineCommand6({
|
|
|
680
762
|
const { config, path } = await loadProject();
|
|
681
763
|
console.log(pc2.green(`\u2713 config valid`) + ` (${path})`);
|
|
682
764
|
console.log(
|
|
683
|
-
` template: ${config.template},
|
|
765
|
+
` template: ${config.template}, commandStore: ${config.infrastructure.commandStore}, documentStore: ${config.infrastructure.documentStore}, eventBus: ${config.infrastructure.eventBus}`
|
|
684
766
|
);
|
|
685
767
|
process.exit(0);
|
|
686
|
-
} catch (
|
|
687
|
-
if (
|
|
768
|
+
} catch (err2) {
|
|
769
|
+
if (err2 instanceof NymbalError) {
|
|
688
770
|
console.error(pc2.red("\u2717 config invalid"));
|
|
689
|
-
console.error(
|
|
771
|
+
console.error(err2.message);
|
|
690
772
|
} else {
|
|
691
|
-
console.error(
|
|
773
|
+
console.error(err2);
|
|
692
774
|
}
|
|
693
775
|
process.exit(1);
|
|
694
776
|
}
|
|
695
777
|
}
|
|
696
778
|
});
|
|
697
779
|
|
|
698
|
-
// src/commands/
|
|
780
|
+
// src/commands/add.ts
|
|
781
|
+
import { spawnSync } from "child_process";
|
|
782
|
+
import { existsSync as existsSync6, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
783
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
784
|
+
import { parseEnv } from "util";
|
|
699
785
|
import { defineCommand as defineCommand7 } from "citty";
|
|
700
|
-
import
|
|
701
|
-
|
|
786
|
+
import pc3 from "picocolors";
|
|
787
|
+
import { findConfigFile as findConfigFile2, loadConfig as loadConfig2 } from "@nymbalhq/config";
|
|
788
|
+
import { installLine } from "@nymbalhq/platform";
|
|
789
|
+
|
|
790
|
+
// src/recipes/holders.ts
|
|
791
|
+
import { existsSync as existsSync5 } from "fs";
|
|
792
|
+
import { readFile } from "fs/promises";
|
|
793
|
+
import { createRequire as createRequire2 } from "module";
|
|
794
|
+
import { dirname as dirname5, join as join4, resolve as resolve5 } from "path";
|
|
795
|
+
var RECIPE_HOLDERS = {
|
|
796
|
+
postgres: "@nymbalhq/platform",
|
|
797
|
+
redis: "@nymbalhq/doc-store-redis",
|
|
798
|
+
s3: "@nymbalhq/media-s3",
|
|
799
|
+
stripe: "@nymbalhq/adapter-stripe"
|
|
800
|
+
};
|
|
801
|
+
function knownRecipes() {
|
|
802
|
+
return Object.keys(RECIPE_HOLDERS).sort();
|
|
803
|
+
}
|
|
804
|
+
function isKnownRecipe(name) {
|
|
805
|
+
return Object.hasOwn(RECIPE_HOLDERS, name);
|
|
806
|
+
}
|
|
807
|
+
function unknownRecipe(name) {
|
|
808
|
+
return new Error(`unknown recipe '${name}': the recipes are ${knownRecipes().join(", ")}`);
|
|
809
|
+
}
|
|
810
|
+
function packageDir(pkg, projectRoot) {
|
|
811
|
+
let dir = resolve5(projectRoot);
|
|
812
|
+
while (true) {
|
|
813
|
+
const candidate = join4(dir, "node_modules", pkg);
|
|
814
|
+
if (existsSync5(join4(candidate, "package.json"))) return candidate;
|
|
815
|
+
const parent = dirname5(dir);
|
|
816
|
+
if (parent === dir) return null;
|
|
817
|
+
dir = parent;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function recipeFile(name, projectRoot) {
|
|
821
|
+
const holder = RECIPE_HOLDERS[name];
|
|
822
|
+
if (holder === void 0 || !isKnownRecipe(name)) throw unknownRecipe(name);
|
|
823
|
+
const dir = packageDir(holder, projectRoot);
|
|
824
|
+
if (dir === null) return null;
|
|
825
|
+
try {
|
|
826
|
+
return createRequire2(join4(dir, "package.json")).resolve(`${holder}/recipes/${name}.json`);
|
|
827
|
+
} catch (error) {
|
|
828
|
+
const code = error.code;
|
|
829
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED") return null;
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function isInstalled(pkg, projectRoot) {
|
|
834
|
+
return packageDir(pkg, projectRoot) !== null;
|
|
835
|
+
}
|
|
836
|
+
async function readRecipe(name, projectRoot) {
|
|
837
|
+
const file = recipeFile(name, projectRoot);
|
|
838
|
+
if (file === null) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`${RECIPE_HOLDERS[name]} is not installed in ${projectRoot}, so its ${name} recipe cannot be read`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const recipe = JSON.parse(await readFile(file, "utf8"));
|
|
844
|
+
const malformed = recipe === null || typeof recipe !== "object" || recipe.name !== name || !Array.isArray(recipe.install) || !Array.isArray(recipe.set) || !Array.isArray(recipe.env) || !Array.isArray(recipe.next);
|
|
845
|
+
if (malformed) throw new Error(`${file} is not a ${name} recipe`);
|
|
846
|
+
return recipe;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/recipes/config-edit.ts
|
|
850
|
+
import { parseModule } from "magicast";
|
|
851
|
+
var SCAFFOLD_DEFAULTS = [
|
|
852
|
+
"sqlite",
|
|
853
|
+
"in-memory",
|
|
854
|
+
"in-process",
|
|
855
|
+
"local-fs",
|
|
856
|
+
"native"
|
|
857
|
+
];
|
|
858
|
+
var CONFIG_MODULE = "@nymbalhq/config";
|
|
859
|
+
var child = (node, key) => node[key];
|
|
860
|
+
var children = (node, key) => node[key];
|
|
861
|
+
function parse(lf) {
|
|
862
|
+
return parseModule(lf, { tabWidth: 1 }).$ast;
|
|
863
|
+
}
|
|
864
|
+
var lfCopy = (source) => source.replace(/\r\n/g, "\n");
|
|
865
|
+
function unwrap(node) {
|
|
866
|
+
let at = node;
|
|
867
|
+
while (at !== void 0 && (at.type === "TSAsExpression" || at.type === "TSSatisfiesExpression" || at.type === "TSNonNullExpression" || at.type === "ParenthesizedExpression")) {
|
|
868
|
+
at = child(at, "expression");
|
|
869
|
+
}
|
|
870
|
+
return at;
|
|
871
|
+
}
|
|
872
|
+
function nameOf(node) {
|
|
873
|
+
if (node.type === "Identifier") return node["name"];
|
|
874
|
+
if (node.type === "StringLiteral") return node["value"];
|
|
875
|
+
if (node.type === "NumericLiteral") return String(node["value"]);
|
|
876
|
+
return void 0;
|
|
877
|
+
}
|
|
878
|
+
function configImports(program) {
|
|
879
|
+
return children(program, "body").filter(
|
|
880
|
+
(n) => n.type === "ImportDeclaration" && child(n, "source")["value"] === CONFIG_MODULE && n["importKind"] !== "type"
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
function importedAs(program, imported) {
|
|
884
|
+
for (const decl of configImports(program)) {
|
|
885
|
+
for (const spec of children(decl, "specifiers")) {
|
|
886
|
+
if (spec.type !== "ImportSpecifier" || spec["importKind"] === "type") continue;
|
|
887
|
+
if (nameOf(child(spec, "imported")) === imported)
|
|
888
|
+
return child(spec, "local")["name"];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
function bindsEnv(program) {
|
|
894
|
+
const bound = (id) => id?.type === "Identifier" && id["name"] === "env";
|
|
895
|
+
return children(program, "body").some((n) => {
|
|
896
|
+
const decl = n.type === "ExportNamedDeclaration" ? child(n, "declaration") : n;
|
|
897
|
+
if (decl === null || decl === void 0) return false;
|
|
898
|
+
if (decl.type === "ImportDeclaration")
|
|
899
|
+
return children(decl, "specifiers").some((s) => bound(child(s, "local")));
|
|
900
|
+
if (decl.type === "VariableDeclaration")
|
|
901
|
+
return children(decl, "declarations").some((d) => bound(child(d, "id")));
|
|
902
|
+
return bound(child(decl, "id"));
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
function locate(source) {
|
|
906
|
+
const program = parse(source);
|
|
907
|
+
const exported = children(program, "body").find((n) => n.type === "ExportDefaultDeclaration");
|
|
908
|
+
if (exported === void 0) return void 0;
|
|
909
|
+
let config = unwrap(child(exported, "declaration"));
|
|
910
|
+
if (config?.type === "CallExpression") {
|
|
911
|
+
const callee = child(config, "callee");
|
|
912
|
+
const define = importedAs(program, "defineConfig") ?? "defineConfig";
|
|
913
|
+
if (callee.type !== "Identifier" || callee["name"] !== define) return void 0;
|
|
914
|
+
config = unwrap(children(config, "arguments")[0]);
|
|
915
|
+
}
|
|
916
|
+
if (config?.type !== "ObjectExpression") return void 0;
|
|
917
|
+
if (source[config.start] !== "{" || source[config.end - 1] !== "}") return void 0;
|
|
918
|
+
return {
|
|
919
|
+
program,
|
|
920
|
+
config,
|
|
921
|
+
envName: importedAs(program, "env"),
|
|
922
|
+
envBound: bindsEnv(program),
|
|
923
|
+
namedImport: configImports(program).find(
|
|
924
|
+
(d) => children(d, "specifiers").some((s) => s.type === "ImportSpecifier")
|
|
925
|
+
)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function isEnvRef(value) {
|
|
929
|
+
if (typeof value !== "object" || value === null) return false;
|
|
930
|
+
const keys = Object.keys(value);
|
|
931
|
+
return typeof value.env === "string" && keys.every((k) => k === "env" || k === "default");
|
|
932
|
+
}
|
|
933
|
+
function evaluate(node, envName) {
|
|
934
|
+
const at = unwrap(node);
|
|
935
|
+
switch (at.type) {
|
|
936
|
+
case "StringLiteral":
|
|
937
|
+
case "NumericLiteral":
|
|
938
|
+
case "BooleanLiteral":
|
|
939
|
+
return at["value"];
|
|
940
|
+
case "TemplateLiteral": {
|
|
941
|
+
const quasis = children(at, "quasis");
|
|
942
|
+
return children(at, "expressions").length === 0 ? (quasis[0]?.["value"]).cooked : void 0;
|
|
943
|
+
}
|
|
944
|
+
case "ObjectExpression": {
|
|
945
|
+
const out2 = {};
|
|
946
|
+
for (const prop of children(at, "properties")) {
|
|
947
|
+
const key = prop.type === "ObjectProperty" && !prop["computed"] ? nameOf(child(prop, "key")) : void 0;
|
|
948
|
+
const value = key === void 0 ? void 0 : evaluate(child(prop, "value"), envName);
|
|
949
|
+
if (key === void 0 || value === void 0) return void 0;
|
|
950
|
+
out2[key] = value;
|
|
951
|
+
}
|
|
952
|
+
return out2;
|
|
953
|
+
}
|
|
954
|
+
case "CallExpression": {
|
|
955
|
+
const callee = child(at, "callee");
|
|
956
|
+
const args = children(at, "arguments");
|
|
957
|
+
if (callee.type !== "Identifier" || callee["name"] !== (envName ?? "env")) return void 0;
|
|
958
|
+
if (args.length < 1 || args.length > 2 || args.some((a) => a.type !== "StringLiteral"))
|
|
959
|
+
return void 0;
|
|
960
|
+
return {
|
|
961
|
+
env: args[0]["value"],
|
|
962
|
+
...args[1] !== void 0 && { default: args[1]["value"] }
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
default:
|
|
966
|
+
return void 0;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function sameValue(a, b) {
|
|
970
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return a === b;
|
|
971
|
+
const ka = Object.keys(a).sort();
|
|
972
|
+
const kb = Object.keys(b).sort();
|
|
973
|
+
return ka.length === kb.length && ka.every(
|
|
974
|
+
(k, i) => k === kb[i] && sameValue(a[k], b[k])
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
function isScaffoldDefault(value) {
|
|
978
|
+
if (typeof value === "string") return SCAFFOLD_DEFAULTS.includes(value);
|
|
979
|
+
const provider = value?.provider;
|
|
980
|
+
return typeof provider === "string" && SCAFFOLD_DEFAULTS.includes(provider);
|
|
981
|
+
}
|
|
982
|
+
function styleOf(source, mod) {
|
|
983
|
+
const firstString = /(['"])(?:(?!\1)[^\\\r\n]|\\.)*\1/.exec(source.slice(mod.config.start));
|
|
984
|
+
const indented = /\n([ \t]+)\S/.exec(source.slice(mod.config.start));
|
|
985
|
+
return {
|
|
986
|
+
quote: firstString?.[1] ?? "'",
|
|
987
|
+
unit: indented?.[1] ?? " ",
|
|
988
|
+
envName: mod.envName ?? "env"
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function quoted(text2, style) {
|
|
992
|
+
const q = style.quote;
|
|
993
|
+
const body = text2.replace(/\\/g, "\\\\").replace(new RegExp(q, "g"), `\\${q}`).replace(/\r/g, "\\r").replace(/\n/g, "\\n");
|
|
994
|
+
return `${q}${body}${q}`;
|
|
995
|
+
}
|
|
996
|
+
function renderKey(key, style) {
|
|
997
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : quoted(key, style);
|
|
998
|
+
}
|
|
999
|
+
function render(value, indent, style) {
|
|
1000
|
+
if (typeof value === "string") return quoted(value, style);
|
|
1001
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1002
|
+
if (isEnvRef(value)) {
|
|
1003
|
+
const fallback = value.default === void 0 ? "" : `, ${quoted(value.default, style)}`;
|
|
1004
|
+
return `${style.envName}(${quoted(value.env, style)}${fallback})`;
|
|
1005
|
+
}
|
|
1006
|
+
const entries = Object.entries(value);
|
|
1007
|
+
if (entries.length === 0) return "{}";
|
|
1008
|
+
const inner = indent + style.unit;
|
|
1009
|
+
const lines = entries.map(
|
|
1010
|
+
([k, v]) => `${inner}${renderKey(k, style)}: ${render(v, inner, style)},`
|
|
1011
|
+
);
|
|
1012
|
+
return `{
|
|
1013
|
+
${lines.join("\n")}
|
|
1014
|
+
${indent}}`;
|
|
1015
|
+
}
|
|
1016
|
+
function writesEnv(value) {
|
|
1017
|
+
if (isEnvRef(value)) return true;
|
|
1018
|
+
return typeof value === "object" && Object.values(value).some(writesEnv);
|
|
1019
|
+
}
|
|
1020
|
+
function indentAt(source, offset) {
|
|
1021
|
+
const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
|
|
1022
|
+
return /^[ \t]*/.exec(source.slice(lineStart))[0];
|
|
1023
|
+
}
|
|
1024
|
+
function spliced(source, edits) {
|
|
1025
|
+
const crlf = [];
|
|
1026
|
+
for (const m of source.matchAll(/\r\n/g)) crlf.push(m.index - crlf.length);
|
|
1027
|
+
const at = (lf) => lf + crlf.filter((p2) => p2 < lf).length;
|
|
1028
|
+
const lastBreak = /\r?\n(?![\s\S]*\n)/.exec(source)?.[0] ?? "\n";
|
|
1029
|
+
return edits.map((e) => {
|
|
1030
|
+
const start = at(e.at);
|
|
1031
|
+
const end = at(e.at + e.remove);
|
|
1032
|
+
const eol = /\r?\n/.exec(source.slice(end))?.[0] ?? lastBreak;
|
|
1033
|
+
return { start, end, text: e.text.replace(/\n/g, eol) };
|
|
1034
|
+
}).sort((a, b) => b.start - a.start).reduce((s, e) => s.slice(0, e.start) + e.text + s.slice(e.end), source);
|
|
1035
|
+
}
|
|
1036
|
+
function addProperty(source, object, key, value, style) {
|
|
1037
|
+
const props = children(object, "properties");
|
|
1038
|
+
const close = object.end - 1;
|
|
1039
|
+
const last = props.at(-1);
|
|
1040
|
+
if (last === void 0) {
|
|
1041
|
+
const indent2 = indentAt(source, object.start);
|
|
1042
|
+
const inside = source.slice(object.start + 1, close);
|
|
1043
|
+
const text2 = `${indent2}${style.unit}${renderKey(key, style)}: ${render(value, indent2 + style.unit, style)},`;
|
|
1044
|
+
return inside.trim() === "" ? [
|
|
1045
|
+
{
|
|
1046
|
+
at: object.start + 1,
|
|
1047
|
+
remove: inside.length,
|
|
1048
|
+
text: `
|
|
1049
|
+
${text2}
|
|
1050
|
+
${indent2}`
|
|
1051
|
+
}
|
|
1052
|
+
] : [{ at: close, remove: 0, text: `
|
|
1053
|
+
${text2}
|
|
1054
|
+
${indent2}` }];
|
|
1055
|
+
}
|
|
1056
|
+
const after = /^[ \t]*,/.exec(source.slice(last.end, close));
|
|
1057
|
+
const comma = after === null ? void 0 : last.end + after[0].length;
|
|
1058
|
+
const lineEnd = source.slice(last.end, close).indexOf("\n");
|
|
1059
|
+
const property = (indent2) => `${renderKey(key, style)}: ${render(value, indent2, style)}`;
|
|
1060
|
+
if (lineEnd === -1) {
|
|
1061
|
+
return comma === void 0 ? [{ at: last.end, remove: 0, text: `, ${property(indentAt(source, last.start))}` }] : [{ at: comma, remove: 0, text: ` ${property(indentAt(source, last.start))},` }];
|
|
1062
|
+
}
|
|
1063
|
+
const indent = indentAt(source, last.start);
|
|
1064
|
+
const edits = [
|
|
1065
|
+
{
|
|
1066
|
+
at: last.end + lineEnd,
|
|
1067
|
+
remove: 0,
|
|
1068
|
+
text: `
|
|
1069
|
+
${indent}${property(indent)}${comma === void 0 ? "" : ","}`
|
|
1070
|
+
}
|
|
1071
|
+
];
|
|
1072
|
+
if (comma === void 0) edits.push({ at: last.end, remove: 0, text: "," });
|
|
1073
|
+
return edits;
|
|
1074
|
+
}
|
|
1075
|
+
function findProperty(object, key) {
|
|
1076
|
+
return children(object, "properties").findLast(
|
|
1077
|
+
(p2) => p2.type === "ObjectProperty" && !p2["computed"] && nameOf(child(p2, "key")) === key
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
function editByHand(recipe, why) {
|
|
1081
|
+
const wanted = recipe.set.map(({ path, value }) => `${path} = ${JSON.stringify(value)}`);
|
|
1082
|
+
return new Error(
|
|
1083
|
+
`nymbal.config.ts ${why}, so the ${recipe.name} recipe cannot edit it: edit by hand and set ${wanted.join("; ")}`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
function setPath(source, recipe, path, value, force) {
|
|
1087
|
+
const lf = lfCopy(source);
|
|
1088
|
+
const mod = locate(lf);
|
|
1089
|
+
if (mod === void 0)
|
|
1090
|
+
throw editByHand(recipe, "does not export a defineConfig({...}) object literal");
|
|
1091
|
+
const style = styleOf(lf, mod);
|
|
1092
|
+
const keys = path.split(".");
|
|
1093
|
+
let object = mod.config;
|
|
1094
|
+
for (const [i, key] of keys.entries()) {
|
|
1095
|
+
const prop = findProperty(object, key);
|
|
1096
|
+
if (prop === void 0) {
|
|
1097
|
+
const nested = keys.slice(i + 1).reduceRight((v, k) => ({ [k]: v }), value);
|
|
1098
|
+
return {
|
|
1099
|
+
source: spliced(source, addProperty(lf, object, key, nested, style)),
|
|
1100
|
+
wrote: true
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const held = child(prop, "value");
|
|
1104
|
+
if (i === keys.length - 1) {
|
|
1105
|
+
const current = evaluate(held, mod.envName);
|
|
1106
|
+
if (current !== void 0 && sameValue(current, value)) return { source, wrote: false };
|
|
1107
|
+
if (!force && !isScaffoldDefault(current)) {
|
|
1108
|
+
return {
|
|
1109
|
+
conflict: { path, current: current ?? lf.slice(held.start, held.end), wanted: value }
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
const text2 = render(value, indentAt(lf, prop.start), style);
|
|
1113
|
+
return {
|
|
1114
|
+
source: spliced(source, [{ at: held.start, remove: held.end - held.start, text: text2 }]),
|
|
1115
|
+
wrote: true
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const inner = unwrap(held);
|
|
1119
|
+
if (inner?.type !== "ObjectExpression") {
|
|
1120
|
+
throw editByHand(
|
|
1121
|
+
recipe,
|
|
1122
|
+
`holds ${keys.slice(0, i + 1).join(".")} as something other than an object literal`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
object = inner;
|
|
1126
|
+
}
|
|
1127
|
+
return { source, wrote: false };
|
|
1128
|
+
}
|
|
1129
|
+
function importEnv(source, recipe) {
|
|
1130
|
+
const lf = lfCopy(source);
|
|
1131
|
+
const mod = locate(lf);
|
|
1132
|
+
if (mod.envName !== void 0) return source;
|
|
1133
|
+
if (mod.envBound)
|
|
1134
|
+
throw editByHand(recipe, "binds `env` to something other than the env of @nymbalhq/config");
|
|
1135
|
+
const style = styleOf(lf, mod);
|
|
1136
|
+
if (mod.namedImport !== void 0) {
|
|
1137
|
+
const last = children(mod.namedImport, "specifiers").filter((s) => s.type === "ImportSpecifier").at(-1);
|
|
1138
|
+
return spliced(source, [{ at: last.end, remove: 0, text: ", env" }]);
|
|
1139
|
+
}
|
|
1140
|
+
const imports = children(mod.program, "body").filter((n) => n.type === "ImportDeclaration");
|
|
1141
|
+
const line = `import { env } from ${quoted(CONFIG_MODULE, style)}
|
|
1142
|
+
`;
|
|
1143
|
+
const lastImport = imports.at(-1);
|
|
1144
|
+
const lineEnd = lastImport === void 0 ? -1 : lf.indexOf("\n", lastImport.end);
|
|
1145
|
+
const edit = lastImport === void 0 ? { at: 0, remove: 0, text: line } : lineEnd === -1 ? { at: lf.length, remove: 0, text: `
|
|
1146
|
+
${line}` } : { at: lineEnd + 1, remove: 0, text: line };
|
|
1147
|
+
return spliced(source, [edit]);
|
|
1148
|
+
}
|
|
1149
|
+
function applyRecipeToConfig(source, recipe, opts) {
|
|
1150
|
+
if (locate(lfCopy(source)) === void 0) {
|
|
1151
|
+
throw editByHand(recipe, "does not export a defineConfig({...}) object literal");
|
|
1152
|
+
}
|
|
1153
|
+
let current = source;
|
|
1154
|
+
const changed = [];
|
|
1155
|
+
let envWritten = false;
|
|
1156
|
+
for (const { path, value } of recipe.set) {
|
|
1157
|
+
const step = setPath(current, recipe, path, value, opts.force);
|
|
1158
|
+
if ("conflict" in step) return step;
|
|
1159
|
+
if (!step.wrote) continue;
|
|
1160
|
+
current = step.source;
|
|
1161
|
+
changed.push(path);
|
|
1162
|
+
envWritten ||= writesEnv(value);
|
|
1163
|
+
}
|
|
1164
|
+
if (envWritten) current = importEnv(current, recipe);
|
|
1165
|
+
return { source: current, changed };
|
|
1166
|
+
}
|
|
1167
|
+
function envCalls(source) {
|
|
1168
|
+
const lf = lfCopy(source);
|
|
1169
|
+
const mod = locate(lf);
|
|
1170
|
+
const envName = mod === void 0 ? importedAs(parse(lf), "env") : mod.envName;
|
|
1171
|
+
if (envName === void 0) return [];
|
|
1172
|
+
const defaulted = /* @__PURE__ */ new Map();
|
|
1173
|
+
const visit = (node) => {
|
|
1174
|
+
if (Array.isArray(node)) return node.forEach(visit);
|
|
1175
|
+
if (typeof node !== "object" || node === null || typeof node.type !== "string") return;
|
|
1176
|
+
const n = node;
|
|
1177
|
+
if (n.type === "CallExpression") {
|
|
1178
|
+
const callee = child(n, "callee");
|
|
1179
|
+
const args = children(n, "arguments");
|
|
1180
|
+
const first = args[0];
|
|
1181
|
+
if (callee.type === "Identifier" && callee["name"] === envName && first?.type === "StringLiteral") {
|
|
1182
|
+
const name = first["value"];
|
|
1183
|
+
defaulted.set(name, (defaulted.get(name) ?? true) && args.length > 1);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const [key, value] of Object.entries(n)) {
|
|
1187
|
+
if (key !== "loc" && key !== "comments" && key !== "extra" && key !== "original") visit(value);
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
visit(mod?.program ?? parse(lf));
|
|
1191
|
+
return [...defaulted].map(([name, all]) => ({ name, defaulted: all }));
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/recipes/env-example.ts
|
|
1195
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
1196
|
+
function declares(lines, name) {
|
|
1197
|
+
const re = new RegExp(`^(#\\s*)?${escapeRegExp(name)}=`);
|
|
1198
|
+
return lines.some((line) => re.test(line.trim()));
|
|
1199
|
+
}
|
|
1200
|
+
function missingEnv(example, recipe) {
|
|
1201
|
+
const lines = example.split(/\r?\n/);
|
|
1202
|
+
return recipe.env.filter(
|
|
1203
|
+
(v, i) => !declares(lines, v.name) && recipe.env.findIndex((w) => w.name === v.name) === i
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
function appendRecipeEnv(example, recipe) {
|
|
1207
|
+
const eol = example.includes("\r\n") ? "\r\n" : "\n";
|
|
1208
|
+
const missing = missingEnv(example, recipe);
|
|
1209
|
+
if (missing.length === 0) return example;
|
|
1210
|
+
const block = [`# ${recipe.name} (nymbal add ${recipe.name})`];
|
|
1211
|
+
for (const v of missing) {
|
|
1212
|
+
if (v.comment !== "") block.push(`# ${v.comment}`);
|
|
1213
|
+
block.push(`${v.required ? "" : "# "}${v.name}=${v.example}`);
|
|
1214
|
+
}
|
|
1215
|
+
let head = example;
|
|
1216
|
+
if (head !== "" && !head.endsWith("\n")) head += eol;
|
|
1217
|
+
const last = head.split(/\r?\n/).at(-2);
|
|
1218
|
+
const gap = last === void 0 || last.trim() === "" ? "" : eol;
|
|
1219
|
+
return `${head}${gap}${block.join(eol)}${eol}`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/commands/add.ts
|
|
1223
|
+
var out = (line = "") => void process.stdout.write(`${line}
|
|
1224
|
+
`);
|
|
1225
|
+
var err = (line) => void process.stderr.write(`${line}
|
|
1226
|
+
`);
|
|
1227
|
+
function fail(message) {
|
|
1228
|
+
err(pc3.red(`\u2717 ${message}`));
|
|
1229
|
+
return 1;
|
|
1230
|
+
}
|
|
1231
|
+
function runInstall(line, root) {
|
|
1232
|
+
out(pc3.dim(`$ ${line}`));
|
|
1233
|
+
const [command, ...args] = line.split(" ");
|
|
1234
|
+
const result = spawnSync(command, args, {
|
|
1235
|
+
cwd: root,
|
|
1236
|
+
stdio: "inherit",
|
|
1237
|
+
shell: process.platform === "win32"
|
|
1238
|
+
});
|
|
1239
|
+
return result.status === 0;
|
|
1240
|
+
}
|
|
1241
|
+
function dotEnv(root) {
|
|
1242
|
+
const file = join5(root, ".env");
|
|
1243
|
+
return existsSync6(file) ? parseEnv(readFileSync2(file, "utf8")) : {};
|
|
1244
|
+
}
|
|
1245
|
+
var isSet = (name, fromFile) => (process.env[name] ?? "") !== "" || (fromFile[name] ?? "") !== "";
|
|
1246
|
+
async function problemsWith(candidate, recipe, root, fromFile) {
|
|
1247
|
+
const problems = [];
|
|
1248
|
+
const missing = recipe.install.filter((pkg) => !isInstalled(pkg, root));
|
|
1249
|
+
if (missing.length > 0) {
|
|
1250
|
+
problems.push(
|
|
1251
|
+
`${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not installed: ${installLine(missing, process.env, root)}`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
const saved = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const { name, defaulted } of envCalls(candidate)) {
|
|
1256
|
+
if ((process.env[name] ?? "") !== "") continue;
|
|
1257
|
+
const value = fromFile[name] || (defaulted ? void 0 : `nymbal-add-placeholder-${name}`);
|
|
1258
|
+
if (value === void 0) continue;
|
|
1259
|
+
saved.set(name, process.env[name]);
|
|
1260
|
+
process.env[name] = value;
|
|
1261
|
+
}
|
|
1262
|
+
const dir = mkdtempSync(join5(root, ".nymbal-add-"));
|
|
1263
|
+
try {
|
|
1264
|
+
writeFileSync(join5(dir, "nymbal.config.ts"), candidate);
|
|
1265
|
+
await loadConfig2(dir);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
problems.push(error.message);
|
|
1268
|
+
} finally {
|
|
1269
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1270
|
+
for (const [name, value] of saved) {
|
|
1271
|
+
if (value === void 0) delete process.env[name];
|
|
1272
|
+
else process.env[name] = value;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return problems;
|
|
1276
|
+
}
|
|
1277
|
+
function lineDiff(before, after) {
|
|
1278
|
+
const a = before.split("\n");
|
|
1279
|
+
const b = after.split("\n");
|
|
1280
|
+
const t = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
1281
|
+
for (let i2 = a.length - 1; i2 >= 0; i2--)
|
|
1282
|
+
for (let j2 = b.length - 1; j2 >= 0; j2--)
|
|
1283
|
+
t[i2][j2] = a[i2] === b[j2] ? t[i2 + 1][j2 + 1] + 1 : Math.max(t[i2 + 1][j2], t[i2][j2 + 1]);
|
|
1284
|
+
const lines = [];
|
|
1285
|
+
let i = 0;
|
|
1286
|
+
let j = 0;
|
|
1287
|
+
while (i < a.length || j < b.length) {
|
|
1288
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
1289
|
+
i++;
|
|
1290
|
+
j++;
|
|
1291
|
+
} else if (j < b.length && (i === a.length || t[i][j + 1] >= t[i + 1][j])) {
|
|
1292
|
+
lines.push(pc3.green(`+ ${b[j++]}`));
|
|
1293
|
+
} else {
|
|
1294
|
+
lines.push(pc3.red(`- ${a[i++]}`));
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return lines;
|
|
1298
|
+
}
|
|
1299
|
+
var show = (value) => typeof value === "string" ? `'${value}'` : JSON.stringify(value);
|
|
1300
|
+
async function add(opts, cwd = process.cwd()) {
|
|
1301
|
+
const name = opts.recipe;
|
|
1302
|
+
if (!isKnownRecipe(name)) {
|
|
1303
|
+
return fail(`unknown recipe '${name}'. The recipes are: ${knownRecipes().join(", ")}`);
|
|
1304
|
+
}
|
|
1305
|
+
const configPath = findConfigFile2(cwd);
|
|
1306
|
+
if (configPath === null) return fail(`no nymbal.config.ts in ${cwd} or any parent directory`);
|
|
1307
|
+
const root = dirname6(configPath);
|
|
1308
|
+
const holder = RECIPE_HOLDERS[name];
|
|
1309
|
+
const installed = [];
|
|
1310
|
+
if (recipeFile(name, root) === null) {
|
|
1311
|
+
const line = installLine([holder], process.env, root);
|
|
1312
|
+
if (!opts.install || opts.dryRun) {
|
|
1313
|
+
return fail(
|
|
1314
|
+
`the ${name} recipe ships in ${holder}, which is not installed. Install it: ${line}`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (!runInstall(line, root)) return fail(`${line} failed`);
|
|
1318
|
+
installed.push(holder);
|
|
1319
|
+
}
|
|
1320
|
+
const recipe = await readRecipe(name, root);
|
|
1321
|
+
const source = readFileSync2(configPath, "utf8");
|
|
1322
|
+
let edited;
|
|
1323
|
+
try {
|
|
1324
|
+
const applied = applyRecipeToConfig(source, recipe, { force: opts.force });
|
|
1325
|
+
if ("conflict" in applied) {
|
|
1326
|
+
const { path, current, wanted } = applied.conflict;
|
|
1327
|
+
return fail(
|
|
1328
|
+
`${path} is ${show(current)} in nymbal.config.ts, and the ${name} recipe sets it to ${show(wanted)}. nymbal.config.ts is unchanged: edit it by hand, or re-run with --force to overwrite it.`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
edited = applied;
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
return fail(error.message);
|
|
1334
|
+
}
|
|
1335
|
+
const examplePath = join5(root, ".env.example");
|
|
1336
|
+
const example = existsSync6(examplePath) ? readFileSync2(examplePath, "utf8") : "";
|
|
1337
|
+
const nextExample = appendRecipeEnv(example, recipe);
|
|
1338
|
+
if (opts.dryRun) {
|
|
1339
|
+
out(pc3.bold(`nymbal add ${name} --dry-run: nothing is written`));
|
|
1340
|
+
out();
|
|
1341
|
+
out(
|
|
1342
|
+
recipe.install.length > 0 ? `install: ${installLine(recipe.install, process.env, root)}` : "install: nothing"
|
|
1343
|
+
);
|
|
1344
|
+
out();
|
|
1345
|
+
out("nymbal.config.ts:");
|
|
1346
|
+
const diff = lineDiff(source, edited.source);
|
|
1347
|
+
for (const line of diff.length > 0 ? diff : ["(already set)"]) out(` ${line}`);
|
|
1348
|
+
out();
|
|
1349
|
+
out(".env.example:");
|
|
1350
|
+
const added = nextExample.slice(example.length).split(/\r?\n/).filter((l) => l !== "");
|
|
1351
|
+
for (const line of added.length > 0 ? added : ["(every variable already listed)"])
|
|
1352
|
+
out(` ${line}`);
|
|
1353
|
+
return 0;
|
|
1354
|
+
}
|
|
1355
|
+
const pending = recipe.install.filter((pkg) => !isInstalled(pkg, root));
|
|
1356
|
+
if (pending.length > 0 && opts.install) {
|
|
1357
|
+
const line = installLine(pending, process.env, root);
|
|
1358
|
+
if (!runInstall(line, root)) return fail(`${line} failed`);
|
|
1359
|
+
installed.push(...pending);
|
|
1360
|
+
}
|
|
1361
|
+
const fromFile = dotEnv(root);
|
|
1362
|
+
const problems = await problemsWith(edited.source, recipe, root, fromFile);
|
|
1363
|
+
if (problems.length > 0) {
|
|
1364
|
+
return fail(
|
|
1365
|
+
`the ${name} recipe would leave a config that does not load, so nymbal.config.ts is unchanged:
|
|
1366
|
+
${problems.join("\n ")}`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const written = [];
|
|
1370
|
+
if (installed.length > 0) written.push(`package.json and its lockfile (${installed.join(", ")})`);
|
|
1371
|
+
if (edited.source !== source) {
|
|
1372
|
+
writeFileSync(configPath, edited.source);
|
|
1373
|
+
written.push(`nymbal.config.ts (${edited.changed.join(", ")})`);
|
|
1374
|
+
}
|
|
1375
|
+
if (nextExample !== example) {
|
|
1376
|
+
writeFileSync(examplePath, nextExample);
|
|
1377
|
+
written.push(
|
|
1378
|
+
`.env.example (${missingEnv(example, recipe).map((v) => v.name).join(", ")})`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
out(pc3.green(`\u2713 ${name}: ${recipe.summary}`));
|
|
1382
|
+
out(written.length > 0 ? ` changed ${written.join(", ")}` : " already added: nothing changed");
|
|
1383
|
+
const unset = recipe.env.filter((v) => v.required && !isSet(v.name, fromFile));
|
|
1384
|
+
if (unset.length > 0 || recipe.next.length > 0) {
|
|
1385
|
+
out();
|
|
1386
|
+
out("Next:");
|
|
1387
|
+
for (const v of unset) out(` - set ${v.name} in .env`);
|
|
1388
|
+
for (const line of recipe.next) out(` - ${line}`);
|
|
1389
|
+
}
|
|
1390
|
+
return 0;
|
|
1391
|
+
}
|
|
1392
|
+
var addCommand = defineCommand7({
|
|
1393
|
+
meta: {
|
|
1394
|
+
name: "add",
|
|
1395
|
+
description: `Put the store on a service: ${knownRecipes().join(", ")}`
|
|
1396
|
+
},
|
|
1397
|
+
args: {
|
|
1398
|
+
recipe: { type: "positional", required: true, description: knownRecipes().join(", ") },
|
|
1399
|
+
install: {
|
|
1400
|
+
type: "boolean",
|
|
1401
|
+
default: true,
|
|
1402
|
+
description: "Install the packages (--no-install: they must already be installed)"
|
|
1403
|
+
},
|
|
1404
|
+
force: {
|
|
1405
|
+
type: "boolean",
|
|
1406
|
+
default: false,
|
|
1407
|
+
description: "Overwrite a config value the recipe conflicts with"
|
|
1408
|
+
},
|
|
1409
|
+
"dry-run": {
|
|
1410
|
+
type: "boolean",
|
|
1411
|
+
default: false,
|
|
1412
|
+
description: "Print the config change, the .env.example lines and the install line, and write nothing"
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
async run({ args }) {
|
|
1416
|
+
process.exitCode = await add({
|
|
1417
|
+
recipe: args.recipe,
|
|
1418
|
+
install: args.install,
|
|
1419
|
+
force: args.force,
|
|
1420
|
+
dryRun: args["dry-run"]
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
// src/commands/doctor.ts
|
|
1426
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1427
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1428
|
+
import { createLogger as createLogger2, diagnose } from "@nymbalhq/platform";
|
|
1429
|
+
import { errorText, HEALTH_CHECK_TIMEOUT_MS } from "@nymbalhq/types";
|
|
1430
|
+
|
|
1431
|
+
// src/commands/doctor-report.ts
|
|
1432
|
+
import pc4 from "picocolors";
|
|
1433
|
+
import { connectionSecrets, displayUrl as displayUrl2, SECRET_MIN_LENGTH } from "@nymbalhq/types";
|
|
1434
|
+
var TEXT_STATUS = {
|
|
1435
|
+
ok: "ok",
|
|
1436
|
+
healthy: "ok",
|
|
1437
|
+
degraded: "warn",
|
|
1438
|
+
down: "down"
|
|
1439
|
+
};
|
|
1440
|
+
var JSON_STATUS = {
|
|
1441
|
+
ok: "ok",
|
|
1442
|
+
healthy: "ok",
|
|
1443
|
+
degraded: "degraded",
|
|
1444
|
+
down: "down"
|
|
1445
|
+
};
|
|
1446
|
+
var COLOUR = { ok: pc4.green, warn: pc4.yellow, down: pc4.red };
|
|
1447
|
+
var SECRET_KEY = /secret|passw(?:or)?d|^pass$|token|api[-_]?key|private[-_]?key|credential|access[-_]?key/i;
|
|
1448
|
+
var SECRET_ENV = /SECRET|PASSWORD|PASSWD|(?:^|_)PASS(?:$|_)|TOKEN|API_KEY|PRIVATE_KEY|CREDENTIAL|ACCESS_KEY/i;
|
|
1449
|
+
var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>]+/gi;
|
|
1450
|
+
var TRAILING_PUNCTUATION = /[.,;:)\]}]+$/;
|
|
1451
|
+
function addSecret(into, value) {
|
|
1452
|
+
if (value.length >= SECRET_MIN_LENGTH) into.add(value);
|
|
1453
|
+
}
|
|
1454
|
+
function collectFromConfig(value, key, into, seen) {
|
|
1455
|
+
if (typeof value === "string") {
|
|
1456
|
+
if (SECRET_KEY.test(key)) addSecret(into, value);
|
|
1457
|
+
if (value.includes("://")) for (const s of connectionSecrets(value, { user: false })) addSecret(into, s);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
1461
|
+
seen.add(value);
|
|
1462
|
+
for (const [k, v] of Object.entries(value)) collectFromConfig(v, k, into, seen);
|
|
1463
|
+
}
|
|
1464
|
+
function secretsOf(config, env) {
|
|
1465
|
+
const found = /* @__PURE__ */ new Set();
|
|
1466
|
+
collectFromConfig(config, "", found, /* @__PURE__ */ new WeakSet());
|
|
1467
|
+
for (const [name, value] of Object.entries(env)) {
|
|
1468
|
+
if (value === void 0) continue;
|
|
1469
|
+
if (SECRET_ENV.test(name)) addSecret(found, value);
|
|
1470
|
+
if (value.includes("://")) for (const s of connectionSecrets(value, { user: false })) addSecret(found, s);
|
|
1471
|
+
}
|
|
1472
|
+
return [...found].sort((a, b) => b.length - a.length);
|
|
1473
|
+
}
|
|
1474
|
+
function scrub(detail, secrets) {
|
|
1475
|
+
let text2 = detail.replace(URL_IN_TEXT, (match) => {
|
|
1476
|
+
const trailing = TRAILING_PUNCTUATION.exec(match)?.[0] ?? "";
|
|
1477
|
+
return displayUrl2(match.slice(0, match.length - trailing.length)) + trailing;
|
|
1478
|
+
});
|
|
1479
|
+
for (const secret of secrets) text2 = text2.split(secret).join("***");
|
|
1480
|
+
return text2.replace(/\s*\n\s*(?:•\s*)?/g, " ").trim();
|
|
1481
|
+
}
|
|
1482
|
+
function formatDoctorReport(report, options) {
|
|
1483
|
+
const secrets = secretsOf(options.config, options.env ?? {});
|
|
1484
|
+
const checks = report.checks.map((check) => ({
|
|
1485
|
+
name: check.name,
|
|
1486
|
+
status: check.status,
|
|
1487
|
+
detail: scrub(check.detail, secrets)
|
|
1488
|
+
}));
|
|
1489
|
+
if (options.json) {
|
|
1490
|
+
return JSON.stringify(
|
|
1491
|
+
{
|
|
1492
|
+
checks: checks.map((c) => ({ name: c.name, status: JSON_STATUS[c.status], detail: c.detail }))
|
|
1493
|
+
},
|
|
1494
|
+
null,
|
|
1495
|
+
2
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const width = Math.max(...checks.map((c) => c.name.length), 0);
|
|
1499
|
+
const lines = checks.map((c) => {
|
|
1500
|
+
const status = TEXT_STATUS[c.status];
|
|
1501
|
+
return `${COLOUR[status](status.padEnd(4))} ${c.name.padEnd(width)} ${c.detail}`.trimEnd();
|
|
1502
|
+
});
|
|
1503
|
+
const count = (s) => checks.filter((c) => TEXT_STATUS[c.status] === s).length;
|
|
1504
|
+
return [...lines, "", `${count("down")} down, ${count("warn")} warn, ${count("ok")} ok`].join("\n");
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/commands/doctor.ts
|
|
1508
|
+
function timeoutOf(flag2) {
|
|
1509
|
+
if (flag2 === void 0 || flag2.trim() === "") return HEALTH_CHECK_TIMEOUT_MS;
|
|
1510
|
+
const ms = Number(flag2);
|
|
1511
|
+
if (!Number.isInteger(ms) || ms < 1) {
|
|
1512
|
+
throw new Error(`--timeout must be a whole number of milliseconds, got "${flag2}"`);
|
|
1513
|
+
}
|
|
1514
|
+
return ms;
|
|
1515
|
+
}
|
|
1516
|
+
function migrationsOf(projectRoot) {
|
|
1517
|
+
const root = resolveMigrations(projectRoot);
|
|
1518
|
+
return existsSync7(root) ? { migrationsRoot: root } : {};
|
|
1519
|
+
}
|
|
1520
|
+
var doctorCommand = defineCommand8({
|
|
1521
|
+
meta: {
|
|
1522
|
+
name: "doctor",
|
|
1523
|
+
description: "Check the project without changing it: config, packages, command store, migrations, slots and primitives"
|
|
1524
|
+
},
|
|
1525
|
+
args: {
|
|
1526
|
+
json: { type: "boolean", description: "Print the checks as one JSON document", default: false },
|
|
1527
|
+
timeout: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
description: `How long each check may take, in ms (default ${HEALTH_CHECK_TIMEOUT_MS})`
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
async run({ args }) {
|
|
1533
|
+
const timeoutMs = timeoutOf(args.timeout);
|
|
1534
|
+
let loaded;
|
|
1535
|
+
try {
|
|
1536
|
+
loaded = await loadProject();
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
loaded = { error };
|
|
1539
|
+
}
|
|
1540
|
+
const report = "error" in loaded ? (
|
|
1541
|
+
// Nothing else can be checked without a config, so it is the only line.
|
|
1542
|
+
{ checks: [{ name: "config", status: "down", detail: errorText(loaded.error) }] }
|
|
1543
|
+
) : await diagnose(loaded.config, {
|
|
1544
|
+
...loaded.adapters !== void 0 && { adapters: loaded.adapters },
|
|
1545
|
+
// The report carries every failure; a log line would only repeat one, and break `--json`.
|
|
1546
|
+
logger: createLogger2({ level: "silent", pretty: false }),
|
|
1547
|
+
timeoutMs,
|
|
1548
|
+
...migrationsOf(loaded.projectRoot)
|
|
1549
|
+
});
|
|
1550
|
+
const text2 = formatDoctorReport(report, {
|
|
1551
|
+
json: args.json,
|
|
1552
|
+
config: "error" in loaded ? void 0 : loaded.config,
|
|
1553
|
+
env: process.env
|
|
1554
|
+
});
|
|
1555
|
+
const down = report.checks.some((check) => check.status === "down");
|
|
1556
|
+
process.stdout.write(`${text2}
|
|
1557
|
+
`, () => process.exit(down ? 1 : 0));
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
// src/commands/test.ts
|
|
1562
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
1563
|
+
import { loadConfig as loadConfig3 } from "@nymbalhq/config";
|
|
1564
|
+
var testCommand = defineCommand9({
|
|
702
1565
|
meta: { name: "test", description: "Run unit + integration tests (vitest)" },
|
|
703
1566
|
async run() {
|
|
704
1567
|
await runOrFail("pnpm", ["vitest", "run"]);
|
|
705
1568
|
}
|
|
706
1569
|
});
|
|
707
|
-
var testE2eCommand =
|
|
1570
|
+
var testE2eCommand = defineCommand9({
|
|
708
1571
|
meta: { name: "test:e2e", description: "Run Playwright E2E against the configured template" },
|
|
709
1572
|
async run() {
|
|
710
|
-
const { config } = await
|
|
1573
|
+
const { config } = await loadConfig3();
|
|
711
1574
|
await runOrFail("pnpm", [
|
|
712
1575
|
"--filter",
|
|
713
1576
|
"@nymbalhq/e2e",
|
|
@@ -721,7 +1584,7 @@ var testE2eCommand = defineCommand7({
|
|
|
721
1584
|
]);
|
|
722
1585
|
}
|
|
723
1586
|
});
|
|
724
|
-
var testVisualCommand =
|
|
1587
|
+
var testVisualCommand = defineCommand9({
|
|
725
1588
|
meta: { name: "test:visual", description: "Run visual regression tests (Playwright toHaveScreenshot)" },
|
|
726
1589
|
args: {
|
|
727
1590
|
"update-baselines": {
|
|
@@ -731,7 +1594,7 @@ var testVisualCommand = defineCommand7({
|
|
|
731
1594
|
}
|
|
732
1595
|
},
|
|
733
1596
|
async run({ args }) {
|
|
734
|
-
const { config } = await
|
|
1597
|
+
const { config } = await loadConfig3();
|
|
735
1598
|
const isCI = !!process.env.CI;
|
|
736
1599
|
const updateBaselines = args["update-baselines"];
|
|
737
1600
|
if (updateBaselines && !isCI) {
|
|
@@ -766,7 +1629,7 @@ var testVisualCommand = defineCommand7({
|
|
|
766
1629
|
}
|
|
767
1630
|
}
|
|
768
1631
|
});
|
|
769
|
-
var testContractsCommand =
|
|
1632
|
+
var testContractsCommand = defineCommand9({
|
|
770
1633
|
meta: { name: "test:contracts", description: "Run adapter contract tests" },
|
|
771
1634
|
args: {
|
|
772
1635
|
adapter: { type: "string", description: "Run only contracts for a specific adapter (e.g. payments, email)" }
|
|
@@ -784,10 +1647,10 @@ var testContractsCommand = defineCommand7({
|
|
|
784
1647
|
await runOrFail("pnpm", vArgs);
|
|
785
1648
|
}
|
|
786
1649
|
});
|
|
787
|
-
var testPerformanceCommand =
|
|
1650
|
+
var testPerformanceCommand = defineCommand9({
|
|
788
1651
|
meta: { name: "test:performance", description: "Run Lighthouse CI against the configured template" },
|
|
789
1652
|
async run() {
|
|
790
|
-
const { config } = await
|
|
1653
|
+
const { config } = await loadConfig3();
|
|
791
1654
|
const port = config.template === "nextjs" ? 3e3 : 4321;
|
|
792
1655
|
await runOrFail("pnpm", ["lhci", "autorun"], {
|
|
793
1656
|
env: {
|
|
@@ -798,7 +1661,7 @@ var testPerformanceCommand = defineCommand7({
|
|
|
798
1661
|
});
|
|
799
1662
|
}
|
|
800
1663
|
});
|
|
801
|
-
var testAllCommand =
|
|
1664
|
+
var testAllCommand = defineCommand9({
|
|
802
1665
|
meta: { name: "test:all", description: "Run unit, contract, e2e, visual, and performance tests" },
|
|
803
1666
|
async run() {
|
|
804
1667
|
const steps = [
|
|
@@ -813,7 +1676,7 @@ var testAllCommand = defineCommand7({
|
|
|
813
1676
|
process.exit(code);
|
|
814
1677
|
}
|
|
815
1678
|
}
|
|
816
|
-
const { config } = await
|
|
1679
|
+
const { config } = await loadConfig3();
|
|
817
1680
|
await runOrFail("pnpm", [
|
|
818
1681
|
"--filter",
|
|
819
1682
|
"@nymbalhq/e2e",
|
|
@@ -843,7 +1706,7 @@ var testAllCommand = defineCommand7({
|
|
|
843
1706
|
});
|
|
844
1707
|
|
|
845
1708
|
// src/index.ts
|
|
846
|
-
var main =
|
|
1709
|
+
var main = defineCommand10({
|
|
847
1710
|
meta: {
|
|
848
1711
|
name: "nymbal",
|
|
849
1712
|
version: getCliVersion(),
|
|
@@ -856,6 +1719,8 @@ var main = defineCommand8({
|
|
|
856
1719
|
seed: seedCommand,
|
|
857
1720
|
import: importCommand,
|
|
858
1721
|
"config:validate": configValidateCommand,
|
|
1722
|
+
add: addCommand,
|
|
1723
|
+
doctor: doctorCommand,
|
|
859
1724
|
test: testCommand,
|
|
860
1725
|
"test:e2e": testE2eCommand,
|
|
861
1726
|
"test:visual": testVisualCommand,
|