nymbal 0.2.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 +1002 -75
- 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);
|
|
@@ -217,6 +219,102 @@ function resolveAdminRoot() {
|
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
// src/utils/shutdown.ts
|
|
223
|
+
var SHUTDOWN_BOUND_MS = 3e3;
|
|
224
|
+
var running = false;
|
|
225
|
+
async function runShutdown(steps, opts) {
|
|
226
|
+
if (running) {
|
|
227
|
+
opts.logger.error({}, "Shutdown already running; exiting now");
|
|
228
|
+
return opts.exit(1);
|
|
229
|
+
}
|
|
230
|
+
running = true;
|
|
231
|
+
let pending;
|
|
232
|
+
let failed = false;
|
|
233
|
+
const work = (async () => {
|
|
234
|
+
for (const step of steps) {
|
|
235
|
+
pending = step.name;
|
|
236
|
+
try {
|
|
237
|
+
await step.run();
|
|
238
|
+
} catch (err2) {
|
|
239
|
+
failed = true;
|
|
240
|
+
opts.logger.error({ err: err2, step: step.name }, `Shutdown step "${step.name}" failed`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
pending = void 0;
|
|
244
|
+
})();
|
|
245
|
+
let timer;
|
|
246
|
+
const bound = new Promise((resolve6) => {
|
|
247
|
+
timer = setTimeout(() => resolve6("bound"), opts.boundMs);
|
|
248
|
+
});
|
|
249
|
+
const outcome = await Promise.race([work.then(() => "done"), bound]);
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
if (outcome === "bound") {
|
|
252
|
+
opts.logger.error(
|
|
253
|
+
{ step: pending, boundMs: opts.boundMs },
|
|
254
|
+
`Shutdown step "${pending}" did not finish within ${opts.boundMs} ms; exiting`
|
|
255
|
+
);
|
|
256
|
+
return opts.exit(1);
|
|
257
|
+
}
|
|
258
|
+
return opts.exit(failed ? 1 : 0);
|
|
259
|
+
}
|
|
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
|
+
|
|
220
318
|
// src/commands/dev.ts
|
|
221
319
|
function defaultStorefrontPort(template) {
|
|
222
320
|
return template === "astro" ? 4321 : 3e3;
|
|
@@ -230,13 +328,7 @@ function preferredStorefrontPort(template, flagPort, env) {
|
|
|
230
328
|
}
|
|
231
329
|
return defaultStorefrontPort(template);
|
|
232
330
|
}
|
|
233
|
-
function storefrontEnv(
|
|
234
|
-
const stubPayments = config.commerce.payments.provider === "stripe" ? {} : {
|
|
235
|
-
PUBLIC_NYMBAL_PAYMENTS_PROVIDER: "native-stub",
|
|
236
|
-
// Astro
|
|
237
|
-
NEXT_PUBLIC_NYMBAL_PAYMENTS_PROVIDER: "native-stub"
|
|
238
|
-
// Next.js
|
|
239
|
-
};
|
|
331
|
+
function storefrontEnv(_config, apiUrl, storefrontPort, baseEnv = process.env) {
|
|
240
332
|
return {
|
|
241
333
|
...baseEnv,
|
|
242
334
|
NYMBAL_API_URL: apiUrl,
|
|
@@ -246,10 +338,41 @@ function storefrontEnv(config, apiUrl, storefrontPort, baseEnv = process.env) {
|
|
|
246
338
|
// Next.js
|
|
247
339
|
// The framework also reads the port flag; the env var keeps the scaffold
|
|
248
340
|
// config (astro.config.mjs) in sync when it honours NYMBAL_STOREFRONT_PORT.
|
|
249
|
-
NYMBAL_STOREFRONT_PORT: String(storefrontPort)
|
|
250
|
-
...stubPayments
|
|
341
|
+
NYMBAL_STOREFRONT_PORT: String(storefrontPort)
|
|
251
342
|
};
|
|
252
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
|
+
}))
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function apiPort(config, flagPort) {
|
|
369
|
+
if (flagPort === void 0 || flagPort.trim() === "") return config.http.port;
|
|
370
|
+
const port = Number(flagPort);
|
|
371
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
372
|
+
throw new Error(`--port must be an integer from 1 to 65535, got "${flagPort}"`);
|
|
373
|
+
}
|
|
374
|
+
return port;
|
|
375
|
+
}
|
|
253
376
|
var devCommand = defineCommand2({
|
|
254
377
|
meta: {
|
|
255
378
|
name: "dev",
|
|
@@ -273,17 +396,37 @@ var devCommand = defineCommand2({
|
|
|
273
396
|
description: "Start only the API server (skip template dev server)",
|
|
274
397
|
default: false
|
|
275
398
|
},
|
|
399
|
+
port: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Port for the API server (default: the config's http.port)"
|
|
402
|
+
},
|
|
276
403
|
storefrontPort: {
|
|
277
404
|
type: "string",
|
|
278
405
|
alias: "storefront-port",
|
|
279
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
|
|
280
412
|
}
|
|
281
413
|
},
|
|
282
414
|
async run({ args }) {
|
|
283
|
-
const
|
|
415
|
+
const loaded = await loadProject();
|
|
416
|
+
const { projectRoot } = loaded;
|
|
417
|
+
const port = apiPort(loaded.config, args.port);
|
|
418
|
+
const config = { ...loaded.config, http: { ...loaded.config.http, port } };
|
|
284
419
|
const version = getCliVersion();
|
|
285
420
|
await migrateProject(config, projectRoot);
|
|
286
|
-
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
|
+
});
|
|
287
430
|
const { platform } = app;
|
|
288
431
|
if (args.seed) {
|
|
289
432
|
const existing = await platform.documentStore.query("products", {
|
|
@@ -309,23 +452,26 @@ var devCommand = defineCommand2({
|
|
|
309
452
|
version
|
|
310
453
|
});
|
|
311
454
|
const adminRoot = resolveAdminRoot();
|
|
312
|
-
if (adminRoot === null) {
|
|
313
|
-
console.log(
|
|
314
|
-
pc.yellow(
|
|
315
|
-
"\u2192 The admin is not built, so /admin/ answers 404: run `pnpm --filter @nymbalhq/admin build`."
|
|
316
|
-
)
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
455
|
await app.attachHttp(http.adapter, { adminRoot });
|
|
320
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
|
+
};
|
|
461
|
+
const stopApi = [
|
|
462
|
+
{ name: "http.stop", run: () => http.stop() },
|
|
463
|
+
{ name: "app.stop", run: () => app.stop() }
|
|
464
|
+
];
|
|
465
|
+
const shutdownWith = (steps) => runShutdown(steps, {
|
|
466
|
+
boundMs: SHUTDOWN_BOUND_MS,
|
|
467
|
+
logger: platform.logger,
|
|
468
|
+
exit: (code) => process.exit(code)
|
|
469
|
+
});
|
|
321
470
|
if (args.apiOnly) {
|
|
322
|
-
|
|
471
|
+
printSummary(null);
|
|
472
|
+
const shutdown2 = (signal) => {
|
|
323
473
|
platform.logger.info({ signal }, "Shutting down API");
|
|
324
|
-
|
|
325
|
-
});
|
|
326
|
-
await app.stop().catch(() => {
|
|
327
|
-
});
|
|
328
|
-
process.exit(0);
|
|
474
|
+
return shutdownWith(stopApi);
|
|
329
475
|
};
|
|
330
476
|
process.on("SIGINT", () => void shutdown2("SIGINT"));
|
|
331
477
|
process.on("SIGTERM", () => void shutdown2("SIGTERM"));
|
|
@@ -343,34 +489,31 @@ var devCommand = defineCommand2({
|
|
|
343
489
|
);
|
|
344
490
|
}
|
|
345
491
|
const launch = planTemplateLaunch(projectRoot, config.template, storefrontPort);
|
|
346
|
-
const apiUrl = `http://localhost:${config.http.port}`;
|
|
347
492
|
const storefrontUrl = `http://localhost:${storefrontPort}`;
|
|
348
|
-
console.log(
|
|
349
|
-
pc.cyan(
|
|
350
|
-
`\u2192 Starting storefront (${pc.bold(config.template)}, ${launch.mode}) on ${pc.bold(storefrontUrl)}`
|
|
351
|
-
)
|
|
352
|
-
);
|
|
353
|
-
console.log(pc.dim(` API: ${apiUrl}`));
|
|
354
493
|
const templateProc = run(launch.command, launch.args, {
|
|
355
494
|
cwd: launch.cwd,
|
|
356
495
|
env: storefrontEnv(config, apiUrl, storefrontPort)
|
|
357
496
|
});
|
|
358
|
-
|
|
497
|
+
printSummary({ template: `${config.template}, ${launch.mode}`, url: storefrontUrl });
|
|
498
|
+
let shuttingDown = false;
|
|
499
|
+
const shutdown = (signal) => {
|
|
500
|
+
shuttingDown = true;
|
|
359
501
|
platform.logger.info({ signal }, "Shutting down");
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
502
|
+
return shutdownWith([
|
|
503
|
+
{
|
|
504
|
+
name: "storefront.kill",
|
|
505
|
+
run: async () => {
|
|
506
|
+
templateProc.child.kill("SIGINT");
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
...stopApi
|
|
510
|
+
]);
|
|
369
511
|
};
|
|
370
512
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
371
513
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
372
514
|
const templateExit = await templateProc.done;
|
|
373
515
|
platform.logger.info({ code: templateExit }, "Template process exited");
|
|
516
|
+
if (shuttingDown) return;
|
|
374
517
|
await shutdown("template-exit");
|
|
375
518
|
}
|
|
376
519
|
});
|
|
@@ -400,13 +543,13 @@ var buildCommand = defineCommand3({
|
|
|
400
543
|
|
|
401
544
|
// src/commands/seed.ts
|
|
402
545
|
import { defineCommand as defineCommand4 } from "citty";
|
|
403
|
-
import { createPlatform, runSeed as runSeed2 } from "@nymbalhq/platform";
|
|
546
|
+
import { createPlatform, resolvePrimitives, runSeed as runSeed2 } from "@nymbalhq/platform";
|
|
404
547
|
var seedCommand = defineCommand4({
|
|
405
548
|
meta: { name: "seed", description: "Re-seed demo content (products, categories)" },
|
|
406
549
|
async run() {
|
|
407
550
|
const { config, projectRoot } = await loadProject();
|
|
408
551
|
await migrateProject(config, projectRoot);
|
|
409
|
-
const platform = createPlatform(config);
|
|
552
|
+
const platform = createPlatform(config, await resolvePrimitives(config));
|
|
410
553
|
try {
|
|
411
554
|
const result = await runSeed2({
|
|
412
555
|
config,
|
|
@@ -428,7 +571,7 @@ var seedCommand = defineCommand4({
|
|
|
428
571
|
// src/commands/import.ts
|
|
429
572
|
import { defineCommand as defineCommand5 } from "citty";
|
|
430
573
|
import { intro, outro, text, select, spinner, isCancel, cancel } from "@clack/prompts";
|
|
431
|
-
import
|
|
574
|
+
import p from "picocolors";
|
|
432
575
|
import { createApp as createApp2 } from "@nymbalhq/platform";
|
|
433
576
|
import { runWooCommerceImport, writeReport } from "@nymbalhq/importers";
|
|
434
577
|
import { join as join3 } from "path";
|
|
@@ -573,6 +716,7 @@ var importCommand = defineCommand5({
|
|
|
573
716
|
services: app.services,
|
|
574
717
|
repos: app.repos,
|
|
575
718
|
adapters: { ai: app.adapters.ai, reviews: app.adapters.reviews },
|
|
719
|
+
media: app.platform.media,
|
|
576
720
|
onProgress(event) {
|
|
577
721
|
if (event.kind === "phase") {
|
|
578
722
|
console.log(p.cyan(` \u2192 ${event.phase}`));
|
|
@@ -618,34 +762,815 @@ var configValidateCommand = defineCommand6({
|
|
|
618
762
|
const { config, path } = await loadProject();
|
|
619
763
|
console.log(pc2.green(`\u2713 config valid`) + ` (${path})`);
|
|
620
764
|
console.log(
|
|
621
|
-
` template: ${config.template},
|
|
765
|
+
` template: ${config.template}, commandStore: ${config.infrastructure.commandStore}, documentStore: ${config.infrastructure.documentStore}, eventBus: ${config.infrastructure.eventBus}`
|
|
622
766
|
);
|
|
623
767
|
process.exit(0);
|
|
624
|
-
} catch (
|
|
625
|
-
if (
|
|
768
|
+
} catch (err2) {
|
|
769
|
+
if (err2 instanceof NymbalError) {
|
|
626
770
|
console.error(pc2.red("\u2717 config invalid"));
|
|
627
|
-
console.error(
|
|
771
|
+
console.error(err2.message);
|
|
628
772
|
} else {
|
|
629
|
-
console.error(
|
|
773
|
+
console.error(err2);
|
|
630
774
|
}
|
|
631
775
|
process.exit(1);
|
|
632
776
|
}
|
|
633
777
|
}
|
|
634
778
|
});
|
|
635
779
|
|
|
636
|
-
// 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";
|
|
637
785
|
import { defineCommand as defineCommand7 } from "citty";
|
|
638
|
-
import
|
|
639
|
-
|
|
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({
|
|
640
1565
|
meta: { name: "test", description: "Run unit + integration tests (vitest)" },
|
|
641
1566
|
async run() {
|
|
642
1567
|
await runOrFail("pnpm", ["vitest", "run"]);
|
|
643
1568
|
}
|
|
644
1569
|
});
|
|
645
|
-
var testE2eCommand =
|
|
1570
|
+
var testE2eCommand = defineCommand9({
|
|
646
1571
|
meta: { name: "test:e2e", description: "Run Playwright E2E against the configured template" },
|
|
647
1572
|
async run() {
|
|
648
|
-
const { config } = await
|
|
1573
|
+
const { config } = await loadConfig3();
|
|
649
1574
|
await runOrFail("pnpm", [
|
|
650
1575
|
"--filter",
|
|
651
1576
|
"@nymbalhq/e2e",
|
|
@@ -659,7 +1584,7 @@ var testE2eCommand = defineCommand7({
|
|
|
659
1584
|
]);
|
|
660
1585
|
}
|
|
661
1586
|
});
|
|
662
|
-
var testVisualCommand =
|
|
1587
|
+
var testVisualCommand = defineCommand9({
|
|
663
1588
|
meta: { name: "test:visual", description: "Run visual regression tests (Playwright toHaveScreenshot)" },
|
|
664
1589
|
args: {
|
|
665
1590
|
"update-baselines": {
|
|
@@ -669,7 +1594,7 @@ var testVisualCommand = defineCommand7({
|
|
|
669
1594
|
}
|
|
670
1595
|
},
|
|
671
1596
|
async run({ args }) {
|
|
672
|
-
const { config } = await
|
|
1597
|
+
const { config } = await loadConfig3();
|
|
673
1598
|
const isCI = !!process.env.CI;
|
|
674
1599
|
const updateBaselines = args["update-baselines"];
|
|
675
1600
|
if (updateBaselines && !isCI) {
|
|
@@ -704,7 +1629,7 @@ var testVisualCommand = defineCommand7({
|
|
|
704
1629
|
}
|
|
705
1630
|
}
|
|
706
1631
|
});
|
|
707
|
-
var testContractsCommand =
|
|
1632
|
+
var testContractsCommand = defineCommand9({
|
|
708
1633
|
meta: { name: "test:contracts", description: "Run adapter contract tests" },
|
|
709
1634
|
args: {
|
|
710
1635
|
adapter: { type: "string", description: "Run only contracts for a specific adapter (e.g. payments, email)" }
|
|
@@ -722,10 +1647,10 @@ var testContractsCommand = defineCommand7({
|
|
|
722
1647
|
await runOrFail("pnpm", vArgs);
|
|
723
1648
|
}
|
|
724
1649
|
});
|
|
725
|
-
var testPerformanceCommand =
|
|
1650
|
+
var testPerformanceCommand = defineCommand9({
|
|
726
1651
|
meta: { name: "test:performance", description: "Run Lighthouse CI against the configured template" },
|
|
727
1652
|
async run() {
|
|
728
|
-
const { config } = await
|
|
1653
|
+
const { config } = await loadConfig3();
|
|
729
1654
|
const port = config.template === "nextjs" ? 3e3 : 4321;
|
|
730
1655
|
await runOrFail("pnpm", ["lhci", "autorun"], {
|
|
731
1656
|
env: {
|
|
@@ -736,7 +1661,7 @@ var testPerformanceCommand = defineCommand7({
|
|
|
736
1661
|
});
|
|
737
1662
|
}
|
|
738
1663
|
});
|
|
739
|
-
var testAllCommand =
|
|
1664
|
+
var testAllCommand = defineCommand9({
|
|
740
1665
|
meta: { name: "test:all", description: "Run unit, contract, e2e, visual, and performance tests" },
|
|
741
1666
|
async run() {
|
|
742
1667
|
const steps = [
|
|
@@ -751,7 +1676,7 @@ var testAllCommand = defineCommand7({
|
|
|
751
1676
|
process.exit(code);
|
|
752
1677
|
}
|
|
753
1678
|
}
|
|
754
|
-
const { config } = await
|
|
1679
|
+
const { config } = await loadConfig3();
|
|
755
1680
|
await runOrFail("pnpm", [
|
|
756
1681
|
"--filter",
|
|
757
1682
|
"@nymbalhq/e2e",
|
|
@@ -781,7 +1706,7 @@ var testAllCommand = defineCommand7({
|
|
|
781
1706
|
});
|
|
782
1707
|
|
|
783
1708
|
// src/index.ts
|
|
784
|
-
var main =
|
|
1709
|
+
var main = defineCommand10({
|
|
785
1710
|
meta: {
|
|
786
1711
|
name: "nymbal",
|
|
787
1712
|
version: getCliVersion(),
|
|
@@ -794,6 +1719,8 @@ var main = defineCommand8({
|
|
|
794
1719
|
seed: seedCommand,
|
|
795
1720
|
import: importCommand,
|
|
796
1721
|
"config:validate": configValidateCommand,
|
|
1722
|
+
add: addCommand,
|
|
1723
|
+
doctor: doctorCommand,
|
|
797
1724
|
test: testCommand,
|
|
798
1725
|
"test:e2e": testE2eCommand,
|
|
799
1726
|
"test:visual": testVisualCommand,
|