@metaobjectsdev/cli 0.11.6 → 0.12.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.
Files changed (45) hide show
  1. package/README.md +17 -3
  2. package/dist/src/commands/gen.d.ts +2 -1
  3. package/dist/src/commands/gen.d.ts.map +1 -1
  4. package/dist/src/commands/gen.js +8 -4
  5. package/dist/src/commands/gen.js.map +1 -1
  6. package/dist/src/commands/migrate.d.ts +4 -3
  7. package/dist/src/commands/migrate.d.ts.map +1 -1
  8. package/dist/src/commands/migrate.js +304 -206
  9. package/dist/src/commands/migrate.js.map +1 -1
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +182 -7
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/lib/args.d.ts +8 -0
  14. package/dist/src/lib/args.d.ts.map +1 -1
  15. package/dist/src/lib/args.js +21 -0
  16. package/dist/src/lib/args.js.map +1 -1
  17. package/dist/src/lib/format.d.ts +8 -0
  18. package/dist/src/lib/format.d.ts.map +1 -0
  19. package/dist/src/lib/format.js +18 -0
  20. package/dist/src/lib/format.js.map +1 -0
  21. package/dist/src/lib/kysely.d.ts.map +1 -1
  22. package/dist/src/lib/kysely.js +5 -2
  23. package/dist/src/lib/kysely.js.map +1 -1
  24. package/dist/src/lib/output-json.d.ts +4 -0
  25. package/dist/src/lib/output-json.d.ts.map +1 -0
  26. package/dist/src/lib/output-json.js +8 -0
  27. package/dist/src/lib/output-json.js.map +1 -0
  28. package/dist/src/lib/output.d.ts +23 -0
  29. package/dist/src/lib/output.d.ts.map +1 -1
  30. package/dist/src/lib/output.js +88 -0
  31. package/dist/src/lib/output.js.map +1 -1
  32. package/dist/src/lib/pm-detect.d.ts +12 -0
  33. package/dist/src/lib/pm-detect.d.ts.map +1 -0
  34. package/dist/src/lib/pm-detect.js +52 -0
  35. package/dist/src/lib/pm-detect.js.map +1 -0
  36. package/package.json +17 -20
  37. package/src/commands/gen.ts +10 -4
  38. package/src/commands/migrate.ts +134 -10
  39. package/src/index.ts +183 -7
  40. package/src/lib/args.ts +34 -0
  41. package/src/lib/format.ts +23 -0
  42. package/src/lib/kysely.ts +5 -2
  43. package/src/lib/output-json.ts +10 -0
  44. package/src/lib/output.ts +100 -0
  45. package/src/lib/pm-detect.ts +53 -0
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { log } from "./lib/log.js";
3
3
  import { cliVersion } from "./lib/version.js";
4
+ import { resolveFormat, isValidFormat, VALID_FORMATS } from "./lib/format.js";
4
5
  export { defineConfig } from "@metaobjectsdev/codegen-ts";
5
6
  export type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
6
7
 
@@ -14,6 +15,7 @@ USAGE:
14
15
  COMMANDS:
15
16
  init Scaffold metaobjects/ + .metaobjects/ in the current repo
16
17
  init --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades
18
+ agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports
17
19
  gen [<entity>...] Codegen TS targets from metaobjects/ entities
18
20
  export Flatten loaded metadata to one canonical JSON artifact
19
21
  docs <metadata> --out <dir> Generate neutral metadata documentation (entity + template pages)
@@ -25,6 +27,7 @@ COMMANDS:
25
27
 
26
28
  GLOBAL OPTIONS:
27
29
  --cwd <path>, -C <path> Run as if launched from <path> (default: current directory)
30
+ --format <toon|json|text> Output format (default: toon on non-TTY, text on TTY)
28
31
 
29
32
  GEN FLAGS:
30
33
  --dry-run Compute and print, don't write
@@ -74,10 +77,104 @@ Other commands (ingest, mcp, serve, install-hooks, audit, capture, promote)
74
77
  ship in later sub-projects. See https://metaobjects.com for docs.
75
78
  `;
76
79
 
80
+ /** Focused per-subcommand usage slices shown by `<cmd> --help`. */
81
+ const COMMAND_HELP: Record<string, string> = {
82
+ gen: `meta gen — codegen TS targets from metaobjects/ entities
83
+
84
+ USAGE:
85
+ meta gen [<entity>...] [flags]
86
+
87
+ FLAGS:
88
+ --dry-run Compute and print, don't write
89
+ <entity> [<entity>] Positional filter on entity names
90
+ --help, -h Print this help
91
+
92
+ NOTE: outDir, dialect, dbImport, extStyle are read from metaobjects.config.ts
93
+ `,
94
+ verify: `meta verify — drift gate (templates / DB schema / codegen)
95
+
96
+ USAGE:
97
+ meta verify [flags]
98
+
99
+ FLAGS:
100
+ --templates Template/prompt {{field}}↔payload drift (default when bare)
101
+ --codegen Codegen drift — regenerate to temp dir and diff committed output
102
+ Needs metaobjects.config.ts; exit 2 if absent.
103
+ --db <url> Schema drift — live DB URL enables the schema-drift gate.
104
+ Supports: file:, libsql:, postgres:, postgresql:
105
+ --prompts <dir> Directory of provider-resolved template text (default: prompts)
106
+ --dialect sqlite|postgres Optional override (auto-detected from --db URL scheme)
107
+ --allow <csv> Accepted for parity with 'migrate'; does NOT affect the drift gate
108
+ --skip-schema Skip the schema-drift gate even when --db is present
109
+ --help, -h Print this help
110
+ `,
111
+ export: `meta export — flatten loaded metadata to one canonical JSON artifact
112
+
113
+ USAGE:
114
+ meta export [flags]
115
+
116
+ FLAGS:
117
+ --out <file> Write output to a file (default: stdout)
118
+ --help, -h Print this help
119
+ `,
120
+ docs: `meta docs — generate neutral metadata documentation (entity + template pages)
121
+
122
+ USAGE:
123
+ meta docs [<metadata>] [flags]
124
+
125
+ FLAGS:
126
+ <metadata> Project root holding metaobjects/ (default: current directory)
127
+ --out <dir>, -o Output directory for the pages (default: ./docs)
128
+ --templates <dir> Project root to resolve adopter templates/ overrides (default: <metadata>)
129
+ --help, -h Print this help
130
+ `,
131
+ init: `meta init — scaffold metaobjects/ + .metaobjects/ in the current repo
132
+
133
+ USAGE:
134
+ meta init [flags]
135
+
136
+ FLAGS:
137
+ --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades
138
+ --force Overwrite existing files
139
+ --quiet Suppress output
140
+ --print-only Print what would be written, don't write
141
+ --d1 Include D1 (Cloudflare) migration config
142
+ --no-wire-root Skip wiring root metaobjects.config.ts
143
+ --help, -h Print this help
144
+ `,
145
+ "agent-docs": `meta agent-docs — scaffold the agent-context (.metaobjects/ always-on files + .claude/skills/)
146
+
147
+ USAGE:
148
+ meta agent-docs [--server <lang>]... [--client <fw>]... [--out <dir>] [flags]
149
+
150
+ FLAGS:
151
+ --server <lang> Server language (repeatable; e.g. csharp, kotlin, python, node)
152
+ --client <fw> Client framework (repeatable; e.g. react, vue)
153
+ --out <dir> Output directory (default: current directory)
154
+ --no-skills Skip .claude/skills/ scaffold
155
+ --no-wire-root Skip wiring root CLAUDE.md @import
156
+ --help, -h Print this help
157
+
158
+ NOTE: This is the canonical scaffolder for all language ports. Non-Node CLIs redirect here.
159
+ `,
160
+ "prompt-snapshot": `meta prompt-snapshot — snapshot rendered template.* output
161
+
162
+ USAGE:
163
+ meta prompt-snapshot [flags]
164
+
165
+ FLAGS:
166
+ --check Compare against committed snapshots; exit 1 on drift (CI gate)
167
+ --prompts <dir> Directory of provider-resolved template text (default: prompts)
168
+ --help, -h Print this help
169
+ `,
170
+ };
171
+
77
172
  export async function run(argv: string[]): Promise<number> {
78
- // Extract the global --cwd / -C flag (anywhere in argv). A relative path
79
- // resolves against the real process.cwd(). Absent → process.cwd().
173
+ // Extract the global --cwd / -C and --format flags (anywhere in argv).
174
+ // A relative --cwd path resolves against the real process.cwd().
175
+ // Absent --cwd → process.cwd(). Absent --format → TTY-aware default.
80
176
  let cwd = process.cwd();
177
+ let formatFlag: string | undefined;
81
178
  const cleaned: string[] = [];
82
179
  for (let i = 0; i < argv.length; i++) {
83
180
  const a = argv[i]!;
@@ -100,12 +197,63 @@ export async function run(argv: string[]): Promise<number> {
100
197
  cwd = resolve(process.cwd(), val);
101
198
  continue;
102
199
  }
200
+ if (a === "--format") {
201
+ formatFlag = argv[i + 1];
202
+ i++; // consume the value
203
+ continue;
204
+ }
205
+ if (a.startsWith("--format=")) {
206
+ formatFlag = a.slice("--format=".length);
207
+ continue;
208
+ }
103
209
  cleaned.push(a);
104
210
  }
105
211
 
212
+ // An explicit but unrecognized --format is a usage error (exit 2) — mirrors the
213
+ // --cwd missing-value handling above. Absent --format keeps the TTY-aware default.
214
+ if (formatFlag !== undefined && !isValidFormat(formatFlag)) {
215
+ log.error(`--format must be one of: ${VALID_FORMATS.join(", ")} (got '${formatFlag}')`);
216
+ return 2;
217
+ }
218
+ const fmt = resolveFormat(formatFlag, process.stdout.isTTY ?? false);
219
+
106
220
  const [cmd, ...rest] = cleaned;
221
+
222
+ // Intercept per-subcommand --help / -h before dispatching (mirrors migrate's own pattern).
223
+ if (cmd !== undefined && cmd !== "--help" && cmd !== "-h" && cmd !== "--version" && cmd !== "-v") {
224
+ if (rest.includes("--help") || rest.includes("-h")) {
225
+ const helpText = COMMAND_HELP[cmd];
226
+ if (helpText !== undefined) {
227
+ log.info(helpText);
228
+ return 0;
229
+ }
230
+ // Unknown command with --help → fall through to the default: branch below.
231
+ }
232
+ }
233
+
107
234
  switch (cmd) {
108
- case undefined:
235
+ case undefined: {
236
+ // Content-first no-args view: concise status + next-step help[] rather than
237
+ // dumping the full manual (full manual is still available via `meta --help`).
238
+ const metaobjectsExists = await import("node:fs/promises")
239
+ .then(({ stat }) => stat(resolve(cwd, "metaobjects")).then(() => true).catch(() => false));
240
+ const statusLine = metaobjectsExists
241
+ ? `meta — MetaObjects CLI (v${VERSION}) · metaobjects/ found`
242
+ : `meta — MetaObjects CLI (v${VERSION}) · no metaobjects/ here`;
243
+ const nextSteps = metaobjectsExists
244
+ ? [
245
+ " meta gen Run codegen",
246
+ " meta verify Check for drift",
247
+ " meta migrate Diff vs DB and emit SQL",
248
+ " meta --help Full command reference",
249
+ ]
250
+ : [
251
+ " meta init Scaffold metaobjects/ in this directory",
252
+ " meta --help Full command reference",
253
+ ];
254
+ log.info(`${statusLine}\n\n${nextSteps.join("\n")}\n`);
255
+ return 0;
256
+ }
109
257
  case "--help":
110
258
  case "-h":
111
259
  log.info(HELP_TEXT);
@@ -118,9 +266,38 @@ export async function run(argv: string[]): Promise<number> {
118
266
  const { initCommand } = await import("./commands/init.js");
119
267
  return initCommand(rest, cwd);
120
268
  }
269
+ case "agent-docs": {
270
+ const { parseAgentDocsArgs } = await import("./lib/args.js");
271
+ const { init } = await import("./commands/init.js");
272
+ let flags;
273
+ try {
274
+ flags = parseAgentDocsArgs(rest);
275
+ } catch (err) {
276
+ log.error((err as Error).message);
277
+ return 2;
278
+ }
279
+ const targetCwd = flags.out !== undefined ? resolve(cwd, flags.out) : cwd;
280
+ try {
281
+ const result = await init({
282
+ cwd: targetCwd,
283
+ servers: flags.servers,
284
+ clients: flags.clients,
285
+ noSkills: flags.noSkills,
286
+ wireRoot: flags.wireRoot,
287
+ docsOnly: true,
288
+ });
289
+ log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.`);
290
+ for (const w of result.warnings) log.info(` ${w}`);
291
+ log.info("Re-run agent-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
292
+ return 0;
293
+ } catch (err) {
294
+ log.error((err as Error).message);
295
+ return 1;
296
+ }
297
+ }
121
298
  case "gen": {
122
299
  const { genCommand } = await import("./commands/gen.js");
123
- return genCommand(rest, cwd);
300
+ return genCommand(rest, cwd, fmt);
124
301
  }
125
302
  case "export": {
126
303
  const { exportCommand } = await import("./commands/export.js");
@@ -140,11 +317,10 @@ export async function run(argv: string[]): Promise<number> {
140
317
  }
141
318
  case "migrate": {
142
319
  const { migrateCommand } = await import("./commands/migrate.js");
143
- return migrateCommand(rest, cwd);
320
+ return migrateCommand(rest, cwd, undefined, fmt);
144
321
  }
145
322
  default:
146
- log.error(`Unknown command: ${cmd}`);
147
- log.info(HELP_TEXT);
323
+ log.error(`Unknown command: ${cmd}. Run \`meta --help\` for available commands.`);
148
324
  return 2;
149
325
  }
150
326
  }
package/src/lib/args.ts CHANGED
@@ -49,6 +49,40 @@ export function parseInitArgs(argv: string[]): InitFlags {
49
49
  };
50
50
  }
51
51
 
52
+ // ---------------------------------------------------------------------------
53
+ // agent-docs flags — docs-only scaffold (always-on + skills), no metaobjects/ project
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export interface AgentDocsFlags {
57
+ servers: string[];
58
+ clients: string[];
59
+ out: string | undefined;
60
+ noSkills: boolean;
61
+ wireRoot: boolean;
62
+ }
63
+
64
+ export function parseAgentDocsArgs(argv: string[]): AgentDocsFlags {
65
+ const { values } = parseArgs({
66
+ args: argv,
67
+ options: {
68
+ server: { type: "string", multiple: true },
69
+ client: { type: "string", multiple: true },
70
+ out: { type: "string" },
71
+ "no-skills": { type: "boolean", default: false },
72
+ "no-wire-root": { type: "boolean", default: false },
73
+ },
74
+ strict: true,
75
+ allowPositionals: false,
76
+ });
77
+ return {
78
+ servers: (values.server as string[] | undefined) ?? [],
79
+ clients: (values.client as string[] | undefined) ?? [],
80
+ out: values.out as string | undefined,
81
+ noSkills: !!values["no-skills"],
82
+ wireRoot: !values["no-wire-root"],
83
+ };
84
+ }
85
+
52
86
  // ---------------------------------------------------------------------------
53
87
  // gen flags — minimal: metaobjects.config.ts holds outDir/dialect/dbImport/extStyle
54
88
  // ---------------------------------------------------------------------------
@@ -0,0 +1,23 @@
1
+ import { encode } from "@toon-format/toon";
2
+
3
+ export type OutputFormat = "toon" | "json" | "text";
4
+
5
+ const VALID = new Set<OutputFormat>(["toon", "json", "text"]);
6
+
7
+ /** Valid --format values, for usage messages. */
8
+ export const VALID_FORMATS: readonly OutputFormat[] = ["toon", "json", "text"];
9
+
10
+ /** True iff `flag` is one of the recognized output formats. */
11
+ export function isValidFormat(flag: string): flag is OutputFormat {
12
+ return VALID.has(flag as OutputFormat);
13
+ }
14
+
15
+ export function resolveFormat(flag: string | undefined, isTTY: boolean): OutputFormat {
16
+ if (flag && VALID.has(flag as OutputFormat)) return flag as OutputFormat;
17
+ // TTY-aware default: humans at a terminal get text; pipes/agents get TOON.
18
+ return isTTY ? "text" : "toon";
19
+ }
20
+
21
+ export function toonEncode(value: unknown): string {
22
+ return encode(value);
23
+ }
package/src/lib/kysely.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Kysely } from "kysely";
2
2
  import { BunSqliteDialect, isBun } from "./bun-sqlite-dialect.js";
3
+ import { installCommand } from "./pm-detect.js";
3
4
 
4
5
  export type Dialect = "sqlite" | "postgres" | "d1";
5
6
 
@@ -80,8 +81,9 @@ export async function buildKyselyFromUrl(
80
81
  const mod = await import("@libsql/kysely-libsql");
81
82
  LibsqlDialect = mod.LibsqlDialect as unknown as LibsqlDialectCtor;
82
83
  } catch {
84
+ const cmd = await installCommand("@libsql/kysely-libsql", process.cwd());
83
85
  throw new Error(
84
- `dialect 'sqlite' requires '@libsql/kysely-libsql'; install it: 'bun add @libsql/kysely-libsql'`,
86
+ `dialect 'sqlite' requires '@libsql/kysely-libsql'; install it: '${cmd}'`,
85
87
  );
86
88
  }
87
89
  sqliteDialect = new LibsqlDialect({ url });
@@ -108,8 +110,9 @@ export async function buildKyselyFromUrl(
108
110
  pg = await import("pg") as unknown as PgPoolModule;
109
111
  ({ PostgresDialect } = await import("kysely"));
110
112
  } catch {
113
+ const cmd = await installCommand("pg", process.cwd());
111
114
  throw new Error(
112
- `dialect 'postgres' requires 'pg'; install it: 'bun add pg'`,
115
+ `dialect 'postgres' requires 'pg'; install it: '${cmd}'`,
113
116
  );
114
117
  }
115
118
  const PoolCtor = pg.Pool ?? pg.default?.Pool;
@@ -0,0 +1,10 @@
1
+ import type { GenResultShape, MigrateResultShape } from "./output.js";
2
+ import { genResultToData, migrateResultToData } from "./output.js";
3
+
4
+ export function formatGenResultJson(result: GenResultShape): string {
5
+ return JSON.stringify(genResultToData(result), null, 2);
6
+ }
7
+
8
+ export function formatMigrateResultJson(result: MigrateResultShape): string {
9
+ return JSON.stringify(migrateResultToData(result), null, 2);
10
+ }
package/src/lib/output.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // (NEW MERGED CONFLICT UNCHANGED REFUSED) otherwise. Per SP5 §5.1.
5
5
 
6
6
  import type { Dialect } from "./kysely.js";
7
+ import { toonEncode } from "./format.js";
7
8
 
8
9
  export interface FormatOptions {
9
10
  isTTY: boolean;
@@ -112,6 +113,10 @@ export interface MigrateResultShape {
112
113
  ambiguous: AmbiguousEntry[];
113
114
  writtenPaths: string[];
114
115
  dryRun: boolean;
116
+ /** Names of migrations actually applied to the DB this run (empty unless --apply ran and succeeded). */
117
+ applied?: string[];
118
+ /** True when --apply was attempted but failed (exit 1). */
119
+ applyFailed?: boolean;
115
120
  }
116
121
 
117
122
  export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOptions): string {
@@ -156,3 +161,98 @@ export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOpt
156
161
 
157
162
  return lines.join("\n");
158
163
  }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // gen TOON/JSON formatters (axi)
167
+ // ---------------------------------------------------------------------------
168
+
169
+ export function genResultToData(result: GenResultShape): {
170
+ gen: { file: string; status: GenFileStatus }[]; summary: string; help: string[];
171
+ } {
172
+ const counts = result.files.reduce<Record<GenFileStatus, number>>(
173
+ (a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
174
+ { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0 },
175
+ );
176
+ const parts: string[] = [];
177
+ if (counts.new) parts.push(`${counts.new} written`);
178
+ if (counts.merged) parts.push(`${counts.merged} merged`);
179
+ if (counts.conflict) parts.push(`${counts.conflict} conflict`);
180
+ if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
181
+ if (counts.refused) parts.push(`${counts.refused} refused`);
182
+ const summary = result.files.length === 0
183
+ ? `no entities to generate in ${result.outDir}`
184
+ : parts.join(", ");
185
+ const help = result.files.length === 0
186
+ ? ["author entities under metaobjects/ then re-run `meta gen`"]
187
+ : ["typecheck the generated code with `npx tsc`", "run schema with `meta migrate --db <url> --slug <name>`"];
188
+ return { gen: result.files.map((f) => ({ file: f.path, status: f.status })), summary, help };
189
+ }
190
+
191
+ export function formatGenResultToon(result: GenResultShape): string {
192
+ return toonEncode(genResultToData(result));
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // migrate TOON/JSON formatters (axi)
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export function migrateResultToData(result: MigrateResultShape): {
200
+ changes: { kind: string; count: number }[];
201
+ written: string[];
202
+ summary: string;
203
+ help: string[];
204
+ } {
205
+ const changeEntries = Object.entries(result.changeCounts).filter(([, v]) => v > 0);
206
+ const changes = changeEntries.map(([kind, count]) => ({ kind, count }));
207
+
208
+ const isBlocked = result.blocked.length > 0 || result.ambiguous.length > 0;
209
+ const changeSummary = changeEntries.map(([k, v]) => `${v} ${k}`).join(", ");
210
+ const applied = result.applied ?? [];
211
+ const applyFailed = result.applyFailed ?? false;
212
+ // `--apply` also applies previously-written-but-unapplied ledger files, so
213
+ // `applied` can be non-empty even when there is no fresh metadata diff.
214
+ const hasChanges = changeEntries.length > 0 || isBlocked;
215
+ const prefix = changeSummary.length > 0 ? `${changeSummary}; ` : "";
216
+
217
+ // Summary + help reflect what ACTUALLY happened, computed from the real signals
218
+ // (dry-run, blocked/ambiguous, files written, files applied) rather than
219
+ // short-circuiting on the presence of a fresh diff: a dry-run wrote nothing, a
220
+ // generate-only run wrote files but applied nothing, and `--apply` can apply a
221
+ // pending ledger file even with no new diff. The `--rollback` hint appears only
222
+ // when something was actually applied.
223
+ let summary: string;
224
+ let help: string[];
225
+ if (isBlocked) {
226
+ summary = `${changeSummary}; not applied`;
227
+ help = [
228
+ ...result.blocked.map((b) => `re-run with --allow ${b.allowFlag} to apply: ${b.description}`),
229
+ ...result.ambiguous.map((a) => `re-run with --on-ambiguous to resolve: ${a.hint}`),
230
+ ];
231
+ } else if (result.dryRun) {
232
+ summary = hasChanges ? `${changeSummary}; preview only (nothing written)` : "no schema changes";
233
+ help = hasChanges
234
+ ? ["re-run without --dry-run to write the migration"]
235
+ : ["metadata and schema are in sync — nothing to do"];
236
+ } else if (applyFailed) {
237
+ summary = `${prefix}apply failed`;
238
+ help = ["resolve the apply error above, then re-run `meta migrate --apply`"];
239
+ } else if (applied.length > 0) {
240
+ summary = `${prefix}applied ${applied.length} migration(s)`;
241
+ help = ["roll back with `meta migrate --rollback <target>`"];
242
+ } else if (result.writtenPaths.length > 0) {
243
+ summary = `${prefix}wrote ${result.writtenPaths.length} migration file(s)`;
244
+ help = ["apply with `meta migrate --db <url> --apply`"];
245
+ } else if (!hasChanges) {
246
+ summary = "no schema changes";
247
+ help = ["metadata and schema are in sync — nothing to do"];
248
+ } else {
249
+ summary = `${changeSummary}; not written`;
250
+ help = ["re-run with --slug <name> to write the migration"];
251
+ }
252
+
253
+ return { changes, written: result.writtenPaths, summary, help };
254
+ }
255
+
256
+ export function formatMigrateResultToon(result: MigrateResultShape): string {
257
+ return toonEncode(migrateResultToData(result));
258
+ }
@@ -0,0 +1,53 @@
1
+ import { access } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
5
+
6
+ /**
7
+ * Detect the package manager in use by looking for the lockfile closest to
8
+ * `dir`. Walks up to the root. Returns "bun" as the default when no lockfile
9
+ * is found.
10
+ */
11
+ export async function detectPackageManager(dir: string): Promise<PackageManager> {
12
+ // Check in the given dir first, then parent dirs up to the FS root.
13
+ let current = dir;
14
+ while (true) {
15
+ const candidates: [string, PackageManager][] = [
16
+ [join(current, "package-lock.json"), "npm"],
17
+ [join(current, "pnpm-lock.yaml"), "pnpm"],
18
+ [join(current, "yarn.lock"), "yarn"],
19
+ [join(current, "bun.lockb"), "bun"],
20
+ [join(current, "bun.lock"), "bun"],
21
+ ];
22
+ for (const [path, pm] of candidates) {
23
+ try {
24
+ await access(path);
25
+ return pm;
26
+ } catch {
27
+ // not found, try next
28
+ }
29
+ }
30
+ const parent = join(current, "..");
31
+ if (parent === current) break; // reached root
32
+ current = parent;
33
+ }
34
+ // Default: bun (most common for this toolchain)
35
+ return "bun";
36
+ }
37
+
38
+ /**
39
+ * Returns the install command for a missing package, using the detected PM.
40
+ */
41
+ export async function installCommand(pkg: string, dir: string): Promise<string> {
42
+ const pm = await detectPackageManager(dir);
43
+ switch (pm) {
44
+ case "npm":
45
+ return `npm install ${pkg}`;
46
+ case "pnpm":
47
+ return `pnpm add ${pkg}`;
48
+ case "yarn":
49
+ return `yarn add ${pkg}`;
50
+ case "bun":
51
+ return `bun add ${pkg}`;
52
+ }
53
+ }