@metaobjectsdev/cli 0.5.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/LICENSE +189 -0
- package/README.md +184 -0
- package/dist/bin/meta.d.ts +3 -0
- package/dist/bin/meta.d.ts.map +1 -0
- package/dist/bin/meta.js +7 -0
- package/dist/bin/meta.js.map +1 -0
- package/dist/src/commands/export.d.ts +2 -0
- package/dist/src/commands/export.d.ts.map +1 -0
- package/dist/src/commands/export.js +45 -0
- package/dist/src/commands/export.js.map +1 -0
- package/dist/src/commands/gen.d.ts +2 -0
- package/dist/src/commands/gen.d.ts.map +1 -0
- package/dist/src/commands/gen.js +86 -0
- package/dist/src/commands/gen.js.map +1 -0
- package/dist/src/commands/init.d.ts +16 -0
- package/dist/src/commands/init.d.ts.map +1 -0
- package/dist/src/commands/init.js +222 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/migrate.d.ts +2 -0
- package/dist/src/commands/migrate.d.ts.map +1 -0
- package/dist/src/commands/migrate.js +361 -0
- package/dist/src/commands/migrate.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +105 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/args.d.ts +34 -0
- package/dist/src/lib/args.d.ts.map +1 -0
- package/dist/src/lib/args.js +103 -0
- package/dist/src/lib/args.js.map +1 -0
- package/dist/src/lib/config.d.ts +17 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +48 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/kysely.d.ts +28 -0
- package/dist/src/lib/kysely.d.ts.map +1 -0
- package/dist/src/lib/kysely.js +100 -0
- package/dist/src/lib/kysely.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts +3 -0
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -0
- package/dist/src/lib/load-metaobjects-config.js +83 -0
- package/dist/src/lib/load-metaobjects-config.js.map +1 -0
- package/dist/src/lib/log.d.ts +6 -0
- package/dist/src/lib/log.d.ts.map +1 -0
- package/dist/src/lib/log.js +6 -0
- package/dist/src/lib/log.js.map +1 -0
- package/dist/src/lib/output.d.ts +38 -0
- package/dist/src/lib/output.d.ts.map +1 -0
- package/dist/src/lib/output.js +96 -0
- package/dist/src/lib/output.js.map +1 -0
- package/dist/src/lib/projection-migrations.d.ts +34 -0
- package/dist/src/lib/projection-migrations.d.ts.map +1 -0
- package/dist/src/lib/projection-migrations.js +112 -0
- package/dist/src/lib/projection-migrations.js.map +1 -0
- package/package.json +71 -0
- package/src/commands/export.ts +50 -0
- package/src/commands/gen.ts +88 -0
- package/src/commands/init.ts +272 -0
- package/src/commands/migrate.ts +390 -0
- package/src/index.ts +109 -0
- package/src/lib/args.ts +157 -0
- package/src/lib/config.ts +78 -0
- package/src/lib/kysely.ts +114 -0
- package/src/lib/load-metaobjects-config.ts +89 -0
- package/src/lib/log.ts +5 -0
- package/src/lib/output.ts +156 -0
- package/src/lib/projection-migrations.ts +159 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { log } from "./lib/log.js";
|
|
3
|
+
export { defineConfig } from "@metaobjectsdev/codegen-ts";
|
|
4
|
+
export type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
5
|
+
|
|
6
|
+
const VERSION = "0.2.0";
|
|
7
|
+
|
|
8
|
+
const HELP_TEXT = `meta — MetaObjects CLI (v${VERSION})
|
|
9
|
+
|
|
10
|
+
USAGE:
|
|
11
|
+
meta <command> [flags]
|
|
12
|
+
|
|
13
|
+
COMMANDS:
|
|
14
|
+
init Scaffold metaobjects/ + .metaobjects/ in the current repo
|
|
15
|
+
init --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades
|
|
16
|
+
gen [<entity>...] Codegen TS targets from metaobjects/ entities
|
|
17
|
+
export Flatten loaded metadata to one canonical JSON artifact
|
|
18
|
+
migrate Diff metadata vs live DB; emit migration SQL files
|
|
19
|
+
--version, -v Print version
|
|
20
|
+
--help, -h Print this help
|
|
21
|
+
|
|
22
|
+
GLOBAL OPTIONS:
|
|
23
|
+
--cwd <path>, -C <path> Run as if launched from <path> (default: current directory)
|
|
24
|
+
|
|
25
|
+
GEN FLAGS:
|
|
26
|
+
--dry-run Compute and print, don't write
|
|
27
|
+
<entity> [<entity>] Positional filter on entity names
|
|
28
|
+
(outDir, dialect, dbImport, extStyle are read from metaobjects.config.ts)
|
|
29
|
+
|
|
30
|
+
EXPORT FLAGS:
|
|
31
|
+
--out <file> Write output to a file (default: stdout)
|
|
32
|
+
|
|
33
|
+
MIGRATE FLAGS:
|
|
34
|
+
--db <url> DB connection URL (required, or set DATABASE_URL or config)
|
|
35
|
+
Supports: file:, libsql:, postgres:, postgresql:
|
|
36
|
+
--dialect sqlite|postgres Optional override (auto-detected from URL scheme)
|
|
37
|
+
--out-dir <path> Migration directory (default: ./.metaobjects/migrations)
|
|
38
|
+
--slug <name> Required when changes are present (e.g., add-user-shipping)
|
|
39
|
+
--allow <csv> Comma-separated destructive-change permissions:
|
|
40
|
+
drop-column,drop-table,type-change,drop-index,drop-fk,nullable-to-not-null
|
|
41
|
+
--on-ambiguous abort|rename|drop-add Default abort
|
|
42
|
+
--dry-run Print SQL to stdout, don't write
|
|
43
|
+
|
|
44
|
+
Other commands (ingest, mcp, serve, install-hooks, audit, capture, promote)
|
|
45
|
+
ship in later sub-projects. See https://metaobjects.com for docs.
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export async function run(argv: string[]): Promise<number> {
|
|
49
|
+
// Extract the global --cwd / -C flag (anywhere in argv). A relative path
|
|
50
|
+
// resolves against the real process.cwd(). Absent → process.cwd().
|
|
51
|
+
let cwd = process.cwd();
|
|
52
|
+
const cleaned: string[] = [];
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const a = argv[i]!;
|
|
55
|
+
if (a === "--cwd" || a === "-C") {
|
|
56
|
+
const val = argv[i + 1];
|
|
57
|
+
if (val === undefined) {
|
|
58
|
+
log.error(`${a} requires a path argument`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
cwd = resolve(process.cwd(), val);
|
|
62
|
+
i++; // consume the value
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (a.startsWith("--cwd=")) {
|
|
66
|
+
const val = a.slice("--cwd=".length);
|
|
67
|
+
if (val === "") {
|
|
68
|
+
log.error("--cwd= requires a path argument");
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
cwd = resolve(process.cwd(), val);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
cleaned.push(a);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const [cmd, ...rest] = cleaned;
|
|
78
|
+
switch (cmd) {
|
|
79
|
+
case undefined:
|
|
80
|
+
case "--help":
|
|
81
|
+
case "-h":
|
|
82
|
+
log.info(HELP_TEXT);
|
|
83
|
+
return 0;
|
|
84
|
+
case "--version":
|
|
85
|
+
case "-v":
|
|
86
|
+
log.info(VERSION);
|
|
87
|
+
return 0;
|
|
88
|
+
case "init": {
|
|
89
|
+
const { initCommand } = await import("./commands/init.js");
|
|
90
|
+
return initCommand(rest, cwd);
|
|
91
|
+
}
|
|
92
|
+
case "gen": {
|
|
93
|
+
const { genCommand } = await import("./commands/gen.js");
|
|
94
|
+
return genCommand(rest, cwd);
|
|
95
|
+
}
|
|
96
|
+
case "export": {
|
|
97
|
+
const { exportCommand } = await import("./commands/export.js");
|
|
98
|
+
return exportCommand(rest, cwd);
|
|
99
|
+
}
|
|
100
|
+
case "migrate": {
|
|
101
|
+
const { migrateCommand } = await import("./commands/migrate.js");
|
|
102
|
+
return migrateCommand(rest, cwd);
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
log.error(`Unknown command: ${cmd}`);
|
|
106
|
+
log.info(HELP_TEXT);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/lib/args.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// init flags
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export interface InitFlags {
|
|
8
|
+
force: boolean;
|
|
9
|
+
quiet: boolean;
|
|
10
|
+
printOnly: boolean;
|
|
11
|
+
refreshDocs: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseInitArgs(argv: string[]): InitFlags {
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args: argv,
|
|
17
|
+
options: {
|
|
18
|
+
force: { type: "boolean", default: false },
|
|
19
|
+
quiet: { type: "boolean", default: false },
|
|
20
|
+
"print-only": { type: "boolean", default: false },
|
|
21
|
+
"refresh-docs": { type: "boolean", default: false },
|
|
22
|
+
},
|
|
23
|
+
strict: true,
|
|
24
|
+
allowPositionals: false,
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
force: !!values.force,
|
|
28
|
+
quiet: !!values.quiet,
|
|
29
|
+
printOnly: !!values["print-only"],
|
|
30
|
+
refreshDocs: !!values["refresh-docs"],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// gen flags — minimal: metaobjects.config.ts holds outDir/dialect/dbImport/extStyle
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export interface GenFlags {
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
entities: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseGenArgs(argv: string[]): GenFlags {
|
|
44
|
+
const { values, positionals } = parseArgs({
|
|
45
|
+
args: argv,
|
|
46
|
+
options: {
|
|
47
|
+
"dry-run": { type: "boolean", default: false },
|
|
48
|
+
},
|
|
49
|
+
strict: true,
|
|
50
|
+
allowPositionals: true,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
dryRun: !!values["dry-run"],
|
|
54
|
+
entities: positionals,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// export flags
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export interface ExportFlags {
|
|
63
|
+
out: string | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseExportArgs(argv: string[]): ExportFlags {
|
|
67
|
+
const { values } = parseArgs({
|
|
68
|
+
args: argv,
|
|
69
|
+
options: {
|
|
70
|
+
out: { type: "string" },
|
|
71
|
+
},
|
|
72
|
+
strict: true,
|
|
73
|
+
allowPositionals: false,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
out: values.out,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// migrate flags
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const DIALECTS = ["sqlite", "postgres"] as const;
|
|
85
|
+
type Dialect = (typeof DIALECTS)[number];
|
|
86
|
+
|
|
87
|
+
const ALLOW_TOKENS = [
|
|
88
|
+
"drop-column",
|
|
89
|
+
"drop-table",
|
|
90
|
+
"type-change",
|
|
91
|
+
"drop-index",
|
|
92
|
+
"drop-fk",
|
|
93
|
+
"nullable-to-not-null",
|
|
94
|
+
] as const;
|
|
95
|
+
type AllowToken = (typeof ALLOW_TOKENS)[number];
|
|
96
|
+
|
|
97
|
+
const ON_AMBIGUOUS = ["abort", "rename", "drop-add"] as const;
|
|
98
|
+
type OnAmbiguous = (typeof ON_AMBIGUOUS)[number];
|
|
99
|
+
|
|
100
|
+
export interface MigrateFlags {
|
|
101
|
+
db: string | undefined;
|
|
102
|
+
dialect: Dialect | undefined;
|
|
103
|
+
outDir: string | undefined;
|
|
104
|
+
slug: string | undefined;
|
|
105
|
+
allow: AllowToken[];
|
|
106
|
+
onAmbiguous: OnAmbiguous | undefined;
|
|
107
|
+
dryRun: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
111
|
+
const { values } = parseArgs({
|
|
112
|
+
args: argv,
|
|
113
|
+
options: {
|
|
114
|
+
"db": { type: "string" },
|
|
115
|
+
"dialect": { type: "string" },
|
|
116
|
+
"out-dir": { type: "string" },
|
|
117
|
+
"slug": { type: "string" },
|
|
118
|
+
"allow": { type: "string" },
|
|
119
|
+
"on-ambiguous": { type: "string" },
|
|
120
|
+
"dry-run": { type: "boolean", default: false },
|
|
121
|
+
},
|
|
122
|
+
strict: true,
|
|
123
|
+
allowPositionals: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const dialect = values.dialect as string | undefined;
|
|
127
|
+
if (dialect !== undefined && !DIALECTS.includes(dialect as Dialect)) {
|
|
128
|
+
throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const onAmb = values["on-ambiguous"] as string | undefined;
|
|
132
|
+
if (onAmb !== undefined && !ON_AMBIGUOUS.includes(onAmb as OnAmbiguous)) {
|
|
133
|
+
throw new Error(`invalid --on-ambiguous '${onAmb}'; expected: ${ON_AMBIGUOUS.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const allowRaw = (values.allow as string | undefined) ?? "";
|
|
137
|
+
const allowTokens = allowRaw.length === 0
|
|
138
|
+
? []
|
|
139
|
+
: allowRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
140
|
+
for (const tok of allowTokens) {
|
|
141
|
+
if (!ALLOW_TOKENS.includes(tok as AllowToken)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`invalid --allow token '${tok}'; expected one of: ${ALLOW_TOKENS.join(", ")}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
db: values.db as string | undefined,
|
|
150
|
+
dialect: dialect as Dialect | undefined,
|
|
151
|
+
outDir: values["out-dir"] as string | undefined,
|
|
152
|
+
slug: values.slug as string | undefined,
|
|
153
|
+
allow: allowTokens as AllowToken[],
|
|
154
|
+
onAmbiguous: onAmb as OnAmbiguous | undefined,
|
|
155
|
+
dryRun: !!values["dry-run"],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ConfigSchema, type Config, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
|
|
4
|
+
import type { GenFlags, MigrateFlags } from "./args.js";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Built-in defaults
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const MIGRATE_DEFAULTS = {
|
|
11
|
+
outDir: "./.metaobjects/migrations",
|
|
12
|
+
databaseUrl: undefined as string | undefined,
|
|
13
|
+
dialect: undefined as "sqlite" | "postgres" | undefined,
|
|
14
|
+
onAmbiguous: "abort" as const,
|
|
15
|
+
allow: [] as string[],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Resolved option shapes
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface ResolvedGenConfig {
|
|
23
|
+
dryRun: boolean;
|
|
24
|
+
entities: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResolvedMigrateConfig {
|
|
28
|
+
outDir: string;
|
|
29
|
+
databaseUrl: string | undefined;
|
|
30
|
+
dialect: "sqlite" | "postgres" | undefined;
|
|
31
|
+
onAmbiguous: "abort" | "rename" | "drop-add";
|
|
32
|
+
allow: string[];
|
|
33
|
+
slug: string | undefined;
|
|
34
|
+
dryRun: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Config loader (silent if file missing)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
async function tryLoadConfig(metaRoot: string): Promise<Config | undefined> {
|
|
42
|
+
try {
|
|
43
|
+
const raw = await readFile(join(metaRoot, DEFAULT_METAOBJECTS_DIR, "config.json"), "utf8");
|
|
44
|
+
return ConfigSchema.parse(JSON.parse(raw));
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Public resolvers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export function resolveGenConfig(flags: GenFlags): ResolvedGenConfig {
|
|
55
|
+
return { dryRun: flags.dryRun, entities: flags.entities };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function resolveMigrateConfig(
|
|
59
|
+
flags: MigrateFlags,
|
|
60
|
+
metaRoot: string,
|
|
61
|
+
): Promise<ResolvedMigrateConfig> {
|
|
62
|
+
const config = await tryLoadConfig(metaRoot);
|
|
63
|
+
const cfgBlock = config?.migrate ?? {};
|
|
64
|
+
|
|
65
|
+
const envUrl = process.env.DATABASE_URL;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
outDir: flags.outDir ?? cfgBlock.outDir ?? MIGRATE_DEFAULTS.outDir,
|
|
69
|
+
databaseUrl: flags.db ?? envUrl ?? cfgBlock.databaseUrl ?? MIGRATE_DEFAULTS.databaseUrl,
|
|
70
|
+
dialect: flags.dialect ?? cfgBlock.dialect ?? MIGRATE_DEFAULTS.dialect,
|
|
71
|
+
onAmbiguous: flags.onAmbiguous ?? cfgBlock.onAmbiguous ?? MIGRATE_DEFAULTS.onAmbiguous,
|
|
72
|
+
allow: flags.allow.length > 0
|
|
73
|
+
? flags.allow
|
|
74
|
+
: (cfgBlock.allow ?? MIGRATE_DEFAULTS.allow),
|
|
75
|
+
slug: flags.slug,
|
|
76
|
+
dryRun: flags.dryRun,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
|
|
3
|
+
export type Dialect = "sqlite" | "postgres";
|
|
4
|
+
|
|
5
|
+
export interface KyselyHandle {
|
|
6
|
+
db: Kysely<Record<string, unknown>>;
|
|
7
|
+
dialect: Dialect;
|
|
8
|
+
/** URL with credentials redacted, safe for display. */
|
|
9
|
+
displayUrl: string;
|
|
10
|
+
/** Idempotent — safe to call multiple times. */
|
|
11
|
+
close: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Infer dialect from URL scheme. Throws if the scheme isn't recognized.
|
|
16
|
+
*/
|
|
17
|
+
export function inferDialect(url: string): Dialect {
|
|
18
|
+
const match = /^([a-z]+):/i.exec(url);
|
|
19
|
+
if (match === null) {
|
|
20
|
+
throw new Error(`unrecognized URL '${url}'; expected scheme prefix like file:, libsql:, postgres:, postgresql:`);
|
|
21
|
+
}
|
|
22
|
+
const scheme = match[1]!.toLowerCase();
|
|
23
|
+
switch (scheme) {
|
|
24
|
+
case "file":
|
|
25
|
+
case "libsql":
|
|
26
|
+
return "sqlite";
|
|
27
|
+
case "postgres":
|
|
28
|
+
case "postgresql":
|
|
29
|
+
return "postgres";
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(
|
|
32
|
+
`unrecognized URL scheme '${scheme}'; supported: file, libsql, postgres, postgresql`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Strip credentials from a URL for display. Returns unchanged if no userinfo.
|
|
39
|
+
*/
|
|
40
|
+
export function redactUrl(url: string): string {
|
|
41
|
+
return url.replace(/^([a-z]+:\/\/)([^:@/]+):([^@]+)@/i, "$1$2:***@");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Construct a Kysely instance from a URL.
|
|
46
|
+
*
|
|
47
|
+
* For sqlite/libsql, requires `@libsql/kysely-libsql` peer dep.
|
|
48
|
+
* For postgres, requires `pg` peer dep.
|
|
49
|
+
*
|
|
50
|
+
* Surfaces an install hint if the peer dep is missing.
|
|
51
|
+
*/
|
|
52
|
+
export async function buildKyselyFromUrl(
|
|
53
|
+
url: string,
|
|
54
|
+
dialectOverride?: Dialect,
|
|
55
|
+
): Promise<KyselyHandle> {
|
|
56
|
+
const dialect = dialectOverride ?? inferDialect(url);
|
|
57
|
+
const displayUrl = redactUrl(url);
|
|
58
|
+
|
|
59
|
+
if (dialect === "sqlite") {
|
|
60
|
+
type LibsqlDialectCtor = new (opts: { url: string }) => ConstructorParameters<typeof Kysely<Record<string, unknown>>>[0]["dialect"];
|
|
61
|
+
let LibsqlDialect: LibsqlDialectCtor;
|
|
62
|
+
try {
|
|
63
|
+
const mod = await import("@libsql/kysely-libsql");
|
|
64
|
+
LibsqlDialect = mod.LibsqlDialect as unknown as LibsqlDialectCtor;
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`dialect 'sqlite' requires '@libsql/kysely-libsql'; install it: 'bun add @libsql/kysely-libsql'`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const db = new Kysely<Record<string, unknown>>({ dialect: new LibsqlDialect({ url }) });
|
|
71
|
+
let closed = false;
|
|
72
|
+
return {
|
|
73
|
+
db,
|
|
74
|
+
dialect,
|
|
75
|
+
displayUrl,
|
|
76
|
+
close: async () => {
|
|
77
|
+
if (closed) return;
|
|
78
|
+
closed = true;
|
|
79
|
+
try { await db.destroy(); } catch { /* swallow */ }
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// postgres
|
|
85
|
+
type PgPoolModule = { Pool: new (opts: { connectionString: string }) => unknown; default?: { Pool: new (opts: { connectionString: string }) => unknown } };
|
|
86
|
+
let pg: PgPoolModule;
|
|
87
|
+
let PostgresDialect: typeof import("kysely").PostgresDialect;
|
|
88
|
+
try {
|
|
89
|
+
pg = await import("pg") as unknown as PgPoolModule;
|
|
90
|
+
({ PostgresDialect } = await import("kysely"));
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`dialect 'postgres' requires 'pg'; install it: 'bun add pg'`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const PoolCtor = pg.Pool ?? pg.default?.Pool;
|
|
97
|
+
if (PoolCtor === undefined) {
|
|
98
|
+
throw new Error(`dialect 'postgres' requires 'pg' (no Pool export found)`);
|
|
99
|
+
}
|
|
100
|
+
const pool = new PoolCtor({ connectionString: url });
|
|
101
|
+
const db = new Kysely<Record<string, unknown>>({ dialect: new PostgresDialect({ pool: pool as never }) });
|
|
102
|
+
let closed = false;
|
|
103
|
+
return {
|
|
104
|
+
db,
|
|
105
|
+
dialect,
|
|
106
|
+
displayUrl,
|
|
107
|
+
close: async () => {
|
|
108
|
+
if (closed) return;
|
|
109
|
+
closed = true;
|
|
110
|
+
try { await db.destroy(); } catch { /* swallow */ }
|
|
111
|
+
try { await (pool as unknown as { end: () => Promise<void> }).end(); } catch { /* swallow */ }
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createJiti } from "jiti";
|
|
6
|
+
import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = "metaobjects.config.ts";
|
|
9
|
+
|
|
10
|
+
// Resolve @metaobjectsdev/codegen-ts from the CLI's own node_modules so that
|
|
11
|
+
// metaobjects.config.ts (which lives in the user's project) can import it even
|
|
12
|
+
// when the user's project has no direct dependency on the package.
|
|
13
|
+
//
|
|
14
|
+
// When compiled: import.meta.url is dist/src/lib/load-metaobjects-config.js — four
|
|
15
|
+
// levels up (past lib/, src/, dist/) reaches the CLI package root (packages/cli/).
|
|
16
|
+
// When run as TS source (e.g. bun test): import.meta.url is src/lib/load-metaobjects-config.ts
|
|
17
|
+
// — three levels up (past lib/, src/) reaches the package root.
|
|
18
|
+
const _thisFile = fileURLToPath(import.meta.url);
|
|
19
|
+
const _isCompiled = _thisFile.includes("/dist/");
|
|
20
|
+
const _cliDir = resolve(_thisFile, _isCompiled ? "../../../.." : "../../..");
|
|
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/.
|
|
27
|
+
//
|
|
28
|
+
// The three workspace deps resolve through the CLI's own node_modules.
|
|
29
|
+
// @metaobjectsdev/cli is this package itself, so it resolves directly from
|
|
30
|
+
// _cliDir rather than through node_modules (which would be a non-existent
|
|
31
|
+
// self-referential symlink).
|
|
32
|
+
const CLI_PKG_PATHS: Record<string, { dist: string; src: string }> = {
|
|
33
|
+
"@metaobjectsdev/codegen-ts": {
|
|
34
|
+
dist: "node_modules/@metaobjectsdev/codegen-ts/dist/index.js",
|
|
35
|
+
src: "node_modules/@metaobjectsdev/codegen-ts/src/index.ts",
|
|
36
|
+
},
|
|
37
|
+
"@metaobjectsdev/codegen-ts/generators": {
|
|
38
|
+
dist: "node_modules/@metaobjectsdev/codegen-ts/dist/generators/index.js",
|
|
39
|
+
src: "node_modules/@metaobjectsdev/codegen-ts/src/generators/index.ts",
|
|
40
|
+
},
|
|
41
|
+
"@metaobjectsdev/codegen-ts-tanstack": {
|
|
42
|
+
dist: "node_modules/@metaobjectsdev/codegen-ts-tanstack/dist/index.js",
|
|
43
|
+
src: "node_modules/@metaobjectsdev/codegen-ts-tanstack/src/index.ts",
|
|
44
|
+
},
|
|
45
|
+
"@metaobjectsdev/cli": {
|
|
46
|
+
dist: "dist/src/index.js",
|
|
47
|
+
src: "src/index.ts",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function resolveCliPkg(specifier: string): string {
|
|
52
|
+
const paths = CLI_PKG_PATHS[specifier];
|
|
53
|
+
if (paths !== undefined) {
|
|
54
|
+
return resolve(_cliDir, _isCompiled ? paths.dist : paths.src);
|
|
55
|
+
}
|
|
56
|
+
return _require.resolve(specifier);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
|
|
60
|
+
const fullPath = resolve(projectRoot, CONFIG_FILE);
|
|
61
|
+
if (!existsSync(fullPath)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`metaobjects.config.ts not found at ${fullPath}. Run 'meta init' to scaffold one.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
// Use import.meta.url as base so jiti resolves workspace deps (@metaobjectsdev/*)
|
|
67
|
+
// from the CLI's own node_modules, not from the user's project root.
|
|
68
|
+
// The alias map redirects codegen-ts imports to the CLI's own copy so that
|
|
69
|
+
// user projects don't need @metaobjectsdev/codegen-ts as a direct dependency.
|
|
70
|
+
const jiti = createJiti(import.meta.url, {
|
|
71
|
+
interopDefault: true,
|
|
72
|
+
alias: {
|
|
73
|
+
"@metaobjectsdev/codegen-ts": resolveCliPkg("@metaobjectsdev/codegen-ts"),
|
|
74
|
+
"@metaobjectsdev/codegen-ts/generators": resolveCliPkg("@metaobjectsdev/codegen-ts/generators"),
|
|
75
|
+
"@metaobjectsdev/codegen-ts-tanstack": resolveCliPkg("@metaobjectsdev/codegen-ts-tanstack"),
|
|
76
|
+
"@metaobjectsdev/cli": resolveCliPkg("@metaobjectsdev/cli"),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const raw = (await jiti.import(fullPath)) as MetaobjectsGenConfig | { default: MetaobjectsGenConfig };
|
|
80
|
+
// jiti's interopDefault doesn't always unwrap the default export when accessed
|
|
81
|
+
// across module boundaries — explicitly unwrap if present.
|
|
82
|
+
const cfg = (raw && typeof raw === "object" && "default" in raw && raw.default
|
|
83
|
+
? (raw as { default: MetaobjectsGenConfig }).default
|
|
84
|
+
: raw) as MetaobjectsGenConfig;
|
|
85
|
+
if (!cfg || typeof cfg !== "object" || !Array.isArray(cfg.generators)) {
|
|
86
|
+
throw new Error(`metaobjects.config.ts at ${fullPath} did not export a valid MetaobjectsGenConfig (missing 'generators' array).`);
|
|
87
|
+
}
|
|
88
|
+
return cfg;
|
|
89
|
+
}
|