@metaobjectsdev/cli 0.5.0 → 0.6.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.
Files changed (74) hide show
  1. package/README.md +20 -1
  2. package/dist/src/commands/init.d.ts +1 -0
  3. package/dist/src/commands/init.d.ts.map +1 -1
  4. package/dist/src/commands/init.js +34 -5
  5. package/dist/src/commands/init.js.map +1 -1
  6. package/dist/src/commands/migrate.d.ts +4 -1
  7. package/dist/src/commands/migrate.d.ts.map +1 -1
  8. package/dist/src/commands/migrate.js +233 -5
  9. package/dist/src/commands/migrate.js.map +1 -1
  10. package/dist/src/commands/prompt-snapshot.d.ts +2 -0
  11. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -0
  12. package/dist/src/commands/prompt-snapshot.js +125 -0
  13. package/dist/src/commands/prompt-snapshot.js.map +1 -0
  14. package/dist/src/commands/verify.d.ts +2 -0
  15. package/dist/src/commands/verify.d.ts.map +1 -0
  16. package/dist/src/commands/verify.js +93 -0
  17. package/dist/src/commands/verify.js.map +1 -0
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/index.js +22 -1
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/lib/args.d.ts +18 -1
  22. package/dist/src/lib/args.d.ts.map +1 -1
  23. package/dist/src/lib/args.js +39 -1
  24. package/dist/src/lib/args.js.map +1 -1
  25. package/dist/src/lib/config.d.ts +11 -1
  26. package/dist/src/lib/config.d.ts.map +1 -1
  27. package/dist/src/lib/config.js +10 -1
  28. package/dist/src/lib/config.js.map +1 -1
  29. package/dist/src/lib/file-provider.d.ts +7 -0
  30. package/dist/src/lib/file-provider.d.ts.map +1 -0
  31. package/dist/src/lib/file-provider.js +33 -0
  32. package/dist/src/lib/file-provider.js.map +1 -0
  33. package/dist/src/lib/kysely.d.ts +1 -1
  34. package/dist/src/lib/kysely.d.ts.map +1 -1
  35. package/dist/src/lib/kysely.js +3 -0
  36. package/dist/src/lib/kysely.js.map +1 -1
  37. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  38. package/dist/src/lib/load-metaobjects-config.js +32 -8
  39. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  40. package/dist/src/lib/output.d.ts +3 -2
  41. package/dist/src/lib/output.d.ts.map +1 -1
  42. package/dist/src/lib/output.js.map +1 -1
  43. package/dist/src/lib/payload-field-tree.d.ts +9 -0
  44. package/dist/src/lib/payload-field-tree.d.ts.map +1 -0
  45. package/dist/src/lib/payload-field-tree.js +36 -0
  46. package/dist/src/lib/payload-field-tree.js.map +1 -0
  47. package/dist/src/lib/projection-migrations.d.ts +2 -1
  48. package/dist/src/lib/projection-migrations.d.ts.map +1 -1
  49. package/dist/src/lib/projection-migrations.js +4 -2
  50. package/dist/src/lib/projection-migrations.js.map +1 -1
  51. package/dist/src/lib/snapshot.d.ts +11 -0
  52. package/dist/src/lib/snapshot.d.ts.map +1 -0
  53. package/dist/src/lib/snapshot.js +36 -0
  54. package/dist/src/lib/snapshot.js.map +1 -0
  55. package/dist/src/lib/wrangler.d.ts +18 -0
  56. package/dist/src/lib/wrangler.d.ts.map +1 -0
  57. package/dist/src/lib/wrangler.js +30 -0
  58. package/dist/src/lib/wrangler.js.map +1 -0
  59. package/package.json +8 -7
  60. package/src/commands/init.ts +35 -5
  61. package/src/commands/migrate.ts +287 -5
  62. package/src/commands/prompt-snapshot.ts +142 -0
  63. package/src/commands/verify.ts +111 -0
  64. package/src/index.ts +22 -1
  65. package/src/lib/args.ts +67 -1
  66. package/src/lib/config.ts +23 -3
  67. package/src/lib/file-provider.ts +33 -0
  68. package/src/lib/kysely.ts +7 -1
  69. package/src/lib/load-metaobjects-config.ts +32 -8
  70. package/src/lib/output.ts +4 -2
  71. package/src/lib/payload-field-tree.ts +47 -0
  72. package/src/lib/projection-migrations.ts +6 -3
  73. package/src/lib/snapshot.ts +50 -0
  74. package/src/lib/wrangler.ts +45 -0
package/src/lib/args.ts CHANGED
@@ -9,6 +9,7 @@ export interface InitFlags {
9
9
  quiet: boolean;
10
10
  printOnly: boolean;
11
11
  refreshDocs: boolean;
12
+ d1: boolean;
12
13
  }
13
14
 
14
15
  export function parseInitArgs(argv: string[]): InitFlags {
@@ -19,6 +20,7 @@ export function parseInitArgs(argv: string[]): InitFlags {
19
20
  quiet: { type: "boolean", default: false },
20
21
  "print-only": { type: "boolean", default: false },
21
22
  "refresh-docs": { type: "boolean", default: false },
23
+ d1: { type: "boolean", default: false },
22
24
  },
23
25
  strict: true,
24
26
  allowPositionals: false,
@@ -28,6 +30,7 @@ export function parseInitArgs(argv: string[]): InitFlags {
28
30
  quiet: !!values.quiet,
29
31
  printOnly: !!values["print-only"],
30
32
  refreshDocs: !!values["refresh-docs"],
33
+ d1: !!values.d1,
31
34
  };
32
35
  }
33
36
 
@@ -77,11 +80,61 @@ export function parseExportArgs(argv: string[]): ExportFlags {
77
80
  };
78
81
  }
79
82
 
83
+ // ---------------------------------------------------------------------------
84
+ // verify flags
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export interface VerifyFlags {
88
+ /** Directory (relative to cwd) holding provider-resolved template text. */
89
+ prompts: string | undefined;
90
+ }
91
+
92
+ export function parseVerifyArgs(argv: string[]): VerifyFlags {
93
+ const { values } = parseArgs({
94
+ args: argv,
95
+ options: {
96
+ prompts: { type: "string" },
97
+ },
98
+ strict: true,
99
+ allowPositionals: false,
100
+ });
101
+ return {
102
+ prompts: values.prompts,
103
+ };
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // prompt-snapshot flags
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export interface PromptSnapshotFlags {
111
+ /** Compare against committed snapshots and fail on drift; never write. */
112
+ check: boolean;
113
+ /** Directory (relative to cwd) holding provider-resolved template text. */
114
+ prompts: string | undefined;
115
+ }
116
+
117
+ export function parsePromptSnapshotArgs(argv: string[]): PromptSnapshotFlags {
118
+ const { values } = parseArgs({
119
+ args: argv,
120
+ options: {
121
+ check: { type: "boolean", default: false },
122
+ prompts: { type: "string" },
123
+ },
124
+ strict: true,
125
+ allowPositionals: false,
126
+ });
127
+ return {
128
+ check: !!values.check,
129
+ prompts: values.prompts,
130
+ };
131
+ }
132
+
80
133
  // ---------------------------------------------------------------------------
81
134
  // migrate flags
82
135
  // ---------------------------------------------------------------------------
83
136
 
84
- const DIALECTS = ["sqlite", "postgres"] as const;
137
+ const DIALECTS = ["sqlite", "postgres", "d1"] as const;
85
138
  type Dialect = (typeof DIALECTS)[number];
86
139
 
87
140
  const ALLOW_TOKENS = [
@@ -105,6 +158,11 @@ export interface MigrateFlags {
105
158
  allow: AllowToken[];
106
159
  onAmbiguous: OnAmbiguous | undefined;
107
160
  dryRun: boolean;
161
+ // D1-specific:
162
+ d1Binding: string | undefined;
163
+ remote: boolean;
164
+ apply: boolean;
165
+ yes: boolean;
108
166
  }
109
167
 
110
168
  export function parseMigrateArgs(argv: string[]): MigrateFlags {
@@ -118,6 +176,10 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
118
176
  "allow": { type: "string" },
119
177
  "on-ambiguous": { type: "string" },
120
178
  "dry-run": { type: "boolean", default: false },
179
+ "d1": { type: "string" },
180
+ "remote": { type: "boolean", default: false },
181
+ "apply": { type: "boolean", default: false },
182
+ "yes": { type: "boolean", default: false },
121
183
  },
122
184
  strict: true,
123
185
  allowPositionals: false,
@@ -153,5 +215,9 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
153
215
  allow: allowTokens as AllowToken[],
154
216
  onAmbiguous: onAmb as OnAmbiguous | undefined,
155
217
  dryRun: !!values["dry-run"],
218
+ d1Binding: values.d1 as string | undefined,
219
+ remote: !!values.remote,
220
+ apply: !!values.apply,
221
+ yes: !!values.yes,
156
222
  };
157
223
  }
package/src/lib/config.ts CHANGED
@@ -2,15 +2,18 @@ import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { ConfigSchema, type Config, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
4
4
  import type { GenFlags, MigrateFlags } from "./args.js";
5
+ import type { Dialect } from "./kysely.js";
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Built-in defaults
8
9
  // ---------------------------------------------------------------------------
9
10
 
11
+ export const MIGRATE_DEFAULT_OUT_DIR = "./.metaobjects/migrations";
12
+
10
13
  const MIGRATE_DEFAULTS = {
11
- outDir: "./.metaobjects/migrations",
14
+ outDir: MIGRATE_DEFAULT_OUT_DIR,
12
15
  databaseUrl: undefined as string | undefined,
13
- dialect: undefined as "sqlite" | "postgres" | undefined,
16
+ dialect: undefined as Dialect | undefined,
14
17
  onAmbiguous: "abort" as const,
15
18
  allow: [] as string[],
16
19
  };
@@ -24,14 +27,23 @@ export interface ResolvedGenConfig {
24
27
  entities: string[];
25
28
  }
26
29
 
30
+ export interface ResolvedD1Config {
31
+ binding: string | undefined;
32
+ remote: boolean;
33
+ autoApply: boolean;
34
+ wranglerConfigPath: string | undefined;
35
+ }
36
+
27
37
  export interface ResolvedMigrateConfig {
28
38
  outDir: string;
29
39
  databaseUrl: string | undefined;
30
- dialect: "sqlite" | "postgres" | undefined;
40
+ dialect: Dialect | undefined;
31
41
  onAmbiguous: "abort" | "rename" | "drop-add";
32
42
  allow: string[];
33
43
  slug: string | undefined;
34
44
  dryRun: boolean;
45
+ yes: boolean;
46
+ d1: ResolvedD1Config;
35
47
  }
36
48
 
37
49
  // ---------------------------------------------------------------------------
@@ -61,6 +73,7 @@ export async function resolveMigrateConfig(
61
73
  ): Promise<ResolvedMigrateConfig> {
62
74
  const config = await tryLoadConfig(metaRoot);
63
75
  const cfgBlock = config?.migrate ?? {};
76
+ const d1Block = cfgBlock.d1 ?? {};
64
77
 
65
78
  const envUrl = process.env.DATABASE_URL;
66
79
 
@@ -74,5 +87,12 @@ export async function resolveMigrateConfig(
74
87
  : (cfgBlock.allow ?? MIGRATE_DEFAULTS.allow),
75
88
  slug: flags.slug,
76
89
  dryRun: flags.dryRun,
90
+ yes: flags.yes,
91
+ d1: {
92
+ binding: flags.d1Binding ?? d1Block.binding,
93
+ remote: flags.remote || (d1Block.remote ?? false),
94
+ autoApply: flags.apply || (d1Block.autoApply ?? false),
95
+ wranglerConfigPath: d1Block.wranglerConfigPath,
96
+ },
77
97
  };
78
98
  }
@@ -0,0 +1,33 @@
1
+ // The filesystem template provider for `meta verify` (FR-004 Plan #3, T6).
2
+ //
3
+ // Maps a 2-layer logical reference (`group/source`) onto a file under a base
4
+ // directory, trying a small set of conventional extensions. This is the CLI's
5
+ // concrete provider; the render engine itself stays provider-agnostic (it only
6
+ // knows the `Provider` interface), so production hosts can swap in an RDB/NoSQL
7
+ // provider without touching the engine.
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import type { Provider } from "@metaobjectsdev/render";
12
+
13
+ const EXTENSIONS = [".mustache", ".txt", ""] as const;
14
+
15
+ export class FileProvider implements Provider {
16
+ constructor(private readonly baseDir: string) {}
17
+
18
+ resolve(ref: string): string | undefined {
19
+ // `group/source` → <baseDir>/group/source<ext>; node:path.join normalizes
20
+ // the embedded "/" separators for the host OS.
21
+ for (const ext of EXTENSIONS) {
22
+ const path = join(this.baseDir, ref) + ext;
23
+ if (existsSync(path)) {
24
+ try {
25
+ return readFileSync(path, "utf8");
26
+ } catch {
27
+ // unreadable — fall through and try the next candidate
28
+ }
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ }
package/src/lib/kysely.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Kysely } from "kysely";
2
2
 
3
- export type Dialect = "sqlite" | "postgres";
3
+ export type Dialect = "sqlite" | "postgres" | "d1";
4
4
 
5
5
  export interface KyselyHandle {
6
6
  db: Kysely<Record<string, unknown>>;
@@ -56,6 +56,12 @@ export async function buildKyselyFromUrl(
56
56
  const dialect = dialectOverride ?? inferDialect(url);
57
57
  const displayUrl = redactUrl(url);
58
58
 
59
+ if (dialect === "d1") {
60
+ throw new Error(
61
+ `dialect 'd1' does not use a URL connection; use meta migrate --d1 <binding>`,
62
+ );
63
+ }
64
+
59
65
  if (dialect === "sqlite") {
60
66
  type LibsqlDialectCtor = new (opts: { url: string }) => ConstructorParameters<typeof Kysely<Record<string, unknown>>>[0]["dialect"];
61
67
  let LibsqlDialect: LibsqlDialectCtor;
@@ -19,13 +19,11 @@ const _thisFile = fileURLToPath(import.meta.url);
19
19
  const _isCompiled = _thisFile.includes("/dist/");
20
20
  const _cliDir = resolve(_thisFile, _isCompiled ? "../../../.." : "../../..");
21
21
  const _require = createRequire(import.meta.url);
22
- // Each aliased specifier maps to a compiled-output path and a TS-source path,
23
- // relative to _cliDir. When running from compiled output we resolve into the
24
- // package's dist/; when running TS source directly (bun test, `meta` run from
25
- // the workspace) we resolve into src/ so the CLI never depends on a stale,
26
- // unrebuilt dist/.
22
+ // Fallback layout for each codegen specifier (relative to _cliDir), used only
23
+ // when standard module resolution can't locate it. Compiled output lives in
24
+ // dist/; un-compiled runs (bun test, `meta` from the workspace) use src/ so the
25
+ // CLI never depends on a stale, unrebuilt dist/.
27
26
  //
28
- // The three workspace deps resolve through the CLI's own node_modules.
29
27
  // @metaobjectsdev/cli is this package itself, so it resolves directly from
30
28
  // _cliDir rather than through node_modules (which would be a non-existent
31
29
  // self-referential symlink).
@@ -38,6 +36,10 @@ const CLI_PKG_PATHS: Record<string, { dist: string; src: string }> = {
38
36
  dist: "node_modules/@metaobjectsdev/codegen-ts/dist/generators/index.js",
39
37
  src: "node_modules/@metaobjectsdev/codegen-ts/src/generators/index.ts",
40
38
  },
39
+ "@metaobjectsdev/codegen-ts-react": {
40
+ dist: "node_modules/@metaobjectsdev/codegen-ts-react/dist/index.js",
41
+ src: "node_modules/@metaobjectsdev/codegen-ts-react/src/index.ts",
42
+ },
41
43
  "@metaobjectsdev/codegen-ts-tanstack": {
42
44
  dist: "node_modules/@metaobjectsdev/codegen-ts-tanstack/dist/index.js",
43
45
  src: "node_modules/@metaobjectsdev/codegen-ts-tanstack/src/index.ts",
@@ -48,12 +50,33 @@ const CLI_PKG_PATHS: Record<string, { dist: string; src: string }> = {
48
50
  },
49
51
  };
50
52
 
53
+ // Resolve a codegen specifier to an absolute path for jiti's alias map, so a
54
+ // user's metaobjects.config.ts can import @metaobjectsdev/codegen-ts* without
55
+ // declaring it directly — the CLI's own copy is used.
56
+ //
57
+ // Standard module resolution is tried first: it follows whatever node_modules
58
+ // layout exists — npm (flat), pnpm (deps as siblings in the virtual store,
59
+ // NOT nested under the CLI dir), or bun — and honors the package's export
60
+ // conditions. The CLI_PKG_PATHS fallback only kicks in when a specifier isn't
61
+ // require-resolvable from the CLI module.
51
62
  function resolveCliPkg(specifier: string): string {
52
63
  const paths = CLI_PKG_PATHS[specifier];
53
- if (paths !== undefined) {
64
+ // The cli self-reference always points at this package's own entry, never a
65
+ // (possibly absent) self-referential node_modules symlink.
66
+ if (specifier === "@metaobjectsdev/cli" && paths !== undefined) {
54
67
  return resolve(_cliDir, _isCompiled ? paths.dist : paths.src);
55
68
  }
56
- return _require.resolve(specifier);
69
+ try {
70
+ return _require.resolve(specifier);
71
+ } catch {
72
+ if (paths !== undefined) {
73
+ const candidate = resolve(_cliDir, _isCompiled ? paths.dist : paths.src);
74
+ if (existsSync(candidate)) return candidate;
75
+ }
76
+ throw new Error(
77
+ `metaobjects: could not resolve ${specifier} from the CLI — try reinstalling @metaobjectsdev/cli.`,
78
+ );
79
+ }
57
80
  }
58
81
 
59
82
  export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
@@ -72,6 +95,7 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
72
95
  alias: {
73
96
  "@metaobjectsdev/codegen-ts": resolveCliPkg("@metaobjectsdev/codegen-ts"),
74
97
  "@metaobjectsdev/codegen-ts/generators": resolveCliPkg("@metaobjectsdev/codegen-ts/generators"),
98
+ "@metaobjectsdev/codegen-ts-react": resolveCliPkg("@metaobjectsdev/codegen-ts-react"),
75
99
  "@metaobjectsdev/codegen-ts-tanstack": resolveCliPkg("@metaobjectsdev/codegen-ts-tanstack"),
76
100
  "@metaobjectsdev/cli": resolveCliPkg("@metaobjectsdev/cli"),
77
101
  },
package/src/lib/output.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  // TTY-gated glyphs: unicode (✓ ↺ ✗ = ⚠) when stdout is a TTY, plain words
4
4
  // (NEW MERGED CONFLICT UNCHANGED REFUSED) otherwise. Per SP5 §5.1.
5
5
 
6
+ import type { Dialect } from "./kysely.js";
7
+
6
8
  export interface FormatOptions {
7
9
  isTTY: boolean;
8
10
  }
@@ -22,7 +24,7 @@ export interface GenFileEntry {
22
24
  export interface GenResultShape {
23
25
  files: GenFileEntry[];
24
26
  outDir: string;
25
- dialect: "sqlite" | "postgres";
27
+ dialect: Dialect;
26
28
  dryRun: boolean;
27
29
  warnings: string[];
28
30
  }
@@ -103,7 +105,7 @@ export interface AmbiguousEntry {
103
105
  }
104
106
 
105
107
  export interface MigrateResultShape {
106
- dialect: "sqlite" | "postgres";
108
+ dialect: Dialect;
107
109
  displayUrl: string;
108
110
  changeCounts: Record<string, number>;
109
111
  blocked: BlockedEntry[];
@@ -0,0 +1,47 @@
1
+ // Derive a plain payload field tree from a loaded `object.value` view-object,
2
+ // for `meta verify` (FR-004 Plan #3, T6). This is the metadata-side bridge to
3
+ // the zero-core-dependency render engine: render's `verify` takes a PLAIN
4
+ // PayloadField[] (no metadata import), and this function produces it by walking
5
+ // the view-object exactly as payload-codegen.ts does — scalars become leaves,
6
+ // `field.object` with an `@objectRef` becomes a nested tree.
7
+
8
+ import {
9
+ type MetaData,
10
+ TYPE_OBJECT,
11
+ TYPE_FIELD,
12
+ FIELD_SUBTYPE_OBJECT,
13
+ FIELD_ATTR_OBJECT_REF,
14
+ } from "@metaobjectsdev/metadata";
15
+ import type { PayloadField } from "@metaobjectsdev/render";
16
+
17
+ function findObject(root: MetaData, name: string): MetaData | undefined {
18
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
19
+ }
20
+
21
+ /**
22
+ * Walk an `object.value` view-object into a render `PayloadField[]`. Object-ref
23
+ * fields recurse into their referenced view-object; a `seen` set guards against
24
+ * a (pathological) reference cycle.
25
+ */
26
+ export function derivePayloadFieldTree(
27
+ root: MetaData,
28
+ voName: string,
29
+ seen: ReadonlySet<string> = new Set(),
30
+ ): PayloadField[] {
31
+ if (seen.has(voName)) return [];
32
+ const vo = findObject(root, voName);
33
+ if (!vo) return [];
34
+ const nextSeen = new Set(seen).add(voName);
35
+ const fields: PayloadField[] = [];
36
+ for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
37
+ if (f.subType === FIELD_SUBTYPE_OBJECT) {
38
+ const ref = f.ownAttr(FIELD_ATTR_OBJECT_REF);
39
+ if (typeof ref === "string") {
40
+ fields.push({ name: f.name, fields: derivePayloadFieldTree(root, ref, nextSeen) });
41
+ continue;
42
+ }
43
+ }
44
+ fields.push({ name: f.name });
45
+ }
46
+ return fields;
47
+ }
@@ -13,13 +13,14 @@ import {
13
13
  type ViewMigrationInput,
14
14
  type ViewMigrationsResult,
15
15
  } from "@metaobjectsdev/migrate-ts";
16
+ import type { Dialect } from "./kysely.js";
16
17
 
17
18
  /** view-name → set of source-table names the view's SELECT depends on. */
18
19
  export type ProjectionViewDependencies = ReadonlyMap<string, ReadonlySet<string>>;
19
20
 
20
21
  export interface ProjectionMigrationsOpts {
21
22
  readonly metadata: MetaData;
22
- readonly dialect: "postgres" | "sqlite";
23
+ readonly dialect: Dialect;
23
24
  readonly allowBreaking?: boolean;
24
25
  /** Column naming strategy forwarded to extractViewSpec. Defaults to "snake_case". */
25
26
  readonly columnNamingStrategy?: "snake_case" | "literal" | "kebab-case";
@@ -53,6 +54,8 @@ export function computeProjectionMigrations(
53
54
  if (!(opts.metadata instanceof MetaRoot)) {
54
55
  throw new Error("computeProjectionMigrations: opts.metadata must be a loaded MetaRoot.");
55
56
  }
57
+ // D1 is SQLite at the SQL level; normalize before passing to downstream emitters.
58
+ const dialect: "postgres" | "sqlite" = opts.dialect === "d1" ? "sqlite" : opts.dialect;
56
59
  const root = opts.metadata;
57
60
  const columnNamingStrategy = opts.columnNamingStrategy ?? "snake_case";
58
61
 
@@ -84,7 +87,7 @@ export function computeProjectionMigrations(
84
87
  }
85
88
 
86
89
  const createSql = emitViewDdl(spec, {
87
- dialect: opts.dialect,
90
+ dialect,
88
91
  baseTableName,
89
92
  joinTables,
90
93
  });
@@ -111,7 +114,7 @@ export function computeProjectionMigrations(
111
114
  }
112
115
 
113
116
  return computeViewMigrations({
114
- dialect: opts.dialect,
117
+ dialect,
115
118
  allowBreaking: opts.allowBreaking ?? false,
116
119
  views,
117
120
  });
@@ -0,0 +1,50 @@
1
+ // Path + diff helpers for `meta prompt-snapshot`. Pure (no I/O, no metadata) so
2
+ // they unit-test trivially; the command does the filesystem work.
3
+
4
+ import { join } from "node:path";
5
+
6
+ export interface SnapshotPaths {
7
+ /** The per-template snapshot directory: <cwd>/.metaobjects/snapshots/<name>. */
8
+ dir: string;
9
+ /** The committed fixture payload (author-owned input). */
10
+ payloadPath: string;
11
+ /** The golden rendered output (tool-managed, byte-exact). */
12
+ snapPath: string;
13
+ }
14
+
15
+ export function snapshotPaths(cwd: string, templateName: string): SnapshotPaths {
16
+ const dir = join(cwd, ".metaobjects", "snapshots", templateName);
17
+ return {
18
+ dir,
19
+ payloadPath: join(dir, "payload.json"),
20
+ snapPath: join(dir, "output.snap"),
21
+ };
22
+ }
23
+
24
+ // A compact line diff: trim the common leading/trailing lines, then show the
25
+ // differing middle as `- <expected>` followed by `+ <actual>`. Enough to make
26
+ // drift reviewable in CI output without pulling in a diff dependency.
27
+ export function unifiedDiff(expected: string, actual: string): string {
28
+ const e = expected.split("\n");
29
+ const a = actual.split("\n");
30
+
31
+ let pre = 0;
32
+ while (pre < e.length && pre < a.length && e[pre] === a[pre]) pre++;
33
+
34
+ let suf = 0;
35
+ while (
36
+ suf < e.length - pre &&
37
+ suf < a.length - pre &&
38
+ e[e.length - 1 - suf] === a[a.length - 1 - suf]
39
+ ) {
40
+ suf++;
41
+ }
42
+
43
+ const eMid = e.slice(pre, e.length - suf);
44
+ const aMid = a.slice(pre, a.length - suf);
45
+
46
+ const out: string[] = [`@@ line ${pre + 1} @@`];
47
+ for (const line of eMid) out.push(`- ${line}`);
48
+ for (const line of aMid) out.push(`+ ${line}`);
49
+ return out.join("\n");
50
+ }
@@ -0,0 +1,45 @@
1
+ import { execFile as execFileCb } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFile = promisify(execFileCb);
5
+
6
+ export interface WranglerExecuteOptions {
7
+ binding: string;
8
+ remote: boolean;
9
+ command: string;
10
+ configPath: string | undefined;
11
+ }
12
+
13
+ export function buildWranglerExecuteArgs(opts: WranglerExecuteOptions): string[] {
14
+ const args: string[] = [
15
+ "d1", "execute", opts.binding,
16
+ opts.remote ? "--remote" : "--local",
17
+ "--json",
18
+ "--command", opts.command,
19
+ ];
20
+ if (opts.configPath !== undefined) {
21
+ args.push("--config", opts.configPath);
22
+ }
23
+ return args;
24
+ }
25
+
26
+ /**
27
+ * Run wrangler with the given args; return stdout. Stderr is included in the
28
+ * error message when wrangler exits non-zero. `cwd` is the directory wrangler
29
+ * runs in (defaults to process.cwd() — caller should pass the project root).
30
+ */
31
+ export type WranglerRunner = (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }>;
32
+
33
+ export const defaultWranglerRunner: WranglerRunner = async (args, cwd) => {
34
+ try {
35
+ const { stdout, stderr } = await execFile("wrangler", args, { cwd, maxBuffer: 16 * 1024 * 1024 });
36
+ return { stdout, stderr };
37
+ } catch (err) {
38
+ const e = err as NodeJS.ErrnoException & { stderr?: string; stdout?: string };
39
+ if (e.code === "ENOENT") {
40
+ throw new Error(`wrangler not found on PATH; install it: 'npm i -D wrangler'`);
41
+ }
42
+ const stderr = e.stderr ?? "";
43
+ throw new Error(`wrangler ${args.join(" ")} failed: ${stderr || e.message}`);
44
+ }
45
+ };