@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 +259 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +85 -19
- package/dist/index.js.map +1 -1
- package/dist/integrations/docker/index.d.ts +1 -1
- package/dist/integrations/filesystem/index.d.ts +1 -1
- package/dist/integrations/kysely/index.d.ts +27 -0
- package/dist/integrations/kysely/index.js +36 -0
- package/dist/integrations/kysely/index.js.map +1 -0
- package/dist/integrations/permissions/index.d.ts +53 -0
- package/dist/integrations/permissions/index.js +164 -0
- package/dist/integrations/permissions/index.js.map +1 -0
- package/dist/integrations/postgres/index.d.ts +1 -1
- package/dist/integrations/redis/index.d.ts +1 -1
- package/dist/integrations/serverkit/index.d.ts +1 -1
- package/dist/integrations/versions/index.d.ts +1 -1
- package/dist/{types-CH7ccr3j.d.ts → types-DBGyauec.d.ts} +29 -1
- package/package.json +22 -4
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# @maroonedsoftware/johnny5
|
|
2
2
|
|
|
3
|
-
A CLI framework for ServerKit
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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-
|
|
3
|
-
export { A as ArgSpec, e as CheckResult, f as CliPaths, g as CreateLoggerOptions, O as OptionSpec,
|
|
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"]}
|
|
@@ -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":";;;;AAAA,SAASA,YAAY;AACrB,SAASC,eAAe;AAuBjB,IAAMC,4BAA4B,wBAACC,UAA4C,CAAC,OAAc;EACjGC,MAAM;EACNC,KAAK,8BAAMC,QAAOC,2BAA2BD,KAAKH,SAAS;IAAEK,OAAO;EAAM,CAAA,GAArE;EACLC,SAAS,8BAAMH,QAAOC,2BAA2BD,KAAKH,SAAS;IAAEK,OAAO;EAAK,CAAA,GAApE;AACb,IAJyC;AAMzC,IAAMD,6BAA6B,8BAC/BD,KACAH,SACA,EAAEK,MAAK,MAAsB;AAE7B,MAAIE;AACJ,MAAI;AACAA,UAAM,MAAM,OAAO,mCAAA;EACvB,QAAQ;AACJ,WAAO;MAAEC,IAAI;MAAOC,SAAS;IAAiG;EAClI;AAEA,QAAMC,WAAWV,QAAQW;AACzB,QAAMA,aAAaD,WAAWE,QAAQT,IAAIU,MAAMC,UAAUJ,QAAAA,IAAYH,IAAIQ,WAAWZ,IAAIU,MAAMC,QAAQ;AACvG,MAAI,CAACH,YAAY;AACb,WAAO;MACHH,IAAI;MACJC,SAAS,0CAA0CN,IAAIU,MAAMC,QAAQ;MACrEE,SAAS;IACb;EACJ;AAEA,MAAIC;AACJ,MAAI;AACC,KAAA,EAAEA,OAAM,IAAK,MAAMV,IAAIW,WAAWP,UAAAA;EACvC,SAASQ,KAAK;AACV,WAAO;MAAEX,IAAI;MAAOC,SAAS,kBAAkBE,UAAAA,KAAgBQ,IAAcV,OAAO;IAAG;EAC3F;AAEA,MAAI;AACA,UAAMW,SAAS,MAAMb,IAAIc,QAAQJ,QAAQZ,QAAQ,CAAC,IAAI;MAAEiB,QAAQ;IAAK,CAAA;AACrE,UAAMC,QAAQH,OAAOI,QAAQC,SAASL,OAAOM,SAASD;AACtD,QAAIF,UAAU,GAAG;AACb,aAAO;QAAEf,IAAI;QAAMC,SAAS,GAAGW,OAAOO,WAAWF,MAAM;MAAgC;IAC3F;AACA,QAAIpB,OAAO;AACP,aAAO;QAAEG,IAAI;QAAMC,SAAS,eAAeW,OAAOI,QAAQC,MAAM,qBAAqBL,OAAOM,SAASD,MAAM;MAAa;IAC5H;AACA,WAAO;MACHjB,IAAI;MACJC,SAAS,GAAGW,OAAOI,QAAQC,MAAM,wBAAwBL,OAAOM,SAASD,MAAM;MAC/ET,SAAS;IACb;EACJ,SAASG,KAAK;AACV,QAAIA,eAAeZ,IAAIqB,uBAAuB;AAC1C,YAAMC,UAAUV,IAAIW,OAAOC,IAAIC,CAAAA,MAAKA,EAAEvB,OAAO,EAAEwB,KAAK,MAAA;AACpD,aAAO;QAAEzB,IAAI;QAAOC,SAASoB;MAAQ;IACzC;AACA,QAAIV,eAAeZ,IAAI2B,gBAAgBf,eAAeZ,IAAI4B,YAAY;AAClE,aAAO;QAAE3B,IAAI;QAAOC,SAASU,IAAIV;MAAQ;IAC7C;AACA,WAAO;MAAED,IAAI;MAAOC,SAAUU,IAAcV;IAAQ;EACxD;AACJ,GArDmC;AAkE5B,IAAM2B,0BAA0B,wBAACpC,aAAoD;EACxFC,MAAM;EACNC,KAAK,8BAAMC,QAAAA;AACP,QAAII;AACJ,QAAI;AACAA,YAAM,MAAM,OAAO,mCAAA;IACvB,QAAQ;AACJ,aAAO;QAAEC,IAAI;QAAOC,SAAS;MAAiG;IAClI;AAEA,UAAM4B,QAAQ,oBAAIC,IAAAA;AAClB,eAAWC,WAAWvC,QAAQwC,UAAU;AACpC,uBAAiBC,SAASC,KAAKH,SAAS;QAAEI,KAAKxC,IAAIU,MAAMC;MAAS,CAAA,GAAI;AAClEuB,cAAMO,IAAIhC,QAAQT,IAAIU,MAAMC,UAAU2B,KAAAA,CAAAA;MAC1C;IACJ;AACA,QAAIJ,MAAMQ,SAAS,GAAG;AAClB,aAAO;QAAErC,IAAI;QAAOC,SAAS,uBAAuBT,QAAQwC,SAASP,KAAK,GAAA,CAAA;MAAO;IACrF;AAEA,QAAIa,cAAc;AAClB,QAAIC,cAAc;AAClB,QAAIC,cAAc;AAClB,eAAWC,QAAQZ,OAAO;AACtB,UAAI;AACA,cAAMa,UAAU,MAAM3C,IAAI4C,YAAYF,IAAAA;AACtC,cAAMG,SAAS,MAAM7C,IAAI8C,WAAWH,OAAAA;AACpCH,uBAAeK,OAAOvB,QAAQyB;AAC9BR,uBAAeM,OAAOvB,QAAQ0B;AAC9B,YAAIH,OAAOvB,QAAQ0B,SAAS,EAAGP;MACnC,SAAS7B,KAAK;AACV6B;AACAF;AACA3C,YAAIqD,OAAOC,MAAM,GAAGR,IAAAA,KAAU9B,IAAcV,OAAO,EAAE;MACzD;IACJ;AAEA,QAAIqC,gBAAgB,GAAG;AACnB,aAAO;QAAEtC,IAAI;QAAMC,SAAS,GAAG4B,MAAMQ,IAAI,gBAAgBE,WAAAA;MAAkC;IAC/F;AACA,WAAO;MACHvC,IAAI;MACJC,SAAS,GAAGqC,WAAAA,+BAA0CE,WAAAA;MACtDhC,SAAS,wBAAwBhB,QAAQwC,SAAS,CAAA,CAAE;IACxD;EACJ,GA3CK;AA4CT,IA9CuC;AAgEhC,IAAMkB,wBAAwB,wBAAC1D,aAAkD;EACpFC,MAAM;EACNC,KAAK,8BAAMC,QAAAA;AACP,QAAI;AACA,YAAMwD,QAAQ,MAAM3D,QAAQ4D,UAAUzD,GAAAA;AACtC,YAAM0D,QAAQF,MAAMhC,WAAU,EAAGF;AACjC,aAAO;QAAEjB,IAAI;QAAMC,SAAS,GAAGoD,KAAAA;MAA4B;IAC/D,SAAS1C,KAAK;AACV,aAAO;QAAEX,IAAI;QAAOC,SAAUU,IAAcV;MAAQ;IACxD;EACJ,GARK;AAST,IAXqC;","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"]}
|
|
@@ -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-
|
|
5
|
+
import { C as CliContext, d as CommandModule } from '../../types-DBGyauec.js';
|
|
6
6
|
import 'execa';
|
|
7
7
|
|
|
8
8
|
/** Options accepted by `bootstrapForCli`. */
|
|
@@ -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
|
|
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": "
|
|
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/
|
|
71
|
-
"@maroonedsoftware/
|
|
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": {
|