@maroonedsoftware/johnny5 1.1.2 → 1.2.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
@@ -320,11 +320,86 @@ const create = defineCommand({
320
320
 
321
321
  `interactive` only runs when both stdin and stdout are TTYs, so the same command works unchanged in CI.
322
322
 
323
+ ## Wizard flows
324
+
325
+ Multi-step interactive flows pile up `clack.isCancel` / `clack.outro('aborted')` boilerplate at every prompt. `wizard` collapses that into a single try/catch: prompt methods on the session throw `PromptCancelledError` on cancellation, and the wrapper catches it, prints a uniform outro, and returns an exit code.
326
+
327
+ ```ts
328
+ import { wizard } from '@maroonedsoftware/johnny5';
329
+
330
+ const setup = defineCommand({
331
+ description: 'first-run wizard',
332
+ run: async (_opts, ctx) =>
333
+ wizard(ctx, { title: 'my-cli — local dev bootstrap' }, async w => {
334
+ if (await w.confirm({ message: 'Start docker compose services?', initialValue: true })) {
335
+ const exit = await ctx.shell.runStreaming('docker', ['compose', 'up', '-d'], { cwd: ctx.paths.repoRoot });
336
+ if (exit !== 0) {
337
+ w.log.error(`docker compose up exited ${exit}`);
338
+ return exit;
339
+ }
340
+ }
341
+ const email = await w.text({ message: 'Test user email' });
342
+ const password = await w.password({ message: 'Test user password' });
343
+ await ctx.shell.runStreaming('pnpm', ['seed', `--email=${email}`, `--password=${password}`]);
344
+ }),
345
+ });
346
+ ```
347
+
348
+ - The body is linear — no `isCancel` checks anywhere. Cancelling any prompt aborts the whole flow with the configured outro and exit code (defaults: `'aborted'` and `1`).
349
+ - Return a number from the body to override the success exit code; return `void` for `0`.
350
+ - `w.cancel()` aborts from inside the body the same as a user cancel. Non-cancel errors propagate unchanged.
351
+ - `w.outro('all set — run pnpm doctor')` lets the body customize the success outro dynamically. Static overrides go on `WizardOptions` (`successOutro` / `cancelOutro` / `cancelExitCode`).
352
+ - `w.log` and `w.spinner` are pass-throughs to the matching `clack` helpers, so existing logging stays consistent inside the wizard.
353
+
354
+ `wizard` only assumes interactivity to the extent that `clack` does. Gate the call with `if (!ctx.isInteractive()) return 1` (or surface a clearer error) when running non-interactively makes no sense for the command.
355
+
356
+ ## Optional: system keyring
357
+
358
+ `@maroonedsoftware/johnny5/keyring` wraps the OS keyring (Keychain on macOS, Credential Manager on Windows, libsecret on Linux) via [@napi-rs/keyring](https://github.com/napi-rs/node-keyring). The native module is an **optional peer dependency** — install it only in CLIs that need credential storage:
359
+
360
+ ```bash
361
+ pnpm add @napi-rs/keyring
362
+ ```
363
+
364
+ `keyringEntry` builds a single safe slot. Every operation returns `null` / `false` instead of throwing — failures (missing peer dep, locked keychain, OS denial) are surfaced through `ctx.logger.warn` exactly once.
365
+
366
+ ```ts
367
+ import { keyringEntry, resolveSecret } from '@maroonedsoftware/johnny5/keyring';
368
+
369
+ const apiKeyEntry = keyringEntry(ctx, { service: 'my-cli', account: 'anthropic.api.key' });
370
+
371
+ await apiKeyEntry.write('sk-…'); // true on success, false otherwise
372
+ const stored = await apiKeyEntry.read(); // string | null
373
+ await apiKeyEntry.delete(); // true if a value was removed
374
+ ```
375
+
376
+ `resolveSecret` codifies the override → env → keyring → prompt chain that most CLIs end up writing by hand:
377
+
378
+ ```ts
379
+ const apiKey = await resolveSecret(ctx, {
380
+ override: opts.apiKey, // wins over everything; never persisted
381
+ envKeys: ['ANTHROPIC_API_KEY'], // first non-empty wins
382
+ keyring: apiKeyEntry,
383
+ prompt: async () => unwrap(await prompts.password({ message: 'Paste your API key' })),
384
+ promptStore: 'ask', // 'ask' (default) | 'always' | 'never'
385
+ label: 'API key',
386
+ });
387
+
388
+ if (!apiKey) {
389
+ ctx.logger.error('No credentials found. Set ANTHROPIC_API_KEY or run `my-cli login`.');
390
+ return 1;
391
+ }
392
+ ```
393
+
394
+ - The resolution chain is fixed: `override` first, then `envKeys` in order (both `ctx.env` and `process.env` are checked), then `keyring.read()`, then `prompt(ctx)`.
395
+ - A freshly-prompted value is written back to `keyring` according to `promptStore`. `'ask'` runs a `clack.confirm` (defaulting to yes); `'always'` and `'never'` skip the question. The `label` shows up in the confirm message.
396
+ - `resolveSecret` returns `null` when every source yields nothing and never calls `process.exit` — callers decide whether missing credentials are a hard error.
397
+
323
398
  ## Exports
324
399
 
325
400
  | Path | Provides |
326
401
  | --- | --- |
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. |
402
+ | `@maroonedsoftware/johnny5` | `createCliApp`, `defineCommand`, `registerCommands`, `runChecks`, `buildContext`, `buildDefaultAppConfig`, `loadWorkspacePlugins`, `createShell`, `createDaemons`, `johnnyPaths`, `projectSlug`, `createDefaultLogger`, `prompts`, `unwrap`, `wizard`, `isInteractive`, plus the `Check` / `CommandModule` / `CliContext` / `Daemons` / `WizardSession` types. |
328
403
  | `/serverkit` | `bootstrapForCli`, `configureServerKitModules`, `getOrBootstrapContainer`, `requireContainer`. |
329
404
  | `/versions` | `nodeVersion`, `pnpmVersion`. |
330
405
  | `/filesystem` | `envFile`, `portsFree`. |
@@ -333,6 +408,7 @@ const create = defineCommand({
333
408
  | `/docker` | `dockerServicesUp`. |
334
409
  | `/kysely` | `kyselyTableExists` (lazy-loads `kysely`). |
335
410
  | `/permissions` | `permissionsSchemaCompiled`, `permissionsFixturesPass`, `permissionsModelLoads` (lazy-load `@maroonedsoftware/permissions[-dsl]`). |
411
+ | `/keyring` | `keyringEntry`, `resolveSecret` (lazy-load `@napi-rs/keyring`). |
336
412
 
337
413
  ## License
338
414
 
package/dist/index.d.ts CHANGED
@@ -143,4 +143,47 @@ declare class PromptCancelledError extends Error {
143
143
  */
144
144
  declare const unwrap: <T>(value: T | symbol) => T;
145
145
 
146
- export { type BuildContextOptions, Check, type CliApp, type CliAppOptions, CliContext, CliLogger, CommandModule, CommandRegistration, DiscoveredCommand, type DoctorOptions, PromptCancelledError, type WorkspacePluginOptions, buildContext, buildDefaultAppConfig, buildDoctorCommand, createCliApp, defineCommand, isInteractive, loadWorkspacePlugins, prompts, registerCommands, runChecks, unwrap };
146
+ /** Options controlling a `wizard` session's framing and exit behavior. */
147
+ interface WizardOptions {
148
+ /** Title shown via `clack.intro` at the start of the session. */
149
+ title: string;
150
+ /** Outro printed when the body resolves successfully. Defaults to `'done'`. */
151
+ successOutro?: string;
152
+ /** Outro printed when the user cancels a prompt or `w.cancel()` is called. Defaults to `'aborted'`. */
153
+ cancelOutro?: string;
154
+ /** Exit code returned on cancel. Defaults to `1`. */
155
+ cancelExitCode?: number;
156
+ }
157
+ /**
158
+ * Session passed to the body of a `wizard` call. Prompt methods internally
159
+ * `unwrap` cancellation, so the body can be written linearly without
160
+ * `isCancel` checks at every step.
161
+ */
162
+ interface WizardSession {
163
+ readonly ctx: CliContext;
164
+ confirm: (options: Parameters<typeof prompts.confirm>[0]) => Promise<boolean>;
165
+ text: (options: Parameters<typeof prompts.text>[0]) => Promise<string>;
166
+ password: (options: Parameters<typeof prompts.password>[0]) => Promise<string>;
167
+ select: <T>(options: Parameters<typeof prompts.select<T>>[0]) => Promise<T>;
168
+ multiselect: <T>(options: Parameters<typeof prompts.multiselect<T>>[0]) => Promise<T[]>;
169
+ /** Pass-through to `clack.log.*` (success/info/warn/error/step/message). */
170
+ log: typeof prompts.log;
171
+ /** Pass-through to `clack.spinner()` for long-running steps inside the session. */
172
+ spinner: typeof prompts.spinner;
173
+ /** Override the outro printed on success; takes effect when the body resolves. */
174
+ outro: (message: string) => void;
175
+ /** Cancel the wizard from inside the body. Throws `PromptCancelledError`. */
176
+ cancel: () => never;
177
+ }
178
+ /**
179
+ * Run a guided multi-step flow with uniform intro/outro framing and cancel
180
+ * handling. The `body` receives a `WizardSession` whose prompt methods throw
181
+ * `PromptCancelledError` on cancellation; `wizard` catches that and prints
182
+ * the cancel outro, returning the configured exit code.
183
+ *
184
+ * The body's resolved value (or `0` when it returns `void`) is returned as
185
+ * the exit code on success. Any other thrown error propagates unchanged.
186
+ */
187
+ declare const wizard: (ctx: CliContext, options: WizardOptions, body: (w: WizardSession) => Promise<number | void>) => Promise<number>;
188
+
189
+ export { type BuildContextOptions, Check, type CliApp, type CliAppOptions, CliContext, CliLogger, CommandModule, CommandRegistration, DiscoveredCommand, type DoctorOptions, PromptCancelledError, type WizardOptions, type WizardSession, type WorkspacePluginOptions, buildContext, buildDefaultAppConfig, buildDoctorCommand, createCliApp, defineCommand, isInteractive, loadWorkspacePlugins, prompts, registerCommands, runChecks, unwrap, wizard };
package/dist/index.js CHANGED
@@ -768,6 +768,42 @@ var createCliApp = /* @__PURE__ */ __name(async (options) => {
768
768
  }, "run")
769
769
  };
770
770
  }, "createCliApp");
771
+
772
+ // src/util/wizard.ts
773
+ var wizard = /* @__PURE__ */ __name(async (ctx, options, body) => {
774
+ const successOutro = options.successOutro ?? "done";
775
+ const cancelOutro = options.cancelOutro ?? "aborted";
776
+ const cancelExitCode = options.cancelExitCode ?? 1;
777
+ let dynamicOutro;
778
+ const session = {
779
+ ctx,
780
+ confirm: /* @__PURE__ */ __name(async (opts) => unwrap(await prompts.confirm(opts)), "confirm"),
781
+ text: /* @__PURE__ */ __name(async (opts) => unwrap(await prompts.text(opts)), "text"),
782
+ password: /* @__PURE__ */ __name(async (opts) => unwrap(await prompts.password(opts)), "password"),
783
+ select: /* @__PURE__ */ __name(async (opts) => unwrap(await prompts.select(opts)), "select"),
784
+ multiselect: /* @__PURE__ */ __name(async (opts) => unwrap(await prompts.multiselect(opts)), "multiselect"),
785
+ log: prompts.log,
786
+ spinner: prompts.spinner,
787
+ outro: /* @__PURE__ */ __name((message) => {
788
+ dynamicOutro = message;
789
+ }, "outro"),
790
+ cancel: /* @__PURE__ */ __name(() => {
791
+ throw new PromptCancelledError();
792
+ }, "cancel")
793
+ };
794
+ prompts.intro(options.title);
795
+ try {
796
+ const result = await body(session);
797
+ prompts.outro(dynamicOutro ?? successOutro);
798
+ return typeof result === "number" ? result : 0;
799
+ } catch (err) {
800
+ if (err instanceof PromptCancelledError) {
801
+ prompts.outro(cancelOutro);
802
+ return cancelExitCode;
803
+ }
804
+ throw err;
805
+ }
806
+ }, "wizard");
771
807
  export {
772
808
  PromptCancelledError,
773
809
  buildContext,
@@ -785,6 +821,7 @@ export {
785
821
  prompts,
786
822
  registerCommands,
787
823
  runChecks,
788
- unwrap
824
+ unwrap,
825
+ wizard
789
826
  };
790
827
  //# sourceMappingURL=index.js.map
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/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
+ {"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","../src/util/wizard.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","import type { CliContext } from '../types.js';\nimport { PromptCancelledError, prompts, unwrap } from './prompts.js';\n\n/** Options controlling a `wizard` session's framing and exit behavior. */\nexport interface WizardOptions {\n /** Title shown via `clack.intro` at the start of the session. */\n title: string;\n /** Outro printed when the body resolves successfully. Defaults to `'done'`. */\n successOutro?: string;\n /** Outro printed when the user cancels a prompt or `w.cancel()` is called. Defaults to `'aborted'`. */\n cancelOutro?: string;\n /** Exit code returned on cancel. Defaults to `1`. */\n cancelExitCode?: number;\n}\n\n/**\n * Session passed to the body of a `wizard` call. Prompt methods internally\n * `unwrap` cancellation, so the body can be written linearly without\n * `isCancel` checks at every step.\n */\nexport interface WizardSession {\n readonly ctx: CliContext;\n confirm: (options: Parameters<typeof prompts.confirm>[0]) => Promise<boolean>;\n text: (options: Parameters<typeof prompts.text>[0]) => Promise<string>;\n password: (options: Parameters<typeof prompts.password>[0]) => Promise<string>;\n select: <T>(options: Parameters<typeof prompts.select<T>>[0]) => Promise<T>;\n multiselect: <T>(options: Parameters<typeof prompts.multiselect<T>>[0]) => Promise<T[]>;\n /** Pass-through to `clack.log.*` (success/info/warn/error/step/message). */\n log: typeof prompts.log;\n /** Pass-through to `clack.spinner()` for long-running steps inside the session. */\n spinner: typeof prompts.spinner;\n /** Override the outro printed on success; takes effect when the body resolves. */\n outro: (message: string) => void;\n /** Cancel the wizard from inside the body. Throws `PromptCancelledError`. */\n cancel: () => never;\n}\n\n/**\n * Run a guided multi-step flow with uniform intro/outro framing and cancel\n * handling. The `body` receives a `WizardSession` whose prompt methods throw\n * `PromptCancelledError` on cancellation; `wizard` catches that and prints\n * the cancel outro, returning the configured exit code.\n *\n * The body's resolved value (or `0` when it returns `void`) is returned as\n * the exit code on success. Any other thrown error propagates unchanged.\n */\nexport const wizard = async (\n ctx: CliContext,\n options: WizardOptions,\n body: (w: WizardSession) => Promise<number | void>,\n): Promise<number> => {\n const successOutro = options.successOutro ?? 'done';\n const cancelOutro = options.cancelOutro ?? 'aborted';\n const cancelExitCode = options.cancelExitCode ?? 1;\n\n let dynamicOutro: string | undefined;\n\n const session: WizardSession = {\n ctx,\n confirm: async opts => unwrap(await prompts.confirm(opts)),\n text: async opts => unwrap(await prompts.text(opts)),\n password: async opts => unwrap(await prompts.password(opts)),\n select: async opts => unwrap(await prompts.select(opts)),\n multiselect: async opts => unwrap(await prompts.multiselect(opts)),\n log: prompts.log,\n spinner: prompts.spinner,\n outro: message => {\n dynamicOutro = message;\n },\n cancel: () => {\n throw new PromptCancelledError();\n },\n };\n\n prompts.intro(options.title);\n\n try {\n const result = await body(session);\n prompts.outro(dynamicOutro ?? successOutro);\n return typeof result === 'number' ? result : 0;\n } catch (err) {\n if (err instanceof PromptCancelledError) {\n prompts.outro(cancelOutro);\n return cancelExitCode;\n }\n throw err;\n }\n};\n"],"mappings":";;;;;;;;;;;;AAAA;;;;;;;SAASA,yBAA+D;AACxE,SAASC,iBAAiB;AAC1B,SAASC,eAAeC,cAAc;AAFtC,IAyBaC,iBAyCPC,WAMAC,UAaOC,2BASAC,yBAuBAC;AArHb;;;AAyBO,IAAML,kBAAkB,8BAC3BM,YAAAA;AAEA,YAAMC,WAAW,IAAIX,kBAAAA;AAErBW,eAASC,SAAST,MAAAA,EAAQU,YAAYH,QAAQI,UAAU,IAAIZ,cAAAA,CAAAA;AAC5DS,eAASC,SAASX,SAAAA,EAAWY,YAAYH,QAAQK,MAAM;AAEvD,iBAAWC,UAAUN,QAAQO,SAAS;AAClC,YAAID,OAAOE,MAAO,OAAMF,OAAOE,MAAMP,UAAUD,QAAQK,MAAM;MACjE;AAEA,YAAMI,YAAYR,SAASS,MAAK;AAEhC,YAAMC,WAAW,mCAAA;AACb,mBAAWL,UAAU;aAAIN,QAAQO;UAASK,QAAO,GAAI;AACjD,cAAI,CAACN,OAAOK,SAAU;AACtB,cAAI;AACA,kBAAML,OAAOK,SAASF,SAAAA;UAC1B,QAAQ;UAER;QACJ;MACJ,GATiB;AAWjB,aAAO;QAAEA;QAAWE;MAAS;IACjC,GA1B+B;AAyC/B,IAAMhB,YAAYkB,uBAAOC,IAAI,8CAAA;AAM7B,IAAMlB,WAAW,6BAAA;AACb,YAAMmB,IAAIC;AACV,UAAI,CAACD,EAAEpB,SAAAA,GAAY;AACfoB,UAAEpB,SAAAA,IAAa;UAAEsB,oBAAoB,oBAAIC,QAAAA;QAAU;MACvD;AACA,aAAOH,EAAEpB,SAAAA;IACb,GANiB;AAaV,IAAME,4BAA4B,wBAA4BsB,KAAiBZ,YAAAA;AAClFX,eAAAA,EAAWqB,mBAAmBG,IAAID,KAAK;QAAEZ;MAAiD,CAAA;IAC9F,GAFyC;AASlC,IAAMT,0BAA0B,8BAAOqB,QAAAA;AAC1C,YAAME,OAAOzB,SAAAA,EAAWqB,mBAAmBK,IAAIH,GAAAA;AAC/C,UAAI,CAACE,KAAM,OAAM,IAAIE,MAAM,8HAAA;AAC3B,UAAI,CAACF,KAAKG,SAAS;AACfH,aAAKG,UAAU9B,gBAAgB;UAC3Ba,SAASc,KAAKd;UACdF,QAAQc,IAAId;QAChB,CAAA;MACJ;AACA,aAAOgB,KAAKG;IAChB,GAVuC;AAuBhC,IAAMzB,mBAAmB,wBAC5B0B,YAAAA;AAEA,aAAO,OAAOC,MAAMP,KAAKQ,SAAAA;AACrB,cAAM,EAAElB,UAAS,IAAK,MAAMX,wBAAwBqB,GAAAA;AACpD,cAAMS,SAASnB,UAAUoB,sBAAqB;AAC9C,cAAMC,WAAgCC,OAAOC,OAAO,CAAC,GAAGb,KAAK;UAAEV,WAAWmB;QAAO,CAAA;AACjF,eAAOH,QAAQC,MAAMI,UAAUH,IAAAA;MACnC;IACJ,GATgC;;;;;ACrHhC,SAASM,eAAe;;;ACAxB,YAAYC,WAAW;AAGhB,IAAMC,UAAUC;AAGhB,IAAMC,uBAAN,cAAmCC,MAAAA;EAN1C,OAM0CA;;;EACtC,cAAc;AACV,UAAM,kBAAA;AACN,SAAKC,OAAO;EAChB;AACJ;AAOO,IAAMC,SAAS,wBAAIC,UAAAA;AACtB,MAAUC,eAASD,KAAAA,EAAQ,OAAM,IAAIJ,qBAAAA;AACrC,SAAOI;AACX,GAHsB;;;ACftB,IAAME,kBAAkB,wBAACC,QAAAA;AACrB,MAAI,CAACA,IAAIC,oBAAqB,QAAO;AACrC,MAAIC,MAAMC,QAAQH,IAAIC,mBAAmB,EAAG,QAAO;IAAEG,SAASJ,IAAIC;EAAoB;AACtF,SAAOD,IAAIC;AACf,GAJwB;AAMxB,IAAMI,mBAAmB,wBAACL,QAAAA;AACtB,MAAI,CAACA,IAAIM,UAAW,QAAO;AAC3B,MAAIN,IAAIM,cAAc,KAAM,QAAO,CAAC;AACpC,SAAON,IAAIM;AACf,GAJyB;AAMzB,IAAMC,eAAe,wBAACP,SAAiCA,IAAIQ,WAAW,CAAA,GAAIC,KAAKC,CAAAA,MAAK,+BAA+BC,KAAKD,EAAEE,KAAK,CAAA,GAA1G;AAOd,IAAMC,wBAAwB,wBAACb,KAAoBc,KAAiBC,cAAAA;AACvE,QAAMC,QAAQjB,gBAAgBC,GAAAA;AAC9B,MAAI,CAACgB,MAAO,QAAO;AACnB,QAAMC,WAAWD,MAAMC,YAAY;AACnC,QAAMC,UAAUJ,IAAIK,IAAIF,QAAAA;AACxB,MAAIC,YAAYE,UAAaJ,MAAMZ,QAAQiB,SAASH,OAAAA,EAAU,QAAO;AACrE,QAAMI,QAAQJ,YAAYE,SAAY,YAAYF;AAClDJ,MAAIS,OAAOC,MAAM,oBAAoBT,SAAAA,UAAmBE,QAAAA,IAAYK,KAAAA,cAAmBN,MAAMZ,QAAQqB,KAAK,IAAA,CAAA,GAAQ;AAClH,SAAO;AACX,GATqC;AAgB9B,IAAMC,mBAAmB,8BAAO1B,KAAoBc,KAAiBC,WAAmBY,gBAAAA;AAC3F,QAAMC,OAAOvB,iBAAiBL,GAAAA;AAC9B,MAAI,CAAC4B,KAAM,QAAO;AAClB,MAAID,YAAa,QAAO;AACxB,MAAI,CAACb,IAAIe,cAAa,GAAI;AACtBf,QAAIS,OAAOC,MAAM,IAAIT,SAAAA,kEAA2E;AAChG,WAAO;EACX;AACA,MAAIa,KAAKE,YAAY,SAAS;AAC1B,UAAMC,SAASH,KAAKG,UAAUhB;AAC9B,UAAMiB,UAAS,MAAMC,QAAQC,KAAK;MAAEC,SAASP,KAAKO,WAAW,8BAA8BJ,MAAAA;IAAuB,CAAA;AAClH,QAAIE,QAAQG,SAASJ,OAAAA,EAAS,QAAO;AACrC,QAAIA,YAAWD,QAAQ;AACnBjB,UAAIS,OAAOc,KAAK,6CAAA;AAChB,aAAO;IACX;AACA,WAAO;EACX;AACA,QAAML,SAAS,MAAMC,QAAQH,QAAQ;IAAEK,SAASP,KAAKO,WAAW,4BAA4BpB,SAAAA;IAAeuB,cAAc;EAAM,CAAA;AAC/H,MAAIL,QAAQG,SAASJ,MAAAA,EAAS,QAAO;AACrC,SAAOA,WAAW;AACtB,GArBgC;AAwBzB,IAAMO,iBAAiB,wBAACvC,QAAgCwC,QAAQxC,IAAIM,SAAS,KAAK,CAACC,aAAaP,GAAAA,GAAzE;;;AC1D9B,IAAMyC,cAAc,wBAACC,KAAcC,SAAAA;AAC/B,MAAIA,KAAKC,UAAU;AACfF,QAAIG,eAAeF,KAAKG,OAAOH,KAAKI,aAAaJ,KAAKK,OAAO;AAC7D;EACJ;AACA,MAAIL,KAAKK,YAAYC,QAAW;AAC5BP,QAAIQ,OAAOP,KAAKG,OAAOH,KAAKI,aAAaJ,KAAKK,OAAO;EACzD,OAAO;AACHN,QAAIQ,OAAOP,KAAKG,OAAOH,KAAKI,WAAW;EAC3C;AACJ,GAVoB;AAYpB,IAAMI,oBAAoB,wBAACC,QAAiBC,SAAAA;AACxC,QAAMC,WAAWF,OAAOG,SAASC,KAAKC,CAAAA,MAAKA,EAAEJ,KAAI,MAAOA,IAAAA;AACxD,MAAIC,SAAU,QAAOA;AACrB,SAAOF,OAAOM,QAAQL,IAAAA,EAAMN,YAAY,GAAGM,IAAAA,WAAe;AAC9D,GAJ0B;AAS1B,IAAMM,kBAAkB,wBAACb,UAAAA;AACrB,QAAMc,SAASd,MAAMe,MAAM,OAAA;AAC3B,QAAMC,OAAOF,OAAOJ,KAAKO,CAAAA,MAAKA,EAAEC,WAAW,IAAA,CAAA;AAC3C,QAAMC,SAASH,QAAQF,OAAOJ,KAAKO,CAAAA,MAAKA,EAAEC,WAAW,GAAA,CAAA;AACrD,MAAI,CAACC,OAAQ,QAAOnB;AACpB,QAAMoB,WAAWD,OAAOE,QAAQ,OAAO,EAAA;AACvC,SAAOD,SAASC,QAAQ,aAAa,CAACC,GAAGX,MAAcA,EAAEY,YAAW,CAAA;AACxE,GAPwB;AASxB,IAAMC,aAAa,wBAAClB,QAAiBmB,UAAkBC,KAAoBC,KAAiBC,aAAqBC,aAAAA;AAC7G,QAAMjC,MAAMU,OAAOM,QAAQa,QAAAA,EAAUxB,YAAYyB,IAAIzB,WAAW;AAChE,QAAM6B,YAAYD,SAASE,KAAK,GAAA;AAEhC,aAAWC,OAAON,IAAIO,QAAQ,CAAA,GAAI;AAC9B,UAAMC,UAAUF,IAAIG,WAAW,GAAGH,IAAIzB,IAAI,QAAQyB,IAAIzB;AACtD,QAAIyB,IAAIlC,SAAUF,KAAIwC,SAAS,IAAIF,OAAAA,KAAYF,IAAI/B,WAAW;QACzDL,KAAIwC,SAAS,IAAIF,OAAAA,KAAYF,IAAI/B,WAAW;EACrD;AAEA,aAAWoC,OAAOX,IAAIY,WAAW,CAAA,GAAI;AACjC3C,gBAAYC,KAAKyC,GAAAA;EACrB;AAEA,MAAIE,eAAeb,GAAAA,EAAM9B,KAAIQ,OAAO,aAAa,yDAAyD,KAAA;AAE1G,MAAIsB,IAAIc,YAAa5C,KAAI6C,mBAAmB,IAAA,EAAMC,qBAAqB,IAAA;AAEvE9C,MAAI+C,OAAO,UAAUC,YAAAA;AAGjB,UAAMC,kBAAkBD,QAAQA,QAAQE,SAAS,CAAA;AACjD,UAAMC,OAAQH,QAAQA,QAAQE,SAAS,CAAA,KAAM,CAAC;AAC9C,UAAME,aAAaJ,QAAQK,MAAM,GAAGL,QAAQE,SAAS,CAAA;AAErD,UAAMI,oBAA8BF,WAAWG,QAAQC,CAAAA,MAAMC,MAAMC,QAAQF,CAAAA,IAAKA,EAAEG,IAAIC,MAAAA,IAAUJ,KAAK,OAAO,CAAA,IAAK;MAACI,OAAOJ,CAAAA;KAAG;AAC5H,UAAMK,kBAAkB/B,IAAIc,cAAcK,gBAAgBZ,OAAOiB;AAEjE,eAAWQ,WAAWhC,IAAIY,WAAW,CAAA,GAAI;AACrC,UAAI,CAACoB,QAAQC,OAAQ;AACrB,YAAMC,MAAM/C,gBAAgB6C,QAAQ1D,KAAK;AACzC,UAAI+C,KAAKa,GAAAA,MAASzD,UAAa0D,QAAQC,IAAIJ,QAAQC,MAAM,MAAMxD,QAAW;AACtE4C,aAAKa,GAAAA,IAAOC,QAAQC,IAAIJ,QAAQC,MAAM;MAC1C;IACJ;AAEA,QAAI,CAACI,sBAAsBrC,KAAKC,KAAKG,SAAAA,GAAY;AAC7C+B,cAAQG,KAAK,CAAA;AACb;IACJ;AAEA,QAAItC,IAAIuC,WAAW;AACf,YAAMC,UAAU,MAAMC,iBAAiBzC,KAAKC,KAAKG,WAAWiB,KAAK,KAAA,MAAW,IAAA;AAC5E,UAAI,CAACmB,SAAS;AACVL,gBAAQG,KAAK,CAAA;AACb;MACJ;IACJ;AAEA,QAAII,YAAYrB;AAChB,QAAIrB,IAAI2C,eAAe1C,IAAI2C,cAAa,GAAI;AACxCF,kBAAa,MAAM1C,IAAI2C,YAAY1C,KAAKoB,IAAAA;IAC5C;AAEA,QAAI;AACA,YAAMwB,WAAW,MAAM7C,IAAI8C,IAAIJ,WAAWzC,KAAK8B,eAAAA;AAC/C,UAAI,OAAOc,aAAa,YAAYA,aAAa,EAAGV,SAAQG,KAAKO,QAAAA;IACrE,SAASE,KAAK;AACV9C,UAAI+C,OAAOC,MAAM,IAAI/C,WAAAA,KAAiB6C,IAAcG,OAAO,EAAE;AAC7D,UAAKH,IAAcI,MAAOlD,KAAI+C,OAAOI,MAAOL,IAAcI,SAAS,EAAA;AACnEhB,cAAQG,KAAK,CAAA;IACjB;EACJ,CAAA;AACJ,GA/DmB;AAuEZ,IAAMe,mBAAmB,wBAACC,SAAkBC,YAAiCtD,QAAAA;AAChF,QAAMuD,kBAAkB,oBAAIC,IAAAA;AAG5B,QAAMC,SAAS;OAAIH;IAAYI,KAAK,CAACC,GAAGC,MAAAA;AACpC,QAAID,EAAEE,WAAWD,EAAEC,OAAQ,QAAO;AAClC,WAAOF,EAAEE,WAAW,SAAS,KAAK;EACtC,CAAA;AAEA,aAAWC,SAASL,QAAQ;AACxB,UAAMxB,MAAM6B,MAAMC,KAAK3D,KAAK,GAAA;AAC5B,UAAMvB,WAAW0E,gBAAgBS,IAAI/B,GAAAA;AACrC,QAAIpD,UAAU;AACV,YAAMoF,WAAWH,MAAMD,WAAW,WAAYC,MAAMI,cAAc,mBAAoB;AACtF,YAAMC,QAAQtF,SAASgF,WAAW,WAAYhF,SAASqF,cAAc,mBAAoB;AACzF,YAAM,IAAIE,MAAM,YAAYnC,GAAAA,8BAAiCkC,KAAAA,KAAUF,QAAAA,qBAA6B;IACxG;AACAV,oBAAgBc,IAAIpC,KAAK;MAAE4B,QAAQC,MAAMD;MAAQK,YAAYJ,MAAMI;IAAW,CAAA;AAE9E,QAAIvF,SAAkB0E;AACtB,eAAWiB,WAAWR,MAAMC,KAAKzC,MAAM,GAAG,EAAC,GAAI;AAC3C3C,eAASD,kBAAkBC,QAAQ2F,OAAAA;IACvC;AACA,UAAMxE,WAAWgE,MAAMC,KAAKD,MAAMC,KAAK5C,SAAS,CAAA;AAChD,QAAI,CAACrB,SAAU;AACf,UAAMG,cAAc6D,MAAMD,WAAW,WAAYC,MAAMI,cAAc,WAAY;AACjFrE,eAAWlB,QAAQmB,UAAUgE,MAAMS,QAAQvE,KAAKC,aAAa6D,MAAMC,IAAI;EAC3E;AACJ,GA5BgC;;;ACzGhC,SAASS,cAAAA,aAAYC,gBAAAA,qBAAoB;AACzC,SAASC,WAAAA,UAASC,WAAAA,gBAAe;AACjC,SAASC,kBAAkBC,+BAA+C;;;ACF1E,SAASC,YAAYC,WAAWC,aAAaC,cAAcC,QAAQC,qBAAqB;AACxF,SAASC,WAAAA,gBAAe;;;ACDxB,SAASC,kBAAkB;AAC3B,SAASC,SAASC,cAAc;AAChC,SAASC,UAAUC,eAAe;AAYlC,IAAMC,MAAM,wBAACC,QAAAA;AACT,QAAMC,QAAQC,QAAQH,IAAIC,GAAAA;AAC1B,SAAOC,SAASA,MAAME,SAAS,IAAIF,QAAQG;AAC/C,GAHY;AASL,IAAMC,cAAc,wBAACC,QAAAA;AACxB,QAAMC,WAAWL,QAAQK;AACzB,MAAIA,aAAa,UAAU;AACvB,UAAMC,QAAOC,QAAAA;AACb,WAAO;MACHC,KAAKC,QAAQH,OAAM,gBAAgBF,GAAAA;MACnCM,SAASD,QAAQE,OAAAA,GAAUP,GAAAA;MAC3BQ,OAAOH,QAAQH,OAAM,kBAAkBF,GAAAA;IAC3C;EACJ;AACA,MAAIC,aAAa,SAAS;AACtB,UAAMQ,OAAOhB,IAAI,cAAA,KAAmBY,QAAQF,QAAAA,GAAW,eAAA;AACvD,WAAO;MACHC,KAAKC,QAAQI,MAAMT,KAAK,KAAA;MACxBM,SAASD,QAAQI,MAAMT,KAAK,MAAA;MAC5BQ,OAAOH,QAAQI,MAAMT,KAAK,OAAA;IAC9B;EACJ;AAEA,QAAME,OAAOC,QAAAA;AACb,QAAMO,QAAQjB,IAAI,gBAAA,KAAqBY,QAAQH,MAAM,cAAA;AACrD,QAAMM,QAAQf,IAAI,gBAAA,KAAqBY,QAAQH,MAAM,QAAA;AAGrD,QAAMS,cAAclB,IAAI,iBAAA,KAAsBY,QAAQE,OAAAA,GAAU,GAAGP,GAAAA,IAAOJ,QAAQgB,SAAM,KAAQ,CAAA,EAAG;AACnG,QAAMN,UAAUb,IAAI,iBAAA,IAAqBY,QAAQM,aAAaX,GAAAA,IAAOW;AACrE,SAAO;IACHP,KAAKC,QAAQK,OAAOV,GAAAA;IACpBM;IACAE,OAAOH,QAAQG,OAAOR,GAAAA;EAC1B;AACJ,GA/B2B;AAyCpB,IAAMa,cAAc,wBAACC,gBAAAA;AACxB,QAAMC,WAAWV,QAAQS,WAAAA;AACzB,QAAME,OAAOC,WAAW,QAAA,EAAUC,OAAOH,QAAAA,EAAUI,OAAO,KAAA,EAAOC,MAAM,GAAG,CAAA;AAC1E,QAAMC,OAAOC,SAASP,QAAAA,EAAUQ,QAAQ,oBAAoB,GAAA,KAAQ;AACpE,SAAO,GAAGF,IAAAA,IAAQL,IAAAA;AACtB,GAL2B;;;AD1D3B,IAAMQ,WAAW;AAoDjB,IAAMC,eAAe;AAUrB,IAAMC,UAAU,wBAACC,QAAAA;AACb,MAAI;AACAC,YAAQC,KAAKF,KAAK,CAAA;AAClB,WAAO;EACX,SAASG,KAAK;AAEV,WAAQA,IAA8BC,SAAS;EACnD;AACJ,GARgB;AAUhB,IAAMC,gBAAgB,wBAACC,SAAAA;AACnB,MAAI,CAACC,WAAWD,IAAAA,EAAO,QAAOE;AAC9B,MAAI;AACA,UAAMC,MAAMC,aAAaJ,MAAM,OAAA;AAC/B,UAAMK,SAASC,KAAKC,MAAMJ,GAAAA;AAC1B,QAAI,OAAOE,OAAOX,QAAQ,SAAU,QAAOQ;AAC3C,WAAOG;EACX,QAAQ;AACJ,WAAOH;EACX;AACJ,GAVsB;AAYtB,IAAMM,WAAW,wBAACC,MAAcC,QAAmBC,SAAiBC,aAAmC;EACnGH;EACAf,KAAKgB,OAAOhB;EACZmB,SAASpB,QAAQiB,OAAOhB,GAAG;EAC3BkB;EACAD;EACAG,SAASJ,OAAOI;EAChBC,MAAML,OAAOK;EACbC,KAAKN,OAAOM;EACZC,WAAW,IAAIC,KAAKR,OAAOO,SAAS;AACxC,IAViB;AAmBV,IAAME,gBAAgB,wBAACC,aAAqBC,OAAcC,QAAmBC,QAAqBC,YAAYjC,QAAAA,MAAS;AAC1H,QAAMkC,OAAOC,YAAYN,WAAAA;AACzB,QAAMO,SAASC,SAAQL,MAAMM,SAASJ,IAAAA;AACtC,QAAMK,SAASF,SAAQL,MAAMQ,KAAKN,IAAAA;AAElC,QAAMd,UAAU,wBAACF,SAAAA;AACb,QAAI,CAACjB,aAAawC,KAAKvB,IAAAA,GAAO;AAC1B,YAAM,IAAIwB,MAAM,wBAAwBxB,IAAAA,0CAA8C;IAC1F;AACA,WAAOmB,SAAQD,QAAQ,GAAGlB,IAAAA,MAAU;EACxC,GALgB;AAMhB,QAAMG,UAAU,wBAACH,SAAAA;AACb,QAAI,CAACjB,aAAawC,KAAKvB,IAAAA,GAAO;AAC1B,YAAM,IAAIwB,MAAM,wBAAwBxB,IAAAA,0CAA8C;IAC1F;AACA,WAAOmB,SAAQE,QAAQ,GAAGrB,IAAAA,MAAU;EACxC,GALgB;AAOhB,QAAMyB,SAAS,wBAACzB,SAAAA;AACZ,UAAMT,OAAOW,QAAQF,IAAAA;AACrB,UAAMC,SAASX,cAAcC,IAAAA;AAC7B,QAAI,CAACU,OAAQ,QAAOR;AACpB,WAAOM,SAASC,MAAMC,QAAQV,MAAMY,QAAQH,IAAAA,CAAAA;EAChD,GALe;AAOf,QAAM0B,OAAO,wBAAC1B,MAAc2B,UAAuC,CAAC,MAAC;AACjE,UAAMC,UAAUH,OAAOzB,IAAAA;AACvB,QAAI,CAAC4B,QAAS,QAAO;AACrB,QAAIC,YAAY;AAChB,QAAID,QAAQxB,SAAS;AACjB,UAAI;AACAlB,gBAAQC,KAAKyC,QAAQ3C,KAAK0C,QAAQG,UAAU,SAAA;AAC5CD,oBAAY;MAChB,SAASzC,KAAK;AACV,YAAKA,IAA8BC,SAAS,QAAS,OAAMD;MAC/D;IACJ;AACA2C,WAAOH,QAAQ1B,SAAS;MAAE8B,OAAO;IAAK,CAAA;AACtCnB,WAAOoB,MAAM,WAAWjC,IAAAA,kBAAsB4B,QAAQ3C,GAAG,GAAG;AAC5D,WAAO4C;EACX,GAfa;AAiBb,QAAMK,QAAQ,wBAACP,YAAAA;AACX,UAAMQ,WAAWV,OAAOE,QAAQ3B,IAAI;AACpC,QAAImC,UAAU/B,SAAS;AACnB,YAAMgC,SAAST,QAAQU,cAAc;AACrC,UAAID,WAAW,QAAS,QAAOD;AAC/B,UAAIC,WAAW,SAAS;AACpB,cAAM,IAAIZ,MAAM,WAAWG,QAAQ3B,IAAI,6BAA6BmC,SAASlD,GAAG,IAAI;MACxF;AACAyC,WAAKC,QAAQ3B,IAAI;IACrB,WAAWmC,UAAU;AAEjBJ,aAAOI,SAASjC,SAAS;QAAE8B,OAAO;MAAK,CAAA;IAC3C;AAEAM,cAAUpB,QAAQ;MAAEqB,WAAW;IAAK,CAAA;AACpCD,cAAUjB,QAAQ;MAAEkB,WAAW;IAAK,CAAA;AACpC,UAAMhD,OAAOW,QAAQyB,QAAQ3B,IAAI;AACjC,UAAMsB,MAAMnB,QAAQwB,QAAQ3B,IAAI;AAChC,UAAMwC,SAAS5B,MAAM6B,YAAYd,QAAQtB,SAASsB,QAAQrB,MAAM;MAC5DC,KAAKoB,QAAQpB;MACbmC,KAAKf,QAAQe;MACbvC,SAASmB;IACb,CAAA;AACA,UAAMrB,SAAoB;MACtBhB,KAAKuD,OAAOvD;MACZoB,SAASsB,QAAQtB;MACjBC,MAAMqB,QAAQrB;MACdC,KAAKoB,QAAQpB,OAAOI;MACpBH,YAAW,oBAAIC,KAAAA,GAAOkC,YAAW;IACrC;AACAC,kBAAcrD,MAAMM,KAAKgD,UAAU5C,QAAQ,MAAM,CAAA,CAAA;AACjDY,WAAOoB,MAAM,WAAWN,QAAQ3B,IAAI,kBAAkBwC,OAAOvD,GAAG,SAASqC,GAAAA,GAAM;AAC/E,WAAOvB,SAAS4B,QAAQ3B,MAAMC,QAAQV,MAAM+B,GAAAA;EAChD,GAjCc;AAmCd,QAAMwB,OAAO,6BAAA;AACT,QAAI,CAACtD,WAAW0B,MAAAA,EAAS,QAAO,CAAA;AAChC,UAAM6B,UAA0B,CAAA;AAChC,eAAWC,SAASC,YAAY/B,MAAAA,GAAS;AACrC,UAAI,CAAC8B,MAAME,SAAS,MAAA,EAAS;AAC7B,YAAMlD,OAAOgD,MAAMG,MAAM,GAAG,CAAC,OAAOC,MAAM;AAC1C,YAAMC,WAAW5B,OAAOzB,IAAAA;AACxB,UAAIqD,SAAUN,SAAQO,KAAKD,QAAAA;IAC/B;AACA,WAAON;EACX,GAVa;AAYb,SAAO;IAAEb;IAAOR;IAAMD;IAAQqB;IAAM3C;IAASD;EAAQ;AACzD,GA1F6B;;;AEpG7B,IAAMqD,SAAS,wBAACC,MAAcC,SAAyB,QAAQD,IAAAA,IAAQC,IAAAA,WAAxD;AAYR,IAAMC,sBAAsB,wBAACC,UAA+B,CAAC,OAAkB;EAClFC,MAAMC,wBAAAA,QAAOC,QAAQC,IAAIF,GAAAA,GAAnBA;EACNG,MAAMH,wBAAAA,QAAOC,QAAQE,KAAKT,OAAO,IAAI,KAAKM,GAAAA,EAAK,CAAA,GAAzCA;EACNI,OAAOJ,wBAAAA,QAAOC,QAAQG,MAAMV,OAAO,IAAI,UAAKM,GAAAA,EAAK,CAAA,GAA1CA;EACPK,SAASL,wBAAAA,QAAOC,QAAQC,IAAIR,OAAO,IAAI,UAAKM,GAAAA,EAAK,CAAA,GAAxCA;EACTM,OAAON,wBAAAA,QAAAA;AACH,QAAIF,QAAQS,QAASN,SAAQC,IAAIR,OAAO,IAAI,QAAKM,GAAAA,EAAK,CAAA;EAC1D,GAFOA;AAGX,IARmC;;;ACrBnC,SAASQ,aAAgC;AACzC,SAASC,aAAAA,YAAWC,gBAAgB;AACpC,SAASC,eAAe;AACxB,SAASC,aAA+D;AAwCjE,IAAMC,cAAc,wBAACC,KAAaC,YAA8B;EACnEC,KAAK,wBAACC,SAASC,MAAMC,YAAYC,MAAMH,SAASC,MAAM;IAAEJ;IAAK,GAAGK;EAAQ,CAAA,GAAnE;EACLE,cAAc,8BAAOJ,SAASC,MAAMC,YAAAA;AAChCJ,WAAOO,MAAM,KAAKL,OAAAA,IAAWC,KAAKK,KAAK,GAAA,CAAA,EAAM;AAC7C,UAAMC,QAAQJ,MAAMH,SAASC,MAAM;MAAEJ;MAAKW,OAAO;MAAWC,QAAQ;MAAO,GAAGP;IAAQ,CAAA;AACtF,UAAMQ,SAAS,MAAMH;AACrB,WAAOG,OAAOC,YAAY;EAC9B,GALc;EAMdC,aAAa,wBAACZ,SAASC,MAAMC,UAAU,CAAC,MAAC;AACrC,UAAMW,aAAaX,QAAQL,OAAOA;AAClC,QAAIW,QAAsB;AAC1B,QAAIN,QAAQY,SAAS;AACjBC,MAAAA,WAAUC,QAAQd,QAAQY,OAAO,GAAG;QAAEG,WAAW;MAAK,CAAA;AACtD,YAAMC,KAAKC,SAASjB,QAAQY,SAAS,GAAA;AACrCN,cAAQ;QAAC;QAAUU;QAAIA;;IAC3B;AACApB,WAAOO,MAAM,gBAAgBL,OAAAA,IAAWC,KAAKK,KAAK,GAAA,CAAA,EAAM;AACxD,UAAMC,QAAQa,MAAMpB,SAASC,MAAM;MAC/BJ,KAAKgB;MACLQ,KAAKnB,QAAQmB,OAAOC,QAAQD;MAC5BE,UAAU;MACVf;IACJ,CAAA;AACA,QAAID,MAAMiB,QAAQC,QAAW;AACzB,YAAM,IAAIC,MAAM,qCAAqC1B,OAAAA,EAAS;IAClE;AACAO,UAAMoB,MAAK;AACX,WAAO;MAAEH,KAAKjB,MAAMiB;MAAKV,SAASZ,QAAQY;IAAQ;EACtD,GApBa;AAqBjB,IA7B2B;;;ACtCpB,IAAMc,gBAAgB,6BAAA;AACzB,MAAIC,QAAQC,IAAI,IAAA,MAAU,UAAUD,QAAQC,IAAI,IAAA,MAAU,IAAK,QAAO;AACtE,MAAID,QAAQC,IAAI,yBAAA,MAA+B,IAAK,QAAO;AAC3D,SAAOC,QAAQF,QAAQG,OAAOC,SAASJ,QAAQK,MAAMD,KAAK;AAC9D,GAJ6B;;;ALoB7B,IAAME,eAAe,wBAACC,UAAAA;AAClB,MAAIC,MAAMD;AACV,WAASE,IAAI,GAAGA,IAAI,IAAIA,KAAK;AACzB,QAAIC,YAAWC,SAAQH,KAAK,qBAAA,CAAA,EAAyB,QAAOA;AAC5D,UAAMI,SAASC,SAAQL,GAAAA;AACvB,QAAII,WAAWJ,IAAK;AACpBA,UAAMI;EACV;AACA,SAAOE,QAAQC,IAAG;AACtB,GATqB;AAcrB,IAAMC,cAAc,wBAACC,UACjBA,MAAMC,QAAQ,8DAA8D,CAACC,GAAGC,QAA4BC,SAAAA;AACxG,QAAMC,MAAOF,UAAUC;AACvB,SAAOP,QAAQS,IAAID,GAAAA,KAAQ;AAC/B,CAAA,GAJgB;AAMpB,IAAME,cAAc,wBAACC,SAAAA;AACjB,MAAI,CAACf,YAAWe,IAAAA,EAAO;AACvB,aAAWC,QAAQC,cAAaF,MAAM,OAAA,EAASG,MAAM,IAAA,GAAO;AACxD,UAAMC,UAAUH,KAAKI,KAAI;AACzB,QAAI,CAACD,WAAWA,QAAQE,WAAW,GAAA,EAAM;AACzC,UAAMC,QAAQH,QAAQI,QAAQ,GAAA;AAC9B,QAAID,UAAU,GAAI;AAClB,UAAMV,MAAMO,QAAQK,MAAM,GAAGF,KAAAA,EAAOF,KAAI;AACxC,UAAMK,WAAWN,QAAQK,MAAMF,QAAQ,CAAA,EAAGF,KAAI;AAK9C,UAAMM,eAAeD,SAASJ,WAAW,GAAA,KAAQI,SAASE,SAAS,GAAA;AACnE,UAAMC,eAAeH,SAASJ,WAAW,GAAA,KAAQI,SAASE,SAAS,GAAA;AACnE,QAAIpB,QAAQmB,gBAAgBE,eAAeH,SAASD,MAAM,GAAG,EAAC,IAAKC;AACnE,QAAI,CAACC,aAAcnB,SAAQD,YAAYC,KAAAA;AAEvC,QAAI,EAAEK,OAAOR,QAAQS,KAAMT,SAAQS,IAAID,GAAAA,IAAOL;EAClD;AACJ,GApBoB;AA2Bb,IAAMsB,wBAAwB,mCACjC,IAAIC,iBAAAA,EAAmBC,YAAY,IAAIC,wBAAAA,CAAAA,EAA2BC,MAAK,GADtC;AAQ9B,IAAMC,eAAe,8BAAOC,UAA+B,CAAC,MAAC;AAIhE,QAAM9B,MAAMD,QAAQC,IAAG;AACvB,QAAM+B,WAAWD,QAAQC,YAAYxC,aAAaS,GAAAA;AAClD,QAAMgC,QAAkB;IAAEhC;IAAK+B;EAAS;AAExC,aAAWE,WAAWH,QAAQI,YAAY;IAAC;IAAQ;KAAkB;AACjE,UAAMC,WAAWF,QAAQjB,WAAW,GAAA,IAAOiB,UAAUrC,SAAQmC,UAAUE,OAAAA;AACvExB,gBAAY0B,QAAAA;EAChB;AAEA,QAAMC,SAASN,QAAQM,UAAUC,oBAAoB;IAAEC,SAASR,QAAQQ;EAAQ,CAAA;AAChF,QAAMC,QAAQC,YAAYT,UAAUK,MAAAA;AACpC,QAAMK,UAAUC,cAAcX,UAAUQ,OAAOH,MAAAA;AAC/C,QAAMO,SAASb,QAAQa,UAAW,MAAMnB,sBAAAA;AAExC,SAAO;IACHQ;IACAI;IACAG;IACAE;IACAE;IACAnC,KAAKT,QAAQS;IACboC;EACJ;AACJ,GA3B4B;;;AMnErB,IAAMC,YAAY,8BAAOC,KAAiBC,QAAiBC,YAAAA;AAC9DF,MAAIG,OAAOC,KAAK,wBAAA;AAEhB,MAAIC,SAAS;AACb,MAAIC,QAAQ;AAEZ,aAAWC,SAASN,QAAQ;AACxBO,YAAQC,OAAOC,MAAM,KAAKH,MAAMI,KAAKC,OAAO,IAAI,GAAA,CAAA,GAAO;AACvD,QAAIC;AACJ,QAAI;AACAA,eAAS,MAAMN,MAAMO,IAAId,GAAAA;IAC7B,SAASe,KAAK;AACVF,eAAS;QAAEG,IAAI;QAAOC,SAAS,UAAWF,IAAcE,OAAO;MAAG;IACtE;AAEA,QAAIJ,OAAOG,IAAI;AACXR,cAAQC,OAAOC,MAAM,yBAAoBG,OAAOI,OAAO;CAAI;AAC3D;IACJ;AAEAT,YAAQC,OAAOC,MAAM,yBAAoBG,OAAOI,OAAO;CAAI;AAE3D,QAAIf,QAAQgB,OAAOX,MAAMY,SAAS;AAC9BX,cAAQC,OAAOC,MAAM,uCAA6B;AAClD,UAAI;AACA,cAAMU,YAAY,MAAMb,MAAMY,QAAQnB,GAAAA;AACtC,YAAIoB,UAAUJ,IAAI;AACdR,kBAAQC,OAAOC,MAAM,yBAAoBU,UAAUH,OAAO;CAAI;AAC9DX;AACA;QACJ;AACAE,gBAAQC,OAAOC,MAAM,yBAAoBU,UAAUH,OAAO;CAAI;MAClE,SAASF,KAAK;AACVP,gBAAQC,OAAOC,MAAM,yBAAqBK,IAAcE,OAAO;CAAI;MACvE;IACJ,WAAWJ,OAAOQ,SAAS;AACvBb,cAAQC,OAAOC,MAAM,cAASG,OAAOQ,OAAO;CAAI;IACpD;AACAhB;EACJ;AAEAG,UAAQC,OAAOC,MAAM,IAAA;AACrB,MAAIL,WAAW,GAAG;AACdL,QAAIG,OAAOmB,QAAQ,oBAAA;AACnB,WAAO;EACX;AACA,MAAIpB,QAAQgB,OAAOZ,QAAQ,GAAG;AAC1BN,QAAIG,OAAOC,KAAK,cAAcE,KAAAA,0CAA+C;EACjF;AACAN,MAAIG,OAAOoB,MAAM,GAAGlB,MAAAA,mBAAyB;AAC7C,SAAO;AACX,GAnDyB;AAyDlB,IAAMmB,qBAAqB,wBAACvB,YAAuD;EACtFwB,aAAa;EACbvB,SAAS;IAAC;MAAEwB,OAAO;MAASD,aAAa;IAAsD;;EAC/FX,KAAK,8BAAOa,MAAM3B,QAAQD,UAAUC,KAAKC,QAAQ;IAAEiB,KAAKS,KAAKT,QAAQ;EAAK,CAAA,GAArE;AACT,IAJkC;;;ACtElC,SAASU,cAAAA,aAAYC,gBAAAA,eAAcC,eAAAA,oBAAmB;AACtD,SAASC,MAAMC,WAAAA,gBAAe;AAC9B,SAASC,qBAAqB;AA+BvB,IAAMC,uBAAuB,8BAAOC,KAAiBC,YAAAA;AACxD,QAAMC,WAAWD,QAAQE,SAAS;IAAC;IAAQ;;AAC3C,QAAMC,UAAU,IAAIC,IAAIJ,QAAQK,mBAAmB,CAAA,CAAE;AACrD,QAAMC,aAAkC,CAAA;AAExC,aAAWC,WAAWN,UAAU;AAC5B,UAAMO,OAAOC,SAAQT,QAAQU,UAAUH,OAAAA;AACvC,QAAI,CAACI,YAAWH,IAAAA,EAAO;AACvB,eAAWI,SAASC,aAAYL,MAAM;MAAEM,eAAe;IAAK,CAAA,GAAI;AAC5D,UAAI,CAACF,MAAMG,YAAW,EAAI;AAC1B,YAAMC,UAAUC,KAAKT,MAAMI,MAAMM,MAAM,cAAA;AACvC,UAAI,CAACP,YAAWK,OAAAA,EAAU;AAE1B,UAAIG;AACJ,UAAI;AACAA,cAAMC,KAAKC,MAAMC,cAAaN,SAAS,OAAA,CAAA;MAC3C,QAAQ;AACJ;MACJ;AAEA,YAAMO,cAAcJ,IAAIK,SAASC;AACjC,UAAI,CAACF,YAAa;AAClB,UAAIJ,IAAID,QAAQf,QAAQuB,IAAIP,IAAID,IAAI,EAAG;AAEvC,YAAMS,eAAelB,SAAQD,MAAMI,MAAMM,MAAMK,WAAAA;AAC/C,UAAI,CAACZ,YAAWgB,YAAAA,GAAe;AAC3B5B,YAAI6B,OAAOC,KAAK,uCAAuCV,IAAID,QAAQN,MAAMM,IAAI,KAAKS,YAAAA,EAAc;AAChG;MACJ;AAEA,UAAI;AACA,cAAMG,MAAO,MAAM,OAAOC,cAAcJ,YAAAA,EAAcK;AACtD,cAAMC,WAAWH,IAAII;AACrB,YAAI,CAACD,YAAY,CAACE,MAAMC,QAAQH,SAASR,QAAQ,GAAG;AAChD1B,cAAI6B,OAAOC,KAAK,kBAAkBV,IAAID,QAAQN,MAAMM,IAAI,kCAAkC;AAC1F;QACJ;AACA,mBAAWmB,OAAOJ,SAASR,UAAU;AACjCnB,qBAAWgC,KAAK;YACZC,MAAMF,IAAIE;YACVC,QAAQ;YACRC,YAAYR,SAASf,QAAQC,IAAID;YACjCwB,QAAQL,IAAIK;UAChB,CAAA;QACJ;MACJ,SAASC,KAAK;AACV5C,YAAI6B,OAAOC,KAAK,kBAAkBV,IAAID,QAAQN,MAAMM,IAAI,oBAAqByB,IAAcC,OAAO,EAAE;MACxG;IACJ;EACJ;AAEA,SAAOtC;AACX,GApDoC;;;AXqB7B,IAAMuC,gBAAgB,wBAAiCC,QAAkDA,KAAnF;AAQtB,IAAMC,eAAe,8BAA8CC,YAAAA;AACtE,QAAMC,UAAUC,QAAQC,KAAKC,SAAS,IAAA,KAASF,QAAQC,KAAKC,SAAS,WAAA;AACrE,QAAMC,iBAAiB,OAAOL,QAAQM,WAAW,aAAa,MAAMN,QAAQM,OAAM,IAAKN,QAAQM;AAC/F,QAAMC,MAAM,MAAMC,aAAa;IAC3BF,QAAQD;IACRI,QAAQT,QAAQS;IAChBR;EACJ,CAAA;AAEA,MAAID,QAAQU,WAAWV,QAAQU,QAAQC,SAAS,GAAG;AAC/C,UAAM,EAAEC,2BAAAA,2BAAyB,IAAM,MAAM;AAG7CA,IAAAA,2BAA0BL,KAAKP,QAAQU,OAAO;EAClD;AAEA,QAAMG,UAAU,IAAIC,QAAAA,EACfC,KAAKf,QAAQe,IAAI,EACjBC,YAAYhB,QAAQgB,WAAW,EAC/BC,QAAQjB,QAAQiB,OAAO,EACvBC,OAAO,iBAAiB,0BAA0B,KAAA;AAEvD,QAAMC,aAAkCnB,QAAQoB,SAASC,IAAIC,CAAAA,OAAM;IAAE,GAAGA;IAAGC,QAAQ;EAAgB,EAAA;AAEnG,MAAIvB,QAAQwB,UAAUxB,QAAQwB,OAAOb,SAAS,KAAKX,QAAQyB,sBAAsB,MAAM;AACnF,UAAMC,aAAa1B,QAAQyB,qBAAqB;MAAC;;AACjD,UAAME,iBAAiBR,WAAWS,KAAKN,CAAAA,MAAKA,EAAEO,KAAKC,KAAK,GAAA,MAASJ,WAAWI,KAAK,GAAA,CAAA;AACjF,QAAI,CAACH,gBAAgB;AACjBR,iBAAWY,KAAK;QACZF,MAAMH;QACNH,QAAQ;QACRS,QAAQC,mBAAmBjC,QAAQwB,MAAM;MAC7C,CAAA;IACJ;EACJ;AAEA,MAAIxB,QAAQkC,SAASC,WAAW;AAC5B,UAAMC,gBAAwC;MAC1C,GAAGpC,QAAQkC,QAAQC;MACnBE,UAAUrC,QAAQkC,QAAQC,UAAUE,YAAY9B,IAAI+B,MAAMD;IAC9D;AACA,UAAMH,UAAU,MAAMK,qBAAqBhC,KAAK6B,aAAAA;AAChDjB,eAAWY,KAAI,GAAIG,OAAAA;EACvB;AAEAM,mBAAiB3B,SAASM,YAAYZ,GAAAA;AAEtC,SAAO;IACHkC,KAAK,8BAAOtC,OAAOD,QAAQC,SAAI;AAC3B,UAAI;AACA,cAAMU,QAAQ6B,WAAWvC,IAAAA;AACzB,eAAO;MACX,SAASwC,KAAK;AACVpC,YAAIE,OAAOmC,MAAOD,IAAcE,OAAO;AACvC,eAAO;MACX;IACJ,GARK;EAST;AACJ,GA1D4B;;;AYhBrB,IAAMC,SAAS,8BAClBC,KACAC,SACAC,SAAAA;AAEA,QAAMC,eAAeF,QAAQE,gBAAgB;AAC7C,QAAMC,cAAcH,QAAQG,eAAe;AAC3C,QAAMC,iBAAiBJ,QAAQI,kBAAkB;AAEjD,MAAIC;AAEJ,QAAMC,UAAyB;IAC3BP;IACAQ,SAAS,8BAAMC,SAAQC,OAAO,MAAMC,QAAQH,QAAQC,IAAAA,CAAAA,GAA3C;IACTG,MAAM,8BAAMH,SAAQC,OAAO,MAAMC,QAAQC,KAAKH,IAAAA,CAAAA,GAAxC;IACNI,UAAU,8BAAMJ,SAAQC,OAAO,MAAMC,QAAQE,SAASJ,IAAAA,CAAAA,GAA5C;IACVK,QAAQ,8BAAML,SAAQC,OAAO,MAAMC,QAAQG,OAAOL,IAAAA,CAAAA,GAA1C;IACRM,aAAa,8BAAMN,SAAQC,OAAO,MAAMC,QAAQI,YAAYN,IAAAA,CAAAA,GAA/C;IACbO,KAAKL,QAAQK;IACbC,SAASN,QAAQM;IACjBC,OAAOC,wBAAAA,YAAAA;AACHb,qBAAea;IACnB,GAFOA;IAGPC,QAAQ,6BAAA;AACJ,YAAM,IAAIC,qBAAAA;IACd,GAFQ;EAGZ;AAEAV,UAAQW,MAAMrB,QAAQsB,KAAK;AAE3B,MAAI;AACA,UAAMC,SAAS,MAAMtB,KAAKK,OAAAA;AAC1BI,YAAQO,MAAMZ,gBAAgBH,YAAAA;AAC9B,WAAO,OAAOqB,WAAW,WAAWA,SAAS;EACjD,SAASC,KAAK;AACV,QAAIA,eAAeJ,sBAAsB;AACrCV,cAAQO,MAAMd,WAAAA;AACd,aAAOC;IACX;AACA,UAAMoB;EACV;AACJ,GAzCsB;","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","wizard","ctx","options","body","successOutro","cancelOutro","cancelExitCode","dynamicOutro","session","confirm","opts","unwrap","prompts","text","password","select","multiselect","log","spinner","outro","message","cancel","PromptCancelledError","intro","title","result","err"]}
@@ -0,0 +1,76 @@
1
+ import { b as CliContext } from '../../types-D4sOs9OQ.js';
2
+ import '@maroonedsoftware/appconfig';
3
+ import 'execa';
4
+
5
+ /** Identifies a single secret slot in the OS keyring. */
6
+ interface KeyringEntryOptions {
7
+ /** Service identifier (e.g. `'my-cli'`). Usually a stable per-app constant. */
8
+ service: string;
9
+ /** Account name under the service (e.g. `'api.key'`). */
10
+ account: string;
11
+ }
12
+ /**
13
+ * A single keyring slot. All operations are safe — they never throw to the
14
+ * caller. Failures (missing peer dep, keyring unavailable, OS denied access)
15
+ * are logged once via `ctx.logger.warn` and surfaced as `null` / `false`.
16
+ */
17
+ interface KeyringEntry {
18
+ /** Returns the stored password, or `null` if missing or on any error. */
19
+ read: () => Promise<string | null>;
20
+ /** Stores `value`. Returns `true` on success, `false` on any error. */
21
+ write: (value: string) => Promise<boolean>;
22
+ /** Removes the entry. Returns `true` if something was removed. */
23
+ delete: () => Promise<boolean>;
24
+ }
25
+ /**
26
+ * Build a `KeyringEntry` backed by `@napi-rs/keyring`. The native module is
27
+ * loaded lazily on first use; when it isn't installed, the returned entry
28
+ * degrades gracefully (`read` → `null`, `write` → `false`, `delete` → `false`)
29
+ * after logging a one-shot warning.
30
+ *
31
+ * Intended for use under the `@maroonedsoftware/johnny5/keyring` subpath, so
32
+ * consumers that don't need keyring access never resolve the peer dependency.
33
+ */
34
+ declare const keyringEntry: (ctx: CliContext, options: KeyringEntryOptions) => KeyringEntry;
35
+
36
+ /** Policy controlling whether `resolveSecret` persists a freshly-prompted value into the supplied keyring. */
37
+ type PromptStorePolicy = 'ask' | 'always' | 'never';
38
+ /** Options for `resolveSecret`. */
39
+ interface ResolveSecretOptions {
40
+ /** Explicit value (e.g. from a `--api-key` flag). Wins over everything else and is never persisted. */
41
+ override?: string;
42
+ /** Process env var names checked in order. The first non-empty value wins. */
43
+ envKeys?: string[];
44
+ /** Keyring entry consulted after env. Omit to skip keyring entirely. */
45
+ keyring?: KeyringEntry;
46
+ /**
47
+ * Interactive prompt invoked when nothing else resolved. Receives the `CliContext`
48
+ * and returns the secret, or `undefined` to abort. Skipping `prompt` makes
49
+ * `resolveSecret` return `null` when no source produced a value.
50
+ */
51
+ prompt?: (ctx: CliContext) => Promise<string | undefined>;
52
+ /**
53
+ * After `prompt` succeeds, whether to persist the value into `keyring`:
54
+ * - `'ask'` (default): confirm with the user before writing.
55
+ * - `'always'`: persist without asking.
56
+ * - `'never'`: never persist.
57
+ *
58
+ * Ignored when `keyring` is omitted.
59
+ */
60
+ promptStore?: PromptStorePolicy;
61
+ /** Used in the confirm message when `promptStore` is `'ask'`. Defaults to `'credential'`. */
62
+ label?: string;
63
+ }
64
+ /**
65
+ * Resolve a secret value by walking a fixed chain: `override` → first non-empty
66
+ * `envKeys` entry → `keyring.read()` → `prompt(ctx)`. When `prompt` produces a
67
+ * value and a `keyring` is supplied, the value is optionally persisted according
68
+ * to `promptStore`.
69
+ *
70
+ * Returns `null` when every step yields nothing (or `prompt` returned
71
+ * `undefined`). Never calls `process.exit` — callers own the "missing
72
+ * credential" policy.
73
+ */
74
+ declare const resolveSecret: (ctx: CliContext, options: ResolveSecretOptions) => Promise<string | null>;
75
+
76
+ export { type KeyringEntry, type KeyringEntryOptions, type PromptStorePolicy, type ResolveSecretOptions, keyringEntry, resolveSecret };
@@ -0,0 +1,104 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/integrations/keyring/entry.ts
5
+ var cachedModule;
6
+ var missingWarned = false;
7
+ var loadKeyring = /* @__PURE__ */ __name(async (ctx) => {
8
+ if (cachedModule !== void 0) return cachedModule;
9
+ try {
10
+ cachedModule = await import("@napi-rs/keyring");
11
+ } catch {
12
+ cachedModule = null;
13
+ if (!missingWarned) {
14
+ missingWarned = true;
15
+ ctx.logger.warn("system keyring is unavailable (@napi-rs/keyring is not installed).");
16
+ }
17
+ }
18
+ return cachedModule;
19
+ }, "loadKeyring");
20
+ var keyringEntry = /* @__PURE__ */ __name((ctx, options) => {
21
+ return {
22
+ read: /* @__PURE__ */ __name(async () => {
23
+ const mod = await loadKeyring(ctx);
24
+ if (!mod) return null;
25
+ try {
26
+ return new mod.Entry(options.service, options.account).getPassword();
27
+ } catch (err) {
28
+ ctx.logger.warn(`system keyring read failed (${err.message}).`);
29
+ return null;
30
+ }
31
+ }, "read"),
32
+ write: /* @__PURE__ */ __name(async (value) => {
33
+ const mod = await loadKeyring(ctx);
34
+ if (!mod) return false;
35
+ try {
36
+ new mod.Entry(options.service, options.account).setPassword(value);
37
+ return true;
38
+ } catch (err) {
39
+ ctx.logger.warn(`could not persist credential to system keyring (${err.message}).`);
40
+ return false;
41
+ }
42
+ }, "write"),
43
+ delete: /* @__PURE__ */ __name(async () => {
44
+ const mod = await loadKeyring(ctx);
45
+ if (!mod) return false;
46
+ try {
47
+ return new mod.Entry(options.service, options.account).deletePassword();
48
+ } catch {
49
+ return false;
50
+ }
51
+ }, "delete")
52
+ };
53
+ }, "keyringEntry");
54
+
55
+ // src/util/prompts.ts
56
+ import * as clack from "@clack/prompts";
57
+ var prompts = clack;
58
+ var PromptCancelledError = class extends Error {
59
+ static {
60
+ __name(this, "PromptCancelledError");
61
+ }
62
+ constructor() {
63
+ super("prompt cancelled");
64
+ this.name = "PromptCancelledError";
65
+ }
66
+ };
67
+ var unwrap = /* @__PURE__ */ __name((value) => {
68
+ if (clack.isCancel(value)) throw new PromptCancelledError();
69
+ return value;
70
+ }, "unwrap");
71
+
72
+ // src/integrations/keyring/resolve.ts
73
+ var resolveSecret = /* @__PURE__ */ __name(async (ctx, options) => {
74
+ if (options.override && options.override.length > 0) return options.override;
75
+ for (const key of options.envKeys ?? []) {
76
+ const value = ctx.env[key] ?? process.env[key];
77
+ if (value && value.length > 0) return value;
78
+ }
79
+ if (options.keyring) {
80
+ const fromKeyring = await options.keyring.read();
81
+ if (fromKeyring && fromKeyring.length > 0) return fromKeyring;
82
+ }
83
+ if (!options.prompt) return null;
84
+ const prompted = await options.prompt(ctx);
85
+ if (prompted === void 0 || prompted.length === 0) return null;
86
+ if (options.keyring) {
87
+ const policy = options.promptStore ?? "ask";
88
+ const shouldStore = policy === "always" ? true : policy === "never" ? false : await askToPersist(options.label ?? "credential");
89
+ if (shouldStore) await options.keyring.write(prompted);
90
+ }
91
+ return prompted;
92
+ }, "resolveSecret");
93
+ var askToPersist = /* @__PURE__ */ __name(async (label) => {
94
+ const answer = unwrap(await prompts.confirm({
95
+ message: `Save ${label} to system keyring for future runs?`,
96
+ initialValue: true
97
+ }));
98
+ return answer === true;
99
+ }, "askToPersist");
100
+ export {
101
+ keyringEntry,
102
+ resolveSecret
103
+ };
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/integrations/keyring/entry.ts","../../../src/util/prompts.ts","../../../src/integrations/keyring/resolve.ts"],"sourcesContent":["import type { CliContext } from '../../types.js';\n\ntype KeyringModule = typeof import('@napi-rs/keyring');\n\n/** Identifies a single secret slot in the OS keyring. */\nexport interface KeyringEntryOptions {\n /** Service identifier (e.g. `'my-cli'`). Usually a stable per-app constant. */\n service: string;\n /** Account name under the service (e.g. `'api.key'`). */\n account: string;\n}\n\n/**\n * A single keyring slot. All operations are safe — they never throw to the\n * caller. Failures (missing peer dep, keyring unavailable, OS denied access)\n * are logged once via `ctx.logger.warn` and surfaced as `null` / `false`.\n */\nexport interface KeyringEntry {\n /** Returns the stored password, or `null` if missing or on any error. */\n read: () => Promise<string | null>;\n /** Stores `value`. Returns `true` on success, `false` on any error. */\n write: (value: string) => Promise<boolean>;\n /** Removes the entry. Returns `true` if something was removed. */\n delete: () => Promise<boolean>;\n}\n\nlet cachedModule: KeyringModule | null | undefined;\nlet missingWarned = false;\n\nconst loadKeyring = async (ctx: CliContext): Promise<KeyringModule | null> => {\n if (cachedModule !== undefined) return cachedModule;\n try {\n cachedModule = await import('@napi-rs/keyring');\n } catch {\n cachedModule = null;\n if (!missingWarned) {\n missingWarned = true;\n ctx.logger.warn('system keyring is unavailable (@napi-rs/keyring is not installed).');\n }\n }\n return cachedModule;\n};\n\n/**\n * Build a `KeyringEntry` backed by `@napi-rs/keyring`. The native module is\n * loaded lazily on first use; when it isn't installed, the returned entry\n * degrades gracefully (`read` → `null`, `write` → `false`, `delete` → `false`)\n * after logging a one-shot warning.\n *\n * Intended for use under the `@maroonedsoftware/johnny5/keyring` subpath, so\n * consumers that don't need keyring access never resolve the peer dependency.\n */\nexport const keyringEntry = (ctx: CliContext, options: KeyringEntryOptions): KeyringEntry => {\n return {\n read: async () => {\n const mod = await loadKeyring(ctx);\n if (!mod) return null;\n try {\n return new mod.Entry(options.service, options.account).getPassword();\n } catch (err) {\n ctx.logger.warn(`system keyring read failed (${(err as Error).message}).`);\n return null;\n }\n },\n write: async value => {\n const mod = await loadKeyring(ctx);\n if (!mod) return false;\n try {\n new mod.Entry(options.service, options.account).setPassword(value);\n return true;\n } catch (err) {\n ctx.logger.warn(`could not persist credential to system keyring (${(err as Error).message}).`);\n return false;\n }\n },\n delete: async () => {\n const mod = await loadKeyring(ctx);\n if (!mod) return false;\n try {\n return new mod.Entry(options.service, options.account).deletePassword();\n } catch {\n return false;\n }\n },\n };\n};\n\n/** @internal — exported for tests. Resets the lazy-load cache between cases. */\nexport const __resetKeyringCache = (): void => {\n cachedModule = undefined;\n missingWarned = false;\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 } from '../../types.js';\nimport { prompts, unwrap } from '../../util/prompts.js';\nimport type { KeyringEntry } from './entry.js';\n\n/** Policy controlling whether `resolveSecret` persists a freshly-prompted value into the supplied keyring. */\nexport type PromptStorePolicy = 'ask' | 'always' | 'never';\n\n/** Options for `resolveSecret`. */\nexport interface ResolveSecretOptions {\n /** Explicit value (e.g. from a `--api-key` flag). Wins over everything else and is never persisted. */\n override?: string;\n /** Process env var names checked in order. The first non-empty value wins. */\n envKeys?: string[];\n /** Keyring entry consulted after env. Omit to skip keyring entirely. */\n keyring?: KeyringEntry;\n /**\n * Interactive prompt invoked when nothing else resolved. Receives the `CliContext`\n * and returns the secret, or `undefined` to abort. Skipping `prompt` makes\n * `resolveSecret` return `null` when no source produced a value.\n */\n prompt?: (ctx: CliContext) => Promise<string | undefined>;\n /**\n * After `prompt` succeeds, whether to persist the value into `keyring`:\n * - `'ask'` (default): confirm with the user before writing.\n * - `'always'`: persist without asking.\n * - `'never'`: never persist.\n *\n * Ignored when `keyring` is omitted.\n */\n promptStore?: PromptStorePolicy;\n /** Used in the confirm message when `promptStore` is `'ask'`. Defaults to `'credential'`. */\n label?: string;\n}\n\n/**\n * Resolve a secret value by walking a fixed chain: `override` → first non-empty\n * `envKeys` entry → `keyring.read()` → `prompt(ctx)`. When `prompt` produces a\n * value and a `keyring` is supplied, the value is optionally persisted according\n * to `promptStore`.\n *\n * Returns `null` when every step yields nothing (or `prompt` returned\n * `undefined`). Never calls `process.exit` — callers own the \"missing\n * credential\" policy.\n */\nexport const resolveSecret = async (ctx: CliContext, options: ResolveSecretOptions): Promise<string | null> => {\n if (options.override && options.override.length > 0) return options.override;\n\n for (const key of options.envKeys ?? []) {\n const value = ctx.env[key] ?? process.env[key];\n if (value && value.length > 0) return value;\n }\n\n if (options.keyring) {\n const fromKeyring = await options.keyring.read();\n if (fromKeyring && fromKeyring.length > 0) return fromKeyring;\n }\n\n if (!options.prompt) return null;\n const prompted = await options.prompt(ctx);\n if (prompted === undefined || prompted.length === 0) return null;\n\n if (options.keyring) {\n const policy: PromptStorePolicy = options.promptStore ?? 'ask';\n const shouldStore =\n policy === 'always'\n ? true\n : policy === 'never'\n ? false\n : await askToPersist(options.label ?? 'credential');\n if (shouldStore) await options.keyring.write(prompted);\n }\n\n return prompted;\n};\n\nconst askToPersist = async (label: string): Promise<boolean> => {\n const answer = unwrap(await prompts.confirm({ message: `Save ${label} to system keyring for future runs?`, initialValue: true }));\n return answer === true;\n};\n"],"mappings":"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","names":["cachedModule","missingWarned","loadKeyring","ctx","undefined","logger","warn","keyringEntry","options","read","mod","Entry","service","account","getPassword","err","message","write","value","setPassword","delete","deletePassword","clack","prompts","clack","PromptCancelledError","Error","name","unwrap","value","isCancel","resolveSecret","ctx","options","override","length","key","envKeys","value","env","process","keyring","fromKeyring","read","prompt","prompted","undefined","policy","promptStore","shouldStore","askToPersist","label","write","answer","unwrap","prompts","confirm","message","initialValue"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedsoftware/johnny5",
3
- "version": "1.1.2",
3
+ "version": "1.2.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",
@@ -64,6 +64,10 @@
64
64
  "./permissions": {
65
65
  "types": "./dist/integrations/permissions/index.d.ts",
66
66
  "import": "./dist/integrations/permissions/index.js"
67
+ },
68
+ "./keyring": {
69
+ "types": "./dist/integrations/keyring/index.d.ts",
70
+ "import": "./dist/integrations/keyring/index.js"
67
71
  }
68
72
  },
69
73
  "license": "MIT",
@@ -75,16 +79,17 @@
75
79
  "commander": "^14.0.3",
76
80
  "execa": "^9.6.1",
77
81
  "injectkit": "^1.4.1",
78
- "@maroonedsoftware/appconfig": "1.5.1",
79
- "@maroonedsoftware/logger": "1.1.1"
82
+ "@maroonedsoftware/logger": "1.1.1",
83
+ "@maroonedsoftware/appconfig": "1.5.1"
80
84
  },
81
85
  "peerDependencies": {
86
+ "@napi-rs/keyring": "^1.1.6",
82
87
  "ioredis": "^5.10.0",
83
88
  "kysely": "^0.28.0",
84
89
  "pg": "^8.20.0",
85
- "@maroonedsoftware/koa": "2.2.6",
86
90
  "@maroonedsoftware/permissions": "0.2.1",
87
- "@maroonedsoftware/permissions-dsl": "0.4.1"
91
+ "@maroonedsoftware/permissions-dsl": "0.4.1",
92
+ "@maroonedsoftware/koa": "2.2.7"
88
93
  },
89
94
  "peerDependenciesMeta": {
90
95
  "@maroonedsoftware/koa": {
@@ -96,6 +101,9 @@
96
101
  "@maroonedsoftware/permissions-dsl": {
97
102
  "optional": true
98
103
  },
104
+ "@napi-rs/keyring": {
105
+ "optional": true
106
+ },
99
107
  "ioredis": {
100
108
  "optional": true
101
109
  },
@@ -107,16 +115,17 @@
107
115
  }
108
116
  },
109
117
  "devDependencies": {
118
+ "@napi-rs/keyring": "^1.1.6",
110
119
  "@types/pg": "^8.20.0",
111
120
  "ioredis": "^5.10.1",
112
121
  "kysely": "^0.29.2",
113
122
  "pg": "^8.21.0",
114
123
  "tsup": "^8.5.1",
115
124
  "vitest": "^4.1.7",
125
+ "@maroonedsoftware/permissions": "0.2.1",
116
126
  "@maroonedsoftware/permissions-dsl": "0.4.1",
127
+ "@maroonedsoftware/koa": "2.2.7",
117
128
  "@repo/config-eslint": "0.2.1",
118
- "@maroonedsoftware/koa": "2.2.6",
119
- "@maroonedsoftware/permissions": "0.2.1",
120
129
  "@repo/config-typescript": "0.1.0"
121
130
  },
122
131
  "scripts": {