@commandgarden/cli 0.1.0 → 0.2.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/dist/main.js +221 -37
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -7,7 +7,7 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/main.ts
|
|
9
9
|
import { Command } from "commander";
|
|
10
|
-
import { join as join2, dirname
|
|
10
|
+
import { join as join2, dirname } from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
|
|
@@ -58,6 +58,54 @@ var DaemonClient = class {
|
|
|
58
58
|
}
|
|
59
59
|
return data;
|
|
60
60
|
}
|
|
61
|
+
async connectSSE(path, onEvent, signal) {
|
|
62
|
+
const resp = await this.rawFetch(path, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: {
|
|
65
|
+
"Authorization": `Bearer ${this.token}`,
|
|
66
|
+
"X-CommandGarden": "1",
|
|
67
|
+
"Accept": "text/event-stream"
|
|
68
|
+
},
|
|
69
|
+
signal
|
|
70
|
+
});
|
|
71
|
+
if (!resp.ok || !resp.body) {
|
|
72
|
+
throw new Error(`SSE connection failed: HTTP ${resp.status}`);
|
|
73
|
+
}
|
|
74
|
+
const reader = resp.body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buffer = "";
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
buffer += decoder.decode(value, { stream: true });
|
|
82
|
+
const lines = buffer.split("\n");
|
|
83
|
+
buffer = lines.pop() ?? "";
|
|
84
|
+
let currentEvent = "message";
|
|
85
|
+
let currentData = "";
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.startsWith("event: ")) {
|
|
88
|
+
currentEvent = line.slice(7).trim();
|
|
89
|
+
} else if (line.startsWith("data: ")) {
|
|
90
|
+
currentData = line.slice(6);
|
|
91
|
+
} else if (line === "") {
|
|
92
|
+
if (currentData) {
|
|
93
|
+
try {
|
|
94
|
+
onEvent(currentEvent, JSON.parse(currentData));
|
|
95
|
+
} catch {
|
|
96
|
+
onEvent(currentEvent, currentData);
|
|
97
|
+
}
|
|
98
|
+
currentData = "";
|
|
99
|
+
currentEvent = "message";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (signal?.aborted) return;
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
61
109
|
rawFetch(path, init) {
|
|
62
110
|
return fetch(`${this.baseUrl}${path}`, init);
|
|
63
111
|
}
|
|
@@ -79,6 +127,9 @@ function parseDuration(input) {
|
|
|
79
127
|
return new Date(Date.now() - value * UNITS[unit]);
|
|
80
128
|
}
|
|
81
129
|
|
|
130
|
+
// src/commands/run.ts
|
|
131
|
+
import { createInterface } from "readline";
|
|
132
|
+
|
|
82
133
|
// src/formatters.ts
|
|
83
134
|
import Table from "cli-table3";
|
|
84
135
|
function formatTable(data, columns) {
|
|
@@ -138,6 +189,30 @@ function parseConnectorArgs(rawArgs) {
|
|
|
138
189
|
}
|
|
139
190
|
return result;
|
|
140
191
|
}
|
|
192
|
+
function promptApproval(request) {
|
|
193
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
194
|
+
let cancelled = false;
|
|
195
|
+
const cancel = () => {
|
|
196
|
+
cancelled = true;
|
|
197
|
+
rl.close();
|
|
198
|
+
};
|
|
199
|
+
const prompt = `
|
|
200
|
+
\u26A0 Step ${request.stepIndex + 1} [${request.stepType}] in ${request.connectorKey} requires approval.
|
|
201
|
+
Capability: ${request.capability}
|
|
202
|
+
${request.description}
|
|
203
|
+
Approve? (y/n): `;
|
|
204
|
+
const promise = new Promise((resolve) => {
|
|
205
|
+
rl.question(prompt, (answer) => {
|
|
206
|
+
rl.close();
|
|
207
|
+
if (cancelled) return;
|
|
208
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
209
|
+
});
|
|
210
|
+
rl.on("close", () => {
|
|
211
|
+
if (cancelled) resolve(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return { promise, cancel };
|
|
215
|
+
}
|
|
141
216
|
async function executeRun(client, connector, args, outputFormat) {
|
|
142
217
|
try {
|
|
143
218
|
const resp = await client.post("/api/run", {
|
|
@@ -145,10 +220,57 @@ async function executeRun(client, connector, args, outputFormat) {
|
|
|
145
220
|
args,
|
|
146
221
|
format: outputFormat
|
|
147
222
|
});
|
|
148
|
-
if (!resp.
|
|
149
|
-
|
|
223
|
+
if (!resp.requiresApproval) {
|
|
224
|
+
if (!resp.ok) {
|
|
225
|
+
return `Error: ${resp.error ?? "Unknown error from extension"} (${resp.durationMs}ms)`;
|
|
226
|
+
}
|
|
227
|
+
return format(resp.data, resp.columns, connector, outputFormat);
|
|
150
228
|
}
|
|
151
|
-
|
|
229
|
+
const requestId = resp.requestId;
|
|
230
|
+
process.stderr.write(`Pipeline requires approval. Waiting for steps...
|
|
231
|
+
`);
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
const abortController = new AbortController();
|
|
234
|
+
let activePrompt = null;
|
|
235
|
+
client.connectSSE(
|
|
236
|
+
`/api/run/events/${requestId}`,
|
|
237
|
+
(event, data) => {
|
|
238
|
+
if (event === "approval") {
|
|
239
|
+
const approvalReq = data;
|
|
240
|
+
activePrompt = promptApproval(approvalReq);
|
|
241
|
+
activePrompt.promise.then(async (approved) => {
|
|
242
|
+
activePrompt = null;
|
|
243
|
+
try {
|
|
244
|
+
await client.post("/api/approval", {
|
|
245
|
+
approvalId: approvalReq.approvalId,
|
|
246
|
+
approved
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
if (!approved) {
|
|
251
|
+
abortController.abort();
|
|
252
|
+
resolve(`Aborted: Step ${approvalReq.stepIndex + 1} [${approvalReq.stepType}] was rejected.`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
} else if (event === "result") {
|
|
256
|
+
if (activePrompt) {
|
|
257
|
+
activePrompt.cancel();
|
|
258
|
+
activePrompt = null;
|
|
259
|
+
process.stderr.write("\n (approved via extension)\n");
|
|
260
|
+
}
|
|
261
|
+
abortController.abort();
|
|
262
|
+
const result = data;
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
resolve(`Error: ${result.error ?? "Unknown error"} (${result.durationMs}ms)`);
|
|
265
|
+
} else {
|
|
266
|
+
resolve(format(result.data, result.columns ?? [], connector, outputFormat));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
abortController.signal
|
|
271
|
+
).catch(() => {
|
|
272
|
+
});
|
|
273
|
+
});
|
|
152
274
|
} catch (err) {
|
|
153
275
|
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
154
276
|
}
|
|
@@ -4603,6 +4725,7 @@ function buildQueryString(filter) {
|
|
|
4603
4725
|
const params = new URLSearchParams();
|
|
4604
4726
|
if (filter.since) params.set("since", filter.since);
|
|
4605
4727
|
if (filter.connector) params.set("connector", filter.connector);
|
|
4728
|
+
if (filter.type) params.set("type", filter.type);
|
|
4606
4729
|
if (filter.limit) params.set("limit", String(filter.limit));
|
|
4607
4730
|
const qs = params.toString();
|
|
4608
4731
|
return qs ? `?${qs}` : "";
|
|
@@ -4614,7 +4737,7 @@ async function executeAuditList(client, filter) {
|
|
|
4614
4737
|
);
|
|
4615
4738
|
if (resp.events.length === 0) return "No audit events found.";
|
|
4616
4739
|
const table = new Table3({
|
|
4617
|
-
head: ["Timestamp", "Type", "Connector", "User", "Duration", "Rows", "Error"],
|
|
4740
|
+
head: ["Timestamp", "Type", "Connector", "User", "Duration", "Rows", "Source", "Error"],
|
|
4618
4741
|
style: { head: ["cyan"] }
|
|
4619
4742
|
});
|
|
4620
4743
|
for (const e of resp.events) {
|
|
@@ -4625,6 +4748,7 @@ async function executeAuditList(client, filter) {
|
|
|
4625
4748
|
e.user,
|
|
4626
4749
|
`${e.durationMs}ms`,
|
|
4627
4750
|
e.rowCount?.toString() ?? "-",
|
|
4751
|
+
e.source ?? "-",
|
|
4628
4752
|
e.error ?? e.denialReason ?? ""
|
|
4629
4753
|
]);
|
|
4630
4754
|
}
|
|
@@ -4633,7 +4757,24 @@ async function executeAuditList(client, filter) {
|
|
|
4633
4757
|
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
4634
4758
|
}
|
|
4635
4759
|
}
|
|
4636
|
-
var AUDIT_COLUMNS = [
|
|
4760
|
+
var AUDIT_COLUMNS = [
|
|
4761
|
+
"id",
|
|
4762
|
+
"timestamp",
|
|
4763
|
+
"type",
|
|
4764
|
+
"user",
|
|
4765
|
+
"connector",
|
|
4766
|
+
"durationMs",
|
|
4767
|
+
"rowCount",
|
|
4768
|
+
"error",
|
|
4769
|
+
"denialReason",
|
|
4770
|
+
"correlationId",
|
|
4771
|
+
"connectorHash",
|
|
4772
|
+
"source",
|
|
4773
|
+
"args",
|
|
4774
|
+
"domains",
|
|
4775
|
+
"capabilities",
|
|
4776
|
+
"steps"
|
|
4777
|
+
];
|
|
4637
4778
|
async function executeAuditExport(client, filter, format2) {
|
|
4638
4779
|
try {
|
|
4639
4780
|
const resp = await client.get(
|
|
@@ -4646,8 +4787,15 @@ async function executeAuditExport(client, filter, format2) {
|
|
|
4646
4787
|
const rows = resp.events.map(
|
|
4647
4788
|
(e) => AUDIT_COLUMNS.map((col) => {
|
|
4648
4789
|
const val = e[col];
|
|
4649
|
-
|
|
4650
|
-
|
|
4790
|
+
let str;
|
|
4791
|
+
if (val === null || val === void 0) {
|
|
4792
|
+
str = "";
|
|
4793
|
+
} else if (typeof val === "object") {
|
|
4794
|
+
str = JSON.stringify(val);
|
|
4795
|
+
} else {
|
|
4796
|
+
str = String(val);
|
|
4797
|
+
}
|
|
4798
|
+
return str.includes(",") || str.includes('"') || str.includes("\n") ? `"${str.replace(/"/g, '""')}"` : str;
|
|
4651
4799
|
}).join(",")
|
|
4652
4800
|
);
|
|
4653
4801
|
return [header, ...rows].join("\n") + "\n";
|
|
@@ -4655,12 +4803,54 @@ async function executeAuditExport(client, filter, format2) {
|
|
|
4655
4803
|
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
4656
4804
|
}
|
|
4657
4805
|
}
|
|
4806
|
+
async function executeAuditShow(client, id) {
|
|
4807
|
+
try {
|
|
4808
|
+
const resp = await client.get(`/api/audit/${id}`);
|
|
4809
|
+
const e = resp.event;
|
|
4810
|
+
const lines = [
|
|
4811
|
+
`Event: ${e.id}`,
|
|
4812
|
+
`Type: ${e.type}`,
|
|
4813
|
+
`Connector: ${e.connector}${e.connectorHash ? ` (hash: ${e.connectorHash})` : ""}`,
|
|
4814
|
+
`Correlation: ${e.correlationId ?? "-"}`,
|
|
4815
|
+
`User: ${e.user}`,
|
|
4816
|
+
`Timestamp: ${e.timestamp.replace("T", " ").slice(0, 19)}`,
|
|
4817
|
+
`Duration: ${e.durationMs}ms`
|
|
4818
|
+
];
|
|
4819
|
+
if (e.rowCount !== void 0) lines.push(`Rows: ${e.rowCount}`);
|
|
4820
|
+
if (e.error) lines.push(`Error: ${e.error}`);
|
|
4821
|
+
if (e.denialReason) lines.push(`Denial: ${e.denialReason}`);
|
|
4822
|
+
if (e.source) lines.push(`Source: ${e.source}`);
|
|
4823
|
+
if (e.previousValue !== void 0) lines.push(`Previous: ${e.previousValue}`);
|
|
4824
|
+
if (e.newValue !== void 0) lines.push(`New: ${e.newValue}`);
|
|
4825
|
+
if (Object.keys(e.args).length > 0) {
|
|
4826
|
+
lines.push(`Args: ${JSON.stringify(e.args)}`);
|
|
4827
|
+
}
|
|
4828
|
+
if (e.steps && e.steps.length > 0) {
|
|
4829
|
+
lines.push("");
|
|
4830
|
+
lines.push("Pipeline Steps:");
|
|
4831
|
+
const stepTable = new Table3({
|
|
4832
|
+
head: ["#", "Step", "Capability", "Duration", "Error"],
|
|
4833
|
+
style: { head: ["cyan"] }
|
|
4834
|
+
});
|
|
4835
|
+
for (const s of e.steps) {
|
|
4836
|
+
stepTable.push([
|
|
4837
|
+
s.index + 1,
|
|
4838
|
+
s.step,
|
|
4839
|
+
s.capability ?? "-",
|
|
4840
|
+
`${s.durationMs}ms`,
|
|
4841
|
+
s.error ?? ""
|
|
4842
|
+
]);
|
|
4843
|
+
}
|
|
4844
|
+
lines.push(stepTable.toString());
|
|
4845
|
+
}
|
|
4846
|
+
return lines.join("\n");
|
|
4847
|
+
} catch (err) {
|
|
4848
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4658
4851
|
|
|
4659
4852
|
// src/commands/config-cmd.ts
|
|
4660
|
-
import { readFileSync as readFileSync4,
|
|
4661
|
-
import { dirname } from "path";
|
|
4662
|
-
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
4663
|
-
var VALID_SECTIONS = ["daemon", "security", "connectors", "audit", "output"];
|
|
4853
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
4664
4854
|
function executeConfigShow(configPath) {
|
|
4665
4855
|
if (!existsSync2(configPath)) {
|
|
4666
4856
|
return [
|
|
@@ -4671,31 +4861,18 @@ function executeConfigShow(configPath) {
|
|
|
4671
4861
|
}
|
|
4672
4862
|
return readFileSync4(configPath, "utf-8");
|
|
4673
4863
|
}
|
|
4674
|
-
function executeConfigSet(
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
return `
|
|
4678
|
-
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
const raw = readFileSync4(configPath, "utf-8");
|
|
4682
|
-
config2 = parseYaml2(raw) ?? {};
|
|
4683
|
-
}
|
|
4684
|
-
const [section, prop] = parts;
|
|
4685
|
-
if (!config2[section]) config2[section] = {};
|
|
4686
|
-
let parsed = value;
|
|
4687
|
-
if (value === "true") parsed = true;
|
|
4688
|
-
else if (value === "false") parsed = false;
|
|
4689
|
-
else if (!isNaN(Number(value)) && value !== "") parsed = Number(value);
|
|
4690
|
-
config2[section][prop] = parsed;
|
|
4691
|
-
mkdirSync2(dirname(configPath), { recursive: true });
|
|
4692
|
-
writeFileSync2(configPath, stringifyYaml(config2), "utf-8");
|
|
4693
|
-
return `Set ${key} = ${value}`;
|
|
4864
|
+
async function executeConfigSet(client, key, value) {
|
|
4865
|
+
try {
|
|
4866
|
+
await client.post("/api/config", { key, value });
|
|
4867
|
+
return `Set ${key} = ${value}`;
|
|
4868
|
+
} catch (err) {
|
|
4869
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
4870
|
+
}
|
|
4694
4871
|
}
|
|
4695
4872
|
|
|
4696
4873
|
// src/main.ts
|
|
4697
4874
|
var __filename = fileURLToPath(import.meta.url);
|
|
4698
|
-
var __dirname =
|
|
4875
|
+
var __dirname = dirname(__filename);
|
|
4699
4876
|
var DAEMON_SCRIPT = join2(__dirname, "..", "..", "daemon", "dist", "main.js");
|
|
4700
4877
|
var CG_HOME = join2(homedir(), ".commandgarden");
|
|
4701
4878
|
var TOKEN_PATH = join2(CG_HOME, "session-token");
|
|
@@ -4739,26 +4916,33 @@ daemon.command("stop").description("Stop the daemon").action(async () => {
|
|
|
4739
4916
|
console.log(await executeDaemonStop(CG_HOME));
|
|
4740
4917
|
});
|
|
4741
4918
|
var audit = program.command("audit").description("View audit event log");
|
|
4742
|
-
audit.command("list").description("List recent audit events").option("--since <duration>", "time window, e.g. 7d, 2w, 12h").option("--connector <pattern>", "filter by connector pattern, e.g. test/*").option("--limit <n>", "max events to return", "100").action(async (opts) => {
|
|
4919
|
+
audit.command("list").description("List recent audit events").option("--since <duration>", "time window, e.g. 7d, 2w, 12h").option("--connector <pattern>", "filter by connector pattern, e.g. test/*").option("--type <pattern>", "filter by event type, e.g. auth.failed, command.*").option("--limit <n>", "max events to return", "100").action(async (opts) => {
|
|
4743
4920
|
const client = createClient();
|
|
4744
4921
|
const filter = {};
|
|
4745
4922
|
if (opts.since) filter.since = parseDuration(opts.since).toISOString();
|
|
4746
4923
|
if (opts.connector) filter.connector = opts.connector;
|
|
4924
|
+
if (opts.type) filter.type = opts.type;
|
|
4747
4925
|
filter.limit = parseInt(opts.limit, 10);
|
|
4748
4926
|
console.log(await executeAuditList(client, filter));
|
|
4749
4927
|
});
|
|
4750
|
-
audit.command("export").description("Export audit events").option("--format <format>", "export format: json, csv", "json").option("--since <duration>", "time window, e.g. 7d, 30d").option("--connector <pattern>", "filter by connector pattern").action(async (opts) => {
|
|
4928
|
+
audit.command("export").description("Export audit events").option("--format <format>", "export format: json, csv", "json").option("--since <duration>", "time window, e.g. 7d, 30d").option("--connector <pattern>", "filter by connector pattern").option("--type <pattern>", "filter by event type").action(async (opts) => {
|
|
4751
4929
|
const client = createClient();
|
|
4752
4930
|
const filter = {};
|
|
4753
4931
|
if (opts.since) filter.since = parseDuration(opts.since).toISOString();
|
|
4754
4932
|
if (opts.connector) filter.connector = opts.connector;
|
|
4933
|
+
if (opts.type) filter.type = opts.type;
|
|
4755
4934
|
console.log(await executeAuditExport(client, filter, opts.format));
|
|
4756
4935
|
});
|
|
4936
|
+
audit.command("show <id>").description("Show full details of an audit event").action(async (id) => {
|
|
4937
|
+
const client = createClient();
|
|
4938
|
+
console.log(await executeAuditShow(client, id));
|
|
4939
|
+
});
|
|
4757
4940
|
var config = program.command("config").description("Manage daemon configuration");
|
|
4758
4941
|
config.command("show").description("Show current configuration").action(() => {
|
|
4759
4942
|
console.log(executeConfigShow(CONFIG_PATH));
|
|
4760
4943
|
});
|
|
4761
|
-
config.command("set <key> <value>").description("Set a configuration value (e.g. daemon.port 9999)").action((key, value) => {
|
|
4762
|
-
|
|
4944
|
+
config.command("set <key> <value>").description("Set a configuration value (e.g. daemon.port 9999)").action(async (key, value) => {
|
|
4945
|
+
const client = createClient();
|
|
4946
|
+
console.log(await executeConfigSet(client, key, value));
|
|
4763
4947
|
});
|
|
4764
4948
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commandgarden/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Enterprise browser automation CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/main.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"commandgarden": "
|
|
10
|
-
"cg": "
|
|
9
|
+
"commandgarden": "dist/main.js",
|
|
10
|
+
"cg": "dist/main.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist"
|