@maroonedsoftware/johnny5 0.1.0 → 1.0.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
@@ -1,21 +1,21 @@
1
1
  # @maroonedsoftware/johnny5
2
2
 
3
- A CLI framework for ServerKit-based applications. Provides:
3
+ A small CLI framework for ServerKit applications. Build a typed [commander](https://github.com/tj/commander.js) program from declarative `CommandModule` objects, plug in a `doctor` health-check runner, discover commands contributed by workspace packages, and (optionally) hand commands a fully-bootstrapped InjectKit container.
4
4
 
5
- - **`createCliApp`** — single-call factory that wires a `commander` program from a list of `CommandModule` definitions.
6
- - **Doctor runner** — built-in `doctor` subcommand backed by a list of `Check` objects, with optional `--fix` auto-remediation.
7
- - **Plugin discovery** workspace packages can register additional commands by declaring `"johnny5": { "commands": "./path.js" }` in their `package.json`; collisions with core commands throw at startup.
8
- - **ServerKit integration** (subpath `/serverkit`) — `bootstrapForCli` runs each `ServerKitModule.setup()` (but not `start()`) and gives commands a scoped InjectKit container via `requireContainer`.
9
- - **Opt-in check libraries** under subpath exports — `/postgres`, `/redis`, `/docker`, `/versions`, `/filesystem`. Each is a tiny module so non-Postgres (or non-Redis, etc.) consumers don't pull the underlying drivers.
5
+ ## Why
6
+
7
+ Most internal CLIs grow the same way: a `commander` skeleton, a sprawling `doctor` script, ad-hoc DI wiring so commands can reuse application services, and a copy of the same Postgres/Redis/Docker liveness checks across every repo. Johnny5 packages those into one composable surface so each repo only writes the commands that are actually unique to it.
10
8
 
11
9
  ## Install
12
10
 
13
11
  ```bash
14
12
  pnpm add @maroonedsoftware/johnny5
15
- # plus whichever optional peers your CLI needs:
16
- pnpm add pg ioredis kysely
13
+ # Optional peers install only what your checks/commands need:
14
+ pnpm add pg ioredis kysely @maroonedsoftware/koa
17
15
  ```
18
16
 
17
+ `pg`, `ioredis`, `kysely`, and `@maroonedsoftware/koa` are declared as optional peers. The Postgres / Redis / ServerKit integrations are lazy-loaded, so packages you don't install are never required at runtime.
18
+
19
19
  ## Quickstart
20
20
 
21
21
  ```ts
@@ -43,7 +43,257 @@ const app = await createCliApp({
43
43
  process.exit(await app.run(process.argv));
44
44
  ```
45
45
 
46
- `my-cli doctor` auto-registers because `checks` is non-empty. `my-cli hello --name alice` invokes the registered command. Add `modules: [...]` to enable the ServerKit integration and use `requireContainer` for commands that need DI-resolved services.
46
+ - `my-cli hello --name alice` invokes the command.
47
+ - `my-cli doctor` is auto-registered because `checks` is non-empty; `my-cli doctor --fix` runs `autoFix` hooks where available.
48
+ - `-v` / `--verbose` is wired up globally and flips `logger.debug` on.
49
+
50
+ ## Defining commands
51
+
52
+ `defineCommand` is an identity helper that lets TypeScript infer the option object from the literal:
53
+
54
+ ```ts
55
+ const greet = defineCommand({
56
+ description: 'greet a person',
57
+ args: [{ name: 'who', description: 'name', required: true }],
58
+ options: [
59
+ { flags: '--loud', description: 'shout', type: 'boolean' },
60
+ { flags: '--times <n>', description: 'repeat', type: 'number', default: 1, envVar: 'GREET_TIMES' },
61
+ ],
62
+ run: async (opts, ctx, args) => {
63
+ const [who] = args;
64
+ const message = opts.loud ? `HI, ${who?.toUpperCase()}!` : `hi, ${who}`;
65
+ for (let i = 0; i < Number(opts.times ?? 1); i++) ctx.logger.info(message);
66
+ },
67
+ });
68
+ ```
69
+
70
+ Key behaviours:
71
+
72
+ - **`path`** registers the command in the CLI tree. `['db', 'migrate']` becomes `my-cli db migrate`; intermediate groups are created on demand.
73
+ - **`envVar`** falls back to `process.env` when a flag isn't supplied on the command line.
74
+ - **`interactive`** runs after parsing only when stdin/stdout are TTYs — use it to prompt for missing options with `prompts` / `unwrap`.
75
+ - **`passthrough: true`** forwards unknown options and extra positional args verbatim, useful for proxying through to a wrapped binary.
76
+ - **Return value** — return a non-zero number from `run` to `process.exit(code)`. Throwing logs the error and exits 1.
77
+
78
+ ## Safety guards
79
+
80
+ Two declarative fields on `CommandModule` let you fence in commands that shouldn't run unchecked:
81
+
82
+ ```ts
83
+ const drop = defineCommand({
84
+ description: 'drop the database',
85
+ dangerous: true, // prompts Y/N in a TTY; refuses without --yes in CI
86
+ allowedEnvironments: ['development', 'staging'], // refuses unless NODE_ENV matches
87
+ run: async (_opts, ctx) => {
88
+ ctx.logger.success('dropped');
89
+ },
90
+ });
91
+ ```
92
+
93
+ - **`dangerous: true`** auto-registers a `-y, --yes` flag (skipped if you already declared one) and runs a confirmation prompt before `run`. In non-TTY contexts `--yes` is required; without it the command exits 1.
94
+ - **`dangerous: { confirm: 'typed', phrase: 'DROP PRODUCTION' }`** requires the user to retype an exact phrase. `phrase` defaults to the full command path (e.g. `db drop`). A custom `message` overrides the prompt text.
95
+ - **`allowedEnvironments: ['development']`** reads `NODE_ENV` from `ctx.env`. Pass the spec form `{ allowed: ['dev'], variable: 'APP_ENV' }` to read a different variable. The guard runs before any dangerous prompt, so a misconfigured environment fails fast.
96
+
97
+ ## CliContext
98
+
99
+ Every command, check, and plugin hook receives the same `CliContext`:
100
+
101
+ | Field | Description |
102
+ | --- | --- |
103
+ | `paths.cwd` | `process.cwd()` at startup. |
104
+ | `paths.repoRoot` | Nearest ancestor containing `pnpm-workspace.yaml`, else `cwd`. |
105
+ | `logger` | ANSI-coloured console logger (`info`/`warn`/`error`/`debug`/`success`). Override via `createCliApp({ logger })`. |
106
+ | `shell` | `execa` wrapper bound to `repoRoot`; `run` returns the result promise, `runStreaming` inherits stdio and returns the exit code. |
107
+ | `config` | An `AppConfig` instance. Defaults to one with only the dotenv provider attached; pass `config` to `createCliApp` for the full builder. |
108
+ | `env` | `process.env`. |
109
+ | `isInteractive()` | True when both stdin and stdout are TTYs. |
110
+
111
+ `buildContext` automatically loads `.env` and `apps/api/.env` from the workspace root into `process.env` before building `AppConfig`. Override the list via `BuildContextOptions.envFiles` if you call `buildContext` directly. Existing env vars are never overwritten; `$VAR` and `${VAR}` references inside unquoted/double-quoted values are expanded.
112
+
113
+ ## The doctor runner
114
+
115
+ A `Check` is a named async function that returns `{ ok, message, fixHint?, autoFix? }`. The runner renders progress as it goes:
116
+
117
+ ```
118
+ Running doctor…
119
+
120
+ node ≥ 22 ✓ Node v22.11.0
121
+ postgres reachable ✗ DATABASE_URL is not set
122
+ → Set DATABASE_URL in your .env.
123
+ docker compose services up ✗ not running: postgres, redis
124
+ ↻ attempting auto-fix… ✓ compose services started
125
+ ```
126
+
127
+ `my-cli doctor --fix` invokes `autoFix` on any failing check that provides one. The exit code is `0` when every check ends green and `1` otherwise.
128
+
129
+ ### Built-in check libraries
130
+
131
+ Each integration is a subpath export so you only import the drivers you actually use.
132
+
133
+ ```ts
134
+ import { nodeVersion, pnpmVersion } from '@maroonedsoftware/johnny5/versions';
135
+ import { envFile, portsFree } from '@maroonedsoftware/johnny5/filesystem';
136
+ import { postgresReachable } from '@maroonedsoftware/johnny5/postgres';
137
+ import { redisReachable } from '@maroonedsoftware/johnny5/redis';
138
+ import { dockerServicesUp } from '@maroonedsoftware/johnny5/docker';
139
+ import { kyselyTableExists } from '@maroonedsoftware/johnny5/kysely';
140
+ import { permissionsSchemaCompiled, permissionsFixturesPass, permissionsModelLoads } from '@maroonedsoftware/johnny5/permissions';
141
+
142
+ const checks = [
143
+ nodeVersion({ min: 22 }),
144
+ pnpmVersion({ expected: '10.24.0' }),
145
+ envFile({ path: '.env', required: ['DATABASE_URL', 'REDIS_HOST'] }),
146
+ portsFree({ ports: [{ port: 3000, label: 'api' }, 5432, 6379] }),
147
+ postgresReachable(), // reads DATABASE_URL from AppConfig / env
148
+ redisReachable({ hostConfigKey: 'REDIS_HOST', portConfigKey: 'REDIS_PORT' }),
149
+ dockerServicesUp({ autoStart: true }), // adds an autoFix that runs `docker compose up -d`
150
+ kyselyTableExists({ db, table: 'relation_tuples' }), // verify a migration-managed table
151
+ permissionsSchemaCompiled(), // .perm files in sync with generated TS
152
+ permissionsFixturesPass({ patterns: ['permissions/**/*.perm.yaml'] }),
153
+ permissionsModelLoads({ loadModel: async () => (await import('./permissions/generated/index.js')).model }),
154
+ ];
155
+ ```
156
+
157
+ The `kysely` and `permissions` subpaths lazy-load their peer deps (`kysely`, `@maroonedsoftware/permissions`, `@maroonedsoftware/permissions-dsl`), so the bundle cost is paid only by the checks you actually wire up.
158
+
159
+ - **`permissionsSchemaCompiled({ configPath? })`** runs `compile()` in dry-run mode and fails if any generated TypeScript would be rewritten or removed. `doctor --fix` performs the real compile.
160
+ - **`permissionsFixturesPass({ patterns })`** evaluates every matched `.perm.yaml` fixture. See `pdsl validate` for the full TAP-style report.
161
+ - **`permissionsModelLoads({ loadModel })`** surfaces duplicate-namespace / unresolved-reference errors from the `AuthorizationModel` constructor at doctor time instead of on the first runtime Check.
162
+ - **`kyselyTableExists({ db, table, schema? })`** asks Kysely's introspection API whether a table exists. Pair with the permissions tuples table, the jobs table, or any other migration-managed schema.
163
+
164
+ Custom checks are just `Check` objects — there's no registration step:
165
+
166
+ ```ts
167
+ const migrationsApplied: Check = {
168
+ name: 'db migrations',
169
+ run: async ctx => {
170
+ const result = await ctx.shell.run('dbmate', ['status']);
171
+ return String(result.stdout).includes('Pending: 0')
172
+ ? { ok: true, message: 'up to date' }
173
+ : { ok: false, message: 'pending migrations', fixHint: 'Run `pnpm db:migrate`.' };
174
+ },
175
+ autoFix: async ctx => {
176
+ const exit = await ctx.shell.runStreaming('dbmate', ['up']);
177
+ return exit === 0 ? { ok: true, message: 'migrated' } : { ok: false, message: `dbmate exited ${exit}` };
178
+ },
179
+ };
180
+ ```
181
+
182
+ Pass `doctorCommandPath: ['health']` to change the subcommand name, or `null` to suppress auto-registration when you want to ship your own.
183
+
184
+ ## ServerKit integration
185
+
186
+ Wire a list of `ServerKitModule`s into the CLI and any command can resolve services from a scoped InjectKit container — without you writing the bootstrap glue.
187
+
188
+ ```ts
189
+ import { createCliApp, defineCommand } from '@maroonedsoftware/johnny5';
190
+ import { requireContainer } from '@maroonedsoftware/johnny5/serverkit';
191
+ import { UserService } from './services/user.service.js';
192
+ import { databaseModule, jobsModule } from './modules.js';
193
+
194
+ const listUsers = defineCommand({
195
+ description: 'list active users',
196
+ run: requireContainer(async (_opts, ctx) => {
197
+ const users = await ctx.container.resolve(UserService).listActive();
198
+ for (const u of users) ctx.logger.info(`${u.id}\t${u.email}`);
199
+ }),
200
+ });
201
+
202
+ const app = await createCliApp({
203
+ name: 'my-cli',
204
+ description: 'example CLI',
205
+ version: '0.0.1',
206
+ config: () => loadAppConfig(),
207
+ modules: [databaseModule, jobsModule],
208
+ commands: [{ path: ['users', 'list'], module: listUsers }],
209
+ });
210
+ ```
211
+
212
+ Behaviour worth knowing:
213
+
214
+ - The container is bootstrapped **lazily** on the first `requireContainer` call. Commands that never touch DI don't pay for it.
215
+ - Each invocation of a wrapped handler gets a **fresh scoped container** via `container.createScopedContainer()`.
216
+ - `module.setup(registry, config)` runs; `module.start(container)` does **not** — CLIs don't want HTTP listeners or job pollers spinning up.
217
+ - The root container survives between handler calls in the same process (handy for composite commands). `module.shutdown` hooks only fire if you call `bootstrapForCli` yourself.
218
+
219
+ For finer control, call `bootstrapForCli({ modules, config })` directly and manage the container/shutdown lifecycle yourself.
220
+
221
+ ## Plugin discovery
222
+
223
+ Workspace packages can contribute commands without the CLI entrypoint knowing about them. In a plugin package's `package.json`:
224
+
225
+ ```json
226
+ {
227
+ "name": "@acme/billing",
228
+ "johnny5": { "commands": "./dist/cli/commands.js" }
229
+ }
230
+ ```
231
+
232
+ The referenced file must default-export a `PluginManifest`:
233
+
234
+ ```ts
235
+ // apps/billing/src/cli/commands.ts
236
+ import type { PluginManifest } from '@maroonedsoftware/johnny5';
237
+ import { reconcile } from './commands/reconcile.js';
238
+
239
+ const manifest: PluginManifest = {
240
+ name: '@acme/billing',
241
+ commands: [{ path: ['billing', 'reconcile'], module: reconcile }],
242
+ };
243
+
244
+ export default manifest;
245
+ ```
246
+
247
+ Enable discovery in the host CLI:
248
+
249
+ ```ts
250
+ await createCliApp({
251
+ /* … */
252
+ plugins: {
253
+ workspace: {
254
+ roots: ['apps', 'packages'], // dirs to scan; default shown
255
+ excludePackages: ['@acme/cli'], // skip the host CLI's own package
256
+ },
257
+ },
258
+ });
259
+ ```
260
+
261
+ Core commands are registered before plugin commands. If a plugin tries to claim a path already owned by core (or by another plugin loaded earlier), `createCliApp` throws with a descriptive error — there's no silent override. Plugins that fail to load are logged through `ctx.logger.warn` and skipped.
262
+
263
+ ## Interactive prompts
264
+
265
+ `@clack/prompts` is re-exported under `prompts`, plus an `unwrap` helper that converts cancellations into a thrown `PromptCancelledError` so command handlers can rely on plain try/catch:
266
+
267
+ ```ts
268
+ import { prompts, unwrap } from '@maroonedsoftware/johnny5';
269
+
270
+ const create = defineCommand({
271
+ description: 'create a user',
272
+ options: [{ flags: '--email <email>', description: 'email address' }],
273
+ interactive: async (_ctx, partial) => ({
274
+ email: partial.email ?? unwrap(await prompts.text({ message: 'Email?' })),
275
+ }),
276
+ run: async (opts, ctx) => {
277
+ ctx.logger.success(`Creating ${opts.email}…`);
278
+ },
279
+ });
280
+ ```
281
+
282
+ `interactive` only runs when both stdin and stdout are TTYs, so the same command works unchanged in CI.
283
+
284
+ ## Exports
285
+
286
+ | Path | Provides |
287
+ | --- | --- |
288
+ | `@maroonedsoftware/johnny5` | `createCliApp`, `defineCommand`, `registerCommands`, `runChecks`, `buildContext`, `buildDefaultAppConfig`, `loadWorkspacePlugins`, `createShell`, `createDefaultLogger`, `prompts`, `unwrap`, `isInteractive`, plus the `Check` / `CommandModule` / `CliContext` types. |
289
+ | `/serverkit` | `bootstrapForCli`, `configureServerKitModules`, `getOrBootstrapContainer`, `requireContainer`. |
290
+ | `/versions` | `nodeVersion`, `pnpmVersion`. |
291
+ | `/filesystem` | `envFile`, `portsFree`. |
292
+ | `/postgres` | `postgresReachable` (lazy-loads `pg`). |
293
+ | `/redis` | `redisReachable` (lazy-loads `ioredis`). |
294
+ | `/docker` | `dockerServicesUp`. |
295
+ | `/kysely` | `kyselyTableExists` (lazy-loads `kysely`). |
296
+ | `/permissions` | `permissionsSchemaCompiled`, `permissionsFixturesPass`, `permissionsModelLoads` (lazy-load `@maroonedsoftware/permissions[-dsl]`). |
47
297
 
48
298
  ## License
49
299
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AppConfig } from '@maroonedsoftware/appconfig';
2
- import { C as CliContext, D as DiscoveredCommand, a as CommandRegistration, b as Check, c as CliLogger, d as CommandModule } from './types-CH7ccr3j.js';
3
- export { A as ArgSpec, e as CheckResult, f as CliPaths, g as CreateLoggerOptions, O as OptionSpec, h as OptionType, P as PluginManifest, S as Shell, i as ShellOptions, j as createDefaultLogger, k as createShell } from './types-CH7ccr3j.js';
2
+ import { C as CliContext, D as DiscoveredCommand, a as CommandRegistration, b as Check, c as CliLogger, d as CommandModule } from './types-DBGyauec.js';
3
+ export { A as ArgSpec, e as CheckResult, f as CliPaths, g as CreateLoggerOptions, h as DangerousSpec, E as EnvironmentGuardSpec, O as OptionSpec, i as OptionType, P as PluginManifest, S as Shell, j as ShellOptions, k as createDefaultLogger, l as createShell } from './types-DBGyauec.js';
4
4
  import { Command } from 'commander';
5
5
  import * as clack from '@clack/prompts';
6
6
  import 'execa';
package/dist/index.js CHANGED
@@ -90,6 +90,76 @@ var init_serverkit = __esm({
90
90
  // src/app.ts
91
91
  import { Command } from "commander";
92
92
 
93
+ // src/util/prompts.ts
94
+ import * as clack from "@clack/prompts";
95
+ var prompts = clack;
96
+ var PromptCancelledError = class extends Error {
97
+ static {
98
+ __name(this, "PromptCancelledError");
99
+ }
100
+ constructor() {
101
+ super("prompt cancelled");
102
+ this.name = "PromptCancelledError";
103
+ }
104
+ };
105
+ var unwrap = /* @__PURE__ */ __name((value) => {
106
+ if (clack.isCancel(value)) throw new PromptCancelledError();
107
+ return value;
108
+ }, "unwrap");
109
+
110
+ // src/commander/safety.ts
111
+ var resolveEnvGuard = /* @__PURE__ */ __name((mod) => {
112
+ if (!mod.allowedEnvironments) return null;
113
+ if (Array.isArray(mod.allowedEnvironments)) return {
114
+ allowed: mod.allowedEnvironments
115
+ };
116
+ return mod.allowedEnvironments;
117
+ }, "resolveEnvGuard");
118
+ var resolveDangerous = /* @__PURE__ */ __name((mod) => {
119
+ if (!mod.dangerous) return null;
120
+ if (mod.dangerous === true) return {};
121
+ return mod.dangerous;
122
+ }, "resolveDangerous");
123
+ var hasYesOption = /* @__PURE__ */ __name((mod) => (mod.options ?? []).some((o) => /(^|[\s,])(-y|--yes)([\s,]|$)/.test(o.flags)), "hasYesOption");
124
+ var checkEnvironmentGuard = /* @__PURE__ */ __name((mod, ctx, pathLabel) => {
125
+ const guard = resolveEnvGuard(mod);
126
+ if (!guard) return true;
127
+ const variable = guard.variable ?? "NODE_ENV";
128
+ const current = ctx.env[variable];
129
+ if (current !== void 0 && guard.allowed.includes(current)) return true;
130
+ const shown = current === void 0 ? "(unset)" : current;
131
+ ctx.logger.error(`Refusing to run "${pathLabel}" with ${variable}=${shown}. Allowed: ${guard.allowed.join(", ")}.`);
132
+ return false;
133
+ }, "checkEnvironmentGuard");
134
+ var confirmDangerous = /* @__PURE__ */ __name(async (mod, ctx, pathLabel, userOptedIn) => {
135
+ const spec = resolveDangerous(mod);
136
+ if (!spec) return true;
137
+ if (userOptedIn) return true;
138
+ if (!ctx.isInteractive()) {
139
+ ctx.logger.error(`"${pathLabel}" is destructive; pass --yes to confirm in non-interactive mode.`);
140
+ return false;
141
+ }
142
+ if (spec.confirm === "typed") {
143
+ const phrase = spec.phrase ?? pathLabel;
144
+ const result2 = await prompts.text({
145
+ message: spec.message ?? `This is destructive. Type "${phrase}" to continue:`
146
+ });
147
+ if (prompts.isCancel(result2)) return false;
148
+ if (result2 !== phrase) {
149
+ ctx.logger.warn("Confirmation did not match \u2014 aborting.");
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+ const result = await prompts.confirm({
155
+ message: spec.message ?? `Run destructive command "${pathLabel}"?`,
156
+ initialValue: false
157
+ });
158
+ if (prompts.isCancel(result)) return false;
159
+ return result === true;
160
+ }, "confirmDangerous");
161
+ var needsYesOption = /* @__PURE__ */ __name((mod) => Boolean(mod.dangerous) && !hasYesOption(mod), "needsYesOption");
162
+
93
163
  // src/commander/register.ts
94
164
  var applyOption = /* @__PURE__ */ __name((cmd, spec) => {
95
165
  if (spec.required) {
@@ -115,8 +185,9 @@ var deriveOptionKey = /* @__PURE__ */ __name((flags) => {
115
185
  const stripped = target.replace(/^-+/, "");
116
186
  return stripped.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
117
187
  }, "deriveOptionKey");
118
- var attachLeaf = /* @__PURE__ */ __name((parent, leafName, mod, ctx, sourceLabel) => {
188
+ var attachLeaf = /* @__PURE__ */ __name((parent, leafName, mod, ctx, sourceLabel, fullPath) => {
119
189
  const cmd = parent.command(leafName).description(mod.description);
190
+ const pathLabel = fullPath.join(" ");
120
191
  for (const arg of mod.args ?? []) {
121
192
  const argName = arg.variadic ? `${arg.name}...` : arg.name;
122
193
  if (arg.required) cmd.argument(`<${argName}>`, arg.description);
@@ -125,6 +196,7 @@ var attachLeaf = /* @__PURE__ */ __name((parent, leafName, mod, ctx, sourceLabel
125
196
  for (const opt of mod.options ?? []) {
126
197
  applyOption(cmd, opt);
127
198
  }
199
+ if (needsYesOption(mod)) cmd.option("-y, --yes", "Skip confirmation prompt for this destructive command", false);
128
200
  if (mod.passthrough) cmd.allowUnknownOption(true).allowExcessArguments(true);
129
201
  cmd.action(async (...allArgs) => {
130
202
  const commandInstance = allArgs[allArgs.length - 1];
@@ -141,6 +213,17 @@ var attachLeaf = /* @__PURE__ */ __name((parent, leafName, mod, ctx, sourceLabel
141
213
  opts[key] = process.env[optSpec.envVar];
142
214
  }
143
215
  }
216
+ if (!checkEnvironmentGuard(mod, ctx, pathLabel)) {
217
+ process.exit(1);
218
+ return;
219
+ }
220
+ if (mod.dangerous) {
221
+ const proceed = await confirmDangerous(mod, ctx, pathLabel, opts["yes"] === true);
222
+ if (!proceed) {
223
+ process.exit(1);
224
+ return;
225
+ }
226
+ }
144
227
  let finalOpts = opts;
145
228
  if (mod.interactive && ctx.isInteractive()) {
146
229
  finalOpts = await mod.interactive(ctx, opts);
@@ -182,7 +265,7 @@ var registerCommands = /* @__PURE__ */ __name((program, discovered, ctx) => {
182
265
  const leafName = entry.path[entry.path.length - 1];
183
266
  if (!leafName) continue;
184
267
  const sourceLabel = entry.source === "plugin" ? entry.sourceName ?? "plugin" : "core";
185
- attachLeaf(parent, leafName, entry.module, ctx, sourceLabel);
268
+ attachLeaf(parent, leafName, entry.module, ctx, sourceLabel, entry.path);
186
269
  }
187
270
  }, "registerCommands");
188
271
 
@@ -470,23 +553,6 @@ var createCliApp = /* @__PURE__ */ __name(async (options) => {
470
553
  }, "run")
471
554
  };
472
555
  }, "createCliApp");
473
-
474
- // src/util/prompts.ts
475
- import * as clack from "@clack/prompts";
476
- var prompts = clack;
477
- var PromptCancelledError = class extends Error {
478
- static {
479
- __name(this, "PromptCancelledError");
480
- }
481
- constructor() {
482
- super("prompt cancelled");
483
- this.name = "PromptCancelledError";
484
- }
485
- };
486
- var unwrap = /* @__PURE__ */ __name((value) => {
487
- if (clack.isCancel(value)) throw new PromptCancelledError();
488
- return value;
489
- }, "unwrap");
490
556
  export {
491
557
  PromptCancelledError,
492
558
  buildContext,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrations/serverkit/index.ts","../src/app.ts","../src/commander/register.ts","../src/context.ts","../src/util/logger.ts","../src/util/shell.ts","../src/util/tty.ts","../src/doctor/runner.ts","../src/plugin/workspace.loader.ts","../src/util/prompts.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 { Command } from 'commander';\nimport type { CliContext, CommandModule, DiscoveredCommand, OptionSpec } from '../types.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): void => {\n const cmd = parent.command(leafName).description(mod.description);\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 (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 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);\n }\n};\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { AppConfigBuilder, AppConfigProviderDotenv, type AppConfig } from '@maroonedsoftware/appconfig';\nimport type { CliContext, CliPaths } from './types.js';\nimport type { CliLogger } from './util/logger.js';\nimport { createDefaultLogger } from './util/logger.js';\nimport { createShell } from './util/shell.js';\nimport { isInteractive } from './util/tty.js';\n\n/** Options accepted by `buildContext`. */\nexport interface BuildContextOptions {\n config?: AppConfig;\n logger?: CliLogger;\n verbose?: boolean;\n repoRoot?: string;\n /**\n * Paths to .env files (absolute, or relative to the resolved repoRoot) to\n * load into process.env before building AppConfig. Missing files are\n * silently skipped. Existing process.env values are not overridden.\n * Defaults to ['.env', 'apps/api/.env'].\n */\n envFiles?: string[];\n}\n\nconst findRepoRoot = (start: string): string => {\n let dir = start;\n for (let i = 0; i < 12; i++) {\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n};\n\n// Expands `$VAR` and `${VAR}` references against process.env. Matches the\n// behaviour of dotenv-expand so .env files authored for dbmate/docker-compose\n// (where placeholders are common) still produce usable runtime values.\nconst expandValue = (value: string): string =>\n value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}|\\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced: string | undefined, bare: string | undefined) => {\n const key = (braced ?? bare) as string;\n return process.env[key] ?? '';\n });\n\nconst loadEnvFile = (path: string): void => {\n if (!existsSync(path)) return;\n for (const line of readFileSync(path, 'utf-8').split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eqIdx = trimmed.indexOf('=');\n if (eqIdx === -1) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n const rawValue = trimmed.slice(eqIdx + 1).trim();\n\n // Detect quoting style before unwrapping. Single-quoted values are\n // taken literally; double-quoted and unquoted values get $VAR\n // expansion against the current process.env.\n const singleQuoted = rawValue.startsWith(\"'\") && rawValue.endsWith(\"'\");\n const doubleQuoted = rawValue.startsWith('\"') && rawValue.endsWith('\"');\n let value = singleQuoted || doubleQuoted ? rawValue.slice(1, -1) : rawValue;\n if (!singleQuoted) value = expandValue(value);\n\n if (!(key in process.env)) process.env[key] = value;\n }\n};\n\n/**\n * Build an AppConfig with only the dotenv provider attached. Callers are\n * expected to have loaded .env files into `process.env` beforehand — see\n * `buildContext` for the default loading sequence.\n */\nexport const buildDefaultAppConfig = async (): Promise<AppConfig> =>\n new AppConfigBuilder().addProvider(new AppConfigProviderDotenv()).build();\n\n/**\n * Build the `CliContext` handed to every command, check, and plugin hook. Loads\n * `.env` files into `process.env`, resolves the workspace `repoRoot`, and wires\n * up shell, logger, and config.\n */\nexport const buildContext = async (options: BuildContextOptions = {}): Promise<CliContext> => {\n // Start from cwd so consumers linked from a sibling repo (or installed\n // from npm into node_modules) still resolve to the CONSUMER's workspace\n // root rather than wherever johnny5 itself happens to live.\n const cwd = process.cwd();\n const repoRoot = options.repoRoot ?? findRepoRoot(cwd);\n const paths: CliPaths = { cwd, repoRoot };\n\n for (const envFile of options.envFiles ?? ['.env', 'apps/api/.env']) {\n const absolute = envFile.startsWith('/') ? envFile : resolve(repoRoot, envFile);\n loadEnvFile(absolute);\n }\n\n const logger = options.logger ?? createDefaultLogger({ verbose: options.verbose });\n const shell = createShell(repoRoot, logger);\n const config = options.config ?? (await buildDefaultAppConfig());\n\n return {\n paths,\n logger,\n shell,\n config,\n env: process.env,\n isInteractive,\n };\n};\n","/** Minimal logger interface that every command and check receives via `CliContext`. */\nexport interface CliLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n debug: (msg: string) => void;\n success: (msg: string) => void;\n}\n\nconst colour = (code: number, text: string): string => `\\x1b[${code}m${text}\\x1b[0m`;\n\n/** Options accepted by `createDefaultLogger`. */\nexport interface CreateLoggerOptions {\n /** When true, `debug` writes to stdout; otherwise it's a no-op. */\n verbose?: boolean;\n}\n\n/**\n * Build the default ANSI-coloured console logger used when a consumer doesn't\n * supply their own. `debug` output is gated on `verbose`.\n */\nexport const createDefaultLogger = (options: CreateLoggerOptions = {}): CliLogger => ({\n info: msg => console.log(msg),\n warn: msg => console.warn(colour(33, `! ${msg}`)),\n error: msg => console.error(colour(31, `✗ ${msg}`)),\n success: msg => console.log(colour(32, `✓ ${msg}`)),\n debug: msg => {\n if (options.verbose) console.log(colour(90, `· ${msg}`));\n },\n});\n","import { execa, type Options as ExecaOptions, type ResultPromise } from 'execa';\nimport type { CliLogger } from './logger.js';\n\n/** Execa options re-typed to require a string `cwd` at the call site. */\nexport interface ShellOptions extends ExecaOptions {\n cwd?: string;\n}\n\n/** Tiny shell wrapper around execa exposed on `CliContext.shell`. */\nexport interface Shell {\n /** Run a command, returning the execa result promise. Use this when the caller needs stdout/stderr. */\n run: (command: string, args: string[], options?: ShellOptions) => ResultPromise;\n /** Run a command with inherited stdio, returning the exit code. Failures don't throw — the exit code is returned instead. */\n runStreaming: (command: string, args: string[], options?: ShellOptions) => Promise<number>;\n}\n\n/** Build a `Shell` bound to `cwd`, logging streaming invocations through `logger.debug`. */\nexport const createShell = (cwd: string, logger: CliLogger): Shell => ({\n run: (command, args, options) => execa(command, args, { cwd, ...options }),\n runStreaming: async (command, args, options) => {\n logger.debug(`$ ${command} ${args.join(' ')}`);\n const child = execa(command, args, { cwd, stdio: 'inherit', reject: false, ...options });\n const result = await child;\n return result.exitCode ?? 0;\n },\n});\n","/**\n * Best-effort guess at whether the CLI is talking to a human. Returns false in\n * CI (`CI=true` / `CI=1`), when `JOHNNY5_NON_INTERACTIVE=1`, or when either of\n * stdout/stdin isn't a TTY.\n */\nexport const isInteractive = (): boolean => {\n if (process.env['CI'] === 'true' || process.env['CI'] === '1') return false;\n if (process.env['JOHNNY5_NON_INTERACTIVE'] === '1') return false;\n return Boolean(process.stdout.isTTY && process.stdin.isTTY);\n};\n","import type { Check, CheckResult, CliContext, CommandModule } from '../types.js';\n\n/** Options passed to `runChecks`. */\nexport interface DoctorOptions {\n /** When true, failing checks with an `autoFix` hook get a chance to remediate. */\n fix?: boolean;\n}\n\n/**\n * Run a list of doctor checks sequentially, rendering progress to stdout. Returns\n * a process exit code: `0` when every check passes (including via `autoFix` when\n * `--fix` is supplied), `1` when at least one check fails.\n */\nexport const runChecks = async (ctx: CliContext, checks: Check[], options: DoctorOptions): Promise<number> => {\n ctx.logger.info('Running doctor…\\n');\n\n let failed = 0;\n let fixed = 0;\n\n for (const check of checks) {\n process.stdout.write(` ${check.name.padEnd(36, ' ')} `);\n let result: CheckResult;\n try {\n result = await check.run(ctx);\n } catch (err) {\n result = { ok: false, message: `threw: ${(err as Error).message}` };\n }\n\n if (result.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${result.message}\\n`);\n continue;\n }\n\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${result.message}\\n`);\n\n if (options.fix && check.autoFix) {\n process.stdout.write(` ↻ attempting auto-fix… `);\n try {\n const fixResult = await check.autoFix(ctx);\n if (fixResult.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${fixResult.message}\\n`);\n fixed++;\n continue;\n }\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${fixResult.message}\\n`);\n } catch (err) {\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${(err as Error).message}\\n`);\n }\n } else if (result.fixHint) {\n process.stdout.write(` → ${result.fixHint}\\n`);\n }\n failed++;\n }\n\n process.stdout.write('\\n');\n if (failed === 0) {\n ctx.logger.success('All checks passed.');\n return 0;\n }\n if (options.fix && fixed > 0) {\n ctx.logger.info(`Auto-fixed ${fixed} issue(s); re-run \\`doctor\\` to confirm.`);\n }\n ctx.logger.error(`${failed} check(s) failed.`);\n return 1;\n};\n\n/**\n * Build the `CommandModule` for the built-in `doctor` subcommand from a set of\n * checks. `createCliApp` auto-registers this when `checks` is non-empty.\n */\nexport const buildDoctorCommand = (checks: Check[]): CommandModule<{ fix?: boolean }> => ({\n description: 'Run local-dev health checks',\n options: [{ flags: '--fix', description: 'Attempt auto-remediation for checks that support it' }],\n run: async (opts, ctx) => runChecks(ctx, checks, { fix: opts.fix === true }),\n});\n","import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { CliContext, DiscoveredCommand, PluginManifest } from '../types.js';\n\ninterface WorkspacePackageJson {\n name?: string;\n johnny5?: {\n commands?: string;\n };\n}\n\n/** Options accepted by `loadWorkspacePlugins`. */\nexport interface WorkspacePluginOptions {\n repoRoot: string;\n /**\n * Workspace-relative directories whose immediate children are scanned for\n * `package.json` files. Defaults to `['apps', 'packages']`.\n */\n roots?: string[];\n /**\n * Package names to skip — typically the consumer's own CLI package whose\n * commands are loaded directly, not via plugin discovery.\n */\n excludePackages?: string[];\n}\n\n/**\n * Scan every workspace package in the configured roots for a `\"johnny5\"` field\n * in `package.json`. When present, the referenced file is dynamically imported\n * and expected to default-export a `PluginManifest`. Failures to load a single\n * plugin log a warning through `ctx.logger.warn` but don't abort the CLI.\n */\nexport const loadWorkspacePlugins = async (ctx: CliContext, options: WorkspacePluginOptions): Promise<DiscoveredCommand[]> => {\n const rootDirs = options.roots ?? ['apps', 'packages'];\n const exclude = new Set(options.excludePackages ?? []);\n const discovered: DiscoveredCommand[] = [];\n\n for (const rootRel of rootDirs) {\n const root = resolve(options.repoRoot, rootRel);\n if (!existsSync(root)) continue;\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const pkgPath = join(root, entry.name, 'package.json');\n if (!existsSync(pkgPath)) continue;\n\n let pkg: WorkspacePackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as WorkspacePackageJson;\n } catch {\n continue;\n }\n\n const commandsRel = pkg.johnny5?.commands;\n if (!commandsRel) continue;\n if (pkg.name && exclude.has(pkg.name)) continue;\n\n const manifestPath = resolve(root, entry.name, commandsRel);\n if (!existsSync(manifestPath)) {\n ctx.logger.warn(`johnny5 plugin manifest missing for ${pkg.name ?? entry.name}: ${manifestPath}`);\n continue;\n }\n\n try {\n const mod = (await import(pathToFileURL(manifestPath).href)) as { default: PluginManifest };\n const manifest = mod.default;\n if (!manifest || !Array.isArray(manifest.commands)) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} has no commands array; skipping`);\n continue;\n }\n for (const cmd of manifest.commands) {\n discovered.push({\n path: cmd.path,\n source: 'plugin',\n sourceName: manifest.name ?? pkg.name,\n module: cmd.module,\n });\n }\n } catch (err) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} failed to load: ${(err as Error).message}`);\n }\n }\n }\n\n return discovered;\n};\n","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"],"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","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","arg","args","argName","variadic","argument","opt","options","passthrough","allowUnknownOption","allowExcessArguments","action","allArgs","commandInstance","length","opts","positional","slice","positionalStrings","flatMap","p","Array","isArray","map","String","passthroughArgs","optSpec","envVar","key","process","env","finalOpts","interactive","isInteractive","exitCode","run","exit","err","logger","error","message","stack","debug","registerCommands","program","discovered","registeredPaths","Map","sorted","sort","a","b","source","entry","path","join","get","incoming","sourceName","owner","Error","set","segment","module","existsSync","readFileSync","dirname","resolve","AppConfigBuilder","AppConfigProviderDotenv","colour","code","text","createDefaultLogger","options","info","msg","console","log","warn","error","success","debug","verbose","execa","createShell","cwd","logger","run","command","args","options","execa","runStreaming","debug","join","child","stdio","reject","result","exitCode","isInteractive","process","env","Boolean","stdout","isTTY","stdin","findRepoRoot","start","dir","i","existsSync","resolve","parent","dirname","process","cwd","expandValue","value","replace","_","braced","bare","key","env","loadEnvFile","path","line","readFileSync","split","trimmed","trim","startsWith","eqIdx","indexOf","slice","rawValue","singleQuoted","endsWith","doubleQuoted","buildDefaultAppConfig","AppConfigBuilder","addProvider","AppConfigProviderDotenv","build","buildContext","options","repoRoot","paths","envFile","envFiles","absolute","logger","createDefaultLogger","verbose","shell","createShell","config","isInteractive","runChecks","ctx","checks","options","logger","info","failed","fixed","check","process","stdout","write","name","padEnd","result","run","err","ok","message","fix","autoFix","fixResult","fixHint","success","error","buildDoctorCommand","description","flags","opts","existsSync","readFileSync","readdirSync","join","resolve","pathToFileURL","loadWorkspacePlugins","ctx","options","rootDirs","roots","exclude","Set","excludePackages","discovered","rootRel","root","resolve","repoRoot","existsSync","entry","readdirSync","withFileTypes","isDirectory","pkgPath","join","name","pkg","JSON","parse","readFileSync","commandsRel","johnny5","commands","has","manifestPath","logger","warn","mod","pathToFileURL","href","manifest","default","Array","isArray","cmd","push","path","source","sourceName","module","err","message","defineCommand","mod","createCliApp","options","verbose","process","argv","includes","resolvedConfig","config","ctx","buildContext","logger","modules","length","configureServerKitModules","program","Command","name","description","version","option","discovered","commands","map","c","source","checks","doctorCommandPath","doctorPath","alreadyDefined","some","path","join","push","module","buildDoctorCommand","plugins","workspace","workspaceOpts","repoRoot","paths","loadWorkspacePlugins","registerCommands","run","parseAsync","err","error","message","clack","prompts","clack","PromptCancelledError","Error","name","unwrap","value","isCancel"]}
1
+ {"version":3,"sources":["../src/integrations/serverkit/index.ts","../src/app.ts","../src/util/prompts.ts","../src/commander/safety.ts","../src/commander/register.ts","../src/context.ts","../src/util/logger.ts","../src/util/shell.ts","../src/util/tty.ts","../src/doctor/runner.ts","../src/plugin/workspace.loader.ts"],"sourcesContent":["import { InjectKitRegistry, type Container, type ScopedContainer } from 'injectkit';\nimport { AppConfig } from '@maroonedsoftware/appconfig';\nimport { ConsoleLogger, Logger } from '@maroonedsoftware/logger';\nimport type { ServerKitModule } from '@maroonedsoftware/koa';\nimport type { CliContext, CommandModule } from '../../types.js';\n\n/** Options accepted by `bootstrapForCli`. */\nexport interface BootstrapForCliOptions<ConfigT extends AppConfig = AppConfig> {\n modules: ServerKitModule<ConfigT>[];\n config: ConfigT;\n logger?: Logger;\n}\n\n/** An InjectKit container and a `shutdown` hook that runs every module's `shutdown` in reverse order. */\nexport interface CliContainer {\n container: Container;\n shutdown: () => Promise<void>;\n}\n\n/**\n * Run each `module.setup(registry, config)` and build the InjectKit container.\n * Deliberately does NOT call `module.start()` — CLIs don't want background work\n * (HTTP listeners, job pollers) spinning up. Module `shutdown` hooks are\n * invoked when the returned `shutdown` is called.\n */\nexport const bootstrapForCli = async <ConfigT extends AppConfig = AppConfig>(\n options: BootstrapForCliOptions<ConfigT>,\n): Promise<CliContainer> => {\n const registry = new InjectKitRegistry();\n\n registry.register(Logger).useInstance(options.logger ?? new ConsoleLogger());\n registry.register(AppConfig).useInstance(options.config);\n\n for (const module of options.modules) {\n if (module.setup) await module.setup(registry, options.config);\n }\n\n const container = registry.build();\n\n const shutdown = async (): Promise<void> => {\n for (const module of [...options.modules].reverse()) {\n if (!module.shutdown) continue;\n try {\n await module.shutdown(container);\n } catch {\n // Ignore individual module shutdown failures during teardown.\n }\n }\n };\n\n return { container, shutdown };\n};\n\n// Lazy, per-process bootstrap cache. Composite commands within a single\n// invocation reuse the same container; subsequent invocations bootstrap fresh.\ninterface LazyBootstrap<ConfigT extends AppConfig> {\n modules: ServerKitModule<ConfigT>[];\n promise?: Promise<CliContainer>;\n}\n\n// State must live on globalThis under a Symbol.for key so that the main johnny5\n// bundle and the /serverkit subpath bundle share it. tsup with `splitting:\n// false` builds each entry independently, so module-scoped state would be\n// duplicated — createCliApp would write to one copy and requireContainer would\n// read from another. Symbol.for makes the WeakMap process-wide regardless of\n// which bundle initialised it first.\nconst STATE_KEY = Symbol.for('@maroonedsoftware/johnny5/serverkit/state.v1');\n\ninterface Johnny5ServerkitState {\n containerByContext: WeakMap<CliContext, LazyBootstrap<AppConfig>>;\n}\n\nconst getState = (): Johnny5ServerkitState => {\n const g = globalThis as unknown as Record<symbol, Johnny5ServerkitState | undefined>;\n if (!g[STATE_KEY]) {\n g[STATE_KEY] = { containerByContext: new WeakMap() };\n }\n return g[STATE_KEY] as Johnny5ServerkitState;\n};\n\n/**\n * Associate a list of ServerKit modules with a `CliContext`. The first call to\n * `getOrBootstrapContainer` for that context will lazily run their `setup`\n * hooks. `createCliApp` calls this automatically when `modules` is supplied.\n */\nexport const configureServerKitModules = <ConfigT extends AppConfig>(ctx: CliContext, modules: ServerKitModule<ConfigT>[]): void => {\n getState().containerByContext.set(ctx, { modules: modules as ServerKitModule<AppConfig>[] });\n};\n\n/**\n * Return the bootstrapped container for `ctx`, building it on the first call\n * and caching the promise for subsequent calls within the same process.\n * Throws if `configureServerKitModules` hasn't been called for this context.\n */\nexport const getOrBootstrapContainer = async (ctx: CliContext): Promise<CliContainer> => {\n const lazy = getState().containerByContext.get(ctx);\n if (!lazy) throw new Error('ServerKit modules have not been configured on this CliContext — call configureServerKitModules() in createCliApp first.');\n if (!lazy.promise) {\n lazy.promise = bootstrapForCli({\n modules: lazy.modules,\n config: ctx.config,\n });\n }\n return lazy.promise;\n};\n\n/** `CliContext` augmented with a scoped InjectKit container, handed to `requireContainer` handlers. */\nexport interface RequireContainerCtx extends CliContext {\n container: ScopedContainer;\n}\n\n/**\n * Wrap a command handler so it lazily bootstraps the ServerKit container and\n * receives a fresh scoped container per invocation. The root container is NOT\n * shut down between commands within the same process — call `bootstrapForCli`\n * directly when explicit teardown is required.\n */\nexport const requireContainer = <Opts = Record<string, unknown>>(\n handler: (opts: Opts, ctx: RequireContainerCtx, args: string[]) => Promise<number | void>,\n): CommandModule<Opts>['run'] => {\n return async (opts, ctx, args) => {\n const { container } = await getOrBootstrapContainer(ctx);\n const scoped = container.createScopedContainer() as ScopedContainer;\n const enriched: RequireContainerCtx = Object.assign({}, ctx, { container: scoped });\n return handler(opts, enriched, args);\n };\n};\n","import { Command } from 'commander';\nimport type { AppConfig } from '@maroonedsoftware/appconfig';\nimport type { Check, CliContext, CommandModule, CommandRegistration, DiscoveredCommand } from './types.js';\nimport { registerCommands } from './commander/register.js';\nimport { buildContext } from './context.js';\nimport { buildDoctorCommand } from './doctor/runner.js';\nimport { loadWorkspacePlugins, type WorkspacePluginOptions } from './plugin/workspace.loader.js';\nimport type { CliLogger } from './util/logger.js';\n\n// Opaque ServerKit module shape — the concrete `ServerKitModule` type lives in\n// `@maroonedsoftware/koa`. Importing it here would force every johnny5 consumer\n// to pull koa as a hard dep even when not using ServerKit. The serverkit\n// integration is responsible for the actual setup() / shutdown() calls.\ninterface ServerKitModuleLike<ConfigT> {\n name?: string;\n setup?: (registry: unknown, config: ConfigT) => Promise<void>;\n start?: (container: unknown) => Promise<void>;\n shutdown?: (container: unknown) => Promise<void>;\n}\n\n/** Options accepted by `createCliApp`. */\nexport interface CliAppOptions<ConfigT extends AppConfig = AppConfig> {\n name: string;\n description: string;\n version: string;\n commands: CommandRegistration[];\n checks?: Check[];\n config?: ConfigT | (() => Promise<ConfigT>);\n logger?: CliLogger;\n // ServerKit modules to bootstrap lazily for commands written with\n // `requireContainer`. Setting this enables the @maroonedsoftware/johnny5/serverkit\n // integration — make sure that subpath is imported once for its side effect\n // of installing the bootstrap hook (or call configureServerKitModules\n // manually).\n modules?: ServerKitModuleLike<ConfigT>[];\n plugins?: {\n workspace?: Omit<WorkspacePluginOptions, 'repoRoot'> & { repoRoot?: string };\n };\n // Path of the built-in doctor command. Defaults to ['doctor']. Set to\n // null explicitly when supplying your own doctor command.\n doctorCommandPath?: string[] | null;\n}\n\n/** The runnable CLI returned by `createCliApp`. */\nexport interface CliApp {\n /** Parse `argv` (defaults to `process.argv`) and resolve with a process exit code. */\n run: (argv?: string[]) => Promise<number>;\n}\n\n/**\n * Identity helper that exists purely to give TypeScript a place to infer the\n * `Opts` generic from the literal passed in. Equivalent to writing the type\n * annotation manually.\n */\nexport const defineCommand = <Opts = Record<string, unknown>>(mod: CommandModule<Opts>): CommandModule<Opts> => mod;\n\n/**\n * Build a CLI from a list of `CommandModule` registrations. Auto-registers a\n * `doctor` subcommand when `checks` is non-empty, discovers workspace plugins\n * when `plugins.workspace` is configured, and wires up the ServerKit\n * integration when `modules` is supplied.\n */\nexport const createCliApp = async <ConfigT extends AppConfig = AppConfig>(options: CliAppOptions<ConfigT>): Promise<CliApp> => {\n const verbose = process.argv.includes('-v') || process.argv.includes('--verbose');\n const resolvedConfig = typeof options.config === 'function' ? await options.config() : options.config;\n const ctx = await buildContext({\n config: resolvedConfig,\n logger: options.logger,\n verbose,\n });\n\n if (options.modules && options.modules.length > 0) {\n const { configureServerKitModules } = (await import('./integrations/serverkit/index.js')) as {\n configureServerKitModules: (ctx: CliContext, modules: unknown[]) => void;\n };\n configureServerKitModules(ctx, options.modules);\n }\n\n const program = new Command()\n .name(options.name)\n .description(options.description)\n .version(options.version)\n .option('-v, --verbose', 'Enable verbose logging', false);\n\n const discovered: DiscoveredCommand[] = options.commands.map(c => ({ ...c, source: 'core' as const }));\n\n if (options.checks && options.checks.length > 0 && options.doctorCommandPath !== null) {\n const doctorPath = options.doctorCommandPath ?? ['doctor'];\n const alreadyDefined = discovered.some(c => c.path.join(' ') === doctorPath.join(' '));\n if (!alreadyDefined) {\n discovered.push({\n path: doctorPath,\n source: 'core',\n module: buildDoctorCommand(options.checks),\n });\n }\n }\n\n if (options.plugins?.workspace) {\n const workspaceOpts: WorkspacePluginOptions = {\n ...options.plugins.workspace,\n repoRoot: options.plugins.workspace.repoRoot ?? ctx.paths.repoRoot,\n };\n const plugins = await loadWorkspacePlugins(ctx, workspaceOpts);\n discovered.push(...plugins);\n }\n\n registerCommands(program, discovered, ctx);\n\n return {\n run: async (argv = process.argv) => {\n try {\n await program.parseAsync(argv);\n return 0;\n } catch (err) {\n ctx.logger.error((err as Error).message);\n return 1;\n }\n },\n };\n};\n","import * as clack from '@clack/prompts';\n\n/** Re-export of the `@clack/prompts` namespace under a stable name. */\nexport const prompts = clack;\n\n/** Thrown by `unwrap` when the user cancels a clack prompt (e.g. Ctrl+C). */\nexport class PromptCancelledError extends Error {\n constructor() {\n super('prompt cancelled');\n this.name = 'PromptCancelledError';\n }\n}\n\n/**\n * Unwrap a clack prompt result, throwing `PromptCancelledError` when the user\n * cancelled. Lets command handlers use try/catch instead of branching on\n * `isCancel` at every prompt.\n */\nexport const unwrap = <T>(value: T | symbol): T => {\n if (clack.isCancel(value)) throw new PromptCancelledError();\n return value as T;\n};\n","import type { CliContext, CommandModule, DangerousSpec, EnvironmentGuardSpec } from '../types.js';\nimport { prompts } from '../util/prompts.js';\n\nconst resolveEnvGuard = (mod: CommandModule): EnvironmentGuardSpec | null => {\n if (!mod.allowedEnvironments) return null;\n if (Array.isArray(mod.allowedEnvironments)) return { allowed: mod.allowedEnvironments };\n return mod.allowedEnvironments;\n};\n\nconst resolveDangerous = (mod: CommandModule): DangerousSpec | null => {\n if (!mod.dangerous) return null;\n if (mod.dangerous === true) return {};\n return mod.dangerous;\n};\n\nconst hasYesOption = (mod: CommandModule): boolean => (mod.options ?? []).some(o => /(^|[\\s,])(-y|--yes)([\\s,]|$)/.test(o.flags));\n\n/**\n * Returns true when the env guard is satisfied or absent. Logs and returns\n * false when the current environment is not in the allowed list — the caller\n * should treat that as a refusal and exit non-zero.\n */\nexport const checkEnvironmentGuard = (mod: CommandModule, ctx: CliContext, pathLabel: string): boolean => {\n const guard = resolveEnvGuard(mod);\n if (!guard) return true;\n const variable = guard.variable ?? 'NODE_ENV';\n const current = ctx.env[variable];\n if (current !== undefined && guard.allowed.includes(current)) return true;\n const shown = current === undefined ? '(unset)' : current;\n ctx.logger.error(`Refusing to run \"${pathLabel}\" with ${variable}=${shown}. Allowed: ${guard.allowed.join(', ')}.`);\n return false;\n};\n\n/**\n * Resolves a destructive-command confirmation. Returns true when the command\n * should proceed. In non-interactive contexts the caller must pass `--yes`\n * (reflected in `userOptedIn`); otherwise the user is prompted.\n */\nexport const confirmDangerous = async (mod: CommandModule, ctx: CliContext, pathLabel: string, userOptedIn: boolean): Promise<boolean> => {\n const spec = resolveDangerous(mod);\n if (!spec) return true;\n if (userOptedIn) return true;\n if (!ctx.isInteractive()) {\n ctx.logger.error(`\"${pathLabel}\" is destructive; pass --yes to confirm in non-interactive mode.`);\n return false;\n }\n if (spec.confirm === 'typed') {\n const phrase = spec.phrase ?? pathLabel;\n const result = await prompts.text({ message: spec.message ?? `This is destructive. Type \"${phrase}\" to continue:` });\n if (prompts.isCancel(result)) return false;\n if (result !== phrase) {\n ctx.logger.warn('Confirmation did not match — aborting.');\n return false;\n }\n return true;\n }\n const result = await prompts.confirm({ message: spec.message ?? `Run destructive command \"${pathLabel}\"?`, initialValue: false });\n if (prompts.isCancel(result)) return false;\n return result === true;\n};\n\n/** Whether the command needs an injected `-y, --yes` option to be registered. */\nexport const needsYesOption = (mod: CommandModule): boolean => Boolean(mod.dangerous) && !hasYesOption(mod);\n","import { Command } from 'commander';\nimport type { CliContext, CommandModule, DiscoveredCommand, OptionSpec } from '../types.js';\nimport { checkEnvironmentGuard, confirmDangerous, needsYesOption } from './safety.js';\n\nconst applyOption = (cmd: Command, spec: OptionSpec): void => {\n if (spec.required) {\n cmd.requiredOption(spec.flags, spec.description, spec.default as string | undefined);\n return;\n }\n if (spec.default !== undefined) {\n cmd.option(spec.flags, spec.description, spec.default as string | boolean);\n } else {\n cmd.option(spec.flags, spec.description);\n }\n};\n\nconst findOrCreateGroup = (parent: Command, name: string): Command => {\n const existing = parent.commands.find(c => c.name() === name);\n if (existing) return existing;\n return parent.command(name).description(`${name} commands`);\n};\n\n// Extract the long-name (or short-name) of a commander flags string and\n// convert kebab-case to camelCase, matching commander's own option key\n// derivation. e.g. `--org-name <name>` → 'orgName'.\nconst deriveOptionKey = (flags: string): string => {\n const tokens = flags.split(/[ ,]+/);\n const long = tokens.find(t => t.startsWith('--'));\n const target = long ?? tokens.find(t => t.startsWith('-'));\n if (!target) return flags;\n const stripped = target.replace(/^-+/, '');\n return stripped.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());\n};\n\nconst attachLeaf = (parent: Command, leafName: string, mod: CommandModule, ctx: CliContext, sourceLabel: string, fullPath: string[]): void => {\n const cmd = parent.command(leafName).description(mod.description);\n const pathLabel = fullPath.join(' ');\n\n for (const arg of mod.args ?? []) {\n const argName = arg.variadic ? `${arg.name}...` : arg.name;\n if (arg.required) cmd.argument(`<${argName}>`, arg.description);\n else cmd.argument(`[${argName}]`, arg.description);\n }\n\n for (const opt of mod.options ?? []) {\n applyOption(cmd, opt);\n }\n\n if (needsYesOption(mod)) cmd.option('-y, --yes', 'Skip confirmation prompt for this destructive command', false);\n\n if (mod.passthrough) cmd.allowUnknownOption(true).allowExcessArguments(true);\n\n cmd.action(async (...allArgs: unknown[]) => {\n // Commander passes positional args first, then the parsed options\n // object, then the Command instance. We slice off the last two.\n const commandInstance = allArgs[allArgs.length - 1] as Command;\n const opts = (allArgs[allArgs.length - 2] ?? {}) as Record<string, unknown>;\n const positional = allArgs.slice(0, allArgs.length - 2);\n\n const positionalStrings: string[] = positional.flatMap(p => (Array.isArray(p) ? p.map(String) : p == null ? [] : [String(p)]));\n const passthroughArgs = mod.passthrough ? commandInstance.args : positionalStrings;\n\n for (const optSpec of mod.options ?? []) {\n if (!optSpec.envVar) continue;\n const key = deriveOptionKey(optSpec.flags);\n if (opts[key] === undefined && process.env[optSpec.envVar] !== undefined) {\n opts[key] = process.env[optSpec.envVar];\n }\n }\n\n if (!checkEnvironmentGuard(mod, ctx, pathLabel)) {\n process.exit(1);\n return;\n }\n\n if (mod.dangerous) {\n const proceed = await confirmDangerous(mod, ctx, pathLabel, opts['yes'] === true);\n if (!proceed) {\n process.exit(1);\n return;\n }\n }\n\n let finalOpts = opts;\n if (mod.interactive && ctx.isInteractive()) {\n finalOpts = (await mod.interactive(ctx, opts)) as Record<string, unknown>;\n }\n\n try {\n const exitCode = await mod.run(finalOpts, ctx, passthroughArgs);\n if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);\n } catch (err) {\n ctx.logger.error(`[${sourceLabel}] ${(err as Error).message}`);\n if ((err as Error).stack) ctx.logger.debug((err as Error).stack ?? '');\n process.exit(1);\n }\n });\n};\n\n/**\n * Attach every discovered command to a commander `Program`, building intermediate\n * group nodes as needed. Core registrations are processed before plugin ones, so\n * a plugin that tries to claim a path already held by core throws with a\n * descriptive error.\n */\nexport const registerCommands = (program: Command, discovered: DiscoveredCommand[], ctx: CliContext): void => {\n const registeredPaths = new Map<string, { source: 'core' | 'plugin'; sourceName?: string }>();\n\n // Core first, then plugins — plugins can extend but not override.\n const sorted = [...discovered].sort((a, b) => {\n if (a.source === b.source) return 0;\n return a.source === 'core' ? -1 : 1;\n });\n\n for (const entry of sorted) {\n const key = entry.path.join(' ');\n const existing = registeredPaths.get(key);\n if (existing) {\n const incoming = entry.source === 'plugin' ? (entry.sourceName ?? 'unknown plugin') : 'core';\n const owner = existing.source === 'plugin' ? (existing.sourceName ?? 'unknown plugin') : 'core';\n throw new Error(`command \"${key}\" is already registered by ${owner}; ${incoming} cannot override it`);\n }\n registeredPaths.set(key, { source: entry.source, sourceName: entry.sourceName });\n\n let parent: Command = program;\n for (const segment of entry.path.slice(0, -1)) {\n parent = findOrCreateGroup(parent, segment);\n }\n const leafName = entry.path[entry.path.length - 1];\n if (!leafName) continue;\n const sourceLabel = entry.source === 'plugin' ? (entry.sourceName ?? 'plugin') : 'core';\n attachLeaf(parent, leafName, entry.module, ctx, sourceLabel, entry.path);\n }\n};\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { AppConfigBuilder, AppConfigProviderDotenv, type AppConfig } from '@maroonedsoftware/appconfig';\nimport type { CliContext, CliPaths } from './types.js';\nimport type { CliLogger } from './util/logger.js';\nimport { createDefaultLogger } from './util/logger.js';\nimport { createShell } from './util/shell.js';\nimport { isInteractive } from './util/tty.js';\n\n/** Options accepted by `buildContext`. */\nexport interface BuildContextOptions {\n config?: AppConfig;\n logger?: CliLogger;\n verbose?: boolean;\n repoRoot?: string;\n /**\n * Paths to .env files (absolute, or relative to the resolved repoRoot) to\n * load into process.env before building AppConfig. Missing files are\n * silently skipped. Existing process.env values are not overridden.\n * Defaults to ['.env', 'apps/api/.env'].\n */\n envFiles?: string[];\n}\n\nconst findRepoRoot = (start: string): string => {\n let dir = start;\n for (let i = 0; i < 12; i++) {\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n};\n\n// Expands `$VAR` and `${VAR}` references against process.env. Matches the\n// behaviour of dotenv-expand so .env files authored for dbmate/docker-compose\n// (where placeholders are common) still produce usable runtime values.\nconst expandValue = (value: string): string =>\n value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}|\\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced: string | undefined, bare: string | undefined) => {\n const key = (braced ?? bare) as string;\n return process.env[key] ?? '';\n });\n\nconst loadEnvFile = (path: string): void => {\n if (!existsSync(path)) return;\n for (const line of readFileSync(path, 'utf-8').split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eqIdx = trimmed.indexOf('=');\n if (eqIdx === -1) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n const rawValue = trimmed.slice(eqIdx + 1).trim();\n\n // Detect quoting style before unwrapping. Single-quoted values are\n // taken literally; double-quoted and unquoted values get $VAR\n // expansion against the current process.env.\n const singleQuoted = rawValue.startsWith(\"'\") && rawValue.endsWith(\"'\");\n const doubleQuoted = rawValue.startsWith('\"') && rawValue.endsWith('\"');\n let value = singleQuoted || doubleQuoted ? rawValue.slice(1, -1) : rawValue;\n if (!singleQuoted) value = expandValue(value);\n\n if (!(key in process.env)) process.env[key] = value;\n }\n};\n\n/**\n * Build an AppConfig with only the dotenv provider attached. Callers are\n * expected to have loaded .env files into `process.env` beforehand — see\n * `buildContext` for the default loading sequence.\n */\nexport const buildDefaultAppConfig = async (): Promise<AppConfig> =>\n new AppConfigBuilder().addProvider(new AppConfigProviderDotenv()).build();\n\n/**\n * Build the `CliContext` handed to every command, check, and plugin hook. Loads\n * `.env` files into `process.env`, resolves the workspace `repoRoot`, and wires\n * up shell, logger, and config.\n */\nexport const buildContext = async (options: BuildContextOptions = {}): Promise<CliContext> => {\n // Start from cwd so consumers linked from a sibling repo (or installed\n // from npm into node_modules) still resolve to the CONSUMER's workspace\n // root rather than wherever johnny5 itself happens to live.\n const cwd = process.cwd();\n const repoRoot = options.repoRoot ?? findRepoRoot(cwd);\n const paths: CliPaths = { cwd, repoRoot };\n\n for (const envFile of options.envFiles ?? ['.env', 'apps/api/.env']) {\n const absolute = envFile.startsWith('/') ? envFile : resolve(repoRoot, envFile);\n loadEnvFile(absolute);\n }\n\n const logger = options.logger ?? createDefaultLogger({ verbose: options.verbose });\n const shell = createShell(repoRoot, logger);\n const config = options.config ?? (await buildDefaultAppConfig());\n\n return {\n paths,\n logger,\n shell,\n config,\n env: process.env,\n isInteractive,\n };\n};\n","/** Minimal logger interface that every command and check receives via `CliContext`. */\nexport interface CliLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n debug: (msg: string) => void;\n success: (msg: string) => void;\n}\n\nconst colour = (code: number, text: string): string => `\\x1b[${code}m${text}\\x1b[0m`;\n\n/** Options accepted by `createDefaultLogger`. */\nexport interface CreateLoggerOptions {\n /** When true, `debug` writes to stdout; otherwise it's a no-op. */\n verbose?: boolean;\n}\n\n/**\n * Build the default ANSI-coloured console logger used when a consumer doesn't\n * supply their own. `debug` output is gated on `verbose`.\n */\nexport const createDefaultLogger = (options: CreateLoggerOptions = {}): CliLogger => ({\n info: msg => console.log(msg),\n warn: msg => console.warn(colour(33, `! ${msg}`)),\n error: msg => console.error(colour(31, `✗ ${msg}`)),\n success: msg => console.log(colour(32, `✓ ${msg}`)),\n debug: msg => {\n if (options.verbose) console.log(colour(90, `· ${msg}`));\n },\n});\n","import { execa, type Options as ExecaOptions, type ResultPromise } from 'execa';\nimport type { CliLogger } from './logger.js';\n\n/** Execa options re-typed to require a string `cwd` at the call site. */\nexport interface ShellOptions extends ExecaOptions {\n cwd?: string;\n}\n\n/** Tiny shell wrapper around execa exposed on `CliContext.shell`. */\nexport interface Shell {\n /** Run a command, returning the execa result promise. Use this when the caller needs stdout/stderr. */\n run: (command: string, args: string[], options?: ShellOptions) => ResultPromise;\n /** Run a command with inherited stdio, returning the exit code. Failures don't throw — the exit code is returned instead. */\n runStreaming: (command: string, args: string[], options?: ShellOptions) => Promise<number>;\n}\n\n/** Build a `Shell` bound to `cwd`, logging streaming invocations through `logger.debug`. */\nexport const createShell = (cwd: string, logger: CliLogger): Shell => ({\n run: (command, args, options) => execa(command, args, { cwd, ...options }),\n runStreaming: async (command, args, options) => {\n logger.debug(`$ ${command} ${args.join(' ')}`);\n const child = execa(command, args, { cwd, stdio: 'inherit', reject: false, ...options });\n const result = await child;\n return result.exitCode ?? 0;\n },\n});\n","/**\n * Best-effort guess at whether the CLI is talking to a human. Returns false in\n * CI (`CI=true` / `CI=1`), when `JOHNNY5_NON_INTERACTIVE=1`, or when either of\n * stdout/stdin isn't a TTY.\n */\nexport const isInteractive = (): boolean => {\n if (process.env['CI'] === 'true' || process.env['CI'] === '1') return false;\n if (process.env['JOHNNY5_NON_INTERACTIVE'] === '1') return false;\n return Boolean(process.stdout.isTTY && process.stdin.isTTY);\n};\n","import type { Check, CheckResult, CliContext, CommandModule } from '../types.js';\n\n/** Options passed to `runChecks`. */\nexport interface DoctorOptions {\n /** When true, failing checks with an `autoFix` hook get a chance to remediate. */\n fix?: boolean;\n}\n\n/**\n * Run a list of doctor checks sequentially, rendering progress to stdout. Returns\n * a process exit code: `0` when every check passes (including via `autoFix` when\n * `--fix` is supplied), `1` when at least one check fails.\n */\nexport const runChecks = async (ctx: CliContext, checks: Check[], options: DoctorOptions): Promise<number> => {\n ctx.logger.info('Running doctor…\\n');\n\n let failed = 0;\n let fixed = 0;\n\n for (const check of checks) {\n process.stdout.write(` ${check.name.padEnd(36, ' ')} `);\n let result: CheckResult;\n try {\n result = await check.run(ctx);\n } catch (err) {\n result = { ok: false, message: `threw: ${(err as Error).message}` };\n }\n\n if (result.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${result.message}\\n`);\n continue;\n }\n\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${result.message}\\n`);\n\n if (options.fix && check.autoFix) {\n process.stdout.write(` ↻ attempting auto-fix… `);\n try {\n const fixResult = await check.autoFix(ctx);\n if (fixResult.ok) {\n process.stdout.write(`\\x1b[32m✓\\x1b[0m ${fixResult.message}\\n`);\n fixed++;\n continue;\n }\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${fixResult.message}\\n`);\n } catch (err) {\n process.stdout.write(`\\x1b[31m✗\\x1b[0m ${(err as Error).message}\\n`);\n }\n } else if (result.fixHint) {\n process.stdout.write(` → ${result.fixHint}\\n`);\n }\n failed++;\n }\n\n process.stdout.write('\\n');\n if (failed === 0) {\n ctx.logger.success('All checks passed.');\n return 0;\n }\n if (options.fix && fixed > 0) {\n ctx.logger.info(`Auto-fixed ${fixed} issue(s); re-run \\`doctor\\` to confirm.`);\n }\n ctx.logger.error(`${failed} check(s) failed.`);\n return 1;\n};\n\n/**\n * Build the `CommandModule` for the built-in `doctor` subcommand from a set of\n * checks. `createCliApp` auto-registers this when `checks` is non-empty.\n */\nexport const buildDoctorCommand = (checks: Check[]): CommandModule<{ fix?: boolean }> => ({\n description: 'Run local-dev health checks',\n options: [{ flags: '--fix', description: 'Attempt auto-remediation for checks that support it' }],\n run: async (opts, ctx) => runChecks(ctx, checks, { fix: opts.fix === true }),\n});\n","import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { CliContext, DiscoveredCommand, PluginManifest } from '../types.js';\n\ninterface WorkspacePackageJson {\n name?: string;\n johnny5?: {\n commands?: string;\n };\n}\n\n/** Options accepted by `loadWorkspacePlugins`. */\nexport interface WorkspacePluginOptions {\n repoRoot: string;\n /**\n * Workspace-relative directories whose immediate children are scanned for\n * `package.json` files. Defaults to `['apps', 'packages']`.\n */\n roots?: string[];\n /**\n * Package names to skip — typically the consumer's own CLI package whose\n * commands are loaded directly, not via plugin discovery.\n */\n excludePackages?: string[];\n}\n\n/**\n * Scan every workspace package in the configured roots for a `\"johnny5\"` field\n * in `package.json`. When present, the referenced file is dynamically imported\n * and expected to default-export a `PluginManifest`. Failures to load a single\n * plugin log a warning through `ctx.logger.warn` but don't abort the CLI.\n */\nexport const loadWorkspacePlugins = async (ctx: CliContext, options: WorkspacePluginOptions): Promise<DiscoveredCommand[]> => {\n const rootDirs = options.roots ?? ['apps', 'packages'];\n const exclude = new Set(options.excludePackages ?? []);\n const discovered: DiscoveredCommand[] = [];\n\n for (const rootRel of rootDirs) {\n const root = resolve(options.repoRoot, rootRel);\n if (!existsSync(root)) continue;\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const pkgPath = join(root, entry.name, 'package.json');\n if (!existsSync(pkgPath)) continue;\n\n let pkg: WorkspacePackageJson;\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as WorkspacePackageJson;\n } catch {\n continue;\n }\n\n const commandsRel = pkg.johnny5?.commands;\n if (!commandsRel) continue;\n if (pkg.name && exclude.has(pkg.name)) continue;\n\n const manifestPath = resolve(root, entry.name, commandsRel);\n if (!existsSync(manifestPath)) {\n ctx.logger.warn(`johnny5 plugin manifest missing for ${pkg.name ?? entry.name}: ${manifestPath}`);\n continue;\n }\n\n try {\n const mod = (await import(pathToFileURL(manifestPath).href)) as { default: PluginManifest };\n const manifest = mod.default;\n if (!manifest || !Array.isArray(manifest.commands)) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} has no commands array; skipping`);\n continue;\n }\n for (const cmd of manifest.commands) {\n discovered.push({\n path: cmd.path,\n source: 'plugin',\n sourceName: manifest.name ?? pkg.name,\n module: cmd.module,\n });\n }\n } catch (err) {\n ctx.logger.warn(`johnny5 plugin ${pkg.name ?? entry.name} failed to load: ${(err as Error).message}`);\n }\n }\n }\n\n return discovered;\n};\n"],"mappings":"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","names":["InjectKitRegistry","AppConfig","ConsoleLogger","Logger","bootstrapForCli","STATE_KEY","getState","configureServerKitModules","getOrBootstrapContainer","requireContainer","options","registry","register","useInstance","logger","config","module","modules","setup","container","build","shutdown","reverse","Symbol","for","g","globalThis","containerByContext","WeakMap","ctx","set","lazy","get","Error","promise","handler","opts","args","scoped","createScopedContainer","enriched","Object","assign","Command","clack","prompts","clack","PromptCancelledError","Error","name","unwrap","value","isCancel","resolveEnvGuard","mod","allowedEnvironments","Array","isArray","allowed","resolveDangerous","dangerous","hasYesOption","options","some","o","test","flags","checkEnvironmentGuard","ctx","pathLabel","guard","variable","current","env","undefined","includes","shown","logger","error","join","confirmDangerous","userOptedIn","spec","isInteractive","confirm","phrase","result","prompts","text","message","isCancel","warn","initialValue","needsYesOption","Boolean","applyOption","cmd","spec","required","requiredOption","flags","description","default","undefined","option","findOrCreateGroup","parent","name","existing","commands","find","c","command","deriveOptionKey","tokens","split","long","t","startsWith","target","stripped","replace","_","toUpperCase","attachLeaf","leafName","mod","ctx","sourceLabel","fullPath","pathLabel","join","arg","args","argName","variadic","argument","opt","options","needsYesOption","passthrough","allowUnknownOption","allowExcessArguments","action","allArgs","commandInstance","length","opts","positional","slice","positionalStrings","flatMap","p","Array","isArray","map","String","passthroughArgs","optSpec","envVar","key","process","env","checkEnvironmentGuard","exit","dangerous","proceed","confirmDangerous","finalOpts","interactive","isInteractive","exitCode","run","err","logger","error","message","stack","debug","registerCommands","program","discovered","registeredPaths","Map","sorted","sort","a","b","source","entry","path","get","incoming","sourceName","owner","Error","set","segment","module","existsSync","readFileSync","dirname","resolve","AppConfigBuilder","AppConfigProviderDotenv","colour","code","text","createDefaultLogger","options","info","msg","console","log","warn","error","success","debug","verbose","execa","createShell","cwd","logger","run","command","args","options","execa","runStreaming","debug","join","child","stdio","reject","result","exitCode","isInteractive","process","env","Boolean","stdout","isTTY","stdin","findRepoRoot","start","dir","i","existsSync","resolve","parent","dirname","process","cwd","expandValue","value","replace","_","braced","bare","key","env","loadEnvFile","path","line","readFileSync","split","trimmed","trim","startsWith","eqIdx","indexOf","slice","rawValue","singleQuoted","endsWith","doubleQuoted","buildDefaultAppConfig","AppConfigBuilder","addProvider","AppConfigProviderDotenv","build","buildContext","options","repoRoot","paths","envFile","envFiles","absolute","logger","createDefaultLogger","verbose","shell","createShell","config","isInteractive","runChecks","ctx","checks","options","logger","info","failed","fixed","check","process","stdout","write","name","padEnd","result","run","err","ok","message","fix","autoFix","fixResult","fixHint","success","error","buildDoctorCommand","description","flags","opts","existsSync","readFileSync","readdirSync","join","resolve","pathToFileURL","loadWorkspacePlugins","ctx","options","rootDirs","roots","exclude","Set","excludePackages","discovered","rootRel","root","resolve","repoRoot","existsSync","entry","readdirSync","withFileTypes","isDirectory","pkgPath","join","name","pkg","JSON","parse","readFileSync","commandsRel","johnny5","commands","has","manifestPath","logger","warn","mod","pathToFileURL","href","manifest","default","Array","isArray","cmd","push","path","source","sourceName","module","err","message","defineCommand","mod","createCliApp","options","verbose","process","argv","includes","resolvedConfig","config","ctx","buildContext","logger","modules","length","configureServerKitModules","program","Command","name","description","version","option","discovered","commands","map","c","source","checks","doctorCommandPath","doctorPath","alreadyDefined","some","path","join","push","module","buildDoctorCommand","plugins","workspace","workspaceOpts","repoRoot","paths","loadWorkspacePlugins","registerCommands","run","parseAsync","err","error","message"]}
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-CH7ccr3j.js';
1
+ import { b as Check } from '../../types-DBGyauec.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-CH7ccr3j.js';
1
+ import { b as Check } from '../../types-DBGyauec.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -0,0 +1,27 @@
1
+ import { Kysely } from 'kysely';
2
+ import { b as Check } from '../../types-DBGyauec.js';
3
+ import '@maroonedsoftware/appconfig';
4
+ import 'execa';
5
+
6
+ /** Options for `kyselyTableExists`. */
7
+ interface KyselyTableExistsOptions {
8
+ /** Kysely instance to introspect. Typically resolved from the bootstrapped container. */
9
+ db: Kysely<unknown>;
10
+ /** Unqualified table name to look for. */
11
+ table: string;
12
+ /** Optional schema to scope the lookup. When omitted, any schema is accepted. */
13
+ schema?: string;
14
+ }
15
+ /**
16
+ * Check that a table exists in the database by asking Kysely's introspection
17
+ * API. Useful for surfacing "did you run migrations?" failures at doctor time
18
+ * — e.g. `kyselyTableExists({ db, table: 'relation_tuples' })` for the
19
+ * `@maroonedsoftware/permissions` tuple store, or any other migration-managed
20
+ * table.
21
+ *
22
+ * No `autoFix` — creating tables belongs in a migrations command, not in a
23
+ * doctor pass.
24
+ */
25
+ declare const kyselyTableExists: (options: KyselyTableExistsOptions) => Check;
26
+
27
+ export { type KyselyTableExistsOptions, kyselyTableExists };
@@ -0,0 +1,36 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/integrations/kysely/index.ts
5
+ var kyselyTableExists = /* @__PURE__ */ __name((options) => {
6
+ const qualified = options.schema ? `${options.schema}.${options.table}` : options.table;
7
+ return {
8
+ name: `table ${qualified} exists`,
9
+ run: /* @__PURE__ */ __name(async () => {
10
+ try {
11
+ const tables = await options.db.introspection.getTables({
12
+ withInternalKyselyTables: false
13
+ });
14
+ const match = tables.find((t) => t.name === options.table && (options.schema === void 0 || t.schema === options.schema));
15
+ if (match) return {
16
+ ok: true,
17
+ message: `${qualified} exists`
18
+ };
19
+ return {
20
+ ok: false,
21
+ message: `${qualified} not found`,
22
+ fixHint: "Run your database migrations."
23
+ };
24
+ } catch (err) {
25
+ return {
26
+ ok: false,
27
+ message: `introspection failed: ${err.message}`
28
+ };
29
+ }
30
+ }, "run")
31
+ };
32
+ }, "kyselyTableExists");
33
+ export {
34
+ kyselyTableExists
35
+ };
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/integrations/kysely/index.ts"],"sourcesContent":["import type { Kysely } from 'kysely';\nimport type { Check } from '../../types.js';\n\n/** Options for `kyselyTableExists`. */\nexport interface KyselyTableExistsOptions {\n /** Kysely instance to introspect. Typically resolved from the bootstrapped container. */\n db: Kysely<unknown>;\n /** Unqualified table name to look for. */\n table: string;\n /** Optional schema to scope the lookup. When omitted, any schema is accepted. */\n schema?: string;\n}\n\n/**\n * Check that a table exists in the database by asking Kysely's introspection\n * API. Useful for surfacing \"did you run migrations?\" failures at doctor time\n * — e.g. `kyselyTableExists({ db, table: 'relation_tuples' })` for the\n * `@maroonedsoftware/permissions` tuple store, or any other migration-managed\n * table.\n *\n * No `autoFix` — creating tables belongs in a migrations command, not in a\n * doctor pass.\n */\nexport const kyselyTableExists = (options: KyselyTableExistsOptions): Check => {\n const qualified = options.schema ? `${options.schema}.${options.table}` : options.table;\n return {\n name: `table ${qualified} exists`,\n run: async () => {\n try {\n const tables = await options.db.introspection.getTables({ withInternalKyselyTables: false });\n const match = tables.find(t => t.name === options.table && (options.schema === undefined || t.schema === options.schema));\n if (match) return { ok: true, message: `${qualified} exists` };\n return {\n ok: false,\n message: `${qualified} not found`,\n fixHint: 'Run your database migrations.',\n };\n } catch (err) {\n return { ok: false, message: `introspection failed: ${(err as Error).message}` };\n }\n },\n };\n};\n"],"mappings":";;;;AAuBO,IAAMA,oBAAoB,wBAACC,YAAAA;AAC9B,QAAMC,YAAYD,QAAQE,SAAS,GAAGF,QAAQE,MAAM,IAAIF,QAAQG,KAAK,KAAKH,QAAQG;AAClF,SAAO;IACHC,MAAM,SAASH,SAAAA;IACfI,KAAK,mCAAA;AACD,UAAI;AACA,cAAMC,SAAS,MAAMN,QAAQO,GAAGC,cAAcC,UAAU;UAAEC,0BAA0B;QAAM,CAAA;AAC1F,cAAMC,QAAQL,OAAOM,KAAKC,CAAAA,MAAKA,EAAET,SAASJ,QAAQG,UAAUH,QAAQE,WAAWY,UAAaD,EAAEX,WAAWF,QAAQE,OAAK;AACtH,YAAIS,MAAO,QAAO;UAAEI,IAAI;UAAMC,SAAS,GAAGf,SAAAA;QAAmB;AAC7D,eAAO;UACHc,IAAI;UACJC,SAAS,GAAGf,SAAAA;UACZgB,SAAS;QACb;MACJ,SAASC,KAAK;AACV,eAAO;UAAEH,IAAI;UAAOC,SAAS,yBAA0BE,IAAcF,OAAO;QAAG;MACnF;IACJ,GAbK;EAcT;AACJ,GAnBiC;","names":["kyselyTableExists","options","qualified","schema","table","name","run","tables","db","introspection","getTables","withInternalKyselyTables","match","find","t","undefined","ok","message","fixHint","err"]}
@@ -0,0 +1,53 @@
1
+ import { AuthorizationModel } from '@maroonedsoftware/permissions';
2
+ import { C as CliContext, b as Check } from '../../types-DBGyauec.js';
3
+ import '@maroonedsoftware/appconfig';
4
+ import 'execa';
5
+
6
+ /** Options for `permissionsSchemaCompiled`. */
7
+ interface PermissionsSchemaCompiledOptions {
8
+ /**
9
+ * Path to `permissions.config.json`. Relative paths resolve against
10
+ * `ctx.paths.repoRoot`. When omitted, the check walks up from `repoRoot`
11
+ * looking for the config (matches the `pdsl` CLI default).
12
+ */
13
+ configPath?: string;
14
+ }
15
+ /**
16
+ * Check that the TypeScript files generated from `.perm` sources are in sync
17
+ * with the sources. Runs `compile({ dryRun: true })` from
18
+ * `@maroonedsoftware/permissions-dsl` and fails when any output would be
19
+ * rewritten or removed. `autoFix` runs the real `compile()`.
20
+ *
21
+ * Lazy-loads `@maroonedsoftware/permissions-dsl` — returns a clear failure if
22
+ * the package isn't installed.
23
+ */
24
+ declare const permissionsSchemaCompiled: (options?: PermissionsSchemaCompiledOptions) => Check;
25
+ /** Options for `permissionsFixturesPass`. */
26
+ interface PermissionsFixturesPassOptions {
27
+ /** Globs of `.perm.yaml` fixtures. Resolved against `ctx.paths.repoRoot`. */
28
+ patterns: string[];
29
+ }
30
+ /**
31
+ * Check that every assertion in every matched `.perm.yaml` fixture passes.
32
+ * Mirrors `pdsl validate` but renders a single doctor-friendly line summary.
33
+ * No `autoFix` — fixture failures need human judgment.
34
+ */
35
+ declare const permissionsFixturesPass: (options: PermissionsFixturesPassOptions) => Check;
36
+ /** Options for `permissionsModelLoads`. */
37
+ interface PermissionsModelLoadsOptions {
38
+ /**
39
+ * Caller-supplied loader that constructs the project's `AuthorizationModel`.
40
+ * Throws are caught and surfaced as a check failure — `AuthorizationModel`'s
41
+ * constructor already validates names and cross-references.
42
+ */
43
+ loadModel: (ctx: CliContext) => Promise<AuthorizationModel>;
44
+ }
45
+ /**
46
+ * Check that the project's `AuthorizationModel` constructs without throwing.
47
+ * Surfaces duplicate-namespace, unknown-subject, and unresolved
48
+ * `tupleToUserset` errors at doctor time instead of on the first runtime
49
+ * Check call.
50
+ */
51
+ declare const permissionsModelLoads: (options: PermissionsModelLoadsOptions) => Check;
52
+
53
+ export { type PermissionsFixturesPassOptions, type PermissionsModelLoadsOptions, type PermissionsSchemaCompiledOptions, permissionsFixturesPass, permissionsModelLoads, permissionsSchemaCompiled };
@@ -0,0 +1,164 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/integrations/permissions/index.ts
5
+ import { glob } from "fs/promises";
6
+ import { resolve } from "path";
7
+ var permissionsSchemaCompiled = /* @__PURE__ */ __name((options = {}) => ({
8
+ name: "permissions schema compiled",
9
+ run: /* @__PURE__ */ __name(async (ctx) => evaluatePermissionsCompile(ctx, options, {
10
+ write: false
11
+ }), "run"),
12
+ autoFix: /* @__PURE__ */ __name(async (ctx) => evaluatePermissionsCompile(ctx, options, {
13
+ write: true
14
+ }), "autoFix")
15
+ }), "permissionsSchemaCompiled");
16
+ var evaluatePermissionsCompile = /* @__PURE__ */ __name(async (ctx, options, { write }) => {
17
+ let dsl;
18
+ try {
19
+ dsl = await import("@maroonedsoftware/permissions-dsl");
20
+ } catch {
21
+ return {
22
+ ok: false,
23
+ message: "`@maroonedsoftware/permissions-dsl` is not installed; add it as a dependency to use this check"
24
+ };
25
+ }
26
+ const explicit = options.configPath;
27
+ const configPath = explicit ? resolve(ctx.paths.repoRoot, explicit) : dsl.findConfig(ctx.paths.repoRoot);
28
+ if (!configPath) {
29
+ return {
30
+ ok: false,
31
+ message: `no permissions.config.json found under ${ctx.paths.repoRoot}`,
32
+ fixHint: "Create a permissions.config.json or pass `configPath` to permissionsSchemaCompiled()."
33
+ };
34
+ }
35
+ let config;
36
+ try {
37
+ ({ config } = await dsl.loadConfig(configPath));
38
+ } catch (err) {
39
+ return {
40
+ ok: false,
41
+ message: `failed to load ${configPath}: ${err.message}`
42
+ };
43
+ }
44
+ try {
45
+ const result = await dsl.compile(config, write ? {} : {
46
+ dryRun: true
47
+ });
48
+ const drift = result.outputs.length + result.orphaned.length;
49
+ if (drift === 0) {
50
+ return {
51
+ ok: true,
52
+ message: `${result.namespaces.length} namespace(s), all up to date`
53
+ };
54
+ }
55
+ if (write) {
56
+ return {
57
+ ok: true,
58
+ message: `regenerated ${result.outputs.length} file(s), removed ${result.orphaned.length} orphan(s)`
59
+ };
60
+ }
61
+ return {
62
+ ok: false,
63
+ message: `${result.outputs.length} regeneration(s) and ${result.orphaned.length} orphan(s) pending`,
64
+ fixHint: "Run `pdsl compile` or rerun this command with `--fix`."
65
+ };
66
+ } catch (err) {
67
+ if (err instanceof dsl.AggregateCompileError) {
68
+ const summary = err.errors.map((e) => e.message).join("\n\n");
69
+ return {
70
+ ok: false,
71
+ message: summary
72
+ };
73
+ }
74
+ if (err instanceof dsl.CompileError || err instanceof dsl.ParseError) {
75
+ return {
76
+ ok: false,
77
+ message: err.message
78
+ };
79
+ }
80
+ return {
81
+ ok: false,
82
+ message: err.message
83
+ };
84
+ }
85
+ }, "evaluatePermissionsCompile");
86
+ var permissionsFixturesPass = /* @__PURE__ */ __name((options) => ({
87
+ name: "permissions fixtures pass",
88
+ run: /* @__PURE__ */ __name(async (ctx) => {
89
+ let dsl;
90
+ try {
91
+ dsl = await import("@maroonedsoftware/permissions-dsl");
92
+ } catch {
93
+ return {
94
+ ok: false,
95
+ message: "`@maroonedsoftware/permissions-dsl` is not installed; add it as a dependency to use this check"
96
+ };
97
+ }
98
+ const files = /* @__PURE__ */ new Set();
99
+ for (const pattern of options.patterns) {
100
+ for await (const match of glob(pattern, {
101
+ cwd: ctx.paths.repoRoot
102
+ })) {
103
+ files.add(resolve(ctx.paths.repoRoot, match));
104
+ }
105
+ }
106
+ if (files.size === 0) {
107
+ return {
108
+ ok: false,
109
+ message: `no fixtures matched ${options.patterns.join(" ")}`
110
+ };
111
+ }
112
+ let totalFailed = 0;
113
+ let totalPassed = 0;
114
+ let failedFiles = 0;
115
+ for (const file of files) {
116
+ try {
117
+ const fixture = await dsl.loadFixture(file);
118
+ const report = await dsl.runFixture(fixture);
119
+ totalPassed += report.summary.passed;
120
+ totalFailed += report.summary.failed;
121
+ if (report.summary.failed > 0) failedFiles++;
122
+ } catch (err) {
123
+ failedFiles++;
124
+ totalFailed++;
125
+ ctx.logger.debug(`${file}: ${err.message}`);
126
+ }
127
+ }
128
+ if (totalFailed === 0) {
129
+ return {
130
+ ok: true,
131
+ message: `${files.size} fixture(s), ${totalPassed} assertion(s) passed`
132
+ };
133
+ }
134
+ return {
135
+ ok: false,
136
+ message: `${totalFailed} assertion(s) failed across ${failedFiles} fixture(s)`,
137
+ fixHint: `Run \`pdsl validate '${options.patterns[0]}'\` for the full report.`
138
+ };
139
+ }, "run")
140
+ }), "permissionsFixturesPass");
141
+ var permissionsModelLoads = /* @__PURE__ */ __name((options) => ({
142
+ name: "permissions model loads",
143
+ run: /* @__PURE__ */ __name(async (ctx) => {
144
+ try {
145
+ const model = await options.loadModel(ctx);
146
+ const count = model.namespaces().length;
147
+ return {
148
+ ok: true,
149
+ message: `${count} namespace(s) loaded`
150
+ };
151
+ } catch (err) {
152
+ return {
153
+ ok: false,
154
+ message: err.message
155
+ };
156
+ }
157
+ }, "run")
158
+ }), "permissionsModelLoads");
159
+ export {
160
+ permissionsFixturesPass,
161
+ permissionsModelLoads,
162
+ permissionsSchemaCompiled
163
+ };
164
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/integrations/permissions/index.ts"],"sourcesContent":["import { glob } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport type { AuthorizationModel } from '@maroonedsoftware/permissions';\nimport type { Check, CliContext } from '../../types.js';\n\n/** Options for `permissionsSchemaCompiled`. */\nexport interface PermissionsSchemaCompiledOptions {\n /**\n * Path to `permissions.config.json`. Relative paths resolve against\n * `ctx.paths.repoRoot`. When omitted, the check walks up from `repoRoot`\n * looking for the config (matches the `pdsl` CLI default).\n */\n configPath?: string;\n}\n\n/**\n * Check that the TypeScript files generated from `.perm` sources are in sync\n * with the sources. Runs `compile({ dryRun: true })` from\n * `@maroonedsoftware/permissions-dsl` and fails when any output would be\n * rewritten or removed. `autoFix` runs the real `compile()`.\n *\n * Lazy-loads `@maroonedsoftware/permissions-dsl` — returns a clear failure if\n * the package isn't installed.\n */\nexport const permissionsSchemaCompiled = (options: PermissionsSchemaCompiledOptions = {}): Check => ({\n name: 'permissions schema compiled',\n run: async ctx => evaluatePermissionsCompile(ctx, options, { write: false }),\n autoFix: async ctx => evaluatePermissionsCompile(ctx, options, { write: true }),\n});\n\nconst evaluatePermissionsCompile = async (\n ctx: CliContext,\n options: PermissionsSchemaCompiledOptions,\n { write }: { write: boolean },\n): Promise<{ ok: boolean; message: string; fixHint?: string }> => {\n let dsl: typeof import('@maroonedsoftware/permissions-dsl');\n try {\n dsl = await import('@maroonedsoftware/permissions-dsl');\n } catch {\n return { ok: false, message: '`@maroonedsoftware/permissions-dsl` is not installed; add it as a dependency to use this check' };\n }\n\n const explicit = options.configPath;\n const configPath = explicit ? resolve(ctx.paths.repoRoot, explicit) : dsl.findConfig(ctx.paths.repoRoot);\n if (!configPath) {\n return {\n ok: false,\n message: `no permissions.config.json found under ${ctx.paths.repoRoot}`,\n fixHint: 'Create a permissions.config.json or pass `configPath` to permissionsSchemaCompiled().',\n };\n }\n\n let config: import('@maroonedsoftware/permissions-dsl').PermissionsConfig;\n try {\n ({ config } = await dsl.loadConfig(configPath));\n } catch (err) {\n return { ok: false, message: `failed to load ${configPath}: ${(err as Error).message}` };\n }\n\n try {\n const result = await dsl.compile(config, write ? {} : { dryRun: true });\n const drift = result.outputs.length + result.orphaned.length;\n if (drift === 0) {\n return { ok: true, message: `${result.namespaces.length} namespace(s), all up to date` };\n }\n if (write) {\n return { ok: true, message: `regenerated ${result.outputs.length} file(s), removed ${result.orphaned.length} orphan(s)` };\n }\n return {\n ok: false,\n message: `${result.outputs.length} regeneration(s) and ${result.orphaned.length} orphan(s) pending`,\n fixHint: 'Run `pdsl compile` or rerun this command with `--fix`.',\n };\n } catch (err) {\n if (err instanceof dsl.AggregateCompileError) {\n const summary = err.errors.map(e => e.message).join('\\n\\n');\n return { ok: false, message: summary };\n }\n if (err instanceof dsl.CompileError || err instanceof dsl.ParseError) {\n return { ok: false, message: err.message };\n }\n return { ok: false, message: (err as Error).message };\n }\n};\n\n/** Options for `permissionsFixturesPass`. */\nexport interface PermissionsFixturesPassOptions {\n /** Globs of `.perm.yaml` fixtures. Resolved against `ctx.paths.repoRoot`. */\n patterns: string[];\n}\n\n/**\n * Check that every assertion in every matched `.perm.yaml` fixture passes.\n * Mirrors `pdsl validate` but renders a single doctor-friendly line summary.\n * No `autoFix` — fixture failures need human judgment.\n */\nexport const permissionsFixturesPass = (options: PermissionsFixturesPassOptions): Check => ({\n name: 'permissions fixtures pass',\n run: async ctx => {\n let dsl: typeof import('@maroonedsoftware/permissions-dsl');\n try {\n dsl = await import('@maroonedsoftware/permissions-dsl');\n } catch {\n return { ok: false, message: '`@maroonedsoftware/permissions-dsl` is not installed; add it as a dependency to use this check' };\n }\n\n const files = new Set<string>();\n for (const pattern of options.patterns) {\n for await (const match of glob(pattern, { cwd: ctx.paths.repoRoot })) {\n files.add(resolve(ctx.paths.repoRoot, match));\n }\n }\n if (files.size === 0) {\n return { ok: false, message: `no fixtures matched ${options.patterns.join(' ')}` };\n }\n\n let totalFailed = 0;\n let totalPassed = 0;\n let failedFiles = 0;\n for (const file of files) {\n try {\n const fixture = await dsl.loadFixture(file);\n const report = await dsl.runFixture(fixture);\n totalPassed += report.summary.passed;\n totalFailed += report.summary.failed;\n if (report.summary.failed > 0) failedFiles++;\n } catch (err) {\n failedFiles++;\n totalFailed++;\n ctx.logger.debug(`${file}: ${(err as Error).message}`);\n }\n }\n\n if (totalFailed === 0) {\n return { ok: true, message: `${files.size} fixture(s), ${totalPassed} assertion(s) passed` };\n }\n return {\n ok: false,\n message: `${totalFailed} assertion(s) failed across ${failedFiles} fixture(s)`,\n fixHint: `Run \\`pdsl validate '${options.patterns[0]}'\\` for the full report.`,\n };\n },\n});\n\n/** Options for `permissionsModelLoads`. */\nexport interface PermissionsModelLoadsOptions {\n /**\n * Caller-supplied loader that constructs the project's `AuthorizationModel`.\n * Throws are caught and surfaced as a check failure — `AuthorizationModel`'s\n * constructor already validates names and cross-references.\n */\n loadModel: (ctx: CliContext) => Promise<AuthorizationModel>;\n}\n\n/**\n * Check that the project's `AuthorizationModel` constructs without throwing.\n * Surfaces duplicate-namespace, unknown-subject, and unresolved\n * `tupleToUserset` errors at doctor time instead of on the first runtime\n * Check call.\n */\nexport const permissionsModelLoads = (options: PermissionsModelLoadsOptions): Check => ({\n name: 'permissions model loads',\n run: async ctx => {\n try {\n const model = await options.loadModel(ctx);\n const count = model.namespaces().length;\n return { ok: true, message: `${count} namespace(s) loaded` };\n } catch (err) {\n return { ok: false, message: (err as Error).message };\n }\n },\n});\n"],"mappings":"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","names":["glob","resolve","permissionsSchemaCompiled","options","name","run","ctx","evaluatePermissionsCompile","write","autoFix","dsl","ok","message","explicit","configPath","resolve","paths","repoRoot","findConfig","fixHint","config","loadConfig","err","result","compile","dryRun","drift","outputs","length","orphaned","namespaces","AggregateCompileError","summary","errors","map","e","join","CompileError","ParseError","permissionsFixturesPass","files","Set","pattern","patterns","match","glob","cwd","add","size","totalFailed","totalPassed","failedFiles","file","fixture","loadFixture","report","runFixture","passed","failed","logger","debug","permissionsModelLoads","model","loadModel","count"]}
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-CH7ccr3j.js';
1
+ import { b as Check } from '../../types-DBGyauec.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-CH7ccr3j.js';
1
+ import { b as Check } from '../../types-DBGyauec.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -2,7 +2,7 @@ import { Container, ScopedContainer } from 'injectkit';
2
2
  import { AppConfig } from '@maroonedsoftware/appconfig';
3
3
  import { Logger } from '@maroonedsoftware/logger';
4
4
  import { ServerKitModule } from '@maroonedsoftware/koa';
5
- import { C as CliContext, d as CommandModule } from '../../types-CH7ccr3j.js';
5
+ import { C as CliContext, d as CommandModule } from '../../types-DBGyauec.js';
6
6
  import 'execa';
7
7
 
8
8
  /** Options accepted by `bootstrapForCli`. */
@@ -1,4 +1,4 @@
1
- import { b as Check } from '../../types-CH7ccr3j.js';
1
+ import { b as Check } from '../../types-DBGyauec.js';
2
2
  import '@maroonedsoftware/appconfig';
3
3
  import 'execa';
4
4
 
@@ -54,6 +54,22 @@ interface ArgSpec {
54
54
  required?: boolean;
55
55
  variadic?: boolean;
56
56
  }
57
+ /** Confirmation policy applied before a destructive command runs. */
58
+ interface DangerousSpec {
59
+ /** `yes` (default) shows a Y/N prompt; `typed` requires the user to retype `phrase`. */
60
+ confirm?: 'yes' | 'typed';
61
+ /** Exact string the user must enter under `confirm: 'typed'`. Defaults to the full command path. */
62
+ phrase?: string;
63
+ /** Override the prompt message shown to the user. */
64
+ message?: string;
65
+ }
66
+ /** Restricts which runtime environments a command may execute in. */
67
+ interface EnvironmentGuardSpec {
68
+ /** Env values that allow this command to run. */
69
+ allowed: string[];
70
+ /** Env var to read. Defaults to `NODE_ENV`. */
71
+ variable?: string;
72
+ }
57
73
  /**
58
74
  * A single CLI command unit. `defineCommand` is the recommended way to create one
59
75
  * so TypeScript can infer `Opts` from the literal.
@@ -69,6 +85,18 @@ interface CommandModule<Opts = Record<string, unknown>> {
69
85
  run: (opts: Opts, ctx: CliContext, args: string[]) => Promise<number | void>;
70
86
  /** When true, unknown options and excess positional args are forwarded to `run` verbatim instead of triggering commander errors. */
71
87
  passthrough?: boolean;
88
+ /**
89
+ * Marks the command as destructive. Adds a `-y, --yes` flag (unless one is
90
+ * already declared) and refuses to run without confirmation. In a TTY the
91
+ * user is prompted; in non-interactive contexts `--yes` is required.
92
+ */
93
+ dangerous?: boolean | DangerousSpec;
94
+ /**
95
+ * Limits which runtime environments may execute this command. Pass either a
96
+ * list of allowed values for `NODE_ENV`, or a full `EnvironmentGuardSpec`
97
+ * to read a different variable.
98
+ */
99
+ allowedEnvironments?: string[] | EnvironmentGuardSpec;
72
100
  }
73
101
  /** A `CommandModule` plus the path under which it should appear in the CLI tree. */
74
102
  interface CommandRegistration {
@@ -116,4 +144,4 @@ interface CliContext {
116
144
  env: NodeJS.ProcessEnv;
117
145
  }
118
146
 
119
- export { type ArgSpec as A, type CliContext as C, type DiscoveredCommand as D, type OptionSpec as O, type PluginManifest as P, type Shell as S, type CommandRegistration as a, type Check as b, type CliLogger as c, type CommandModule as d, type CheckResult as e, type CliPaths as f, type CreateLoggerOptions as g, type OptionType as h, type ShellOptions as i, createDefaultLogger as j, createShell as k };
147
+ export { type ArgSpec as A, type CliContext as C, type DiscoveredCommand as D, type EnvironmentGuardSpec as E, type OptionSpec as O, type PluginManifest as P, type Shell as S, type CommandRegistration as a, type Check as b, type CliLogger as c, type CommandModule as d, type CheckResult as e, type CliPaths as f, type CreateLoggerOptions as g, type DangerousSpec as h, type OptionType as i, type ShellOptions as j, createDefaultLogger as k, createShell as l };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedsoftware/johnny5",
3
- "version": "0.1.0",
3
+ "version": "1.0.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",
@@ -56,6 +56,14 @@
56
56
  "./filesystem": {
57
57
  "types": "./dist/integrations/filesystem/index.d.ts",
58
58
  "import": "./dist/integrations/filesystem/index.js"
59
+ },
60
+ "./kysely": {
61
+ "types": "./dist/integrations/kysely/index.d.ts",
62
+ "import": "./dist/integrations/kysely/index.js"
63
+ },
64
+ "./permissions": {
65
+ "types": "./dist/integrations/permissions/index.d.ts",
66
+ "import": "./dist/integrations/permissions/index.js"
59
67
  }
60
68
  },
61
69
  "license": "MIT",
@@ -67,19 +75,27 @@
67
75
  "commander": "^14.0.3",
68
76
  "execa": "^9.6.0",
69
77
  "injectkit": "^1.2.0",
70
- "@maroonedsoftware/logger": "1.1.0",
71
- "@maroonedsoftware/appconfig": "1.4.1"
78
+ "@maroonedsoftware/appconfig": "1.4.1",
79
+ "@maroonedsoftware/logger": "1.1.0"
72
80
  },
73
81
  "peerDependencies": {
74
82
  "ioredis": "^5.10.0",
75
83
  "kysely": "^0.28.0",
76
84
  "pg": "^8.20.0",
85
+ "@maroonedsoftware/permissions": "0.2.0",
86
+ "@maroonedsoftware/permissions-dsl": "0.4.0",
77
87
  "@maroonedsoftware/koa": "2.2.1"
78
88
  },
79
89
  "peerDependenciesMeta": {
80
90
  "@maroonedsoftware/koa": {
81
91
  "optional": true
82
92
  },
93
+ "@maroonedsoftware/permissions": {
94
+ "optional": true
95
+ },
96
+ "@maroonedsoftware/permissions-dsl": {
97
+ "optional": true
98
+ },
83
99
  "ioredis": {
84
100
  "optional": true
85
101
  },
@@ -97,8 +113,10 @@
97
113
  "pg": "^8.20.0",
98
114
  "tsup": "^8.5.1",
99
115
  "vitest": "^4.1.5",
100
- "@repo/config-eslint": "0.2.1",
101
116
  "@maroonedsoftware/koa": "2.2.1",
117
+ "@maroonedsoftware/permissions": "0.2.0",
118
+ "@maroonedsoftware/permissions-dsl": "0.4.0",
119
+ "@repo/config-eslint": "0.2.1",
102
120
  "@repo/config-typescript": "0.1.0"
103
121
  },
104
122
  "scripts": {