@duckcodeailabs/dql-cli 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +123 -0
- package/README.md +72 -0
- package/{apps-api.d.ts → dist/apps-api.d.ts} +27 -3
- package/dist/apps-api.d.ts.map +1 -0
- package/{apps-api.js → dist/apps-api.js} +512 -8
- package/dist/apps-api.js.map +1 -0
- package/{apps-api.test.js → dist/apps-api.test.js} +6 -0
- package/{apps-api.test.js.map → dist/apps-api.test.js.map} +1 -1
- package/{args.test.js → dist/args.test.js} +8 -0
- package/{args.test.js.map → dist/args.test.js.map} +1 -1
- package/dist/assets/dql-notebook/assets/index-DZ2X3-OY.js +862 -0
- package/dist/assets/dql-notebook/assets/index-R3UrqjLQ.css +1 -0
- package/{assets → dist/assets}/dql-notebook/index.html +2 -2
- package/dist/block-studio-import.d.ts +59 -0
- package/dist/block-studio-import.d.ts.map +1 -0
- package/dist/block-studio-import.js +398 -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 +110 -0
- package/dist/block-studio-import.test.js.map +1 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/{commands → dist/commands}/agent.js +98 -5
- package/dist/commands/agent.js.map +1 -0
- package/{commands → dist/commands}/app.d.ts.map +1 -1
- package/{commands → dist/commands}/app.js +6 -0
- package/dist/commands/app.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/{commands → dist/commands}/init.d.ts.map +1 -1
- package/{commands → dist/commands}/init.js +8 -5
- package/dist/commands/init.js.map +1 -0
- package/{commands → dist/commands}/init.test.js +84 -24
- package/dist/commands/init.test.js.map +1 -0
- package/{commands → dist/commands}/migrate.d.ts.map +1 -1
- package/{commands → dist/commands}/migrate.js +5 -0
- package/dist/commands/migrate.js.map +1 -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/{index.js → dist/index.js} +6 -1
- package/dist/index.js.map +1 -0
- package/{llm → dist/llm}/index.d.ts.map +1 -1
- package/{llm → dist/llm}/index.js +4 -3
- package/{llm → dist/llm}/index.js.map +1 -1
- package/{llm → dist/llm}/providers/dql-agent-provider.d.ts +1 -1
- 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/{llm → dist/llm}/types.d.ts +3 -1
- package/dist/llm/types.d.ts.map +1 -0
- package/{local-runtime.d.ts.map → dist/local-runtime.d.ts.map} +1 -1
- package/{local-runtime.js → dist/local-runtime.js} +567 -50
- package/dist/local-runtime.js.map +1 -0
- package/{schedule → dist/schedule}/runner.d.ts.map +1 -1
- package/{schedule → dist/schedule}/runner.js +4 -0
- package/{schedule → dist/schedule}/runner.js.map +1 -1
- 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 +31 -20
- package/apps-api.d.ts.map +0 -1
- package/apps-api.js.map +0 -1
- package/assets/dql-notebook/assets/index-DUTeFz5j.js +0 -858
- package/assets/dql-notebook/assets/index-DrhoZmtv.css +0 -1
- package/commands/agent.d.ts.map +0 -1
- package/commands/agent.js.map +0 -1
- package/commands/app.js.map +0 -1
- package/commands/init.js.map +0 -1
- package/commands/init.test.js.map +0 -1
- package/commands/migrate.js.map +0 -1
- package/commands/validate.d.ts.map +0 -1
- package/commands/validate.js +0 -116
- package/commands/validate.js.map +0 -1
- package/index.js.map +0 -1
- package/llm/providers/dql-agent-provider.d.ts.map +0 -1
- package/llm/providers/dql-agent-provider.js +0 -99
- package/llm/providers/dql-agent-provider.js.map +0 -1
- package/llm/types.d.ts.map +0 -1
- package/local-runtime.js.map +0 -1
- /package/{apps-api.test.d.ts → dist/apps-api.test.d.ts} +0 -0
- /package/{apps-api.test.d.ts.map → dist/apps-api.test.d.ts.map} +0 -0
- /package/{args.d.ts → dist/args.d.ts} +0 -0
- /package/{args.d.ts.map → dist/args.d.ts.map} +0 -0
- /package/{args.js → dist/args.js} +0 -0
- /package/{args.js.map → dist/args.js.map} +0 -0
- /package/{args.test.d.ts → dist/args.test.d.ts} +0 -0
- /package/{args.test.d.ts.map → dist/args.test.d.ts.map} +0 -0
- /package/{assets → dist/assets}/dql-notebook/assets/codemirror-DJYUkPr1.js +0 -0
- /package/{assets → dist/assets}/dql-notebook/assets/react-CRB3T2We.js +0 -0
- /package/{assets → dist/assets}/notebook-browser/app.js +0 -0
- /package/{assets → dist/assets}/notebook-browser/index.html +0 -0
- /package/{assets → dist/assets}/notebook-browser/styles.css +0 -0
- /package/{block-templates.d.ts → dist/block-templates.d.ts} +0 -0
- /package/{block-templates.d.ts.map → dist/block-templates.d.ts.map} +0 -0
- /package/{block-templates.js → dist/block-templates.js} +0 -0
- /package/{block-templates.js.map → dist/block-templates.js.map} +0 -0
- /package/{commands → dist/commands}/agent.d.ts +0 -0
- /package/{commands → dist/commands}/app.d.ts +0 -0
- /package/{commands → dist/commands}/build.d.ts +0 -0
- /package/{commands → dist/commands}/build.d.ts.map +0 -0
- /package/{commands → dist/commands}/build.js +0 -0
- /package/{commands → dist/commands}/build.js.map +0 -0
- /package/{commands → dist/commands}/build.test.d.ts +0 -0
- /package/{commands → dist/commands}/build.test.d.ts.map +0 -0
- /package/{commands → dist/commands}/build.test.js +0 -0
- /package/{commands → dist/commands}/build.test.js.map +0 -0
- /package/{commands → dist/commands}/certify.d.ts +0 -0
- /package/{commands → dist/commands}/certify.d.ts.map +0 -0
- /package/{commands → dist/commands}/certify.js +0 -0
- /package/{commands → dist/commands}/certify.js.map +0 -0
- /package/{commands → dist/commands}/compile.d.ts +0 -0
- /package/{commands → dist/commands}/compile.d.ts.map +0 -0
- /package/{commands → dist/commands}/compile.js +0 -0
- /package/{commands → dist/commands}/compile.js.map +0 -0
- /package/{commands → dist/commands}/compile.test.d.ts +0 -0
- /package/{commands → dist/commands}/compile.test.d.ts.map +0 -0
- /package/{commands → dist/commands}/compile.test.js +0 -0
- /package/{commands → dist/commands}/compile.test.js.map +0 -0
- /package/{commands → dist/commands}/diff.d.ts +0 -0
- /package/{commands → dist/commands}/diff.d.ts.map +0 -0
- /package/{commands → dist/commands}/diff.js +0 -0
- /package/{commands → dist/commands}/diff.js.map +0 -0
- /package/{commands → dist/commands}/doctor.d.ts +0 -0
- /package/{commands → dist/commands}/doctor.d.ts.map +0 -0
- /package/{commands → dist/commands}/doctor.js +0 -0
- /package/{commands → dist/commands}/doctor.js.map +0 -0
- /package/{commands → dist/commands}/doctor.test.d.ts +0 -0
- /package/{commands → dist/commands}/doctor.test.d.ts.map +0 -0
- /package/{commands → dist/commands}/doctor.test.js +0 -0
- /package/{commands → dist/commands}/doctor.test.js.map +0 -0
- /package/{commands → dist/commands}/fmt.d.ts +0 -0
- /package/{commands → dist/commands}/fmt.d.ts.map +0 -0
- /package/{commands → dist/commands}/fmt.js +0 -0
- /package/{commands → dist/commands}/fmt.js.map +0 -0
- /package/{commands → dist/commands}/info.d.ts +0 -0
- /package/{commands → dist/commands}/info.d.ts.map +0 -0
- /package/{commands → dist/commands}/info.js +0 -0
- /package/{commands → dist/commands}/info.js.map +0 -0
- /package/{commands → dist/commands}/init.d.ts +0 -0
- /package/{commands → dist/commands}/init.test.d.ts +0 -0
- /package/{commands → dist/commands}/init.test.d.ts.map +0 -0
- /package/{commands → dist/commands}/lineage.d.ts +0 -0
- /package/{commands → dist/commands}/lineage.d.ts.map +0 -0
- /package/{commands → dist/commands}/lineage.js +0 -0
- /package/{commands → dist/commands}/lineage.js.map +0 -0
- /package/{commands → dist/commands}/mcp.d.ts +0 -0
- /package/{commands → dist/commands}/mcp.d.ts.map +0 -0
- /package/{commands → dist/commands}/mcp.js +0 -0
- /package/{commands → dist/commands}/mcp.js.map +0 -0
- /package/{commands → dist/commands}/migrate.d.ts +0 -0
- /package/{commands → dist/commands}/new.d.ts +0 -0
- /package/{commands → dist/commands}/new.d.ts.map +0 -0
- /package/{commands → dist/commands}/new.js +0 -0
- /package/{commands → dist/commands}/new.js.map +0 -0
- /package/{commands → dist/commands}/new.test.d.ts +0 -0
- /package/{commands → dist/commands}/new.test.d.ts.map +0 -0
- /package/{commands → dist/commands}/new.test.js +0 -0
- /package/{commands → dist/commands}/new.test.js.map +0 -0
- /package/{commands → dist/commands}/notebook.d.ts +0 -0
- /package/{commands → dist/commands}/notebook.d.ts.map +0 -0
- /package/{commands → dist/commands}/notebook.js +0 -0
- /package/{commands → dist/commands}/notebook.js.map +0 -0
- /package/{commands → dist/commands}/parse.d.ts +0 -0
- /package/{commands → dist/commands}/parse.d.ts.map +0 -0
- /package/{commands → dist/commands}/parse.js +0 -0
- /package/{commands → dist/commands}/parse.js.map +0 -0
- /package/{commands → dist/commands}/preview.d.ts +0 -0
- /package/{commands → dist/commands}/preview.d.ts.map +0 -0
- /package/{commands → dist/commands}/preview.js +0 -0
- /package/{commands → dist/commands}/preview.js.map +0 -0
- /package/{commands → dist/commands}/schedule.d.ts +0 -0
- /package/{commands → dist/commands}/schedule.d.ts.map +0 -0
- /package/{commands → dist/commands}/schedule.js +0 -0
- /package/{commands → dist/commands}/schedule.js.map +0 -0
- /package/{commands → dist/commands}/semantic.d.ts +0 -0
- /package/{commands → dist/commands}/semantic.d.ts.map +0 -0
- /package/{commands → dist/commands}/semantic.js +0 -0
- /package/{commands → dist/commands}/semantic.js.map +0 -0
- /package/{commands → dist/commands}/serve.d.ts +0 -0
- /package/{commands → dist/commands}/serve.d.ts.map +0 -0
- /package/{commands → dist/commands}/serve.js +0 -0
- /package/{commands → dist/commands}/serve.js.map +0 -0
- /package/{commands → dist/commands}/slack.d.ts +0 -0
- /package/{commands → dist/commands}/slack.d.ts.map +0 -0
- /package/{commands → dist/commands}/slack.js +0 -0
- /package/{commands → dist/commands}/slack.js.map +0 -0
- /package/{commands → dist/commands}/sync.d.ts +0 -0
- /package/{commands → dist/commands}/sync.d.ts.map +0 -0
- /package/{commands → dist/commands}/sync.js +0 -0
- /package/{commands → dist/commands}/sync.js.map +0 -0
- /package/{commands → dist/commands}/sync.test.d.ts +0 -0
- /package/{commands → dist/commands}/sync.test.d.ts.map +0 -0
- /package/{commands → dist/commands}/sync.test.js +0 -0
- /package/{commands → dist/commands}/sync.test.js.map +0 -0
- /package/{commands → dist/commands}/test.d.ts +0 -0
- /package/{commands → dist/commands}/test.d.ts.map +0 -0
- /package/{commands → dist/commands}/test.js +0 -0
- /package/{commands → dist/commands}/test.js.map +0 -0
- /package/{commands → dist/commands}/validate.d.ts +0 -0
- /package/{commands → dist/commands}/verify.d.ts +0 -0
- /package/{commands → dist/commands}/verify.d.ts.map +0 -0
- /package/{commands → dist/commands}/verify.js +0 -0
- /package/{commands → dist/commands}/verify.js.map +0 -0
- /package/{digest.d.ts → dist/digest.d.ts} +0 -0
- /package/{digest.d.ts.map → dist/digest.d.ts.map} +0 -0
- /package/{digest.js → dist/digest.js} +0 -0
- /package/{digest.js.map → dist/digest.js.map} +0 -0
- /package/{git-service.d.ts → dist/git-service.d.ts} +0 -0
- /package/{git-service.d.ts.map → dist/git-service.d.ts.map} +0 -0
- /package/{git-service.js → dist/git-service.js} +0 -0
- /package/{git-service.js.map → dist/git-service.js.map} +0 -0
- /package/{governance-runtime.d.ts → dist/governance-runtime.d.ts} +0 -0
- /package/{governance-runtime.d.ts.map → dist/governance-runtime.d.ts.map} +0 -0
- /package/{governance-runtime.js → dist/governance-runtime.js} +0 -0
- /package/{governance-runtime.js.map → dist/governance-runtime.js.map} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{index.d.ts.map → dist/index.d.ts.map} +0 -0
- /package/{llm → dist/llm}/index.d.ts +0 -0
- /package/{llm → dist/llm}/providers/claude-agent-sdk.d.ts +0 -0
- /package/{llm → dist/llm}/providers/claude-agent-sdk.d.ts.map +0 -0
- /package/{llm → dist/llm}/providers/claude-agent-sdk.js +0 -0
- /package/{llm → dist/llm}/providers/claude-agent-sdk.js.map +0 -0
- /package/{llm → dist/llm}/providers/claude-code.d.ts +0 -0
- /package/{llm → dist/llm}/providers/claude-code.d.ts.map +0 -0
- /package/{llm → dist/llm}/providers/claude-code.js +0 -0
- /package/{llm → dist/llm}/providers/claude-code.js.map +0 -0
- /package/{llm → dist/llm}/tools.d.ts +0 -0
- /package/{llm → dist/llm}/tools.d.ts.map +0 -0
- /package/{llm → dist/llm}/tools.js +0 -0
- /package/{llm → dist/llm}/tools.js.map +0 -0
- /package/{llm → dist/llm}/types.js +0 -0
- /package/{llm → dist/llm}/types.js.map +0 -0
- /package/{local-runtime.d.ts → dist/local-runtime.d.ts} +0 -0
- /package/{local-runtime.test.d.ts → dist/local-runtime.test.d.ts} +0 -0
- /package/{local-runtime.test.d.ts.map → dist/local-runtime.test.d.ts.map} +0 -0
- /package/{local-runtime.test.js → dist/local-runtime.test.js} +0 -0
- /package/{local-runtime.test.js.map → dist/local-runtime.test.js.map} +0 -0
- /package/{metricflow.d.ts → dist/metricflow.d.ts} +0 -0
- /package/{metricflow.d.ts.map → dist/metricflow.d.ts.map} +0 -0
- /package/{metricflow.js → dist/metricflow.js} +0 -0
- /package/{metricflow.js.map → dist/metricflow.js.map} +0 -0
- /package/{metricflow.test.d.ts → dist/metricflow.test.d.ts} +0 -0
- /package/{metricflow.test.d.ts.map → dist/metricflow.test.d.ts.map} +0 -0
- /package/{metricflow.test.js → dist/metricflow.test.js} +0 -0
- /package/{metricflow.test.js.map → dist/metricflow.test.js.map} +0 -0
- /package/{open-browser.d.ts → dist/open-browser.d.ts} +0 -0
- /package/{open-browser.d.ts.map → dist/open-browser.d.ts.map} +0 -0
- /package/{open-browser.js → dist/open-browser.js} +0 -0
- /package/{open-browser.js.map → dist/open-browser.js.map} +0 -0
- /package/{schedule → dist/schedule}/alerts.d.ts +0 -0
- /package/{schedule → dist/schedule}/alerts.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/alerts.js +0 -0
- /package/{schedule → dist/schedule}/alerts.js.map +0 -0
- /package/{schedule → dist/schedule}/discovery.d.ts +0 -0
- /package/{schedule → dist/schedule}/discovery.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/discovery.js +0 -0
- /package/{schedule → dist/schedule}/discovery.js.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/email.d.ts +0 -0
- /package/{schedule → dist/schedule}/notifiers/email.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/email.js +0 -0
- /package/{schedule → dist/schedule}/notifiers/email.js.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/file.d.ts +0 -0
- /package/{schedule → dist/schedule}/notifiers/file.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/file.js +0 -0
- /package/{schedule → dist/schedule}/notifiers/file.js.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/index.d.ts +0 -0
- /package/{schedule → dist/schedule}/notifiers/index.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/index.js +0 -0
- /package/{schedule → dist/schedule}/notifiers/index.js.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/slack.d.ts +0 -0
- /package/{schedule → dist/schedule}/notifiers/slack.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/notifiers/slack.js +0 -0
- /package/{schedule → dist/schedule}/notifiers/slack.js.map +0 -0
- /package/{schedule → dist/schedule}/runner.d.ts +0 -0
- /package/{schedule → dist/schedule}/runs.d.ts +0 -0
- /package/{schedule → dist/schedule}/runs.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/runs.js +0 -0
- /package/{schedule → dist/schedule}/runs.js.map +0 -0
- /package/{schedule → dist/schedule}/service.d.ts +0 -0
- /package/{schedule → dist/schedule}/service.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/service.js +0 -0
- /package/{schedule → dist/schedule}/service.js.map +0 -0
- /package/{schedule → dist/schedule}/types.d.ts +0 -0
- /package/{schedule → dist/schedule}/types.d.ts.map +0 -0
- /package/{schedule → dist/schedule}/types.js +0 -0
- /package/{schedule → dist/schedule}/types.js.map +0 -0
- /package/{semantic-import.d.ts → dist/semantic-import.d.ts} +0 -0
- /package/{semantic-import.d.ts.map → dist/semantic-import.d.ts.map} +0 -0
- /package/{semantic-import.js → dist/semantic-import.js} +0 -0
- /package/{semantic-import.js.map → dist/semantic-import.js.map} +0 -0
- /package/{semantic-import.test.d.ts → dist/semantic-import.test.d.ts} +0 -0
- /package/{semantic-import.test.d.ts.map → dist/semantic-import.test.d.ts.map} +0 -0
- /package/{semantic-import.test.js → dist/semantic-import.test.js} +0 -0
- /package/{semantic-import.test.js.map → dist/semantic-import.test.js.map} +0 -0
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* dispatcher — returns `true` if the request was handled, `false` otherwise.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { join, dirname } from 'node:path';
|
|
7
|
+
import { join, dirname, relative, basename } from 'node:path';
|
|
8
8
|
import { loadAppDocument, findAppDocuments, loadDashboardDocument, findDashboardsForApp, parseAppDocument, parseDashboardDocument, suggestAppId, } from '@duckcodeailabs/dql-core';
|
|
9
|
-
import { defaultPersonaRegistry, personaFromMember, } from '@duckcodeailabs/dql-project';
|
|
9
|
+
import { defaultPersonaRegistry, defaultLocalAppsDbPath, LocalAppStorage, personaFromMember, } from '@duckcodeailabs/dql-project';
|
|
10
10
|
export async function handleAppsApi(ctx) {
|
|
11
11
|
const { req, res, path, projectRoot } = ctx;
|
|
12
12
|
// ── Apps ────────────────────────────────────────────────────────────────
|
|
@@ -40,8 +40,166 @@ export async function handleAppsApi(ctx) {
|
|
|
40
40
|
}
|
|
41
41
|
return true;
|
|
42
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\/([^/]+)\/notebooks$/);
|
|
87
|
+
if (m && req.method === 'POST') {
|
|
88
|
+
const appId = decodeURIComponent(m[1]);
|
|
89
|
+
try {
|
|
90
|
+
const body = await readJson(req);
|
|
91
|
+
const result = attachNotebookToApp(projectRoot, appId, body);
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
sendJson(res, 400, { error: result.error });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
sendJson(res, 200, loadAppById(projectRoot, appId) ?? result);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
sendJson(res, 500, { error: err.message });
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins$/);
|
|
104
|
+
if (m) {
|
|
105
|
+
const appId = decodeURIComponent(m[1]);
|
|
106
|
+
if (req.method === 'GET') {
|
|
107
|
+
const dashboardId = ctx.url.searchParams.get('dashboardId') ?? undefined;
|
|
108
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
109
|
+
try {
|
|
110
|
+
sendJson(res, 200, { pins: storage.listAiPins(appId, dashboardId) });
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
storage.close();
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (req.method === 'POST') {
|
|
118
|
+
try {
|
|
119
|
+
const body = await readJson(req);
|
|
120
|
+
const created = createAiPinTile(projectRoot, appId, body);
|
|
121
|
+
if (!created.ok) {
|
|
122
|
+
sendJson(res, 400, { error: created.error });
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
sendJson(res, 201, created);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
sendJson(res, 500, { error: err.message });
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins\/([^/]+)\/refresh$/);
|
|
134
|
+
if (m && req.method === 'POST') {
|
|
135
|
+
const pinId = decodeURIComponent(m[2]);
|
|
136
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
137
|
+
try {
|
|
138
|
+
const pin = storage.getAiPin(pinId);
|
|
139
|
+
if (!pin) {
|
|
140
|
+
sendJson(res, 404, { error: `AI pin "${pinId}" not found` });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (!pin.sql) {
|
|
144
|
+
const updated = storage.updateAiPinResult(pinId, pin.result, 'Pin has no SQL to refresh.');
|
|
145
|
+
sendJson(res, 400, { error: 'Pin has no SQL to refresh.', pin: updated });
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (!ctx.executeSql) {
|
|
149
|
+
const updated = storage.updateAiPinResult(pinId, pin.result, 'This host cannot execute AI pin SQL.');
|
|
150
|
+
sendJson(res, 400, { error: 'This host cannot execute AI pin SQL.', pin: updated });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const result = await ctx.executeSql(pin.sql);
|
|
154
|
+
const updated = storage.updateAiPinResult(pinId, result);
|
|
155
|
+
sendJson(res, 200, { ok: true, pin: updated });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const pin = storage.updateAiPinResult(pinId, undefined, err instanceof Error ? err.message : String(err));
|
|
159
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err), pin });
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
storage.close();
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins\/([^/]+)\/promote$/);
|
|
167
|
+
if (m && req.method === 'POST') {
|
|
168
|
+
const appId = decodeURIComponent(m[1]);
|
|
169
|
+
const pinId = decodeURIComponent(m[2]);
|
|
170
|
+
try {
|
|
171
|
+
const result = promoteAiPinToDraftBlock(projectRoot, appId, pinId);
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
sendJson(res, 400, { error: result.error });
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
sendJson(res, 200, result);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
sendJson(res, 500, { error: err.message });
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)\/layout$/);
|
|
184
|
+
if (m && req.method === 'PATCH') {
|
|
185
|
+
const appId = decodeURIComponent(m[1]);
|
|
186
|
+
const dashboardId = decodeURIComponent(m[2]);
|
|
187
|
+
try {
|
|
188
|
+
const body = await readJson(req);
|
|
189
|
+
const result = patchDashboardLayout(projectRoot, appId, dashboardId, body);
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
sendJson(res, 400, { error: result.error });
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
sendJson(res, 200, result);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
sendJson(res, 500, { error: err.message });
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
43
201
|
// /api/apps/:id — single App with dashboards summary
|
|
44
|
-
|
|
202
|
+
m = path.match(/^\/api\/apps\/([^/]+)$/);
|
|
45
203
|
if (m && req.method === 'GET') {
|
|
46
204
|
const id = m[1];
|
|
47
205
|
const result = loadAppById(projectRoot, id);
|
|
@@ -129,7 +287,6 @@ export async function handleAppsApi(ctx) {
|
|
|
129
287
|
}
|
|
130
288
|
return false;
|
|
131
289
|
}
|
|
132
|
-
// ---- Helpers ----
|
|
133
290
|
function collectAppsList(projectRoot) {
|
|
134
291
|
const out = [];
|
|
135
292
|
for (const p of findAppDocuments(projectRoot)) {
|
|
@@ -147,9 +304,15 @@ function collectAppsList(projectRoot) {
|
|
|
147
304
|
id: document.id,
|
|
148
305
|
name: document.name,
|
|
149
306
|
domain: document.domain,
|
|
307
|
+
subdomain: document.subdomain,
|
|
308
|
+
groups: document.groups ?? [],
|
|
150
309
|
description: document.description,
|
|
151
|
-
audience: audienceFromTags(document.tags ?? []),
|
|
152
|
-
|
|
310
|
+
audience: document.audience ?? audienceFromTags(document.tags ?? []),
|
|
311
|
+
lifecycle: document.lifecycle ?? 'draft',
|
|
312
|
+
certification: document.lifecycle === 'certified' ? 'certified' : 'uncertified',
|
|
313
|
+
status: document.lifecycle === 'review' ? 'review' : dashboards.length > 0 ? 'ready' : 'empty',
|
|
314
|
+
storage: document.visibility === 'private' ? 'mine' : document.visibility === 'template' ? 'template' : 'shared',
|
|
315
|
+
visibility: document.visibility ?? 'shared',
|
|
153
316
|
owners: document.owners,
|
|
154
317
|
tags: document.tags ?? [],
|
|
155
318
|
members: document.members.length,
|
|
@@ -157,6 +320,9 @@ function collectAppsList(projectRoot) {
|
|
|
157
320
|
policies: document.policies.length,
|
|
158
321
|
schedules: (document.schedules ?? []).length,
|
|
159
322
|
dashboards,
|
|
323
|
+
notebooks: listAppNotebookRefs(projectRoot, document, appDir),
|
|
324
|
+
drafts: listAppDrafts(projectRoot, appDir),
|
|
325
|
+
aiPins: countAiPins(projectRoot, document.id),
|
|
160
326
|
homepage: document.homepage,
|
|
161
327
|
});
|
|
162
328
|
}
|
|
@@ -224,6 +390,12 @@ export function createAppPackage(projectRoot, input) {
|
|
|
224
390
|
return { ok: false, error: `App already exists: ${id}` };
|
|
225
391
|
const owner = cleanString(input.owners?.[0]) || `${process.env.USER ?? 'owner'}@local`;
|
|
226
392
|
const audience = cleanString(input.audience);
|
|
393
|
+
const subdomain = cleanString(input.subdomain);
|
|
394
|
+
const groups = normalizeTags(input.groups ?? []);
|
|
395
|
+
const visibility = input.visibility === 'private' || input.visibility === 'template' ? input.visibility : 'shared';
|
|
396
|
+
const lifecycle = input.lifecycle === 'certified' || input.lifecycle === 'review' || input.lifecycle === 'deprecated'
|
|
397
|
+
? input.lifecycle
|
|
398
|
+
: 'draft';
|
|
227
399
|
const tags = normalizeTags([...(input.tags ?? []), audience ? `audience:${slugify(audience)}` : '']);
|
|
228
400
|
const selectedIds = Array.from(new Set((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean)));
|
|
229
401
|
const blocks = collectBlockCandidates(projectRoot);
|
|
@@ -235,7 +407,12 @@ export function createAppPackage(projectRoot, input) {
|
|
|
235
407
|
id,
|
|
236
408
|
name,
|
|
237
409
|
description: cleanString(input.purpose) || `${name} consumption surface for ${domain}`,
|
|
410
|
+
visibility,
|
|
238
411
|
domain,
|
|
412
|
+
subdomain: subdomain || undefined,
|
|
413
|
+
groups,
|
|
414
|
+
audience: audience || undefined,
|
|
415
|
+
lifecycle,
|
|
239
416
|
owners: [owner],
|
|
240
417
|
tags,
|
|
241
418
|
members: [
|
|
@@ -283,6 +460,11 @@ export function createAppPackage(projectRoot, input) {
|
|
|
283
460
|
title: `${name} Overview`,
|
|
284
461
|
description: cleanString(input.purpose) || `Starter dashboard for ${name}`,
|
|
285
462
|
domain,
|
|
463
|
+
subdomain: subdomain || undefined,
|
|
464
|
+
groups,
|
|
465
|
+
audience: audience || undefined,
|
|
466
|
+
visibility,
|
|
467
|
+
lifecycle,
|
|
286
468
|
tags,
|
|
287
469
|
},
|
|
288
470
|
layout: {
|
|
@@ -374,10 +556,26 @@ function normalizeVizType(chartType) {
|
|
|
374
556
|
return 'line';
|
|
375
557
|
if (normalized === 'bar')
|
|
376
558
|
return 'bar';
|
|
559
|
+
if (normalized === 'grouped_bar')
|
|
560
|
+
return 'grouped_bar';
|
|
561
|
+
if (normalized === 'stacked_bar')
|
|
562
|
+
return 'stacked_bar';
|
|
377
563
|
if (normalized === 'area')
|
|
378
564
|
return 'area';
|
|
379
565
|
if (normalized === 'pie')
|
|
380
566
|
return 'pie';
|
|
567
|
+
if (normalized === 'donut')
|
|
568
|
+
return 'donut';
|
|
569
|
+
if (normalized === 'scatter')
|
|
570
|
+
return 'scatter';
|
|
571
|
+
if (normalized === 'heatmap')
|
|
572
|
+
return 'heatmap';
|
|
573
|
+
if (normalized === 'histogram')
|
|
574
|
+
return 'histogram';
|
|
575
|
+
if (normalized === 'waterfall')
|
|
576
|
+
return 'waterfall';
|
|
577
|
+
if (normalized === 'gauge')
|
|
578
|
+
return 'gauge';
|
|
381
579
|
if (normalized === 'pivot')
|
|
382
580
|
return 'pivot';
|
|
383
581
|
if (normalized === 'map')
|
|
@@ -393,9 +591,15 @@ function appReadme(app, audience, blocks) {
|
|
|
393
591
|
app.description ?? '',
|
|
394
592
|
'',
|
|
395
593
|
`- Domain: ${app.domain}`,
|
|
594
|
+
...(app.subdomain ? [`- Subdomain: ${app.subdomain}`] : []),
|
|
595
|
+
...(app.groups?.length ? [`- Groups: ${app.groups.join(', ')}`] : []),
|
|
396
596
|
`- Audience: ${audience || 'not specified'}`,
|
|
597
|
+
`- Visibility: ${app.visibility}`,
|
|
598
|
+
`- Lifecycle: ${app.lifecycle}`,
|
|
397
599
|
`- Owners: ${app.owners.join(', ')}`,
|
|
398
600
|
`- Starter dashboard: dashboards/overview.dqld`,
|
|
601
|
+
`- Supporting notebooks: notebooks/`,
|
|
602
|
+
`- Draft blocks: drafts/`,
|
|
399
603
|
'',
|
|
400
604
|
'## Selected Certified Blocks',
|
|
401
605
|
'',
|
|
@@ -486,18 +690,213 @@ function cleanString(value) {
|
|
|
486
690
|
function normalizeTags(values) {
|
|
487
691
|
return Array.from(new Set(values.map((value) => cleanString(value)).filter(Boolean)));
|
|
488
692
|
}
|
|
693
|
+
function unique(values) {
|
|
694
|
+
return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
695
|
+
}
|
|
489
696
|
function slugify(value) {
|
|
490
697
|
return value
|
|
491
698
|
.toLowerCase()
|
|
492
699
|
.replace(/[^a-z0-9]+/g, '-')
|
|
493
700
|
.replace(/^-+|-+$/g, '');
|
|
494
701
|
}
|
|
702
|
+
function titleFromPath(path) {
|
|
703
|
+
return basename(path)
|
|
704
|
+
.replace(/\.(dqlnb|dql)$/i, '')
|
|
705
|
+
.replace(/[_-]+/g, ' ')
|
|
706
|
+
.replace(/\s+/g, ' ')
|
|
707
|
+
.trim()
|
|
708
|
+
.replace(/\b\w/g, (char) => char.toUpperCase()) || path;
|
|
709
|
+
}
|
|
495
710
|
function audienceFromTags(tags) {
|
|
496
711
|
const tag = tags.find((value) => value.startsWith('audience:'));
|
|
497
712
|
if (!tag)
|
|
498
713
|
return undefined;
|
|
499
714
|
return tag.slice('audience:'.length).replace(/-/g, ' ');
|
|
500
715
|
}
|
|
716
|
+
function appAllowsExecute(app, domain) {
|
|
717
|
+
return (app.policies ?? []).some((policy) => {
|
|
718
|
+
if (policy.enabled === false)
|
|
719
|
+
return false;
|
|
720
|
+
if (policy.domain !== '*' && policy.domain !== domain)
|
|
721
|
+
return false;
|
|
722
|
+
return policy.accessLevel === 'execute' || policy.accessLevel === 'admin';
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function createDashboardForApp(projectRoot, appId, input) {
|
|
726
|
+
const loaded = loadAppById(projectRoot, appId);
|
|
727
|
+
if (!loaded)
|
|
728
|
+
return { ok: false, error: `App "${appId}" not found` };
|
|
729
|
+
const title = cleanString(input.title) || 'New tab';
|
|
730
|
+
const id = slugify(cleanString(input.id) || title) || `tab-${Date.now()}`;
|
|
731
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(id))
|
|
732
|
+
return { ok: false, error: 'dashboard id must be folder-safe' };
|
|
733
|
+
const appDir = join(projectRoot, 'apps', appId);
|
|
734
|
+
const dashboardPath = join(appDir, 'dashboards', `${id}.dqld`);
|
|
735
|
+
if (existsSync(dashboardPath))
|
|
736
|
+
return { ok: false, error: `Dashboard already exists: ${id}` };
|
|
737
|
+
const dashboard = {
|
|
738
|
+
version: 1,
|
|
739
|
+
id,
|
|
740
|
+
metadata: {
|
|
741
|
+
title,
|
|
742
|
+
description: cleanString(input.description) || `${title} dashboard tab`,
|
|
743
|
+
domain: loaded.app.domain,
|
|
744
|
+
subdomain: loaded.app.subdomain,
|
|
745
|
+
groups: loaded.app.groups ?? [],
|
|
746
|
+
audience: loaded.app.audience,
|
|
747
|
+
visibility: loaded.app.visibility ?? 'shared',
|
|
748
|
+
lifecycle: loaded.app.lifecycle ?? 'draft',
|
|
749
|
+
tags: loaded.app.tags ?? [],
|
|
750
|
+
},
|
|
751
|
+
layout: {
|
|
752
|
+
kind: 'grid',
|
|
753
|
+
cols: 12,
|
|
754
|
+
rowHeight: 80,
|
|
755
|
+
items: [],
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
mkdirSync(dirname(dashboardPath), { recursive: true });
|
|
759
|
+
writeFileSync(dashboardPath, JSON.stringify(dashboard, null, 2) + '\n', 'utf-8');
|
|
760
|
+
return { ok: true, dashboard, path: relative(projectRoot, dashboardPath) };
|
|
761
|
+
}
|
|
762
|
+
function patchDashboardLayout(projectRoot, appId, dashboardId, input) {
|
|
763
|
+
const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
|
|
764
|
+
if (!loaded)
|
|
765
|
+
return { ok: false, error: `Dashboard "${dashboardId}" not found in app "${appId}"` };
|
|
766
|
+
const next = {
|
|
767
|
+
...loaded.dashboard,
|
|
768
|
+
layout: input.layout
|
|
769
|
+
? input.layout
|
|
770
|
+
: {
|
|
771
|
+
...loaded.dashboard.layout,
|
|
772
|
+
items: input.items ?? loaded.dashboard.layout.items,
|
|
773
|
+
},
|
|
774
|
+
};
|
|
775
|
+
const written = writeDashboard(projectRoot, appId, dashboardId, next);
|
|
776
|
+
if (!written.ok)
|
|
777
|
+
return written;
|
|
778
|
+
return { ok: true, dashboard: next, path: relative(projectRoot, written.path) };
|
|
779
|
+
}
|
|
780
|
+
function createAiPinTile(projectRoot, appId, input) {
|
|
781
|
+
const dashboardId = cleanString(input.dashboardId);
|
|
782
|
+
if (!dashboardId)
|
|
783
|
+
return { ok: false, error: 'dashboardId is required' };
|
|
784
|
+
const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
|
|
785
|
+
if (!loaded)
|
|
786
|
+
return { ok: false, error: `Dashboard "${dashboardId}" not found in app "${appId}"` };
|
|
787
|
+
const title = cleanString(input.title) || 'AI result';
|
|
788
|
+
const tileId = cleanString(input.tileId) || nextTileId(loaded.dashboard, slugify(title) || 'ai-pin');
|
|
789
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
790
|
+
try {
|
|
791
|
+
const pin = storage.createAiPin({
|
|
792
|
+
appId,
|
|
793
|
+
dashboardId,
|
|
794
|
+
tileId,
|
|
795
|
+
title,
|
|
796
|
+
answer: cleanString(input.answer) || title,
|
|
797
|
+
sql: cleanString(input.sql) || undefined,
|
|
798
|
+
sourceTier: cleanString(input.sourceTier) || undefined,
|
|
799
|
+
certification: input.certification === 'certified' ? 'certified' : 'ai_generated',
|
|
800
|
+
reviewStatus: input.reviewStatus,
|
|
801
|
+
refreshCadence: input.refreshCadence === 'daily' ? 'daily' : 'none',
|
|
802
|
+
chartConfig: input.chartConfig,
|
|
803
|
+
result: input.result,
|
|
804
|
+
citations: Array.isArray(input.citations) ? input.citations : [],
|
|
805
|
+
});
|
|
806
|
+
const tile = {
|
|
807
|
+
i: tileId,
|
|
808
|
+
...nextTilePosition(loaded.dashboard),
|
|
809
|
+
aiPin: { id: pin.id },
|
|
810
|
+
viz: { type: normalizeVizTypeFromChart(input.chartConfig) },
|
|
811
|
+
title,
|
|
812
|
+
};
|
|
813
|
+
const dashboard = {
|
|
814
|
+
...loaded.dashboard,
|
|
815
|
+
layout: {
|
|
816
|
+
...loaded.dashboard.layout,
|
|
817
|
+
items: [...loaded.dashboard.layout.items, tile],
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
const written = writeDashboard(projectRoot, appId, dashboardId, dashboard);
|
|
821
|
+
if (!written.ok) {
|
|
822
|
+
return { ok: false, error: written.error };
|
|
823
|
+
}
|
|
824
|
+
return { ok: true, pin, dashboard, tile };
|
|
825
|
+
}
|
|
826
|
+
finally {
|
|
827
|
+
storage.close();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function promoteAiPinToDraftBlock(projectRoot, appId, pinId) {
|
|
831
|
+
const loaded = loadAppById(projectRoot, appId);
|
|
832
|
+
if (!loaded)
|
|
833
|
+
return { ok: false, error: `App "${appId}" not found` };
|
|
834
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
835
|
+
try {
|
|
836
|
+
const pin = storage.getAiPin(pinId);
|
|
837
|
+
if (!pin)
|
|
838
|
+
return { ok: false, error: `AI pin "${pinId}" not found` };
|
|
839
|
+
if (!pin.sql)
|
|
840
|
+
return { ok: false, error: 'AI pin has no SQL to promote' };
|
|
841
|
+
const blockName = slugify(pin.title) || pin.id;
|
|
842
|
+
const draftDir = join(projectRoot, 'apps', appId, 'drafts');
|
|
843
|
+
const blockPath = join(draftDir, `${blockName}.dql`);
|
|
844
|
+
mkdirSync(draftDir, { recursive: true });
|
|
845
|
+
const source = [
|
|
846
|
+
`block "${blockName}" {`,
|
|
847
|
+
` domain = "${escapeDqlString(loaded.app.domain)}"`,
|
|
848
|
+
' type = "custom"',
|
|
849
|
+
' status = "review"',
|
|
850
|
+
` owner = "${escapeDqlString(loaded.app.owners[0] ?? `${process.env.USER ?? 'analyst'}@local`)}"`,
|
|
851
|
+
` description = "${escapeDqlString(pin.answer.slice(0, 240))}"`,
|
|
852
|
+
' tags = ["ai-generated", "needs-review"]',
|
|
853
|
+
'',
|
|
854
|
+
' query = """',
|
|
855
|
+
pin.sql,
|
|
856
|
+
' """',
|
|
857
|
+
'',
|
|
858
|
+
' visualization {',
|
|
859
|
+
` chart = "${escapeDqlString(String(pin.chartConfig?.chart ?? 'table'))}"`,
|
|
860
|
+
' }',
|
|
861
|
+
'}',
|
|
862
|
+
'',
|
|
863
|
+
].join('\n');
|
|
864
|
+
writeFileSync(blockPath, source, 'utf-8');
|
|
865
|
+
const updated = storage.markAiPinPromoted(pinId, relative(projectRoot, blockPath));
|
|
866
|
+
return { ok: true, pin: updated, blockPath: relative(projectRoot, blockPath) };
|
|
867
|
+
}
|
|
868
|
+
finally {
|
|
869
|
+
storage.close();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function nextTilePosition(dashboard) {
|
|
873
|
+
const maxY = dashboard.layout.items.reduce((value, item) => Math.max(value, item.y + item.h), 0);
|
|
874
|
+
return { x: 0, y: maxY, w: 6, h: 3 };
|
|
875
|
+
}
|
|
876
|
+
function nextTileId(dashboard, base) {
|
|
877
|
+
const used = new Set(dashboard.layout.items.map((item) => item.i));
|
|
878
|
+
if (!used.has(base))
|
|
879
|
+
return base;
|
|
880
|
+
for (let i = 2; i < 1000; i++) {
|
|
881
|
+
const candidate = `${base}-${i}`;
|
|
882
|
+
if (!used.has(candidate))
|
|
883
|
+
return candidate;
|
|
884
|
+
}
|
|
885
|
+
return `${base}-${Date.now()}`;
|
|
886
|
+
}
|
|
887
|
+
function normalizeVizTypeFromChart(chartConfig) {
|
|
888
|
+
const chart = String(chartConfig?.chart ?? '').toLowerCase().replace(/-/g, '_');
|
|
889
|
+
if (chart === 'single_value' || chart === 'kpi' || chart === 'line' || chart === 'bar' || chart === 'area'
|
|
890
|
+
|| chart === 'grouped_bar' || chart === 'stacked_bar' || chart === 'pie' || chart === 'donut'
|
|
891
|
+
|| chart === 'scatter' || chart === 'heatmap' || chart === 'histogram' || chart === 'waterfall'
|
|
892
|
+
|| chart === 'gauge' || chart === 'pivot' || chart === 'map' || chart === 'funnel') {
|
|
893
|
+
return chart;
|
|
894
|
+
}
|
|
895
|
+
return 'table';
|
|
896
|
+
}
|
|
897
|
+
function escapeDqlString(value) {
|
|
898
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ');
|
|
899
|
+
}
|
|
501
900
|
function loadAppById(projectRoot, id) {
|
|
502
901
|
for (const p of findAppDocuments(projectRoot)) {
|
|
503
902
|
const { document } = loadAppDocument(p);
|
|
@@ -516,10 +915,115 @@ function loadAppById(projectRoot, id) {
|
|
|
516
915
|
});
|
|
517
916
|
}
|
|
518
917
|
}
|
|
519
|
-
return {
|
|
918
|
+
return {
|
|
919
|
+
app: document,
|
|
920
|
+
dashboards,
|
|
921
|
+
notebooks: listAppNotebookRefs(projectRoot, document, appDir),
|
|
922
|
+
drafts: listAppDrafts(projectRoot, appDir),
|
|
923
|
+
aiPins: listAiPins(projectRoot, document.id),
|
|
924
|
+
};
|
|
520
925
|
}
|
|
521
926
|
return null;
|
|
522
927
|
}
|
|
928
|
+
function attachNotebookToApp(projectRoot, appId, input) {
|
|
929
|
+
const notebookPath = cleanString(input.path).replaceAll('\\', '/');
|
|
930
|
+
if (!notebookPath)
|
|
931
|
+
return { ok: false, error: 'path is required' };
|
|
932
|
+
if (notebookPath.startsWith('/') || notebookPath.includes('..')) {
|
|
933
|
+
return { ok: false, error: 'notebook path must be project-relative' };
|
|
934
|
+
}
|
|
935
|
+
if (!existsSync(join(projectRoot, notebookPath))) {
|
|
936
|
+
return { ok: false, error: `Notebook not found: ${notebookPath}` };
|
|
937
|
+
}
|
|
938
|
+
for (const appJsonPath of findAppDocuments(projectRoot)) {
|
|
939
|
+
const { document } = loadAppDocument(appJsonPath);
|
|
940
|
+
if (!document || document.id !== appId)
|
|
941
|
+
continue;
|
|
942
|
+
const role = input.role === 'source' || input.role === 'analysis' ? input.role : 'supporting';
|
|
943
|
+
const visibility = input.visibility === 'private' || input.visibility === 'template' ? input.visibility : 'shared';
|
|
944
|
+
const next = {
|
|
945
|
+
...document,
|
|
946
|
+
notebooks: [
|
|
947
|
+
...(document.notebooks ?? []).filter((notebook) => notebook.path !== notebookPath),
|
|
948
|
+
{
|
|
949
|
+
path: notebookPath,
|
|
950
|
+
title: cleanString(input.title) || titleFromPath(notebookPath),
|
|
951
|
+
role,
|
|
952
|
+
visibility,
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
};
|
|
956
|
+
const { document: validated, errors } = parseAppDocument(JSON.stringify(next), appJsonPath);
|
|
957
|
+
if (!validated)
|
|
958
|
+
return { ok: false, error: errors.map((e) => e.message).join('; ') };
|
|
959
|
+
writeFileSync(appJsonPath, JSON.stringify(validated, null, 2) + '\n', 'utf-8');
|
|
960
|
+
return { ok: true, path: relative(projectRoot, appJsonPath) };
|
|
961
|
+
}
|
|
962
|
+
return { ok: false, error: `App "${appId}" not found` };
|
|
963
|
+
}
|
|
964
|
+
function listAppNotebookRefs(projectRoot, app, appDir) {
|
|
965
|
+
const byPath = new Map();
|
|
966
|
+
for (const notebook of app.notebooks ?? []) {
|
|
967
|
+
byPath.set(notebook.path, {
|
|
968
|
+
path: notebook.path,
|
|
969
|
+
title: notebook.title,
|
|
970
|
+
role: notebook.role,
|
|
971
|
+
visibility: notebook.visibility ?? 'shared',
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
const notebooksDir = join(appDir, 'notebooks');
|
|
975
|
+
for (const file of scanFiles(notebooksDir, '.dqlnb')) {
|
|
976
|
+
const rel = relative(projectRoot, file).replaceAll('\\', '/');
|
|
977
|
+
if (byPath.has(rel))
|
|
978
|
+
continue;
|
|
979
|
+
byPath.set(rel, {
|
|
980
|
+
path: rel,
|
|
981
|
+
title: titleFromPath(rel),
|
|
982
|
+
role: 'supporting',
|
|
983
|
+
visibility: app.visibility ?? 'shared',
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
return Array.from(byPath.values()).sort((a, b) => (a.title ?? a.path).localeCompare(b.title ?? b.path));
|
|
987
|
+
}
|
|
988
|
+
function listAppDrafts(projectRoot, appDir) {
|
|
989
|
+
return scanFiles(join(appDir, 'drafts'), '.dql').map((file) => {
|
|
990
|
+
const source = readFileSync(file, 'utf-8');
|
|
991
|
+
const path = relative(projectRoot, file).replaceAll('\\', '/');
|
|
992
|
+
return {
|
|
993
|
+
path,
|
|
994
|
+
name: matchString(source, /block\s+"([^"]+)"/) ?? titleFromPath(path),
|
|
995
|
+
reviewStatus: matchString(source, /status\s*=\s*"([^"]+)"/) ?? 'review',
|
|
996
|
+
};
|
|
997
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
998
|
+
}
|
|
999
|
+
function scanFiles(root, extension) {
|
|
1000
|
+
if (!existsSync(root))
|
|
1001
|
+
return [];
|
|
1002
|
+
const out = [];
|
|
1003
|
+
for (const entry of readdirSyncSafe(root)) {
|
|
1004
|
+
const full = join(root, entry.name);
|
|
1005
|
+
if (entry.isDirectory())
|
|
1006
|
+
out.push(...scanFiles(full, extension));
|
|
1007
|
+
else if (entry.isFile() && entry.name.endsWith(extension))
|
|
1008
|
+
out.push(full);
|
|
1009
|
+
}
|
|
1010
|
+
return out.sort();
|
|
1011
|
+
}
|
|
1012
|
+
function countAiPins(projectRoot, appId) {
|
|
1013
|
+
return listAiPins(projectRoot, appId).length;
|
|
1014
|
+
}
|
|
1015
|
+
function listAiPins(projectRoot, appId) {
|
|
1016
|
+
const dbPath = defaultLocalAppsDbPath(projectRoot);
|
|
1017
|
+
if (!existsSync(dbPath))
|
|
1018
|
+
return [];
|
|
1019
|
+
const storage = new LocalAppStorage(dbPath);
|
|
1020
|
+
try {
|
|
1021
|
+
return storage.listAiPins(appId);
|
|
1022
|
+
}
|
|
1023
|
+
finally {
|
|
1024
|
+
storage.close();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
523
1027
|
function listDashboardsFor(projectRoot, id) {
|
|
524
1028
|
const result = loadAppById(projectRoot, id);
|
|
525
1029
|
return result?.dashboards ?? null;
|
|
@@ -539,7 +1043,7 @@ function loadDashboardForApp(projectRoot, appId, dashboardId) {
|
|
|
539
1043
|
}
|
|
540
1044
|
return null;
|
|
541
1045
|
}
|
|
542
|
-
|
|
1046
|
+
function writeDashboard(projectRoot, appId, dashboardId, payload) {
|
|
543
1047
|
// Validate against the dashboard schema before touching disk.
|
|
544
1048
|
const { document, errors } = parseDashboardDocument(JSON.stringify(payload), '<incoming>');
|
|
545
1049
|
if (!document) {
|