@driftgate/cli 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,1293 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { randomUUID } from "node:crypto";
8
+ import {
9
+ DriftGateClient,
10
+ DriftGateError,
11
+ type DriftGateCanonicalResponse,
12
+ type DriftGateEphemeralExecuteInput,
13
+ type DriftGateSessionExecuteInput,
14
+ type DriftGateSessionStartInput
15
+ } from "@driftgate/sdk";
16
+ import { compileWorkflowYaml } from "@driftgate/workflow-compiler";
17
+
18
+ type CliConfig = {
19
+ baseUrl: string;
20
+ sessionToken?: string;
21
+ apiKey?: string;
22
+ expiresAt?: string;
23
+ };
24
+
25
+ type ParsedArgs = {
26
+ positionals: string[];
27
+ options: Record<string, string | true>;
28
+ };
29
+
30
+ const REQUIRED_V4_ERROR_CODES = new Set([
31
+ "AUTH_INVALID",
32
+ "POLICY_DENIED",
33
+ "RISK_EXCEEDED",
34
+ "ROUTE_UNAVAILABLE",
35
+ "TOOL_BLOCKED",
36
+ "RATE_LIMITED",
37
+ "TIMEOUT",
38
+ "INTERNAL",
39
+ "INVALID_REQUEST"
40
+ ]);
41
+
42
+ async function main(): Promise<void> {
43
+ const [, , command, ...rest] = process.argv;
44
+
45
+ switch (command) {
46
+ case "login":
47
+ await handleLogin(rest);
48
+ return;
49
+ case "init":
50
+ await handleInit(rest);
51
+ return;
52
+ case "deploy":
53
+ await handleDeploy(rest);
54
+ return;
55
+ case "publish":
56
+ await handlePublish(rest);
57
+ return;
58
+ case "session":
59
+ await handleSession(rest);
60
+ return;
61
+ case "execute":
62
+ await handleExecute(rest);
63
+ return;
64
+ case "execution":
65
+ await handleExecution(rest);
66
+ return;
67
+ case "run":
68
+ throw deprecatedCommandError("run", "driftgate session execute <sessionId> --input '{...}'");
69
+ case "status":
70
+ throw deprecatedCommandError("status", "driftgate execution status <executionId>");
71
+ case "approvals":
72
+ await handleApprovals(rest);
73
+ return;
74
+ case "connectors":
75
+ await handleConnectors(rest);
76
+ return;
77
+ case "secrets":
78
+ await handleSecrets(rest);
79
+ return;
80
+ case "webhooks":
81
+ await handleWebhooks(rest);
82
+ return;
83
+ case "help":
84
+ case "--help":
85
+ case "-h":
86
+ case undefined:
87
+ printHelp();
88
+ return;
89
+ default:
90
+ throw new Error(`unknown command: ${command}`);
91
+ }
92
+ }
93
+
94
+ function parseArgs(input: string[]): ParsedArgs {
95
+ const positionals: string[] = [];
96
+ const options: Record<string, string | true> = {};
97
+
98
+ for (let index = 0; index < input.length; index += 1) {
99
+ const item = input[index];
100
+ if (!item.startsWith("--")) {
101
+ positionals.push(item);
102
+ continue;
103
+ }
104
+
105
+ const key = item.slice(2);
106
+ const next = input[index + 1];
107
+ if (!next || next.startsWith("--")) {
108
+ options[key] = true;
109
+ continue;
110
+ }
111
+
112
+ options[key] = next;
113
+ index += 1;
114
+ }
115
+
116
+ return { positionals, options };
117
+ }
118
+
119
+ function resolveBaseUrl(args: ParsedArgs, config?: CliConfig): string {
120
+ const option = args.options["api-base"];
121
+ if (typeof option === "string" && option.length > 0) {
122
+ return option.replace(/\/$/, "");
123
+ }
124
+ const envBase = process.env.DG_API_BASE ?? process.env.DRIFTGATE_API_BASE;
125
+ if (envBase && envBase.length > 0) {
126
+ return envBase.replace(/\/$/, "");
127
+ }
128
+ if (config?.baseUrl) {
129
+ return config.baseUrl.replace(/\/$/, "");
130
+ }
131
+ return "http://127.0.0.1:3001";
132
+ }
133
+
134
+ function getConfigDir(): string {
135
+ const custom = process.env.DRIFTGATE_CLI_CONFIG_DIR;
136
+ if (custom && custom.length > 0) {
137
+ return custom;
138
+ }
139
+ const home = process.env.HOME;
140
+ if (!home) {
141
+ throw new Error("HOME is required for CLI config storage");
142
+ }
143
+ return path.join(home, ".config", "driftgate");
144
+ }
145
+
146
+ function getConfigPath(): string {
147
+ return path.join(getConfigDir(), "credentials.json");
148
+ }
149
+
150
+ async function loadConfig(): Promise<CliConfig | null> {
151
+ const configPath = getConfigPath();
152
+ if (!existsSync(configPath)) {
153
+ return null;
154
+ }
155
+
156
+ const raw = await readFile(configPath, "utf8");
157
+ return JSON.parse(raw) as CliConfig;
158
+ }
159
+
160
+ async function saveConfig(config: CliConfig): Promise<void> {
161
+ const configDir = getConfigDir();
162
+ await mkdir(configDir, { recursive: true });
163
+ await writeFile(getConfigPath(), JSON.stringify(config, null, 2));
164
+ }
165
+
166
+ function buildClient(baseUrl: string, config: CliConfig | null): DriftGateClient {
167
+ const apiKey = process.env.DRIFTGATE_API_KEY ?? config?.apiKey;
168
+ const sessionToken = apiKey ? undefined : process.env.DRIFTGATE_SESSION_TOKEN ?? config?.sessionToken;
169
+
170
+ if (!apiKey && !sessionToken) {
171
+ throw new Error("no credentials found; run `driftgate login` or set DRIFTGATE_API_KEY");
172
+ }
173
+
174
+ return new DriftGateClient({
175
+ baseUrl,
176
+ apiKey,
177
+ sessionToken
178
+ });
179
+ }
180
+
181
+ function requireWorkspaceId(args: ParsedArgs, usage: string): string {
182
+ const workspaceId =
183
+ (typeof args.options.workspace === "string" && args.options.workspace) ||
184
+ process.env.DRIFTGATE_WORKSPACE_ID;
185
+ if (!workspaceId) {
186
+ throw new Error(usage);
187
+ }
188
+ return workspaceId;
189
+ }
190
+
191
+ function parseBooleanValue(value: string | true | undefined, label: string): boolean | undefined {
192
+ if (value === undefined) {
193
+ return undefined;
194
+ }
195
+ if (value === true) {
196
+ return true;
197
+ }
198
+
199
+ const normalized = value.trim().toLowerCase();
200
+ if (["1", "true", "yes", "on"].includes(normalized)) {
201
+ return true;
202
+ }
203
+ if (["0", "false", "no", "off"].includes(normalized)) {
204
+ return false;
205
+ }
206
+
207
+ throw new Error(`${label} must be one of: true|false|1|0|yes|no|on|off`);
208
+ }
209
+
210
+ async function parseJsonObjectOption(
211
+ value: string | true | undefined,
212
+ label: string,
213
+ fallback: Record<string, unknown>
214
+ ): Promise<Record<string, unknown>> {
215
+ if (typeof value !== "string" || value.length === 0) {
216
+ return fallback;
217
+ }
218
+ const raw = value.startsWith("@") ? await readFile(value.slice(1), "utf8") : value;
219
+ const parsed = JSON.parse(raw);
220
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
221
+ throw new Error(`${label} must be a JSON object`);
222
+ }
223
+ return parsed as Record<string, unknown>;
224
+ }
225
+
226
+ function parseNullableConnectorId(value: string | true | undefined): string | null | undefined {
227
+ if (value === undefined || value === true) {
228
+ return undefined;
229
+ }
230
+ if (value.trim().toLowerCase() === "null") {
231
+ return null;
232
+ }
233
+ return value;
234
+ }
235
+
236
+ function optionString(value: string | true | undefined): string | undefined {
237
+ return typeof value === "string" && value.length > 0 ? value : undefined;
238
+ }
239
+
240
+ function deprecatedCommandError(command: string, replacement: string): Error {
241
+ return new Error(`command '${command}' was removed in V4 CLI; use '${replacement}'`);
242
+ }
243
+
244
+ function nowNs(): bigint {
245
+ return process.hrtime.bigint();
246
+ }
247
+
248
+ function elapsedMs(startNs: bigint): number {
249
+ const elapsed = process.hrtime.bigint() - startNs;
250
+ return Number((Number(elapsed) / 1_000_000).toFixed(3));
251
+ }
252
+
253
+ function parseNumberOption(
254
+ value: string | true | undefined,
255
+ label: string,
256
+ { integer = false, minimum }: { integer?: boolean; minimum?: number } = {}
257
+ ): number | undefined {
258
+ const raw = optionString(value);
259
+ if (!raw) {
260
+ return undefined;
261
+ }
262
+ const parsed = Number(raw);
263
+ if (!Number.isFinite(parsed)) {
264
+ throw new Error(`${label} must be a valid number`);
265
+ }
266
+ if (integer && !Number.isInteger(parsed)) {
267
+ throw new Error(`${label} must be an integer`);
268
+ }
269
+ if (minimum !== undefined && parsed < minimum) {
270
+ throw new Error(`${label} must be >= ${minimum}`);
271
+ }
272
+ return parsed;
273
+ }
274
+
275
+ async function parseRequiredInputOption(args: ParsedArgs): Promise<Record<string, unknown>> {
276
+ const rawInput = optionString(args.options.input);
277
+ if (!rawInput) {
278
+ throw new Error("usage: --input '{...}' or --input @input.json is required");
279
+ }
280
+ const source = rawInput.startsWith("@") ? await readFile(rawInput.slice(1), "utf8") : rawInput;
281
+ const parsed = JSON.parse(source);
282
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
283
+ throw new Error("--input must parse to a JSON object");
284
+ }
285
+ return parsed as Record<string, unknown>;
286
+ }
287
+
288
+ async function parsePolicyOption(
289
+ args: ParsedArgs
290
+ ): Promise<DriftGateSessionStartInput["policy"] | undefined> {
291
+ const policyOption = optionString(args.options.policy);
292
+ if (policyOption) {
293
+ const value = await parseJsonObjectOption(policyOption, "--policy", {});
294
+ const ref = typeof value.ref === "string" ? value.ref : undefined;
295
+ const version = typeof value.version === "string" ? value.version : undefined;
296
+ if (!ref || !version) {
297
+ throw new Error("--policy requires JSON object with string fields 'ref' and 'version'");
298
+ }
299
+ return { ref, version };
300
+ }
301
+
302
+ const ref = optionString(args.options["policy-ref"]);
303
+ const version = optionString(args.options["policy-version"]);
304
+ if (!ref && !version) {
305
+ return undefined;
306
+ }
307
+ if (!ref || !version) {
308
+ throw new Error("--policy-ref and --policy-version must be provided together");
309
+ }
310
+ return { ref, version };
311
+ }
312
+
313
+ async function parseRouteOption(
314
+ args: ParsedArgs
315
+ ): Promise<DriftGateSessionStartInput["route"] | undefined> {
316
+ const routeOption = optionString(args.options.route);
317
+ if (routeOption) {
318
+ const value = await parseJsonObjectOption(routeOption, "--route", {});
319
+ const provider = typeof value.provider === "string" ? value.provider : undefined;
320
+ const model = typeof value.model === "string" ? value.model : undefined;
321
+ const region = typeof value.region === "string" ? value.region : undefined;
322
+ if (!provider && !model && !region) {
323
+ throw new Error("--route requires at least one of provider/model/region");
324
+ }
325
+ return { provider, model, region };
326
+ }
327
+
328
+ const provider = optionString(args.options["route-provider"]);
329
+ const model = optionString(args.options["route-model"]);
330
+ const region = optionString(args.options["route-region"]);
331
+ if (!provider && !model && !region) {
332
+ return undefined;
333
+ }
334
+ return { provider, model, region };
335
+ }
336
+
337
+ async function parseRiskOption(
338
+ args: ParsedArgs
339
+ ): Promise<DriftGateSessionStartInput["risk"] | undefined> {
340
+ const riskOption = optionString(args.options.risk);
341
+ if (riskOption) {
342
+ const value = await parseJsonObjectOption(riskOption, "--risk", {});
343
+ const score = typeof value.score === "number" ? value.score : undefined;
344
+ const decision =
345
+ value.decision === "allow" || value.decision === "deny" || value.decision === "review"
346
+ ? value.decision
347
+ : undefined;
348
+ if (score === undefined && decision === undefined) {
349
+ throw new Error("--risk requires at least one of score/decision");
350
+ }
351
+ return { score, decision };
352
+ }
353
+
354
+ const score = parseNumberOption(args.options["risk-score"], "--risk-score");
355
+ const decisionRaw = optionString(args.options["risk-decision"]);
356
+ if (decisionRaw && !["allow", "deny", "review"].includes(decisionRaw)) {
357
+ throw new Error("--risk-decision must be one of: allow|deny|review");
358
+ }
359
+ const decision = decisionRaw as "allow" | "deny" | "review" | undefined;
360
+ if (score === undefined && decision === undefined) {
361
+ return undefined;
362
+ }
363
+ return { score, decision };
364
+ }
365
+
366
+ async function parseV4ExecutionDefaults(
367
+ args: ParsedArgs
368
+ ): Promise<Pick<DriftGateSessionExecuteInput, "policy" | "route" | "risk" | "workflowVersionId">> {
369
+ return {
370
+ policy: await parsePolicyOption(args),
371
+ route: await parseRouteOption(args),
372
+ risk: await parseRiskOption(args),
373
+ workflowVersionId: optionString(args.options["workflow-version-id"])
374
+ };
375
+ }
376
+
377
+ function canonicalOutput<T>(response: DriftGateCanonicalResponse<T>): {
378
+ ok: boolean;
379
+ data: T | null;
380
+ meta: DriftGateCanonicalResponse<T>["meta"];
381
+ error: DriftGateCanonicalResponse<T>["error"];
382
+ } {
383
+ return {
384
+ ok: response.ok,
385
+ data: response.data,
386
+ meta: response.meta,
387
+ error: response.error
388
+ };
389
+ }
390
+
391
+ function printJson(value: unknown): void {
392
+ console.log(JSON.stringify(value, null, 2));
393
+ }
394
+
395
+ function mapStableErrorCode(inputCode: string, status: number): string {
396
+ const normalized = inputCode.trim().toUpperCase();
397
+ if (REQUIRED_V4_ERROR_CODES.has(normalized)) {
398
+ return normalized;
399
+ }
400
+
401
+ switch (normalized) {
402
+ case "FORBIDDEN":
403
+ case "POLICY_DENIED":
404
+ case "ENTITLEMENT_DENIED":
405
+ return "POLICY_DENIED";
406
+ case "UNAUTHORIZED":
407
+ case "AUTH_INVALID":
408
+ return "AUTH_INVALID";
409
+ case "FIREWALL_DENIED":
410
+ case "TOOL_BLOCKED":
411
+ return "TOOL_BLOCKED";
412
+ case "NOT_FOUND":
413
+ return "ROUTE_UNAVAILABLE";
414
+ case "TIMEOUT":
415
+ return "TIMEOUT";
416
+ case "RATE_LIMITED":
417
+ return "RATE_LIMITED";
418
+ case "RISK_EXCEEDED":
419
+ return "RISK_EXCEEDED";
420
+ case "INVALID_REQUEST":
421
+ return "INVALID_REQUEST";
422
+ default:
423
+ break;
424
+ }
425
+
426
+ if (status === 401 || status === 403) {
427
+ return "AUTH_INVALID";
428
+ }
429
+ if (status === 404) {
430
+ return "ROUTE_UNAVAILABLE";
431
+ }
432
+ if (status === 408 || status === 504) {
433
+ return "TIMEOUT";
434
+ }
435
+ if (status === 429) {
436
+ return "RATE_LIMITED";
437
+ }
438
+ return "INTERNAL";
439
+ }
440
+
441
+ function renderErrorEnvelope(error: unknown): {
442
+ ok: false;
443
+ data: null;
444
+ meta: { requestId: string; timingMs: { total: number } };
445
+ error: {
446
+ code: string;
447
+ message: string;
448
+ status: number;
449
+ retryable: boolean;
450
+ details?: Record<string, unknown>;
451
+ };
452
+ } {
453
+ if (error instanceof DriftGateError) {
454
+ const code = mapStableErrorCode(error.code, error.status);
455
+ const retryable =
456
+ code === "RATE_LIMITED" || code === "TIMEOUT" || error.status >= 500 || error.status === 429;
457
+ return {
458
+ ok: false,
459
+ data: null,
460
+ meta: {
461
+ requestId: error.correlationId ?? `cli_${randomUUID()}`,
462
+ timingMs: { total: 0 }
463
+ },
464
+ error: {
465
+ code,
466
+ message: error.message,
467
+ status: error.status,
468
+ retryable,
469
+ ...(error.details && typeof error.details === "object"
470
+ ? { details: error.details as Record<string, unknown> }
471
+ : {})
472
+ }
473
+ };
474
+ }
475
+
476
+ const message = error instanceof Error ? error.message : String(error);
477
+ const isUsageError = message.startsWith("usage:") || message.includes("removed in V4 CLI");
478
+ const status = isUsageError ? 400 : 500;
479
+ const code = isUsageError ? "INVALID_REQUEST" : "INTERNAL";
480
+ return {
481
+ ok: false,
482
+ data: null,
483
+ meta: {
484
+ requestId: `cli_${randomUUID()}`,
485
+ timingMs: { total: 0 }
486
+ },
487
+ error: {
488
+ code,
489
+ message,
490
+ status,
491
+ retryable: false
492
+ }
493
+ };
494
+ }
495
+
496
+ async function handleLogin(rest: string[]): Promise<void> {
497
+ const args = parseArgs(rest);
498
+ const existingConfig = await loadConfig();
499
+ const baseUrl = resolveBaseUrl(args, existingConfig ?? undefined);
500
+
501
+ const directApiKey = process.env.DRIFTGATE_API_KEY;
502
+ if (directApiKey && directApiKey.length > 0) {
503
+ await saveConfig({ baseUrl, apiKey: directApiKey });
504
+ console.log("Saved API key credentials for DriftGate CLI.");
505
+ return;
506
+ }
507
+
508
+ const auth0Domain = process.env.AUTH0_DOMAIN;
509
+ const auth0ClientId = process.env.AUTH0_CLIENT_ID;
510
+ if (!auth0Domain || !auth0ClientId) {
511
+ throw new Error("AUTH0_DOMAIN and AUTH0_CLIENT_ID are required for device-code login");
512
+ }
513
+
514
+ const auth0Audience = process.env.AUTH0_AUDIENCE;
515
+ const deviceCodeResponse = await fetch(`https://${auth0Domain}/oauth/device/code`, {
516
+ method: "POST",
517
+ headers: {
518
+ "content-type": "application/x-www-form-urlencoded"
519
+ },
520
+ body: new URLSearchParams({
521
+ client_id: auth0ClientId,
522
+ scope: "openid profile email offline_access",
523
+ ...(auth0Audience ? { audience: auth0Audience } : {})
524
+ })
525
+ });
526
+
527
+ if (!deviceCodeResponse.ok) {
528
+ const body = await deviceCodeResponse.text();
529
+ throw new Error(`device-code start failed (${deviceCodeResponse.status}): ${body}`);
530
+ }
531
+
532
+ const deviceBody = (await deviceCodeResponse.json()) as {
533
+ device_code: string;
534
+ user_code: string;
535
+ verification_uri: string;
536
+ verification_uri_complete?: string;
537
+ expires_in: number;
538
+ interval?: number;
539
+ };
540
+
541
+ console.log("Complete DriftGate login in your browser:");
542
+ console.log(deviceBody.verification_uri_complete ?? deviceBody.verification_uri);
543
+ console.log(`User code: ${deviceBody.user_code}`);
544
+
545
+ const tokenEndpoint = `https://${auth0Domain}/oauth/token`;
546
+ const pollIntervalMs = (deviceBody.interval ?? 5) * 1_000;
547
+ const timeoutAt = Date.now() + deviceBody.expires_in * 1_000;
548
+
549
+ let idToken: string | null = null;
550
+ while (Date.now() < timeoutAt) {
551
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
552
+ const tokenResponse = await fetch(tokenEndpoint, {
553
+ method: "POST",
554
+ headers: {
555
+ "content-type": "application/x-www-form-urlencoded"
556
+ },
557
+ body: new URLSearchParams({
558
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
559
+ device_code: deviceBody.device_code,
560
+ client_id: auth0ClientId
561
+ })
562
+ });
563
+
564
+ const tokenJson = (await tokenResponse.json()) as {
565
+ access_token?: string;
566
+ id_token?: string;
567
+ error?: string;
568
+ error_description?: string;
569
+ };
570
+
571
+ if (tokenResponse.ok) {
572
+ idToken = tokenJson.id_token ?? null;
573
+ break;
574
+ }
575
+
576
+ if (tokenJson.error === "authorization_pending" || tokenJson.error === "slow_down") {
577
+ continue;
578
+ }
579
+
580
+ throw new Error(
581
+ `device-code login failed: ${tokenJson.error ?? "unknown_error"} ${tokenJson.error_description ?? ""}`
582
+ );
583
+ }
584
+
585
+ if (!idToken) {
586
+ throw new Error("device-code login timed out or missing id_token in token response");
587
+ }
588
+
589
+ const exchangeResponse = await fetch(`${baseUrl}/v1/auth/session/exchange`, {
590
+ method: "POST",
591
+ headers: {
592
+ "content-type": "application/json"
593
+ },
594
+ body: JSON.stringify({ idToken })
595
+ });
596
+ const exchangeBody = await exchangeResponse.json();
597
+
598
+ if (!exchangeResponse.ok || typeof exchangeBody.sessionToken !== "string") {
599
+ throw new Error(`session exchange failed (${exchangeResponse.status}): ${JSON.stringify(exchangeBody)}`);
600
+ }
601
+
602
+ await saveConfig({
603
+ baseUrl,
604
+ sessionToken: exchangeBody.sessionToken,
605
+ expiresAt: typeof exchangeBody.expiresAt === "string" ? exchangeBody.expiresAt : undefined
606
+ });
607
+ console.log("Device-code login successful. Session token stored for DriftGate CLI.");
608
+ }
609
+
610
+ async function handleInit(rest: string[]): Promise<void> {
611
+ const args = parseArgs(rest);
612
+ const outputPath =
613
+ (typeof args.options["out"] === "string" && args.options["out"]) || "workflow.yaml";
614
+
615
+ if (existsSync(outputPath) && args.options.force !== true) {
616
+ throw new Error(`${outputPath} already exists. Pass --force to overwrite.`);
617
+ }
618
+
619
+ const template = `apiVersion: driftgate.ai/v1
620
+ kind: Workflow
621
+ metadata:
622
+ name: starter-workflow
623
+ workspace: workspace_id_here
624
+ spec:
625
+ governance:
626
+ policyBindings: []
627
+ slaBindings: []
628
+ nodes:
629
+ - id: intake
630
+ type: http
631
+ config:
632
+ method: POST
633
+ path: /intake
634
+ - id: complete
635
+ type: task
636
+ config: {}
637
+ edges:
638
+ - from: intake
639
+ to: complete
640
+ `;
641
+
642
+ await writeFile(outputPath, template, "utf8");
643
+ console.log(`Created ${outputPath}`);
644
+ }
645
+
646
+ async function handleDeploy(rest: string[]): Promise<void> {
647
+ const args = parseArgs(rest);
648
+ const [yamlPath] = args.positionals;
649
+ if (!yamlPath) {
650
+ throw new Error("usage: driftgate deploy <workflow.yaml> [--workspace <workspaceId>] [--project <name>] [--workflow <name>]");
651
+ }
652
+
653
+ const config = await loadConfig();
654
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
655
+ const client = buildClient(baseUrl, config);
656
+
657
+ const workflowYaml = await readFile(yamlPath, "utf8");
658
+ const compiled = compileWorkflowYaml(workflowYaml);
659
+ const workspaceId =
660
+ (typeof args.options.workspace === "string" && args.options.workspace) ||
661
+ process.env.DRIFTGATE_WORKSPACE_ID;
662
+ if (!workspaceId) {
663
+ throw new Error("workspace id is required (--workspace or DRIFTGATE_WORKSPACE_ID)");
664
+ }
665
+
666
+ const response = await client.deployWorkflow({
667
+ workspaceId,
668
+ projectName:
669
+ (typeof args.options.project === "string" && args.options.project) || compiled.workflow.metadata.name,
670
+ workflowName:
671
+ (typeof args.options.workflow === "string" && args.options.workflow) || compiled.workflow.metadata.name,
672
+ workflowYaml
673
+ });
674
+
675
+ console.log(
676
+ JSON.stringify(
677
+ {
678
+ workflowId: response.workflow.id,
679
+ projectId: response.project.id,
680
+ draftVersion: response.draft.version,
681
+ checksum: response.compile.checksum,
682
+ mutationNodeIds: response.compile.mutationNodeIds
683
+ },
684
+ null,
685
+ 2
686
+ )
687
+ );
688
+ }
689
+
690
+ async function handlePublish(rest: string[]): Promise<void> {
691
+ const args = parseArgs(rest);
692
+ const [workflowId] = args.positionals;
693
+ if (!workflowId) {
694
+ throw new Error("usage: driftgate publish <workflowId> [--yaml <workflow.yaml>]");
695
+ }
696
+
697
+ const config = await loadConfig();
698
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
699
+ const client = buildClient(baseUrl, config);
700
+
701
+ const yamlPath = typeof args.options.yaml === "string" ? args.options.yaml : null;
702
+ const workflowYaml = yamlPath ? await readFile(yamlPath, "utf8") : undefined;
703
+ const version = await client.publishWorkflow(workflowId, workflowYaml);
704
+
705
+ console.log(JSON.stringify(version, null, 2));
706
+ }
707
+
708
+ async function handleSession(rest: string[]): Promise<void> {
709
+ const [subcommand, ...tail] = rest;
710
+ switch (subcommand) {
711
+ case "start":
712
+ await handleSessionStart(tail);
713
+ return;
714
+ case "execute":
715
+ await handleSessionExecute(tail);
716
+ return;
717
+ default:
718
+ throw new Error(
719
+ "usage: driftgate session start --agent <agent> [--workspace <workspaceId>] [--metadata '{...}'] [--policy '{...}'] [--route '{...}'] [--risk '{...}'] [--workflow-version-id <id>] [--expires-at <ISO-8601>] | driftgate session execute <sessionId> --input '{...}' [--policy '{...}'] [--route '{...}'] [--risk '{...}'] [--workflow-version-id <id>]"
720
+ );
721
+ }
722
+ }
723
+
724
+ async function handleSessionStart(rest: string[]): Promise<void> {
725
+ const args = parseArgs(rest);
726
+ const config = await loadConfig();
727
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
728
+ const client = buildClient(baseUrl, config);
729
+ const agent = optionString(args.options.agent);
730
+ if (!agent) {
731
+ throw new Error(
732
+ "usage: driftgate session start --agent <agent> [--workspace <workspaceId>] [--metadata '{...}'] [--policy '{...}'] [--route '{...}'] [--risk '{...}'] [--workflow-version-id <id>] [--expires-at <ISO-8601>]"
733
+ );
734
+ }
735
+
736
+ const metadata =
737
+ typeof args.options.metadata === "string"
738
+ ? await parseJsonObjectOption(args.options.metadata, "--metadata", {})
739
+ : undefined;
740
+ const workspaceId = optionString(args.options.workspace) ?? process.env.DRIFTGATE_WORKSPACE_ID;
741
+ const input: DriftGateSessionStartInput = {
742
+ ...(workspaceId ? { workspaceId } : {}),
743
+ agent,
744
+ ...(optionString(args.options.subject) ? { subject: optionString(args.options.subject) } : {}),
745
+ ...(metadata ? { metadata } : {}),
746
+ ...(await parseV4ExecutionDefaults(args)),
747
+ ...(optionString(args.options["expires-at"]) ? { expiresAt: optionString(args.options["expires-at"]) } : {})
748
+ };
749
+
750
+ const session = await client.session.start(input);
751
+ printJson(canonicalOutput(session.startEnvelope));
752
+ }
753
+
754
+ async function handleSessionExecute(rest: string[]): Promise<void> {
755
+ const args = parseArgs(rest);
756
+ const [sessionId] = args.positionals;
757
+ if (!sessionId) {
758
+ throw new Error(
759
+ "usage: driftgate session execute <sessionId> --input '{...}' [--policy '{...}'] [--route '{...}'] [--risk '{...}'] [--workflow-version-id <id>]"
760
+ );
761
+ }
762
+ const config = await loadConfig();
763
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
764
+ const client = buildClient(baseUrl, config);
765
+ const input: DriftGateSessionExecuteInput = {
766
+ input: await parseRequiredInputOption(args),
767
+ ...(await parseV4ExecutionDefaults(args))
768
+ };
769
+ const response = await client.executeSession(sessionId, input);
770
+ printJson(canonicalOutput(response));
771
+ }
772
+
773
+ async function handleExecute(rest: string[]): Promise<void> {
774
+ const args = parseArgs(rest);
775
+ const config = await loadConfig();
776
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
777
+ const client = buildClient(baseUrl, config);
778
+ const agent = optionString(args.options.agent);
779
+ if (!agent) {
780
+ throw new Error(
781
+ "usage: driftgate execute --agent <agent> --input '{...}' [--workspace <workspaceId>] [--metadata '{...}'] [--policy '{...}'] [--route '{...}'] [--risk '{...}'] [--workflow-version-id <id>]"
782
+ );
783
+ }
784
+ const metadata =
785
+ typeof args.options.metadata === "string"
786
+ ? await parseJsonObjectOption(args.options.metadata, "--metadata", {})
787
+ : undefined;
788
+ const workspaceId = optionString(args.options.workspace) ?? process.env.DRIFTGATE_WORKSPACE_ID;
789
+ const input: DriftGateEphemeralExecuteInput = {
790
+ ...(workspaceId ? { workspaceId } : {}),
791
+ agent,
792
+ ...(optionString(args.options.subject) ? { subject: optionString(args.options.subject) } : {}),
793
+ ...(metadata ? { metadata } : {}),
794
+ ...(await parseV4ExecutionDefaults(args)),
795
+ input: await parseRequiredInputOption(args)
796
+ };
797
+ const response = await client.execute(input);
798
+ printJson(canonicalOutput(response));
799
+ }
800
+
801
+ async function handleExecution(rest: string[]): Promise<void> {
802
+ const [subcommand, ...tail] = rest;
803
+ switch (subcommand) {
804
+ case "status":
805
+ await handleExecutionStatus(tail);
806
+ return;
807
+ case "events":
808
+ await handleExecutionEvents(tail);
809
+ return;
810
+ case "wait":
811
+ await handleExecutionWait(tail);
812
+ return;
813
+ default:
814
+ throw new Error(
815
+ "usage: driftgate execution status <executionId> | driftgate execution events <executionId> | driftgate execution wait <executionId> [--interval-ms <ms>] [--timeout-ms <ms>]"
816
+ );
817
+ }
818
+ }
819
+
820
+ async function handleExecutionStatus(rest: string[]): Promise<void> {
821
+ const args = parseArgs(rest);
822
+ const [executionId] = args.positionals;
823
+ if (!executionId) {
824
+ throw new Error("usage: driftgate execution status <executionId>");
825
+ }
826
+ const config = await loadConfig();
827
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
828
+ const client = buildClient(baseUrl, config);
829
+ const startedAtNs = nowNs();
830
+ const response = await client.status(executionId);
831
+ const events = await client.events(executionId);
832
+ const requestId =
833
+ typeof response.run.correlationId === "string" && response.run.correlationId.length > 0
834
+ ? response.run.correlationId
835
+ : `cli_${randomUUID()}`;
836
+
837
+ printJson({
838
+ ok: true,
839
+ data: {
840
+ run: response.run,
841
+ approval: response.approval ?? null,
842
+ latestEvent: events.length > 0 ? events[events.length - 1] : null,
843
+ sourcePath: "/v1/headless/runs/:runId"
844
+ },
845
+ meta: {
846
+ requestId,
847
+ executionId,
848
+ timingMs: { total: elapsedMs(startedAtNs) }
849
+ },
850
+ error: null
851
+ });
852
+ }
853
+
854
+ async function handleExecutionEvents(rest: string[]): Promise<void> {
855
+ const args = parseArgs(rest);
856
+ const [executionId] = args.positionals;
857
+ if (!executionId) {
858
+ throw new Error("usage: driftgate execution events <executionId>");
859
+ }
860
+ const config = await loadConfig();
861
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
862
+ const client = buildClient(baseUrl, config);
863
+ const startedAtNs = nowNs();
864
+ const events = await client.events(executionId);
865
+ printJson({
866
+ ok: true,
867
+ data: {
868
+ events,
869
+ sourcePath: "/v1/headless/runs/:runId/events"
870
+ },
871
+ meta: {
872
+ requestId: `cli_${randomUUID()}`,
873
+ executionId,
874
+ timingMs: { total: elapsedMs(startedAtNs) }
875
+ },
876
+ error: null
877
+ });
878
+ }
879
+
880
+ async function handleExecutionWait(rest: string[]): Promise<void> {
881
+ const args = parseArgs(rest);
882
+ const [executionId] = args.positionals;
883
+ if (!executionId) {
884
+ throw new Error("usage: driftgate execution wait <executionId> [--interval-ms <ms>] [--timeout-ms <ms>]");
885
+ }
886
+
887
+ const config = await loadConfig();
888
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
889
+ const client = buildClient(baseUrl, config);
890
+ const intervalMs = parseNumberOption(args.options["interval-ms"], "--interval-ms", {
891
+ integer: true,
892
+ minimum: 1
893
+ });
894
+ const timeoutMs = parseNumberOption(args.options["timeout-ms"], "--timeout-ms", {
895
+ integer: true,
896
+ minimum: 1
897
+ });
898
+
899
+ const startedAtNs = nowNs();
900
+ const response = await client.waitForTerminal(executionId, {
901
+ ...(intervalMs !== undefined ? { intervalMs } : {}),
902
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
903
+ });
904
+ const requestId =
905
+ typeof response.run.correlationId === "string" && response.run.correlationId.length > 0
906
+ ? response.run.correlationId
907
+ : `cli_${randomUUID()}`;
908
+
909
+ printJson({
910
+ ok: true,
911
+ data: {
912
+ run: response.run,
913
+ approval: response.approval ?? null,
914
+ sourcePath: "/v1/headless/runs/:runId"
915
+ },
916
+ meta: {
917
+ requestId,
918
+ executionId,
919
+ timingMs: { total: elapsedMs(startedAtNs) }
920
+ },
921
+ error: null
922
+ });
923
+ }
924
+
925
+ async function handleApprovals(rest: string[]): Promise<void> {
926
+ const args = parseArgs(rest);
927
+ const [subcommand, subject] = args.positionals;
928
+
929
+ const config = await loadConfig();
930
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
931
+ const client = buildClient(baseUrl, config);
932
+
933
+ if (subcommand === "list") {
934
+ const workspaceId =
935
+ (typeof args.options.workspace === "string" && args.options.workspace) ||
936
+ process.env.DRIFTGATE_WORKSPACE_ID;
937
+ if (!workspaceId) {
938
+ throw new Error("usage: driftgate approvals list --workspace <workspaceId>");
939
+ }
940
+
941
+ const status =
942
+ typeof args.options.status === "string"
943
+ ? (args.options.status as "pending" | "approved" | "denied")
944
+ : undefined;
945
+ const approvals = await client.approvals.list(workspaceId, status);
946
+ console.log(JSON.stringify(approvals, null, 2));
947
+ return;
948
+ }
949
+
950
+ if (subcommand === "approve") {
951
+ if (!subject) {
952
+ throw new Error("usage: driftgate approvals approve <approvalId>");
953
+ }
954
+ const result = await client.approvals.approve(subject);
955
+ console.log(JSON.stringify(result, null, 2));
956
+ return;
957
+ }
958
+
959
+ if (subcommand === "deny") {
960
+ if (!subject) {
961
+ throw new Error("usage: driftgate approvals deny <approvalId>");
962
+ }
963
+ const result = await client.approvals.deny(subject);
964
+ console.log(JSON.stringify(result, null, 2));
965
+ return;
966
+ }
967
+
968
+ throw new Error("usage: driftgate approvals list|approve|deny ...");
969
+ }
970
+
971
+ async function handleConnectors(rest: string[]): Promise<void> {
972
+ const args = parseArgs(rest);
973
+ const [subcommand, connectorId] = args.positionals;
974
+
975
+ const config = await loadConfig();
976
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
977
+ const client = buildClient(baseUrl, config);
978
+
979
+ if (subcommand === "list") {
980
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate connectors list --workspace <workspaceId>");
981
+ const connectors = await client.connectors.list(workspaceId);
982
+ console.log(JSON.stringify(connectors, null, 2));
983
+ return;
984
+ }
985
+
986
+ if (subcommand === "create") {
987
+ const workspaceId = requireWorkspaceId(
988
+ args,
989
+ "usage: driftgate connectors create --workspace <workspaceId> --name <name> --type <connectorType> [--status active|disabled] [--config '{...}']"
990
+ );
991
+ const name = typeof args.options.name === "string" ? args.options.name : "";
992
+ const connectorType = typeof args.options.type === "string" ? args.options.type : "";
993
+ if (!name || !connectorType) {
994
+ throw new Error("usage: driftgate connectors create --workspace <workspaceId> --name <name> --type <connectorType>");
995
+ }
996
+ const configJson = await parseJsonObjectOption(args.options.config, "--config", {});
997
+ const status =
998
+ typeof args.options.status === "string" ? (args.options.status as "active" | "disabled") : undefined;
999
+ const connector = await client.connectors.create(workspaceId, {
1000
+ name,
1001
+ connectorType,
1002
+ status,
1003
+ config: configJson
1004
+ });
1005
+ console.log(JSON.stringify(connector, null, 2));
1006
+ return;
1007
+ }
1008
+
1009
+ if (subcommand === "update") {
1010
+ if (!connectorId) {
1011
+ throw new Error("usage: driftgate connectors update <connectorId> --workspace <workspaceId> [--name <name>] [--type <connectorType>] [--status active|disabled] [--config '{...}']");
1012
+ }
1013
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate connectors update <connectorId> --workspace <workspaceId> ...");
1014
+ const configPatch =
1015
+ typeof args.options.config === "string"
1016
+ ? await parseJsonObjectOption(args.options.config, "--config", {})
1017
+ : undefined;
1018
+ const input = {
1019
+ ...(typeof args.options.name === "string" ? { name: args.options.name } : {}),
1020
+ ...(typeof args.options.type === "string" ? { connectorType: args.options.type } : {}),
1021
+ ...(typeof args.options.status === "string"
1022
+ ? { status: args.options.status as "active" | "disabled" }
1023
+ : {}),
1024
+ ...(configPatch ? { config: configPatch } : {})
1025
+ };
1026
+ if (Object.keys(input).length === 0) {
1027
+ throw new Error("usage: driftgate connectors update <connectorId> --workspace <workspaceId> requires at least one field");
1028
+ }
1029
+ const connector = await client.connectors.update(workspaceId, connectorId, input);
1030
+ console.log(JSON.stringify(connector, null, 2));
1031
+ return;
1032
+ }
1033
+
1034
+ if (subcommand === "delete") {
1035
+ if (!connectorId) {
1036
+ throw new Error("usage: driftgate connectors delete <connectorId> --workspace <workspaceId>");
1037
+ }
1038
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate connectors delete <connectorId> --workspace <workspaceId>");
1039
+ const connector = await client.connectors.delete(workspaceId, connectorId);
1040
+ console.log(JSON.stringify(connector, null, 2));
1041
+ return;
1042
+ }
1043
+
1044
+ throw new Error("usage: driftgate connectors list|create|update|delete ...");
1045
+ }
1046
+
1047
+ async function handleSecrets(rest: string[]): Promise<void> {
1048
+ const args = parseArgs(rest);
1049
+ const [subcommand, secretId] = args.positionals;
1050
+
1051
+ const config = await loadConfig();
1052
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
1053
+ const client = buildClient(baseUrl, config);
1054
+
1055
+ if (subcommand === "list") {
1056
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate secrets list --workspace <workspaceId>");
1057
+ const secrets = await client.secrets.list(workspaceId);
1058
+ console.log(JSON.stringify(secrets, null, 2));
1059
+ return;
1060
+ }
1061
+
1062
+ if (subcommand === "create") {
1063
+ const workspaceId = requireWorkspaceId(
1064
+ args,
1065
+ "usage: driftgate secrets create --workspace <workspaceId> --name <name> --value <value> [--connector-id <id|null>] [--metadata '{...}']"
1066
+ );
1067
+ const name = typeof args.options.name === "string" ? args.options.name : "";
1068
+ const value = typeof args.options.value === "string" ? args.options.value : "";
1069
+ if (!name || !value) {
1070
+ throw new Error("usage: driftgate secrets create --workspace <workspaceId> --name <name> --value <value>");
1071
+ }
1072
+ const metadata = await parseJsonObjectOption(args.options.metadata, "--metadata", {});
1073
+ const secret = await client.secrets.create(workspaceId, {
1074
+ name,
1075
+ value,
1076
+ connectorId: parseNullableConnectorId(args.options["connector-id"]),
1077
+ keyVersion: typeof args.options["key-version"] === "string" ? args.options["key-version"] : undefined,
1078
+ metadata
1079
+ });
1080
+ console.log(JSON.stringify(secret, null, 2));
1081
+ return;
1082
+ }
1083
+
1084
+ if (subcommand === "update") {
1085
+ if (!secretId) {
1086
+ throw new Error("usage: driftgate secrets update <secretId> --workspace <workspaceId> [--name <name>] [--value <value>] [--connector-id <id|null>] [--metadata '{...}']");
1087
+ }
1088
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate secrets update <secretId> --workspace <workspaceId> ...");
1089
+ const metadataPatch =
1090
+ typeof args.options.metadata === "string"
1091
+ ? await parseJsonObjectOption(args.options.metadata, "--metadata", {})
1092
+ : undefined;
1093
+ const input = {
1094
+ ...(typeof args.options.name === "string" ? { name: args.options.name } : {}),
1095
+ ...(typeof args.options.value === "string" ? { value: args.options.value } : {}),
1096
+ ...(args.options["connector-id"] !== undefined
1097
+ ? { connectorId: parseNullableConnectorId(args.options["connector-id"]) }
1098
+ : {}),
1099
+ ...(typeof args.options["key-version"] === "string" ? { keyVersion: args.options["key-version"] } : {}),
1100
+ ...(metadataPatch ? { metadata: metadataPatch } : {})
1101
+ };
1102
+ if (Object.keys(input).length === 0) {
1103
+ throw new Error("usage: driftgate secrets update <secretId> --workspace <workspaceId> requires at least one field");
1104
+ }
1105
+ const secret = await client.secrets.update(workspaceId, secretId, input);
1106
+ console.log(JSON.stringify(secret, null, 2));
1107
+ return;
1108
+ }
1109
+
1110
+ if (subcommand === "delete") {
1111
+ if (!secretId) {
1112
+ throw new Error("usage: driftgate secrets delete <secretId> --workspace <workspaceId>");
1113
+ }
1114
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate secrets delete <secretId> --workspace <workspaceId>");
1115
+ const secret = await client.secrets.delete(workspaceId, secretId);
1116
+ console.log(JSON.stringify(secret, null, 2));
1117
+ return;
1118
+ }
1119
+
1120
+ throw new Error("usage: driftgate secrets list|create|update|delete ...");
1121
+ }
1122
+
1123
+ async function handleWebhooks(rest: string[]): Promise<void> {
1124
+ const args = parseArgs(rest);
1125
+ const [subcommand, webhookId] = args.positionals;
1126
+
1127
+ const config = await loadConfig();
1128
+ const baseUrl = resolveBaseUrl(args, config ?? undefined);
1129
+ const client = buildClient(baseUrl, config);
1130
+
1131
+ if (subcommand === "list") {
1132
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate webhooks list --workspace <workspaceId>");
1133
+ const webhooks = await client.webhooks.list(workspaceId);
1134
+ console.log(JSON.stringify(webhooks, null, 2));
1135
+ return;
1136
+ }
1137
+
1138
+ if (subcommand === "create") {
1139
+ const workspaceId = requireWorkspaceId(
1140
+ args,
1141
+ "usage: driftgate webhooks create --workspace <workspaceId> --name <name> --path </hook> --target-workflow <workflowId> --signing-secret <secret>"
1142
+ );
1143
+ const name = typeof args.options.name === "string" ? args.options.name : "";
1144
+ const hookPath = typeof args.options.path === "string" ? args.options.path : "";
1145
+ const targetWorkflowId =
1146
+ typeof args.options["target-workflow"] === "string" ? args.options["target-workflow"] : "";
1147
+ const signingSecret =
1148
+ typeof args.options["signing-secret"] === "string" ? args.options["signing-secret"] : "";
1149
+ if (!name || !hookPath || !targetWorkflowId || !signingSecret) {
1150
+ throw new Error(
1151
+ "usage: driftgate webhooks create --workspace <workspaceId> --name <name> --path </hook> --target-workflow <workflowId> --signing-secret <secret>"
1152
+ );
1153
+ }
1154
+
1155
+ const eventFilter = await parseJsonObjectOption(args.options["event-filter"], "--event-filter", {});
1156
+ const executionJson = await parseJsonObjectOption(args.options.execution, "--execution", {});
1157
+ const requiresApproval = parseBooleanValue(args.options["requires-approval"], "--requires-approval");
1158
+ const execution = {
1159
+ ...executionJson,
1160
+ ...(requiresApproval !== undefined ? { requiresApproval } : {}),
1161
+ ...(typeof args.options["required-role"] === "string"
1162
+ ? { requiredRole: args.options["required-role"] }
1163
+ : {}),
1164
+ ...(typeof args.options["sla-policy-id"] === "string"
1165
+ ? { slaPolicyId: args.options["sla-policy-id"] }
1166
+ : {})
1167
+ };
1168
+
1169
+ const webhook = await client.webhooks.create(workspaceId, {
1170
+ connectorId: parseNullableConnectorId(args.options["connector-id"]),
1171
+ name,
1172
+ path: hookPath,
1173
+ targetWorkflowId,
1174
+ status:
1175
+ typeof args.options.status === "string" ? (args.options.status as "active" | "disabled") : undefined,
1176
+ eventFilter,
1177
+ execution,
1178
+ signingSecret
1179
+ });
1180
+ console.log(JSON.stringify(webhook, null, 2));
1181
+ return;
1182
+ }
1183
+
1184
+ if (subcommand === "update") {
1185
+ if (!webhookId) {
1186
+ throw new Error("usage: driftgate webhooks update <webhookId> --workspace <workspaceId> [--name ...]");
1187
+ }
1188
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate webhooks update <webhookId> --workspace <workspaceId> ...");
1189
+ const eventFilterPatch =
1190
+ typeof args.options["event-filter"] === "string"
1191
+ ? await parseJsonObjectOption(args.options["event-filter"], "--event-filter", {})
1192
+ : undefined;
1193
+ const executionPatch =
1194
+ typeof args.options.execution === "string"
1195
+ ? await parseJsonObjectOption(args.options.execution, "--execution", {})
1196
+ : undefined;
1197
+ const requiresApprovalPatch = parseBooleanValue(
1198
+ args.options["requires-approval"],
1199
+ "--requires-approval"
1200
+ );
1201
+
1202
+ const execution = {
1203
+ ...(executionPatch ?? {}),
1204
+ ...(requiresApprovalPatch !== undefined ? { requiresApproval: requiresApprovalPatch } : {}),
1205
+ ...(typeof args.options["required-role"] === "string"
1206
+ ? { requiredRole: args.options["required-role"] }
1207
+ : {}),
1208
+ ...(typeof args.options["sla-policy-id"] === "string"
1209
+ ? { slaPolicyId: args.options["sla-policy-id"] }
1210
+ : {})
1211
+ };
1212
+
1213
+ const input = {
1214
+ ...(args.options["connector-id"] !== undefined
1215
+ ? { connectorId: parseNullableConnectorId(args.options["connector-id"]) }
1216
+ : {}),
1217
+ ...(typeof args.options.name === "string" ? { name: args.options.name } : {}),
1218
+ ...(typeof args.options.path === "string" ? { path: args.options.path } : {}),
1219
+ ...(typeof args.options["target-workflow"] === "string"
1220
+ ? { targetWorkflowId: args.options["target-workflow"] }
1221
+ : {}),
1222
+ ...(typeof args.options.status === "string"
1223
+ ? { status: args.options.status as "active" | "disabled" }
1224
+ : {}),
1225
+ ...(eventFilterPatch ? { eventFilter: eventFilterPatch } : {}),
1226
+ ...(Object.keys(execution).length > 0 ? { execution } : {}),
1227
+ ...(typeof args.options["signing-secret"] === "string"
1228
+ ? { signingSecret: args.options["signing-secret"] }
1229
+ : {})
1230
+ };
1231
+
1232
+ if (Object.keys(input).length === 0) {
1233
+ throw new Error("usage: driftgate webhooks update <webhookId> --workspace <workspaceId> requires at least one field");
1234
+ }
1235
+
1236
+ const webhook = await client.webhooks.update(workspaceId, webhookId, input);
1237
+ console.log(JSON.stringify(webhook, null, 2));
1238
+ return;
1239
+ }
1240
+
1241
+ if (subcommand === "delete") {
1242
+ if (!webhookId) {
1243
+ throw new Error("usage: driftgate webhooks delete <webhookId> --workspace <workspaceId>");
1244
+ }
1245
+ const workspaceId = requireWorkspaceId(args, "usage: driftgate webhooks delete <webhookId> --workspace <workspaceId>");
1246
+ const webhook = await client.webhooks.delete(workspaceId, webhookId);
1247
+ console.log(JSON.stringify(webhook, null, 2));
1248
+ return;
1249
+ }
1250
+
1251
+ throw new Error("usage: driftgate webhooks list|create|update|delete ...");
1252
+ }
1253
+
1254
+ function printHelp(): void {
1255
+ console.log(`driftgate CLI
1256
+
1257
+ Commands:
1258
+ driftgate login [--api-base <url>]
1259
+ driftgate init [--out workflow.yaml] [--force]
1260
+ driftgate deploy <workflow.yaml> --workspace <workspaceId> [--project <name>] [--workflow <name>]
1261
+ driftgate publish <workflowId> [--yaml workflow.yaml]
1262
+ driftgate session start --agent <agent> [--workspace <workspaceId>] [--subject <subject>] [--metadata '{"key":"value"}' | --metadata @metadata.json] [--policy '{"ref":"default","version":"latest"}'] [--route '{"provider":"openai","model":"gpt-4.1-mini","region":"us-east-1"}'] [--risk '{"score":12.5,"decision":"allow"}'] [--workflow-version-id <id>] [--expires-at <ISO-8601>]
1263
+ driftgate session execute <sessionId> --input '{"key":"value"}' | --input @input.json [--policy '{"ref":"default","version":"latest"}'] [--route '{"provider":"openai"}'] [--risk '{"score":12.5,"decision":"allow"}'] [--workflow-version-id <id>]
1264
+ driftgate execute --agent <agent> --input '{"key":"value"}' | --input @input.json [--workspace <workspaceId>] [--subject <subject>] [--metadata '{"key":"value"}' | --metadata @metadata.json] [--policy '{"ref":"default","version":"latest"}'] [--route '{"provider":"openai"}'] [--risk '{"score":12.5,"decision":"allow"}'] [--workflow-version-id <id>]
1265
+ driftgate execution status <executionId>
1266
+ driftgate execution events <executionId>
1267
+ driftgate execution wait <executionId> [--interval-ms <ms>] [--timeout-ms <ms>]
1268
+ driftgate approvals list --workspace <workspaceId> [--status pending|approved|denied]
1269
+ driftgate approvals approve <approvalId>
1270
+ driftgate approvals deny <approvalId>
1271
+ driftgate connectors list --workspace <workspaceId>
1272
+ driftgate connectors create --workspace <workspaceId> --name <name> --type <connectorType> [--status active|disabled] [--config '{...}']
1273
+ driftgate connectors update <connectorId> --workspace <workspaceId> [--name <name>] [--type <connectorType>] [--status active|disabled] [--config '{...}']
1274
+ driftgate connectors delete <connectorId> --workspace <workspaceId>
1275
+ driftgate secrets list --workspace <workspaceId>
1276
+ driftgate secrets create --workspace <workspaceId> --name <name> --value <value> [--connector-id <id|null>] [--key-version <v>] [--metadata '{...}']
1277
+ driftgate secrets update <secretId> --workspace <workspaceId> [--name <name>] [--value <value>] [--connector-id <id|null>] [--key-version <v>] [--metadata '{...}']
1278
+ driftgate secrets delete <secretId> --workspace <workspaceId>
1279
+ driftgate webhooks list --workspace <workspaceId>
1280
+ driftgate webhooks create --workspace <workspaceId> --name <name> --path </hook> --target-workflow <workflowId> --signing-secret <secret> [--connector-id <id|null>] [--status active|disabled] [--event-filter '{...}'] [--execution '{...}'] [--requires-approval true|false] [--required-role <role>] [--sla-policy-id <id>]
1281
+ driftgate webhooks update <webhookId> --workspace <workspaceId> [--name <name>] [--path </hook>] [--target-workflow <workflowId>] [--status active|disabled] [--event-filter '{...}'] [--execution '{...}'] [--requires-approval true|false] [--required-role <role>] [--sla-policy-id <id>] [--signing-secret <secret>] [--connector-id <id|null>]
1282
+ driftgate webhooks delete <webhookId> --workspace <workspaceId>
1283
+
1284
+ Removed in V4 CLI:
1285
+ driftgate run ...
1286
+ driftgate status ...
1287
+ `);
1288
+ }
1289
+
1290
+ void main().catch((error: unknown) => {
1291
+ console.error(JSON.stringify(renderErrorEnvelope(error), null, 2));
1292
+ process.exitCode = 1;
1293
+ });