@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.
@@ -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"}
@@ -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 { output, errorOutput, note, getOutputFormat } from "../output/format.js";
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
- // Project #244: print soft, non-blocking shape advisories. Print-and-proceed -
13
- // these never gate a create or verify.
14
- function printShapeWarnings(validation) {
15
- if (!validation?.warnings?.length)
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