askdb 1.0.0-beta.48 → 1.0.0-beta.49
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/dist/ask.d.ts +15 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +168 -0
- package/dist/ask.js.map +1 -0
- package/dist/bundle.d.ts +10 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +51 -0
- package/dist/bundle.js.map +1 -0
- package/dist/cli-error.d.ts +10 -0
- package/dist/cli-error.d.ts.map +1 -0
- package/dist/cli-error.js +39 -0
- package/dist/cli-error.js.map +1 -0
- package/dist/cli.js +27 -308
- package/dist/cli.js.map +1 -1
- package/dist/command.d.ts +56 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +139 -0
- package/dist/command.js.map +1 -0
- package/dist/dispatch.d.ts +6 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +65 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/init.d.ts +33 -20
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +273 -591
- package/dist/init.js.map +1 -1
- package/dist/introspect.d.ts +21 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +57 -156
- package/dist/introspect.js.map +1 -1
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +34 -1
- package/dist/logger.js.map +1 -1
- package/dist/prisma-install.d.ts +1 -1
- package/dist/prisma-install.js +1 -1
- package/dist/rag.d.ts +31 -0
- package/dist/rag.d.ts.map +1 -1
- package/dist/rag.js +175 -185
- package/dist/rag.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +13 -14
package/dist/init.js
CHANGED
|
@@ -4,21 +4,20 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { getBuiltinAiProviderSetup, listBuiltinAiProviderSetups } from "@askdb/ai";
|
|
7
|
-
import { ASKDB_AI_PROVIDERS } from "@askdb/config";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { ASKDB_AI_PROVIDERS, ASKDB_RAG_STORES } from "@askdb/config";
|
|
8
|
+
import { AskDbScaffoldInputError, detectPackageManager, findNearestPackageJsonDir, formatInstallCommand, isAskDbScaffoldEnvName, isLikelyWorkspaceRoot, packageManagerAddArgs, packageManagerSpawnSpec, planAskDbScaffold, supportedBuildApprovals, } from "@askdb/config/scaffold";
|
|
9
|
+
import { errorMessage } from "./cli-error.js";
|
|
10
|
+
import { runCommand } from "./command.js";
|
|
11
|
+
import { showable } from "./error-values.js";
|
|
10
12
|
const DEFAULT_CONFIG_PATH = "askdb.config.ts";
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Config rendering
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
13
|
/**
|
|
15
14
|
* Selectable AI providers, in `@askdb/ai`'s built-in table order: every id that has an
|
|
16
15
|
* `askdb.config.*` branch (`ASKDB_AI_PROVIDERS`), with the key/model env var names to
|
|
17
16
|
* scaffold. Derived from `@askdb/ai`'s `BUILTIN_AI_PROVIDERS` so it cannot drift.
|
|
18
17
|
*/
|
|
19
18
|
const AI_PROVIDER_SETUPS = listBuiltinAiProviderSetups(ASKDB_AI_PROVIDERS);
|
|
20
|
-
/** Every id `--ai-provider` accepts;
|
|
21
|
-
|
|
19
|
+
/** Every id `--ai-provider` accepts; its help line lists them. */
|
|
20
|
+
const VALID_AI_PROVIDERS = AI_PROVIDER_SETUPS.map((setup) => setup.id);
|
|
22
21
|
function aiDefaults(provider) {
|
|
23
22
|
const setup = getBuiltinAiProviderSetup(provider);
|
|
24
23
|
if (!setup)
|
|
@@ -26,166 +25,69 @@ function aiDefaults(provider) {
|
|
|
26
25
|
return setup;
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* Mirrors `tsString` in `apps/studio/src/setup.ts`.
|
|
28
|
+
* `--sqlite-file`'s value as the config reads it: a value the planner accepts as an env var
|
|
29
|
+
* name (`SQLITE_FILE`, `UPPER_SNAKE_CASE`) is a variable, anything else (`data.db`,
|
|
30
|
+
* `../db/app.db`, `/srv/app.db`) a path. No value means the `SQLITE_FILE` variable.
|
|
33
31
|
*/
|
|
34
|
-
function
|
|
35
|
-
return JSON.stringify(value);
|
|
36
|
-
}
|
|
37
|
-
/** An env var name as `--sqlite-file` accepts one, e.g. `SQLITE_FILE`. Anything else is a path. */
|
|
38
|
-
const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
39
|
-
/**
|
|
40
|
-
* The env var a SQLite `file` setting reads, or `undefined` when it's a literal
|
|
41
|
-
* path (`data.db`, `../db/app.db`, `/srv/app.db`, …). Only an env-name-shaped value
|
|
42
|
-
* is a variable. No value means the `SQLITE_FILE` default. Rendering, the
|
|
43
|
-
* `.env.example`, and the printed variable list all go through this.
|
|
44
|
-
*/
|
|
45
|
-
function sqliteFileEnv(file) {
|
|
32
|
+
function sqliteFileValue(file) {
|
|
46
33
|
if (!file)
|
|
47
|
-
return "SQLITE_FILE";
|
|
48
|
-
return
|
|
34
|
+
return { env: "SQLITE_FILE" };
|
|
35
|
+
return isAskDbScaffoldEnvName(file) ? { env: file } : { path: file };
|
|
36
|
+
}
|
|
37
|
+
/** `InitAnswers` as the planner in `@askdb/config/scaffold` takes them. It ignores what the engine doesn't read. */
|
|
38
|
+
function scaffoldAnswers(answers) {
|
|
39
|
+
const { studioExecute } = answers;
|
|
40
|
+
return {
|
|
41
|
+
database: answers.database,
|
|
42
|
+
connectionEnv: answers.connectionEnv ?? "DATABASE_URL",
|
|
43
|
+
sqliteFile: sqliteFileValue(answers.sqliteFile),
|
|
44
|
+
...(answers.prismaSchema ? { prismaSchemaPath: answers.prismaSchema } : {}),
|
|
45
|
+
schemaOut: answers.schemaOut,
|
|
46
|
+
ai: {
|
|
47
|
+
provider: answers.aiProvider,
|
|
48
|
+
keyEnv: answers.aiKeyEnv,
|
|
49
|
+
...(answers.aiModelEnv ? { modelEnv: answers.aiModelEnv } : {}),
|
|
50
|
+
},
|
|
51
|
+
rag: { store: answers.ragStore, pgvectorEnv: answers.pgvectorEnv ?? "ASKDB_PGVECTOR_URL" },
|
|
52
|
+
...(studioExecute.enabled
|
|
53
|
+
? {
|
|
54
|
+
studioExecute: {
|
|
55
|
+
provider: studioExecute.provider,
|
|
56
|
+
connectionEnv: studioExecute.connectionEnv ?? "DATABASE_URL",
|
|
57
|
+
sqliteFile: sqliteFileValue(studioExecute.sqliteFile),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
49
62
|
}
|
|
63
|
+
/** The flag that sets each planner answer, for errors. */
|
|
64
|
+
const ANSWER_FLAGS = {
|
|
65
|
+
database: "--database",
|
|
66
|
+
connectionEnv: "--connection-env",
|
|
67
|
+
sqliteFile: "--sqlite-file",
|
|
68
|
+
prismaSchemaPath: "--prisma-schema",
|
|
69
|
+
schemaOut: "--schema-out",
|
|
70
|
+
"ai.provider": "--ai-provider",
|
|
71
|
+
"ai.keyEnv": "--ai-key-env",
|
|
72
|
+
"ai.modelEnv": "--ai-model-env",
|
|
73
|
+
"rag.store": "--rag-store",
|
|
74
|
+
"rag.pgvectorEnv": "--pgvector-env",
|
|
75
|
+
"studioExecute.sqliteFile": "--sqlite-file",
|
|
76
|
+
};
|
|
50
77
|
/**
|
|
51
|
-
* The `
|
|
52
|
-
* Studio's setup wizard
|
|
53
|
-
*
|
|
78
|
+
* The config, `.env.example` and installs for `answers`, from `@askdb/config/scaffold`'s
|
|
79
|
+
* planner (shared with Studio's setup wizard). A bad answer fails with an error that names
|
|
80
|
+
* its flag, before anything is written.
|
|
54
81
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
keyEnv: answers.aiKeyEnv,
|
|
59
|
-
...(answers.aiModelEnv ? { modelEnv: answers.aiModelEnv } : {}),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function renderIntrospectionSection(answers) {
|
|
63
|
-
const { database, connectionEnv, sqliteFile, prismaSchema, schemaOut } = answers;
|
|
64
|
-
const outputDirLine = `\n outputDir: ${tsString(schemaOut)},`;
|
|
65
|
-
const connectionEnvExpr = `env(${tsString(connectionEnv ?? "DATABASE_URL")})`;
|
|
66
|
-
switch (database) {
|
|
67
|
-
case "postgres":
|
|
68
|
-
return ` introspection: {
|
|
69
|
-
provider: "postgres",
|
|
70
|
-
providerConfig: {
|
|
71
|
-
postgres: {
|
|
72
|
-
databaseUrl: ${connectionEnvExpr},
|
|
73
|
-
},
|
|
74
|
-
},${outputDirLine}
|
|
75
|
-
},`;
|
|
76
|
-
case "mysql":
|
|
77
|
-
return ` introspection: {
|
|
78
|
-
provider: "mysql",
|
|
79
|
-
providerConfig: {
|
|
80
|
-
mysql: {
|
|
81
|
-
databaseUrl: ${connectionEnvExpr},
|
|
82
|
-
},
|
|
83
|
-
},${outputDirLine}
|
|
84
|
-
},`;
|
|
85
|
-
case "sqlite": {
|
|
86
|
-
const fileEnv = sqliteFileEnv(sqliteFile);
|
|
87
|
-
const resolvedFile = fileEnv ? `env(${tsString(fileEnv)})` : tsString(sqliteFile);
|
|
88
|
-
return ` introspection: {
|
|
89
|
-
provider: "sqlite",
|
|
90
|
-
providerConfig: {
|
|
91
|
-
sqlite: {
|
|
92
|
-
file: ${resolvedFile},
|
|
93
|
-
},
|
|
94
|
-
},${outputDirLine}
|
|
95
|
-
},`;
|
|
96
|
-
}
|
|
97
|
-
case "sqlserver":
|
|
98
|
-
return ` introspection: {
|
|
99
|
-
provider: "sqlserver",
|
|
100
|
-
providerConfig: {
|
|
101
|
-
sqlserver: {
|
|
102
|
-
databaseUrl: ${connectionEnvExpr},
|
|
103
|
-
},
|
|
104
|
-
},${outputDirLine}
|
|
105
|
-
},`;
|
|
106
|
-
case "prisma": {
|
|
107
|
-
const schemaLine = prismaSchema ? `\n schemaPath: ${tsString(prismaSchema)},` : "";
|
|
108
|
-
return ` introspection: {
|
|
109
|
-
provider: "prisma",
|
|
110
|
-
providerConfig: {
|
|
111
|
-
prisma: {${schemaLine}
|
|
112
|
-
},
|
|
113
|
-
},${outputDirLine}
|
|
114
|
-
},`;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function renderRagSection(answers) {
|
|
119
|
-
const { ragStore, pgvectorEnv } = answers;
|
|
120
|
-
switch (ragStore) {
|
|
121
|
-
case "file":
|
|
122
|
-
return ` rag: {
|
|
123
|
-
embedder: "mock",
|
|
124
|
-
store: "file",
|
|
125
|
-
storeConfig: {
|
|
126
|
-
file: {},
|
|
127
|
-
},
|
|
128
|
-
},`;
|
|
129
|
-
case "memory":
|
|
130
|
-
return ` rag: {
|
|
131
|
-
embedder: "mock",
|
|
132
|
-
store: "memory",
|
|
133
|
-
storeConfig: {
|
|
134
|
-
memory: {},
|
|
135
|
-
},
|
|
136
|
-
},`;
|
|
137
|
-
case "pgvector":
|
|
138
|
-
return ` rag: {
|
|
139
|
-
embedder: "mock",
|
|
140
|
-
store: "pgvector",
|
|
141
|
-
storeConfig: {
|
|
142
|
-
pgvector: {
|
|
143
|
-
databaseUrl: env(${tsString(pgvectorEnv ?? "ASKDB_PGVECTOR_URL")}),
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
},`;
|
|
82
|
+
export function planInitScaffold(answers, specs) {
|
|
83
|
+
try {
|
|
84
|
+
return planAskDbScaffold(scaffoldAnswers(answers), { config: specs.configSpec, prisma: specs.prismaSpec });
|
|
147
85
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
const { provider } = studioExecute;
|
|
154
|
-
if (provider === "sqlite") {
|
|
155
|
-
const fileEnv = sqliteFileEnv(studioExecute.sqliteFile);
|
|
156
|
-
const fileExpr = fileEnv ? `env(${tsString(fileEnv)})` : tsString(studioExecute.sqliteFile);
|
|
157
|
-
return ` studio: {
|
|
158
|
-
execute: {
|
|
159
|
-
enabled: true,
|
|
160
|
-
provider: "sqlite",
|
|
161
|
-
file: ${fileExpr},
|
|
162
|
-
},
|
|
163
|
-
},`;
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!(error instanceof AskDbScaffoldInputError))
|
|
88
|
+
throw error;
|
|
89
|
+
throw new Error(`${ANSWER_FLAGS[error.field] ?? error.field} ${error.reason}`);
|
|
164
90
|
}
|
|
165
|
-
const urlEnv = studioExecute.connectionEnv ?? "DATABASE_URL";
|
|
166
|
-
return ` studio: {
|
|
167
|
-
execute: {
|
|
168
|
-
enabled: true,
|
|
169
|
-
provider: ${tsString(provider)},
|
|
170
|
-
databaseUrl: env(${tsString(urlEnv)}),
|
|
171
|
-
},
|
|
172
|
-
},`;
|
|
173
|
-
}
|
|
174
|
-
export function renderInitConfig(answers) {
|
|
175
|
-
const sections = [
|
|
176
|
-
aiScaffold(answers).source,
|
|
177
|
-
renderIntrospectionSection(answers),
|
|
178
|
-
renderRagSection(answers),
|
|
179
|
-
];
|
|
180
|
-
const studio = renderStudioSection(answers);
|
|
181
|
-
if (studio)
|
|
182
|
-
sections.push(studio);
|
|
183
|
-
return `import { defineConfig, env, type AskDbConfig } from "@askdb/config";
|
|
184
|
-
|
|
185
|
-
export default defineConfig({
|
|
186
|
-
${sections.join("\n")}
|
|
187
|
-
} satisfies AskDbConfig);
|
|
188
|
-
`;
|
|
189
91
|
}
|
|
190
92
|
export function resolveDefaultInitAnswers(overrides = {}) {
|
|
191
93
|
const database = overrides.database ?? "postgres";
|
|
@@ -203,7 +105,9 @@ export function resolveDefaultInitAnswers(overrides = {}) {
|
|
|
203
105
|
studioExecute = {
|
|
204
106
|
enabled: true,
|
|
205
107
|
provider: execProvider,
|
|
206
|
-
|
|
108
|
+
// On the introspection engine, execute reads the same connection env var, as the wizard does (#777).
|
|
109
|
+
connectionEnv: overrides.studioExecuteConnectionEnv ??
|
|
110
|
+
(execProvider === "sqlite" ? undefined : execProvider === database ? connectionEnv : "DATABASE_URL"),
|
|
207
111
|
sqliteFile: overrides.studioExecuteSqliteFile ?? (execProvider === "sqlite" ? overrides.sqliteFile : undefined),
|
|
208
112
|
};
|
|
209
113
|
}
|
|
@@ -224,94 +128,7 @@ export function resolveDefaultInitAnswers(overrides = {}) {
|
|
|
224
128
|
studioExecute,
|
|
225
129
|
};
|
|
226
130
|
}
|
|
227
|
-
/** Package maps — names come from an allowlist, not from user input */
|
|
228
|
-
const DB_DRIVER_PACKAGES = {
|
|
229
|
-
postgres: "pg",
|
|
230
|
-
mysql: "mysql2",
|
|
231
|
-
sqlite: "better-sqlite3",
|
|
232
|
-
sqlserver: "mssql",
|
|
233
|
-
};
|
|
234
|
-
export function buildInitInstallPlan(answers, specs) {
|
|
235
|
-
const packages = [
|
|
236
|
-
`@askdb/config@${specs.configSpec}`,
|
|
237
|
-
];
|
|
238
|
-
const labels = ["@askdb/config"];
|
|
239
|
-
const allowBuilds = [];
|
|
240
|
-
// `@askdb/prisma` is an optional peer of askdb (#259). It depends on Prisma, whose
|
|
241
|
-
// `@prisma/engines` postinstall pnpm 11 refuses to run until it's approved, and then
|
|
242
|
-
// exits 1. A Prisma project approves it anyway, so the install does it here.
|
|
243
|
-
// Studio's guided setup does the same (`apps/studio/src/setup.ts`).
|
|
244
|
-
if (answers.database === "prisma") {
|
|
245
|
-
packages.push(`@askdb/prisma@${specs.prismaSpec}`);
|
|
246
|
-
labels.push("@askdb/prisma");
|
|
247
|
-
allowBuilds.push(...PRISMA_BUILD_APPROVALS);
|
|
248
|
-
}
|
|
249
|
-
const driversNeeded = new Set();
|
|
250
|
-
// Live DB driver for introspection
|
|
251
|
-
if (answers.database !== "prisma") {
|
|
252
|
-
driversNeeded.add(answers.database);
|
|
253
|
-
}
|
|
254
|
-
// Live DB driver for Studio execute
|
|
255
|
-
if (answers.studioExecute.enabled) {
|
|
256
|
-
driversNeeded.add(answers.studioExecute.provider);
|
|
257
|
-
}
|
|
258
|
-
for (const db of driversNeeded) {
|
|
259
|
-
const pkg = DB_DRIVER_PACKAGES[db];
|
|
260
|
-
if (pkg) {
|
|
261
|
-
packages.push(pkg);
|
|
262
|
-
labels.push(pkg);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return { packages, labels, allowBuilds };
|
|
266
|
-
}
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// Package manager detection
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
131
|
const require = createRequire(import.meta.url);
|
|
271
|
-
/** Walk upward from `startDir` to find a directory containing `package.json`. */
|
|
272
|
-
export function findNearestPackageJsonDir(startDir) {
|
|
273
|
-
let dir = resolve(startDir);
|
|
274
|
-
for (let i = 0; i < 40; i += 1) {
|
|
275
|
-
if (existsSync(join(dir, "package.json")))
|
|
276
|
-
return dir;
|
|
277
|
-
const parent = dirname(dir);
|
|
278
|
-
if (parent === dir)
|
|
279
|
-
return undefined;
|
|
280
|
-
dir = parent;
|
|
281
|
-
}
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
/** Monorepo / workspace root — avoid mutating root `package.json` from `askdb init`. */
|
|
285
|
-
export function isLikelyWorkspaceRoot(packageDir) {
|
|
286
|
-
if (existsSync(join(packageDir, "pnpm-workspace.yaml")) || existsSync(join(packageDir, "pnpm-workspace.yml"))) {
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const pkg = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
291
|
-
return pkg.workspaces !== undefined && pkg.workspaces !== null;
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
export function detectPackageManager(packageDir) {
|
|
298
|
-
let dir = packageDir;
|
|
299
|
-
for (let i = 0; i < 40; i += 1) {
|
|
300
|
-
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
301
|
-
return "pnpm";
|
|
302
|
-
if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock")))
|
|
303
|
-
return "bun";
|
|
304
|
-
if (existsSync(join(dir, "yarn.lock")))
|
|
305
|
-
return "yarn";
|
|
306
|
-
if (existsSync(join(dir, "package-lock.json")))
|
|
307
|
-
return "npm";
|
|
308
|
-
const parent = dirname(dir);
|
|
309
|
-
if (parent === dir)
|
|
310
|
-
break;
|
|
311
|
-
dir = parent;
|
|
312
|
-
}
|
|
313
|
-
return "npm";
|
|
314
|
-
}
|
|
315
132
|
/**
|
|
316
133
|
* The versions `askdb init` installs: `@askdb/config` and `@askdb/prisma` at the versions
|
|
317
134
|
* this CLI was built with. Published, those are its `@askdb/config` dependency and the floor
|
|
@@ -342,196 +159,156 @@ function builtWithVersion(packageName, declared) {
|
|
|
342
159
|
}
|
|
343
160
|
function defaultInstaller(pm, packageDir, packages, allowBuilds) {
|
|
344
161
|
const env = { ...process.env, CI: process.env.CI ?? "true" };
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const r = spawnSync(cmd, installArgs(pm, packages, allowBuilds), { cwd: packageDir, stdio: "inherit", env, shell: false });
|
|
162
|
+
const { command, args, shell } = packageManagerSpawnSpec(pm, packageManagerAddArgs(pm, packages, allowBuilds));
|
|
163
|
+
const r = spawnSync(command, args, { cwd: packageDir, stdio: "inherit", env, shell });
|
|
348
164
|
return r.status === 0;
|
|
349
165
|
}
|
|
350
|
-
function formatManualInstallCommand(pm, packages, allowBuilds) {
|
|
351
|
-
return [pm, ...installArgs(pm, packages, allowBuilds)].join(" ");
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* The build approvals to pass `pm` in `packageDir`: `allowBuilds`, or none for a pnpm older
|
|
355
|
-
* than 10.4.0, whose `add` rejects `--allow-build` (pnpm 9 runs install scripts, and 10.0 to
|
|
356
|
-
* 10.3 skip unapproved ones without failing). A pnpm whose version can't be read keeps them.
|
|
357
|
-
* Studio's setup applies the same rule (`apps/studio/src/package-manager.ts`).
|
|
358
|
-
*/
|
|
359
|
-
function supportedBuildApprovals(pm, packageDir, allowBuilds) {
|
|
360
|
-
if (pm !== "pnpm" || allowBuilds.length === 0)
|
|
361
|
-
return allowBuilds;
|
|
362
|
-
const version = spawnSync("pnpm", ["--version"], {
|
|
363
|
-
cwd: packageDir,
|
|
364
|
-
encoding: "utf8",
|
|
365
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
366
|
-
// Windows runs pnpm through its `.cmd` shim, which needs a shell; the arguments are fixed.
|
|
367
|
-
shell: process.platform === "win32",
|
|
368
|
-
timeout: 15_000,
|
|
369
|
-
});
|
|
370
|
-
const match = version.status === 0 ? /^(\d+)\.(\d+)\./.exec(version.stdout.trim()) : null;
|
|
371
|
-
if (!match)
|
|
372
|
-
return allowBuilds;
|
|
373
|
-
const [major, minor] = [Number(match[1]), Number(match[2])];
|
|
374
|
-
return major > 10 || (major === 10 && minor >= 4) ? allowBuilds : [];
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* The add command's arguments. Build approvals apply to pnpm only: npm and Yarn run install
|
|
378
|
-
* scripts, and Bun skips untrusted ones without failing. Mirrors Studio's
|
|
379
|
-
* `packageManagerAddArgs` (`apps/studio/src/package-manager.ts`); change both together.
|
|
380
|
-
*/
|
|
381
|
-
function installArgs(pm, packages, allowBuilds) {
|
|
382
|
-
switch (pm) {
|
|
383
|
-
case "pnpm":
|
|
384
|
-
return ["add", ...allowBuilds.map((name) => `--allow-build=${name}`), ...packages];
|
|
385
|
-
case "yarn":
|
|
386
|
-
case "bun":
|
|
387
|
-
return ["add", ...packages];
|
|
388
|
-
default:
|
|
389
|
-
return ["install", "--save", ...packages];
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
166
|
const VALID_DATABASES = ["postgres", "mysql", "sqlite", "sqlserver", "prisma"];
|
|
393
|
-
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
167
|
+
/** A flag that takes one of `allowed`; the error repeats a bad value only when it's `showable`. */
|
|
168
|
+
function oneOf(flag, allowed, value) {
|
|
169
|
+
if (allowed.includes(value))
|
|
170
|
+
return value;
|
|
171
|
+
const got = showable(value) ? ` "${value}"` : "";
|
|
172
|
+
throw new Error(`${flag}: invalid value${got}. Allowed: ${allowed.join(", ")}.`);
|
|
173
|
+
}
|
|
174
|
+
/** Every flag `askdb init` accepts, for the parser and the help. */
|
|
175
|
+
const INIT_FLAGS = [
|
|
176
|
+
{ names: ["-y", "--yes"], help: "Accept defaults without prompts.", apply: (opts) => (opts.yes = true) },
|
|
177
|
+
{
|
|
178
|
+
names: ["--interactive"],
|
|
179
|
+
help: "Force wizard mode; errors if stdin/stdout are not TTYs.",
|
|
180
|
+
apply: (opts) => (opts.interactive = true),
|
|
181
|
+
},
|
|
182
|
+
{ names: ["--no-interactive"], help: "Never prompt; use defaults or flags.", apply: (opts) => (opts.noInteractive = true) },
|
|
183
|
+
{
|
|
184
|
+
names: ["--path"],
|
|
185
|
+
value: "<file>",
|
|
186
|
+
help: `Write the config to this path. Default: ${DEFAULT_CONFIG_PATH}.`,
|
|
187
|
+
apply: (opts, value) => (opts.path = value),
|
|
188
|
+
},
|
|
189
|
+
{ names: ["-f", "--force"], help: "Overwrite an existing file.", apply: (opts) => (opts.force = true) },
|
|
190
|
+
{
|
|
191
|
+
names: ["--skip-install"],
|
|
192
|
+
help: "Only write the file; do not install packages.",
|
|
193
|
+
apply: (opts) => (opts.skipInstall = true),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
names: ["--database"],
|
|
197
|
+
value: "<db>",
|
|
198
|
+
group: "Database options",
|
|
199
|
+
help: `${VALID_DATABASES.join("|")} (default: postgres)`,
|
|
200
|
+
apply: (opts, value) => (opts.database = oneOf("--database", VALID_DATABASES, value)),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
names: ["--connection-env"],
|
|
204
|
+
value: "<name>",
|
|
205
|
+
group: "Database options",
|
|
206
|
+
help: "Env var name for connection URL",
|
|
207
|
+
apply: (opts, value) => (opts.connectionEnv = value),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
names: ["--sqlite-file"],
|
|
211
|
+
value: "<path-or-env>",
|
|
212
|
+
group: "Database options",
|
|
213
|
+
help: "SQLite file path, or an UPPER_SNAKE_CASE env var name",
|
|
214
|
+
apply: (opts, value) => (opts.sqliteFile = value),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
names: ["--prisma-schema"],
|
|
218
|
+
value: "<path>",
|
|
219
|
+
group: "Database options",
|
|
220
|
+
help: "Path to schema.prisma",
|
|
221
|
+
apply: (opts, value) => (opts.prismaSchema = value),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
names: ["--schema-out"],
|
|
225
|
+
value: "<dir>",
|
|
226
|
+
group: "Database options",
|
|
227
|
+
help: "Schema output directory (default: ./askdb)",
|
|
228
|
+
apply: (opts, value) => (opts.schemaOut = value),
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
names: ["--ai-provider"],
|
|
232
|
+
value: "<name>",
|
|
233
|
+
group: "AI options",
|
|
234
|
+
help: `${VALID_AI_PROVIDERS.join("|")} (default: openai)`,
|
|
235
|
+
apply: (opts, value) => (opts.aiProvider = oneOf("--ai-provider", VALID_AI_PROVIDERS, value)),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
names: ["--ai-key-env"],
|
|
239
|
+
value: "<name>",
|
|
240
|
+
group: "AI options",
|
|
241
|
+
help: "Env var name for API key",
|
|
242
|
+
apply: (opts, value) => (opts.aiKeyEnv = value),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
names: ["--ai-model-env"],
|
|
246
|
+
value: "<name>",
|
|
247
|
+
group: "AI options",
|
|
248
|
+
help: "Env var name for model override",
|
|
249
|
+
apply: (opts, value) => (opts.aiModelEnv = value),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
names: ["--rag-store"],
|
|
253
|
+
value: "<name>",
|
|
254
|
+
group: "RAG options",
|
|
255
|
+
help: `${ASKDB_RAG_STORES.join("|")} (default: file)`,
|
|
256
|
+
apply: (opts, value) => (opts.ragStore = oneOf("--rag-store", ASKDB_RAG_STORES, value)),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
names: ["--pgvector-env"],
|
|
260
|
+
value: "<name>",
|
|
261
|
+
group: "RAG options",
|
|
262
|
+
help: "Env var name for pgvector URL",
|
|
263
|
+
apply: (opts, value) => (opts.pgvectorEnv = value),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
names: ["--studio-execute"],
|
|
267
|
+
group: "Studio options",
|
|
268
|
+
help: "Enable Studio execute (default: off)",
|
|
269
|
+
apply: (opts) => (opts.studioExecute = true),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
names: ["--no-studio-execute"],
|
|
273
|
+
group: "Studio options",
|
|
274
|
+
help: "Disable Studio execute",
|
|
275
|
+
apply: (opts) => (opts.studioExecute = false),
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
/** `askdb init`, installing packages with `installer`. It never reads askdb.config. */
|
|
279
|
+
export function initCommand(installer = defaultInstaller) {
|
|
280
|
+
return {
|
|
281
|
+
name: "init",
|
|
282
|
+
summary: "Create askdb.config.ts tailored to your database and AI provider",
|
|
283
|
+
usage: [
|
|
284
|
+
"askdb init Start wizard in a TTY, or use defaults in CI",
|
|
285
|
+
"askdb init --yes Accept defaults without prompts",
|
|
286
|
+
"askdb init --no-interactive Same as --yes",
|
|
287
|
+
"askdb init --force Overwrite an existing askdb.config.ts",
|
|
288
|
+
"askdb init --path <file> Write config to a custom file path",
|
|
289
|
+
"askdb init --skip-install Only write the file; do not install packages",
|
|
290
|
+
],
|
|
291
|
+
flags: INIT_FLAGS,
|
|
292
|
+
initial: () => ({
|
|
293
|
+
force: false,
|
|
294
|
+
path: DEFAULT_CONFIG_PATH,
|
|
295
|
+
skipInstall: false,
|
|
296
|
+
yes: false,
|
|
297
|
+
interactive: undefined,
|
|
298
|
+
noInteractive: false,
|
|
299
|
+
}),
|
|
300
|
+
check(opts) {
|
|
301
|
+
if (opts.yes && opts.interactive) {
|
|
302
|
+
throw new Error("--yes and --interactive cannot be used together.");
|
|
507
303
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (!value || value.startsWith("--"))
|
|
511
|
-
throw new Error(`${arg} requires a value.`);
|
|
512
|
-
opts.pgvectorEnv = value;
|
|
513
|
-
break;
|
|
304
|
+
if (opts.interactive && opts.noInteractive) {
|
|
305
|
+
throw new Error("--interactive and --no-interactive cannot be used together.");
|
|
514
306
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
default:
|
|
520
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// Conflict checks
|
|
524
|
-
if (opts.yes && opts.interactive) {
|
|
525
|
-
throw new Error("--yes and --interactive cannot be used together.");
|
|
526
|
-
}
|
|
527
|
-
if (opts.interactive && opts.noInteractive) {
|
|
528
|
-
throw new Error("--interactive and --no-interactive cannot be used together.");
|
|
529
|
-
}
|
|
530
|
-
return opts;
|
|
307
|
+
},
|
|
308
|
+
needsConfig: () => false,
|
|
309
|
+
run: (opts) => runInit(opts, installer),
|
|
310
|
+
};
|
|
531
311
|
}
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
// TTY wizard
|
|
534
|
-
// ---------------------------------------------------------------------------
|
|
535
312
|
function validatePath(value) {
|
|
536
313
|
if (!value.trim())
|
|
537
314
|
return "Cannot be empty.";
|
|
@@ -671,9 +448,6 @@ export async function runWizard(prompter) {
|
|
|
671
448
|
studioExecute,
|
|
672
449
|
};
|
|
673
450
|
}
|
|
674
|
-
// ---------------------------------------------------------------------------
|
|
675
|
-
// Real Inquirer prompter
|
|
676
|
-
// ---------------------------------------------------------------------------
|
|
677
451
|
async function buildInquirerPrompter() {
|
|
678
452
|
const { select, input, confirm } = await import("@inquirer/prompts");
|
|
679
453
|
return {
|
|
@@ -682,79 +456,6 @@ async function buildInquirerPrompter() {
|
|
|
682
456
|
confirm: (opts) => confirm({ message: opts.message, default: opts.default }),
|
|
683
457
|
};
|
|
684
458
|
}
|
|
685
|
-
// ---------------------------------------------------------------------------
|
|
686
|
-
// Next steps printer
|
|
687
|
-
// ---------------------------------------------------------------------------
|
|
688
|
-
// ---------------------------------------------------------------------------
|
|
689
|
-
// .env.example generation
|
|
690
|
-
// ---------------------------------------------------------------------------
|
|
691
|
-
const DB_URL_PLACEHOLDER = {
|
|
692
|
-
postgres: "postgresql://<USERNAME>:<PASSWORD>@<DATABASE_HOST>:<DATABASE_PORT>/<DATABASE_NAME>",
|
|
693
|
-
mysql: "mysql://<USERNAME>:<PASSWORD>@<DATABASE_HOST>:<DATABASE_PORT>/<DATABASE_NAME>",
|
|
694
|
-
sqlserver: "Data Source=<DATABASE_HOST>,<DATABASE_PORT>;Initial Catalog=<DATABASE_NAME>;User ID=<USERNAME>;Password=<PASSWORD>;Trust Server Certificate=True;Authentication=SqlPassword;",
|
|
695
|
-
};
|
|
696
|
-
/**
|
|
697
|
-
* The database-side env vars the generated config reads (the introspection URL,
|
|
698
|
-
* SQLite file paths, the pgvector URL, and Studio execute's URL), each once.
|
|
699
|
-
* Names fall back to the same defaults the renderers above use, so
|
|
700
|
-
* `.env.example` and the printed variable list match the config.
|
|
701
|
-
*/
|
|
702
|
-
function databaseEnvVars(answers) {
|
|
703
|
-
const { database, studioExecute } = answers;
|
|
704
|
-
const vars = new Map();
|
|
705
|
-
const add = (envVar) => {
|
|
706
|
-
if (!vars.has(envVar.name))
|
|
707
|
-
vars.set(envVar.name, envVar);
|
|
708
|
-
};
|
|
709
|
-
if (database !== "sqlite" && database !== "prisma") {
|
|
710
|
-
add({ name: answers.connectionEnv ?? "DATABASE_URL", placeholder: DB_URL_PLACEHOLDER[database] ?? "" });
|
|
711
|
-
}
|
|
712
|
-
const sqliteFileEnvs = [
|
|
713
|
-
database === "sqlite" ? sqliteFileEnv(answers.sqliteFile) : undefined,
|
|
714
|
-
studioExecute.enabled && studioExecute.provider === "sqlite" ? sqliteFileEnv(studioExecute.sqliteFile) : undefined,
|
|
715
|
-
];
|
|
716
|
-
for (const name of sqliteFileEnvs) {
|
|
717
|
-
if (name)
|
|
718
|
-
add({ name, placeholder: "", purpose: "SQLite database file path" });
|
|
719
|
-
}
|
|
720
|
-
if (answers.ragStore === "pgvector") {
|
|
721
|
-
add({ name: answers.pgvectorEnv ?? "ASKDB_PGVECTOR_URL", placeholder: DB_URL_PLACEHOLDER.postgres ?? "" });
|
|
722
|
-
}
|
|
723
|
-
if (studioExecute.enabled && studioExecute.provider !== "sqlite") {
|
|
724
|
-
add({
|
|
725
|
-
name: studioExecute.connectionEnv ?? "DATABASE_URL",
|
|
726
|
-
placeholder: DB_URL_PLACEHOLDER[studioExecute.provider] ?? "",
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
return [...vars.values()];
|
|
730
|
-
}
|
|
731
|
-
function buildEnvExample(answers) {
|
|
732
|
-
const lines = [
|
|
733
|
-
"# AskDB environment — copy to .env and fill in real values.",
|
|
734
|
-
"# .env is read by the CLI, Studio, and the HTTP API; never commit it.",
|
|
735
|
-
"",
|
|
736
|
-
];
|
|
737
|
-
for (const envVar of databaseEnvVars(answers)) {
|
|
738
|
-
if (envVar.purpose)
|
|
739
|
-
lines.push(`# ${envVar.purpose}`);
|
|
740
|
-
lines.push(`${envVar.name}=${envVar.placeholder}`);
|
|
741
|
-
}
|
|
742
|
-
for (const envVar of aiScaffold(answers).envVars) {
|
|
743
|
-
lines.push(`# ${envVar.purpose}`);
|
|
744
|
-
lines.push(`${envVar.name}=`);
|
|
745
|
-
}
|
|
746
|
-
lines.push("");
|
|
747
|
-
return lines.join("\n");
|
|
748
|
-
}
|
|
749
|
-
/** Env var NAMES the generated config references — conventional defaults, not values. */
|
|
750
|
-
function collectEnvVarNames(answers) {
|
|
751
|
-
const names = new Set();
|
|
752
|
-
for (const envVar of aiScaffold(answers).envVars)
|
|
753
|
-
names.add(envVar.name);
|
|
754
|
-
for (const envVar of databaseEnvVars(answers))
|
|
755
|
-
names.add(envVar.name);
|
|
756
|
-
return Array.from(names);
|
|
757
|
-
}
|
|
758
459
|
function printNextSteps(answers) {
|
|
759
460
|
const lines = ["\nNext steps:"];
|
|
760
461
|
lines.push(" 1. Copy .env.example → .env and fill in your credentials.");
|
|
@@ -771,24 +472,10 @@ function printNextSteps(answers) {
|
|
|
771
472
|
lines.push(` ${answers.studioExecute.enabled ? "4" : "3"}. Ask a question: askdb ask --question "Which customers signed up last week?"`);
|
|
772
473
|
process.stdout.write(lines.join("\n") + "\n");
|
|
773
474
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const installer = _installer ?? defaultInstaller;
|
|
779
|
-
let opts;
|
|
780
|
-
try {
|
|
781
|
-
opts = parseOptions(argv);
|
|
782
|
-
}
|
|
783
|
-
catch (e) {
|
|
784
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
785
|
-
process.stderr.write(`${msg}\n`);
|
|
786
|
-
return 1;
|
|
787
|
-
}
|
|
788
|
-
if (opts.path === "--help") {
|
|
789
|
-
printHelp();
|
|
790
|
-
return 0;
|
|
791
|
-
}
|
|
475
|
+
export async function runInitCli(argv, installer) {
|
|
476
|
+
return runCommand(initCommand(installer), argv);
|
|
477
|
+
}
|
|
478
|
+
async function runInit(opts, installer) {
|
|
792
479
|
const configTarget = resolve(process.cwd(), opts.path);
|
|
793
480
|
if (existsSync(configTarget) && !opts.force) {
|
|
794
481
|
process.stderr.write(`Refusing to overwrite existing file:\n` +
|
|
@@ -796,7 +483,6 @@ export async function runInitCli(argv, _installer) {
|
|
|
796
483
|
`Use \`askdb init --force\` to overwrite.\n`);
|
|
797
484
|
return 1;
|
|
798
485
|
}
|
|
799
|
-
// Determine interactive mode
|
|
800
486
|
const isTTY = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
801
487
|
const wantInteractive = opts.interactive === true || (!opts.yes && !opts.noInteractive && isTTY);
|
|
802
488
|
if (opts.interactive && !isTTY) {
|
|
@@ -804,6 +490,7 @@ export async function runInitCli(argv, _installer) {
|
|
|
804
490
|
return 1;
|
|
805
491
|
}
|
|
806
492
|
let answers;
|
|
493
|
+
let plan;
|
|
807
494
|
if (wantInteractive) {
|
|
808
495
|
let prompter;
|
|
809
496
|
try {
|
|
@@ -812,24 +499,16 @@ export async function runInitCli(argv, _installer) {
|
|
|
812
499
|
catch {
|
|
813
500
|
process.stderr.write("Failed to load @inquirer/prompts. Falling back to non-interactive mode.\n");
|
|
814
501
|
answers = resolveDefaultInitAnswers(optsToOverrides(opts));
|
|
815
|
-
return await finishInit(configTarget, answers, opts, installer);
|
|
502
|
+
return await finishInit(configTarget, answers, planInitScaffold(answers, resolveInitDepSpecs()), opts, installer);
|
|
816
503
|
}
|
|
817
|
-
let wizardResult;
|
|
818
504
|
try {
|
|
819
|
-
|
|
505
|
+
answers = await runWizard(prompter);
|
|
820
506
|
}
|
|
821
507
|
catch {
|
|
822
508
|
process.stderr.write("\nSetup cancelled.\n");
|
|
823
509
|
return 1;
|
|
824
510
|
}
|
|
825
|
-
|
|
826
|
-
process.stderr.write("\nSetup cancelled.\n");
|
|
827
|
-
return 1;
|
|
828
|
-
}
|
|
829
|
-
answers = wizardResult;
|
|
830
|
-
// Summary and confirmation
|
|
831
|
-
const specs = resolveInitDepSpecs();
|
|
832
|
-
const plan = buildInitInstallPlan(answers, specs);
|
|
511
|
+
plan = planInitScaffold(answers, resolveInitDepSpecs());
|
|
833
512
|
process.stdout.write("\n--- Summary ---\n");
|
|
834
513
|
process.stdout.write(` Config path: ${configTarget}\n`);
|
|
835
514
|
process.stdout.write(` Database: ${answers.database}\n`);
|
|
@@ -837,9 +516,10 @@ export async function runInitCli(argv, _installer) {
|
|
|
837
516
|
process.stdout.write(` RAG store: ${answers.ragStore}\n`);
|
|
838
517
|
process.stdout.write(` Studio execute: ${answers.studioExecute.enabled ? `enabled (${answers.studioExecute.provider})` : "disabled"}\n`);
|
|
839
518
|
if (!opts.skipInstall) {
|
|
840
|
-
|
|
519
|
+
const packages = packagesToInstall(plan, findNearestPackageJsonDir(process.cwd()));
|
|
520
|
+
process.stdout.write(` Packages: ${packages.map(packageNameOf).join(", ")}\n`);
|
|
841
521
|
}
|
|
842
|
-
process.stdout.write(` Env vars: ${
|
|
522
|
+
process.stdout.write(` Env vars: ${plan.envVars.map((v) => v.name).join(", ")}\n`);
|
|
843
523
|
process.stdout.write(' (defaults — rename via `env("...")` in askdb.config.ts if you\'d like)\n');
|
|
844
524
|
process.stdout.write("---------------\n\n");
|
|
845
525
|
let confirmed;
|
|
@@ -857,8 +537,42 @@ export async function runInitCli(argv, _installer) {
|
|
|
857
537
|
}
|
|
858
538
|
else {
|
|
859
539
|
answers = resolveDefaultInitAnswers(optsToOverrides(opts));
|
|
540
|
+
plan = planInitScaffold(answers, resolveInitDepSpecs());
|
|
541
|
+
}
|
|
542
|
+
return await finishInit(configTarget, answers, plan, opts, installer);
|
|
543
|
+
}
|
|
544
|
+
/** `@askdb/prisma` from `@askdb/prisma@1.0.0`, `pg` from `pg`. */
|
|
545
|
+
function packageNameOf(spec) {
|
|
546
|
+
const at = spec.indexOf("@", 1);
|
|
547
|
+
return at === -1 ? spec : spec.slice(0, at);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* The plan's packages to install in `pkgRoot`. AskDB's own packages always install, at the
|
|
551
|
+
* versions this CLI pins. Any other package (a driver, or `ai` and `zod` for Prisma) installs
|
|
552
|
+
* only when the project's `package.json` doesn't list it yet, so `init` never moves a range
|
|
553
|
+
* the project already chose, such as `ai@^6`. With no project, or at a workspace root, where
|
|
554
|
+
* `init` only prints the command for another package, every package is listed.
|
|
555
|
+
*/
|
|
556
|
+
function packagesToInstall(plan, pkgRoot) {
|
|
557
|
+
const listed = pkgRoot && !isLikelyWorkspaceRoot(pkgRoot) ? listedDependencies(pkgRoot) : new Set();
|
|
558
|
+
return plan.install.packages.filter((spec) => {
|
|
559
|
+
const name = packageNameOf(spec);
|
|
560
|
+
return name.startsWith("@askdb/") || !listed.has(name);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/** Every package `dir/package.json` lists as a dependency of any kind. */
|
|
564
|
+
function listedDependencies(dir) {
|
|
565
|
+
try {
|
|
566
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
567
|
+
const fields = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
568
|
+
return new Set(fields.flatMap((field) => {
|
|
569
|
+
const deps = pkg[field];
|
|
570
|
+
return deps !== null && typeof deps === "object" ? Object.keys(deps) : [];
|
|
571
|
+
}));
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return new Set();
|
|
860
575
|
}
|
|
861
|
-
return await finishInit(configTarget, answers, opts, installer);
|
|
862
576
|
}
|
|
863
577
|
function optsToOverrides(opts) {
|
|
864
578
|
return {
|
|
@@ -875,21 +589,19 @@ function optsToOverrides(opts) {
|
|
|
875
589
|
studioExecute: opts.studioExecute,
|
|
876
590
|
};
|
|
877
591
|
}
|
|
878
|
-
async function finishInit(configTarget, answers, opts, installer) {
|
|
879
|
-
const configContent = renderInitConfig(answers);
|
|
592
|
+
async function finishInit(configTarget, answers, plan, opts, installer) {
|
|
880
593
|
try {
|
|
881
|
-
writeFileSync(configTarget,
|
|
594
|
+
writeFileSync(configTarget, plan.configSource, { encoding: "utf8" });
|
|
882
595
|
}
|
|
883
596
|
catch (e) {
|
|
884
|
-
|
|
885
|
-
process.stderr.write(`Failed to write config: ${msg}\n`);
|
|
597
|
+
process.stderr.write(`Failed to write config: ${errorMessage(e)}\n`);
|
|
886
598
|
return 1;
|
|
887
599
|
}
|
|
888
600
|
process.stdout.write(`Wrote:\n - ${configTarget}\n`);
|
|
889
601
|
const envExamplePath = join(dirname(configTarget), ".env.example");
|
|
890
602
|
if (!existsSync(envExamplePath)) {
|
|
891
603
|
try {
|
|
892
|
-
writeFileSync(envExamplePath,
|
|
604
|
+
writeFileSync(envExamplePath, plan.envExample, { encoding: "utf8" });
|
|
893
605
|
process.stdout.write(` - ${envExamplePath}\n`);
|
|
894
606
|
}
|
|
895
607
|
catch {
|
|
@@ -898,67 +610,37 @@ async function finishInit(configTarget, answers, opts, installer) {
|
|
|
898
610
|
}
|
|
899
611
|
if (!opts.skipInstall) {
|
|
900
612
|
const pkgRoot = findNearestPackageJsonDir(process.cwd());
|
|
901
|
-
const
|
|
902
|
-
const
|
|
613
|
+
const packages = packagesToInstall(plan, pkgRoot);
|
|
614
|
+
const { allowBuilds } = plan.install;
|
|
615
|
+
// The manager comes from the project's lockfile or package.json, else from whoever
|
|
616
|
+
// ran init (`pnpm dlx`, `npx`, …); with no package.json, from the current directory.
|
|
617
|
+
const pmDir = pkgRoot ?? process.cwd();
|
|
618
|
+
const pm = detectPackageManager(pmDir);
|
|
619
|
+
const approvedBuilds = supportedBuildApprovals(pm, pmDir, allowBuilds);
|
|
620
|
+
const installCommand = formatInstallCommand(pm, packages, approvedBuilds);
|
|
903
621
|
if (!pkgRoot) {
|
|
904
622
|
process.stdout.write("\nNo package.json found in this directory or any parent directory.\n" +
|
|
905
623
|
"Install the template imports yourself (from a project with a package.json):\n" +
|
|
906
|
-
`
|
|
624
|
+
` ${installCommand}\n\n`);
|
|
907
625
|
}
|
|
908
626
|
else if (isLikelyWorkspaceRoot(pkgRoot)) {
|
|
909
627
|
process.stdout.write("\nSkipped automatic dependency install (workspace / monorepo root).\n" +
|
|
910
628
|
"Add these to the package that will load this config (or run `askdb init` from that package directory):\n" +
|
|
911
|
-
`
|
|
629
|
+
` ${installCommand}\n\n`);
|
|
912
630
|
}
|
|
913
631
|
else {
|
|
914
|
-
const pm = detectPackageManager(pkgRoot);
|
|
915
|
-
const approvedBuilds = supportedBuildApprovals(pm, pkgRoot, plan.allowBuilds);
|
|
916
632
|
process.stdout.write(`\nInstalling dependencies with ${pm} in ${pkgRoot} …\n`);
|
|
917
|
-
const ok = installer(pm, pkgRoot,
|
|
633
|
+
const ok = installer(pm, pkgRoot, packages, approvedBuilds);
|
|
918
634
|
if (!ok) {
|
|
919
635
|
process.stderr.write("Dependency install failed. The config file was written, but packages are not installed yet.\n" +
|
|
920
636
|
` cd ${pkgRoot}\n` +
|
|
921
|
-
` ${
|
|
637
|
+
` ${installCommand}\n`);
|
|
922
638
|
return 1;
|
|
923
639
|
}
|
|
924
|
-
process.stdout.write(`Installed: ${
|
|
640
|
+
process.stdout.write(`Installed: ${packages.map(packageNameOf).join(", ")}.\n`);
|
|
925
641
|
}
|
|
926
642
|
}
|
|
927
643
|
printNextSteps(answers);
|
|
928
644
|
return 0;
|
|
929
645
|
}
|
|
930
|
-
function printHelp() {
|
|
931
|
-
process.stdout.write([
|
|
932
|
-
"askdb init - Create askdb.config.ts tailored to your database and AI provider",
|
|
933
|
-
"",
|
|
934
|
-
"Usage:",
|
|
935
|
-
" askdb init Start wizard in a TTY, or use defaults in CI",
|
|
936
|
-
" askdb init --yes Accept defaults without prompts",
|
|
937
|
-
" askdb init --no-interactive Same as --yes",
|
|
938
|
-
" askdb init --force Overwrite an existing askdb.config.ts",
|
|
939
|
-
" askdb init --path <file> Write config to a custom file path",
|
|
940
|
-
" askdb init --skip-install Only write the file; do not install packages",
|
|
941
|
-
"",
|
|
942
|
-
"Database options:",
|
|
943
|
-
" --database <db> postgres|mysql|sqlite|sqlserver|prisma (default: postgres)",
|
|
944
|
-
" --connection-env <name> Env var name for connection URL",
|
|
945
|
-
" --sqlite-file <path-or-env> SQLite file path, or an UPPER_SNAKE_CASE env var name",
|
|
946
|
-
" --prisma-schema <path> Path to schema.prisma",
|
|
947
|
-
" --schema-out <dir> Schema output directory (default: ./askdb)",
|
|
948
|
-
"",
|
|
949
|
-
"AI options:",
|
|
950
|
-
` --ai-provider <name> ${VALID_AI_PROVIDERS.join("|")} (default: openai)`,
|
|
951
|
-
" --ai-key-env <name> Env var name for API key",
|
|
952
|
-
" --ai-model-env <name> Env var name for model override",
|
|
953
|
-
"",
|
|
954
|
-
"RAG options:",
|
|
955
|
-
" --rag-store <name> file|memory|pgvector (default: file)",
|
|
956
|
-
" --pgvector-env <name> Env var name for pgvector URL",
|
|
957
|
-
"",
|
|
958
|
-
"Studio options:",
|
|
959
|
-
" --studio-execute Enable Studio execute (default: off)",
|
|
960
|
-
" --no-studio-execute Disable Studio execute",
|
|
961
|
-
"",
|
|
962
|
-
].join("\n"));
|
|
963
|
-
}
|
|
964
646
|
//# sourceMappingURL=init.js.map
|