agnes-cli 0.0.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 ADDED
@@ -0,0 +1,140 @@
1
+ # agnes-cli
2
+
3
+ Schema toolkit for **agnes-rs**. Reads your TypeScript `schema.ts` DSL and keeps
4
+ your database in sync — `push`, `pull`, and `migrate`. Runs on [Bun](https://bun.sh)
5
+ and talks to the database through the same Rust bridge (`agnes-library`), so it
6
+ supports **PostgreSQL, MySQL and SQLite**.
7
+
8
+ ## Install
9
+
10
+ Inside this workspace it's already linked. From a consumer project:
11
+
12
+ ```bash
13
+ bun add agnes-cli agnes-library
14
+ ```
15
+
16
+ ## Configure
17
+
18
+ Create `agnes.config.ts` in your project root (see `agnes.config.example.ts`):
19
+
20
+ ```ts
21
+ import { defineConfig } from "agnes-cli";
22
+ import { schema } from "./schema"; // your `export const schema = { ... }`
23
+
24
+ export default defineConfig({
25
+ driver: "postgres",
26
+ url: process.env.DATABASE_URL!,
27
+ schema,
28
+ out: "./schema.ts", // where `pull` writes
29
+ migrationsDir: "./migrations",
30
+
31
+ // `generate` output — extension picks the language (.ts or .js)
32
+ output: "src/services/db.ts",
33
+ schemaPath: "./schema.ts", // module the generated client imports `schema` from
34
+ cache: { enabled: true, walPath: ".agnes/cache.wal" },
35
+ });
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ```bash
41
+ bun agnes push # make the DB match schema.ts (create/alter/DROP)
42
+ bun agnes pull # regenerate schema.ts from the live DB
43
+ bun agnes migrate # write a versioned .sql from drift, then apply pending
44
+ bun agnes generate # emit a pre-wired AgnesClient module (db.ts / db.js)
45
+ ```
46
+
47
+ (From the repo root: `bun run agnes push`.)
48
+
49
+ ### `push` — schema.ts ➜ database
50
+
51
+ Diffs your schema against the live database and applies the difference. This is a
52
+ **full sync**: tables and columns that exist in the DB but not in your schema are
53
+ **dropped**. Destructive operations (`DROP TABLE` / `DROP COLUMN`) require an
54
+ interactive confirmation — pass `-y` / `--yes` to skip it in CI.
55
+
56
+ ```bash
57
+ bun agnes push --dry-run # print the plan + SQL, change nothing
58
+ bun agnes push --yes # apply without prompting
59
+ ```
60
+
61
+ ### `pull` — database ➜ schema.ts
62
+
63
+ Introspects the live database and regenerates `schema.ts` to mirror it exactly
64
+ (tables/columns/indexes/foreign keys no longer present are removed from the file).
65
+ Prompts before overwriting an existing file unless `--yes`.
66
+
67
+ ```bash
68
+ bun agnes pull --out src/schema.ts
69
+ ```
70
+
71
+ ### `migrate` — versioned SQL files
72
+
73
+ 1. Diffs schema vs DB and, if there's drift, writes a timestamped file to
74
+ `migrationsDir` (e.g. `20260701123000_auto.sql`).
75
+ 2. Applies every pending file (those not yet recorded in the `_agnes_migrations`
76
+ tracking table) in order, recording each as it succeeds.
77
+
78
+ Destructive migrations prompt for confirmation unless `--yes`.
79
+
80
+ ```bash
81
+ bun agnes migrate -n add_users # name the generated migration
82
+ bun agnes migrate --dry-run # show what would be generated/applied
83
+ ```
84
+
85
+ ### `generate` — pre-wired client module
86
+
87
+ Writes a ready-to-import `AgnesClient` module built from your config — driver,
88
+ url and cache all baked in. Point `output` anywhere (nested dirs are created):
89
+
90
+ ```ts
91
+ // src/services/db.ts ← generated
92
+ import { AgnesClient } from "agnes-library";
93
+ import { schema } from "../../schema";
94
+
95
+ export const db = await AgnesClient.create(
96
+ {
97
+ driver: "sqlite",
98
+ url: "sqlite:./demo.db",
99
+ cache: { enabled: true, walPath: ".agnes/cache.wal" },
100
+ },
101
+ schema,
102
+ );
103
+ ```
104
+
105
+ - `output` (config) or `--output` picks the path. `.ts` → TypeScript, `.js` → JavaScript.
106
+ - The `import { schema }` path is made relative to the output file automatically.
107
+ - The `cache` block is emitted only if you set `cache` in the config.
108
+
109
+ ```bash
110
+ bun agnes generate # uses config.output
111
+ bun agnes generate --output src/db.js # override; JS output
112
+ ```
113
+
114
+ ## Options
115
+
116
+ | Flag | Applies to | Meaning |
117
+ |------|-----------|---------|
118
+ | `-c, --config <path>` | all | Config file (default `agnes.config.ts`) |
119
+ | `-o, --out <path>` | pull | Output schema file |
120
+ | `--output <path>` | generate | Output client module (.ts/.js) |
121
+ | `--dir <path>` | migrate | Migrations directory |
122
+ | `-n, --name <name>` | migrate | Name for the generated migration |
123
+ | `-y, --yes` | push, pull, migrate | Skip destructive confirmations |
124
+ | `--dry-run` | push, migrate | Show the plan/SQL without executing |
125
+
126
+ ## Type mapping
127
+
128
+ | DSL | PostgreSQL | MySQL | SQLite |
129
+ |-----|-----------|-------|--------|
130
+ | `int` | integer | int | integer |
131
+ | `bigint` | bigint | bigint | integer |
132
+ | `text` | text | text | text |
133
+ | `bool` | boolean | tinyint(1) | integer |
134
+ | `float` | double precision | double | real |
135
+ | `bytes` | bytea | blob | blob |
136
+ | `json` | jsonb | json | text |
137
+
138
+ > **SQLite note:** SQLite can't `ALTER` column types or add/drop foreign keys after
139
+ > table creation. Those operations are emitted as `-- SKIPPED` comments; recreate
140
+ > the table manually if you need them.
@@ -0,0 +1,50 @@
1
+ import { defineConfig } from "agnes-cli";
2
+ import {
3
+ table,
4
+ int,
5
+ text,
6
+ bool,
7
+ many,
8
+ one,
9
+ OnAction,
10
+ } from "agnes-library";
11
+
12
+ // Your schema. Can live here or be imported from ./schema.ts.
13
+ export const schema = {
14
+ user: table(
15
+ {
16
+ id: int("id").primary(),
17
+ name: text("name").index("name_idx"),
18
+ email: text("email").uniqueIndex("email_idx"),
19
+ age: int("age"),
20
+ active: bool("active").default(true),
21
+ posts: many("post", "userId"),
22
+ },
23
+ "users",
24
+ ),
25
+ post: table(
26
+ {
27
+ id: int("id").primary(),
28
+ userId: int("user_id"),
29
+ content: text("content"),
30
+ user: one("user", "userId", "id", OnAction.None, OnAction.Cascade),
31
+ },
32
+ "posts",
33
+ ),
34
+ };
35
+
36
+ export default defineConfig({
37
+ driver: "postgres",
38
+ url: process.env.DATABASE_URL ?? "postgres://user:pass@localhost/db",
39
+ schema,
40
+ out: "./schema.ts",
41
+ migrationsDir: "./migrations",
42
+
43
+ // `agnes generate` writes the ready-to-import client here.
44
+ // Extension picks the language: db.ts → TypeScript, db.js → JavaScript.
45
+ output: "src/services/db.ts",
46
+ // Module the generated client imports `schema` from (default: `out`).
47
+ schemaPath: "./agnes.config.ts",
48
+ // Cache baked into the generated client.
49
+ cache: { enabled: true, walPath: ".agnes/cache.wal" },
50
+ });
package/bin/agnes.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import { run } from "../src/cli";
3
+
4
+ run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "agnes-cli",
3
+ "version": "0.0.1",
4
+ "description": "Schema migration CLI for agnes-rs — push, pull and migrate from schema.ts",
5
+ "type": "module",
6
+ "bin": {
7
+ "agnes": "./bin/agnes.ts"
8
+ },
9
+ "main": "src/index.ts",
10
+ "module": "src/index.ts",
11
+ "exports": {
12
+ ".": "./src/index.ts"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "bin",
17
+ "agnes.config.example.ts",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "agnes": "bun bin/agnes.ts",
22
+ "typecheck": "bunx tsc --noEmit",
23
+ "test": "bun test"
24
+ },
25
+ "dependencies": {
26
+ "agnes-library": "0.0.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "latest"
30
+ },
31
+ "peerDependencies": {
32
+ "typescript": "^5"
33
+ }
34
+ }
package/src/apply.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { Dialect } from "./dialect";
2
+ import type { CliDb } from "./db";
3
+ import { isDestructive, type Operation } from "./diff";
4
+ import { describeOperation, renderPlan } from "./generate";
5
+ import { c, confirm } from "./prompt";
6
+
7
+ export interface ApplyOpts {
8
+ yes?: boolean;
9
+ dryRun?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Print the diff plan, confirm destructive operations, then execute the SQL.
14
+ * Returns the SQL statements that were (or would be) run.
15
+ */
16
+ export async function applyPlan(
17
+ db: CliDb,
18
+ dialect: Dialect,
19
+ ops: Operation[],
20
+ opts: ApplyOpts = {},
21
+ ): Promise<string[]> {
22
+ if (ops.length === 0) {
23
+ console.log(c.green("✓ Database is up to date. Nothing to apply."));
24
+ return [];
25
+ }
26
+
27
+ console.log(c.bold("\nPlan:"));
28
+ for (const op of ops) {
29
+ const line = describeOperation(op);
30
+ console.log(" " + (isDestructive(op) ? c.red(line) : c.green(line)));
31
+ }
32
+
33
+ const statements = renderPlan(dialect, ops);
34
+
35
+ if (opts.dryRun) {
36
+ console.log(c.bold("\nSQL (dry run):"));
37
+ for (const s of statements) console.log(c.dim(s));
38
+ return statements;
39
+ }
40
+
41
+ const destructive = ops.filter(isDestructive);
42
+ if (destructive.length > 0 && !opts.yes) {
43
+ console.log(
44
+ c.yellow(`\n⚠ ${destructive.length} destructive operation(s) will delete data or objects.`),
45
+ );
46
+ const ok = await confirm("Apply these changes?");
47
+ if (!ok) {
48
+ console.log(c.dim("Aborted. No changes made."));
49
+ return [];
50
+ }
51
+ }
52
+
53
+ console.log(c.bold("\nApplying..."));
54
+ for (const sql of statements) {
55
+ if (sql.startsWith("--")) {
56
+ console.log(c.yellow(" " + sql));
57
+ continue;
58
+ }
59
+ await db.mutate(sql);
60
+ console.log(c.dim(" ✓ " + sql.split("\n")[0]));
61
+ }
62
+ console.log(c.green(`\n✓ Applied ${statements.filter((s) => !s.startsWith("--")).length} statement(s).`));
63
+ return statements;
64
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { loadConfig } from "./config";
2
+ import { push } from "./commands/push";
3
+ import { pull } from "./commands/pull";
4
+ import { migrate } from "./commands/migrate";
5
+ import { generate } from "./commands/generate";
6
+ import { c } from "./prompt";
7
+
8
+ interface Flags {
9
+ _: string[];
10
+ config?: string;
11
+ out?: string;
12
+ output?: string;
13
+ dir?: string;
14
+ name?: string;
15
+ yes: boolean;
16
+ dryRun: boolean;
17
+ }
18
+
19
+ function parseArgs(argv: string[]): Flags {
20
+ const flags: Flags = { _: [], yes: false, dryRun: false };
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const a = argv[i]!;
23
+ switch (a) {
24
+ case "-y":
25
+ case "--yes":
26
+ flags.yes = true;
27
+ break;
28
+ case "--dry-run":
29
+ flags.dryRun = true;
30
+ break;
31
+ case "-c":
32
+ case "--config":
33
+ flags.config = argv[++i];
34
+ break;
35
+ case "-o":
36
+ case "--out":
37
+ flags.out = argv[++i];
38
+ break;
39
+ case "--output":
40
+ flags.output = argv[++i];
41
+ break;
42
+ case "--dir":
43
+ flags.dir = argv[++i];
44
+ break;
45
+ case "-n":
46
+ case "--name":
47
+ flags.name = argv[++i];
48
+ break;
49
+ default:
50
+ flags._.push(a);
51
+ }
52
+ }
53
+ return flags;
54
+ }
55
+
56
+ const HELP = `${c.bold("agnes")} — schema toolkit for agnes-rs
57
+
58
+ ${c.bold("Usage:")}
59
+ agnes <command> [options]
60
+
61
+ ${c.bold("Commands:")}
62
+ push Sync the database to match schema.ts (create/alter/drop)
63
+ pull Introspect the database and (re)generate schema.ts
64
+ migrate Generate a versioned SQL migration from drift, then apply pending
65
+ generate Emit a pre-wired AgnesClient module (db.ts/db.js) from the config
66
+
67
+ ${c.bold("Options:")}
68
+ -c, --config <path> Config file (default: agnes.config.ts)
69
+ -o, --out <path> [pull] Output schema file (default: config.out or schema.ts)
70
+ --output <path> [generate] Output client module (default: config.output)
71
+ --dir <path> [migrate] Migrations directory (default: migrations)
72
+ -n, --name <name> [migrate] Name for the generated migration
73
+ -y, --yes Skip confirmation for destructive operations
74
+ --dry-run Show the plan/SQL without executing
75
+ -h, --help Show this help
76
+ `;
77
+
78
+ export async function run(argv: string[]): Promise<void> {
79
+ const flags = parseArgs(argv);
80
+ const command = flags._[0];
81
+
82
+ if (!command || flags._.includes("help") || argv.includes("-h") || argv.includes("--help")) {
83
+ console.log(HELP);
84
+ return;
85
+ }
86
+
87
+ try {
88
+ const config = await loadConfig(flags.config);
89
+ switch (command) {
90
+ case "push":
91
+ await push(config, { yes: flags.yes, dryRun: flags.dryRun });
92
+ break;
93
+ case "pull":
94
+ await pull(config, { out: flags.out, yes: flags.yes });
95
+ break;
96
+ case "migrate":
97
+ await migrate(config, {
98
+ yes: flags.yes,
99
+ dryRun: flags.dryRun,
100
+ dir: flags.dir,
101
+ name: flags.name,
102
+ });
103
+ break;
104
+ case "generate":
105
+ await generate(config, { output: flags.output, yes: flags.yes });
106
+ break;
107
+ default:
108
+ console.error(c.red(`Unknown command: ${command}`));
109
+ console.log(HELP);
110
+ process.exitCode = 1;
111
+ }
112
+ } catch (err) {
113
+ console.error(c.red(`\n✗ ${err instanceof Error ? err.message : String(err)}`));
114
+ process.exitCode = 1;
115
+ }
116
+ }
@@ -0,0 +1,74 @@
1
+ import { dirname, relative, resolve } from "node:path";
2
+ import type { AgnesConfig } from "../config";
3
+ import { c } from "../prompt";
4
+
5
+ export interface GenerateArgs {
6
+ output?: string;
7
+ yes?: boolean;
8
+ }
9
+
10
+ /** Turn a filesystem path into a relative ESM import specifier (no extension). */
11
+ function toImportSpecifier(fromFile: string, toModule: string): string {
12
+ let rel = relative(dirname(fromFile), toModule).replace(/\\/g, "/");
13
+ rel = rel.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
14
+ if (!rel.startsWith(".")) rel = `./${rel}`;
15
+ return rel;
16
+ }
17
+
18
+ function renderConfigObject(config: AgnesConfig): string {
19
+ const lines: string[] = [
20
+ ` driver: ${JSON.stringify(config.driver)},`,
21
+ ` url: ${JSON.stringify(config.url)},`,
22
+ ];
23
+ if (config.maxConnections !== undefined)
24
+ lines.push(` maxConnections: ${config.maxConnections},`);
25
+ if (config.cache) {
26
+ const parts = [`enabled: ${config.cache.enabled}`];
27
+ if (config.cache.walPath !== undefined) parts.push(`walPath: ${JSON.stringify(config.cache.walPath)}`);
28
+ if (config.cache.compactionThreshold !== undefined)
29
+ parts.push(`compactionThreshold: ${config.cache.compactionThreshold}`);
30
+ lines.push(` cache: { ${parts.join(", ")} },`);
31
+ }
32
+ return lines.join("\n");
33
+ }
34
+
35
+ function renderClient(config: AgnesConfig, schemaImport: string): string {
36
+ return (
37
+ `// Generated by \`agnes generate\`. Do not edit — re-run to regenerate.\n` +
38
+ `import { AgnesClient } from "agnes-library";\n` +
39
+ `import { schema } from ${JSON.stringify(schemaImport)};\n\n` +
40
+ `export const db = await AgnesClient.create(\n` +
41
+ ` {\n${renderConfigObject(config)}\n },\n` +
42
+ ` schema,\n` +
43
+ `);\n`
44
+ );
45
+ }
46
+
47
+ /** Emit a ready-to-import AgnesClient module wired to the config. */
48
+ export async function generate(config: AgnesConfig, args: GenerateArgs): Promise<void> {
49
+ const outRel = args.output ?? config.output;
50
+ if (!outRel) {
51
+ throw new Error(
52
+ `No output path. Set \`output\` in agnes.config.ts (e.g. "src/services/db.ts") or pass --output <path>.`,
53
+ );
54
+ }
55
+
56
+ const outPath = resolve(process.cwd(), outRel);
57
+ const schemaFsPath = resolve(process.cwd(), config.schemaPath ?? config.out ?? "schema.ts");
58
+ const schemaImport = toImportSpecifier(outPath, schemaFsPath);
59
+
60
+ const source = renderClient(config, schemaImport);
61
+
62
+ if (await Bun.file(outPath).exists() && !args.yes) {
63
+ console.log(c.yellow(`⚠ ${outPath} already exists and will be overwritten.`));
64
+ const { confirm } = await import("../prompt");
65
+ if (!(await confirm("Overwrite it?"))) {
66
+ console.log(c.dim("Aborted. File left unchanged."));
67
+ return;
68
+ }
69
+ }
70
+
71
+ await Bun.write(outPath, source);
72
+ console.log(c.green(`✓ Generated client → ${outPath}`));
73
+ console.log(c.dim(` imports schema from ${schemaImport}`));
74
+ }
@@ -0,0 +1,146 @@
1
+ import { resolve } from "node:path";
2
+ import { readdir } from "node:fs/promises";
3
+ import type { AgnesConfig } from "../config";
4
+ import type { Dialect } from "../dialect";
5
+ import { openDb, type CliDb } from "../db";
6
+ import { diffSchemas } from "../diff";
7
+ import { renderPlan } from "../generate";
8
+ import { schemaToIR } from "../ir";
9
+ import { normalizeIR } from "../normalize";
10
+ import { introspect } from "../introspect";
11
+ import { c, confirm } from "../prompt";
12
+
13
+ export interface MigrateArgs {
14
+ yes?: boolean;
15
+ dryRun?: boolean;
16
+ dir?: string;
17
+ name?: string;
18
+ }
19
+
20
+ const DESTRUCTIVE = /\bDROP\s+(TABLE|COLUMN|CONSTRAINT|FOREIGN\s+KEY)\b/i;
21
+
22
+ function trackingTableDdl(dialect: Dialect): string {
23
+ const nameType = dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
24
+ return (
25
+ `CREATE TABLE IF NOT EXISTS _agnes_migrations (` +
26
+ `name ${nameType} PRIMARY KEY, ` +
27
+ `applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`
28
+ );
29
+ }
30
+
31
+ function timestamp(): string {
32
+ const d = new Date();
33
+ const p = (n: number, w = 2) => String(n).padStart(w, "0");
34
+ return (
35
+ `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}` +
36
+ `${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
37
+ );
38
+ }
39
+
40
+ function splitStatements(sql: string): string[] {
41
+ return sql
42
+ .split(";")
43
+ // Strip full-line SQL comments so a leading `-- header` doesn't swallow the
44
+ // statement that follows it on the next line.
45
+ .map((chunk) =>
46
+ chunk
47
+ .split("\n")
48
+ .filter((line) => !line.trim().startsWith("--"))
49
+ .join("\n")
50
+ .trim(),
51
+ )
52
+ .filter((s) => s.length > 0);
53
+ }
54
+
55
+ /** Generate a new migration from schema drift, then apply all pending files. */
56
+ export async function migrate(config: AgnesConfig, args: MigrateArgs): Promise<void> {
57
+ console.log(c.cyan(`agnes migrate → ${config.driver}`));
58
+ const db = await openDb(config);
59
+ const dir = resolve(process.cwd(), args.dir ?? config.migrationsDir ?? "migrations");
60
+
61
+ await db.mutate(trackingTableDdl(config.driver));
62
+
63
+ // 1. Detect drift and write a new migration file.
64
+ const desired = normalizeIR(schemaToIR(config.schema), config.driver);
65
+ const current = normalizeIR(await introspect(db, config.driver), config.driver);
66
+ const ops = diffSchemas(desired, current);
67
+
68
+ if (ops.length > 0) {
69
+ const statements = renderPlan(config.driver, ops);
70
+ const fileName = `${timestamp()}_${args.name ?? "auto"}.sql`;
71
+ const filePath = resolve(dir, fileName);
72
+ const body = statements.map((s) => (s.startsWith("--") ? s : `${s}`)).join("\n\n") + "\n";
73
+
74
+ if (args.dryRun) {
75
+ console.log(c.bold(`\nWould create ${fileName}:`));
76
+ console.log(c.dim(body));
77
+ } else {
78
+ await Bun.write(filePath, `-- Migration ${fileName}\n-- Generated by \`agnes migrate\`\n\n${body}`);
79
+ console.log(c.green(`✓ Created migration ${fileName} (${statements.length} statement(s))`));
80
+ }
81
+ } else {
82
+ console.log(c.dim("No schema drift — no new migration generated."));
83
+ }
84
+
85
+ // 2. Apply pending migration files.
86
+ await applyPending(db, config.driver, dir, args);
87
+ }
88
+
89
+ async function applyPending(
90
+ db: CliDb,
91
+ dialect: Dialect,
92
+ dir: string,
93
+ args: MigrateArgs,
94
+ ): Promise<void> {
95
+ let files: string[];
96
+ try {
97
+ files = (await readdir(dir)).filter((f) => f.endsWith(".sql")).sort();
98
+ } catch {
99
+ files = [];
100
+ }
101
+
102
+ const appliedRows = await db.query<{ name: string }>(`SELECT name FROM _agnes_migrations`);
103
+ const applied = new Set(appliedRows.map((r) => String(r.name)));
104
+ const pending = files.filter((f) => !applied.has(f));
105
+
106
+ if (pending.length === 0) {
107
+ console.log(c.green("\n✓ No pending migrations."));
108
+ return;
109
+ }
110
+
111
+ console.log(c.bold(`\nPending migrations (${pending.length}):`));
112
+ for (const f of pending) console.log(" " + c.green(f));
113
+
114
+ if (args.dryRun) {
115
+ console.log(c.dim("\nDry run — not applied."));
116
+ return;
117
+ }
118
+
119
+ // Confirm if any pending file contains destructive statements.
120
+ const bodies = new Map<string, string>();
121
+ let anyDestructive = false;
122
+ for (const f of pending) {
123
+ const text = await Bun.file(resolve(dir, f)).text();
124
+ bodies.set(f, text);
125
+ if (DESTRUCTIVE.test(text)) anyDestructive = true;
126
+ }
127
+
128
+ if (anyDestructive && !args.yes) {
129
+ console.log(c.yellow("\n⚠ Some pending migrations contain destructive statements (DROP)."));
130
+ const ok = await confirm("Apply all pending migrations?");
131
+ if (!ok) {
132
+ console.log(c.dim("Aborted. No migrations applied."));
133
+ return;
134
+ }
135
+ }
136
+
137
+ for (const f of pending) {
138
+ console.log(c.bold(`\nApplying ${f}...`));
139
+ for (const stmt of splitStatements(bodies.get(f)!)) {
140
+ await db.mutate(stmt);
141
+ console.log(c.dim(" ✓ " + stmt.split("\n")[0]));
142
+ }
143
+ await db.mutate(`INSERT INTO _agnes_migrations (name) VALUES (${dialect === "postgres" ? "$1" : "?"})`, [f]);
144
+ }
145
+ console.log(c.green(`\n✓ Applied ${pending.length} migration(s).`));
146
+ }
@@ -0,0 +1,38 @@
1
+ import { resolve } from "node:path";
2
+ import type { AgnesConfig } from "../config";
3
+ import { openDb } from "../db";
4
+ import { introspect } from "../introspect";
5
+ import { printSchema } from "../print";
6
+ import { c, confirm } from "../prompt";
7
+
8
+ export interface PullArgs {
9
+ out?: string;
10
+ yes?: boolean;
11
+ }
12
+
13
+ /** Introspect the database and (re)generate schema.ts to mirror it. */
14
+ export async function pull(config: AgnesConfig, args: PullArgs): Promise<void> {
15
+ console.log(c.cyan(`agnes pull ← ${config.driver}`));
16
+ const db = await openDb(config);
17
+
18
+ const current = await introspect(db, config.driver);
19
+ const tableCount = Object.keys(current).length;
20
+ const source = printSchema(current);
21
+
22
+ const outPath = resolve(process.cwd(), args.out ?? config.out ?? "schema.ts");
23
+ const exists = await Bun.file(outPath).exists();
24
+
25
+ console.log(c.green(`\n✓ Introspected ${tableCount} table(s).`));
26
+
27
+ if (exists && !args.yes) {
28
+ console.log(c.yellow(`\n⚠ ${outPath} already exists and will be overwritten.`));
29
+ const ok = await confirm("Overwrite it?");
30
+ if (!ok) {
31
+ console.log(c.dim("Aborted. File left unchanged."));
32
+ return;
33
+ }
34
+ }
35
+
36
+ await Bun.write(outPath, source);
37
+ console.log(c.green(`✓ Wrote ${outPath}`));
38
+ }
@@ -0,0 +1,25 @@
1
+ import { applyPlan } from "../apply";
2
+ import type { AgnesConfig } from "../config";
3
+ import { openDb } from "../db";
4
+ import { diffSchemas } from "../diff";
5
+ import { schemaToIR } from "../ir";
6
+ import { normalizeIR } from "../normalize";
7
+ import { introspect } from "../introspect";
8
+ import { c } from "../prompt";
9
+
10
+ export interface PushArgs {
11
+ yes?: boolean;
12
+ dryRun?: boolean;
13
+ }
14
+
15
+ /** Sync the database to match schema.ts (create/alter/drop). */
16
+ export async function push(config: AgnesConfig, args: PushArgs): Promise<void> {
17
+ console.log(c.cyan(`agnes push → ${config.driver}`));
18
+ const db = await openDb(config);
19
+
20
+ const desired = normalizeIR(schemaToIR(config.schema), config.driver);
21
+ const current = normalizeIR(await introspect(db, config.driver), config.driver);
22
+ const ops = diffSchemas(desired, current);
23
+
24
+ await applyPlan(db, config.driver, ops, args);
25
+ }