@metaobjectsdev/cli 0.11.6-rc.1 → 0.12.0-rc.1
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 +17 -3
- package/dist/src/commands/gen.d.ts +2 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +8 -4
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +4 -3
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +304 -206
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +182 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +8 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +21 -0
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/format.d.ts +8 -0
- package/dist/src/lib/format.d.ts.map +1 -0
- package/dist/src/lib/format.js +18 -0
- package/dist/src/lib/format.js.map +1 -0
- package/dist/src/lib/kysely.d.ts.map +1 -1
- package/dist/src/lib/kysely.js +5 -2
- package/dist/src/lib/kysely.js.map +1 -1
- package/dist/src/lib/output-json.d.ts +4 -0
- package/dist/src/lib/output-json.d.ts.map +1 -0
- package/dist/src/lib/output-json.js +8 -0
- package/dist/src/lib/output-json.js.map +1 -0
- package/dist/src/lib/output.d.ts +23 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +88 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/pm-detect.d.ts +12 -0
- package/dist/src/lib/pm-detect.d.ts.map +1 -0
- package/dist/src/lib/pm-detect.js +52 -0
- package/dist/src/lib/pm-detect.js.map +1 -0
- package/package.json +17 -20
- package/src/commands/gen.ts +10 -4
- package/src/commands/migrate.ts +134 -10
- package/src/index.ts +183 -7
- package/src/lib/args.ts +34 -0
- package/src/lib/format.ts +23 -0
- package/src/lib/kysely.ts +5 -2
- package/src/lib/output-json.ts +10 -0
- package/src/lib/output.ts +100 -0
- 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
|
|
79
|
-
// resolves against the real 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: '
|
|
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: '
|
|
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
|
+
}
|