@graphit/cli 0.2.253 → 0.2.260
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/bin/graphit +1 -1
- package/bin/graphit.ps1 +1 -1
- package/dist/commands/dashboard.d.ts +14 -0
- package/dist/commands/dashboard.js +52 -0
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/ds/api.d.ts +19 -0
- package/dist/commands/ds/api.js +74 -0
- package/dist/commands/ds/api.js.map +1 -0
- package/dist/commands/ds/polling.d.ts +13 -0
- package/dist/commands/ds/polling.js +189 -0
- package/dist/commands/ds/polling.js.map +1 -0
- package/dist/commands/ds/render.d.ts +15 -0
- package/dist/commands/ds/render.js +88 -0
- package/dist/commands/ds/render.js.map +1 -0
- package/dist/commands/ds/types.d.ts +65 -0
- package/dist/commands/ds/types.js +31 -0
- package/dist/commands/ds/types.js.map +1 -0
- package/dist/commands/ds/ui-only.d.ts +15 -0
- package/dist/commands/ds/ui-only.js +29 -0
- package/dist/commands/ds/ui-only.js.map +1 -0
- package/dist/commands/ds.js +7 -352
- package/dist/commands/ds.js.map +1 -1
- package/package.json +1 -1
- package/scripts/sync-plugin-marketplace.sh +134 -0
- package/skills/graphit/SKILL.md +3 -3
- package/skills/graphit/VERSION.json +1 -1
- package/skills/graphit/references/filters.md +43 -47
- package/skills/graphit/references/runtime.md +1 -0
- package/skills/graphit/references/state-contract.md +54 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal rendering for the `ds` commands - status table, verify summary, advisories.
|
|
3
|
+
*
|
|
4
|
+
* Project #272 Phase 0: extracted from `commands/ds.ts` (which crossed 800
|
|
5
|
+
* lines) into the existing `commands/ds/` subfolder, joining `refresh-history.ts`.
|
|
6
|
+
* Mechanical split - no behavior change. `registerDSCommands` stays whole in
|
|
7
|
+
* `ds.ts` so no Commander metadata moves and the generated command table in
|
|
8
|
+
* SKILL.md cannot drift.
|
|
9
|
+
*/
|
|
10
|
+
import { getFrontendUrl } from "../../config.js";
|
|
11
|
+
import { note } from "../../output/format.js";
|
|
12
|
+
import { NOT_FOUND_STATUS } from "../ds-poll.js";
|
|
13
|
+
// Project #244: print soft, non-blocking shape advisories. Print-and-proceed -
|
|
14
|
+
// these never gate a create or verify.
|
|
15
|
+
export function printShapeWarnings(validation) {
|
|
16
|
+
if (!validation?.warnings?.length)
|
|
17
|
+
return;
|
|
18
|
+
note("");
|
|
19
|
+
for (const w of validation.warnings) {
|
|
20
|
+
note(` ! ${w}`);
|
|
21
|
+
}
|
|
22
|
+
note("");
|
|
23
|
+
}
|
|
24
|
+
export function formatRowCount(n) {
|
|
25
|
+
if (n >= 1_000_000)
|
|
26
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
27
|
+
if (n >= 1_000)
|
|
28
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
29
|
+
return String(n);
|
|
30
|
+
}
|
|
31
|
+
export function printStatusTable(targets, statuses, errors, clearPrevious) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
const nameWidth = Math.max(4, ...Array.from(targets.values()).map((ds) => ds.name.length));
|
|
34
|
+
lines.push(` ${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(12)} ${"ROWS".padStart(8)}`);
|
|
35
|
+
lines.push(` ${"─".repeat(nameWidth)} ${"─".repeat(12)} ${"─".repeat(8)}`);
|
|
36
|
+
for (const [id, ds] of targets) {
|
|
37
|
+
const status = statuses.get(id) ?? ds.status;
|
|
38
|
+
const error = errors.get(id);
|
|
39
|
+
const icon = status === "ready" ? "+" : status === "error" ? "x" : status === NOT_FOUND_STATUS ? "?" : "*";
|
|
40
|
+
const rowStr = formatRowCount(ds.row_count).padStart(8);
|
|
41
|
+
let line = ` ${ds.name.padEnd(nameWidth)} ${icon} ${status.padEnd(10)} ${rowStr}`;
|
|
42
|
+
if (error)
|
|
43
|
+
line += ` ${error}`;
|
|
44
|
+
lines.push(line);
|
|
45
|
+
}
|
|
46
|
+
// Progress table is decoration -> stderr (with its in-place cursor escapes on
|
|
47
|
+
// the same stream), so stdout stays a clean payload.
|
|
48
|
+
if (clearPrevious) {
|
|
49
|
+
process.stderr.write(`\x1B[${lines.length + 1}A\x1B[J`);
|
|
50
|
+
}
|
|
51
|
+
note(lines.join("\n"));
|
|
52
|
+
return lines.length;
|
|
53
|
+
}
|
|
54
|
+
export function printVerificationCTA(dsId) {
|
|
55
|
+
const url = `${getFrontendUrl()}/sources?tab=sources&sub=data&id=${dsId}`;
|
|
56
|
+
note(`\n Scanned and awaiting verification. Review and activate in the platform:`);
|
|
57
|
+
note(` ${url}`);
|
|
58
|
+
note(`\n Or accept the AI schema as-is from the CLI:`);
|
|
59
|
+
note(` graphit ds verify ${dsId} --accept-schema\n`);
|
|
60
|
+
}
|
|
61
|
+
export function printVerifySummary(scan, dsId, rowCount) {
|
|
62
|
+
const line = "─".repeat(Math.max(scan.table_name.length, 30));
|
|
63
|
+
note(`\n ${scan.table_name}`);
|
|
64
|
+
note(` ${line}`);
|
|
65
|
+
const colSummary = scan.columns
|
|
66
|
+
.slice(0, 6)
|
|
67
|
+
.map((c) => `${c.name} (${c.type})`)
|
|
68
|
+
.join(", ");
|
|
69
|
+
const more = scan.columns.length > 6 ? `, +${scan.columns.length - 6} more` : "";
|
|
70
|
+
note(` Columns: ${colSummary}${more}`);
|
|
71
|
+
if (rowCount !== undefined) {
|
|
72
|
+
note(` Rows: ${formatRowCount(rowCount)}`);
|
|
73
|
+
}
|
|
74
|
+
// A file-upload verify (or an --accept-schema warehouse verify) activates the
|
|
75
|
+
// source server-side, so there is nothing left to do - say so (Issue #544/#546).
|
|
76
|
+
// Confirmed against the state the server reports back, not against the request
|
|
77
|
+
// we made: a scan that demoted the source must fall through to the CTA below.
|
|
78
|
+
if (scan.activated && scan.status !== "pending_verification") {
|
|
79
|
+
note(`\n Verified - this data source is now queryable.\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Scanned but awaiting review (warehouse/SQL): the AI-inferred schema needs a
|
|
83
|
+
// human to accept it. Offer both governed paths - review in the platform wizard,
|
|
84
|
+
// or accept the schema as-is from the CLI (Issue #546). This is not "still
|
|
85
|
+
// materializing" - the data is on disk; only verification is pending.
|
|
86
|
+
printVerificationCTA(dsId);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/commands/ds/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and timeout budgets for the `ds` command family.
|
|
3
|
+
*
|
|
4
|
+
* Project #272 Phase 0: extracted from `commands/ds.ts` (which crossed 800
|
|
5
|
+
* lines) into the existing `commands/ds/` subfolder, joining `refresh-history.ts`.
|
|
6
|
+
* Mechanical split - no behavior change. `registerDSCommands` stays whole in
|
|
7
|
+
* `ds.ts` so no Commander metadata moves and the generated command table in
|
|
8
|
+
* SKILL.md cannot drift.
|
|
9
|
+
*/
|
|
10
|
+
export interface DSInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
status: string;
|
|
14
|
+
row_count: number;
|
|
15
|
+
source_sql: string;
|
|
16
|
+
verified: boolean;
|
|
17
|
+
connector_type: string;
|
|
18
|
+
last_refreshed: string;
|
|
19
|
+
failure_reason?: string | null;
|
|
20
|
+
last_error?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface DSListResponse {
|
|
23
|
+
data_sources: DSInfo[];
|
|
24
|
+
}
|
|
25
|
+
export interface ShapeValidation {
|
|
26
|
+
status: string;
|
|
27
|
+
warnings?: string[];
|
|
28
|
+
display_instruction?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ScanResult {
|
|
31
|
+
data_source_id: string;
|
|
32
|
+
kb_table_id: string;
|
|
33
|
+
table_name: string;
|
|
34
|
+
column_count: number;
|
|
35
|
+
columns: {
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
description: string;
|
|
39
|
+
}[];
|
|
40
|
+
already_scanned: boolean;
|
|
41
|
+
activated?: boolean;
|
|
42
|
+
validation?: ShapeValidation;
|
|
43
|
+
status?: string;
|
|
44
|
+
verified?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare const POLL_INTERVAL_MS = 3000;
|
|
47
|
+
export declare const MAX_POLL_DURATION_MS: number;
|
|
48
|
+
export declare const MAX_CREATE_POLL_DURATION_MS: number;
|
|
49
|
+
export declare const SCAN_TIMEOUT_MS = 90000;
|
|
50
|
+
export declare const REFRESH_TRIGGER_TIMEOUT_MS = 60000;
|
|
51
|
+
export declare const CREATE_TIMEOUT_MS = 120000;
|
|
52
|
+
export interface FileUploadResult {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
status: string;
|
|
56
|
+
row_count: number;
|
|
57
|
+
kb_table_id: string | null;
|
|
58
|
+
domain: string | null;
|
|
59
|
+
domain_note?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create a data source from a local Excel/CSV file (Feature #645). Uploads the
|
|
63
|
+
* file as multipart; the server parses, runs the inline AI scan, encrypts, and
|
|
64
|
+
* (optionally) attaches the new table to a domain - all in one request.
|
|
65
|
+
*/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and timeout budgets for the `ds` command family.
|
|
3
|
+
*
|
|
4
|
+
* Project #272 Phase 0: extracted from `commands/ds.ts` (which crossed 800
|
|
5
|
+
* lines) into the existing `commands/ds/` subfolder, joining `refresh-history.ts`.
|
|
6
|
+
* Mechanical split - no behavior change. `registerDSCommands` stays whole in
|
|
7
|
+
* `ds.ts` so no Commander metadata moves and the generated command table in
|
|
8
|
+
* SKILL.md cannot drift.
|
|
9
|
+
*/
|
|
10
|
+
export const POLL_INTERVAL_MS = 3_000;
|
|
11
|
+
export const MAX_POLL_DURATION_MS = 5 * 60_000;
|
|
12
|
+
// Feature #661 + #665: creation runs in a durable Job whose export budget was raised
|
|
13
|
+
// to 60 min (Feature #665), with the worker socket timeout at ~70 min (budget + margin).
|
|
14
|
+
// The create poller must outlast the server-side budget so a legit large load isn't
|
|
15
|
+
// reported as "timed out" client-side while it's still materializing.
|
|
16
|
+
export const MAX_CREATE_POLL_DURATION_MS = 70 * 60_000;
|
|
17
|
+
export const SCAN_TIMEOUT_MS = 90_000;
|
|
18
|
+
export const REFRESH_TRIGGER_TIMEOUT_MS = 60_000;
|
|
19
|
+
// Issue #775: a small/single-table create routes INLINE (Project #247) - the POST
|
|
20
|
+
// itself runs the warehouse export + scan + canonical birth in-request and returns
|
|
21
|
+
// a terminal status, so it needs the same extended budget as the other
|
|
22
|
+
// warehouse-touching POSTs (upload 120s, scan 90s) instead of the 30s default,
|
|
23
|
+
// which a cold-connection inline export races even though the source is born fine.
|
|
24
|
+
// A Job-routed create returns CREATING immediately and never approaches this ceiling.
|
|
25
|
+
export const CREATE_TIMEOUT_MS = 120_000;
|
|
26
|
+
/**
|
|
27
|
+
* Create a data source from a local Excel/CSV file (Feature #645). Uploads the
|
|
28
|
+
* file as multipart; the server parses, runs the inline AI scan, encrypts, and
|
|
29
|
+
* (optionally) attaches the new table to a domain - all in one request.
|
|
30
|
+
*/
|
|
31
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/commands/ds/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4CH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAG,MAAM,CAAC;AAC/C,qFAAqF;AACrF,yFAAyF;AACzF,oFAAoF;AACpF,sEAAsE;AACtE,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,GAAG,MAAM,CAAC;AACvD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AACjD,kFAAkF;AAClF,mFAAmF;AACnF,uEAAuE;AACvE,+EAA+E;AAC/E,mFAAmF;AACnF,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC;AAYzC;;;;GAIG"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ds` actions that exist only in the web UI, registered so the CLI names them.
|
|
3
|
+
*
|
|
4
|
+
* Project #272 Phase 0: extracted from `commands/ds.ts` (which crossed 800
|
|
5
|
+
* lines) into the existing `commands/ds/` subfolder, joining `refresh-history.ts`.
|
|
6
|
+
* Mechanical split - no behavior change. `registerDSCommands` stays whole in
|
|
7
|
+
* `ds.ts` so no Commander metadata moves and the generated command table in
|
|
8
|
+
* SKILL.md cannot drift.
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
export declare const UI_ONLY_DS_ACTIONS: {
|
|
12
|
+
name: string;
|
|
13
|
+
summary: string;
|
|
14
|
+
}[];
|
|
15
|
+
export declare function registerUiOnlyDSActions(ds: Command): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ds` actions that exist only in the web UI, registered so the CLI names them.
|
|
3
|
+
*
|
|
4
|
+
* Project #272 Phase 0: extracted from `commands/ds.ts` (which crossed 800
|
|
5
|
+
* lines) into the existing `commands/ds/` subfolder, joining `refresh-history.ts`.
|
|
6
|
+
* Mechanical split - no behavior change. `registerDSCommands` stays whole in
|
|
7
|
+
* `ds.ts` so no Commander metadata moves and the generated command table in
|
|
8
|
+
* SKILL.md cannot drift.
|
|
9
|
+
*/
|
|
10
|
+
import { getFrontendUrl } from "../../config.js";
|
|
11
|
+
import { errorOutput } from "../../output/format.js";
|
|
12
|
+
export const UI_ONLY_DS_ACTIONS = [
|
|
13
|
+
{ name: "delete <id>", summary: "Delete a data source - not available on the CLI, use the Sources Hub" },
|
|
14
|
+
{ name: "move <id>", summary: "Move a data source between domains - not available on the CLI, use the Sources Hub" },
|
|
15
|
+
];
|
|
16
|
+
export function registerUiOnlyDSActions(ds) {
|
|
17
|
+
for (const action of UI_ONLY_DS_ACTIONS) {
|
|
18
|
+
const verb = action.name.split(" ")[0];
|
|
19
|
+
ds.command(action.name)
|
|
20
|
+
.description(action.summary)
|
|
21
|
+
.action(function () {
|
|
22
|
+
const url = `${getFrontendUrl()}/sources?tab=sources&sub=data`;
|
|
23
|
+
errorOutput(new Error(`\`ds ${verb}\` is not available on the CLI. Data source ${verb} runs from the ` +
|
|
24
|
+
`Sources Hub, which shows what the change cascades into before it happens: ${url}`));
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=ui-only.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-only.js","sourceRoot":"","sources":["../../../src/commands/ds/ui-only.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,MAAM,CAAC,MAAM,kBAAkB,GAAwC;IACrE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,sEAAsE,EAAE;IACxG,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,oFAAoF,EAAE;CACrH,CAAC;AAEF,MAAM,UAAU,uBAAuB,CAAC,EAAW;IACjD,KAAK,MAAM,MAAM,IAAI,kBAAkB,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;aACpB,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC;aAC3B,MAAM,CAAC;YACN,MAAM,GAAG,GAAG,GAAG,cAAc,EAAE,+BAA+B,CAAC;YAC/D,WAAW,CACT,IAAI,KAAK,CACP,QAAQ,IAAI,+CAA+C,IAAI,iBAAiB;gBAC9E,6EAA6E,GAAG,EAAE,CACrF,CACF,CAAC;YACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACP,CAAC;AACH,CAAC"}
|
package/dist/commands/ds.js
CHANGED
|
@@ -1,359 +1,14 @@
|
|
|
1
|
-
import { Option } from "commander";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { basename } from "node:path";
|
|
5
1
|
import { apiClient } from "../api/client.js";
|
|
6
|
-
import {
|
|
2
|
+
import { errorOutput, getOutputFormat, note, output } from "../output/format.js";
|
|
7
3
|
import { writeStderr } from "../stderr.js";
|
|
8
|
-
import { getFrontendUrl } from "../config.js";
|
|
9
|
-
import { reconcilePoll, NOT_FOUND_STATUS, isCreatePollTerminal } from "./ds-poll.js";
|
|
10
4
|
import { buildRefreshConfigBody, resolveCreateMode } from "./ds-config.js";
|
|
5
|
+
import { createDataSourceFromFile, fetchDSList, triggerRefresh } from "./ds/api.js";
|
|
6
|
+
import { pollUntilDSReady, pollUntilReady, scanAndPrintSummary } from "./ds/polling.js";
|
|
11
7
|
import { registerRefreshHistoryCommand } from "./ds/refresh-history.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return;
|
|
17
|
-
note("");
|
|
18
|
-
for (const w of validation.warnings) {
|
|
19
|
-
note(` ! ${w}`);
|
|
20
|
-
}
|
|
21
|
-
note("");
|
|
22
|
-
}
|
|
23
|
-
const POLL_INTERVAL_MS = 3_000;
|
|
24
|
-
const MAX_POLL_DURATION_MS = 5 * 60_000;
|
|
25
|
-
// Feature #661 + #665: creation runs in a durable Job whose export budget was raised
|
|
26
|
-
// to 60 min (Feature #665), with the worker socket timeout at ~70 min (budget + margin).
|
|
27
|
-
// The create poller must outlast the server-side budget so a legit large load isn't
|
|
28
|
-
// reported as "timed out" client-side while it's still materializing.
|
|
29
|
-
const MAX_CREATE_POLL_DURATION_MS = 70 * 60_000;
|
|
30
|
-
const SCAN_TIMEOUT_MS = 90_000;
|
|
31
|
-
const REFRESH_TRIGGER_TIMEOUT_MS = 60_000;
|
|
32
|
-
// Issue #775: a small/single-table create routes INLINE (Project #247) - the POST
|
|
33
|
-
// itself runs the warehouse export + scan + canonical birth in-request and returns
|
|
34
|
-
// a terminal status, so it needs the same extended budget as the other
|
|
35
|
-
// warehouse-touching POSTs (upload 120s, scan 90s) instead of the 30s default,
|
|
36
|
-
// which a cold-connection inline export races even though the source is born fine.
|
|
37
|
-
// A Job-routed create returns CREATING immediately and never approaches this ceiling.
|
|
38
|
-
const CREATE_TIMEOUT_MS = 120_000;
|
|
39
|
-
async function fetchDSList(limit = 200) {
|
|
40
|
-
const resp = await apiClient.get(`/api/v1/cli/ds?limit=${limit}`);
|
|
41
|
-
return resp.data_sources;
|
|
42
|
-
}
|
|
43
|
-
async function triggerRefresh(id, force = false) {
|
|
44
|
-
try {
|
|
45
|
-
const query = force ? "?force=true" : "";
|
|
46
|
-
const resp = await apiClient.post(`/api/v1/cli/ds/${id}/refresh${query}`, {}, { timeoutMs: REFRESH_TRIGGER_TIMEOUT_MS });
|
|
47
|
-
return resp;
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
const msg = err instanceof Error ? err.message : String(err.detail ?? err);
|
|
51
|
-
if (msg.toLowerCase().includes("timeout") || msg.toLowerCase().includes("timed out")) {
|
|
52
|
-
return { id, status: "refreshing", error: "trigger timed out (refresh continues server-side)" };
|
|
53
|
-
}
|
|
54
|
-
return { id, status: "error", error: msg };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function formatRowCount(n) {
|
|
58
|
-
if (n >= 1_000_000)
|
|
59
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
60
|
-
if (n >= 1_000)
|
|
61
|
-
return `${(n / 1_000).toFixed(1)}K`;
|
|
62
|
-
return String(n);
|
|
63
|
-
}
|
|
64
|
-
function printStatusTable(targets, statuses, errors, clearPrevious) {
|
|
65
|
-
const lines = [];
|
|
66
|
-
const nameWidth = Math.max(4, ...Array.from(targets.values()).map((ds) => ds.name.length));
|
|
67
|
-
lines.push(` ${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(12)} ${"ROWS".padStart(8)}`);
|
|
68
|
-
lines.push(` ${"─".repeat(nameWidth)} ${"─".repeat(12)} ${"─".repeat(8)}`);
|
|
69
|
-
for (const [id, ds] of targets) {
|
|
70
|
-
const status = statuses.get(id) ?? ds.status;
|
|
71
|
-
const error = errors.get(id);
|
|
72
|
-
const icon = status === "ready" ? "+" : status === "error" ? "x" : status === NOT_FOUND_STATUS ? "?" : "*";
|
|
73
|
-
const rowStr = formatRowCount(ds.row_count).padStart(8);
|
|
74
|
-
let line = ` ${ds.name.padEnd(nameWidth)} ${icon} ${status.padEnd(10)} ${rowStr}`;
|
|
75
|
-
if (error)
|
|
76
|
-
line += ` ${error}`;
|
|
77
|
-
lines.push(line);
|
|
78
|
-
}
|
|
79
|
-
// Progress table is decoration -> stderr (with its in-place cursor escapes on
|
|
80
|
-
// the same stream), so stdout stays a clean payload.
|
|
81
|
-
if (clearPrevious) {
|
|
82
|
-
process.stderr.write(`\x1B[${lines.length + 1}A\x1B[J`);
|
|
83
|
-
}
|
|
84
|
-
note(lines.join("\n"));
|
|
85
|
-
return lines.length;
|
|
86
|
-
}
|
|
87
|
-
// Returns true if it gave up at the timeout, false if every target reached a
|
|
88
|
-
// terminal state - the caller surfaces this as `timed_out` in the JSON summary.
|
|
89
|
-
async function pollUntilReady(targetIds, targets, errors) {
|
|
90
|
-
const isTTY = process.stderr.isTTY ?? false;
|
|
91
|
-
const statuses = new Map();
|
|
92
|
-
for (const id of targetIds)
|
|
93
|
-
statuses.set(id, "refreshing");
|
|
94
|
-
const missCounts = new Map();
|
|
95
|
-
let printed = false;
|
|
96
|
-
printStatusTable(targets, statuses, errors, false);
|
|
97
|
-
printed = true;
|
|
98
|
-
const pollStart = Date.now();
|
|
99
|
-
while (true) {
|
|
100
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
101
|
-
if (Date.now() - pollStart > MAX_POLL_DURATION_MS) {
|
|
102
|
-
note(`\n Timed out after ${MAX_POLL_DURATION_MS / 60_000}m waiting for refresh to complete.`);
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
let fresh;
|
|
106
|
-
try {
|
|
107
|
-
fresh = await fetchDSList();
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// Issue #775: tolerate a transient network/transport blip during the wait
|
|
111
|
-
// instead of aborting the refresh-wait and misreporting still-refreshing
|
|
112
|
-
// sources as failed. Re-poll (the loop sleeps first) until the timeout cap.
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const freshMap = new Map(fresh.map((ds) => [ds.id, ds]));
|
|
116
|
-
// A target absent from the fresh list (deleted mid-wait, or transiently
|
|
117
|
-
// omitted) is declared not_found after a couple of polls so the loop ends
|
|
118
|
-
// promptly instead of spinning to MAX_POLL_DURATION_MS.
|
|
119
|
-
const allDone = reconcilePoll(targetIds, freshMap, statuses, targets, missCounts);
|
|
120
|
-
if (isTTY && printed) {
|
|
121
|
-
printStatusTable(targets, statuses, errors, true);
|
|
122
|
-
}
|
|
123
|
-
if (allDone) {
|
|
124
|
-
if (!isTTY && printed) {
|
|
125
|
-
note("");
|
|
126
|
-
printStatusTable(targets, statuses, errors, false);
|
|
127
|
-
}
|
|
128
|
-
// A target only counts as refreshed if it reached "ready" AND nothing went
|
|
129
|
-
// wrong for it. A trigger that errored (or timed out) leaves the source at
|
|
130
|
-
// its previous "ready", so counting status alone reported "1/1 refreshed"
|
|
131
|
-
// with the error sitting in the same payload - the summary contradicting
|
|
132
|
-
// the data beneath it.
|
|
133
|
-
const readyCount = Array.from(statuses.entries()).filter(([id, s]) => s === "ready" && !errors.has(id)).length;
|
|
134
|
-
const errorCount = Array.from(statuses.entries()).filter(([id, s]) => s === "error" || errors.has(id)).length;
|
|
135
|
-
const notFoundCount = Array.from(statuses.values()).filter((s) => s === NOT_FOUND_STATUS).length;
|
|
136
|
-
note(`\n ${readyCount}/${targetIds.size} refreshed${errorCount > 0 ? `, ${errorCount} failed` : ""}${notFoundCount > 0 ? `, ${notFoundCount} not found` : ""}`);
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
async function scanDS(id, force = false, acceptSchema = false) {
|
|
143
|
-
const params = new URLSearchParams();
|
|
144
|
-
if (force)
|
|
145
|
-
params.set("force", "true");
|
|
146
|
-
if (acceptSchema)
|
|
147
|
-
params.set("accept_schema", "true");
|
|
148
|
-
const qs = params.toString();
|
|
149
|
-
return apiClient.post(`/api/v1/cli/ds/${id}/scan${qs ? `?${qs}` : ""}`, {}, { timeoutMs: SCAN_TIMEOUT_MS });
|
|
150
|
-
}
|
|
151
|
-
async function pollUntilDSReady(id, name) {
|
|
152
|
-
const isTTY = process.stderr.isTTY ?? false;
|
|
153
|
-
const start = Date.now();
|
|
154
|
-
if (isTTY)
|
|
155
|
-
process.stderr.write(` Materializing ${name}...`);
|
|
156
|
-
// Feature #661: creation gets the longer create-poll budget (not the 5-min
|
|
157
|
-
// refresh cap), so a legit large load isn't reported as timed-out while still
|
|
158
|
-
// materializing in the Job.
|
|
159
|
-
let firstPoll = true;
|
|
160
|
-
while (Date.now() - start < MAX_CREATE_POLL_DURATION_MS) {
|
|
161
|
-
// Issue #775: poll immediately on the first pass, then wait between polls.
|
|
162
|
-
// An inline create (Project #247) already returned a terminal status from
|
|
163
|
-
// the POST, so sleeping first would tax every inline `ds create --sql` a
|
|
164
|
-
// guaranteed ~POLL_INTERVAL_MS before it can report done. Every `continue`
|
|
165
|
-
// below falls back here, so later passes (and retries) still wait first.
|
|
166
|
-
if (!firstPoll)
|
|
167
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
168
|
-
firstPoll = false;
|
|
169
|
-
let all;
|
|
170
|
-
try {
|
|
171
|
-
all = await fetchDSList();
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// Issue #775: a transient network/transport blip during the up-to-70-min
|
|
175
|
-
// create window must NOT abort the poll and misreport a still-materializing
|
|
176
|
-
// source as failed. Tolerate the throw and re-poll (the next pass waits the
|
|
177
|
-
// poll interval first, matching triggerRefresh's tolerance). A genuine
|
|
178
|
-
// create failure arrives as status "error" in a successful response and is
|
|
179
|
-
// handled below - it is never swallowed here.
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const ds = all.find((d) => d.id === id);
|
|
183
|
-
if (!ds)
|
|
184
|
-
continue;
|
|
185
|
-
if (!isCreatePollTerminal(ds.status))
|
|
186
|
-
continue;
|
|
187
|
-
// Issue #775: pending_verification is a terminal SUCCESS (server-side scan
|
|
188
|
-
// succeeded during creation); without this branch the loop spun to the
|
|
189
|
-
// 70-minute create cap on every scan-succeeded SQL create.
|
|
190
|
-
if (ds.status === "pending_verification") {
|
|
191
|
-
if (isTTY)
|
|
192
|
-
process.stderr.write(` scanned (${formatRowCount(ds.row_count)} rows), awaiting verification\n`);
|
|
193
|
-
else
|
|
194
|
-
note(` ${name}: scanned (${formatRowCount(ds.row_count)} rows), awaiting verification`);
|
|
195
|
-
return ds;
|
|
196
|
-
}
|
|
197
|
-
if (ds.status === "ready") {
|
|
198
|
-
if (isTTY)
|
|
199
|
-
process.stderr.write(` ready (${formatRowCount(ds.row_count)} rows)\n`);
|
|
200
|
-
else
|
|
201
|
-
note(` ${name}: ready (${formatRowCount(ds.row_count)} rows)`);
|
|
202
|
-
return ds;
|
|
203
|
-
}
|
|
204
|
-
if (ds.status === "error") {
|
|
205
|
-
// Feature #661: surface the real, actionable failure reason (was a bare
|
|
206
|
-
// "error" before - useless to a foreign agent driving the CLI).
|
|
207
|
-
if (isTTY)
|
|
208
|
-
process.stderr.write(` failed\n`);
|
|
209
|
-
const detail = ds.last_error || "Data source creation failed.";
|
|
210
|
-
note(` ${name}: ${detail}`);
|
|
211
|
-
return ds;
|
|
212
|
-
}
|
|
213
|
-
// Issue #775: guard the #775 regression. The status is already terminal (it
|
|
214
|
-
// passed isCreatePollTerminal), so any status without a branch above - e.g. a
|
|
215
|
-
// success status later added to CREATE_SUCCESS_STATUSES - must still end the
|
|
216
|
-
// poll. Falling through here would spin to the 70-min cap and reintroduce the
|
|
217
|
-
// hang the guard and branches must not drift into.
|
|
218
|
-
if (isTTY)
|
|
219
|
-
process.stderr.write(` ${ds.status} (${formatRowCount(ds.row_count)} rows)\n`);
|
|
220
|
-
else
|
|
221
|
-
note(` ${name}: ${ds.status} (${formatRowCount(ds.row_count)} rows)`);
|
|
222
|
-
return ds;
|
|
223
|
-
}
|
|
224
|
-
// Feature #661: non-alarming - the Job may still be materializing server-side.
|
|
225
|
-
note(`\n ${name} is still materializing server-side. Check status later with: graphit ds list`);
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
// Issue #775: single copy of the "scanned, awaiting verification" call to action.
|
|
229
|
-
// Printed identically after a create that lands at pending_verification and after
|
|
230
|
-
// `ds verify` scans a warehouse/SQL source - kept in one place so the two paths
|
|
231
|
-
// (create action + printVerifySummary) cannot drift.
|
|
232
|
-
function printVerificationCTA(dsId) {
|
|
233
|
-
const url = `${getFrontendUrl()}/sources?tab=sources&sub=data&id=${dsId}`;
|
|
234
|
-
note(`\n Scanned and awaiting verification. Review and activate in the platform:`);
|
|
235
|
-
note(` ${url}`);
|
|
236
|
-
note(`\n Or accept the AI schema as-is from the CLI:`);
|
|
237
|
-
note(` graphit ds verify ${dsId} --accept-schema\n`);
|
|
238
|
-
}
|
|
239
|
-
function printVerifySummary(scan, dsId, rowCount) {
|
|
240
|
-
const line = "─".repeat(Math.max(scan.table_name.length, 30));
|
|
241
|
-
note(`\n ${scan.table_name}`);
|
|
242
|
-
note(` ${line}`);
|
|
243
|
-
const colSummary = scan.columns
|
|
244
|
-
.slice(0, 6)
|
|
245
|
-
.map((c) => `${c.name} (${c.type})`)
|
|
246
|
-
.join(", ");
|
|
247
|
-
const more = scan.columns.length > 6 ? `, +${scan.columns.length - 6} more` : "";
|
|
248
|
-
note(` Columns: ${colSummary}${more}`);
|
|
249
|
-
if (rowCount !== undefined) {
|
|
250
|
-
note(` Rows: ${formatRowCount(rowCount)}`);
|
|
251
|
-
}
|
|
252
|
-
// A file-upload verify (or an --accept-schema warehouse verify) activates the
|
|
253
|
-
// source server-side, so there is nothing left to do - say so (Issue #544/#546).
|
|
254
|
-
// Confirmed against the state the server reports back, not against the request
|
|
255
|
-
// we made: a scan that demoted the source must fall through to the CTA below.
|
|
256
|
-
if (scan.activated && scan.status !== "pending_verification") {
|
|
257
|
-
note(`\n Verified - this data source is now queryable.\n`);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Scanned but awaiting review (warehouse/SQL): the AI-inferred schema needs a
|
|
261
|
-
// human to accept it. Offer both governed paths - review in the platform wizard,
|
|
262
|
-
// or accept the schema as-is from the CLI (Issue #546). This is not "still
|
|
263
|
-
// materializing" - the data is on disk; only verification is pending.
|
|
264
|
-
printVerificationCTA(dsId);
|
|
265
|
-
}
|
|
266
|
-
async function scanAndPrintSummary(dsId, dsName, rowCount, force = false, acceptSchema = false) {
|
|
267
|
-
const isTTY = process.stderr.isTTY ?? false;
|
|
268
|
-
if (isTTY)
|
|
269
|
-
process.stderr.write(` Scanning schema...`);
|
|
270
|
-
try {
|
|
271
|
-
const scan = await scanDS(dsId, force, acceptSchema);
|
|
272
|
-
if (isTTY) {
|
|
273
|
-
const label = scan.already_scanned ? "already scanned" : "done";
|
|
274
|
-
process.stderr.write(` ${label} (${scan.column_count} columns)\n`);
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
note(` ${dsName}: scanned (${scan.column_count} columns)`);
|
|
278
|
-
}
|
|
279
|
-
printVerifySummary(scan, dsId, rowCount);
|
|
280
|
-
printShapeWarnings(scan.validation);
|
|
281
|
-
return scan;
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
if (isTTY)
|
|
285
|
-
process.stderr.write(` failed\n`);
|
|
286
|
-
const msg = err instanceof Error ? err.message : String(err.detail ?? err);
|
|
287
|
-
note(` Scan failed: ${msg}`);
|
|
288
|
-
// Project #263 Phase 13 (13.8): never advise a retry the server has already
|
|
289
|
-
// said cannot work. `ds verify` is a gated write, so this catch now sees
|
|
290
|
-
// permission denials - and offering "you can retry" on a `retryable: false`
|
|
291
|
-
// problem is the retry-loop bait 13.5 removed from the denial body, put back
|
|
292
|
-
// by the client. Read the FIELD, not the prose.
|
|
293
|
-
const problem = err.problem;
|
|
294
|
-
if (problem?.retryable === false) {
|
|
295
|
-
if (problem.next_step)
|
|
296
|
-
note(` ${problem.next_step}`);
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
note(` You can retry with: graphit ds verify ${dsId}`);
|
|
300
|
-
}
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Create a data source from a local Excel/CSV file (Feature #645). Uploads the
|
|
306
|
-
* file as multipart; the server parses, runs the inline AI scan, encrypts, and
|
|
307
|
-
* (optionally) attaches the new table to a domain - all in one request.
|
|
308
|
-
*/
|
|
309
|
-
async function createDataSourceFromFile(cmd, opts) {
|
|
310
|
-
const filePath = String(opts.file);
|
|
311
|
-
if (!existsSync(filePath)) {
|
|
312
|
-
errorOutput(new Error(`File not found: ${filePath}`));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
const buf = await readFile(filePath);
|
|
316
|
-
const filename = basename(filePath);
|
|
317
|
-
const form = new FormData();
|
|
318
|
-
form.append("file", new Blob([buf]), filename);
|
|
319
|
-
if (opts.name)
|
|
320
|
-
form.append("name", String(opts.name));
|
|
321
|
-
if (opts.sheet)
|
|
322
|
-
form.append("sheet", String(opts.sheet));
|
|
323
|
-
if (opts.domain)
|
|
324
|
-
form.append("domain", String(opts.domain));
|
|
325
|
-
note(` Uploading ${filename}...`);
|
|
326
|
-
const resp = await apiClient.postMultipart("/api/v1/cli/ds/upload", form, { timeoutMs: 120_000 });
|
|
327
|
-
if (resp.domain_note)
|
|
328
|
-
note(` ${resp.domain_note}`);
|
|
329
|
-
output(cmd, resp, { keyValue: true });
|
|
330
|
-
}
|
|
331
|
-
// Data-source lifecycle actions that are deliberately UI-only. Destroying or
|
|
332
|
-
// re-pointing a source cascades into dashboards, KB tables and cached snapshots,
|
|
333
|
-
// so it stays behind the Sources Hub where the blast radius is shown before you
|
|
334
|
-
// commit. Creating one from the CLI is safe and stays supported.
|
|
335
|
-
//
|
|
336
|
-
// Registered as real commands rather than left to Commander's bare
|
|
337
|
-
// `unknown command 'delete'`: an agent that gets "unknown command" concludes the
|
|
338
|
-
// CLI is incomplete and goes hunting for a flag or a workaround. It needs to
|
|
339
|
-
// learn the boundary is intentional and where the action actually lives.
|
|
340
|
-
const UI_ONLY_DS_ACTIONS = [
|
|
341
|
-
{ name: "delete <id>", summary: "Delete a data source - not available on the CLI, use the Sources Hub" },
|
|
342
|
-
{ name: "move <id>", summary: "Move a data source between domains - not available on the CLI, use the Sources Hub" },
|
|
343
|
-
];
|
|
344
|
-
function registerUiOnlyDSActions(ds) {
|
|
345
|
-
for (const action of UI_ONLY_DS_ACTIONS) {
|
|
346
|
-
const verb = action.name.split(" ")[0];
|
|
347
|
-
ds.command(action.name)
|
|
348
|
-
.description(action.summary)
|
|
349
|
-
.action(function () {
|
|
350
|
-
const url = `${getFrontendUrl()}/sources?tab=sources&sub=data`;
|
|
351
|
-
errorOutput(new Error(`\`ds ${verb}\` is not available on the CLI. Data source ${verb} runs from the ` +
|
|
352
|
-
`Sources Hub, which shows what the change cascades into before it happens: ${url}`));
|
|
353
|
-
process.exitCode = 1;
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
8
|
+
import { printShapeWarnings, printStatusTable, printVerificationCTA } from "./ds/render.js";
|
|
9
|
+
import { CREATE_TIMEOUT_MS } from "./ds/types.js";
|
|
10
|
+
import { registerUiOnlyDSActions } from "./ds/ui-only.js";
|
|
11
|
+
import { Option } from "commander";
|
|
357
12
|
export function registerDSCommands(program) {
|
|
358
13
|
const ds = program.command("ds").description("Data source management");
|
|
359
14
|
// Project #258: read-only refresh-run lineage; lives in its own module to keep
|