@onreza/sqlx-js 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +129 -36
  2. package/ROADMAP.md +3 -4
  3. package/dist/bin/sqlx-js.js +137 -26
  4. package/dist/src/artifacts.d.ts +9 -0
  5. package/dist/src/artifacts.js +26 -0
  6. package/dist/src/cache.d.ts +17 -0
  7. package/dist/src/cache.js +83 -1
  8. package/dist/src/codegen.d.ts +2 -1
  9. package/dist/src/codegen.js +34 -5
  10. package/dist/src/commands/doctor.d.ts +16 -0
  11. package/dist/src/commands/doctor.js +196 -0
  12. package/dist/src/commands/init.d.ts +1 -0
  13. package/dist/src/commands/init.js +36 -9
  14. package/dist/src/commands/migrate.js +7 -22
  15. package/dist/src/commands/pgschema.d.ts +31 -0
  16. package/dist/src/commands/pgschema.js +208 -0
  17. package/dist/src/commands/prepare.d.ts +40 -5
  18. package/dist/src/commands/prepare.js +344 -58
  19. package/dist/src/commands/watch.d.ts +1 -0
  20. package/dist/src/commands/watch.js +23 -9
  21. package/dist/src/config.d.ts +27 -0
  22. package/dist/src/config.js +141 -11
  23. package/dist/src/function-cache.d.ts +18 -0
  24. package/dist/src/function-cache.js +38 -0
  25. package/dist/src/index.d.ts +6 -2
  26. package/dist/src/index.js +2 -1
  27. package/dist/src/pg/functions.d.ts +4 -0
  28. package/dist/src/pg/functions.js +150 -0
  29. package/dist/src/pg/oids.js +2 -0
  30. package/dist/src/pg/schema.d.ts +1 -0
  31. package/dist/src/pg/schema.js +3 -0
  32. package/dist/src/postgres-runtime.d.ts +3 -0
  33. package/dist/src/postgres-runtime.js +66 -29
  34. package/dist/src/runtime.d.ts +36 -3
  35. package/dist/src/runtime.js +91 -22
  36. package/dist/src/scan/scanner.d.ts +9 -2
  37. package/dist/src/scan/scanner.js +79 -28
  38. package/dist/src/typed.d.ts +10 -0
  39. package/package.json +6 -5
@@ -2,11 +2,14 @@
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { runPrepare } from "../src/commands/prepare.js";
5
+ import { PrepareFatalError, runPrepare, } from "../src/commands/prepare.js";
6
6
  import { runWatch } from "../src/commands/watch.js";
7
7
  import { migrateArchiveList, migrateArchiveRestore, migrateCheck, migrateDev, migrateRun, migrateInfo, migrateRevert, migrateAdd, migrateSquash, migrateVerify, } from "../src/commands/migrate.js";
8
8
  import { applyShadowMigrations, runSchemaCheck, runSchemaDump } from "../src/commands/schema.js";
9
9
  import { runInit } from "../src/commands/init.js";
10
+ import { runPgschemaCommand, runPgschemaInstall } from "../src/commands/pgschema.js";
11
+ import { runDoctor } from "../src/commands/doctor.js";
12
+ import { assertSupportedRuntime, loadConfig, loadRootEnv } from "../src/config.js";
10
13
  function packageVersion() {
11
14
  const here = dirname(fileURLToPath(import.meta.url));
12
15
  for (const path of [join(here, "../package.json"), join(here, "../../package.json")]) {
@@ -23,8 +26,10 @@ function help() {
23
26
  console.error(`sqlx-js — compile-time-checked SQL for TypeScript + Postgres (v${VERSION})
24
27
 
25
28
  usage:
26
- sqlx-js init [--root <dir>]
27
- sqlx-js prepare [--check | --watch] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
29
+ sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
30
+ sqlx-js doctor [--root <dir>] [--json]
31
+ sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
32
+ sqlx-js prepare [--check | --verify | --watch] [--json] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
28
33
  sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
29
34
  sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
30
35
  sqlx-js --version
@@ -38,11 +43,12 @@ flags:
38
43
  --root <dir> scan root (default: cwd)
39
44
  --dts <path> declarations output (default: <root>/sqlx-js-env.d.ts)
40
45
  --check offline mode: verify scanned queries exist in cache, no DB
46
+ --verify prepare against DB/shadow and compare committed generated artifacts
41
47
  --watch re-prepare on file change (persistent PG connection)
42
48
  --no-prune keep orphaned cache entries (default: remove)
43
49
  --migrations <dir> migrations directory (default: <root>/migrations)
44
50
  --dry-run validate and print migrate run/revert plan without applying migrations
45
- --json machine-readable output for migrate info/check and migrate run/revert --dry-run
51
+ --json machine-readable output for prepare and migration inspection/dry-run commands
46
52
  --force allow archive restore to overwrite existing migration files
47
53
  --lock-timeout <ms> advisory-lock acquisition timeout for migrate run/revert/dev/verify/squash
48
54
  --shadow-url <url> use an existing disposable shadow DB instead of auto-creating one
@@ -52,23 +58,26 @@ flags:
52
58
  --schema <path> schema snapshot path (default: <root>/.sqlx-js/schema/schema.json)
53
59
  --manifest <path> LLM schema manifest path (default: <root>/.sqlx-js/schema/schema.md)
54
60
  --no-manifest skip writing the LLM schema manifest during schema dump
61
+ --schema-provider <name> init schema workflow: builtin (default) or pgschema
55
62
  `);
56
63
  process.exit(2);
57
64
  }
65
+ const passthroughIndex = process.argv.indexOf("--");
66
+ const cliArgv = passthroughIndex >= 0 ? process.argv.slice(0, passthroughIndex) : process.argv;
67
+ const passthroughArgs = passthroughIndex >= 0 ? process.argv.slice(passthroughIndex + 1) : [];
58
68
  function arg(name, def) {
59
- const argv = process.argv;
60
69
  const eq = `${name}=`;
61
- for (let i = 0; i < argv.length; i++) {
62
- const a = argv[i];
70
+ for (let i = 0; i < cliArgv.length; i++) {
71
+ const a = cliArgv[i];
63
72
  if (a === name)
64
- return argv[i + 1] ?? def;
73
+ return cliArgv[i + 1] ?? def;
65
74
  if (a.startsWith(eq))
66
75
  return a.slice(eq.length);
67
76
  }
68
77
  return def;
69
78
  }
70
79
  function flag(name) {
71
- for (const a of process.argv) {
80
+ for (const a of cliArgv) {
72
81
  if (a === name)
73
82
  return true;
74
83
  }
@@ -83,6 +92,26 @@ if (cmd === "--help" || cmd === "-h" || !cmd) {
83
92
  help();
84
93
  }
85
94
  const root = resolve(arg("--root", process.cwd()));
95
+ if (cmd !== "doctor") {
96
+ try {
97
+ assertSupportedRuntime();
98
+ }
99
+ catch (e) {
100
+ console.error(e.message);
101
+ process.exit(2);
102
+ }
103
+ }
104
+ let envError;
105
+ try {
106
+ loadRootEnv(root);
107
+ }
108
+ catch (e) {
109
+ envError = e.message;
110
+ if (cmd !== "doctor") {
111
+ console.error(envError);
112
+ process.exit(2);
113
+ }
114
+ }
86
115
  const databaseUrl = process.env.DATABASE_URL ?? "";
87
116
  const shadowUrlArg = arg("--shadow-url");
88
117
  const shadowUrl = shadowUrlArg ?? process.env.SHADOW_DATABASE_URL;
@@ -96,32 +125,95 @@ const schemaPath = schemaArg ? resolve(schemaArg) : join(root, ".sqlx-js/schema/
96
125
  const manifestArg = arg("--manifest");
97
126
  const manifestPath = manifestArg ? resolve(manifestArg) : join(root, ".sqlx-js/schema/schema.md");
98
127
  if (cmd === "init") {
99
- runInit({ root });
128
+ const provider = arg("--schema-provider", "builtin");
129
+ if (provider !== "builtin" && provider !== "pgschema") {
130
+ console.error("--schema-provider must be builtin or pgschema");
131
+ process.exit(2);
132
+ }
133
+ runInit({ root, schemaProvider: provider });
134
+ }
135
+ else if (cmd === "doctor") {
136
+ await runDoctor({ root, databaseUrl, cacheDir, dtsPath, json: flag("--json"), envError });
137
+ }
138
+ else if (cmd === "db") {
139
+ const sub = process.argv[3];
140
+ if (sub === "install") {
141
+ try {
142
+ await runPgschemaInstall({ root });
143
+ }
144
+ catch (e) {
145
+ console.error(e.message);
146
+ process.exit(2);
147
+ }
148
+ }
149
+ else {
150
+ if (sub !== "check" && sub !== "plan" && sub !== "apply")
151
+ help();
152
+ try {
153
+ runPgschemaCommand({
154
+ root,
155
+ databaseUrl,
156
+ config: await loadConfig(root),
157
+ subcommand: sub,
158
+ passthrough: passthroughArgs,
159
+ });
160
+ }
161
+ catch (e) {
162
+ console.error(e.message);
163
+ process.exit(2);
164
+ }
165
+ }
100
166
  }
101
167
  else if (cmd === "prepare") {
102
- if (flag("--check") && shadowUrlArg) {
103
- console.error("--shadow-url cannot be used with prepare --check; use live prepare or schema check --shadow-url");
104
- process.exit(2);
168
+ const prepareCheck = flag("--check");
169
+ const prepareVerify = flag("--verify");
170
+ const prepareWatch = flag("--watch");
171
+ const prepareJson = flag("--json");
172
+ const prepareMode = prepareVerify ? "verify" : prepareCheck ? "check" : "prepare";
173
+ const failPrepare = (message, phase, exitCode = 2, location = {}) => {
174
+ if (prepareJson) {
175
+ console.log(JSON.stringify({
176
+ formatVersion: 1,
177
+ ok: false,
178
+ mode: prepareMode,
179
+ sites: 0,
180
+ entries: 0,
181
+ failures: 1,
182
+ pruned: 0,
183
+ functions: 0,
184
+ diagnostics: [{ severity: "error", phase, message, ...location }],
185
+ }, null, 2));
186
+ }
187
+ else {
188
+ console.error(message);
189
+ }
190
+ process.exit(exitCode);
191
+ };
192
+ if ([prepareCheck, prepareVerify, prepareWatch].filter(Boolean).length > 1) {
193
+ failPrepare("--check, --verify, and --watch are mutually exclusive", "config");
194
+ }
195
+ if (prepareCheck && shadowUrlArg) {
196
+ failPrepare("--shadow-url cannot be used with prepare --check; use live prepare or schema check --shadow-url", "config");
105
197
  }
106
- const prepareShadowUrl = flag("--check") ? undefined : shadowUrl;
198
+ if (prepareWatch && prepareJson) {
199
+ failPrepare("--watch and --json are mutually exclusive", "config");
200
+ }
201
+ const prepareShadowUrl = prepareCheck ? undefined : shadowUrl;
107
202
  const prepareDatabaseUrl = prepareShadowUrl ?? databaseUrl;
108
- if (!flag("--check") && !prepareDatabaseUrl) {
109
- console.error("DATABASE_URL is required for prepare (use --check for offline)");
110
- process.exit(2);
203
+ if (!prepareCheck && !prepareDatabaseUrl) {
204
+ failPrepare("DATABASE_URL is required for prepare (use --check for offline)", "connect");
111
205
  }
112
206
  const opts = {
113
207
  root,
114
208
  databaseUrl: prepareDatabaseUrl,
115
209
  cacheDir,
116
210
  dtsPath,
117
- check: flag("--check"),
211
+ check: prepareCheck,
212
+ verify: prepareVerify,
213
+ json: prepareJson,
118
214
  prune: !flag("--no-prune"),
119
215
  };
120
- if (flag("--watch")) {
121
- if (flag("--check")) {
122
- console.error("--watch and --check are mutually exclusive");
123
- process.exit(2);
124
- }
216
+ if (prepareWatch) {
125
217
  await runWatch({
126
218
  ...opts,
127
219
  ...(prepareShadowUrl
@@ -135,9 +227,28 @@ else if (cmd === "prepare") {
135
227
  });
136
228
  }
137
229
  else {
138
- if (prepareShadowUrl)
139
- await applyShadowMigrations(prepareShadowUrl, migrationsDir);
140
- await runPrepare(opts);
230
+ if (prepareShadowUrl) {
231
+ try {
232
+ await applyShadowMigrations(prepareShadowUrl, migrationsDir, prepareJson ? () => { } : console.log);
233
+ }
234
+ catch (e) {
235
+ failPrepare(e.message, "shadow", 1);
236
+ }
237
+ }
238
+ try {
239
+ await runPrepare(opts);
240
+ }
241
+ catch (e) {
242
+ const message = e.message;
243
+ const phase = e instanceof PrepareFatalError
244
+ ? e.phase
245
+ : prepareVerify
246
+ ? "verify"
247
+ : "scan";
248
+ failPrepare(message, phase, 1, e instanceof PrepareFatalError
249
+ ? { file: e.file, line: e.line, column: e.column }
250
+ : {});
251
+ }
141
252
  }
142
253
  }
143
254
  else if (cmd === "schema") {
@@ -0,0 +1,9 @@
1
+ export type ArtifactSet = {
2
+ cacheDir: string;
3
+ dtsPath: string;
4
+ };
5
+ export type ArtifactComparison = {
6
+ ok: boolean;
7
+ changed: string[];
8
+ };
9
+ export declare function compareArtifacts(expected: ArtifactSet, actual: ArtifactSet): ArtifactComparison;
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CACHE_MANIFEST_FILE } from "./cache.js";
4
+ function readGeneratedFiles(set) {
5
+ const files = new Map();
6
+ if (existsSync(set.cacheDir)) {
7
+ for (const name of readdirSync(set.cacheDir).sort()) {
8
+ if (name !== CACHE_MANIFEST_FILE && !/^[0-9a-f]{16}\.json$/.test(name))
9
+ continue;
10
+ files.set(`cache/${name}`, readFileSync(join(set.cacheDir, name), "utf8"));
11
+ }
12
+ const functions = join(set.cacheDir, "functions/functions.json");
13
+ if (existsSync(functions))
14
+ files.set("cache/functions/functions.json", readFileSync(functions, "utf8"));
15
+ }
16
+ if (existsSync(set.dtsPath))
17
+ files.set("sqlx-js-env.d.ts", readFileSync(set.dtsPath, "utf8"));
18
+ return files;
19
+ }
20
+ export function compareArtifacts(expected, actual) {
21
+ const left = readGeneratedFiles(expected);
22
+ const right = readGeneratedFiles(actual);
23
+ const names = new Set([...left.keys(), ...right.keys()]);
24
+ const changed = [...names].filter((name) => left.get(name) !== right.get(name)).sort();
25
+ return { ok: changed.length === 0, changed };
26
+ }
@@ -1,3 +1,11 @@
1
+ export declare const CACHE_FORMAT_VERSION = 2;
2
+ export declare const GENERATOR_REVISION = 3;
3
+ export declare const CACHE_MANIFEST_FILE = "cache-manifest.json";
4
+ export type CacheManifest = {
5
+ cacheFormat: typeof CACHE_FORMAT_VERSION;
6
+ generatorRevision: typeof GENERATOR_REVISION;
7
+ configHash: string;
8
+ };
1
9
  export type CacheColumn = {
2
10
  name: string;
3
11
  typeOid: number;
@@ -19,6 +27,7 @@ export type CacheEntry = {
19
27
  reason: string;
20
28
  };
21
29
  };
30
+ export declare function portableCacheOid(oid: number): number;
22
31
  export declare function fingerprint(query: string): string;
23
32
  export declare function effectiveNullable(c: CacheColumn): boolean;
24
33
  export declare class Cache {
@@ -28,6 +37,10 @@ export declare class Cache {
28
37
  has(fp: string): boolean;
29
38
  read(fp: string): CacheEntry | null;
30
39
  write(fp: string, entry: CacheEntry): void;
40
+ replaceAll(entries: Iterable<{
41
+ fp: string;
42
+ entry: CacheEntry;
43
+ }>, prune?: boolean): string[];
31
44
  list(): {
32
45
  fp: string;
33
46
  entry: CacheEntry;
@@ -35,3 +48,7 @@ export declare class Cache {
35
48
  remove(fp: string): void;
36
49
  prune(keep: Iterable<string>): string[];
37
50
  }
51
+ export declare function cacheManifestPath(cacheDir: string): string;
52
+ export declare function writeCacheManifest(cacheDir: string, configHash: string): void;
53
+ export declare function readCacheManifest(cacheDir: string): CacheManifest | null;
54
+ export declare function assertCacheManifest(cacheDir: string, configHash: string): CacheManifest;
package/dist/src/cache.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { isBuiltinOid } from "./pg/oids.js";
5
+ export const CACHE_FORMAT_VERSION = 2;
6
+ export const GENERATOR_REVISION = 3;
7
+ export const CACHE_MANIFEST_FILE = "cache-manifest.json";
8
+ export function portableCacheOid(oid) {
9
+ return isBuiltinOid(oid) ? oid : 0;
10
+ }
4
11
  export function fingerprint(query) {
5
12
  const norm = normalizeForFingerprint(query);
6
13
  return createHash("sha256").update(norm).digest("hex").slice(0, 16);
@@ -202,11 +209,35 @@ export class Cache {
202
209
  throw err;
203
210
  }
204
211
  }
212
+ replaceAll(entries, prune = true) {
213
+ this.ensure();
214
+ const staged = [];
215
+ try {
216
+ for (const { fp, entry } of entries) {
217
+ const final = join(this.dir, `${fp}.json`);
218
+ const tmp = `${final}.tmp-${randomBytes(4).toString("hex")}`;
219
+ writeFileSync(tmp, JSON.stringify(entry, null, 2));
220
+ staged.push({ fp, tmp, final });
221
+ }
222
+ for (const item of staged)
223
+ renameSync(item.tmp, item.final);
224
+ }
225
+ catch (err) {
226
+ for (const item of staged) {
227
+ try {
228
+ unlinkSync(item.tmp);
229
+ }
230
+ catch { }
231
+ }
232
+ throw err;
233
+ }
234
+ return prune ? this.prune(staged.map((item) => item.fp)) : [];
235
+ }
205
236
  list() {
206
237
  if (!existsSync(this.dir))
207
238
  return [];
208
239
  return readdirSync(this.dir)
209
- .filter((f) => f.endsWith(".json") && !f.includes(".tmp-"))
240
+ .filter((f) => f !== CACHE_MANIFEST_FILE && f.endsWith(".json") && !f.includes(".tmp-"))
210
241
  .map((f) => {
211
242
  const fp = f.replace(/\.json$/, "");
212
243
  return { fp, entry: assertEntryShape(fp, parseEntryJson(join(this.dir, f))) };
@@ -229,3 +260,54 @@ export class Cache {
229
260
  return removed;
230
261
  }
231
262
  }
263
+ export function cacheManifestPath(cacheDir) {
264
+ return join(cacheDir, CACHE_MANIFEST_FILE);
265
+ }
266
+ export function writeCacheManifest(cacheDir, configHash) {
267
+ if (!existsSync(cacheDir))
268
+ mkdirSync(cacheDir, { recursive: true });
269
+ const path = cacheManifestPath(cacheDir);
270
+ const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
271
+ const manifest = {
272
+ cacheFormat: CACHE_FORMAT_VERSION,
273
+ generatorRevision: GENERATOR_REVISION,
274
+ configHash,
275
+ };
276
+ writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
277
+ try {
278
+ renameSync(tmp, path);
279
+ }
280
+ catch (err) {
281
+ try {
282
+ unlinkSync(tmp);
283
+ }
284
+ catch { }
285
+ throw err;
286
+ }
287
+ }
288
+ export function readCacheManifest(cacheDir) {
289
+ const path = cacheManifestPath(cacheDir);
290
+ if (!existsSync(path))
291
+ return null;
292
+ const raw = parseEntryJson(path);
293
+ if (!raw || typeof raw !== "object") {
294
+ throw new Error(`sqlx-js: cache manifest is malformed: ${path}`);
295
+ }
296
+ const value = raw;
297
+ if (value.cacheFormat !== CACHE_FORMAT_VERSION ||
298
+ value.generatorRevision !== GENERATOR_REVISION ||
299
+ typeof value.configHash !== "string") {
300
+ throw new Error(`sqlx-js: cache manifest is stale: ${path}. Run \`sqlx-js prepare\`.`);
301
+ }
302
+ return value;
303
+ }
304
+ export function assertCacheManifest(cacheDir, configHash) {
305
+ const manifest = readCacheManifest(cacheDir);
306
+ if (!manifest) {
307
+ throw new Error(`sqlx-js: cache manifest is missing. Run \`sqlx-js prepare\` to regenerate the cache.`);
308
+ }
309
+ if (manifest.configHash !== configHash) {
310
+ throw new Error("sqlx-js: cache was generated with a different jsonbTypes/customTypes config. Run `sqlx-js prepare`.");
311
+ }
312
+ return manifest;
313
+ }
@@ -1,2 +1,3 @@
1
1
  import { type CacheEntry } from "./cache.js";
2
- export declare function emitDts(outPath: string, entries: CacheEntry[]): void;
2
+ import type { FunctionEntry } from "./function-cache.js";
3
+ export declare function emitDts(outPath: string, entries: CacheEntry[], functions?: FunctionEntry[]): void;
@@ -1,4 +1,5 @@
1
- import { writeFileSync, mkdirSync } from "node:fs";
1
+ import { writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
2
3
  import { dirname } from "node:path";
3
4
  import { effectiveNullable } from "./cache.js";
4
5
  function entrySignature(e) {
@@ -16,18 +17,29 @@ function entrySignature(e) {
16
17
  });
17
18
  return { params, row: `{ ${cols.join("; ")} }` };
18
19
  }
19
- export function emitDts(outPath, entries) {
20
+ export function emitDts(outPath, entries, functions = []) {
20
21
  mkdirSync(dirname(outPath), { recursive: true });
21
22
  const lines = [];
22
23
  lines.push("// AUTO-GENERATED by sqlx-js. Do not edit.");
23
24
  lines.push("// Run `sqlx-js prepare` to regenerate.");
24
25
  lines.push("");
25
- emitModule(lines, "@onreza/sqlx-js", entries);
26
+ emitModule(lines, "@onreza/sqlx-js", entries, functions);
26
27
  lines.push("");
27
28
  lines.push("export {};");
28
- writeFileSync(outPath, lines.join("\n") + "\n");
29
+ const tmp = `${outPath}.tmp-${randomBytes(4).toString("hex")}`;
30
+ writeFileSync(tmp, lines.join("\n") + "\n");
31
+ try {
32
+ renameSync(tmp, outPath);
33
+ }
34
+ catch (err) {
35
+ try {
36
+ unlinkSync(tmp);
37
+ }
38
+ catch { }
39
+ throw err;
40
+ }
29
41
  }
30
- function emitModule(lines, moduleName, entries) {
42
+ function emitModule(lines, moduleName, entries, functions) {
31
43
  lines.push(`declare module ${JSON.stringify(moduleName)} {`);
32
44
  lines.push(" interface KnownQueries {");
33
45
  const inlineSeen = new Set();
@@ -70,5 +82,22 @@ function emitModule(lines, moduleName, entries) {
70
82
  lines.push(` ${JSON.stringify(path)}: { params: ${params}; row: ${row} };`);
71
83
  }
72
84
  lines.push(" }");
85
+ lines.push("");
86
+ lines.push(" interface KnownFunctions {");
87
+ const functionSeen = new Set();
88
+ for (const fn of functions.slice().sort((a, b) => a.signature.localeCompare(b.signature))) {
89
+ if (functionSeen.has(fn.signature))
90
+ continue;
91
+ functionSeen.add(fn.signature);
92
+ const params = inputParams(fn);
93
+ lines.push(` ${JSON.stringify(fn.signature)}: { kind: ${JSON.stringify(fn.kind)}; params: ${params}; returns: ${fn.returns}; returnsSet: ${fn.returnsSet} };`);
94
+ }
95
+ lines.push(" }");
73
96
  lines.push("}");
74
97
  }
98
+ function inputParams(fn) {
99
+ const params = fn.params
100
+ .filter((p) => p.mode === "in" || p.mode === "inout" || p.mode === "variadic")
101
+ .map((p) => p.tsType);
102
+ return params.length === 0 ? "[]" : `[${params.join(", ")}]`;
103
+ }
@@ -0,0 +1,16 @@
1
+ export type DoctorCheck = {
2
+ name: string;
3
+ status: "ok" | "warning" | "error";
4
+ message: string;
5
+ details?: Record<string, unknown>;
6
+ };
7
+ export type DoctorOptions = {
8
+ root: string;
9
+ databaseUrl: string;
10
+ cacheDir: string;
11
+ dtsPath: string;
12
+ json?: boolean;
13
+ envError?: string;
14
+ };
15
+ export declare function inspectDoctor(opts: DoctorOptions): Promise<DoctorCheck[]>;
16
+ export declare function runDoctor(opts: DoctorOptions): Promise<void>;