@askdb/studio 0.2.0-beta.27 → 0.2.0-beta.29

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.
@@ -6,7 +6,7 @@
6
6
  <title>AskDB Studio</title>
7
7
  <link rel="icon" type="image/png" media="(prefers-color-scheme: light)" href="/assets/brand/light-icon.png" />
8
8
  <link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="/assets/brand/dark-icon.png" />
9
- <script type="module" crossorigin src="/assets/index-DYBXDl3K.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BgdvYJyR.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/assets/index-Bj6qGfT6.css">
11
11
  </head>
12
12
  <body>
@@ -0,0 +1,35 @@
1
+ import { type ConnectorProvider } from "@askdb/connectors";
2
+ export type StudioIntrospectionPlan = {
3
+ ok: true;
4
+ engine: ConnectorProvider;
5
+ sourceLabel: string;
6
+ } | {
7
+ ok: false;
8
+ engine: ConnectorProvider | null;
9
+ error: string;
10
+ };
11
+ /**
12
+ * Resolve what a server-side introspection run would do, from the runtime
13
+ * config alone. Mirrors the CLI's flag-free resolution in
14
+ * `apps/cli/src/introspect.ts` (config provider + per-engine connection).
15
+ * Never includes credentials in `sourceLabel` — it is shown in the UI.
16
+ */
17
+ export declare function resolveStudioIntrospectionPlan(): StudioIntrospectionPlan;
18
+ export type StudioIntrospectionRun = {
19
+ engine: ConnectorProvider;
20
+ schemaId: string;
21
+ tables: number;
22
+ warnings: string[];
23
+ };
24
+ /**
25
+ * Run introspection server-side into `outDir`, preserving any existing
26
+ * enrichment markdown (the writer keeps `tables/*.md`, `concepts.md`, and
27
+ * `tenant-policy.md` when `existingArtifactDir` is supplied — the same
28
+ * guarantee `askdb introspect` gives).
29
+ */
30
+ export declare function runStudioIntrospection(options: {
31
+ outDir: string;
32
+ schemaId?: string;
33
+ hasExistingArtifact: boolean;
34
+ }): Promise<StudioIntrospectionRun>;
35
+ //# sourceMappingURL=introspection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAe3B,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;GAKG;AACH,wBAAgB,8BAA8B,IAAI,uBAAuB,CASxE;AA4ED,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAkClC"}
@@ -0,0 +1,138 @@
1
+ import { basename } from "node:path";
2
+ import { getAskDbRuntimeConfig } from "@askdb/config";
3
+ import { introspect, } from "@askdb/introspect";
4
+ import { createConnectorRegistry, } from "@askdb/connectors";
5
+ import { postgresConnectorProvider } from "@askdb/postgres";
6
+ import { mysqlConnectorProvider } from "@askdb/mysql";
7
+ import { sqliteConnectorProvider } from "@askdb/sqlite";
8
+ import { sqlServerConnectorProvider } from "@askdb/sqlserver";
9
+ import { prismaConnectorProvider } from "@askdb/prisma";
10
+ const connectorRegistry = createConnectorRegistry([
11
+ postgresConnectorProvider,
12
+ mysqlConnectorProvider,
13
+ sqliteConnectorProvider,
14
+ sqlServerConnectorProvider,
15
+ prismaConnectorProvider,
16
+ ]);
17
+ /**
18
+ * Resolve what a server-side introspection run would do, from the runtime
19
+ * config alone. Mirrors the CLI's flag-free resolution in
20
+ * `apps/cli/src/introspect.ts` (config provider + per-engine connection).
21
+ * Never includes credentials in `sourceLabel` — it is shown in the UI.
22
+ */
23
+ export function resolveStudioIntrospectionPlan() {
24
+ const rt = getAskDbRuntimeConfig();
25
+ const engine = rt.introspection.provider;
26
+ if (!connectorRegistry.hasProvider(engine)) {
27
+ return { ok: false, engine: null, error: `Unsupported introspection provider: ${engine}` };
28
+ }
29
+ const connection = resolveConnection(engine);
30
+ if (!connection.ok)
31
+ return { ok: false, engine, error: connection.error };
32
+ return { ok: true, engine, sourceLabel: connection.sourceLabel };
33
+ }
34
+ function resolveConnection(engine) {
35
+ const rt = getAskDbRuntimeConfig();
36
+ switch (engine) {
37
+ case "postgres": {
38
+ const url = rt.introspection.postgresDatabaseUrl;
39
+ if (!url) {
40
+ return {
41
+ ok: false,
42
+ error: "No Postgres connection configured. Set introspection.providerConfig.postgres.databaseUrl in askdb.config.ts (bound to an env var in .env).",
43
+ };
44
+ }
45
+ return { ok: true, url, sourceLabel: redactUrl(url) };
46
+ }
47
+ case "mysql": {
48
+ const url = rt.introspection.mysqlDatabaseUrl;
49
+ if (!url) {
50
+ return {
51
+ ok: false,
52
+ error: "No MySQL connection configured. Set introspection.providerConfig.mysql.databaseUrl in askdb.config.ts (bound to an env var in .env).",
53
+ };
54
+ }
55
+ return { ok: true, url, sourceLabel: redactUrl(url) };
56
+ }
57
+ case "sqlserver": {
58
+ const url = rt.introspection.sqlserverDatabaseUrl;
59
+ if (!url) {
60
+ return {
61
+ ok: false,
62
+ error: "No SQL Server connection configured. Set introspection.providerConfig.sqlserver.databaseUrl in askdb.config.ts (bound to an env var in .env).",
63
+ };
64
+ }
65
+ return { ok: true, url, sourceLabel: redactUrl(url) };
66
+ }
67
+ case "sqlite": {
68
+ const file = rt.introspection.sqliteFile;
69
+ if (!file) {
70
+ return {
71
+ ok: false,
72
+ error: "No SQLite file configured. Set introspection.providerConfig.sqlite.file in askdb.config.ts.",
73
+ };
74
+ }
75
+ return { ok: true, url: file, sourceLabel: file };
76
+ }
77
+ case "prisma": {
78
+ // When unset, @askdb/prisma auto-discovers prisma/schema.prisma in the project root.
79
+ const schemaPath = rt.introspection.prismaSchemaPath;
80
+ return {
81
+ ok: true,
82
+ schemaPath,
83
+ sourceLabel: schemaPath ?? "auto-discovered prisma/schema.prisma",
84
+ };
85
+ }
86
+ }
87
+ }
88
+ /** Strip credentials from a connection string for display (never shown raw in the UI). */
89
+ function redactUrl(raw) {
90
+ try {
91
+ const url = new URL(raw);
92
+ return `${url.protocol}//${url.host}${url.pathname}`;
93
+ }
94
+ catch {
95
+ // ADO.NET-style or otherwise unparseable — show only that it is configured.
96
+ return "configured connection";
97
+ }
98
+ }
99
+ /**
100
+ * Run introspection server-side into `outDir`, preserving any existing
101
+ * enrichment markdown (the writer keeps `tables/*.md`, `concepts.md`, and
102
+ * `tenant-policy.md` when `existingArtifactDir` is supplied — the same
103
+ * guarantee `askdb introspect` gives).
104
+ */
105
+ export async function runStudioIntrospection(options) {
106
+ const plan = resolveStudioIntrospectionPlan();
107
+ if (!plan.ok)
108
+ throw new Error(plan.error);
109
+ const connection = resolveConnection(plan.engine);
110
+ if (!connection.ok)
111
+ throw new Error(connection.error);
112
+ const schemaId = options.schemaId ?? inferSchemaId(options.outDir);
113
+ const connectorConfig = {
114
+ provider: plan.engine,
115
+ url: connection.url,
116
+ schemaPath: connection.schemaPath,
117
+ schemaId,
118
+ };
119
+ const runConfig = connectorRegistry.createConnector(connectorConfig);
120
+ const connector = runConfig.connector;
121
+ const result = await introspect(runConfig.input, {
122
+ outDir: options.outDir,
123
+ schemaId,
124
+ existingArtifactDir: options.hasExistingArtifact ? options.outDir : undefined,
125
+ }, { connector });
126
+ return {
127
+ engine: plan.engine,
128
+ schemaId,
129
+ tables: result.schema.schemas.reduce((sum, namespace) => sum + namespace.tables.length, 0),
130
+ warnings: result.warnings.map((warning) => typeof warning === "string" ? warning : JSON.stringify(warning)),
131
+ };
132
+ }
133
+ function inferSchemaId(path) {
134
+ const name = basename(path);
135
+ const inferred = name.endsWith(".schema") ? name.slice(0, -".schema".length) : name;
136
+ return inferred || "introspected";
137
+ }
138
+ //# sourceMappingURL=introspection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection.js","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import { createServer } from "node:http";
2
2
  import { createPgvectorStore } from "@askdb/rag";
3
3
  import { type Workspace } from "@askdb/enrich";
4
- import type { StudioWorkspaceDto } from "./shared/api.js";
4
+ import type { SetupReason, StudioWorkspaceDto } from "./shared/api.js";
5
5
  export type StudioOptions = {
6
6
  schema: string;
7
7
  host?: string;
8
8
  port?: number;
9
+ /**
10
+ * Start in setup mode instead of failing: `"no-config"` when no
11
+ * `askdb.config.*` was found, `"no-artifact"` when the config exists but the
12
+ * schema artifact hasn't been introspected yet. The browser wizard walks the
13
+ * user through the missing steps and flips the server to ready.
14
+ */
15
+ setupReason?: SetupReason | null;
9
16
  };
10
17
  export type StudioServer = ReturnType<typeof createServer>;
11
18
  export declare function setStudioPgvectorStoreFactoryForTests(factory: typeof createPgvectorStore | undefined): void;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AA+BpF,OAAO,EAOL,mBAAmB,EAOpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAaL,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAYV,kBAAkB,EAGnB,MAAM,iBAAiB,CAAC;AAQzB,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAyD3D,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,OAAO,mBAAmB,GAAG,SAAS,GAC9C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAiHvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CAyC3E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AA+BpF,OAAO,EAOL,mBAAmB,EAOpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAaL,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAYV,WAAW,EAKX,kBAAkB,EAGnB,MAAM,iBAAiB,CAAC;AAUzB,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AA0D3D,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,OAAO,mBAAmB,GAAG,SAAS,GAC9C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAwJvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CA8C3E"}
package/dist/server.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { createServer } from "node:http";
4
- import { extname, relative, join, resolve, dirname } from "node:path";
4
+ import { extname, relative, join, resolve, dirname, sep } from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { generateText as defaultGenerateText } from "ai";
8
- import { getAskDbRuntimeConfig } from "@askdb/config";
8
+ import { bootstrapAskDbEnv, getAskDbRuntimeConfig } from "@askdb/config";
9
9
  import { createAiRegistry, } from "@askdb/ai";
10
10
  import { anthropicProvider } from "@askdb/ai-anthropic";
11
11
  import { azureProvider } from "@askdb/ai-azure";
@@ -15,6 +15,8 @@ import { ask, isBuiltInDialectId, loadSchema, suggestEnrichment, tenantScopeSche
15
15
  import { buildSchemaIndex, chunkContentHash, chunkSchema, createAiSdkEmbedder, createFileStore, createMemoryStore, createPgvectorStore, createRetriever, loadChunkerSourcesFromDir, } from "@askdb/rag";
16
16
  import { buildDefaultTableBody, buildFrontmatter, buildSuggestionContext, buildSuggestionTarget, buildTableDraft, loadWorkspace, replaceH2Section, replaceTableDescription, saveConcepts, saveTable, } from "@askdb/enrich";
17
17
  import { EXECUTE_DRIVER_REGISTRY, isDriverInstalled } from "./execute-registry.js";
18
+ import { resolveStudioIntrospectionPlan, runStudioIntrospection } from "./introspection.js";
19
+ import { probeSetupState, writeSetupConfig, SetupError } from "./setup.js";
18
20
  const ai = createAiRegistry([openaiProvider, azureProvider, googleProvider, anthropicProvider]);
19
21
  const CLIENT_DIR = fileURLToPath(new URL("./client/", import.meta.url));
20
22
  const STUDIO_RAG_MOCK_DIMENSIONS = 64;
@@ -26,12 +28,14 @@ export function setStudioPgvectorStoreFactoryForTests(factory) {
26
28
  }
27
29
  export function createStudioServer(options) {
28
30
  const schemaDir = resolve(options.schema);
29
- if (!existsSync(schemaDir)) {
31
+ const setupReason = options.setupReason ?? null;
32
+ if (!setupReason && !existsSync(schemaDir)) {
30
33
  throw new Error(`schema directory not found: ${schemaDir}`);
31
34
  }
32
35
  const state = {
33
36
  schemaDir,
34
- workspace: loadWorkspace(schemaDir),
37
+ workspace: setupReason ? null : loadWorkspace(schemaDir),
38
+ setupReason,
35
39
  };
36
40
  return createServer(async (req, res) => {
37
41
  try {
@@ -42,27 +46,63 @@ export function createStudioServer(options) {
42
46
  if (req.method === "GET" && url.pathname.startsWith("/assets/")) {
43
47
  return serveClientFile(res, decodeURIComponent(url.pathname.slice(1)));
44
48
  }
49
+ if (req.method === "GET" && url.pathname === "/api/setup/status") {
50
+ return writeJson(res, 200, buildSetupStatus(state));
51
+ }
52
+ if (req.method === "POST" && url.pathname === "/api/setup/config") {
53
+ if (!isLoopbackRequest(req)) {
54
+ return writeJson(res, 403, { error: { message: "Setup is only available from loopback clients." } });
55
+ }
56
+ const body = await readJson(req);
57
+ const result = handleSetupConfig(state, body);
58
+ return writeJson(res, 200, result);
59
+ }
60
+ if (req.method === "POST" && url.pathname === "/api/setup/introspect") {
61
+ if (!isLoopbackRequest(req)) {
62
+ return writeJson(res, 403, { error: { message: "Setup is only available from loopback clients." } });
63
+ }
64
+ const result = await handleSetupIntrospect(state);
65
+ return writeJson(res, 200, result);
66
+ }
67
+ // Everything below needs a loaded workspace — route setup-mode clients to the wizard.
68
+ if (state.setupReason && url.pathname.startsWith("/api/")) {
69
+ return writeJson(res, 409, {
70
+ error: { message: "Studio needs setup. Open Studio in the browser to finish the guided setup." },
71
+ setup: buildSetupStatus(state),
72
+ });
73
+ }
45
74
  if (req.method === "GET" && url.pathname === "/api/workspace") {
46
- return writeJson(res, 200, serializeWorkspace(state.workspace));
75
+ return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
76
+ }
77
+ if (req.method === "GET" && url.pathname === "/api/introspect/status") {
78
+ requireWorkspace(state);
79
+ return writeJson(res, 200, buildIntrospectionPlanDto());
80
+ }
81
+ if (req.method === "POST" && url.pathname === "/api/introspect") {
82
+ if (!isLoopbackRequest(req)) {
83
+ return writeJson(res, 403, { error: { message: "Resync is only available from loopback clients." } });
84
+ }
85
+ const result = await handleResync(state);
86
+ return writeJson(res, 200, result);
47
87
  }
48
88
  if (req.method === "POST" && url.pathname.startsWith("/api/tables/")) {
49
89
  const tableId = decodeURIComponent(url.pathname.slice("/api/tables/".length));
50
90
  const body = await readJson(req);
51
91
  const draft = parseTableDraftBody(body);
52
92
  saveDraft(state, tableId, draft);
53
- return writeJson(res, 200, serializeWorkspace(state.workspace));
93
+ return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
54
94
  }
55
95
  if (req.method === "POST" && url.pathname === "/api/concepts") {
56
96
  const body = await readJson(req);
57
97
  const concepts = parseConceptsBody(body);
58
98
  saveConceptsDraft(state, concepts);
59
- return writeJson(res, 200, serializeWorkspace(state.workspace));
99
+ return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
60
100
  }
61
101
  if (req.method === "POST" && url.pathname === "/api/tenant-policy") {
62
102
  const body = await readJson(req);
63
103
  const { frontmatter, bodyText } = parseTenantPolicyBody(body);
64
104
  saveTenantPolicy(state, frontmatter, bodyText);
65
- return writeJson(res, 200, serializeWorkspace(state.workspace));
105
+ return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
66
106
  }
67
107
  if (req.method === "POST" && url.pathname === "/api/suggest-tenant-policy") {
68
108
  const draft = await suggestTenantPolicyDraft(state);
@@ -71,7 +111,7 @@ export function createStudioServer(options) {
71
111
  if (req.method === "POST" && url.pathname === "/api/suggest") {
72
112
  const body = await readJson(req);
73
113
  const source = parseSuggestSource(body);
74
- const candidates = await suggestForSource(state.workspace, source);
114
+ const candidates = await suggestForSource(requireWorkspace(state), source);
75
115
  return writeJson(res, 200, { candidates });
76
116
  }
77
117
  if (req.method === "GET" && url.pathname === "/api/rag/status") {
@@ -130,7 +170,7 @@ export function createStudioServer(options) {
130
170
  writeJson(res, 404, { error: { message: "Not found" } });
131
171
  }
132
172
  catch (error) {
133
- const status = error instanceof StudioHttpError ? error.status : 500;
173
+ const status = error instanceof StudioHttpError || error instanceof SetupError ? error.status : 500;
134
174
  writeJson(res, status, {
135
175
  error: { message: error instanceof Error ? error.message : String(error) },
136
176
  });
@@ -149,9 +189,15 @@ export function serializeWorkspace(workspace) {
149
189
  return undefined;
150
190
  }
151
191
  })();
192
+ const schema = loadSchema(workspace.schemaDir);
193
+ const schemaProvider = "provider" in schema && typeof schema.provider === "string" ? schema.provider : undefined;
194
+ const dialect = rt.nlToSql.dialect ??
195
+ (schemaProvider && isBuiltInDialectId(schemaProvider) ? schemaProvider : "postgres");
152
196
  return {
153
197
  schemaDir: workspace.schemaDir,
198
+ schemaPathRelative: toRelativeSchemaPath(workspace.schemaDir),
154
199
  schemaId: workspace.physical.schemaId,
200
+ dialect,
155
201
  warnings: workspace.warnings,
156
202
  aiConfigured: Boolean(aiConfig),
157
203
  model: aiConfig?.model ?? "gpt-4o-mini",
@@ -169,14 +215,167 @@ export function serializeWorkspace(workspace) {
169
215
  };
170
216
  }),
171
217
  concepts: workspace.concepts?.frontmatter.concepts ?? [],
172
- tenantPolicy: (() => {
173
- const schema = loadSchema(workspace.schemaDir);
174
- return schema.tenantPolicy ?? null;
175
- })(),
218
+ tenantPolicy: schema.tenantPolicy ?? null,
219
+ };
220
+ }
221
+ function toRelativeSchemaPath(schemaDir) {
222
+ const rel = relative(process.cwd(), schemaDir);
223
+ if (rel === "")
224
+ return ".";
225
+ if (rel.startsWith("..") || rel.includes(`..${sep}`))
226
+ return schemaDir;
227
+ return rel.startsWith(".") ? rel : `./${rel}`;
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // Setup wizard + server-side introspection (resync)
231
+ // ---------------------------------------------------------------------------
232
+ function requireWorkspace(state) {
233
+ if (!state.workspace) {
234
+ throw new StudioHttpError(409, "Studio is in setup mode — finish the guided setup first.");
235
+ }
236
+ return state.workspace;
237
+ }
238
+ function buildSetupStatus(state) {
239
+ const projectDir = process.cwd();
240
+ if (!state.setupReason) {
241
+ return {
242
+ needed: false,
243
+ reason: null,
244
+ projectDir,
245
+ configPath: null,
246
+ outputDir: state.schemaDir,
247
+ configLoadError: null,
248
+ };
249
+ }
250
+ const probe = probeSetupState(projectDir);
251
+ return {
252
+ needed: true,
253
+ reason: probe.hasConfig ? "no-artifact" : "no-config",
254
+ projectDir,
255
+ configPath: probe.configPath,
256
+ outputDir: probe.outputDir,
257
+ configLoadError: probe.loadError,
258
+ };
259
+ }
260
+ function handleSetupConfig(state, body) {
261
+ if (!state.setupReason) {
262
+ throw new StudioHttpError(409, "Studio is already set up — edit askdb.config.ts directly.");
263
+ }
264
+ const input = parseSetupConfigBody(body);
265
+ const result = writeSetupConfig(process.cwd(), input);
266
+ // Config now exists; the artifact usually doesn't yet.
267
+ const probe = probeSetupState(process.cwd());
268
+ if (probe.hasArtifact && probe.outputDir) {
269
+ state.schemaDir = probe.outputDir;
270
+ state.workspace = loadWorkspace(probe.outputDir);
271
+ state.setupReason = null;
272
+ }
273
+ else {
274
+ state.setupReason = "no-artifact";
275
+ }
276
+ return {
277
+ configPath: result.configPath,
278
+ envExamplePath: result.envExamplePath,
279
+ envVars: result.envVars,
280
+ installed: result.installed,
281
+ manualInstallCommand: result.manualInstallCommand,
282
+ packageJsonCreated: result.packageJsonCreated,
283
+ status: buildSetupStatus(state),
284
+ };
285
+ }
286
+ async function handleSetupIntrospect(state) {
287
+ if (!state.setupReason) {
288
+ throw new StudioHttpError(409, "Studio is already set up — use Resync schema instead.");
289
+ }
290
+ // probeSetupState re-bootstraps, picking up any .env created since server start.
291
+ const probe = probeSetupState(process.cwd());
292
+ if (!probe.hasConfig) {
293
+ throw new StudioHttpError(409, "Write askdb.config.ts first (setup step 1).");
294
+ }
295
+ if (probe.loadError || !probe.outputDir) {
296
+ throw new StudioHttpError(409, `askdb.config.ts exists but could not be loaded: ${probe.loadError ?? "unknown error"}. ` +
297
+ "Install the project dependencies shown in setup step 1, then retry.");
298
+ }
299
+ const outDir = probe.outputDir;
300
+ const run = await runStudioIntrospection({
301
+ outDir,
302
+ hasExistingArtifact: existsSync(join(outDir, "schema.json")),
303
+ });
304
+ state.schemaDir = outDir;
305
+ state.workspace = loadWorkspace(outDir);
306
+ state.setupReason = null;
307
+ return {
308
+ ok: true,
309
+ engine: run.engine,
310
+ schemaId: run.schemaId,
311
+ tables: run.tables,
312
+ warnings: run.warnings,
313
+ workspace: serializeWorkspace(state.workspace),
314
+ };
315
+ }
316
+ async function handleResync(state) {
317
+ const workspace = requireWorkspace(state);
318
+ // Refresh the runtime snapshot so config edits made while Studio runs are honored.
319
+ try {
320
+ bootstrapAskDbEnv({ cwd: process.cwd() });
321
+ }
322
+ catch {
323
+ // Keep the snapshot the server started with when the config disappeared mid-session.
324
+ }
325
+ const run = await runStudioIntrospection({
326
+ outDir: state.schemaDir,
327
+ schemaId: workspace.physical.schemaId,
328
+ hasExistingArtifact: true,
329
+ });
330
+ state.workspace = loadWorkspace(state.schemaDir);
331
+ return {
332
+ ok: true,
333
+ engine: run.engine,
334
+ schemaId: run.schemaId,
335
+ tables: run.tables,
336
+ warnings: run.warnings,
337
+ workspace: serializeWorkspace(state.workspace),
338
+ };
339
+ }
340
+ function buildIntrospectionPlanDto() {
341
+ const plan = resolveStudioIntrospectionPlan();
342
+ return plan.ok
343
+ ? { ok: true, engine: plan.engine, sourceLabel: plan.sourceLabel }
344
+ : { ok: false, engine: plan.engine, error: plan.error };
345
+ }
346
+ function parseSetupConfigBody(body) {
347
+ if (!isRecord(body)) {
348
+ throw new StudioHttpError(400, "Request body must be a JSON object.");
349
+ }
350
+ const databases = ["postgres", "mysql", "sqlite", "sqlserver", "prisma"];
351
+ const aiProviders = ["openai", "anthropic", "google", "azure"];
352
+ if (typeof body.database !== "string" || !databases.includes(body.database)) {
353
+ throw new StudioHttpError(400, `\`database\` must be one of: ${databases.join(", ")}.`);
354
+ }
355
+ if (typeof body.aiProvider !== "string" ||
356
+ !aiProviders.includes(body.aiProvider)) {
357
+ throw new StudioHttpError(400, `\`aiProvider\` must be one of: ${aiProviders.join(", ")}.`);
358
+ }
359
+ const optionalString = (key) => {
360
+ const value = body[key];
361
+ if (value === undefined || value === null || value === "")
362
+ return undefined;
363
+ if (typeof value !== "string")
364
+ throw new StudioHttpError(400, `\`${key}\` must be a string.`);
365
+ return value;
366
+ };
367
+ return {
368
+ database: body.database,
369
+ aiProvider: body.aiProvider,
370
+ connectionEnv: optionalString("connectionEnv"),
371
+ sqliteFile: optionalString("sqliteFile"),
372
+ prismaSchema: optionalString("prismaSchema"),
373
+ aiKeyEnv: optionalString("aiKeyEnv"),
374
+ schemaOut: optionalString("schemaOut"),
176
375
  };
177
376
  }
178
377
  function saveConceptsDraft(state, concepts) {
179
- saveConcepts(state.workspace, { concepts });
378
+ saveConcepts(requireWorkspace(state), { concepts });
180
379
  state.workspace = loadWorkspace(state.schemaDir);
181
380
  }
182
381
  function saveTenantPolicy(state, frontmatter, body) {
@@ -197,10 +396,11 @@ function parseTenantPolicyBody(body) {
197
396
  return { frontmatter: parsed.data, bodyText };
198
397
  }
199
398
  function saveDraft(state, tableId, draft) {
200
- const table = state.workspace.tables.find((candidate) => candidate.physical.id === tableId);
399
+ const workspace = requireWorkspace(state);
400
+ const table = workspace.tables.find((candidate) => candidate.physical.id === tableId);
201
401
  if (!table)
202
402
  throw new StudioHttpError(404, `No such table: ${tableId}`);
203
- const frontmatter = buildFrontmatter(table.physical, state.workspace.physical.schemaId, draft);
403
+ const frontmatter = buildFrontmatter(table.physical, workspace.physical.schemaId, draft);
204
404
  let body = table.parsed
205
405
  ? replaceTableDescription(table.parsed.body, draft.description)
206
406
  : buildDefaultTableBody(table.physical.name, draft.description);
@@ -210,7 +410,7 @@ function saveDraft(state, tableId, draft) {
210
410
  if (draft.exampleQuestions !== undefined) {
211
411
  body = replaceH2Section(body, "Example questions", draft.exampleQuestions);
212
412
  }
213
- saveTable(state.workspace, tableId, frontmatter, body);
413
+ saveTable(workspace, tableId, frontmatter, body);
214
414
  state.workspace = loadWorkspace(state.schemaDir);
215
415
  }
216
416
  async function suggestForSource(workspace, source) {
@@ -229,7 +429,7 @@ async function suggestTenantPolicyDraft(state) {
229
429
  throw new StudioHttpError(400, ai.keyMissingMessage("AI tenant policy suggestion"));
230
430
  }
231
431
  // Build a compact DDL-like representation of the schema for the prompt
232
- const workspace = state.workspace;
432
+ const workspace = requireWorkspace(state);
233
433
  const schemaId = workspace.physical.schemaId;
234
434
  const schemaDdl = workspace.tables
235
435
  .map((t) => {