ingestron 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +71 -0
- package/SECURITY.md +32 -0
- package/THIRD_PARTY_NOTICES.md +3656 -0
- package/build/cli/cli/index.d.ts +2 -0
- package/build/cli/cli/index.js +588 -0
- package/build/cli/cli/operation-worker.d.ts +1 -0
- package/build/cli/cli/operation-worker.js +5 -0
- package/build/cli/cli/output.d.ts +3 -0
- package/build/cli/cli/output.js +130 -0
- package/build/cli/cli/plugin-browser.d.ts +14 -0
- package/build/cli/cli/plugin-browser.js +74 -0
- package/build/cli/cli/presentation.d.ts +12 -0
- package/build/cli/cli/presentation.js +57 -0
- package/build/cli/cli/workflow-commands.d.ts +15 -0
- package/build/cli/cli/workflow-commands.js +405 -0
- package/build/cli/mcp/index.d.ts +3 -0
- package/build/cli/mcp/index.js +66 -0
- package/build/cli/version.d.ts +1 -0
- package/build/cli/version.js +13 -0
- package/package.json +73 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import * as prompts from "@clack/prompts";
|
|
4
|
+
import { resolve, basename, dirname } from "node:path";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
6
|
+
import { parseDocument } from "yaml";
|
|
7
|
+
import { execute, executeAsync, } from "@ingestron/core";
|
|
8
|
+
import { fence } from "@ingestron/core/adapter";
|
|
9
|
+
import { Configuration } from "@ingestron/core/adapter";
|
|
10
|
+
import { check, Problem } from "@ingestron/core/adapter";
|
|
11
|
+
import { version } from "../version.js";
|
|
12
|
+
import { terminal, exitCode } from "./output.js";
|
|
13
|
+
import { decorate, banner, backgroundOperation } from "./presentation.js";
|
|
14
|
+
import { workflowCommands } from "./workflow-commands.js";
|
|
15
|
+
const app = new Command()
|
|
16
|
+
.name("ingestron")
|
|
17
|
+
.description("Configure flows once. Generate reviewable native data engineering assets offline.")
|
|
18
|
+
.version(version)
|
|
19
|
+
.option("--project <directory>", "Project root", ".")
|
|
20
|
+
.option("-e, --environment <name>", "Selected environment", "dev")
|
|
21
|
+
.option("--env <name>", "Alias for --environment")
|
|
22
|
+
.option("--json", "Versioned JSON output; progress goes to stderr")
|
|
23
|
+
.option("--plain", "Plain noninteractive output; no styling or animation")
|
|
24
|
+
.option("--no-color", "Disable terminal colour and decoration")
|
|
25
|
+
.option("--no-progress", "Disable progress animation")
|
|
26
|
+
.option("--verbose", "Show full result details in human-readable output")
|
|
27
|
+
.option("--no-input", "Never ask interactive questions")
|
|
28
|
+
.option("--dry-run", "Preview local file changes without applying");
|
|
29
|
+
const opts = () => app.opts();
|
|
30
|
+
const presentation = () => ({
|
|
31
|
+
tty: !!process.stdout.isTTY,
|
|
32
|
+
plain: opts().plain,
|
|
33
|
+
colour: opts().color,
|
|
34
|
+
progress: opts().progress,
|
|
35
|
+
json: !!opts().json,
|
|
36
|
+
});
|
|
37
|
+
const context = (root) => ({
|
|
38
|
+
root: root ?? resolve(opts().project),
|
|
39
|
+
environment: opts().env ?? opts().environment,
|
|
40
|
+
allowWrite: true,
|
|
41
|
+
allowNetwork: true,
|
|
42
|
+
allowExecute: true,
|
|
43
|
+
});
|
|
44
|
+
function print(result, args) {
|
|
45
|
+
process.exitCode = exitCode(result);
|
|
46
|
+
if (opts().json || process.argv.includes("--json"))
|
|
47
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
48
|
+
else
|
|
49
|
+
(result.ok ? process.stdout : process.stderr).write(decorate(terminal(result, !!opts().verbose, args), result.ok, presentation()) + "\n");
|
|
50
|
+
}
|
|
51
|
+
// Commander failures must obey the same JSON contract as operation failures.
|
|
52
|
+
// Help/version remain successful envelopes in JSON mode; no progress goes to stdout.
|
|
53
|
+
let helpText = "";
|
|
54
|
+
app.exitOverride();
|
|
55
|
+
app.configureOutput({
|
|
56
|
+
writeOut: (text) => {
|
|
57
|
+
if (process.argv.includes("--json"))
|
|
58
|
+
helpText += text;
|
|
59
|
+
else
|
|
60
|
+
process.stdout.write(text.startsWith("Usage: ingestron [")
|
|
61
|
+
? banner(text, presentation())
|
|
62
|
+
: text);
|
|
63
|
+
},
|
|
64
|
+
writeErr: (text) => {
|
|
65
|
+
if (text.startsWith("Usage:")) {
|
|
66
|
+
if (process.argv.includes("--json"))
|
|
67
|
+
helpText += text;
|
|
68
|
+
else
|
|
69
|
+
process.stdout.write(text.startsWith("Usage: ingestron [")
|
|
70
|
+
? banner(text, presentation())
|
|
71
|
+
: text);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
outputError: () => { },
|
|
75
|
+
});
|
|
76
|
+
const run = async (name, args = {}, ctx = context()) => {
|
|
77
|
+
const result = ["build", "plan", "generate", "packages_install"].includes(name)
|
|
78
|
+
? await backgroundOperation(ctx, name, args, `${name === "packages_install" ? "Installing plugin" : "Preparing native assets"}`, presentation())
|
|
79
|
+
: await executeAsync(ctx, name, args);
|
|
80
|
+
print(result, args);
|
|
81
|
+
};
|
|
82
|
+
function author(name, args, ctx = context()) {
|
|
83
|
+
const result = execute(ctx, name, args);
|
|
84
|
+
return print(!result.ok || opts().dryRun
|
|
85
|
+
? result
|
|
86
|
+
: execute(ctx, "apply", { proposal: result.result }));
|
|
87
|
+
}
|
|
88
|
+
function readData(file) {
|
|
89
|
+
const doc = parseDocument(new Configuration(context().root).text(file), {
|
|
90
|
+
uniqueKeys: true,
|
|
91
|
+
});
|
|
92
|
+
check(!doc.errors.length, "YAML", doc.errors[0]?.message ?? "Invalid YAML");
|
|
93
|
+
return doc.toJS();
|
|
94
|
+
}
|
|
95
|
+
function save(file, value) {
|
|
96
|
+
const absolute = fence(resolve(opts().project), file);
|
|
97
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
98
|
+
writeFileSync(absolute, JSON.stringify(value, null, 2) + "\n");
|
|
99
|
+
}
|
|
100
|
+
const canPrompt = () => !!process.stdin.isTTY &&
|
|
101
|
+
opts().input !== false &&
|
|
102
|
+
!opts().json &&
|
|
103
|
+
!opts().plain &&
|
|
104
|
+
process.env.TERM !== "dumb" &&
|
|
105
|
+
!process.env.CI;
|
|
106
|
+
async function answer(value) {
|
|
107
|
+
const result = await value;
|
|
108
|
+
if (prompts.isCancel(result)) {
|
|
109
|
+
process.exitCode = 130;
|
|
110
|
+
throw new Problem("CANCELLED", "Cancelled");
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
app
|
|
115
|
+
.command("init [directory]")
|
|
116
|
+
.description("Create a project; install a provider when ready to build")
|
|
117
|
+
.option("--id <id>", "Project identifier")
|
|
118
|
+
.option("--provider <reference>", "Installed repository or exact reference, e.g. owner/provider@1.0.0")
|
|
119
|
+
.option("--environments <names>", "Comma-separated environments (default: dev,test,prod; --env selects one)")
|
|
120
|
+
.action(async (directory = ".", command) => {
|
|
121
|
+
const root = resolve(opts().project, directory);
|
|
122
|
+
const id = command.id ??
|
|
123
|
+
(canPrompt()
|
|
124
|
+
? await answer(prompts.text({
|
|
125
|
+
message: "Project identifier",
|
|
126
|
+
initialValue: basename(root),
|
|
127
|
+
}))
|
|
128
|
+
: basename(root));
|
|
129
|
+
author("initialise", {
|
|
130
|
+
id,
|
|
131
|
+
provider: command.provider,
|
|
132
|
+
environments: (command.environments ??
|
|
133
|
+
(opts().env !== undefined ||
|
|
134
|
+
app.getOptionValueSource("environment") === "cli"
|
|
135
|
+
? context().environment
|
|
136
|
+
: "dev,test,prod"))
|
|
137
|
+
.split(",")
|
|
138
|
+
.map((name) => name.trim()),
|
|
139
|
+
}, context(root));
|
|
140
|
+
});
|
|
141
|
+
const flow = app
|
|
142
|
+
.command("flow")
|
|
143
|
+
.description("Create and inspect ingestion/transformation flows");
|
|
144
|
+
flow
|
|
145
|
+
.command("add <id>")
|
|
146
|
+
.option("--kind <kind>", "ingestion or transformation", "ingestion")
|
|
147
|
+
.option("--contract <id>", "Contract ID or project-relative file")
|
|
148
|
+
.option("--source <id>", "Named source; otherwise inferred from contract provenance")
|
|
149
|
+
.option("--standard <name>", "Provider ingestion standard, e.g. snapshot-with-history@v1")
|
|
150
|
+
.option("--provider <configuration>", "Provider configuration for this flow")
|
|
151
|
+
.option("--source-kind <kind>", "Provider source kind")
|
|
152
|
+
.option("--format <format>", "Provider-supported file format (for example csv, json, parquet)")
|
|
153
|
+
.option("--source-binding <id>", "Existing or new environment source binding")
|
|
154
|
+
.option("--input-dataset <id>", "Published source dataset for a native SQL transformation")
|
|
155
|
+
.action(async (id, command) => {
|
|
156
|
+
let standard = command.standard;
|
|
157
|
+
if (command.kind === "ingestion" && !standard && canPrompt())
|
|
158
|
+
standard = await answer(prompts.text({
|
|
159
|
+
message: "Standard from the installed provider (name@version)",
|
|
160
|
+
validate: (value) => value?.trim() ? undefined : "Enter a provider standard",
|
|
161
|
+
}));
|
|
162
|
+
author("flow_create", {
|
|
163
|
+
...(command.contract ? { contract: command.contract } : {}),
|
|
164
|
+
...(command.source ? { source: command.source } : {}),
|
|
165
|
+
id,
|
|
166
|
+
kind: command.kind,
|
|
167
|
+
...(standard ? { standard } : {}),
|
|
168
|
+
...(command.provider ? { provider: command.provider } : {}),
|
|
169
|
+
...(command.sourceKind ? { sourceKind: command.sourceKind } : {}),
|
|
170
|
+
...(command.format ? { format: command.format } : {}),
|
|
171
|
+
...(command.inputDataset ? { inputDataset: command.inputDataset } : {}),
|
|
172
|
+
...(command.sourceBinding
|
|
173
|
+
? { sourceBinding: command.sourceBinding }
|
|
174
|
+
: {}),
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
flow
|
|
178
|
+
.command("configure <id>")
|
|
179
|
+
.requiredOption("--patch <file>")
|
|
180
|
+
.action((id, command) => author("flow_configure", { flow: id, patch: readData(command.patch) }));
|
|
181
|
+
app
|
|
182
|
+
.command("step")
|
|
183
|
+
.command("add <id>")
|
|
184
|
+
.requiredOption("--flow <id>")
|
|
185
|
+
.requiredOption("--uses <activity>")
|
|
186
|
+
.option("--after <id>")
|
|
187
|
+
.option("--with <file>")
|
|
188
|
+
.action((id, command) => author("step_add", {
|
|
189
|
+
flow: command.flow,
|
|
190
|
+
step: {
|
|
191
|
+
id,
|
|
192
|
+
uses: command.uses,
|
|
193
|
+
with: command.with ? readData(command.with) : {},
|
|
194
|
+
},
|
|
195
|
+
...(command.after ? { after: command.after } : {}),
|
|
196
|
+
}));
|
|
197
|
+
flow
|
|
198
|
+
.command("list")
|
|
199
|
+
.description("List configured flows, table names and published outputs")
|
|
200
|
+
.action(() => run("inspect"));
|
|
201
|
+
const table = app
|
|
202
|
+
.command("table")
|
|
203
|
+
.description("Declare ingestion tables once per flow");
|
|
204
|
+
table
|
|
205
|
+
.command("add <id>")
|
|
206
|
+
.requiredOption("--flow <id>", "Owning ingestion flow")
|
|
207
|
+
.requiredOption("--contract <file>", "Source ODCS file inside the project")
|
|
208
|
+
.action((id, command) => author("table_add", {
|
|
209
|
+
id,
|
|
210
|
+
flow: command.flow,
|
|
211
|
+
contract: command.contract,
|
|
212
|
+
}));
|
|
213
|
+
table
|
|
214
|
+
.command("configure <id>")
|
|
215
|
+
.requiredOption("--flow <id>", "Owning flow")
|
|
216
|
+
.requiredOption("--patch <file>", "YAML/JSON override patch")
|
|
217
|
+
.action((id, command) => author("table_configure", {
|
|
218
|
+
id,
|
|
219
|
+
flow: command.flow,
|
|
220
|
+
patch: readData(command.patch),
|
|
221
|
+
}));
|
|
222
|
+
table
|
|
223
|
+
.command("list")
|
|
224
|
+
.description("List configured table names grouped by flow")
|
|
225
|
+
.action(() => run("tables"));
|
|
226
|
+
const dataset = app.command("dataset");
|
|
227
|
+
dataset.command("list").action(() => run("datasets"));
|
|
228
|
+
dataset
|
|
229
|
+
.command("add <id>")
|
|
230
|
+
.requiredOption("--flow <id>", "Transformation flow")
|
|
231
|
+
.requiredOption("--contract <file>", "Output ODCS file")
|
|
232
|
+
.requiredOption("--from <port>", "Producing port, e.g. steps.ready.outputs.table")
|
|
233
|
+
.action((id, command) => author("dataset_add", {
|
|
234
|
+
id,
|
|
235
|
+
flow: command.flow,
|
|
236
|
+
contract: command.contract,
|
|
237
|
+
from: command.from,
|
|
238
|
+
}));
|
|
239
|
+
const config = app
|
|
240
|
+
.command("config")
|
|
241
|
+
.description("Configure existing resources and non-secret values");
|
|
242
|
+
config
|
|
243
|
+
.command("set <path>")
|
|
244
|
+
.requiredOption("--value <value>", "YAML scalar or mapping")
|
|
245
|
+
.action((path, command) => author("config_set", { path, value: parseDocument(command.value).toJS() }));
|
|
246
|
+
config
|
|
247
|
+
.command("explain <path>")
|
|
248
|
+
.action((path) => run("config_explain", { path }));
|
|
249
|
+
config.command("fill").action(async () => {
|
|
250
|
+
const result = execute(context(), "resolve", {});
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
print(result);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
check(canPrompt(), "INPUT", "config fill requires an interactive terminal; use config set or doctor with --no-input/--json");
|
|
256
|
+
const profile = result.result.project.environments[context().environment];
|
|
257
|
+
const fields = [];
|
|
258
|
+
const walk = (value, path) => {
|
|
259
|
+
if (value && typeof value === "object" && value.$env)
|
|
260
|
+
fields.push({ path, variable: value.$env });
|
|
261
|
+
else if (value && typeof value === "object")
|
|
262
|
+
for (const [k, child] of Object.entries(value))
|
|
263
|
+
walk(child, path ? path + "." + k : k);
|
|
264
|
+
};
|
|
265
|
+
walk(profile.bindings, "bindings");
|
|
266
|
+
walk(profile.values, "values");
|
|
267
|
+
for (const field of fields) {
|
|
268
|
+
const value = await answer(prompts.text({
|
|
269
|
+
message: `${field.path} (${field.variable})`,
|
|
270
|
+
placeholder: "Existing resource value; leave blank to keep pending",
|
|
271
|
+
}));
|
|
272
|
+
if (value)
|
|
273
|
+
author("config_set", { path: field.path, value });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
const environments = app.command("environments");
|
|
277
|
+
environments.command("list").action(() => run("environments"));
|
|
278
|
+
environments
|
|
279
|
+
.command("add <name>")
|
|
280
|
+
.description("Copy an existing environment for review and configuration")
|
|
281
|
+
.requiredOption("--from <environment>", "Existing environment to copy")
|
|
282
|
+
.action((name, command) => author("environment_add", { name, from: command.from }));
|
|
283
|
+
for (const kind of ["activities", "standards", "providers"]) {
|
|
284
|
+
const group = app
|
|
285
|
+
.command(kind)
|
|
286
|
+
.description(`Inspect current ${kind} and their supported capabilities`);
|
|
287
|
+
group.command("list").action(() => run("catalogue", { kind }));
|
|
288
|
+
group.command("explain <id>").action((id) => run("catalogue", { kind, id }));
|
|
289
|
+
}
|
|
290
|
+
const connections = app
|
|
291
|
+
.command("connections")
|
|
292
|
+
.description("Validate and prepare project-defined connector flows without source access");
|
|
293
|
+
connections
|
|
294
|
+
.command("validate <flow>")
|
|
295
|
+
.action((flow) => run("connection_prepare", { flow, validateOnly: true }));
|
|
296
|
+
connections
|
|
297
|
+
.command("prepare <flow>")
|
|
298
|
+
.requiredOption("--out <file>", "New provider result JSON file")
|
|
299
|
+
.action((flow, command) => {
|
|
300
|
+
const result = execute(context(), "connection_prepare", { flow });
|
|
301
|
+
if (result.ok) {
|
|
302
|
+
const file = fence(context().root, command.out);
|
|
303
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
304
|
+
writeFileSync(file, JSON.stringify(result.result, null, 2) + "\n", {
|
|
305
|
+
flag: "wx",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
print(result);
|
|
309
|
+
});
|
|
310
|
+
connections
|
|
311
|
+
.command("contracts <flow>")
|
|
312
|
+
.requiredOption("--review <file>", "Approved generated review")
|
|
313
|
+
.requiredOption("--out <file>", "New provider result file")
|
|
314
|
+
.action((flow, command) => {
|
|
315
|
+
const result = execute(context(), "connection_prepare", {
|
|
316
|
+
flow,
|
|
317
|
+
review: command.review,
|
|
318
|
+
});
|
|
319
|
+
if (result.ok) {
|
|
320
|
+
const file = fence(context().root, command.out);
|
|
321
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
322
|
+
writeFileSync(file, JSON.stringify(result.result, null, 2) + "\n", {
|
|
323
|
+
flag: "wx",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
print(result);
|
|
327
|
+
});
|
|
328
|
+
const providerCommands = app.commands.find((command) => command.name() === "providers");
|
|
329
|
+
for (const update of [false, true])
|
|
330
|
+
providerCommands
|
|
331
|
+
.command(`${update ? "update" : "install"} <reference>`)
|
|
332
|
+
.description("Install a provider version into the project cache; select it in project.yaml")
|
|
333
|
+
.option("--from-git <directory>", "Local Git repository")
|
|
334
|
+
.option("--frozen", "Require an existing immutable lock")
|
|
335
|
+
.action((reference, command) => run("packages_install", {
|
|
336
|
+
reference,
|
|
337
|
+
update,
|
|
338
|
+
frozen: !!command.frozen,
|
|
339
|
+
...(command.fromGit ? { fromGit: command.fromGit } : {}),
|
|
340
|
+
}));
|
|
341
|
+
providerCommands
|
|
342
|
+
.command("commands <configuration>")
|
|
343
|
+
.description("List provider-owned commands and input schemas without executing plugin code")
|
|
344
|
+
.action((configuration) => run("provider_commands", { configuration }));
|
|
345
|
+
app
|
|
346
|
+
.command("provider <configuration> <command...>")
|
|
347
|
+
.description("Run an installed provider's offline command; inspect providers commands first")
|
|
348
|
+
.option("--input <file>", "Project-relative JSON input file")
|
|
349
|
+
.option("--out <file>", "Save the provider result JSON to a new project file")
|
|
350
|
+
.action((configuration, words, command) => providerAction("provider_command", { configuration, command: words.join(" ") }, command));
|
|
351
|
+
function providerAction(operation, args, command) {
|
|
352
|
+
const input = command.input
|
|
353
|
+
? JSON.parse(readFileSync(new Configuration(context().root).path(command.input), "utf8"))
|
|
354
|
+
: {};
|
|
355
|
+
const result = execute(context(), operation, { ...args, input });
|
|
356
|
+
if (result.ok && command.out) {
|
|
357
|
+
const file = fence(context().root, command.out);
|
|
358
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
359
|
+
writeFileSync(file, JSON.stringify(result.result, null, 2) + "\n", {
|
|
360
|
+
flag: "wx",
|
|
361
|
+
});
|
|
362
|
+
if (!opts().json && !opts().verbose) {
|
|
363
|
+
process.stdout.write(`Saved provider result to ${command.out}\n`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
print(result);
|
|
368
|
+
}
|
|
369
|
+
providerCommands
|
|
370
|
+
.command("export")
|
|
371
|
+
.description("Review/export files returned by an offline provider; no script execution")
|
|
372
|
+
.requiredOption("--result <file>", "Saved provider result JSON")
|
|
373
|
+
.requiredOption("--out <directory>", "New artifact directory")
|
|
374
|
+
.action((command) => author("provider_export", { result: command.result, out: command.out }));
|
|
375
|
+
providerCommands
|
|
376
|
+
.command("migrate")
|
|
377
|
+
.requiredOption("--from <package>", "Project package key")
|
|
378
|
+
.requiredOption("--to <reference>", "Installed exact provider reference")
|
|
379
|
+
.action((command) => author("provider_migrate", { package: command.from, to: command.to }));
|
|
380
|
+
const packages = app.command("packages");
|
|
381
|
+
packages.command("list").action(() => run("packages_list"));
|
|
382
|
+
for (const update of [false, true])
|
|
383
|
+
packages
|
|
384
|
+
.command(`${update ? "update" : "install"} <reference>`)
|
|
385
|
+
.option("--from-git <directory>", "Explicit local Git source for offline installation")
|
|
386
|
+
.option("--frozen", "Require existing immutable lock entry")
|
|
387
|
+
.action((reference, command) => run("packages_install", {
|
|
388
|
+
reference,
|
|
389
|
+
update,
|
|
390
|
+
...(command.fromGit ? { fromGit: command.fromGit } : {}),
|
|
391
|
+
frozen: !!command.frozen,
|
|
392
|
+
}));
|
|
393
|
+
app
|
|
394
|
+
.command("validate")
|
|
395
|
+
.option("--mode <mode>", "draft or strict", "strict")
|
|
396
|
+
.option("--flow <id>")
|
|
397
|
+
.option("--table <id>")
|
|
398
|
+
.option("--step <id>")
|
|
399
|
+
.action((command) => run("validate", {
|
|
400
|
+
mode: command.mode,
|
|
401
|
+
...(command.flow ? { flow: command.flow } : {}),
|
|
402
|
+
...(command.table ? { table: command.table } : {}),
|
|
403
|
+
...(command.step ? { step: command.step } : {}),
|
|
404
|
+
}));
|
|
405
|
+
app
|
|
406
|
+
.command("doctor")
|
|
407
|
+
.description("Check local configuration and missing inputs; no platform connection")
|
|
408
|
+
.action(() => run("doctor"));
|
|
409
|
+
app.command("resolve").action(() => run("resolve"));
|
|
410
|
+
app
|
|
411
|
+
.command("plan")
|
|
412
|
+
.option("--flow <id>")
|
|
413
|
+
.option("--table <id>")
|
|
414
|
+
.option("--step <id>")
|
|
415
|
+
.option("--out <file>", "Save a digest-bound JSON plan")
|
|
416
|
+
.action((command) => {
|
|
417
|
+
const result = execute(context(), "plan", {
|
|
418
|
+
...(command.flow ? { flow: command.flow } : {}),
|
|
419
|
+
...(command.table ? { table: command.table } : {}),
|
|
420
|
+
...(command.step ? { step: command.step } : {}),
|
|
421
|
+
});
|
|
422
|
+
if (result.ok && command.out)
|
|
423
|
+
save(command.out, result.result);
|
|
424
|
+
print(result);
|
|
425
|
+
});
|
|
426
|
+
app
|
|
427
|
+
.command("diff")
|
|
428
|
+
.description("Compare local plans; no remote deployment drift lookup")
|
|
429
|
+
.requiredOption("--plan <file>", "Previously reviewed plan")
|
|
430
|
+
.action((command) => run("diff", { plan: command.plan }));
|
|
431
|
+
app
|
|
432
|
+
.command("generate")
|
|
433
|
+
.requiredOption("--plan <file>", "Saved plan JSON")
|
|
434
|
+
.requiredOption("--out <directory>", "Owned output directory inside project")
|
|
435
|
+
.option("--ownership <mode>", "managed (regenerate safely) or team (manual handover)", "managed")
|
|
436
|
+
.action((command) => run("generate", {
|
|
437
|
+
plan: command.plan,
|
|
438
|
+
out: command.out,
|
|
439
|
+
ownership: command.ownership,
|
|
440
|
+
}));
|
|
441
|
+
app
|
|
442
|
+
.command("validate-output <directory>")
|
|
443
|
+
.description("Check local output ownership and syntax; no target validation")
|
|
444
|
+
.action((directory) => run("validate_output", { directory }));
|
|
445
|
+
app.command("schema <name>").action((name) => run("schema", { name }));
|
|
446
|
+
const contract = app.command("contract");
|
|
447
|
+
const activity = app.command("activity");
|
|
448
|
+
activity
|
|
449
|
+
.command("validate <file>")
|
|
450
|
+
.description("Validate an activity manifest and inventory source files; no rendering or execution")
|
|
451
|
+
.action((file) => run("activity_inspect", { path: file }));
|
|
452
|
+
contract
|
|
453
|
+
.command("validate <file>")
|
|
454
|
+
.action((file) => run("contract_validate", { path: file }));
|
|
455
|
+
app
|
|
456
|
+
.command("mcp")
|
|
457
|
+
.command("serve")
|
|
458
|
+
.option("--allow-write", "Allow explicit reviewed local changes")
|
|
459
|
+
.option("--allow-execute", "Allow explicit customer-side source execution; requires --allow-write")
|
|
460
|
+
.option("--allow-network", "Allow GitHub package version lookup and fetching; no platform access")
|
|
461
|
+
.action(async (command) => {
|
|
462
|
+
const { serve } = await import("../mcp/index.js");
|
|
463
|
+
await serve(resolve(opts().project), !!command.allowWrite, context().environment, !!command.allowNetwork, !!command.allowExecute);
|
|
464
|
+
});
|
|
465
|
+
workflowCommands(app, {
|
|
466
|
+
context,
|
|
467
|
+
run,
|
|
468
|
+
author,
|
|
469
|
+
print,
|
|
470
|
+
readData,
|
|
471
|
+
canPrompt,
|
|
472
|
+
answer,
|
|
473
|
+
perform: (operation, args, label) => backgroundOperation(context(), operation, args, label, presentation()),
|
|
474
|
+
});
|
|
475
|
+
app.hook("preAction", (_root, command) => {
|
|
476
|
+
if (!opts().dryRun)
|
|
477
|
+
return;
|
|
478
|
+
const parts = [];
|
|
479
|
+
for (let current = command; current && current !== app; current = current.parent)
|
|
480
|
+
parts.unshift(current.name());
|
|
481
|
+
check(new Set([
|
|
482
|
+
"init",
|
|
483
|
+
"source add",
|
|
484
|
+
"source configure",
|
|
485
|
+
"source prepare",
|
|
486
|
+
"source import",
|
|
487
|
+
"plugin configure",
|
|
488
|
+
"plugin migrate",
|
|
489
|
+
"config environment add",
|
|
490
|
+
"flow table add",
|
|
491
|
+
"flow table configure",
|
|
492
|
+
"flow step add",
|
|
493
|
+
"flow output add",
|
|
494
|
+
"advanced export",
|
|
495
|
+
"flow add",
|
|
496
|
+
"flow configure",
|
|
497
|
+
"step add",
|
|
498
|
+
"table add",
|
|
499
|
+
"table configure",
|
|
500
|
+
"dataset add",
|
|
501
|
+
"config set",
|
|
502
|
+
"config fill",
|
|
503
|
+
"environments add",
|
|
504
|
+
"providers migrate",
|
|
505
|
+
"providers export",
|
|
506
|
+
"contract draft",
|
|
507
|
+
"plugin scaffold",
|
|
508
|
+
"plugin pack-add",
|
|
509
|
+
]).has(parts.join(" ")), "OPTION", "--dry-run is supported only by project authoring commands; this command has not run");
|
|
510
|
+
});
|
|
511
|
+
async function parseCommands() {
|
|
512
|
+
const parsed = app.parseOptions(process.argv.slice(2));
|
|
513
|
+
const requested = [...parsed.operands, ...parsed.unknown].find((arg) => !arg.startsWith("-"));
|
|
514
|
+
const known = new Set(app.commands.flatMap((command) => [command.name(), ...command.aliases()]));
|
|
515
|
+
if (existsSync(resolve(context().root, "project.yaml"))) {
|
|
516
|
+
const catalogue = execute(context(), "plugin_commands", {});
|
|
517
|
+
if (!catalogue.ok) {
|
|
518
|
+
if (requested && !known.has(requested)) {
|
|
519
|
+
print(catalogue);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
for (const binding of catalogue.result.bindings) {
|
|
525
|
+
check(!known.has(binding.namespace), "COMMAND_NAMESPACE", `Plugin namespace conflicts with core command ${binding.namespace}`);
|
|
526
|
+
let namespace = app.commands.find((command) => command.name() === binding.namespace);
|
|
527
|
+
if (namespace)
|
|
528
|
+
continue; // Same locked package, multiple selectable configurations.
|
|
529
|
+
namespace = app
|
|
530
|
+
.command(binding.namespace)
|
|
531
|
+
.description(`Installed ${binding.provider} offline operations`);
|
|
532
|
+
for (const definition of binding.commands) {
|
|
533
|
+
const words = definition.name.split(" ");
|
|
534
|
+
let parent = namespace;
|
|
535
|
+
for (const word of words.slice(0, -1)) {
|
|
536
|
+
let group = parent.commands.find((command) => command.name() === word);
|
|
537
|
+
if (!group)
|
|
538
|
+
group = parent.command(word).description(`${word} operations`);
|
|
539
|
+
parent = group;
|
|
540
|
+
}
|
|
541
|
+
parent
|
|
542
|
+
.command(words.at(-1))
|
|
543
|
+
.description(definition.description)
|
|
544
|
+
.option("--target <configuration>", "Provider configuration; required when multiple bindings match")
|
|
545
|
+
.option("--input <file>", "Project-relative JSON input file")
|
|
546
|
+
.option("--out <file>", "Save result JSON to a new project file")
|
|
547
|
+
.addHelpText("after", `\nInput schema:\n${JSON.stringify(definition.inputSchema, null, 2)}\n${(definition.examples ?? []).join("\n")}`)
|
|
548
|
+
.action((options) => providerAction("plugin_command", {
|
|
549
|
+
namespace: binding.namespace,
|
|
550
|
+
command: definition.name,
|
|
551
|
+
...(options.target ? { target: options.target } : {}),
|
|
552
|
+
}, options));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
await app.parseAsync();
|
|
558
|
+
}
|
|
559
|
+
await parseCommands().catch((error) => {
|
|
560
|
+
if (error.code === "commander.helpDisplayed" ||
|
|
561
|
+
error.code === "commander.version" ||
|
|
562
|
+
error.code === "commander.help") {
|
|
563
|
+
process.exitCode = 0;
|
|
564
|
+
if (process.argv.includes("--json"))
|
|
565
|
+
print({
|
|
566
|
+
apiVersion: "ingestron.operation/v1",
|
|
567
|
+
operation: error.code === "commander.version" ? "version" : "help",
|
|
568
|
+
ok: true,
|
|
569
|
+
result: { text: helpText },
|
|
570
|
+
diagnostics: [],
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
print({
|
|
575
|
+
apiVersion: "ingestron.operation/v1",
|
|
576
|
+
operation: "command",
|
|
577
|
+
ok: false,
|
|
578
|
+
diagnostics: [
|
|
579
|
+
{
|
|
580
|
+
code: error instanceof Problem ? error.code : "COMMAND",
|
|
581
|
+
message: error instanceof Error
|
|
582
|
+
? error.message.replace(/^error: /, "")
|
|
583
|
+
: "Command failed",
|
|
584
|
+
...(error instanceof Problem && error.hint ? { hint: error.hint } : {}),
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Keep package-network work off the presentation thread so progress reflects
|
|
2
|
+
* actual pending work. This worker uses the same JSON operations as MCP/Web. */
|
|
3
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
4
|
+
import { execute } from "@ingestron/core";
|
|
5
|
+
parentPort.postMessage(execute(workerData.context, workerData.operation, workerData.args));
|