@maroonedsoftware/johnny5 1.0.3 → 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/README.md CHANGED
@@ -103,7 +103,8 @@ Every command, check, and plugin hook receives the same `CliContext`:
103
103
  | `paths.cwd` | `process.cwd()` at startup. |
104
104
  | `paths.repoRoot` | Nearest ancestor containing `pnpm-workspace.yaml`, else `cwd`. |
105
105
  | `logger` | ANSI-coloured console logger (`info`/`warn`/`error`/`debug`/`success`). Override via `createCliApp({ logger })`. |
106
- | `shell` | `execa` wrapper bound to `repoRoot`; `run` returns the result promise, `runStreaming` inherits stdio and returns the exit code. |
106
+ | `shell` | `execa` wrapper bound to `repoRoot`; `run` returns the result promise, `runStreaming` inherits stdio and returns the exit code, `runDetached` spawns a detached background process. |
107
+ | `daemons` | Project-scoped manager for long-running detached processes. See [Background daemons](#background-daemons). |
107
108
  | `config` | An `AppConfig` instance. Defaults to one with only the dotenv provider attached; pass `config` to `createCliApp` for the full builder. |
108
109
  | `env` | `process.env`. |
109
110
  | `isInteractive()` | True when both stdin and stdout are TTYs. |
@@ -218,6 +219,44 @@ Behaviour worth knowing:
218
219
 
219
220
  For finer control, call `bootstrapForCli({ modules, config })` directly and manage the container/shutdown lifecycle yourself.
220
221
 
222
+ ## Background daemons
223
+
224
+ Commands often need to start a long-running dev process (Storybook, a watch-mode bundler, a vendor daemon) and let the user's terminal go free. `ctx.daemons` is built for that:
225
+
226
+ ```ts
227
+ const start = defineCommand({
228
+ description: 'start storybook in the background',
229
+ run: async (_opts, ctx) => {
230
+ const status = ctx.daemons.start({
231
+ name: 'storybook',
232
+ command: 'pnpm',
233
+ args: ['--filter', '@acme/ui', 'exec', 'storybook', 'dev', '-p', '6006', '--no-open'],
234
+ });
235
+ ctx.logger.success(`storybook running (pid ${status.pid}) — log: ${status.logFile}`);
236
+ },
237
+ });
238
+
239
+ const stop = defineCommand({
240
+ description: 'stop storybook',
241
+ run: async (_opts, ctx) => {
242
+ ctx.daemons.stop('storybook') ? ctx.logger.success('stopped') : ctx.logger.warn('not running');
243
+ },
244
+ });
245
+ ```
246
+
247
+ What you get:
248
+
249
+ - **Idempotent `start`** — when a daemon is already running, the default `onExisting: 'reuse'` policy returns the existing handle and skips the spawn. Use `'restart'` to terminate-and-respawn, or `'error'` to throw.
250
+ - **`stop`, `status`, `list`** — `stop` sends `SIGTERM` (override via `{ signal }`) and deletes the pid file. `status(name)` returns the recorded `{ pid, running, logFile, pidFile, command, args, cwd, startedAt }` (or `undefined`). `list()` returns the status of every daemon registered for the current project.
251
+ - **Project-scoped state** — pid and log files are placed under `<johnnyPaths.runtime>/<projectSlug>/` and `<johnnyPaths.log>/<projectSlug>/`. The slug is `<basename>-<sha256(repoRoot).slice(0,8)>`, so two checkouts of the same repo at different paths get distinct daemon namespaces while remaining easy to identify in `ls` output.
252
+ - **OS-native locations** — `johnnyPaths('johnny5')` returns the conventional dirs for each platform: macOS `~/Library/Logs/johnny5` + `$TMPDIR/johnny5`; Linux `$XDG_STATE_HOME/johnny5` + `$XDG_RUNTIME_DIR/johnny5`; Windows `%LOCALAPPDATA%\johnny5\{Log,Temp}`. Logs are append-only and rotate-friendly; pid files live in runtime/temp dirs that the OS may clear on reboot — exactly the right behaviour for stale records.
253
+
254
+ Under the hood, daemons are spawned via `Shell.runDetached(command, args, { logFile, cwd?, env? })`. Drop down to it directly when you want detached spawn without the pid-file bookkeeping:
255
+
256
+ ```ts
257
+ const { pid, logFile } = ctx.shell.runDetached('node', ['worker.js'], { logFile: '/tmp/worker.log' });
258
+ ```
259
+
221
260
  ## Plugin discovery
222
261
 
223
262
  Workspace packages can contribute commands without the CLI entrypoint knowing about them. In a plugin package's `package.json`:
@@ -285,7 +324,7 @@ const create = defineCommand({
285
324
 
286
325
  | Path | Provides |
287
326
  | --- | --- |
288
- | `@maroonedsoftware/johnny5` | `createCliApp`, `defineCommand`, `registerCommands`, `runChecks`, `buildContext`, `buildDefaultAppConfig`, `loadWorkspacePlugins`, `createShell`, `createDefaultLogger`, `prompts`, `unwrap`, `isInteractive`, plus the `Check` / `CommandModule` / `CliContext` types. |
327
+ | `@maroonedsoftware/johnny5` | `createCliApp`, `defineCommand`, `registerCommands`, `runChecks`, `buildContext`, `buildDefaultAppConfig`, `loadWorkspacePlugins`, `createShell`, `createDaemons`, `johnnyPaths`, `projectSlug`, `createDefaultLogger`, `prompts`, `unwrap`, `isInteractive`, plus the `Check` / `CommandModule` / `CliContext` / `Daemons` types. |
289
328
  | `/serverkit` | `bootstrapForCli`, `configureServerKitModules`, `getOrBootstrapContainer`, `requireContainer`. |
290
329
  | `/versions` | `nodeVersion`, `pnpmVersion`. |
291
330
  | `/filesystem` | `envFile`, `portsFree`. |
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AppConfig } from '@maroonedsoftware/appconfig';
2
- import { C as CliContext, D as DiscoveredCommand, a as CommandRegistration, b as Check, c as CliLogger, d as CommandModule } from './types-DBGyauec.js';
3
- export { A as ArgSpec, e as CheckResult, f as CliPaths, g as CreateLoggerOptions, h as DangerousSpec, E as EnvironmentGuardSpec, O as OptionSpec, i as OptionType, P as PluginManifest, S as Shell, j as ShellOptions, k as createDefaultLogger, l as createShell } from './types-DBGyauec.js';
2
+ import { C as CliContext, D as DiscoveredCommand, a as CommandRegistration, b as Check, c as CliLogger, d as CommandModule } from './types-DH7gcIP5.js';
3
+ export { A as ArgSpec, e as CheckResult, f as CliPaths, g as CreateLoggerOptions, h as DaemonStartOptions, i as DaemonStatus, j as Daemons, k as DangerousSpec, l as DetachedHandle, E as EnvironmentGuardSpec, J as JohnnyPaths, O as OptionSpec, m as OptionType, P as PluginManifest, R as RunDetachedOptions, S as Shell, n as ShellOptions, o as createDaemons, p as createDefaultLogger, q as createShell, r as johnnyPaths, s as projectSlug } from './types-DH7gcIP5.js';
4
4
  import { Command } from 'commander';
5
5
  import * as clack from '@clack/prompts';
6
6
  import 'execa';
package/dist/index.js CHANGED
@@ -270,10 +270,190 @@ var registerCommands = /* @__PURE__ */ __name((program, discovered, ctx) => {
270
270
  }, "registerCommands");
271
271
 
272
272
  // src/context.ts
273
- import { existsSync, readFileSync } from "fs";
274
- import { dirname, resolve } from "path";
273
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
274
+ import { dirname as dirname2, resolve as resolve3 } from "path";
275
275
  import { AppConfigBuilder, AppConfigProviderDotenv } from "@maroonedsoftware/appconfig";
276
276
 
277
+ // src/util/daemons.ts
278
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
279
+ import { resolve as resolve2 } from "path";
280
+
281
+ // src/util/paths.ts
282
+ import { createHash } from "crypto";
283
+ import { homedir, tmpdir } from "os";
284
+ import { basename, resolve } from "path";
285
+ var env = /* @__PURE__ */ __name((key) => {
286
+ const value = process.env[key];
287
+ return value && value.length > 0 ? value : void 0;
288
+ }, "env");
289
+ var johnnyPaths = /* @__PURE__ */ __name((app) => {
290
+ const platform = process.platform;
291
+ if (platform === "darwin") {
292
+ const home2 = homedir();
293
+ return {
294
+ log: resolve(home2, "Library/Logs", app),
295
+ runtime: resolve(tmpdir(), app),
296
+ cache: resolve(home2, "Library/Caches", app)
297
+ };
298
+ }
299
+ if (platform === "win32") {
300
+ const base = env("LOCALAPPDATA") ?? resolve(homedir(), "AppData/Local");
301
+ return {
302
+ log: resolve(base, app, "Log"),
303
+ runtime: resolve(base, app, "Temp"),
304
+ cache: resolve(base, app, "Cache")
305
+ };
306
+ }
307
+ const home = homedir();
308
+ const state = env("XDG_STATE_HOME") ?? resolve(home, ".local/state");
309
+ const cache = env("XDG_CACHE_HOME") ?? resolve(home, ".cache");
310
+ const runtimeBase = env("XDG_RUNTIME_DIR") ?? resolve(tmpdir(), `${app}-${process.getuid?.() ?? 0}`);
311
+ const runtime = env("XDG_RUNTIME_DIR") ? resolve(runtimeBase, app) : runtimeBase;
312
+ return {
313
+ log: resolve(state, app),
314
+ runtime,
315
+ cache: resolve(cache, app)
316
+ };
317
+ }, "johnnyPaths");
318
+ var projectSlug = /* @__PURE__ */ __name((projectRoot) => {
319
+ const absolute = resolve(projectRoot);
320
+ const hash = createHash("sha256").update(absolute).digest("hex").slice(0, 8);
321
+ const name = basename(absolute).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
322
+ return `${name}-${hash}`;
323
+ }, "projectSlug");
324
+
325
+ // src/util/daemons.ts
326
+ var APP_NAME = "johnny5";
327
+ var NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
328
+ var isAlive = /* @__PURE__ */ __name((pid) => {
329
+ try {
330
+ process.kill(pid, 0);
331
+ return true;
332
+ } catch (err) {
333
+ return err.code === "EPERM";
334
+ }
335
+ }, "isAlive");
336
+ var readPidRecord = /* @__PURE__ */ __name((path) => {
337
+ if (!existsSync(path)) return void 0;
338
+ try {
339
+ const raw = readFileSync(path, "utf-8");
340
+ const parsed = JSON.parse(raw);
341
+ if (typeof parsed.pid !== "number") return void 0;
342
+ return parsed;
343
+ } catch {
344
+ return void 0;
345
+ }
346
+ }, "readPidRecord");
347
+ var toStatus = /* @__PURE__ */ __name((name, record, pidFile, logFile) => ({
348
+ name,
349
+ pid: record.pid,
350
+ running: isAlive(record.pid),
351
+ logFile,
352
+ pidFile,
353
+ command: record.command,
354
+ args: record.args,
355
+ cwd: record.cwd,
356
+ startedAt: new Date(record.startedAt)
357
+ }), "toStatus");
358
+ var createDaemons = /* @__PURE__ */ __name((projectRoot, shell, logger, paths = johnnyPaths(APP_NAME)) => {
359
+ const slug = projectSlug(projectRoot);
360
+ const pidDir = resolve2(paths.runtime, slug);
361
+ const logDir = resolve2(paths.log, slug);
362
+ const pidFile = /* @__PURE__ */ __name((name) => {
363
+ if (!NAME_PATTERN.test(name)) {
364
+ throw new Error(`Invalid daemon name '${name}'. Allowed characters: A-Z a-z 0-9 . _ -`);
365
+ }
366
+ return resolve2(pidDir, `${name}.pid`);
367
+ }, "pidFile");
368
+ const logFile = /* @__PURE__ */ __name((name) => {
369
+ if (!NAME_PATTERN.test(name)) {
370
+ throw new Error(`Invalid daemon name '${name}'. Allowed characters: A-Z a-z 0-9 . _ -`);
371
+ }
372
+ return resolve2(logDir, `${name}.log`);
373
+ }, "logFile");
374
+ const status = /* @__PURE__ */ __name((name) => {
375
+ const path = pidFile(name);
376
+ const record = readPidRecord(path);
377
+ if (!record) return void 0;
378
+ return toStatus(name, record, path, logFile(name));
379
+ }, "status");
380
+ const stop = /* @__PURE__ */ __name((name, options = {}) => {
381
+ const current = status(name);
382
+ if (!current) return false;
383
+ let signalled = false;
384
+ if (current.running) {
385
+ try {
386
+ process.kill(current.pid, options.signal ?? "SIGTERM");
387
+ signalled = true;
388
+ } catch (err) {
389
+ if (err.code !== "ESRCH") throw err;
390
+ }
391
+ }
392
+ rmSync(current.pidFile, {
393
+ force: true
394
+ });
395
+ logger.debug(`daemon '${name}' stopped (pid ${current.pid})`);
396
+ return signalled;
397
+ }, "stop");
398
+ const start = /* @__PURE__ */ __name((options) => {
399
+ const existing = status(options.name);
400
+ if (existing?.running) {
401
+ const policy = options.onExisting ?? "reuse";
402
+ if (policy === "reuse") return existing;
403
+ if (policy === "error") {
404
+ throw new Error(`Daemon '${options.name}' is already running (pid ${existing.pid}).`);
405
+ }
406
+ stop(options.name);
407
+ } else if (existing) {
408
+ rmSync(existing.pidFile, {
409
+ force: true
410
+ });
411
+ }
412
+ mkdirSync(pidDir, {
413
+ recursive: true
414
+ });
415
+ mkdirSync(logDir, {
416
+ recursive: true
417
+ });
418
+ const path = pidFile(options.name);
419
+ const log = logFile(options.name);
420
+ const handle = shell.runDetached(options.command, options.args, {
421
+ cwd: options.cwd,
422
+ env: options.env,
423
+ logFile: log
424
+ });
425
+ const record = {
426
+ pid: handle.pid,
427
+ command: options.command,
428
+ args: options.args,
429
+ cwd: options.cwd ?? projectRoot,
430
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
431
+ };
432
+ writeFileSync(path, JSON.stringify(record, null, 2));
433
+ logger.debug(`daemon '${options.name}' started (pid ${handle.pid}, log ${log})`);
434
+ return toStatus(options.name, record, path, log);
435
+ }, "start");
436
+ const list = /* @__PURE__ */ __name(() => {
437
+ if (!existsSync(pidDir)) return [];
438
+ const results = [];
439
+ for (const entry of readdirSync(pidDir)) {
440
+ if (!entry.endsWith(".pid")) continue;
441
+ const name = entry.slice(0, -".pid".length);
442
+ const snapshot = status(name);
443
+ if (snapshot) results.push(snapshot);
444
+ }
445
+ return results;
446
+ }, "list");
447
+ return {
448
+ start,
449
+ stop,
450
+ status,
451
+ list,
452
+ logFile,
453
+ pidFile
454
+ };
455
+ }, "createDaemons");
456
+
277
457
  // src/util/logger.ts
278
458
  var colour = /* @__PURE__ */ __name((code, text) => `\x1B[${code}m${text}\x1B[0m`, "colour");
279
459
  var createDefaultLogger = /* @__PURE__ */ __name((options = {}) => ({
@@ -287,6 +467,9 @@ var createDefaultLogger = /* @__PURE__ */ __name((options = {}) => ({
287
467
  }), "createDefaultLogger");
288
468
 
289
469
  // src/util/shell.ts
470
+ import { spawn } from "child_process";
471
+ import { mkdirSync as mkdirSync2, openSync } from "fs";
472
+ import { dirname } from "path";
290
473
  import { execa } from "execa";
291
474
  var createShell = /* @__PURE__ */ __name((cwd, logger) => ({
292
475
  run: /* @__PURE__ */ __name((command, args, options) => execa(command, args, {
@@ -303,7 +486,37 @@ var createShell = /* @__PURE__ */ __name((cwd, logger) => ({
303
486
  });
304
487
  const result = await child;
305
488
  return result.exitCode ?? 0;
306
- }, "runStreaming")
489
+ }, "runStreaming"),
490
+ runDetached: /* @__PURE__ */ __name((command, args, options = {}) => {
491
+ const workingDir = options.cwd ?? cwd;
492
+ let stdio = "ignore";
493
+ if (options.logFile) {
494
+ mkdirSync2(dirname(options.logFile), {
495
+ recursive: true
496
+ });
497
+ const fd = openSync(options.logFile, "a");
498
+ stdio = [
499
+ "ignore",
500
+ fd,
501
+ fd
502
+ ];
503
+ }
504
+ logger.debug(`$ (detached) ${command} ${args.join(" ")}`);
505
+ const child = spawn(command, args, {
506
+ cwd: workingDir,
507
+ env: options.env ?? process.env,
508
+ detached: true,
509
+ stdio
510
+ });
511
+ if (child.pid === void 0) {
512
+ throw new Error(`Failed to spawn detached process: ${command}`);
513
+ }
514
+ child.unref();
515
+ return {
516
+ pid: child.pid,
517
+ logFile: options.logFile
518
+ };
519
+ }, "runDetached")
307
520
  }), "createShell");
308
521
 
309
522
  // src/util/tty.ts
@@ -317,8 +530,8 @@ var isInteractive = /* @__PURE__ */ __name(() => {
317
530
  var findRepoRoot = /* @__PURE__ */ __name((start) => {
318
531
  let dir = start;
319
532
  for (let i = 0; i < 12; i++) {
320
- if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) return dir;
321
- const parent = dirname(dir);
533
+ if (existsSync2(resolve3(dir, "pnpm-workspace.yaml"))) return dir;
534
+ const parent = dirname2(dir);
322
535
  if (parent === dir) break;
323
536
  dir = parent;
324
537
  }
@@ -329,8 +542,8 @@ var expandValue = /* @__PURE__ */ __name((value) => value.replace(/\$\{([A-Za-z_
329
542
  return process.env[key] ?? "";
330
543
  }), "expandValue");
331
544
  var loadEnvFile = /* @__PURE__ */ __name((path) => {
332
- if (!existsSync(path)) return;
333
- for (const line of readFileSync(path, "utf-8").split("\n")) {
545
+ if (!existsSync2(path)) return;
546
+ for (const line of readFileSync2(path, "utf-8").split("\n")) {
334
547
  const trimmed = line.trim();
335
548
  if (!trimmed || trimmed.startsWith("#")) continue;
336
549
  const eqIdx = trimmed.indexOf("=");
@@ -356,18 +569,20 @@ var buildContext = /* @__PURE__ */ __name(async (options = {}) => {
356
569
  ".env",
357
570
  "apps/api/.env"
358
571
  ]) {
359
- const absolute = envFile.startsWith("/") ? envFile : resolve(repoRoot, envFile);
572
+ const absolute = envFile.startsWith("/") ? envFile : resolve3(repoRoot, envFile);
360
573
  loadEnvFile(absolute);
361
574
  }
362
575
  const logger = options.logger ?? createDefaultLogger({
363
576
  verbose: options.verbose
364
577
  });
365
578
  const shell = createShell(repoRoot, logger);
579
+ const daemons = createDaemons(repoRoot, shell, logger);
366
580
  const config = options.config ?? await buildDefaultAppConfig();
367
581
  return {
368
582
  paths,
369
583
  logger,
370
584
  shell,
585
+ daemons,
371
586
  config,
372
587
  env: process.env,
373
588
  isInteractive
@@ -444,8 +659,8 @@ var buildDoctorCommand = /* @__PURE__ */ __name((checks) => ({
444
659
  }), "buildDoctorCommand");
445
660
 
446
661
  // src/plugin/workspace.loader.ts
447
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
448
- import { join, resolve as resolve2 } from "path";
662
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
663
+ import { join, resolve as resolve4 } from "path";
449
664
  import { pathToFileURL } from "url";
450
665
  var loadWorkspacePlugins = /* @__PURE__ */ __name(async (ctx, options) => {
451
666
  const rootDirs = options.roots ?? [
@@ -455,25 +670,25 @@ var loadWorkspacePlugins = /* @__PURE__ */ __name(async (ctx, options) => {
455
670
  const exclude = new Set(options.excludePackages ?? []);
456
671
  const discovered = [];
457
672
  for (const rootRel of rootDirs) {
458
- const root = resolve2(options.repoRoot, rootRel);
459
- if (!existsSync2(root)) continue;
460
- for (const entry of readdirSync(root, {
673
+ const root = resolve4(options.repoRoot, rootRel);
674
+ if (!existsSync3(root)) continue;
675
+ for (const entry of readdirSync2(root, {
461
676
  withFileTypes: true
462
677
  })) {
463
678
  if (!entry.isDirectory()) continue;
464
679
  const pkgPath = join(root, entry.name, "package.json");
465
- if (!existsSync2(pkgPath)) continue;
680
+ if (!existsSync3(pkgPath)) continue;
466
681
  let pkg;
467
682
  try {
468
- pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
683
+ pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
469
684
  } catch {
470
685
  continue;
471
686
  }
472
687
  const commandsRel = pkg.johnny5?.commands;
473
688
  if (!commandsRel) continue;
474
689
  if (pkg.name && exclude.has(pkg.name)) continue;
475
- const manifestPath = resolve2(root, entry.name, commandsRel);
476
- if (!existsSync2(manifestPath)) {
690
+ const manifestPath = resolve4(root, entry.name, commandsRel);
691
+ if (!existsSync3(manifestPath)) {
477
692
  ctx.logger.warn(`johnny5 plugin manifest missing for ${pkg.name ?? entry.name}: ${manifestPath}`);
478
693
  continue;
479
694
  }
@@ -559,11 +774,14 @@ export {
559
774
  buildDefaultAppConfig,
560
775
  buildDoctorCommand,
561
776
  createCliApp,
777
+ createDaemons,
562
778
  createDefaultLogger,
563
779
  createShell,
564
780
  defineCommand,
565
781
  isInteractive,
782
+ johnnyPaths,
566
783
  loadWorkspacePlugins,
784
+ projectSlug,
567
785
  prompts,
568
786
  registerCommands,
569
787
  runChecks,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrations/serverkit/index.ts","../src/app.ts","../src/util/prompts.ts","../src/commander/safety.ts","../src/commander/register.ts","../src/context.ts","../src/util/logger.ts","../src/util/shell.ts","../src/util/tty.ts","../src/doctor/runner.ts","../src/plugin/workspace.loader.ts"],"sourcesContent":["import { InjectKitRegistry, type Container, type ScopedContainer } from 'injectkit';\nimport { AppConfig } from '@maroonedsoftware/appconfig';\nimport { ConsoleLogger, Logger } from '@maroonedsoftware/logger';\nimport type { ServerKitModule } from '@maroonedsoftware/koa';\nimport type { CliContext, CommandModule } from '../../types.js';\n\n/** Options accepted by `bootstrapForCli`. */\nexport interface BootstrapForCliOptions<ConfigT extends AppConfig = AppConfig> {\n modules: ServerKitModule<ConfigT>[];\n config: ConfigT;\n logger?: Logger;\n}\n\n/** An InjectKit container and a `shutdown` hook that runs every module's `shutdown` in reverse order. */\nexport interface CliContainer {\n container: Container;\n shutdown: () => Promise<void>;\n}\n\n/**\n * Run each `module.setup(registry, config)` and build the InjectKit container.\n * Deliberately does NOT call `module.start()` — CLIs don't want background work\n * (HTTP listeners, job pollers) spinning up. Module `shutdown` hooks are\n * invoked when the returned `shutdown` is called.\n */\nexport const bootstrapForCli = async <ConfigT extends AppConfig = AppConfig>(\n options: BootstrapForCliOptions<ConfigT>,\n): Promise<CliContainer> => {\n const registry = new InjectKitRegistry();\n\n registry.register(Logger).useInstance(options.logger ?? new ConsoleLogger());\n registry.register(AppConfig).useInstance(options.config);\n\n for (const module of options.modules) {\n if (module.setup) await module.setup(registry, options.config);\n }\n\n const container = registry.build();\n\n const shutdown = async (): Promise<void> => {\n for (const module of [...options.modules].reverse()) {\n if (!module.shutdown) continue;\n try {\n await module.shutdown(container);\n } catch {\n // Ignore individual module shutdown failures during teardown.\n }\n }\n };\n\n return { container, shutdown };\n};\n\n// Lazy, per-process bootstrap cache. Composite commands within a single\n// invocation reuse the same container; subsequent invocations bootstrap fresh.\ninterface LazyBootstrap<ConfigT extends AppConfig> {\n modules: ServerKitModule<ConfigT>[];\n promise?: Promise<CliContainer>;\n}\n\n// State must live on globalThis under a Symbol.for key so that the main johnny5\n// bundle and the /serverkit subpath bundle share it. tsup with `splitting:\n// false` builds each entry independently, so module-scoped state would be\n// duplicated — createCliApp would write to one copy and requireContainer would\n// read from another. Symbol.for makes the WeakMap process-wide regardless of\n// which bundle initialised it first.\nconst STATE_KEY = Symbol.for('@maroonedsoftware/johnny5/serverkit/state.v1');\n\ninterface Johnny5ServerkitState {\n containerByContext: WeakMap<CliContext, LazyBootstrap<AppConfig>>;\n}\n\nconst getState = (): Johnny5ServerkitState => {\n const g = globalThis as unknown as Record<symbol, Johnny5ServerkitState | undefined>;\n if (!g[STATE_KEY]) {\n g[STATE_KEY] = { containerByContext: new WeakMap() };\n }\n return g[STATE_KEY] as Johnny5ServerkitState;\n};\n\n/**\n * Associate a list of ServerKit modules with a `CliContext`. The first call to\n * `getOrBootstrapContainer` for that context will lazily run their `setup`\n * hooks. `createCliApp` calls this automatically when `modules` is supplied.\n */\nexport const configureServerKitModules = <ConfigT extends AppConfig>(ctx: CliContext, modules: ServerKitModule<ConfigT>[]): void => {\n getState().containerByContext.set(ctx, { modules: modules as ServerKitModule<AppConfig>[] });\n};\n\n/**\n * Return the bootstrapped container for `ctx`, building it on the first call\n * and caching the promise for subsequent calls within the same process.\n * Throws if `configureServerKitModules` hasn't been called for this context.\n */\nexport const getOrBootstrapContainer = async (ctx: CliContext): Promise<CliContainer> => {\n const lazy = getState().containerByContext.get(ctx);\n if (!lazy) throw new Error('ServerKit modules have not been configured on this CliContext — call configureServerKitModules() in createCliApp first.');\n if (!lazy.promise) {\n lazy.promise = bootstrapForCli({\n modules: lazy.modules,\n config: ctx.config,\n });\n }\n return lazy.promise;\n};\n\n/** `CliContext` augmented with a scoped InjectKit container, handed to `requireContainer` handlers. */\nexport interface RequireContainerCtx extends CliContext {\n container: ScopedContainer;\n}\n\n/**\n * Wrap a command handler so it lazily bootstraps the ServerKit container and\n * receives a fresh scoped container per invocation. The root container is NOT\n * shut down between commands within the same process — call `bootstrapForCli`\n * directly when explicit teardown is required.\n */\nexport const requireContainer = <Opts = Record<string, unknown>>(\n handler: (opts: Opts, ctx: RequireContainerCtx, args: string[]) => Promise<number | void>,\n): CommandModule<Opts>['run'] => {\n return async (opts, ctx, args) => {\n const { container } = await getOrBootstrapContainer(ctx);\n const scoped = container.createScopedContainer() as ScopedContainer;\n const enriched: RequireContainerCtx = Object.assign({}, ctx, { container: scoped });\n return handler(opts, enriched, args);\n };\n};\n","import { Command } from 'commander';\nimport type { AppConfig } from '@maroonedsoftware/appconfig';\nimport type { Check, CliContext, CommandModule, CommandRegistration, DiscoveredCommand } from './types.js';\nimport { registerCommands } from './commander/register.js';\nimport { buildContext } from './context.js';\nimport { buildDoctorCommand } from './doctor/runner.js';\nimport { loadWorkspacePlugins, type WorkspacePluginOptions } from './plugin/workspace.loader.js';\nimport type { CliLogger } from './util/logger.js';\n\n// Opaque ServerKit module shape — the concrete `ServerKitModule` type lives in\n// `@maroonedsoftware/koa`. Importing it here would force every johnny5 consumer\n// to pull koa as a hard dep even when not using ServerKit. The serverkit\n// integration is responsible for the actual setup() / shutdown() calls.\ninterface ServerKitModuleLike<ConfigT> {\n name?: string;\n setup?: (registry: unknown, config: ConfigT) => Promise<void>;\n start?: (container: unknown) => Promise<void>;\n shutdown?: (container: unknown) => Promise<void>;\n}\n\n/** Options accepted by `createCliApp`. */\nexport interface CliAppOptions<ConfigT extends AppConfig = AppConfig> {\n name: string;\n description: string;\n version: string;\n commands: CommandRegistration[];\n checks?: Check[];\n config?: ConfigT | (() => Promise<ConfigT>);\n logger?: CliLogger;\n // ServerKit modules to bootstrap lazily for commands written with\n // `requireContainer`. Setting this enables the @maroonedsoftware/johnny5/serverkit\n // integration — make sure that subpath is imported once for its side effect\n // of installing the bootstrap hook (or call configureServerKitModules\n // manually).\n modules?: ServerKitModuleLike<ConfigT>[];\n plugins?: {\n workspace?: Omit<WorkspacePluginOptions, 'repoRoot'> & { repoRoot?: string };\n };\n // Path of the built-in doctor command. Defaults to ['doctor']. Set to\n // null explicitly when supplying your own doctor command.\n doctorCommandPath?: string[] | null;\n}\n\n/** The runnable CLI returned by `createCliApp`. */\nexport interface CliApp {\n /** Parse `argv` (defaults to `process.argv`) and resolve with a process exit code. */\n run: (argv?: string[]) => Promise<number>;\n}\n\n/**\n * Identity helper that exists purely to give TypeScript a place to infer the\n * `Opts` generic from the literal passed in. Equivalent to writing the type\n * annotation manually.\n */\nexport const defineCommand = <Opts = Record<string, unknown>>(mod: CommandModule<Opts>): CommandModule<Opts> => mod;\n\n/**\n * Build a CLI from a list of `CommandModule` registrations. Auto-registers a\n * `doctor` subcommand when `checks` is non-empty, discovers workspace plugins\n * when `plugins.workspace` is configured, and wires up the ServerKit\n * integration when `modules` is supplied.\n */\nexport const createCliApp = async <ConfigT extends AppConfig = AppConfig>(options: CliAppOptions<ConfigT>): Promise<CliApp> => {\n const verbose = process.argv.includes('-v') || process.argv.includes('--verbose');\n const resolvedConfig = typeof options.config === 'function' ? await options.config() : options.config;\n const ctx = await buildContext({\n config: resolvedConfig,\n logger: options.logger,\n verbose,\n });\n\n if (options.modules && options.modules.length > 0) {\n const { configureServerKitModules } = (await import('./integrations/serverkit/index.js')) as {\n configureServerKitModules: (ctx: CliContext, modules: unknown[]) => void;\n };\n configureServerKitModules(ctx, options.modules);\n }\n\n const program = new Command()\n .name(options.name)\n .description(options.description)\n .version(options.version)\n .option('-v, --verbose', 'Enable verbose logging', false);\n\n const discovered: DiscoveredCommand[] = options.commands.map(c => ({ ...c, source: 'core' as const }));\n\n if (options.checks && options.checks.length > 0 && options.doctorCommandPath !== null) {\n const doctorPath = options.doctorCommandPath ?? ['doctor'];\n const alreadyDefined = discovered.some(c => c.path.join(' ') === doctorPath.join(' '));\n if (!alreadyDefined) {\n discovered.push({\n path: doctorPath,\n source: 'core',\n module: buildDoctorCommand(options.checks),\n });\n }\n }\n\n if (options.plugins?.workspace) {\n const workspaceOpts: WorkspacePluginOptions = {\n ...options.plugins.workspace,\n repoRoot: options.plugins.workspace.repoRoot ?? ctx.paths.repoRoot,\n };\n const plugins = await loadWorkspacePlugins(ctx, workspaceOpts);\n discovered.push(...plugins);\n }\n\n registerCommands(program, discovered, ctx);\n\n return {\n run: async (argv = process.argv) => {\n try {\n await program.parseAsync(argv);\n return 0;\n } catch (err) {\n ctx.logger.error((err as Error).message);\n return 1;\n }\n },\n };\n};\n","import * as clack from '@clack/prompts';\n\n/** Re-export of the `@clack/prompts` namespace under a stable name. */\nexport const prompts = clack;\n\n/** Thrown by `unwrap` when the user cancels a clack prompt (e.g. Ctrl+C). */\nexport class PromptCancelledError extends Error {\n constructor() {\n super('prompt cancelled');\n this.name = 'PromptCancelledError';\n }\n}\n\n/**\n * Unwrap a clack prompt result, throwing `PromptCancelledError` when the user\n * cancelled. Lets command handlers use try/catch instead of branching on\n * `isCancel` at every prompt.\n */\nexport const unwrap = <T>(value: T | symbol): T => {\n if (clack.isCancel(value)) throw new PromptCancelledError();\n return value as T;\n};\n","import type { CliContext, CommandModule, DangerousSpec, EnvironmentGuardSpec } from '../types.js';\nimport { prompts } from '../util/prompts.js';\n\nconst resolveEnvGuard = (mod: CommandModule): EnvironmentGuardSpec | null => {\n if (!mod.allowedEnvironments) return null;\n if (Array.isArray(mod.allowedEnvironments)) return { allowed: mod.allowedEnvironments };\n return mod.allowedEnvironments;\n};\n\nconst resolveDangerous = (mod: CommandModule): DangerousSpec | null => {\n if (!mod.dangerous) return null;\n if (mod.dangerous === true) return {};\n return mod.dangerous;\n};\n\nconst hasYesOption = (mod: CommandModule): boolean => (mod.options ?? []).some(o => /(^|[\\s,])(-y|--yes)([\\s,]|$)/.test(o.flags));\n\n/**\n * Returns true when the env guard is satisfied or absent. Logs and returns\n * false when the current environment is not in the allowed list — the caller\n * should treat that as a refusal and exit non-zero.\n */\nexport const checkEnvironmentGuard = (mod: CommandModule, ctx: CliContext, pathLabel: string): boolean => {\n const guard = resolveEnvGuard(mod);\n if (!guard) return true;\n const variable = guard.variable ?? 'NODE_ENV';\n const current = ctx.env[variable];\n if (current !== undefined && guard.allowed.includes(current)) return true;\n const shown = current === undefined ? '(unset)' : current;\n ctx.logger.error(`Refusing to run \"${pathLabel}\" with ${variable}=${shown}. Allowed: ${guard.allowed.join(', ')}.`);\n return false;\n};\n\n/**\n * Resolves a destructive-command confirmation. Returns true when the command\n * should proceed. In non-interactive contexts the caller must pass `--yes`\n * (reflected in `userOptedIn`); otherwise the user is prompted.\n */\nexport const confirmDangerous = async (mod: CommandModule, ctx: CliContext, pathLabel: string, userOptedIn: boolean): Promise<boolean> => {\n const spec = resolveDangerous(mod);\n if (!spec) return true;\n if (userOptedIn) return true;\n if (!ctx.isInteractive()) {\n ctx.logger.error(`\"${pathLabel}\" is destructive; pass --yes to confirm in non-interactive mode.`);\n return false;\n }\n if (spec.confirm === 'typed') {\n const phrase = spec.phrase ?? pathLabel;\n const result = await prompts.text({ message: spec.message ?? `This is destructive. Type \"${phrase}\" to continue:` });\n if (prompts.isCancel(result)) return false;\n if (result !== phrase) {\n ctx.logger.warn('Confirmation did not match — aborting.');\n return false;\n }\n return true;\n }\n const result = await prompts.confirm({ message: spec.message ?? `Run destructive command \"${pathLabel}\"?`, initialValue: false });\n if (prompts.isCancel(result)) return false;\n return result === true;\n};\n\n/** Whether the command needs an injected `-y, --yes` option to be registered. */\nexport const needsYesOption = (mod: CommandModule): boolean => Boolean(mod.dangerous) && !hasYesOption(mod);\n","import { Command } from 'commander';\nimport type { CliContext, CommandModule, DiscoveredCommand, OptionSpec } from '../types.js';\nimport { checkEnvironmentGuard, confirmDangerous, needsYesOption } from './safety.js';\n\nconst applyOption = (cmd: Command, spec: OptionSpec): void => {\n if (spec.required) {\n cmd.requiredOption(spec.flags, spec.description, spec.default as string | undefined);\n return;\n }\n if (spec.default !== undefined) {\n cmd.option(spec.flags, spec.description, spec.default as string | boolean);\n } else {\n cmd.option(spec.flags, spec.description);\n }\n};\n\nconst findOrCreateGroup = (parent: Command, name: string): Command => {\n const existing = parent.commands.find(c => c.name() === name);\n if (existing) return existing;\n return parent.command(name).description(`${name} commands`);\n};\n\n// Extract the long-name (or short-name) of a commander flags string and\n// convert kebab-case to camelCase, matching commander's own option key\n// derivation. e.g. `--org-name <name>` → 'orgName'.\nconst deriveOptionKey = (flags: string): string => {\n const tokens = flags.split(/[ ,]+/);\n const long = tokens.find(t => t.startsWith('--'));\n const target = long ?? tokens.find(t => t.startsWith('-'));\n if (!target) return flags;\n const stripped = target.replace(/^-+/, '');\n return stripped.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());\n};\n\nconst attachLeaf = (parent: Command, leafName: string, mod: CommandModule, ctx: CliContext, sourceLabel: string, fullPath: string[]): void => {\n const cmd = parent.command(leafName).description(mod.description);\n const pathLabel = fullPath.join(' ');\n\n for (const arg of mod.args ?? []) {\n const argName = arg.variadic ? `${arg.name}...` : arg.name;\n if (arg.required) cmd.argument(`<${argName}>`, arg.description);\n else cmd.argument(`[${argName}]`, arg.description);\n }\n\n for (const opt of mod.options ?? []) {\n applyOption(cmd, opt);\n }\n\n if (needsYesOption(mod)) cmd.option('-y, --yes', 'Skip confirmation prompt for this destructive command', false);\n\n if (mod.passthrough) cmd.allowUnknownOption(true).allowExcessArguments(true);\n\n cmd.action(async (...allArgs: unknown[]) => {\n // Commander passes positional args first, then the parsed options\n // object, then the Command instance. We slice off the last two.\n const commandInstance = allArgs[allArgs.length - 1] as Command;\n const opts = (allArgs[allArgs.length - 2] ?? {}) as Record<string, unknown>;\n const positional = allArgs.slice(0, allArgs.length - 2);\n\n const positionalStrings: string[] = positional.flatMap(p => (Array.isArray(p) ? p.map(String) : p == null ? [] : [String(p)]));\n const passthroughArgs = mod.passthrough ? commandInstance.args : positionalStrings;\n\n for (const optSpec of mod.options ?? []) {\n if (!optSpec.envVar) continue;\n const key = deriveOptionKey(optSpec.flags);\n if (opts[key] === undefined && process.env[optSpec.envVar] !== undefined) {\n opts[key] = process.env[optSpec.envVar];\n }\n }\n\n if (!checkEnvironmentGuard(mod, ctx, pathLabel)) {\n process.exit(1);\n return;\n }\n\n if (mod.dangerous) {\n const proceed = await confirmDangerous(mod, ctx, pathLabel, opts['yes'] === true);\n if (!proceed) {\n process.exit(1);\n return;\n }\n }\n\n let finalOpts = opts;\n if (mod.interactive && ctx.isInteractive()) {\n finalOpts = (await mod.interactive(ctx, opts)) as Record<string, unknown>;\n }\n\n try {\n const exitCode = await mod.run(finalOpts, ctx, passthroughArgs);\n if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);\n } catch (err) {\n ctx.logger.error(`[${sourceLabel}] ${(err as Error).message}`);\n if ((err as Error).stack) ctx.logger.debug((err as Error).stack ?? '');\n process.exit(1);\n }\n });\n};\n\n/**\n * Attach every discovered command to a commander `Program`, building intermediate\n * group nodes as needed. Core registrations are processed before plugin ones, so\n * a plugin that tries to claim a path already held by core throws with a\n * descriptive error.\n */\nexport const registerCommands = (program: Command, discovered: DiscoveredCommand[], ctx: CliContext): void => {\n const registeredPaths = new Map<string, { source: 'core' | 'plugin'; sourceName?: string }>();\n\n // Core first, then plugins — plugins can extend but not override.\n const sorted = [...discovered].sort((a, b) => {\n if (a.source === b.source) return 0;\n return a.source === 'core' ? -1 : 1;\n });\n\n for (const entry of sorted) {\n const key = entry.path.join(' ');\n const existing = registeredPaths.get(key);\n if (existing) {\n const incoming = entry.source === 'plugin' ? (entry.sourceName ?? 'unknown plugin') : 'core';\n const owner = existing.source === 'plugin' ? (existing.sourceName ?? 'unknown plugin') : 'core';\n throw new Error(`command \"${key}\" is already registered by ${owner}; ${incoming} cannot override it`);\n }\n registeredPaths.set(key, { source: entry.source, sourceName: entry.sourceName });\n\n let parent: Command = program;\n for (const segment of entry.path.slice(0, -1)) {\n parent = findOrCreateGroup(parent, segment);\n }\n const leafName = entry.path[entry.path.length - 1];\n if (!leafName) continue;\n const sourceLabel = entry.source === 'plugin' ? (entry.sourceName ?? 'plugin') : 'core';\n attachLeaf(parent, leafName, entry.module, ctx, sourceLabel, entry.path);\n }\n};\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { AppConfigBuilder, AppConfigProviderDotenv, type AppConfig } from '@maroonedsoftware/appconfig';\nimport type { CliContext, CliPaths } from './types.js';\nimport type { CliLogger } from './util/logger.js';\nimport { createDefaultLogger } from './util/logger.js';\nimport { createShell } from './util/shell.js';\nimport { isInteractive } from './util/tty.js';\n\n/** Options accepted by `buildContext`. */\nexport interface BuildContextOptions {\n config?: AppConfig;\n logger?: CliLogger;\n verbose?: boolean;\n repoRoot?: string;\n /**\n * Paths to .env files (absolute, or relative to the resolved repoRoot) to\n * load into process.env before building AppConfig. Missing files are\n * silently skipped. Existing process.env values are not overridden.\n * Defaults to ['.env', 'apps/api/.env'].\n */\n envFiles?: string[];\n}\n\nconst findRepoRoot = (start: string): string => {\n let dir = start;\n for (let i = 0; i < 12; i++) {\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n};\n\n// Expands `$VAR` and `${VAR}` references against process.env. Matches the\n// behaviour of dotenv-expand so .env files authored for dbmate/docker-compose\n// (where placeholders are common) still produce usable runtime values.\nconst expandValue = (value: string): string =>\n value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}|\\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced: string | undefined, bare: string | undefined) => {\n const key = (braced ?? bare) as string;\n return process.env[key] ?? '';\n });\n\nconst loadEnvFile = (path: string): void => {\n if (!existsSync(path)) return;\n for (const line of readFileSync(path, 'utf-8').split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eqIdx = trimmed.indexOf('=');\n if (eqIdx === -1) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n const rawValue = trimmed.slice(eqIdx + 1).trim();\n\n // Detect quoting style before unwrapping. Single-quoted values are\n // taken literally; double-quoted and unquoted values get $VAR\n // expansion against the current process.env.\n const singleQuoted = rawValue.startsWith(\"'\") && rawValue.endsWith(\"'\");\n const doubleQuoted = rawValue.startsWith('\"') && rawValue.endsWith('\"');\n let value = singleQuoted || doubleQuoted ? rawValue.slice(1, -1) : rawValue;\n if (!singleQuoted) value = expandValue(value);\n\n if (!(key in process.env)) process.env[key] = value;\n }\n};\n\n/**\n * Build an AppConfig with only the dotenv provider attached. Callers are\n * expected to have loaded .env files into `process.env` beforehand — see\n * `buildContext` for the default loading sequence.\n */\nexport const buildDefaultAppConfig = async (): Promise<AppConfig> =>\n new AppConfigBuilder().addProvider(new AppConfigProviderDotenv()).build();\n\n/**\n * Build the `CliContext` handed to every command, check, and plugin hook. Loads\n * `.env` files into `process.env`, resolves the workspace `repoRoot`, and wires\n * up shell, logger, and config.\n */\nexport const buildContext = async (options: BuildContextOptions = {}): Promise<CliContext> => {\n // Start from cwd so consumers linked from a sibling repo (or installed\n // from npm into node_modules) still resolve to the CONSUMER's workspace\n // root rather than wherever johnny5 itself happens to live.\n const cwd = process.cwd();\n const repoRoot = options.repoRoot ?? findRepoRoot(cwd);\n const paths: CliPaths = { cwd, repoRoot };\n\n for (const envFile of options.envFiles ?? ['.env', 'apps/api/.env']) {\n const absolute = envFile.startsWith('/') ? envFile : resolve(repoRoot, envFile);\n loadEnvFile(absolute);\n }\n\n const logger = options.logger ?? createDefaultLogger({ verbose: options.verbose });\n const shell = createShell(repoRoot, logger);\n const config = options.config ?? (await buildDefaultAppConfig());\n\n return {\n paths,\n logger,\n shell,\n config,\n env: process.env,\n isInteractive,\n };\n};\n","/** Minimal logger interface that every command and check receives via `CliContext`. */\nexport interface CliLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n debug: (msg: string) => void;\n success: (msg: string) => void;\n}\n\nconst colour = (code: number, text: string): string => `\\x1b[${code}m${text}\\x1b[0m`;\n\n/** Options accepted by `createDefaultLogger`. */\nexport interface CreateLoggerOptions {\n /** When true, `debug` writes to stdout; otherwise it's a no-op. */\n verbose?: boolean;\n}\n\n/**\n * Build the default ANSI-coloured console logger used when a consumer doesn't\n * supply their own. `debug` output is gated on `verbose`.\n */\nexport const createDefaultLogger = (options: CreateLoggerOptions = {}): CliLogger => ({\n info: msg => console.log(msg),\n warn: msg => console.warn(colour(33, `! ${msg}`)),\n error: msg => console.error(colour(31, `✗ ${msg}`)),\n success: msg => console.log(colour(32, `✓ ${msg}`)),\n debug: msg => {\n if (options.verbose) console.log(colour(90, `· ${msg}`));\n },\n});\n","import { execa, type Options as ExecaOptions, type ResultPromise } from 'execa';\nimport type { CliLogger } from './logger.js';\n\n/** Execa options re-typed to require a string `cwd` at the call site. */\nexport interface ShellOptions extends ExecaOptions {\n cwd?: string;\n}\n\n/** Tiny shell wrapper around execa exposed on `CliContext.shell`. */\nexport interface Shell {\n /** Run a command, returning the execa result promise. Use this when the caller needs stdout/stderr. */\n run: (command: string, args: string[], options?: ShellOptions) => ResultPromise;\n /** Run a command with inherited stdio, returning the exit code. Failures don't throw — the exit code is returned instead. */\n runStreaming: (command: string, args: string[], options?: ShellOptions) => Promise<number>;\n}\n\n/** Build a `Shell` bound to `cwd`, logging streaming invocations through `logger.debug`. */\nexport const createShell = (cwd: string, logger: CliLogger): Shell => ({\n run: (command, args, options) => execa(command, args, { cwd, ...options }),\n runStreaming: async (command, args, options) => {\n logger.debug(`$ ${command} ${args.join(' ')}`);\n const child = execa(command, args, { cwd, stdio: 'inherit', reject: false, ...options });\n const result = await child;\n return result.exitCode ?? 0;\n },\n});\n","/**\n * Best-effort guess at whether the CLI is talking to a human. Returns false in\n * CI (`CI=true` / `CI=1`), when `JOHNNY5_NON_INTERACTIVE=1`, or when either of\n * stdout/stdin isn't a TTY.\n */\nexport const isInteractive = (): boolean => {\n if (process.env['CI'] === 'true' || process.env['CI'] === '1') return false;\n if (process.env['JOHNNY5_NON_INTERACTIVE'] === '1') return false;\n return Boolean(process.stdout.isTTY && process.stdin.isTTY);\n};\n","import type { Check, CheckResult, CliContext, CommandModule } from '../types.js';\n\n/** Options passed to `runChecks`. */\nexport interface DoctorOptions {\n /** When true, failing checks with an `autoFix` hook get a chance to remediate. */\n fix?: boolean;\n}\n\n/**\n * Run a list of doctor checks sequentially, rendering progress to stdout. Returns\n * a process exit code: `0` when every check passes (including via `autoFix` when\n * `--fix` is supplied), `1` when at least one check fails.\n */\nexport const runChecks = async (ctx: CliContext, checks: Check[], options: DoctorOptions): Promise<number> => {\n ctx.logger.info('Running doctor…\\n');\n\n let failed = 0;\n let fixed = 0;\n\n for (const check of checks) {\n process.stdout.write(` ${check.name.padEnd(36, ' ')} `);\n let result: CheckResult;\n try {\n result = await check.run(ctx);\n } catch (err) {\n result = { ok: false, message: `threw: ${(err as Error).message}` };\n }\n\n if (result.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${result.message}\\n`);\n continue;\n }\n\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${result.message}\\n`);\n\n if (options.fix && check.autoFix) {\n process.stdout.write(` ↻ attempting auto-fix… `);\n try {\n const fixResult = await check.autoFix(ctx);\n if (fixResult.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${fixResult.message}\\n`);\n fixed++;\n continue;\n }\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${fixResult.message}\\n`);\n } catch (err) {\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${(err as Error).message}\\n`);\n }\n } else if (result.fixHint) {\n process.stdout.write(` → ${result.fixHint}\\n`);\n }\n failed++;\n }\n\n process.stdout.write('\\n');\n if (failed === 0) {\n ctx.logger.success('All checks passed.');\n return 0;\n }\n if (options.fix && fixed > 0) {\n ctx.logger.info(`Auto-fixed ${fixed} issue(s); re-run \\`doctor\\` to confirm.`);\n }\n ctx.logger.error(`${failed} check(s) failed.`);\n return 1;\n};\n\n/**\n * Build the `CommandModule` for the built-in `doctor` subcommand from a set of\n * checks. `createCliApp` auto-registers this when `checks` is non-empty.\n */\nexport const buildDoctorCommand = (checks: Check[]): CommandModule<{ fix?: boolean }> => ({\n description: 'Run local-dev health checks',\n options: [{ flags: '--fix', description: 'Attempt auto-remediation for checks that support it' }],\n run: async (opts, ctx) => runChecks(ctx, checks, { fix: opts.fix === true }),\n});\n","import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { CliContext, DiscoveredCommand, PluginManifest } from '../types.js';\n\ninterface WorkspacePackageJson {\n name?: string;\n johnny5?: {\n commands?: string;\n };\n}\n\n/** Options accepted by `loadWorkspacePlugins`. */\nexport interface WorkspacePluginOptions {\n repoRoot: string;\n /**\n * Workspace-relative directories whose immediate children are scanned for\n * `package.json` files. Defaults to `['apps', 'packages']`.\n */\n roots?: string[];\n /**\n * Package names to skip — typically the consumer's own CLI package whose\n * commands are loaded directly, not via plugin discovery.\n */\n excludePackages?: string[];\n}\n\n/**\n * Scan every workspace package in the configured roots for a `\"johnny5\"` field\n * in `package.json`. When present, the referenced file is dynamically imported\n * and expected to default-export a `PluginManifest`. Failures to load a single\n * plugin log a warning through `ctx.logger.warn` but don't abort the CLI.\n */\nexport const loadWorkspacePlugins = async (ctx: CliContext, options: WorkspacePluginOptions): Promise<DiscoveredCommand[]> => {\n const rootDirs = options.roots ?? ['apps', 'packages'];\n const exclude = new Set(options.excludePackages ?? []);\n const discovered: DiscoveredCommand[] = [];\n\n for (const rootRel of rootDirs) {\n const root = resolve(options.repoRoot, rootRel);\n if (!existsSync(root)) continue;\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const pkgPath = join(root, entry.name, 'package.json');\n if (!existsSync(pkgPath)) continue;\n\n let pkg: WorkspacePackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as WorkspacePackageJson;\n } catch {\n continue;\n }\n\n const commandsRel = pkg.johnny5?.commands;\n if (!commandsRel) continue;\n if (pkg.name && exclude.has(pkg.name)) continue;\n\n const manifestPath = resolve(root, entry.name, commandsRel);\n if (!existsSync(manifestPath)) {\n ctx.logger.warn(`johnny5 plugin manifest missing for ${pkg.name ?? entry.name}: ${manifestPath}`);\n continue;\n }\n\n try {\n const mod = (await import(pathToFileURL(manifestPath).href)) as { default: PluginManifest };\n const manifest = mod.default;\n if (!manifest || !Array.isArray(manifest.commands)) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} has no commands array; skipping`);\n continue;\n }\n for (const cmd of manifest.commands) {\n discovered.push({\n path: cmd.path,\n source: 'plugin',\n sourceName: manifest.name ?? pkg.name,\n module: cmd.module,\n });\n }\n } catch (err) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} failed to load: ${(err as Error).message}`);\n }\n }\n }\n\n return discovered;\n};\n"],"mappings":"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","names":["InjectKitRegistry","AppConfig","ConsoleLogger","Logger","bootstrapForCli","STATE_KEY","getState","configureServerKitModules","getOrBootstrapContainer","requireContainer","options","registry","register","useInstance","logger","config","module","modules","setup","container","build","shutdown","reverse","Symbol","for","g","globalThis","containerByContext","WeakMap","ctx","set","lazy","get","Error","promise","handler","opts","args","scoped","createScopedContainer","enriched","Object","assign","Command","clack","prompts","clack","PromptCancelledError","Error","name","unwrap","value","isCancel","resolveEnvGuard","mod","allowedEnvironments","Array","isArray","allowed","resolveDangerous","dangerous","hasYesOption","options","some","o","test","flags","checkEnvironmentGuard","ctx","pathLabel","guard","variable","current","env","undefined","includes","shown","logger","error","join","confirmDangerous","userOptedIn","spec","isInteractive","confirm","phrase","result","prompts","text","message","isCancel","warn","initialValue","needsYesOption","Boolean","applyOption","cmd","spec","required","requiredOption","flags","description","default","undefined","option","findOrCreateGroup","parent","name","existing","commands","find","c","command","deriveOptionKey","tokens","split","long","t","startsWith","target","stripped","replace","_","toUpperCase","attachLeaf","leafName","mod","ctx","sourceLabel","fullPath","pathLabel","join","arg","args","argName","variadic","argument","opt","options","needsYesOption","passthrough","allowUnknownOption","allowExcessArguments","action","allArgs","commandInstance","length","opts","positional","slice","positionalStrings","flatMap","p","Array","isArray","map","String","passthroughArgs","optSpec","envVar","key","process","env","checkEnvironmentGuard","exit","dangerous","proceed","confirmDangerous","finalOpts","interactive","isInteractive","exitCode","run","err","logger","error","message","stack","debug","registerCommands","program","discovered","registeredPaths","Map","sorted","sort","a","b","source","entry","path","get","incoming","sourceName","owner","Error","set","segment","module","existsSync","readFileSync","dirname","resolve","AppConfigBuilder","AppConfigProviderDotenv","colour","code","text","createDefaultLogger","options","info","msg","console","log","warn","error","success","debug","verbose","execa","createShell","cwd","logger","run","command","args","options","execa","runStreaming","debug","join","child","stdio","reject","result","exitCode","isInteractive","process","env","Boolean","stdout","isTTY","stdin","findRepoRoot","start","dir","i","existsSync","resolve","parent","dirname","process","cwd","expandValue","value","replace","_","braced","bare","key","env","loadEnvFile","path","line","readFileSync","split","trimmed","trim","startsWith","eqIdx","indexOf","slice","rawValue","singleQuoted","endsWith","doubleQuoted","buildDefaultAppConfig","AppConfigBuilder","addProvider","AppConfigProviderDotenv","build","buildContext","options","repoRoot","paths","envFile","envFiles","absolute","logger","createDefaultLogger","verbose","shell","createShell","config","isInteractive","runChecks","ctx","checks","options","logger","info","failed","fixed","check","process","stdout","write","name","padEnd","result","run","err","ok","message","fix","autoFix","fixResult","fixHint","success","error","buildDoctorCommand","description","flags","opts","existsSync","readFileSync","readdirSync","join","resolve","pathToFileURL","loadWorkspacePlugins","ctx","options","rootDirs","roots","exclude","Set","excludePackages","discovered","rootRel","root","resolve","repoRoot","existsSync","entry","readdirSync","withFileTypes","isDirectory","pkgPath","join","name","pkg","JSON","parse","readFileSync","commandsRel","johnny5","commands","has","manifestPath","logger","warn","mod","pathToFileURL","href","manifest","default","Array","isArray","cmd","push","path","source","sourceName","module","err","message","defineCommand","mod","createCliApp","options","verbose","process","argv","includes","resolvedConfig","config","ctx","buildContext","logger","modules","length","configureServerKitModules","program","Command","name","description","version","option","discovered","commands","map","c","source","checks","doctorCommandPath","doctorPath","alreadyDefined","some","path","join","push","module","buildDoctorCommand","plugins","workspace","workspaceOpts","repoRoot","paths","loadWorkspacePlugins","registerCommands","run","parseAsync","err","error","message"]}
1
+ {"version":3,"sources":["../src/integrations/serverkit/index.ts","../src/app.ts","../src/util/prompts.ts","../src/commander/safety.ts","../src/commander/register.ts","../src/context.ts","../src/util/daemons.ts","../src/util/paths.ts","../src/util/logger.ts","../src/util/shell.ts","../src/util/tty.ts","../src/doctor/runner.ts","../src/plugin/workspace.loader.ts"],"sourcesContent":["import { InjectKitRegistry, type Container, type ScopedContainer } from 'injectkit';\nimport { AppConfig } from '@maroonedsoftware/appconfig';\nimport { ConsoleLogger, Logger } from '@maroonedsoftware/logger';\nimport type { ServerKitModule } from '@maroonedsoftware/koa';\nimport type { CliContext, CommandModule } from '../../types.js';\n\n/** Options accepted by `bootstrapForCli`. */\nexport interface BootstrapForCliOptions<ConfigT extends AppConfig = AppConfig> {\n modules: ServerKitModule<ConfigT>[];\n config: ConfigT;\n logger?: Logger;\n}\n\n/** An InjectKit container and a `shutdown` hook that runs every module's `shutdown` in reverse order. */\nexport interface CliContainer {\n container: Container;\n shutdown: () => Promise<void>;\n}\n\n/**\n * Run each `module.setup(registry, config)` and build the InjectKit container.\n * Deliberately does NOT call `module.start()` — CLIs don't want background work\n * (HTTP listeners, job pollers) spinning up. Module `shutdown` hooks are\n * invoked when the returned `shutdown` is called.\n */\nexport const bootstrapForCli = async <ConfigT extends AppConfig = AppConfig>(\n options: BootstrapForCliOptions<ConfigT>,\n): Promise<CliContainer> => {\n const registry = new InjectKitRegistry();\n\n registry.register(Logger).useInstance(options.logger ?? new ConsoleLogger());\n registry.register(AppConfig).useInstance(options.config);\n\n for (const module of options.modules) {\n if (module.setup) await module.setup(registry, options.config);\n }\n\n const container = registry.build();\n\n const shutdown = async (): Promise<void> => {\n for (const module of [...options.modules].reverse()) {\n if (!module.shutdown) continue;\n try {\n await module.shutdown(container);\n } catch {\n // Ignore individual module shutdown failures during teardown.\n }\n }\n };\n\n return { container, shutdown };\n};\n\n// Lazy, per-process bootstrap cache. Composite commands within a single\n// invocation reuse the same container; subsequent invocations bootstrap fresh.\ninterface LazyBootstrap<ConfigT extends AppConfig> {\n modules: ServerKitModule<ConfigT>[];\n promise?: Promise<CliContainer>;\n}\n\n// State must live on globalThis under a Symbol.for key so that the main johnny5\n// bundle and the /serverkit subpath bundle share it. tsup with `splitting:\n// false` builds each entry independently, so module-scoped state would be\n// duplicated — createCliApp would write to one copy and requireContainer would\n// read from another. Symbol.for makes the WeakMap process-wide regardless of\n// which bundle initialised it first.\nconst STATE_KEY = Symbol.for('@maroonedsoftware/johnny5/serverkit/state.v1');\n\ninterface Johnny5ServerkitState {\n containerByContext: WeakMap<CliContext, LazyBootstrap<AppConfig>>;\n}\n\nconst getState = (): Johnny5ServerkitState => {\n const g = globalThis as unknown as Record<symbol, Johnny5ServerkitState | undefined>;\n if (!g[STATE_KEY]) {\n g[STATE_KEY] = { containerByContext: new WeakMap() };\n }\n return g[STATE_KEY] as Johnny5ServerkitState;\n};\n\n/**\n * Associate a list of ServerKit modules with a `CliContext`. The first call to\n * `getOrBootstrapContainer` for that context will lazily run their `setup`\n * hooks. `createCliApp` calls this automatically when `modules` is supplied.\n */\nexport const configureServerKitModules = <ConfigT extends AppConfig>(ctx: CliContext, modules: ServerKitModule<ConfigT>[]): void => {\n getState().containerByContext.set(ctx, { modules: modules as ServerKitModule<AppConfig>[] });\n};\n\n/**\n * Return the bootstrapped container for `ctx`, building it on the first call\n * and caching the promise for subsequent calls within the same process.\n * Throws if `configureServerKitModules` hasn't been called for this context.\n */\nexport const getOrBootstrapContainer = async (ctx: CliContext): Promise<CliContainer> => {\n const lazy = getState().containerByContext.get(ctx);\n if (!lazy) throw new Error('ServerKit modules have not been configured on this CliContext — call configureServerKitModules() in createCliApp first.');\n if (!lazy.promise) {\n lazy.promise = bootstrapForCli({\n modules: lazy.modules,\n config: ctx.config,\n });\n }\n return lazy.promise;\n};\n\n/** `CliContext` augmented with a scoped InjectKit container, handed to `requireContainer` handlers. */\nexport interface RequireContainerCtx extends CliContext {\n container: ScopedContainer;\n}\n\n/**\n * Wrap a command handler so it lazily bootstraps the ServerKit container and\n * receives a fresh scoped container per invocation. The root container is NOT\n * shut down between commands within the same process — call `bootstrapForCli`\n * directly when explicit teardown is required.\n */\nexport const requireContainer = <Opts = Record<string, unknown>>(\n handler: (opts: Opts, ctx: RequireContainerCtx, args: string[]) => Promise<number | void>,\n): CommandModule<Opts>['run'] => {\n return async (opts, ctx, args) => {\n const { container } = await getOrBootstrapContainer(ctx);\n const scoped = container.createScopedContainer() as ScopedContainer;\n const enriched: RequireContainerCtx = Object.assign({}, ctx, { container: scoped });\n return handler(opts, enriched, args);\n };\n};\n","import { Command } from 'commander';\nimport type { AppConfig } from '@maroonedsoftware/appconfig';\nimport type { Check, CliContext, CommandModule, CommandRegistration, DiscoveredCommand } from './types.js';\nimport { registerCommands } from './commander/register.js';\nimport { buildContext } from './context.js';\nimport { buildDoctorCommand } from './doctor/runner.js';\nimport { loadWorkspacePlugins, type WorkspacePluginOptions } from './plugin/workspace.loader.js';\nimport type { CliLogger } from './util/logger.js';\n\n// Opaque ServerKit module shape — the concrete `ServerKitModule` type lives in\n// `@maroonedsoftware/koa`. Importing it here would force every johnny5 consumer\n// to pull koa as a hard dep even when not using ServerKit. The serverkit\n// integration is responsible for the actual setup() / shutdown() calls.\ninterface ServerKitModuleLike<ConfigT> {\n name?: string;\n setup?: (registry: unknown, config: ConfigT) => Promise<void>;\n start?: (container: unknown) => Promise<void>;\n shutdown?: (container: unknown) => Promise<void>;\n}\n\n/** Options accepted by `createCliApp`. */\nexport interface CliAppOptions<ConfigT extends AppConfig = AppConfig> {\n name: string;\n description: string;\n version: string;\n commands: CommandRegistration[];\n checks?: Check[];\n config?: ConfigT | (() => Promise<ConfigT>);\n logger?: CliLogger;\n // ServerKit modules to bootstrap lazily for commands written with\n // `requireContainer`. Setting this enables the @maroonedsoftware/johnny5/serverkit\n // integration — make sure that subpath is imported once for its side effect\n // of installing the bootstrap hook (or call configureServerKitModules\n // manually).\n modules?: ServerKitModuleLike<ConfigT>[];\n plugins?: {\n workspace?: Omit<WorkspacePluginOptions, 'repoRoot'> & { repoRoot?: string };\n };\n // Path of the built-in doctor command. Defaults to ['doctor']. Set to\n // null explicitly when supplying your own doctor command.\n doctorCommandPath?: string[] | null;\n}\n\n/** The runnable CLI returned by `createCliApp`. */\nexport interface CliApp {\n /** Parse `argv` (defaults to `process.argv`) and resolve with a process exit code. */\n run: (argv?: string[]) => Promise<number>;\n}\n\n/**\n * Identity helper that exists purely to give TypeScript a place to infer the\n * `Opts` generic from the literal passed in. Equivalent to writing the type\n * annotation manually.\n */\nexport const defineCommand = <Opts = Record<string, unknown>>(mod: CommandModule<Opts>): CommandModule<Opts> => mod;\n\n/**\n * Build a CLI from a list of `CommandModule` registrations. Auto-registers a\n * `doctor` subcommand when `checks` is non-empty, discovers workspace plugins\n * when `plugins.workspace` is configured, and wires up the ServerKit\n * integration when `modules` is supplied.\n */\nexport const createCliApp = async <ConfigT extends AppConfig = AppConfig>(options: CliAppOptions<ConfigT>): Promise<CliApp> => {\n const verbose = process.argv.includes('-v') || process.argv.includes('--verbose');\n const resolvedConfig = typeof options.config === 'function' ? await options.config() : options.config;\n const ctx = await buildContext({\n config: resolvedConfig,\n logger: options.logger,\n verbose,\n });\n\n if (options.modules && options.modules.length > 0) {\n const { configureServerKitModules } = (await import('./integrations/serverkit/index.js')) as {\n configureServerKitModules: (ctx: CliContext, modules: unknown[]) => void;\n };\n configureServerKitModules(ctx, options.modules);\n }\n\n const program = new Command()\n .name(options.name)\n .description(options.description)\n .version(options.version)\n .option('-v, --verbose', 'Enable verbose logging', false);\n\n const discovered: DiscoveredCommand[] = options.commands.map(c => ({ ...c, source: 'core' as const }));\n\n if (options.checks && options.checks.length > 0 && options.doctorCommandPath !== null) {\n const doctorPath = options.doctorCommandPath ?? ['doctor'];\n const alreadyDefined = discovered.some(c => c.path.join(' ') === doctorPath.join(' '));\n if (!alreadyDefined) {\n discovered.push({\n path: doctorPath,\n source: 'core',\n module: buildDoctorCommand(options.checks),\n });\n }\n }\n\n if (options.plugins?.workspace) {\n const workspaceOpts: WorkspacePluginOptions = {\n ...options.plugins.workspace,\n repoRoot: options.plugins.workspace.repoRoot ?? ctx.paths.repoRoot,\n };\n const plugins = await loadWorkspacePlugins(ctx, workspaceOpts);\n discovered.push(...plugins);\n }\n\n registerCommands(program, discovered, ctx);\n\n return {\n run: async (argv = process.argv) => {\n try {\n await program.parseAsync(argv);\n return 0;\n } catch (err) {\n ctx.logger.error((err as Error).message);\n return 1;\n }\n },\n };\n};\n","import * as clack from '@clack/prompts';\n\n/** Re-export of the `@clack/prompts` namespace under a stable name. */\nexport const prompts = clack;\n\n/** Thrown by `unwrap` when the user cancels a clack prompt (e.g. Ctrl+C). */\nexport class PromptCancelledError extends Error {\n constructor() {\n super('prompt cancelled');\n this.name = 'PromptCancelledError';\n }\n}\n\n/**\n * Unwrap a clack prompt result, throwing `PromptCancelledError` when the user\n * cancelled. Lets command handlers use try/catch instead of branching on\n * `isCancel` at every prompt.\n */\nexport const unwrap = <T>(value: T | symbol): T => {\n if (clack.isCancel(value)) throw new PromptCancelledError();\n return value as T;\n};\n","import type { CliContext, CommandModule, DangerousSpec, EnvironmentGuardSpec } from '../types.js';\nimport { prompts } from '../util/prompts.js';\n\nconst resolveEnvGuard = (mod: CommandModule): EnvironmentGuardSpec | null => {\n if (!mod.allowedEnvironments) return null;\n if (Array.isArray(mod.allowedEnvironments)) return { allowed: mod.allowedEnvironments };\n return mod.allowedEnvironments;\n};\n\nconst resolveDangerous = (mod: CommandModule): DangerousSpec | null => {\n if (!mod.dangerous) return null;\n if (mod.dangerous === true) return {};\n return mod.dangerous;\n};\n\nconst hasYesOption = (mod: CommandModule): boolean => (mod.options ?? []).some(o => /(^|[\\s,])(-y|--yes)([\\s,]|$)/.test(o.flags));\n\n/**\n * Returns true when the env guard is satisfied or absent. Logs and returns\n * false when the current environment is not in the allowed list — the caller\n * should treat that as a refusal and exit non-zero.\n */\nexport const checkEnvironmentGuard = (mod: CommandModule, ctx: CliContext, pathLabel: string): boolean => {\n const guard = resolveEnvGuard(mod);\n if (!guard) return true;\n const variable = guard.variable ?? 'NODE_ENV';\n const current = ctx.env[variable];\n if (current !== undefined && guard.allowed.includes(current)) return true;\n const shown = current === undefined ? '(unset)' : current;\n ctx.logger.error(`Refusing to run \"${pathLabel}\" with ${variable}=${shown}. Allowed: ${guard.allowed.join(', ')}.`);\n return false;\n};\n\n/**\n * Resolves a destructive-command confirmation. Returns true when the command\n * should proceed. In non-interactive contexts the caller must pass `--yes`\n * (reflected in `userOptedIn`); otherwise the user is prompted.\n */\nexport const confirmDangerous = async (mod: CommandModule, ctx: CliContext, pathLabel: string, userOptedIn: boolean): Promise<boolean> => {\n const spec = resolveDangerous(mod);\n if (!spec) return true;\n if (userOptedIn) return true;\n if (!ctx.isInteractive()) {\n ctx.logger.error(`\"${pathLabel}\" is destructive; pass --yes to confirm in non-interactive mode.`);\n return false;\n }\n if (spec.confirm === 'typed') {\n const phrase = spec.phrase ?? pathLabel;\n const result = await prompts.text({ message: spec.message ?? `This is destructive. Type \"${phrase}\" to continue:` });\n if (prompts.isCancel(result)) return false;\n if (result !== phrase) {\n ctx.logger.warn('Confirmation did not match — aborting.');\n return false;\n }\n return true;\n }\n const result = await prompts.confirm({ message: spec.message ?? `Run destructive command \"${pathLabel}\"?`, initialValue: false });\n if (prompts.isCancel(result)) return false;\n return result === true;\n};\n\n/** Whether the command needs an injected `-y, --yes` option to be registered. */\nexport const needsYesOption = (mod: CommandModule): boolean => Boolean(mod.dangerous) && !hasYesOption(mod);\n","import { Command } from 'commander';\nimport type { CliContext, CommandModule, DiscoveredCommand, OptionSpec } from '../types.js';\nimport { checkEnvironmentGuard, confirmDangerous, needsYesOption } from './safety.js';\n\nconst applyOption = (cmd: Command, spec: OptionSpec): void => {\n if (spec.required) {\n cmd.requiredOption(spec.flags, spec.description, spec.default as string | undefined);\n return;\n }\n if (spec.default !== undefined) {\n cmd.option(spec.flags, spec.description, spec.default as string | boolean);\n } else {\n cmd.option(spec.flags, spec.description);\n }\n};\n\nconst findOrCreateGroup = (parent: Command, name: string): Command => {\n const existing = parent.commands.find(c => c.name() === name);\n if (existing) return existing;\n return parent.command(name).description(`${name} commands`);\n};\n\n// Extract the long-name (or short-name) of a commander flags string and\n// convert kebab-case to camelCase, matching commander's own option key\n// derivation. e.g. `--org-name <name>` → 'orgName'.\nconst deriveOptionKey = (flags: string): string => {\n const tokens = flags.split(/[ ,]+/);\n const long = tokens.find(t => t.startsWith('--'));\n const target = long ?? tokens.find(t => t.startsWith('-'));\n if (!target) return flags;\n const stripped = target.replace(/^-+/, '');\n return stripped.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());\n};\n\nconst attachLeaf = (parent: Command, leafName: string, mod: CommandModule, ctx: CliContext, sourceLabel: string, fullPath: string[]): void => {\n const cmd = parent.command(leafName).description(mod.description);\n const pathLabel = fullPath.join(' ');\n\n for (const arg of mod.args ?? []) {\n const argName = arg.variadic ? `${arg.name}...` : arg.name;\n if (arg.required) cmd.argument(`<${argName}>`, arg.description);\n else cmd.argument(`[${argName}]`, arg.description);\n }\n\n for (const opt of mod.options ?? []) {\n applyOption(cmd, opt);\n }\n\n if (needsYesOption(mod)) cmd.option('-y, --yes', 'Skip confirmation prompt for this destructive command', false);\n\n if (mod.passthrough) cmd.allowUnknownOption(true).allowExcessArguments(true);\n\n cmd.action(async (...allArgs: unknown[]) => {\n // Commander passes positional args first, then the parsed options\n // object, then the Command instance. We slice off the last two.\n const commandInstance = allArgs[allArgs.length - 1] as Command;\n const opts = (allArgs[allArgs.length - 2] ?? {}) as Record<string, unknown>;\n const positional = allArgs.slice(0, allArgs.length - 2);\n\n const positionalStrings: string[] = positional.flatMap(p => (Array.isArray(p) ? p.map(String) : p == null ? [] : [String(p)]));\n const passthroughArgs = mod.passthrough ? commandInstance.args : positionalStrings;\n\n for (const optSpec of mod.options ?? []) {\n if (!optSpec.envVar) continue;\n const key = deriveOptionKey(optSpec.flags);\n if (opts[key] === undefined && process.env[optSpec.envVar] !== undefined) {\n opts[key] = process.env[optSpec.envVar];\n }\n }\n\n if (!checkEnvironmentGuard(mod, ctx, pathLabel)) {\n process.exit(1);\n return;\n }\n\n if (mod.dangerous) {\n const proceed = await confirmDangerous(mod, ctx, pathLabel, opts['yes'] === true);\n if (!proceed) {\n process.exit(1);\n return;\n }\n }\n\n let finalOpts = opts;\n if (mod.interactive && ctx.isInteractive()) {\n finalOpts = (await mod.interactive(ctx, opts)) as Record<string, unknown>;\n }\n\n try {\n const exitCode = await mod.run(finalOpts, ctx, passthroughArgs);\n if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);\n } catch (err) {\n ctx.logger.error(`[${sourceLabel}] ${(err as Error).message}`);\n if ((err as Error).stack) ctx.logger.debug((err as Error).stack ?? '');\n process.exit(1);\n }\n });\n};\n\n/**\n * Attach every discovered command to a commander `Program`, building intermediate\n * group nodes as needed. Core registrations are processed before plugin ones, so\n * a plugin that tries to claim a path already held by core throws with a\n * descriptive error.\n */\nexport const registerCommands = (program: Command, discovered: DiscoveredCommand[], ctx: CliContext): void => {\n const registeredPaths = new Map<string, { source: 'core' | 'plugin'; sourceName?: string }>();\n\n // Core first, then plugins — plugins can extend but not override.\n const sorted = [...discovered].sort((a, b) => {\n if (a.source === b.source) return 0;\n return a.source === 'core' ? -1 : 1;\n });\n\n for (const entry of sorted) {\n const key = entry.path.join(' ');\n const existing = registeredPaths.get(key);\n if (existing) {\n const incoming = entry.source === 'plugin' ? (entry.sourceName ?? 'unknown plugin') : 'core';\n const owner = existing.source === 'plugin' ? (existing.sourceName ?? 'unknown plugin') : 'core';\n throw new Error(`command \"${key}\" is already registered by ${owner}; ${incoming} cannot override it`);\n }\n registeredPaths.set(key, { source: entry.source, sourceName: entry.sourceName });\n\n let parent: Command = program;\n for (const segment of entry.path.slice(0, -1)) {\n parent = findOrCreateGroup(parent, segment);\n }\n const leafName = entry.path[entry.path.length - 1];\n if (!leafName) continue;\n const sourceLabel = entry.source === 'plugin' ? (entry.sourceName ?? 'plugin') : 'core';\n attachLeaf(parent, leafName, entry.module, ctx, sourceLabel, entry.path);\n }\n};\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { AppConfigBuilder, AppConfigProviderDotenv, type AppConfig } from '@maroonedsoftware/appconfig';\nimport type { CliContext, CliPaths } from './types.js';\nimport { createDaemons } from './util/daemons.js';\nimport type { CliLogger } from './util/logger.js';\nimport { createDefaultLogger } from './util/logger.js';\nimport { createShell } from './util/shell.js';\nimport { isInteractive } from './util/tty.js';\n\n/** Options accepted by `buildContext`. */\nexport interface BuildContextOptions {\n config?: AppConfig;\n logger?: CliLogger;\n verbose?: boolean;\n repoRoot?: string;\n /**\n * Paths to .env files (absolute, or relative to the resolved repoRoot) to\n * load into process.env before building AppConfig. Missing files are\n * silently skipped. Existing process.env values are not overridden.\n * Defaults to ['.env', 'apps/api/.env'].\n */\n envFiles?: string[];\n}\n\nconst findRepoRoot = (start: string): string => {\n let dir = start;\n for (let i = 0; i < 12; i++) {\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n};\n\n// Expands `$VAR` and `${VAR}` references against process.env. Matches the\n// behaviour of dotenv-expand so .env files authored for dbmate/docker-compose\n// (where placeholders are common) still produce usable runtime values.\nconst expandValue = (value: string): string =>\n value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}|\\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced: string | undefined, bare: string | undefined) => {\n const key = (braced ?? bare) as string;\n return process.env[key] ?? '';\n });\n\nconst loadEnvFile = (path: string): void => {\n if (!existsSync(path)) return;\n for (const line of readFileSync(path, 'utf-8').split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eqIdx = trimmed.indexOf('=');\n if (eqIdx === -1) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n const rawValue = trimmed.slice(eqIdx + 1).trim();\n\n // Detect quoting style before unwrapping. Single-quoted values are\n // taken literally; double-quoted and unquoted values get $VAR\n // expansion against the current process.env.\n const singleQuoted = rawValue.startsWith(\"'\") && rawValue.endsWith(\"'\");\n const doubleQuoted = rawValue.startsWith('\"') && rawValue.endsWith('\"');\n let value = singleQuoted || doubleQuoted ? rawValue.slice(1, -1) : rawValue;\n if (!singleQuoted) value = expandValue(value);\n\n if (!(key in process.env)) process.env[key] = value;\n }\n};\n\n/**\n * Build an AppConfig with only the dotenv provider attached. Callers are\n * expected to have loaded .env files into `process.env` beforehand — see\n * `buildContext` for the default loading sequence.\n */\nexport const buildDefaultAppConfig = async (): Promise<AppConfig> =>\n new AppConfigBuilder().addProvider(new AppConfigProviderDotenv()).build();\n\n/**\n * Build the `CliContext` handed to every command, check, and plugin hook. Loads\n * `.env` files into `process.env`, resolves the workspace `repoRoot`, and wires\n * up shell, logger, and config.\n */\nexport const buildContext = async (options: BuildContextOptions = {}): Promise<CliContext> => {\n // Start from cwd so consumers linked from a sibling repo (or installed\n // from npm into node_modules) still resolve to the CONSUMER's workspace\n // root rather than wherever johnny5 itself happens to live.\n const cwd = process.cwd();\n const repoRoot = options.repoRoot ?? findRepoRoot(cwd);\n const paths: CliPaths = { cwd, repoRoot };\n\n for (const envFile of options.envFiles ?? ['.env', 'apps/api/.env']) {\n const absolute = envFile.startsWith('/') ? envFile : resolve(repoRoot, envFile);\n loadEnvFile(absolute);\n }\n\n const logger = options.logger ?? createDefaultLogger({ verbose: options.verbose });\n const shell = createShell(repoRoot, logger);\n const daemons = createDaemons(repoRoot, shell, logger);\n const config = options.config ?? (await buildDefaultAppConfig());\n\n return {\n paths,\n logger,\n shell,\n daemons,\n config,\n env: process.env,\n isInteractive,\n };\n};\n","import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { CliLogger } from './logger.js';\nimport { johnnyPaths, projectSlug, type JohnnyPaths } from './paths.js';\nimport type { Shell } from './shell.js';\n\nconst APP_NAME = 'johnny5';\n\n/** Options for `Daemons.start`. The daemon name must be unique per project. */\nexport interface DaemonStartOptions {\n /** Identifier used for the pid/log filenames. Must match `/^[A-Za-z0-9._-]+$/`. */\n name: string;\n command: string;\n args: string[];\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n /**\n * When a daemon with this name is already running:\n * - `'reuse'` (default): leave the existing one alone and return its handle.\n * - `'restart'`: terminate it first, then start fresh.\n * - `'error'`: throw.\n */\n onExisting?: 'reuse' | 'restart' | 'error';\n}\n\n/** Snapshot of a registered daemon. `running` is checked at read time via `process.kill(pid, 0)`. */\nexport interface DaemonStatus {\n name: string;\n pid: number;\n running: boolean;\n /** Path to the append-only log file. May not exist yet if the daemon has produced no output. */\n logFile: string;\n /** Path to the on-disk pid record. */\n pidFile: string;\n /** Command line as recorded at start time. */\n command: string;\n args: string[];\n cwd: string;\n /** Wall-clock time the daemon was registered. */\n startedAt: Date;\n}\n\n/** Project-scoped manager for long-running detached processes. */\nexport interface Daemons {\n /** Start (or reuse) a daemon by name. Idempotent under `onExisting: 'reuse'`. */\n start: (options: DaemonStartOptions) => DaemonStatus;\n /** Send a signal to the daemon (default SIGTERM) and remove its pid file. Returns `true` if a process was signalled. */\n stop: (name: string, options?: { signal?: NodeJS.Signals }) => boolean;\n /** Read the recorded status for `name`, or `undefined` if no pid file exists. */\n status: (name: string) => DaemonStatus | undefined;\n /** List every daemon recorded for the current project. */\n list: () => DaemonStatus[];\n /** Absolute path to the daemon's log file (whether or not the daemon has been started). */\n logFile: (name: string) => string;\n /** Absolute path to the daemon's pid file. */\n pidFile: (name: string) => string;\n}\n\nconst NAME_PATTERN = /^[A-Za-z0-9._-]+$/;\n\ninterface PidRecord {\n pid: number;\n command: string;\n args: string[];\n cwd: string;\n startedAt: string;\n}\n\nconst isAlive = (pid: number): boolean => {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // ESRCH = no such process. EPERM = process exists but we lack permission to signal it (still alive).\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n};\n\nconst readPidRecord = (path: string): PidRecord | undefined => {\n if (!existsSync(path)) return undefined;\n try {\n const raw = readFileSync(path, 'utf-8');\n const parsed = JSON.parse(raw) as PidRecord;\n if (typeof parsed.pid !== 'number') return undefined;\n return parsed;\n } catch {\n return undefined;\n }\n};\n\nconst toStatus = (name: string, record: PidRecord, pidFile: string, logFile: string): DaemonStatus => ({\n name,\n pid: record.pid,\n running: isAlive(record.pid),\n logFile,\n pidFile,\n command: record.command,\n args: record.args,\n cwd: record.cwd,\n startedAt: new Date(record.startedAt),\n});\n\n/**\n * Build a `Daemons` manager scoped to the given project root. PID files live\n * under the OS runtime dir keyed by project slug; log files live under the OS\n * log dir keyed by the same slug. See `johnnyPaths` and `projectSlug` for\n * exact locations on each platform. Pass `paths` to redirect runtime/log dirs\n * (useful for tests that need an isolated filesystem location).\n */\nexport const createDaemons = (projectRoot: string, shell: Shell, logger: CliLogger, paths: JohnnyPaths = johnnyPaths(APP_NAME)): Daemons => {\n const slug = projectSlug(projectRoot);\n const pidDir = resolve(paths.runtime, slug);\n const logDir = resolve(paths.log, slug);\n\n const pidFile = (name: string): string => {\n if (!NAME_PATTERN.test(name)) {\n throw new Error(`Invalid daemon name '${name}'. Allowed characters: A-Z a-z 0-9 . _ -`);\n }\n return resolve(pidDir, `${name}.pid`);\n };\n const logFile = (name: string): string => {\n if (!NAME_PATTERN.test(name)) {\n throw new Error(`Invalid daemon name '${name}'. Allowed characters: A-Z a-z 0-9 . _ -`);\n }\n return resolve(logDir, `${name}.log`);\n };\n\n const status = (name: string): DaemonStatus | undefined => {\n const path = pidFile(name);\n const record = readPidRecord(path);\n if (!record) return undefined;\n return toStatus(name, record, path, logFile(name));\n };\n\n const stop = (name: string, options: { signal?: NodeJS.Signals } = {}): boolean => {\n const current = status(name);\n if (!current) return false;\n let signalled = false;\n if (current.running) {\n try {\n process.kill(current.pid, options.signal ?? 'SIGTERM');\n signalled = true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ESRCH') throw err;\n }\n }\n rmSync(current.pidFile, { force: true });\n logger.debug(`daemon '${name}' stopped (pid ${current.pid})`);\n return signalled;\n };\n\n const start = (options: DaemonStartOptions): DaemonStatus => {\n const existing = status(options.name);\n if (existing?.running) {\n const policy = options.onExisting ?? 'reuse';\n if (policy === 'reuse') return existing;\n if (policy === 'error') {\n throw new Error(`Daemon '${options.name}' is already running (pid ${existing.pid}).`);\n }\n stop(options.name);\n } else if (existing) {\n // Stale pid file from a crashed previous run.\n rmSync(existing.pidFile, { force: true });\n }\n\n mkdirSync(pidDir, { recursive: true });\n mkdirSync(logDir, { recursive: true });\n const path = pidFile(options.name);\n const log = logFile(options.name);\n const handle = shell.runDetached(options.command, options.args, {\n cwd: options.cwd,\n env: options.env,\n logFile: log,\n });\n const record: PidRecord = {\n pid: handle.pid,\n command: options.command,\n args: options.args,\n cwd: options.cwd ?? projectRoot,\n startedAt: new Date().toISOString(),\n };\n writeFileSync(path, JSON.stringify(record, null, 2));\n logger.debug(`daemon '${options.name}' started (pid ${handle.pid}, log ${log})`);\n return toStatus(options.name, record, path, log);\n };\n\n const list = (): DaemonStatus[] => {\n if (!existsSync(pidDir)) return [];\n const results: DaemonStatus[] = [];\n for (const entry of readdirSync(pidDir)) {\n if (!entry.endsWith('.pid')) continue;\n const name = entry.slice(0, -'.pid'.length);\n const snapshot = status(name);\n if (snapshot) results.push(snapshot);\n }\n return results;\n };\n\n return { start, stop, status, list, logFile, pidFile };\n};\n","import { createHash } from 'node:crypto';\nimport { homedir, tmpdir } from 'node:os';\nimport { basename, resolve } from 'node:path';\n\n/** Per-app filesystem locations following each OS's native conventions. */\nexport interface JohnnyPaths {\n /** Append-only daemon/process logs. macOS: `~/Library/Logs/<app>`; Linux: `$XDG_STATE_HOME/<app>`; Windows: `%LOCALAPPDATA%\\<app>\\Log`. */\n log: string;\n /** Runtime ephemera (pid files, sockets). macOS: `$TMPDIR/<app>`; Linux: `$XDG_RUNTIME_DIR/<app>` (falls back to `/tmp/<app>-<uid>`); Windows: `%LOCALAPPDATA%\\<app>\\Temp`. */\n runtime: string;\n /** Cross-invocation cache. macOS: `~/Library/Caches/<app>`; Linux: `$XDG_CACHE_HOME/<app>`; Windows: `%LOCALAPPDATA%\\<app>\\Cache`. */\n cache: string;\n}\n\nconst env = (key: string): string | undefined => {\n const value = process.env[key];\n return value && value.length > 0 ? value : undefined;\n};\n\n/**\n * Resolve OS-standard user-level filesystem locations for an app named `app`.\n * `app` should be a stable, lowercase, no-spaces identifier (e.g. `'johnny5'`).\n */\nexport const johnnyPaths = (app: string): JohnnyPaths => {\n const platform = process.platform;\n if (platform === 'darwin') {\n const home = homedir();\n return {\n log: resolve(home, 'Library/Logs', app),\n runtime: resolve(tmpdir(), app),\n cache: resolve(home, 'Library/Caches', app),\n };\n }\n if (platform === 'win32') {\n const base = env('LOCALAPPDATA') ?? resolve(homedir(), 'AppData/Local');\n return {\n log: resolve(base, app, 'Log'),\n runtime: resolve(base, app, 'Temp'),\n cache: resolve(base, app, 'Cache'),\n };\n }\n // POSIX / Linux: follow XDG Base Directory spec.\n const home = homedir();\n const state = env('XDG_STATE_HOME') ?? resolve(home, '.local/state');\n const cache = env('XDG_CACHE_HOME') ?? resolve(home, '.cache');\n // $XDG_RUNTIME_DIR is only set inside a user session. Fall back to a per-uid\n // /tmp directory so the same path resolves across reboots within a session.\n const runtimeBase = env('XDG_RUNTIME_DIR') ?? resolve(tmpdir(), `${app}-${process.getuid?.() ?? 0}`);\n const runtime = env('XDG_RUNTIME_DIR') ? resolve(runtimeBase, app) : runtimeBase;\n return {\n log: resolve(state, app),\n runtime,\n cache: resolve(cache, app),\n };\n};\n\n/**\n * Build a stable, human-readable, collision-free slug for a project root path.\n * Combines the directory basename with a short hash of the absolute path so\n * two checkouts of the same repo at different locations get distinct slugs\n * while remaining easy to identify in `ls` output.\n *\n * Example: `/Users/me/code/homegrown_v2` → `homegrown_v2-a3f1c9b2`.\n */\nexport const projectSlug = (projectRoot: string): string => {\n const absolute = resolve(projectRoot);\n const hash = createHash('sha256').update(absolute).digest('hex').slice(0, 8);\n const name = basename(absolute).replace(/[^A-Za-z0-9._-]/g, '_') || 'project';\n return `${name}-${hash}`;\n};\n","/** Minimal logger interface that every command and check receives via `CliContext`. */\nexport interface CliLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n debug: (msg: string) => void;\n success: (msg: string) => void;\n}\n\nconst colour = (code: number, text: string): string => `\\x1b[${code}m${text}\\x1b[0m`;\n\n/** Options accepted by `createDefaultLogger`. */\nexport interface CreateLoggerOptions {\n /** When true, `debug` writes to stdout; otherwise it's a no-op. */\n verbose?: boolean;\n}\n\n/**\n * Build the default ANSI-coloured console logger used when a consumer doesn't\n * supply their own. `debug` output is gated on `verbose`.\n */\nexport const createDefaultLogger = (options: CreateLoggerOptions = {}): CliLogger => ({\n info: msg => console.log(msg),\n warn: msg => console.warn(colour(33, `! ${msg}`)),\n error: msg => console.error(colour(31, `✗ ${msg}`)),\n success: msg => console.log(colour(32, `✓ ${msg}`)),\n debug: msg => {\n if (options.verbose) console.log(colour(90, `· ${msg}`));\n },\n});\n","import { spawn, type StdioOptions } from 'node:child_process';\nimport { mkdirSync, openSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { execa, type Options as ExecaOptions, type ResultPromise } from 'execa';\nimport type { CliLogger } from './logger.js';\n\n/** Execa options re-typed to require a string `cwd` at the call site. */\nexport interface ShellOptions extends ExecaOptions {\n cwd?: string;\n}\n\n/** Options accepted by `Shell.runDetached`. */\nexport interface RunDetachedOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n /**\n * Absolute path to a log file. Stdout and stderr are appended here.\n * The parent directory is created if missing. When omitted, stdio is ignored.\n */\n logFile?: string;\n}\n\n/** Handle returned by `Shell.runDetached` once the child is spawned and detached. */\nexport interface DetachedHandle {\n pid: number;\n logFile?: string;\n}\n\n/** Tiny shell wrapper around execa exposed on `CliContext.shell`. */\nexport interface Shell {\n /** Run a command, returning the execa result promise. Use this when the caller needs stdout/stderr. */\n run: (command: string, args: string[], options?: ShellOptions) => ResultPromise;\n /** Run a command with inherited stdio, returning the exit code. Failures don't throw — the exit code is returned instead. */\n runStreaming: (command: string, args: string[], options?: ShellOptions) => Promise<number>;\n /**\n * Spawn a command detached from the current process, returning its PID immediately.\n * The child is `unref()`-ed so the CLI can exit while the child keeps running.\n * When `logFile` is supplied, stdout/stderr are appended to it; otherwise stdio is ignored.\n */\n runDetached: (command: string, args: string[], options?: RunDetachedOptions) => DetachedHandle;\n}\n\n/** Build a `Shell` bound to `cwd`, logging streaming invocations through `logger.debug`. */\nexport const createShell = (cwd: string, logger: CliLogger): Shell => ({\n run: (command, args, options) => execa(command, args, { cwd, ...options }),\n runStreaming: async (command, args, options) => {\n logger.debug(`$ ${command} ${args.join(' ')}`);\n const child = execa(command, args, { cwd, stdio: 'inherit', reject: false, ...options });\n const result = await child;\n return result.exitCode ?? 0;\n },\n runDetached: (command, args, options = {}) => {\n const workingDir = options.cwd ?? cwd;\n let stdio: StdioOptions = 'ignore';\n if (options.logFile) {\n mkdirSync(dirname(options.logFile), { recursive: true });\n const fd = openSync(options.logFile, 'a');\n stdio = ['ignore', fd, fd];\n }\n logger.debug(`$ (detached) ${command} ${args.join(' ')}`);\n const child = spawn(command, args, {\n cwd: workingDir,\n env: options.env ?? process.env,\n detached: true,\n stdio,\n });\n if (child.pid === undefined) {\n throw new Error(`Failed to spawn detached process: ${command}`);\n }\n child.unref();\n return { pid: child.pid, logFile: options.logFile };\n },\n});\n","/**\n * Best-effort guess at whether the CLI is talking to a human. Returns false in\n * CI (`CI=true` / `CI=1`), when `JOHNNY5_NON_INTERACTIVE=1`, or when either of\n * stdout/stdin isn't a TTY.\n */\nexport const isInteractive = (): boolean => {\n if (process.env['CI'] === 'true' || process.env['CI'] === '1') return false;\n if (process.env['JOHNNY5_NON_INTERACTIVE'] === '1') return false;\n return Boolean(process.stdout.isTTY && process.stdin.isTTY);\n};\n","import type { Check, CheckResult, CliContext, CommandModule } from '../types.js';\n\n/** Options passed to `runChecks`. */\nexport interface DoctorOptions {\n /** When true, failing checks with an `autoFix` hook get a chance to remediate. */\n fix?: boolean;\n}\n\n/**\n * Run a list of doctor checks sequentially, rendering progress to stdout. Returns\n * a process exit code: `0` when every check passes (including via `autoFix` when\n * `--fix` is supplied), `1` when at least one check fails.\n */\nexport const runChecks = async (ctx: CliContext, checks: Check[], options: DoctorOptions): Promise<number> => {\n ctx.logger.info('Running doctor…\\n');\n\n let failed = 0;\n let fixed = 0;\n\n for (const check of checks) {\n process.stdout.write(` ${check.name.padEnd(36, ' ')} `);\n let result: CheckResult;\n try {\n result = await check.run(ctx);\n } catch (err) {\n result = { ok: false, message: `threw: ${(err as Error).message}` };\n }\n\n if (result.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${result.message}\\n`);\n continue;\n }\n\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${result.message}\\n`);\n\n if (options.fix && check.autoFix) {\n process.stdout.write(` ↻ attempting auto-fix… `);\n try {\n const fixResult = await check.autoFix(ctx);\n if (fixResult.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${fixResult.message}\\n`);\n fixed++;\n continue;\n }\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${fixResult.message}\\n`);\n } catch (err) {\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${(err as Error).message}\\n`);\n }\n } else if (result.fixHint) {\n process.stdout.write(` → ${result.fixHint}\\n`);\n }\n failed++;\n }\n\n process.stdout.write('\\n');\n if (failed === 0) {\n ctx.logger.success('All checks passed.');\n return 0;\n }\n if (options.fix && fixed > 0) {\n ctx.logger.info(`Auto-fixed ${fixed} issue(s); re-run \\`doctor\\` to confirm.`);\n }\n ctx.logger.error(`${failed} check(s) failed.`);\n return 1;\n};\n\n/**\n * Build the `CommandModule` for the built-in `doctor` subcommand from a set of\n * checks. `createCliApp` auto-registers this when `checks` is non-empty.\n */\nexport const buildDoctorCommand = (checks: Check[]): CommandModule<{ fix?: boolean }> => ({\n description: 'Run local-dev health checks',\n options: [{ flags: '--fix', description: 'Attempt auto-remediation for checks that support it' }],\n run: async (opts, ctx) => runChecks(ctx, checks, { fix: opts.fix === true }),\n});\n","import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { CliContext, DiscoveredCommand, PluginManifest } from '../types.js';\n\ninterface WorkspacePackageJson {\n name?: string;\n johnny5?: {\n commands?: string;\n };\n}\n\n/** Options accepted by `loadWorkspacePlugins`. */\nexport interface WorkspacePluginOptions {\n repoRoot: string;\n /**\n * Workspace-relative directories whose immediate children are scanned for\n * `package.json` files. Defaults to `['apps', 'packages']`.\n */\n roots?: string[];\n /**\n * Package names to skip — typically the consumer's own CLI package whose\n * commands are loaded directly, not via plugin discovery.\n */\n excludePackages?: string[];\n}\n\n/**\n * Scan every workspace package in the configured roots for a `\"johnny5\"` field\n * in `package.json`. When present, the referenced file is dynamically imported\n * and expected to default-export a `PluginManifest`. Failures to load a single\n * plugin log a warning through `ctx.logger.warn` but don't abort the CLI.\n */\nexport const loadWorkspacePlugins = async (ctx: CliContext, options: WorkspacePluginOptions): Promise<DiscoveredCommand[]> => {\n const rootDirs = options.roots ?? ['apps', 'packages'];\n const exclude = new Set(options.excludePackages ?? []);\n const discovered: DiscoveredCommand[] = [];\n\n for (const rootRel of rootDirs) {\n const root = resolve(options.repoRoot, rootRel);\n if (!existsSync(root)) continue;\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const pkgPath = join(root, entry.name, 'package.json');\n if (!existsSync(pkgPath)) continue;\n\n let pkg: WorkspacePackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as WorkspacePackageJson;\n } catch {\n continue;\n }\n\n const commandsRel = pkg.johnny5?.commands;\n if (!commandsRel) continue;\n if (pkg.name && exclude.has(pkg.name)) continue;\n\n const manifestPath = resolve(root, entry.name, commandsRel);\n if (!existsSync(manifestPath)) {\n ctx.logger.warn(`johnny5 plugin manifest missing for ${pkg.name ?? entry.name}: ${manifestPath}`);\n continue;\n }\n\n try {\n const mod = (await import(pathToFileURL(manifestPath).href)) as { default: PluginManifest };\n const manifest = mod.default;\n if (!manifest || !Array.isArray(manifest.commands)) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} has no commands array; skipping`);\n continue;\n }\n for (const cmd of manifest.commands) {\n discovered.push({\n path: cmd.path,\n source: 'plugin',\n sourceName: manifest.name ?? pkg.name,\n module: cmd.module,\n });\n }\n } catch (err) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} failed to load: ${(err as Error).message}`);\n }\n }\n }\n\n return discovered;\n};\n"],"mappings":"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","names":["InjectKitRegistry","AppConfig","ConsoleLogger","Logger","bootstrapForCli","STATE_KEY","getState","configureServerKitModules","getOrBootstrapContainer","requireContainer","options","registry","register","useInstance","logger","config","module","modules","setup","container","build","shutdown","reverse","Symbol","for","g","globalThis","containerByContext","WeakMap","ctx","set","lazy","get","Error","promise","handler","opts","args","scoped","createScopedContainer","enriched","Object","assign","Command","clack","prompts","clack","PromptCancelledError","Error","name","unwrap","value","isCancel","resolveEnvGuard","mod","allowedEnvironments","Array","isArray","allowed","resolveDangerous","dangerous","hasYesOption","options","some","o","test","flags","checkEnvironmentGuard","ctx","pathLabel","guard","variable","current","env","undefined","includes","shown","logger","error","join","confirmDangerous","userOptedIn","spec","isInteractive","confirm","phrase","result","prompts","text","message","isCancel","warn","initialValue","needsYesOption","Boolean","applyOption","cmd","spec","required","requiredOption","flags","description","default","undefined","option","findOrCreateGroup","parent","name","existing","commands","find","c","command","deriveOptionKey","tokens","split","long","t","startsWith","target","stripped","replace","_","toUpperCase","attachLeaf","leafName","mod","ctx","sourceLabel","fullPath","pathLabel","join","arg","args","argName","variadic","argument","opt","options","needsYesOption","passthrough","allowUnknownOption","allowExcessArguments","action","allArgs","commandInstance","length","opts","positional","slice","positionalStrings","flatMap","p","Array","isArray","map","String","passthroughArgs","optSpec","envVar","key","process","env","checkEnvironmentGuard","exit","dangerous","proceed","confirmDangerous","finalOpts","interactive","isInteractive","exitCode","run","err","logger","error","message","stack","debug","registerCommands","program","discovered","registeredPaths","Map","sorted","sort","a","b","source","entry","path","get","incoming","sourceName","owner","Error","set","segment","module","existsSync","readFileSync","dirname","resolve","AppConfigBuilder","AppConfigProviderDotenv","existsSync","mkdirSync","readdirSync","readFileSync","rmSync","writeFileSync","resolve","createHash","homedir","tmpdir","basename","resolve","env","key","value","process","length","undefined","johnnyPaths","app","platform","home","homedir","log","resolve","runtime","tmpdir","cache","base","state","runtimeBase","getuid","projectSlug","projectRoot","absolute","hash","createHash","update","digest","slice","name","basename","replace","APP_NAME","NAME_PATTERN","isAlive","pid","process","kill","err","code","readPidRecord","path","existsSync","undefined","raw","readFileSync","parsed","JSON","parse","toStatus","name","record","pidFile","logFile","running","command","args","cwd","startedAt","Date","createDaemons","projectRoot","shell","logger","paths","johnnyPaths","slug","projectSlug","pidDir","resolve","runtime","logDir","log","test","Error","status","stop","options","current","signalled","signal","rmSync","force","debug","start","existing","policy","onExisting","mkdirSync","recursive","handle","runDetached","env","toISOString","writeFileSync","stringify","list","results","entry","readdirSync","endsWith","slice","length","snapshot","push","colour","code","text","createDefaultLogger","options","info","msg","console","log","warn","error","success","debug","verbose","spawn","mkdirSync","openSync","dirname","execa","createShell","cwd","logger","run","command","args","options","execa","runStreaming","debug","join","child","stdio","reject","result","exitCode","runDetached","workingDir","logFile","mkdirSync","dirname","recursive","fd","openSync","spawn","env","process","detached","pid","undefined","Error","unref","isInteractive","process","env","Boolean","stdout","isTTY","stdin","findRepoRoot","start","dir","i","existsSync","resolve","parent","dirname","process","cwd","expandValue","value","replace","_","braced","bare","key","env","loadEnvFile","path","line","readFileSync","split","trimmed","trim","startsWith","eqIdx","indexOf","slice","rawValue","singleQuoted","endsWith","doubleQuoted","buildDefaultAppConfig","AppConfigBuilder","addProvider","AppConfigProviderDotenv","build","buildContext","options","repoRoot","paths","envFile","envFiles","absolute","logger","createDefaultLogger","verbose","shell","createShell","daemons","createDaemons","config","isInteractive","runChecks","ctx","checks","options","logger","info","failed","fixed","check","process","stdout","write","name","padEnd","result","run","err","ok","message","fix","autoFix","fixResult","fixHint","success","error","buildDoctorCommand","description","flags","opts","existsSync","readFileSync","readdirSync","join","resolve","pathToFileURL","loadWorkspacePlugins","ctx","options","rootDirs","roots","exclude","Set","excludePackages","discovered","rootRel","root","resolve","repoRoot","existsSync","entry","readdirSync","withFileTypes","isDirectory","pkgPath","join","name","pkg","JSON","parse","readFileSync","commandsRel","johnny5","commands","has","manifestPath","logger","warn","mod","pathToFileURL","href","manifest","default","Array","isArray","cmd","push","path","source","sourceName","module","err","message","defineCommand","mod","createCliApp","options","verbose","process","argv","includes","resolvedConfig","config","ctx","buildContext","logger","modules","length","configureServerKitModules","program","Command","name","description","version","option","discovered","commands","map","c","source","checks","doctorCommandPath","doctorPath","alreadyDefined","some","path","join","push","module","buildDoctorCommand","plugins","workspace","workspaceOpts","repoRoot","paths","loadWorkspacePlugins","registerCommands","run","parseAsync","err","error","message"]}
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-DBGyauec.js';
1
+ import { b as Check } from '../../types-DH7gcIP5.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-DBGyauec.js';
1
+ import { b as Check } from '../../types-DH7gcIP5.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { Kysely } from 'kysely';
2
- import { b as Check } from '../../types-DBGyauec.js';
2
+ import { b as Check } from '../../types-DH7gcIP5.js';
3
3
  import '@maroonedsoftware/appconfig';
4
4
  import 'execa';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { AuthorizationModel } from '@maroonedsoftware/permissions';
2
- import { C as CliContext, b as Check } from '../../types-DBGyauec.js';
2
+ import { C as CliContext, b as Check } from '../../types-DH7gcIP5.js';
3
3
  import '@maroonedsoftware/appconfig';
4
4
  import 'execa';
5
5
 
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-DBGyauec.js';
1
+ import { b as Check } from '../../types-DH7gcIP5.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-DBGyauec.js';
1
+ import { b as Check } from '../../types-DH7gcIP5.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -2,7 +2,7 @@ import { Container, ScopedContainer } from 'injectkit';
2
2
  import { AppConfig } from '@maroonedsoftware/appconfig';
3
3
  import { Logger } from '@maroonedsoftware/logger';
4
4
  import { ServerKitModule } from '@maroonedsoftware/koa';
5
- import { C as CliContext, d as CommandModule } from '../../types-DBGyauec.js';
5
+ import { C as CliContext, d as CommandModule } from '../../types-DH7gcIP5.js';
6
6
  import 'execa';
7
7
 
8
8
  /** Options accepted by `bootstrapForCli`. */
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-DBGyauec.js';
1
+ import { b as Check } from '../../types-DH7gcIP5.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -20,20 +20,123 @@ interface CreateLoggerOptions {
20
20
  */
21
21
  declare const createDefaultLogger: (options?: CreateLoggerOptions) => CliLogger;
22
22
 
23
+ /** Per-app filesystem locations following each OS's native conventions. */
24
+ interface JohnnyPaths {
25
+ /** Append-only daemon/process logs. macOS: `~/Library/Logs/<app>`; Linux: `$XDG_STATE_HOME/<app>`; Windows: `%LOCALAPPDATA%\<app>\Log`. */
26
+ log: string;
27
+ /** Runtime ephemera (pid files, sockets). macOS: `$TMPDIR/<app>`; Linux: `$XDG_RUNTIME_DIR/<app>` (falls back to `/tmp/<app>-<uid>`); Windows: `%LOCALAPPDATA%\<app>\Temp`. */
28
+ runtime: string;
29
+ /** Cross-invocation cache. macOS: `~/Library/Caches/<app>`; Linux: `$XDG_CACHE_HOME/<app>`; Windows: `%LOCALAPPDATA%\<app>\Cache`. */
30
+ cache: string;
31
+ }
32
+ /**
33
+ * Resolve OS-standard user-level filesystem locations for an app named `app`.
34
+ * `app` should be a stable, lowercase, no-spaces identifier (e.g. `'johnny5'`).
35
+ */
36
+ declare const johnnyPaths: (app: string) => JohnnyPaths;
37
+ /**
38
+ * Build a stable, human-readable, collision-free slug for a project root path.
39
+ * Combines the directory basename with a short hash of the absolute path so
40
+ * two checkouts of the same repo at different locations get distinct slugs
41
+ * while remaining easy to identify in `ls` output.
42
+ *
43
+ * Example: `/Users/me/code/homegrown_v2` → `homegrown_v2-a3f1c9b2`.
44
+ */
45
+ declare const projectSlug: (projectRoot: string) => string;
46
+
23
47
  /** Execa options re-typed to require a string `cwd` at the call site. */
24
48
  interface ShellOptions extends Options {
25
49
  cwd?: string;
26
50
  }
51
+ /** Options accepted by `Shell.runDetached`. */
52
+ interface RunDetachedOptions {
53
+ cwd?: string;
54
+ env?: NodeJS.ProcessEnv;
55
+ /**
56
+ * Absolute path to a log file. Stdout and stderr are appended here.
57
+ * The parent directory is created if missing. When omitted, stdio is ignored.
58
+ */
59
+ logFile?: string;
60
+ }
61
+ /** Handle returned by `Shell.runDetached` once the child is spawned and detached. */
62
+ interface DetachedHandle {
63
+ pid: number;
64
+ logFile?: string;
65
+ }
27
66
  /** Tiny shell wrapper around execa exposed on `CliContext.shell`. */
28
67
  interface Shell {
29
68
  /** Run a command, returning the execa result promise. Use this when the caller needs stdout/stderr. */
30
69
  run: (command: string, args: string[], options?: ShellOptions) => ResultPromise;
31
70
  /** Run a command with inherited stdio, returning the exit code. Failures don't throw — the exit code is returned instead. */
32
71
  runStreaming: (command: string, args: string[], options?: ShellOptions) => Promise<number>;
72
+ /**
73
+ * Spawn a command detached from the current process, returning its PID immediately.
74
+ * The child is `unref()`-ed so the CLI can exit while the child keeps running.
75
+ * When `logFile` is supplied, stdout/stderr are appended to it; otherwise stdio is ignored.
76
+ */
77
+ runDetached: (command: string, args: string[], options?: RunDetachedOptions) => DetachedHandle;
33
78
  }
34
79
  /** Build a `Shell` bound to `cwd`, logging streaming invocations through `logger.debug`. */
35
80
  declare const createShell: (cwd: string, logger: CliLogger) => Shell;
36
81
 
82
+ /** Options for `Daemons.start`. The daemon name must be unique per project. */
83
+ interface DaemonStartOptions {
84
+ /** Identifier used for the pid/log filenames. Must match `/^[A-Za-z0-9._-]+$/`. */
85
+ name: string;
86
+ command: string;
87
+ args: string[];
88
+ cwd?: string;
89
+ env?: NodeJS.ProcessEnv;
90
+ /**
91
+ * When a daemon with this name is already running:
92
+ * - `'reuse'` (default): leave the existing one alone and return its handle.
93
+ * - `'restart'`: terminate it first, then start fresh.
94
+ * - `'error'`: throw.
95
+ */
96
+ onExisting?: 'reuse' | 'restart' | 'error';
97
+ }
98
+ /** Snapshot of a registered daemon. `running` is checked at read time via `process.kill(pid, 0)`. */
99
+ interface DaemonStatus {
100
+ name: string;
101
+ pid: number;
102
+ running: boolean;
103
+ /** Path to the append-only log file. May not exist yet if the daemon has produced no output. */
104
+ logFile: string;
105
+ /** Path to the on-disk pid record. */
106
+ pidFile: string;
107
+ /** Command line as recorded at start time. */
108
+ command: string;
109
+ args: string[];
110
+ cwd: string;
111
+ /** Wall-clock time the daemon was registered. */
112
+ startedAt: Date;
113
+ }
114
+ /** Project-scoped manager for long-running detached processes. */
115
+ interface Daemons {
116
+ /** Start (or reuse) a daemon by name. Idempotent under `onExisting: 'reuse'`. */
117
+ start: (options: DaemonStartOptions) => DaemonStatus;
118
+ /** Send a signal to the daemon (default SIGTERM) and remove its pid file. Returns `true` if a process was signalled. */
119
+ stop: (name: string, options?: {
120
+ signal?: NodeJS.Signals;
121
+ }) => boolean;
122
+ /** Read the recorded status for `name`, or `undefined` if no pid file exists. */
123
+ status: (name: string) => DaemonStatus | undefined;
124
+ /** List every daemon recorded for the current project. */
125
+ list: () => DaemonStatus[];
126
+ /** Absolute path to the daemon's log file (whether or not the daemon has been started). */
127
+ logFile: (name: string) => string;
128
+ /** Absolute path to the daemon's pid file. */
129
+ pidFile: (name: string) => string;
130
+ }
131
+ /**
132
+ * Build a `Daemons` manager scoped to the given project root. PID files live
133
+ * under the OS runtime dir keyed by project slug; log files live under the OS
134
+ * log dir keyed by the same slug. See `johnnyPaths` and `projectSlug` for
135
+ * exact locations on each platform. Pass `paths` to redirect runtime/log dirs
136
+ * (useful for tests that need an isolated filesystem location).
137
+ */
138
+ declare const createDaemons: (projectRoot: string, shell: Shell, logger: CliLogger, paths?: JohnnyPaths) => Daemons;
139
+
37
140
  /** Value-coercion hint for an option declared on a `CommandModule`. */
38
141
  type OptionType = 'string' | 'number' | 'boolean';
39
142
  /** Declarative description of a single CLI option (flag). */
@@ -139,9 +242,11 @@ interface CliContext {
139
242
  paths: CliPaths;
140
243
  logger: CliLogger;
141
244
  shell: Shell;
245
+ /** Project-scoped manager for long-running detached processes (storybook, dev servers, …). */
246
+ daemons: Daemons;
142
247
  config: AppConfig;
143
248
  isInteractive: () => boolean;
144
249
  env: NodeJS.ProcessEnv;
145
250
  }
146
251
 
147
- export { type ArgSpec as A, type CliContext as C, type DiscoveredCommand as D, type EnvironmentGuardSpec as E, type OptionSpec as O, type PluginManifest as P, type Shell as S, type CommandRegistration as a, type Check as b, type CliLogger as c, type CommandModule as d, type CheckResult as e, type CliPaths as f, type CreateLoggerOptions as g, type DangerousSpec as h, type OptionType as i, type ShellOptions as j, createDefaultLogger as k, createShell as l };
252
+ export { type ArgSpec as A, type CliContext as C, type DiscoveredCommand as D, type EnvironmentGuardSpec as E, type JohnnyPaths as J, type OptionSpec as O, type PluginManifest as P, type RunDetachedOptions as R, type Shell as S, type CommandRegistration as a, type Check as b, type CliLogger as c, type CommandModule as d, type CheckResult as e, type CliPaths as f, type CreateLoggerOptions as g, type DaemonStartOptions as h, type DaemonStatus as i, type Daemons as j, type DangerousSpec as k, type DetachedHandle as l, type OptionType as m, type ShellOptions as n, createDaemons as o, createDefaultLogger as p, createShell as q, johnnyPaths as r, projectSlug as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedsoftware/johnny5",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "CLI framework for ServerKit-based applications — plugin registration, doctor runner, and opt-in Postgres/Redis/Docker integrations",
5
5
  "author": {
6
6
  "name": "Marooned Software",
@@ -75,16 +75,16 @@
75
75
  "commander": "^14.0.3",
76
76
  "execa": "^9.6.0",
77
77
  "injectkit": "^1.2.0",
78
- "@maroonedsoftware/appconfig": "1.5.0",
79
- "@maroonedsoftware/logger": "1.1.0"
78
+ "@maroonedsoftware/logger": "1.1.0",
79
+ "@maroonedsoftware/appconfig": "1.5.0"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "ioredis": "^5.10.0",
83
83
  "kysely": "^0.28.0",
84
84
  "pg": "^8.20.0",
85
85
  "@maroonedsoftware/koa": "2.2.4",
86
- "@maroonedsoftware/permissions-dsl": "0.4.0",
87
- "@maroonedsoftware/permissions": "0.2.0"
86
+ "@maroonedsoftware/permissions": "0.2.0",
87
+ "@maroonedsoftware/permissions-dsl": "0.4.0"
88
88
  },
89
89
  "peerDependenciesMeta": {
90
90
  "@maroonedsoftware/koa": {
@@ -113,11 +113,11 @@
113
113
  "pg": "^8.20.0",
114
114
  "tsup": "^8.5.1",
115
115
  "vitest": "^4.1.5",
116
+ "@maroonedsoftware/permissions-dsl": "0.4.0",
116
117
  "@maroonedsoftware/koa": "2.2.4",
117
- "@maroonedsoftware/permissions": "0.2.0",
118
118
  "@repo/config-eslint": "0.2.1",
119
119
  "@repo/config-typescript": "0.1.0",
120
- "@maroonedsoftware/permissions-dsl": "0.4.0"
120
+ "@maroonedsoftware/permissions": "0.2.0"
121
121
  },
122
122
  "scripts": {
123
123
  "build": "tsup",