@duckcodeailabs/dql-cli 1.4.3 → 1.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.
- package/LICENSE +123 -0
- package/README.md +72 -0
- package/dist/apps-api.d.ts +79 -0
- package/dist/apps-api.d.ts.map +1 -0
- package/dist/apps-api.js +934 -0
- package/dist/apps-api.js.map +1 -0
- package/dist/apps-api.test.d.ts +2 -0
- package/dist/apps-api.test.d.ts.map +1 -0
- package/dist/apps-api.test.js +111 -0
- package/dist/apps-api.test.js.map +1 -0
- package/dist/args.d.ts +30 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +105 -0
- package/dist/args.js.map +1 -0
- package/dist/args.test.d.ts +2 -0
- package/dist/args.test.d.ts.map +1 -0
- package/dist/args.test.js +41 -0
- package/dist/args.test.js.map +1 -0
- package/dist/assets/dql-notebook/assets/codemirror-DJYUkPr1.js +11 -0
- package/dist/assets/dql-notebook/assets/index-R3UrqjLQ.css +1 -0
- package/dist/assets/dql-notebook/assets/index-mlfOQ2me.js +857 -0
- package/dist/assets/dql-notebook/assets/react-CRB3T2We.js +32 -0
- package/dist/assets/dql-notebook/index.html +18 -0
- package/dist/assets/notebook-browser/app.js +548 -0
- package/dist/assets/notebook-browser/index.html +83 -0
- package/dist/assets/notebook-browser/styles.css +336 -0
- package/dist/block-studio-import.d.ts +58 -0
- package/dist/block-studio-import.d.ts.map +1 -0
- package/dist/block-studio-import.js +390 -0
- package/dist/block-studio-import.js.map +1 -0
- package/dist/block-studio-import.test.d.ts +2 -0
- package/dist/block-studio-import.test.d.ts.map +1 -0
- package/dist/block-studio-import.test.js +106 -0
- package/dist/block-studio-import.test.js.map +1 -0
- package/dist/block-templates.d.ts +8 -0
- package/dist/block-templates.d.ts.map +1 -0
- package/dist/block-templates.js +60 -0
- package/dist/block-templates.js.map +1 -0
- package/dist/commands/agent.d.ts +19 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +258 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/app.d.ts +32 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +307 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +69 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/build.test.d.ts +2 -0
- package/dist/commands/build.test.d.ts.map +1 -0
- package/dist/commands/build.test.js +44 -0
- package/dist/commands/build.test.js.map +1 -0
- package/dist/commands/certify.d.ts +3 -0
- package/dist/commands/certify.d.ts.map +1 -0
- package/dist/commands/certify.js +228 -0
- package/dist/commands/certify.js.map +1 -0
- package/dist/commands/compile.d.ts +21 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +198 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/compile.test.d.ts +2 -0
- package/dist/commands/compile.test.d.ts.map +1 -0
- package/dist/commands/compile.test.js +115 -0
- package/dist/commands/compile.test.js.map +1 -0
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +52 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +191 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +2 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +43 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/fmt.d.ts +3 -0
- package/dist/commands/fmt.d.ts.map +1 -0
- package/dist/commands/fmt.js +53 -0
- package/dist/commands/fmt.js.map +1 -0
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +50 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/info.d.ts +3 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/info.js +56 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +250 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +118 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/lineage.d.ts +24 -0
- package/dist/commands/lineage.d.ts.map +1 -0
- package/dist/commands/lineage.js +634 -0
- package/dist/commands/lineage.js.map +1 -0
- package/dist/commands/mcp.d.ts +7 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +16 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/migrate.d.ts +12 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +197 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/new.d.ts +3 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +490 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/new.test.d.ts +2 -0
- package/dist/commands/new.test.d.ts.map +1 -0
- package/dist/commands/new.test.js +191 -0
- package/dist/commands/new.test.js.map +1 -0
- package/dist/commands/notebook.d.ts +3 -0
- package/dist/commands/notebook.d.ts.map +1 -0
- package/dist/commands/notebook.js +46 -0
- package/dist/commands/notebook.js.map +1 -0
- package/dist/commands/parse.d.ts +3 -0
- package/dist/commands/parse.d.ts.map +1 -0
- package/dist/commands/parse.js +63 -0
- package/dist/commands/parse.js.map +1 -0
- package/dist/commands/preview.d.ts +3 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +42 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.d.ts.map +1 -0
- package/dist/commands/schedule.js +215 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/semantic.d.ts +12 -0
- package/dist/commands/semantic.d.ts.map +1 -0
- package/dist/commands/semantic.js +356 -0
- package/dist/commands/semantic.js.map +1 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +30 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/slack.d.ts +13 -0
- package/dist/commands/slack.d.ts.map +1 -0
- package/dist/commands/slack.js +53 -0
- package/dist/commands/slack.js.map +1 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +192 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/sync.test.d.ts +2 -0
- package/dist/commands/sync.test.d.ts.map +1 -0
- package/dist/commands/sync.test.js +147 -0
- package/dist/commands/sync.test.js.map +1 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +167 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +163 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/validate.test.d.ts +2 -0
- package/dist/commands/validate.test.d.ts.map +1 -0
- package/dist/commands/validate.test.js +55 -0
- package/dist/commands/validate.test.js.map +1 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +74 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/digest.d.ts +10 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +83 -0
- package/dist/digest.js.map +1 -0
- package/dist/git-service.d.ts +17 -0
- package/dist/git-service.d.ts.map +1 -0
- package/dist/git-service.js +54 -0
- package/dist/git-service.js.map +1 -0
- package/dist/governance-runtime.d.ts +15 -0
- package/dist/governance-runtime.d.ts.map +1 -0
- package/dist/governance-runtime.js +50 -0
- package/dist/governance-runtime.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/{index.js → dist/index.js} +5 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/index.d.ts +4 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +20 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/providers/claude-agent-sdk.d.ts +3 -0
- package/dist/llm/providers/claude-agent-sdk.d.ts.map +1 -0
- package/dist/llm/providers/claude-agent-sdk.js +174 -0
- package/dist/llm/providers/claude-agent-sdk.js.map +1 -0
- package/dist/llm/providers/claude-code.d.ts +8 -0
- package/dist/llm/providers/claude-code.d.ts.map +1 -0
- package/dist/llm/providers/claude-code.js +171 -0
- package/dist/llm/providers/claude-code.js.map +1 -0
- package/dist/llm/providers/dql-agent-provider.d.ts +5 -0
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -0
- package/dist/llm/providers/dql-agent-provider.js +287 -0
- package/dist/llm/providers/dql-agent-provider.js.map +1 -0
- package/dist/llm/tools.d.ts +9 -0
- package/dist/llm/tools.d.ts.map +1 -0
- package/dist/llm/tools.js +112 -0
- package/dist/llm/tools.js.map +1 -0
- package/dist/llm/types.d.ts +72 -0
- package/dist/llm/types.d.ts.map +1 -0
- package/dist/llm/types.js +2 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/local-runtime.d.ts +142 -0
- package/dist/local-runtime.d.ts.map +1 -0
- package/dist/local-runtime.js +4859 -0
- package/dist/local-runtime.js.map +1 -0
- package/dist/local-runtime.test.d.ts +2 -0
- package/dist/local-runtime.test.d.ts.map +1 -0
- package/dist/local-runtime.test.js +241 -0
- package/dist/local-runtime.test.js.map +1 -0
- package/dist/metricflow.d.ts +35 -0
- package/dist/metricflow.d.ts.map +1 -0
- package/dist/metricflow.js +122 -0
- package/dist/metricflow.js.map +1 -0
- package/dist/metricflow.test.d.ts +2 -0
- package/dist/metricflow.test.d.ts.map +1 -0
- package/dist/metricflow.test.js +54 -0
- package/dist/metricflow.test.js.map +1 -0
- package/dist/open-browser.d.ts +2 -0
- package/dist/open-browser.d.ts.map +1 -0
- package/dist/open-browser.js +29 -0
- package/dist/open-browser.js.map +1 -0
- package/dist/schedule/alerts.d.ts +5 -0
- package/dist/schedule/alerts.d.ts.map +1 -0
- package/dist/schedule/alerts.js +54 -0
- package/dist/schedule/alerts.js.map +1 -0
- package/dist/schedule/discovery.d.ts +4 -0
- package/dist/schedule/discovery.d.ts.map +1 -0
- package/dist/schedule/discovery.js +36 -0
- package/dist/schedule/discovery.js.map +1 -0
- package/dist/schedule/notifiers/email.d.ts +3 -0
- package/dist/schedule/notifiers/email.d.ts.map +1 -0
- package/dist/schedule/notifiers/email.js +76 -0
- package/dist/schedule/notifiers/email.js.map +1 -0
- package/dist/schedule/notifiers/file.d.ts +3 -0
- package/dist/schedule/notifiers/file.d.ts.map +1 -0
- package/dist/schedule/notifiers/file.js +50 -0
- package/dist/schedule/notifiers/file.js.map +1 -0
- package/dist/schedule/notifiers/index.d.ts +10 -0
- package/dist/schedule/notifiers/index.d.ts.map +1 -0
- package/dist/schedule/notifiers/index.js +33 -0
- package/dist/schedule/notifiers/index.js.map +1 -0
- package/dist/schedule/notifiers/slack.d.ts +3 -0
- package/dist/schedule/notifiers/slack.d.ts.map +1 -0
- package/dist/schedule/notifiers/slack.js +58 -0
- package/dist/schedule/notifiers/slack.js.map +1 -0
- package/dist/schedule/runner.d.ts +14 -0
- package/dist/schedule/runner.d.ts.map +1 -0
- package/dist/schedule/runner.js +225 -0
- package/dist/schedule/runner.js.map +1 -0
- package/dist/schedule/runs.d.ts +5 -0
- package/dist/schedule/runs.d.ts.map +1 -0
- package/dist/schedule/runs.js +41 -0
- package/dist/schedule/runs.js.map +1 -0
- package/dist/schedule/service.d.ts +13 -0
- package/dist/schedule/service.d.ts.map +1 -0
- package/dist/schedule/service.js +87 -0
- package/dist/schedule/service.js.map +1 -0
- package/dist/schedule/types.d.ts +70 -0
- package/dist/schedule/types.d.ts.map +1 -0
- package/dist/schedule/types.js +2 -0
- package/dist/schedule/types.js.map +1 -0
- package/dist/semantic-import.d.ts +135 -0
- package/dist/semantic-import.d.ts.map +1 -0
- package/dist/semantic-import.js +979 -0
- package/dist/semantic-import.js.map +1 -0
- package/dist/semantic-import.test.d.ts +2 -0
- package/dist/semantic-import.test.d.ts.map +1 -0
- package/dist/semantic-import.test.js +95 -0
- package/dist/semantic-import.test.js.map +1 -0
- package/dist/settings/provider-settings.d.ts +33 -0
- package/dist/settings/provider-settings.d.ts.map +1 -0
- package/dist/settings/provider-settings.js +91 -0
- package/dist/settings/provider-settings.js.map +1 -0
- package/package.json +29 -21
package/dist/apps-api.js
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP handlers for `/api/apps`, `/api/apps/:id`, `/api/apps/:id/dashboards/:did`,
|
|
3
|
+
* `/api/persona`. Designed to be invoked from `local-runtime.ts`'s request
|
|
4
|
+
* dispatcher — returns `true` if the request was handled, `false` otherwise.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, dirname, relative } from 'node:path';
|
|
8
|
+
import { loadAppDocument, findAppDocuments, loadDashboardDocument, findDashboardsForApp, parseAppDocument, parseDashboardDocument, suggestAppId, } from '@duckcodeailabs/dql-core';
|
|
9
|
+
import { defaultPersonaRegistry, defaultLocalAppsDbPath, LocalAppStorage, personaFromMember, } from '@duckcodeailabs/dql-project';
|
|
10
|
+
export async function handleAppsApi(ctx) {
|
|
11
|
+
const { req, res, path, projectRoot } = ctx;
|
|
12
|
+
// ── Apps ────────────────────────────────────────────────────────────────
|
|
13
|
+
if (req.method === 'GET' && path === '/api/apps') {
|
|
14
|
+
const apps = collectAppsList(projectRoot);
|
|
15
|
+
sendJson(res, 200, { apps });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (req.method === 'POST' && path === '/api/apps/recommend-blocks') {
|
|
19
|
+
try {
|
|
20
|
+
const body = await readJson(req);
|
|
21
|
+
sendJson(res, 200, { blocks: recommendBlocks(projectRoot, body) });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
sendJson(res, 500, { error: err.message });
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (req.method === 'POST' && path === '/api/apps') {
|
|
29
|
+
try {
|
|
30
|
+
const body = await readJson(req);
|
|
31
|
+
const result = createAppPackage(projectRoot, body);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
sendJson(res, 400, { error: result.error });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
sendJson(res, 201, result);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
sendJson(res, 500, { error: err.message });
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
let m = path.match(/^\/api\/apps\/([^/]+)\/editor\/catalog$/);
|
|
44
|
+
if (m && req.method === 'GET') {
|
|
45
|
+
const appId = decodeURIComponent(m[1]);
|
|
46
|
+
const app = loadAppById(projectRoot, appId)?.app;
|
|
47
|
+
if (!app) {
|
|
48
|
+
sendJson(res, 404, { error: `App "${appId}" not found` });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const domain = ctx.url.searchParams.get('domain') ?? app.domain;
|
|
52
|
+
const certifiedOnly = ctx.url.searchParams.get('certifiedOnly') !== 'false';
|
|
53
|
+
const blocks = collectBlockCandidates(projectRoot)
|
|
54
|
+
.filter((block) => !certifiedOnly || block.status === 'certified')
|
|
55
|
+
.filter((block) => !domain || block.domain === domain || appAllowsExecute(app, block.domain))
|
|
56
|
+
.sort((a, b) => {
|
|
57
|
+
const aDomain = a.domain === app.domain ? 0 : 1;
|
|
58
|
+
const bDomain = b.domain === app.domain ? 0 : 1;
|
|
59
|
+
return aDomain - bDomain || a.name.localeCompare(b.name);
|
|
60
|
+
});
|
|
61
|
+
sendJson(res, 200, {
|
|
62
|
+
appId,
|
|
63
|
+
defaultDomain: app.domain,
|
|
64
|
+
domains: unique(blocks.map((block) => block.domain)),
|
|
65
|
+
blocks,
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards$/);
|
|
70
|
+
if (m && req.method === 'POST') {
|
|
71
|
+
const appId = decodeURIComponent(m[1]);
|
|
72
|
+
try {
|
|
73
|
+
const body = await readJson(req);
|
|
74
|
+
const result = createDashboardForApp(projectRoot, appId, body);
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
sendJson(res, 400, { error: result.error });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
sendJson(res, 201, result);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
sendJson(res, 500, { error: err.message });
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins$/);
|
|
87
|
+
if (m) {
|
|
88
|
+
const appId = decodeURIComponent(m[1]);
|
|
89
|
+
if (req.method === 'GET') {
|
|
90
|
+
const dashboardId = ctx.url.searchParams.get('dashboardId') ?? undefined;
|
|
91
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
92
|
+
try {
|
|
93
|
+
sendJson(res, 200, { pins: storage.listAiPins(appId, dashboardId) });
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
storage.close();
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (req.method === 'POST') {
|
|
101
|
+
try {
|
|
102
|
+
const body = await readJson(req);
|
|
103
|
+
const created = createAiPinTile(projectRoot, appId, body);
|
|
104
|
+
if (!created.ok) {
|
|
105
|
+
sendJson(res, 400, { error: created.error });
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
sendJson(res, 201, created);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
sendJson(res, 500, { error: err.message });
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins\/([^/]+)\/refresh$/);
|
|
117
|
+
if (m && req.method === 'POST') {
|
|
118
|
+
const pinId = decodeURIComponent(m[2]);
|
|
119
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
120
|
+
try {
|
|
121
|
+
const pin = storage.getAiPin(pinId);
|
|
122
|
+
if (!pin) {
|
|
123
|
+
sendJson(res, 404, { error: `AI pin "${pinId}" not found` });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (!pin.sql) {
|
|
127
|
+
const updated = storage.updateAiPinResult(pinId, pin.result, 'Pin has no SQL to refresh.');
|
|
128
|
+
sendJson(res, 400, { error: 'Pin has no SQL to refresh.', pin: updated });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (!ctx.executeSql) {
|
|
132
|
+
const updated = storage.updateAiPinResult(pinId, pin.result, 'This host cannot execute AI pin SQL.');
|
|
133
|
+
sendJson(res, 400, { error: 'This host cannot execute AI pin SQL.', pin: updated });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const result = await ctx.executeSql(pin.sql);
|
|
137
|
+
const updated = storage.updateAiPinResult(pinId, result);
|
|
138
|
+
sendJson(res, 200, { ok: true, pin: updated });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const pin = storage.updateAiPinResult(pinId, undefined, err instanceof Error ? err.message : String(err));
|
|
142
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err), pin });
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
storage.close();
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins\/([^/]+)\/promote$/);
|
|
150
|
+
if (m && req.method === 'POST') {
|
|
151
|
+
const appId = decodeURIComponent(m[1]);
|
|
152
|
+
const pinId = decodeURIComponent(m[2]);
|
|
153
|
+
try {
|
|
154
|
+
const result = promoteAiPinToDraftBlock(projectRoot, appId, pinId);
|
|
155
|
+
if (!result.ok) {
|
|
156
|
+
sendJson(res, 400, { error: result.error });
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
sendJson(res, 200, result);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
sendJson(res, 500, { error: err.message });
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)\/layout$/);
|
|
167
|
+
if (m && req.method === 'PATCH') {
|
|
168
|
+
const appId = decodeURIComponent(m[1]);
|
|
169
|
+
const dashboardId = decodeURIComponent(m[2]);
|
|
170
|
+
try {
|
|
171
|
+
const body = await readJson(req);
|
|
172
|
+
const result = patchDashboardLayout(projectRoot, appId, dashboardId, body);
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
sendJson(res, 400, { error: result.error });
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
sendJson(res, 200, result);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
sendJson(res, 500, { error: err.message });
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
// /api/apps/:id — single App with dashboards summary
|
|
185
|
+
m = path.match(/^\/api\/apps\/([^/]+)$/);
|
|
186
|
+
if (m && req.method === 'GET') {
|
|
187
|
+
const id = m[1];
|
|
188
|
+
const result = loadAppById(projectRoot, id);
|
|
189
|
+
if (!result) {
|
|
190
|
+
sendJson(res, 404, { error: `App "${id}" not found` });
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
sendJson(res, 200, result);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
// /api/apps/:id/dashboards
|
|
197
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards$/);
|
|
198
|
+
if (m && req.method === 'GET') {
|
|
199
|
+
const id = m[1];
|
|
200
|
+
const dashboards = listDashboardsFor(projectRoot, id);
|
|
201
|
+
if (dashboards === null) {
|
|
202
|
+
sendJson(res, 404, { error: `App "${id}" not found` });
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
sendJson(res, 200, { dashboards });
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// /api/apps/:id/dashboards/:did
|
|
209
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)$/);
|
|
210
|
+
if (m) {
|
|
211
|
+
const id = m[1];
|
|
212
|
+
const did = m[2];
|
|
213
|
+
if (req.method === 'GET') {
|
|
214
|
+
const result = loadDashboardForApp(projectRoot, id, did);
|
|
215
|
+
if (!result) {
|
|
216
|
+
sendJson(res, 404, { error: `Dashboard "${did}" not found in app "${id}"` });
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
sendJson(res, 200, result);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
223
|
+
try {
|
|
224
|
+
const body = await readJson(req);
|
|
225
|
+
const written = await writeDashboard(projectRoot, id, did, body);
|
|
226
|
+
if (!written.ok) {
|
|
227
|
+
sendJson(res, 400, { error: written.error });
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
sendJson(res, 200, { ok: true, path: written.path });
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
sendJson(res, 500, { error: err.message });
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ── Persona ────────────────────────────────────────────────────────────
|
|
239
|
+
if (path === '/api/persona') {
|
|
240
|
+
if (req.method === 'GET') {
|
|
241
|
+
sendJson(res, 200, { persona: defaultPersonaRegistry.active });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (req.method === 'DELETE') {
|
|
245
|
+
defaultPersonaRegistry.clear();
|
|
246
|
+
sendJson(res, 200, { persona: null });
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (req.method === 'POST') {
|
|
250
|
+
try {
|
|
251
|
+
const body = await readJson(req);
|
|
252
|
+
const userId = typeof body.userId === 'string' ? body.userId : null;
|
|
253
|
+
const appId = typeof body.appId === 'string' ? body.appId : null;
|
|
254
|
+
if (!userId) {
|
|
255
|
+
sendJson(res, 400, { error: 'userId is required' });
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
const persona = activatePersona(projectRoot, userId, appId);
|
|
259
|
+
if (!persona) {
|
|
260
|
+
sendJson(res, 404, { error: 'No App member matches this userId' });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
sendJson(res, 200, { persona });
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
sendJson(res, 500, { error: err.message });
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
// ---- Helpers ----
|
|
274
|
+
function collectAppsList(projectRoot) {
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const p of findAppDocuments(projectRoot)) {
|
|
277
|
+
const { document } = loadAppDocument(p);
|
|
278
|
+
if (!document)
|
|
279
|
+
continue;
|
|
280
|
+
const appDir = p.slice(0, -'/dql.app.json'.length);
|
|
281
|
+
const dashboards = [];
|
|
282
|
+
for (const d of findDashboardsForApp(appDir)) {
|
|
283
|
+
const { document: dd } = loadDashboardDocument(d);
|
|
284
|
+
if (dd)
|
|
285
|
+
dashboards.push({ id: dd.id, title: dd.metadata.title });
|
|
286
|
+
}
|
|
287
|
+
out.push({
|
|
288
|
+
id: document.id,
|
|
289
|
+
name: document.name,
|
|
290
|
+
domain: document.domain,
|
|
291
|
+
description: document.description,
|
|
292
|
+
audience: audienceFromTags(document.tags ?? []),
|
|
293
|
+
status: dashboards.length > 0 ? 'ready' : 'empty',
|
|
294
|
+
storage: 'shared',
|
|
295
|
+
owners: document.owners,
|
|
296
|
+
tags: document.tags ?? [],
|
|
297
|
+
members: document.members.length,
|
|
298
|
+
roles: document.roles.length,
|
|
299
|
+
policies: document.policies.length,
|
|
300
|
+
schedules: (document.schedules ?? []).length,
|
|
301
|
+
dashboards,
|
|
302
|
+
homepage: document.homepage,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
306
|
+
}
|
|
307
|
+
export function recommendBlocks(projectRoot, input) {
|
|
308
|
+
const domain = cleanString(input.domain).toLowerCase();
|
|
309
|
+
const tags = normalizeTags(input.tags ?? []);
|
|
310
|
+
const text = [input.purpose, input.audience, ...(input.tags ?? [])].map((v) => cleanString(v).toLowerCase()).filter(Boolean);
|
|
311
|
+
const certifiedOnly = input.certifiedOnly !== false;
|
|
312
|
+
const hasCriteria = Boolean(domain || tags.length > 0 || text.length > 0);
|
|
313
|
+
return collectBlockCandidates(projectRoot)
|
|
314
|
+
.map((block) => {
|
|
315
|
+
let score = 0;
|
|
316
|
+
let criteriaScore = 0;
|
|
317
|
+
const reasons = [];
|
|
318
|
+
if (domain && block.domain.toLowerCase() === domain) {
|
|
319
|
+
score += 100;
|
|
320
|
+
criteriaScore += 100;
|
|
321
|
+
reasons.push('domain match');
|
|
322
|
+
}
|
|
323
|
+
if (certifiedOnly && block.status !== 'certified')
|
|
324
|
+
return null;
|
|
325
|
+
if (block.status === 'certified') {
|
|
326
|
+
score += 30;
|
|
327
|
+
reasons.push('certified');
|
|
328
|
+
}
|
|
329
|
+
const overlap = block.tags.filter((tag) => tags.includes(tag.toLowerCase()));
|
|
330
|
+
if (overlap.length > 0) {
|
|
331
|
+
score += overlap.length * 12;
|
|
332
|
+
criteriaScore += overlap.length * 12;
|
|
333
|
+
reasons.push(`tag match: ${overlap.join(', ')}`);
|
|
334
|
+
}
|
|
335
|
+
const haystack = [block.name, block.description, block.owner ?? '', block.llmContext ?? '', ...block.tags]
|
|
336
|
+
.join(' ')
|
|
337
|
+
.toLowerCase();
|
|
338
|
+
const textHits = text.filter((term) => term && haystack.includes(term));
|
|
339
|
+
if (textHits.length > 0) {
|
|
340
|
+
score += textHits.length * 6;
|
|
341
|
+
criteriaScore += textHits.length * 6;
|
|
342
|
+
reasons.push('context match');
|
|
343
|
+
}
|
|
344
|
+
if (score === 0 && !domain && tags.length === 0 && !certifiedOnly)
|
|
345
|
+
score = 1;
|
|
346
|
+
if (hasCriteria && criteriaScore === 0)
|
|
347
|
+
return null;
|
|
348
|
+
if (score === 0)
|
|
349
|
+
return null;
|
|
350
|
+
return { ...block, score, reasons };
|
|
351
|
+
})
|
|
352
|
+
.filter((block) => Boolean(block))
|
|
353
|
+
.sort((a, b) => b.score - a.score || new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime())
|
|
354
|
+
.slice(0, 50);
|
|
355
|
+
}
|
|
356
|
+
export function createAppPackage(projectRoot, input) {
|
|
357
|
+
const name = cleanString(input.name);
|
|
358
|
+
const domain = cleanString(input.domain);
|
|
359
|
+
if (!name)
|
|
360
|
+
return { ok: false, error: 'name is required' };
|
|
361
|
+
if (!domain)
|
|
362
|
+
return { ok: false, error: 'domain is required' };
|
|
363
|
+
const id = suggestAppId(name);
|
|
364
|
+
const appDir = join(projectRoot, 'apps', id);
|
|
365
|
+
if (existsSync(appDir))
|
|
366
|
+
return { ok: false, error: `App already exists: ${id}` };
|
|
367
|
+
const owner = cleanString(input.owners?.[0]) || `${process.env.USER ?? 'owner'}@local`;
|
|
368
|
+
const audience = cleanString(input.audience);
|
|
369
|
+
const tags = normalizeTags([...(input.tags ?? []), audience ? `audience:${slugify(audience)}` : '']);
|
|
370
|
+
const selectedIds = Array.from(new Set((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean)));
|
|
371
|
+
const blocks = collectBlockCandidates(projectRoot);
|
|
372
|
+
const selectedBlocks = selectedIds
|
|
373
|
+
.map((blockId) => blocks.find((block) => block.id === blockId || block.name === blockId))
|
|
374
|
+
.filter((block) => Boolean(block));
|
|
375
|
+
const app = {
|
|
376
|
+
version: 1,
|
|
377
|
+
id,
|
|
378
|
+
name,
|
|
379
|
+
description: cleanString(input.purpose) || `${name} consumption surface for ${domain}`,
|
|
380
|
+
domain,
|
|
381
|
+
owners: [owner],
|
|
382
|
+
tags,
|
|
383
|
+
members: [
|
|
384
|
+
{ userId: owner, displayName: owner, roles: ['owner', 'analyst'] },
|
|
385
|
+
],
|
|
386
|
+
roles: [
|
|
387
|
+
{ id: 'owner', displayName: 'Owner', description: 'Full access to dashboards and App configuration.' },
|
|
388
|
+
{ id: 'analyst', displayName: 'Analyst', description: 'Can execute dashboards and review generated drafts.' },
|
|
389
|
+
{ id: 'viewer', displayName: 'Viewer', description: 'Read-only access to certified dashboard consumption.' },
|
|
390
|
+
],
|
|
391
|
+
policies: [
|
|
392
|
+
{
|
|
393
|
+
id: 'viewers-read',
|
|
394
|
+
domain,
|
|
395
|
+
minClassification: 'internal',
|
|
396
|
+
allowedRoles: ['viewer', 'analyst', 'owner'],
|
|
397
|
+
accessLevel: 'read',
|
|
398
|
+
enabled: true,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'analyst-execute',
|
|
402
|
+
domain,
|
|
403
|
+
minClassification: 'internal',
|
|
404
|
+
allowedRoles: ['analyst', 'owner'],
|
|
405
|
+
accessLevel: 'execute',
|
|
406
|
+
enabled: true,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'owner-admin',
|
|
410
|
+
domain,
|
|
411
|
+
minClassification: 'restricted',
|
|
412
|
+
allowedRoles: ['owner'],
|
|
413
|
+
accessLevel: 'admin',
|
|
414
|
+
enabled: true,
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
rlsBindings: [],
|
|
418
|
+
schedules: [],
|
|
419
|
+
homepage: { type: 'dashboard', id: 'overview' },
|
|
420
|
+
};
|
|
421
|
+
const dashboard = {
|
|
422
|
+
version: 1,
|
|
423
|
+
id: 'overview',
|
|
424
|
+
metadata: {
|
|
425
|
+
title: `${name} Overview`,
|
|
426
|
+
description: cleanString(input.purpose) || `Starter dashboard for ${name}`,
|
|
427
|
+
domain,
|
|
428
|
+
tags,
|
|
429
|
+
},
|
|
430
|
+
layout: {
|
|
431
|
+
kind: 'grid',
|
|
432
|
+
cols: 12,
|
|
433
|
+
rowHeight: 80,
|
|
434
|
+
items: buildDashboardItems(selectedBlocks),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
const paths = [
|
|
438
|
+
join(appDir, 'dql.app.json'),
|
|
439
|
+
join(appDir, 'README.md'),
|
|
440
|
+
join(appDir, 'dashboards', 'overview.dqld'),
|
|
441
|
+
join(appDir, 'notebooks'),
|
|
442
|
+
join(appDir, 'drafts'),
|
|
443
|
+
];
|
|
444
|
+
mkdirSync(join(appDir, 'dashboards'), { recursive: true });
|
|
445
|
+
mkdirSync(join(appDir, 'notebooks'), { recursive: true });
|
|
446
|
+
mkdirSync(join(appDir, 'drafts'), { recursive: true });
|
|
447
|
+
writeFileSync(join(appDir, 'dql.app.json'), JSON.stringify(app, null, 2) + '\n', 'utf-8');
|
|
448
|
+
writeFileSync(join(appDir, 'dashboards', 'overview.dqld'), JSON.stringify(dashboard, null, 2) + '\n', 'utf-8');
|
|
449
|
+
writeFileSync(join(appDir, 'README.md'), appReadme(app, audience, selectedBlocks), 'utf-8');
|
|
450
|
+
const created = collectAppsList(projectRoot).find((entry) => entry.id === id);
|
|
451
|
+
if (!created)
|
|
452
|
+
return { ok: false, error: `App was written but could not be reloaded: ${id}` };
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
app: created,
|
|
456
|
+
paths: paths.map((path) => path.startsWith(projectRoot) ? path.slice(projectRoot.length + 1) : path),
|
|
457
|
+
dashboardId: 'overview',
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function buildDashboardItems(blocks) {
|
|
461
|
+
let x = 0;
|
|
462
|
+
let y = 0;
|
|
463
|
+
let rowH = 0;
|
|
464
|
+
return [...blocks]
|
|
465
|
+
.sort((a, b) => vizRank(a.chartType) - vizRank(b.chartType))
|
|
466
|
+
.map((block, index) => {
|
|
467
|
+
const chartType = normalizeVizType(block.chartType);
|
|
468
|
+
const size = tileSize(chartType);
|
|
469
|
+
if (x + size.w > 12) {
|
|
470
|
+
x = 0;
|
|
471
|
+
y += rowH || size.h;
|
|
472
|
+
rowH = 0;
|
|
473
|
+
}
|
|
474
|
+
const item = dashboardItemForBlock(block, chartType, x, y, size, index);
|
|
475
|
+
x += size.w;
|
|
476
|
+
rowH = Math.max(rowH, size.h);
|
|
477
|
+
return item;
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function dashboardItemForBlock(block, chartType, x, y, size, index) {
|
|
481
|
+
return {
|
|
482
|
+
i: slugify(block.name) || `tile-${index + 1}`,
|
|
483
|
+
x,
|
|
484
|
+
y,
|
|
485
|
+
w: size.w,
|
|
486
|
+
h: size.h,
|
|
487
|
+
block: { blockId: block.name },
|
|
488
|
+
viz: { type: chartType },
|
|
489
|
+
title: block.name,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function vizRank(chartType) {
|
|
493
|
+
const normalized = normalizeVizType(chartType);
|
|
494
|
+
if (normalized === 'single_value' || normalized === 'kpi')
|
|
495
|
+
return 0;
|
|
496
|
+
if (normalized === 'line' || normalized === 'area')
|
|
497
|
+
return 1;
|
|
498
|
+
if (normalized === 'bar' || normalized === 'pie' || normalized === 'funnel' || normalized === 'map')
|
|
499
|
+
return 2;
|
|
500
|
+
return 3;
|
|
501
|
+
}
|
|
502
|
+
function tileSize(chartType) {
|
|
503
|
+
if (chartType === 'single_value' || chartType === 'kpi')
|
|
504
|
+
return { w: 3, h: 2 };
|
|
505
|
+
if (chartType === 'table' || chartType === 'pivot')
|
|
506
|
+
return { w: 6, h: 4 };
|
|
507
|
+
return { w: 6, h: 3 };
|
|
508
|
+
}
|
|
509
|
+
function normalizeVizType(chartType) {
|
|
510
|
+
const normalized = (chartType ?? 'table').toLowerCase().replace(/-/g, '_');
|
|
511
|
+
if (normalized === 'single' || normalized === 'single_value')
|
|
512
|
+
return 'single_value';
|
|
513
|
+
if (normalized === 'kpi')
|
|
514
|
+
return 'kpi';
|
|
515
|
+
if (normalized === 'line')
|
|
516
|
+
return 'line';
|
|
517
|
+
if (normalized === 'bar')
|
|
518
|
+
return 'bar';
|
|
519
|
+
if (normalized === 'area')
|
|
520
|
+
return 'area';
|
|
521
|
+
if (normalized === 'pie')
|
|
522
|
+
return 'pie';
|
|
523
|
+
if (normalized === 'pivot')
|
|
524
|
+
return 'pivot';
|
|
525
|
+
if (normalized === 'map')
|
|
526
|
+
return 'map';
|
|
527
|
+
if (normalized === 'funnel')
|
|
528
|
+
return 'funnel';
|
|
529
|
+
return 'table';
|
|
530
|
+
}
|
|
531
|
+
function appReadme(app, audience, blocks) {
|
|
532
|
+
return [
|
|
533
|
+
`# ${app.name}`,
|
|
534
|
+
'',
|
|
535
|
+
app.description ?? '',
|
|
536
|
+
'',
|
|
537
|
+
`- Domain: ${app.domain}`,
|
|
538
|
+
`- Audience: ${audience || 'not specified'}`,
|
|
539
|
+
`- Owners: ${app.owners.join(', ')}`,
|
|
540
|
+
`- Starter dashboard: dashboards/overview.dqld`,
|
|
541
|
+
'',
|
|
542
|
+
'## Selected Certified Blocks',
|
|
543
|
+
'',
|
|
544
|
+
...(blocks.length > 0
|
|
545
|
+
? blocks.map((block) => `- ${block.name} (${block.domain}, ${block.status}) - ${block.path}`)
|
|
546
|
+
: ['No blocks selected yet. Add certified blocks from the Apps Command Center.']),
|
|
547
|
+
'',
|
|
548
|
+
'## Governance',
|
|
549
|
+
'',
|
|
550
|
+
'This OSS App uses local persona switching with owner, analyst, and viewer roles. Real authentication and SSO are intentionally outside OSS scope.',
|
|
551
|
+
'',
|
|
552
|
+
].join('\n');
|
|
553
|
+
}
|
|
554
|
+
function collectBlockCandidates(projectRoot) {
|
|
555
|
+
const blocksDir = join(projectRoot, 'blocks');
|
|
556
|
+
const blocks = [];
|
|
557
|
+
if (!existsSync(blocksDir))
|
|
558
|
+
return blocks;
|
|
559
|
+
const scanDir = (dir) => {
|
|
560
|
+
for (const entry of readdirSyncSafe(dir)) {
|
|
561
|
+
const filePath = join(dir, entry.name);
|
|
562
|
+
if (entry.isDirectory()) {
|
|
563
|
+
scanDir(filePath);
|
|
564
|
+
}
|
|
565
|
+
else if (entry.isFile() && entry.name.endsWith('.dql')) {
|
|
566
|
+
try {
|
|
567
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
568
|
+
const stat = statSyncSafe(filePath);
|
|
569
|
+
const name = matchString(source, /block\s+"([^"]+)"/) ?? entry.name.replace(/\.dql$/, '');
|
|
570
|
+
const tags = matchArray(source, /tags\s*=\s*\[([^\]]*)\]/);
|
|
571
|
+
blocks.push({
|
|
572
|
+
id: name,
|
|
573
|
+
name,
|
|
574
|
+
domain: matchString(source, /domain\s*=\s*"([^"]+)"/) ?? 'uncategorized',
|
|
575
|
+
status: matchString(source, /status\s*=\s*"([^"]+)"/) ?? 'draft',
|
|
576
|
+
owner: matchString(source, /owner\s*=\s*"([^"]+)"/),
|
|
577
|
+
tags,
|
|
578
|
+
path: filePath.slice(projectRoot.length + 1),
|
|
579
|
+
lastModified: stat?.mtime.toISOString() ?? new Date(0).toISOString(),
|
|
580
|
+
description: matchString(source, /description\s*=\s*"((?:[^"\\]|\\.)*)"/) ?? '',
|
|
581
|
+
llmContext: matchString(source, /llmContext\s*=\s*"((?:[^"\\]|\\.)*)"/),
|
|
582
|
+
chartType: matchString(source, /chart\s*=\s*"([^"]+)"/) ?? matchString(source, /chart\.(\w+)\s*\(/) ?? undefined,
|
|
583
|
+
score: 0,
|
|
584
|
+
reasons: [],
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// skip unreadable block
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
scanDir(blocksDir);
|
|
594
|
+
return blocks;
|
|
595
|
+
}
|
|
596
|
+
function readdirSyncSafe(dir) {
|
|
597
|
+
try {
|
|
598
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function statSyncSafe(path) {
|
|
605
|
+
try {
|
|
606
|
+
return statSync(path);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function matchString(source, regex) {
|
|
613
|
+
const match = regex.exec(source);
|
|
614
|
+
return match?.[1]?.replace(/\\"/g, '"').trim() || null;
|
|
615
|
+
}
|
|
616
|
+
function matchArray(source, regex) {
|
|
617
|
+
const match = regex.exec(source);
|
|
618
|
+
if (!match)
|
|
619
|
+
return [];
|
|
620
|
+
return match[1]
|
|
621
|
+
.split(',')
|
|
622
|
+
.map((item) => item.trim().replace(/^"|"$/g, ''))
|
|
623
|
+
.filter(Boolean);
|
|
624
|
+
}
|
|
625
|
+
function cleanString(value) {
|
|
626
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
627
|
+
}
|
|
628
|
+
function normalizeTags(values) {
|
|
629
|
+
return Array.from(new Set(values.map((value) => cleanString(value)).filter(Boolean)));
|
|
630
|
+
}
|
|
631
|
+
function unique(values) {
|
|
632
|
+
return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
633
|
+
}
|
|
634
|
+
function slugify(value) {
|
|
635
|
+
return value
|
|
636
|
+
.toLowerCase()
|
|
637
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
638
|
+
.replace(/^-+|-+$/g, '');
|
|
639
|
+
}
|
|
640
|
+
function audienceFromTags(tags) {
|
|
641
|
+
const tag = tags.find((value) => value.startsWith('audience:'));
|
|
642
|
+
if (!tag)
|
|
643
|
+
return undefined;
|
|
644
|
+
return tag.slice('audience:'.length).replace(/-/g, ' ');
|
|
645
|
+
}
|
|
646
|
+
function appAllowsExecute(app, domain) {
|
|
647
|
+
return (app.policies ?? []).some((policy) => {
|
|
648
|
+
if (policy.enabled === false)
|
|
649
|
+
return false;
|
|
650
|
+
if (policy.domain !== '*' && policy.domain !== domain)
|
|
651
|
+
return false;
|
|
652
|
+
return policy.accessLevel === 'execute' || policy.accessLevel === 'admin';
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
function createDashboardForApp(projectRoot, appId, input) {
|
|
656
|
+
const loaded = loadAppById(projectRoot, appId);
|
|
657
|
+
if (!loaded)
|
|
658
|
+
return { ok: false, error: `App "${appId}" not found` };
|
|
659
|
+
const title = cleanString(input.title) || 'New tab';
|
|
660
|
+
const id = slugify(cleanString(input.id) || title) || `tab-${Date.now()}`;
|
|
661
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(id))
|
|
662
|
+
return { ok: false, error: 'dashboard id must be folder-safe' };
|
|
663
|
+
const appDir = join(projectRoot, 'apps', appId);
|
|
664
|
+
const dashboardPath = join(appDir, 'dashboards', `${id}.dqld`);
|
|
665
|
+
if (existsSync(dashboardPath))
|
|
666
|
+
return { ok: false, error: `Dashboard already exists: ${id}` };
|
|
667
|
+
const dashboard = {
|
|
668
|
+
version: 1,
|
|
669
|
+
id,
|
|
670
|
+
metadata: {
|
|
671
|
+
title,
|
|
672
|
+
description: cleanString(input.description) || `${title} dashboard tab`,
|
|
673
|
+
domain: loaded.app.domain,
|
|
674
|
+
tags: loaded.app.tags ?? [],
|
|
675
|
+
},
|
|
676
|
+
layout: {
|
|
677
|
+
kind: 'grid',
|
|
678
|
+
cols: 12,
|
|
679
|
+
rowHeight: 80,
|
|
680
|
+
items: [],
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
mkdirSync(dirname(dashboardPath), { recursive: true });
|
|
684
|
+
writeFileSync(dashboardPath, JSON.stringify(dashboard, null, 2) + '\n', 'utf-8');
|
|
685
|
+
return { ok: true, dashboard, path: relative(projectRoot, dashboardPath) };
|
|
686
|
+
}
|
|
687
|
+
function patchDashboardLayout(projectRoot, appId, dashboardId, input) {
|
|
688
|
+
const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
|
|
689
|
+
if (!loaded)
|
|
690
|
+
return { ok: false, error: `Dashboard "${dashboardId}" not found in app "${appId}"` };
|
|
691
|
+
const next = {
|
|
692
|
+
...loaded.dashboard,
|
|
693
|
+
layout: input.layout
|
|
694
|
+
? input.layout
|
|
695
|
+
: {
|
|
696
|
+
...loaded.dashboard.layout,
|
|
697
|
+
items: input.items ?? loaded.dashboard.layout.items,
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
const written = writeDashboard(projectRoot, appId, dashboardId, next);
|
|
701
|
+
if (!written.ok)
|
|
702
|
+
return written;
|
|
703
|
+
return { ok: true, dashboard: next, path: relative(projectRoot, written.path) };
|
|
704
|
+
}
|
|
705
|
+
function createAiPinTile(projectRoot, appId, input) {
|
|
706
|
+
const dashboardId = cleanString(input.dashboardId);
|
|
707
|
+
if (!dashboardId)
|
|
708
|
+
return { ok: false, error: 'dashboardId is required' };
|
|
709
|
+
const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
|
|
710
|
+
if (!loaded)
|
|
711
|
+
return { ok: false, error: `Dashboard "${dashboardId}" not found in app "${appId}"` };
|
|
712
|
+
const title = cleanString(input.title) || 'AI result';
|
|
713
|
+
const tileId = cleanString(input.tileId) || nextTileId(loaded.dashboard, slugify(title) || 'ai-pin');
|
|
714
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
715
|
+
try {
|
|
716
|
+
const pin = storage.createAiPin({
|
|
717
|
+
appId,
|
|
718
|
+
dashboardId,
|
|
719
|
+
tileId,
|
|
720
|
+
title,
|
|
721
|
+
answer: cleanString(input.answer) || title,
|
|
722
|
+
sql: cleanString(input.sql) || undefined,
|
|
723
|
+
sourceTier: cleanString(input.sourceTier) || undefined,
|
|
724
|
+
certification: input.certification === 'certified' ? 'certified' : 'ai_generated',
|
|
725
|
+
reviewStatus: input.reviewStatus,
|
|
726
|
+
refreshCadence: input.refreshCadence === 'daily' ? 'daily' : 'none',
|
|
727
|
+
chartConfig: input.chartConfig,
|
|
728
|
+
result: input.result,
|
|
729
|
+
citations: Array.isArray(input.citations) ? input.citations : [],
|
|
730
|
+
});
|
|
731
|
+
const tile = {
|
|
732
|
+
i: tileId,
|
|
733
|
+
...nextTilePosition(loaded.dashboard),
|
|
734
|
+
aiPin: { id: pin.id },
|
|
735
|
+
viz: { type: normalizeVizTypeFromChart(input.chartConfig) },
|
|
736
|
+
title,
|
|
737
|
+
};
|
|
738
|
+
const dashboard = {
|
|
739
|
+
...loaded.dashboard,
|
|
740
|
+
layout: {
|
|
741
|
+
...loaded.dashboard.layout,
|
|
742
|
+
items: [...loaded.dashboard.layout.items, tile],
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
const written = writeDashboard(projectRoot, appId, dashboardId, dashboard);
|
|
746
|
+
if (!written.ok) {
|
|
747
|
+
return { ok: false, error: written.error };
|
|
748
|
+
}
|
|
749
|
+
return { ok: true, pin, dashboard, tile };
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
storage.close();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function promoteAiPinToDraftBlock(projectRoot, appId, pinId) {
|
|
756
|
+
const loaded = loadAppById(projectRoot, appId);
|
|
757
|
+
if (!loaded)
|
|
758
|
+
return { ok: false, error: `App "${appId}" not found` };
|
|
759
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
760
|
+
try {
|
|
761
|
+
const pin = storage.getAiPin(pinId);
|
|
762
|
+
if (!pin)
|
|
763
|
+
return { ok: false, error: `AI pin "${pinId}" not found` };
|
|
764
|
+
if (!pin.sql)
|
|
765
|
+
return { ok: false, error: 'AI pin has no SQL to promote' };
|
|
766
|
+
const blockName = slugify(pin.title) || pin.id;
|
|
767
|
+
const draftDir = join(projectRoot, 'apps', appId, 'drafts');
|
|
768
|
+
const blockPath = join(draftDir, `${blockName}.dql`);
|
|
769
|
+
mkdirSync(draftDir, { recursive: true });
|
|
770
|
+
const source = [
|
|
771
|
+
`block "${blockName}" {`,
|
|
772
|
+
` domain = "${escapeDqlString(loaded.app.domain)}"`,
|
|
773
|
+
' type = "custom"',
|
|
774
|
+
' status = "review"',
|
|
775
|
+
` owner = "${escapeDqlString(loaded.app.owners[0] ?? `${process.env.USER ?? 'analyst'}@local`)}"`,
|
|
776
|
+
` description = "${escapeDqlString(pin.answer.slice(0, 240))}"`,
|
|
777
|
+
' tags = ["ai-generated", "needs-review"]',
|
|
778
|
+
'',
|
|
779
|
+
' query = """',
|
|
780
|
+
pin.sql,
|
|
781
|
+
' """',
|
|
782
|
+
'',
|
|
783
|
+
' visualization {',
|
|
784
|
+
` chart = "${escapeDqlString(String(pin.chartConfig?.chart ?? 'table'))}"`,
|
|
785
|
+
' }',
|
|
786
|
+
'}',
|
|
787
|
+
'',
|
|
788
|
+
].join('\n');
|
|
789
|
+
writeFileSync(blockPath, source, 'utf-8');
|
|
790
|
+
const updated = storage.markAiPinPromoted(pinId, relative(projectRoot, blockPath));
|
|
791
|
+
return { ok: true, pin: updated, blockPath: relative(projectRoot, blockPath) };
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
storage.close();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function nextTilePosition(dashboard) {
|
|
798
|
+
const maxY = dashboard.layout.items.reduce((value, item) => Math.max(value, item.y + item.h), 0);
|
|
799
|
+
return { x: 0, y: maxY, w: 6, h: 3 };
|
|
800
|
+
}
|
|
801
|
+
function nextTileId(dashboard, base) {
|
|
802
|
+
const used = new Set(dashboard.layout.items.map((item) => item.i));
|
|
803
|
+
if (!used.has(base))
|
|
804
|
+
return base;
|
|
805
|
+
for (let i = 2; i < 1000; i++) {
|
|
806
|
+
const candidate = `${base}-${i}`;
|
|
807
|
+
if (!used.has(candidate))
|
|
808
|
+
return candidate;
|
|
809
|
+
}
|
|
810
|
+
return `${base}-${Date.now()}`;
|
|
811
|
+
}
|
|
812
|
+
function normalizeVizTypeFromChart(chartConfig) {
|
|
813
|
+
const chart = String(chartConfig?.chart ?? '').toLowerCase().replace(/-/g, '_');
|
|
814
|
+
if (chart === 'single_value' || chart === 'kpi' || chart === 'line' || chart === 'bar' || chart === 'area'
|
|
815
|
+
|| chart === 'pie' || chart === 'pivot' || chart === 'map' || chart === 'funnel') {
|
|
816
|
+
return chart;
|
|
817
|
+
}
|
|
818
|
+
return 'table';
|
|
819
|
+
}
|
|
820
|
+
function escapeDqlString(value) {
|
|
821
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ');
|
|
822
|
+
}
|
|
823
|
+
function loadAppById(projectRoot, id) {
|
|
824
|
+
for (const p of findAppDocuments(projectRoot)) {
|
|
825
|
+
const { document } = loadAppDocument(p);
|
|
826
|
+
if (!document || document.id !== id)
|
|
827
|
+
continue;
|
|
828
|
+
const appDir = p.slice(0, -'/dql.app.json'.length);
|
|
829
|
+
const dashboards = [];
|
|
830
|
+
for (const d of findDashboardsForApp(appDir)) {
|
|
831
|
+
const { document: dd } = loadDashboardDocument(d);
|
|
832
|
+
if (dd) {
|
|
833
|
+
dashboards.push({
|
|
834
|
+
id: dd.id,
|
|
835
|
+
title: dd.metadata.title,
|
|
836
|
+
description: dd.metadata.description,
|
|
837
|
+
itemCount: dd.layout.items.length,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return { app: document, dashboards };
|
|
842
|
+
}
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
function listDashboardsFor(projectRoot, id) {
|
|
846
|
+
const result = loadAppById(projectRoot, id);
|
|
847
|
+
return result?.dashboards ?? null;
|
|
848
|
+
}
|
|
849
|
+
function loadDashboardForApp(projectRoot, appId, dashboardId) {
|
|
850
|
+
for (const p of findAppDocuments(projectRoot)) {
|
|
851
|
+
const { document } = loadAppDocument(p);
|
|
852
|
+
if (!document || document.id !== appId)
|
|
853
|
+
continue;
|
|
854
|
+
const appDir = p.slice(0, -'/dql.app.json'.length);
|
|
855
|
+
for (const d of findDashboardsForApp(appDir)) {
|
|
856
|
+
const { document: dd } = loadDashboardDocument(d);
|
|
857
|
+
if (dd && dd.id === dashboardId) {
|
|
858
|
+
return { app: document, dashboard: dd };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
function writeDashboard(projectRoot, appId, dashboardId, payload) {
|
|
865
|
+
// Validate against the dashboard schema before touching disk.
|
|
866
|
+
const { document, errors } = parseDashboardDocument(JSON.stringify(payload), '<incoming>');
|
|
867
|
+
if (!document) {
|
|
868
|
+
return { ok: false, error: errors.map((e) => e.message).join('; ') };
|
|
869
|
+
}
|
|
870
|
+
if (document.id !== dashboardId) {
|
|
871
|
+
return { ok: false, error: `dashboard.id (${document.id}) does not match URL :did (${dashboardId})` };
|
|
872
|
+
}
|
|
873
|
+
// Confirm the App exists.
|
|
874
|
+
const appDir = join(projectRoot, 'apps', appId);
|
|
875
|
+
if (!existsSync(join(appDir, 'dql.app.json'))) {
|
|
876
|
+
return { ok: false, error: `App "${appId}" not found at ${appDir}` };
|
|
877
|
+
}
|
|
878
|
+
const dashboardPath = join(appDir, 'dashboards', `${dashboardId}.dqld`);
|
|
879
|
+
mkdirSync(dirname(dashboardPath), { recursive: true });
|
|
880
|
+
writeFileSync(dashboardPath, JSON.stringify(document, null, 2) + '\n', 'utf-8');
|
|
881
|
+
return { ok: true, path: dashboardPath };
|
|
882
|
+
}
|
|
883
|
+
function activatePersona(projectRoot, userId, appId) {
|
|
884
|
+
// If an App id is provided, scope the persona to it.
|
|
885
|
+
if (appId) {
|
|
886
|
+
for (const p of findAppDocuments(projectRoot)) {
|
|
887
|
+
const { document } = loadAppDocument(p);
|
|
888
|
+
if (!document || document.id !== appId)
|
|
889
|
+
continue;
|
|
890
|
+
return defaultPersonaRegistry.setFromApp(document, userId);
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
// Otherwise pick the first App that contains the user.
|
|
895
|
+
for (const p of findAppDocuments(projectRoot)) {
|
|
896
|
+
const { document } = loadAppDocument(p);
|
|
897
|
+
if (!document)
|
|
898
|
+
continue;
|
|
899
|
+
const member = document.members.find((m) => m.userId === userId);
|
|
900
|
+
if (member) {
|
|
901
|
+
const persona = personaFromMember(document, member);
|
|
902
|
+
defaultPersonaRegistry.set(persona);
|
|
903
|
+
return persona;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
// ---- IO utilities ----
|
|
909
|
+
function sendJson(res, status, body) {
|
|
910
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
911
|
+
res.end(JSON.stringify(body));
|
|
912
|
+
}
|
|
913
|
+
async function readJson(req) {
|
|
914
|
+
return new Promise((resolve, reject) => {
|
|
915
|
+
const chunks = [];
|
|
916
|
+
req.on('data', (c) => chunks.push(c));
|
|
917
|
+
req.on('end', () => {
|
|
918
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
919
|
+
if (!raw)
|
|
920
|
+
return resolve({});
|
|
921
|
+
try {
|
|
922
|
+
resolve(JSON.parse(raw));
|
|
923
|
+
}
|
|
924
|
+
catch (err) {
|
|
925
|
+
reject(err);
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
req.on('error', reject);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
// reference unused parseAppDocument/readFileSync to keep import stable for forward use
|
|
932
|
+
void parseAppDocument;
|
|
933
|
+
void readFileSync;
|
|
934
|
+
//# sourceMappingURL=apps-api.js.map
|