@mondaydotcomorg/z2h-cli 0.32.2 → 0.33.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.
Files changed (67) hide show
  1. package/dist/backend/__tests__/format-sandbox-run.test.d.ts +2 -0
  2. package/dist/backend/__tests__/format-sandbox-run.test.d.ts.map +1 -0
  3. package/dist/backend/client.d.ts +32 -2
  4. package/dist/backend/client.d.ts.map +1 -1
  5. package/dist/backend/client.js +8 -5
  6. package/dist/backend/format-sandbox-run.d.ts +4 -0
  7. package/dist/backend/format-sandbox-run.d.ts.map +1 -0
  8. package/dist/backend/format-sandbox-run.js +49 -0
  9. package/dist/commands/__tests__/backend-invoke.test.d.ts +2 -0
  10. package/dist/commands/__tests__/backend-invoke.test.d.ts.map +1 -0
  11. package/dist/commands/__tests__/custom-integrations.test.d.ts +2 -0
  12. package/dist/commands/__tests__/custom-integrations.test.d.ts.map +1 -0
  13. package/dist/commands/backend.d.ts +6 -1
  14. package/dist/commands/backend.d.ts.map +1 -1
  15. package/dist/commands/backend.js +35 -3
  16. package/dist/commands/custom-integrations.d.ts +3 -0
  17. package/dist/commands/custom-integrations.d.ts.map +1 -0
  18. package/dist/commands/custom-integrations.js +51 -0
  19. package/dist/commands/deploy.js +3 -0
  20. package/dist/esm/backend/__tests__/format-sandbox-run.test.d.ts +2 -0
  21. package/dist/esm/backend/__tests__/format-sandbox-run.test.d.ts.map +1 -0
  22. package/dist/esm/backend/client.d.ts +32 -2
  23. package/dist/esm/backend/client.d.ts.map +1 -1
  24. package/dist/esm/backend/client.mjs +8 -5
  25. package/dist/esm/backend/format-sandbox-run.d.ts +4 -0
  26. package/dist/esm/backend/format-sandbox-run.d.ts.map +1 -0
  27. package/dist/esm/backend/format-sandbox-run.mjs +47 -0
  28. package/dist/esm/commands/__tests__/backend-invoke.test.d.ts +2 -0
  29. package/dist/esm/commands/__tests__/backend-invoke.test.d.ts.map +1 -0
  30. package/dist/esm/commands/__tests__/custom-integrations.test.d.ts +2 -0
  31. package/dist/esm/commands/__tests__/custom-integrations.test.d.ts.map +1 -0
  32. package/dist/esm/commands/backend.d.ts +6 -1
  33. package/dist/esm/commands/backend.d.ts.map +1 -1
  34. package/dist/esm/commands/backend.mjs +36 -4
  35. package/dist/esm/commands/custom-integrations.d.ts +3 -0
  36. package/dist/esm/commands/custom-integrations.d.ts.map +1 -0
  37. package/dist/esm/commands/custom-integrations.mjs +48 -0
  38. package/dist/esm/commands/deploy.mjs +3 -0
  39. package/dist/esm/index.mjs +18 -4
  40. package/dist/esm/util/app-name.mjs +1 -1
  41. package/dist/esm/util/broker/app.d.ts +4 -1
  42. package/dist/esm/util/broker/app.d.ts.map +1 -1
  43. package/dist/index.js +18 -4
  44. package/dist/util/app-name.js +1 -1
  45. package/dist/util/broker/app.d.ts +4 -1
  46. package/dist/util/broker/app.d.ts.map +1 -1
  47. package/handler-api/backend-runner/backend-runner.errors.ts +13 -0
  48. package/handler-api/backend-runner/backend-runner.types.ts +3 -0
  49. package/handler-api/backend-runner/channels/channel-map.ts +26 -6
  50. package/handler-api/backend-runner/channels/channel-telemetry.ts +13 -7
  51. package/handler-api/backend-runner/channels/channel-trace.ts +52 -0
  52. package/handler-api/backend-runner/channels/custom-integration-channel/custom-integration.channel.ts +243 -0
  53. package/handler-api/signatures.json +4 -0
  54. package/package.json +2 -2
  55. package/src/backend/__tests__/client.test.ts +17 -13
  56. package/src/backend/__tests__/format-sandbox-run.test.ts +42 -0
  57. package/src/backend/client.ts +27 -10
  58. package/src/backend/format-sandbox-run.ts +51 -0
  59. package/src/commands/__tests__/backend-invoke.test.ts +125 -0
  60. package/src/commands/__tests__/custom-integrations.test.ts +62 -0
  61. package/src/commands/backend.ts +43 -4
  62. package/src/commands/custom-integrations.ts +68 -0
  63. package/src/commands/deploy.ts +3 -0
  64. package/src/index.ts +24 -5
  65. package/src/util/app-name.ts +1 -1
  66. package/src/util/broker/__tests__/app.test.ts +32 -0
  67. package/src/util/broker/app.ts +4 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=format-sandbox-run.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-sandbox-run.test.d.ts","sourceRoot":"","sources":["../../../src/backend/__tests__/format-sandbox-run.test.ts"],"names":[],"mappings":""}
@@ -1,10 +1,40 @@
1
1
  /** `Z2H_BACKEND_URL` overrides for staging or a locally-run runner. */
2
2
  export declare function backendBaseUrl(): string;
3
- export interface InvokeResponse {
3
+ export interface SandboxRunRequest {
4
+ code: string;
5
+ input: unknown;
6
+ handlerName?: string;
7
+ }
8
+ /** One channel call the handler made, as the runner traced it. */
9
+ export interface SandboxCall {
10
+ method: string;
11
+ args: unknown[];
12
+ durationMs: number;
13
+ ok: boolean;
14
+ error?: {
15
+ status: number;
16
+ code?: string;
17
+ message: string;
18
+ };
19
+ result?: unknown;
20
+ }
21
+ export interface SandboxRunResponse {
22
+ ok: boolean;
23
+ error?: {
24
+ status: number;
25
+ code?: string;
26
+ message: string;
27
+ };
28
+ calls: SandboxCall[];
4
29
  result: unknown;
5
30
  durationMs: number;
6
31
  }
7
- export declare function invokeBackend(appName: string, handlerName: string, input: unknown): Promise<InvokeResponse>;
32
+ /**
33
+ * Runs handler source from the working tree on the runner's sandbox route —
34
+ * against the app's real channels, without deploying it. A handler failure
35
+ * comes back as `ok: false` with the trace; only a refused request throws.
36
+ */
37
+ export declare function runSandbox(appName: string, body: SandboxRunRequest): Promise<SandboxRunResponse>;
8
38
  export interface AppIntegrationsResponse {
9
39
  integrations: string[];
10
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/backend/client.ts"],"names":[],"mappings":"AAWA,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AA+CD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAS3G;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAErF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/backend/client.ts"],"names":[],"mappings":"AAWA,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AA+CD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAEhG;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAErF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF"}
@@ -56,10 +56,13 @@ async function request(method, baseUrl, path, body) {
56
56
  }
57
57
  return (parsed ?? {});
58
58
  }
59
- function invokeBackend(appName, handlerName, input) {
60
- // The runner resolves the caller from the request or 401s, so an anonymous
61
- // invoke can never reach a handler.
62
- return request('POST', backendBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/invoke/${encodeURIComponent(handlerName)}`, { input });
59
+ /**
60
+ * Runs handler source from the working tree on the runner's sandbox route —
61
+ * against the app's real channels, without deploying it. A handler failure
62
+ * comes back as `ok: false` with the trace; only a refused request throws.
63
+ */
64
+ function runSandbox(appName, body) {
65
+ return request('POST', backendBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/sandbox/run`, body);
63
66
  }
64
67
  function listAppIntegrations(appName) {
65
68
  return request('GET', constants.brokerBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/integrations`);
@@ -73,6 +76,6 @@ function removeIntegration(appName, integration) {
73
76
 
74
77
  exports.backendBaseUrl = backendBaseUrl;
75
78
  exports.declareIntegration = declareIntegration;
76
- exports.invokeBackend = invokeBackend;
77
79
  exports.listAppIntegrations = listAppIntegrations;
78
80
  exports.removeIntegration = removeIntegration;
81
+ exports.runSandbox = runSandbox;
@@ -0,0 +1,4 @@
1
+ import type { SandboxRunResponse } from './client';
2
+ /** Human-readable trace of a sandbox run; `--json` prints the raw response instead. */
3
+ export declare function formatSandboxRun(res: SandboxRunResponse): string;
4
+ //# sourceMappingURL=format-sandbox-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-sandbox-run.d.ts","sourceRoot":"","sources":["../../src/backend/format-sandbox-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAmChE,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,kBAAkB,GAAG,MAAM,CAchE"}
@@ -0,0 +1,49 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const MAX_SQL_CHARS = 400;
4
+ const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
5
+ const oneLine = (text) => text.replace(/\s+/g, ' ').trim();
6
+ const clip = (text, max) => (text.length > max ? `${text.slice(0, max)}…` : text);
7
+ /** The first arg is the SQL / GraphQL text for query channels — shown on its own line so it can be read. */
8
+ function describeArgs(args) {
9
+ const [first, ...rest] = args;
10
+ const lines = [];
11
+ if (typeof first === 'string') {
12
+ lines.push(clip(oneLine(first), MAX_SQL_CHARS));
13
+ }
14
+ else if (first !== undefined) {
15
+ lines.push(clip(JSON.stringify(first), MAX_SQL_CHARS));
16
+ }
17
+ const params = rest.filter(arg => arg !== undefined && arg !== null);
18
+ if (params.length > 0) {
19
+ lines.push(`params: ${clip(params.map(p => JSON.stringify(p)).join(', '), MAX_SQL_CHARS)}`);
20
+ }
21
+ return lines;
22
+ }
23
+ function describeCall(call) {
24
+ const rows = Array.isArray(call.result) ? ` ${call.result.length} rows` : '';
25
+ const head = ` ${call.ok ? '✓' : '✗'} ${call.method} ${Math.round(call.durationMs)}ms${rows}`;
26
+ const detail = describeArgs(call.args);
27
+ if (call.error) {
28
+ const code = call.error.code ? ` ${call.error.code}` : '';
29
+ detail.push(`${call.error.status}${code}: ${call.error.message}`);
30
+ }
31
+ return [head, ...detail.map(line => ` ${line}`)];
32
+ }
33
+ /** Human-readable trace of a sandbox run; `--json` prints the raw response instead. */
34
+ function formatSandboxRun(res) {
35
+ const lines = [`[z2h-cli] sandbox run ${res.ok ? '✓' : '✗'} ${seconds(res.durationMs)}`, ''];
36
+ lines.push(res.calls.length > 0 ? 'calls:' : 'calls: none');
37
+ res.calls.forEach(call => lines.push(...describeCall(call)));
38
+ lines.push('');
39
+ if (res.ok) {
40
+ lines.push('result:', JSON.stringify(res.result, null, 2));
41
+ }
42
+ else if (res.error) {
43
+ const code = res.error.code ? ` ${res.error.code}` : '';
44
+ lines.push(`error: ${res.error.status}${code} — ${res.error.message}`);
45
+ }
46
+ return lines.join('\n');
47
+ }
48
+
49
+ exports.formatSandboxRun = formatSandboxRun;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=backend-invoke.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-invoke.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/backend-invoke.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=custom-integrations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-integrations.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/custom-integrations.test.ts"],"names":[],"mappings":""}
@@ -23,7 +23,12 @@ export declare function deployBackend(opts: {
23
23
  * local `usage.json` — no upload to S3. Throws on the first invalid handler.
24
24
  */
25
25
  export declare function backendValidateCommand(appDir?: string): Promise<void>;
26
+ /**
27
+ * Runs a handler from the working tree (or `--code`) on the runner's sandbox and
28
+ * prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
29
+ */
26
30
  export declare function backendInvokeCommand(handlerName: string, opts?: {
27
31
  input?: string;
28
- }): Promise<void>;
32
+ code?: string;
33
+ }, appDir?: string): Promise<void>;
29
34
  //# sourceMappingURL=backend.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAsBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAqDD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1F;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAqDD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1F;AAsBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,EAC5C,MAAM,GAAE,MAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const path = require('node:path');
4
4
  const fs = require('node:fs');
5
+ const promises = require('node:fs/promises');
5
6
  const fs$1 = require('fs-extra');
6
7
  const clientS3 = require('@aws-sdk/client-s3');
7
8
  const util_logger = require('../util/logger.js');
@@ -12,6 +13,8 @@ const backend_detectIntegrations = require('../backend/detect-integrations.js');
12
13
  const util_s3_client = require('../util/s3/client.js');
13
14
  const constants = require('../constants.js');
14
15
  const backend_client = require('../backend/client.js');
16
+ const backend_formatSandboxRun = require('../backend/format-sandbox-run.js');
17
+ const backend_validate = require('../backend/validate.js');
15
18
  const util_errors = require('../util/errors.js');
16
19
 
17
20
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -111,7 +114,29 @@ async function backendValidateCommand(appDir = process.cwd()) {
111
114
  integrations,
112
115
  });
113
116
  }
114
- async function backendInvokeCommand(handlerName, opts = {}) {
117
+ async function readHandlerSource(appDir, handlerName) {
118
+ const dir = path__default.default.join(appDir, 'backend', 'handlers');
119
+ try {
120
+ return await promises.readFile(path__default.default.join(dir, `${handlerName}.js`), 'utf8');
121
+ }
122
+ catch (err) {
123
+ if (err.code !== 'ENOENT') {
124
+ throw err;
125
+ }
126
+ const files = hasHandlers(appDir) ? await promises.readdir(dir) : [];
127
+ const available = files
128
+ .filter(f => f.endsWith('.js'))
129
+ .map(f => path__default.default.parse(f).name)
130
+ .sort();
131
+ throw new Error(`no handler "${handlerName}" in backend/handlers/` +
132
+ (available.length ? ` (available: ${available.join(', ')})` : ' (no handlers found)'));
133
+ }
134
+ }
135
+ /**
136
+ * Runs a handler from the working tree (or `--code`) on the runner's sandbox and
137
+ * prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
138
+ */
139
+ async function backendInvokeCommand(handlerName, opts = {}, appDir = process.cwd()) {
115
140
  const appName = currentAppName();
116
141
  let input = null;
117
142
  if (opts.input) {
@@ -122,8 +147,15 @@ async function backendInvokeCommand(handlerName, opts = {}) {
122
147
  throw new Error(`--input must be valid JSON (got: ${opts.input})`);
123
148
  }
124
149
  }
125
- const res = await backend_client.invokeBackend(appName, handlerName, input);
126
- util_logger.output(`[z2h-cli] result (${Math.round(res.durationMs)}ms):\n${JSON.stringify(res.result, null, 2)}`, res);
150
+ const file = `${handlerName}.js`;
151
+ const code = opts.code ?? (await readHandlerSource(appDir, handlerName));
152
+ backend_validate.validateHandlerSource(file, code);
153
+ util_logger.info(`validated ${opts.code ? '--code' : file}`);
154
+ const res = await backend_client.runSandbox(appName, { code, input, handlerName });
155
+ util_logger.output(backend_formatSandboxRun.formatSandboxRun(res), res);
156
+ if (!res.ok) {
157
+ process.exitCode = 1;
158
+ }
127
159
  }
128
160
 
129
161
  exports.backendInvokeCommand = backendInvokeCommand;
@@ -0,0 +1,3 @@
1
+ export declare function listCustomIntegrationsCommand(): Promise<void>;
2
+ export declare function removeCustomIntegrationCommand(name: string | undefined): Promise<void>;
3
+ //# sourceMappingURL=custom-integrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-integrations.d.ts","sourceRoot":"","sources":["../../src/commands/custom-integrations.ts"],"names":[],"mappings":"AAyBA,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC,CAsBnE;AAED,wBAAsB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB5F"}
@@ -0,0 +1,51 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const util_logger = require('../util/logger.js');
4
+ const util_appName = require('../util/app-name.js');
5
+ const constants = require('../constants.js');
6
+ const util_auth_authStore = require('../util/auth/auth-store.js');
7
+
8
+ function getTargetAppName() {
9
+ const consumerName = util_appName.getCwdAppName();
10
+ if (!consumerName) {
11
+ throw new Error('Run this from inside a consumer app directory (the one with your package.json).');
12
+ }
13
+ return util_appName.mfAppName(consumerName);
14
+ }
15
+ async function listCustomIntegrationsCommand() {
16
+ const appName = getTargetAppName();
17
+ const { token } = await util_auth_authStore.readZthAuth();
18
+ const res = await fetch(`${constants.brokerBaseUrl()}/z2h-apps/${appName}/custom-integrations`, {
19
+ headers: { Authorization: `Bearer ${token}`, ...constants.routingHeaders() },
20
+ });
21
+ if (!res.ok) {
22
+ const text = await res.text();
23
+ throw new Error(`Listing custom integrations failed (${res.status}): ${text.slice(0, 1024)}`);
24
+ }
25
+ const { connections } = (await res.json());
26
+ if (connections.length === 0) {
27
+ util_logger.output(`[z2h-cli] "${appName}" has no custom integrations registered`, { ok: true, appName, connections: [] });
28
+ return;
29
+ }
30
+ const lines = connections.map(c => ` ${c.name} → ${c.allowedHost} (${c.authHeaderName})${c.inherited ? ' [inherited from base app]' : ''}`);
31
+ util_logger.output(`[z2h-cli] "${appName}" custom integrations:\n${lines.join('\n')}`, { ok: true, appName, connections });
32
+ }
33
+ async function removeCustomIntegrationCommand(name) {
34
+ if (!name) {
35
+ throw new Error('Specify the connection name to remove — e.g. z2h-cli custom-integration:remove stripe');
36
+ }
37
+ const appName = getTargetAppName();
38
+ const { token } = await util_auth_authStore.readZthAuth();
39
+ const res = await fetch(`${constants.brokerBaseUrl()}/z2h-apps/${appName}/custom-integrations/${encodeURIComponent(name)}`, {
40
+ method: 'DELETE',
41
+ headers: { Authorization: `Bearer ${token}`, ...constants.routingHeaders() },
42
+ });
43
+ if (!res.ok) {
44
+ const text = await res.text();
45
+ throw new Error(`Removing custom integration failed (${res.status}): ${text.slice(0, 1024)}`);
46
+ }
47
+ util_logger.output(`[z2h-cli] custom integration "${name}" removed from "${appName}"`, { ok: true, appName, name });
48
+ }
49
+
50
+ exports.listCustomIntegrationsCommand = listCustomIntegrationsCommand;
51
+ exports.removeCustomIntegrationCommand = removeCustomIntegrationCommand;
@@ -148,6 +148,9 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
148
148
  entry,
149
149
  gitRemote: '',
150
150
  isPreview: true,
151
+ // The base app this preview is for — bigbrain-zth links the preview's row
152
+ // to it (once the caller's EDITOR access on it is confirmed) so custom
153
+ // integrations set on the base app are inherited by the preview.
151
154
  masterAppName: appName,
152
155
  });
153
156
  const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=format-sandbox-run.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-sandbox-run.test.d.ts","sourceRoot":"","sources":["../../../../src/backend/__tests__/format-sandbox-run.test.ts"],"names":[],"mappings":""}
@@ -1,10 +1,40 @@
1
1
  /** `Z2H_BACKEND_URL` overrides for staging or a locally-run runner. */
2
2
  export declare function backendBaseUrl(): string;
3
- export interface InvokeResponse {
3
+ export interface SandboxRunRequest {
4
+ code: string;
5
+ input: unknown;
6
+ handlerName?: string;
7
+ }
8
+ /** One channel call the handler made, as the runner traced it. */
9
+ export interface SandboxCall {
10
+ method: string;
11
+ args: unknown[];
12
+ durationMs: number;
13
+ ok: boolean;
14
+ error?: {
15
+ status: number;
16
+ code?: string;
17
+ message: string;
18
+ };
19
+ result?: unknown;
20
+ }
21
+ export interface SandboxRunResponse {
22
+ ok: boolean;
23
+ error?: {
24
+ status: number;
25
+ code?: string;
26
+ message: string;
27
+ };
28
+ calls: SandboxCall[];
4
29
  result: unknown;
5
30
  durationMs: number;
6
31
  }
7
- export declare function invokeBackend(appName: string, handlerName: string, input: unknown): Promise<InvokeResponse>;
32
+ /**
33
+ * Runs handler source from the working tree on the runner's sandbox route —
34
+ * against the app's real channels, without deploying it. A handler failure
35
+ * comes back as `ok: false` with the trace; only a refused request throws.
36
+ */
37
+ export declare function runSandbox(appName: string, body: SandboxRunRequest): Promise<SandboxRunResponse>;
8
38
  export interface AppIntegrationsResponse {
9
39
  integrations: string[];
10
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/backend/client.ts"],"names":[],"mappings":"AAWA,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AA+CD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAS3G;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAErF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/backend/client.ts"],"names":[],"mappings":"AAWA,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AA+CD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAEhG;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAErF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF"}
@@ -54,10 +54,13 @@ async function request(method, baseUrl, path, body) {
54
54
  }
55
55
  return (parsed ?? {});
56
56
  }
57
- function invokeBackend(appName, handlerName, input) {
58
- // The runner resolves the caller from the request or 401s, so an anonymous
59
- // invoke can never reach a handler.
60
- return request('POST', backendBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/invoke/${encodeURIComponent(handlerName)}`, { input });
57
+ /**
58
+ * Runs handler source from the working tree on the runner's sandbox route —
59
+ * against the app's real channels, without deploying it. A handler failure
60
+ * comes back as `ok: false` with the trace; only a refused request throws.
61
+ */
62
+ function runSandbox(appName, body) {
63
+ return request('POST', backendBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/sandbox/run`, body);
61
64
  }
62
65
  function listAppIntegrations(appName) {
63
66
  return request('GET', brokerBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/integrations`);
@@ -69,4 +72,4 @@ function removeIntegration(appName, integration) {
69
72
  return request('DELETE', brokerBaseUrl(), `/z2h-apps/${encodeURIComponent(appName)}/integrations/${encodeURIComponent(integration)}`);
70
73
  }
71
74
 
72
- export { backendBaseUrl, declareIntegration, invokeBackend, listAppIntegrations, removeIntegration };
75
+ export { backendBaseUrl, declareIntegration, listAppIntegrations, removeIntegration, runSandbox };
@@ -0,0 +1,4 @@
1
+ import type { SandboxRunResponse } from './client';
2
+ /** Human-readable trace of a sandbox run; `--json` prints the raw response instead. */
3
+ export declare function formatSandboxRun(res: SandboxRunResponse): string;
4
+ //# sourceMappingURL=format-sandbox-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-sandbox-run.d.ts","sourceRoot":"","sources":["../../../src/backend/format-sandbox-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAmChE,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,kBAAkB,GAAG,MAAM,CAchE"}
@@ -0,0 +1,47 @@
1
+ const MAX_SQL_CHARS = 400;
2
+ const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
3
+ const oneLine = (text) => text.replace(/\s+/g, ' ').trim();
4
+ const clip = (text, max) => (text.length > max ? `${text.slice(0, max)}…` : text);
5
+ /** The first arg is the SQL / GraphQL text for query channels — shown on its own line so it can be read. */
6
+ function describeArgs(args) {
7
+ const [first, ...rest] = args;
8
+ const lines = [];
9
+ if (typeof first === 'string') {
10
+ lines.push(clip(oneLine(first), MAX_SQL_CHARS));
11
+ }
12
+ else if (first !== undefined) {
13
+ lines.push(clip(JSON.stringify(first), MAX_SQL_CHARS));
14
+ }
15
+ const params = rest.filter(arg => arg !== undefined && arg !== null);
16
+ if (params.length > 0) {
17
+ lines.push(`params: ${clip(params.map(p => JSON.stringify(p)).join(', '), MAX_SQL_CHARS)}`);
18
+ }
19
+ return lines;
20
+ }
21
+ function describeCall(call) {
22
+ const rows = Array.isArray(call.result) ? ` ${call.result.length} rows` : '';
23
+ const head = ` ${call.ok ? '✓' : '✗'} ${call.method} ${Math.round(call.durationMs)}ms${rows}`;
24
+ const detail = describeArgs(call.args);
25
+ if (call.error) {
26
+ const code = call.error.code ? ` ${call.error.code}` : '';
27
+ detail.push(`${call.error.status}${code}: ${call.error.message}`);
28
+ }
29
+ return [head, ...detail.map(line => ` ${line}`)];
30
+ }
31
+ /** Human-readable trace of a sandbox run; `--json` prints the raw response instead. */
32
+ function formatSandboxRun(res) {
33
+ const lines = [`[z2h-cli] sandbox run ${res.ok ? '✓' : '✗'} ${seconds(res.durationMs)}`, ''];
34
+ lines.push(res.calls.length > 0 ? 'calls:' : 'calls: none');
35
+ res.calls.forEach(call => lines.push(...describeCall(call)));
36
+ lines.push('');
37
+ if (res.ok) {
38
+ lines.push('result:', JSON.stringify(res.result, null, 2));
39
+ }
40
+ else if (res.error) {
41
+ const code = res.error.code ? ` ${res.error.code}` : '';
42
+ lines.push(`error: ${res.error.status}${code} — ${res.error.message}`);
43
+ }
44
+ return lines.join('\n');
45
+ }
46
+
47
+ export { formatSandboxRun };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=backend-invoke.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-invoke.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/backend-invoke.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=custom-integrations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-integrations.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/custom-integrations.test.ts"],"names":[],"mappings":""}
@@ -23,7 +23,12 @@ export declare function deployBackend(opts: {
23
23
  * local `usage.json` — no upload to S3. Throws on the first invalid handler.
24
24
  */
25
25
  export declare function backendValidateCommand(appDir?: string): Promise<void>;
26
+ /**
27
+ * Runs a handler from the working tree (or `--code`) on the runner's sandbox and
28
+ * prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
29
+ */
26
30
  export declare function backendInvokeCommand(handlerName: string, opts?: {
27
31
  input?: string;
28
- }): Promise<void>;
32
+ code?: string;
33
+ }, appDir?: string): Promise<void>;
29
34
  //# sourceMappingURL=backend.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/commands/backend.ts"],"names":[],"mappings":"AAsBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAqDD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1F;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/commands/backend.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAqDD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1F;AAsBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,EAC5C,MAAM,GAAE,MAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
+ import { readFile, readdir } from 'node:fs/promises';
3
4
  import { readJson, writeJson } from 'fs-extra';
4
5
  import { PutObjectCommand } from '@aws-sdk/client-s3';
5
6
  import { output, info } from '../util/logger.mjs';
@@ -9,7 +10,9 @@ import { readHandlers } from '../backend/handlers.mjs';
9
10
  import { detectIntegrations } from '../backend/detect-integrations.mjs';
10
11
  import { getS3Client } from '../util/s3/client.mjs';
11
12
  import { BUCKET_WRITE_NAME } from '../constants.mjs';
12
- import { invokeBackend } from '../backend/client.mjs';
13
+ import { runSandbox } from '../backend/client.mjs';
14
+ import { formatSandboxRun } from '../backend/format-sandbox-run.mjs';
15
+ import { validateHandlerSource } from '../backend/validate.mjs';
13
16
  import { tagDeployError } from '../util/errors.mjs';
14
17
 
15
18
  function currentAppName() {
@@ -105,7 +108,29 @@ async function backendValidateCommand(appDir = process.cwd()) {
105
108
  integrations,
106
109
  });
107
110
  }
108
- async function backendInvokeCommand(handlerName, opts = {}) {
111
+ async function readHandlerSource(appDir, handlerName) {
112
+ const dir = path.join(appDir, 'backend', 'handlers');
113
+ try {
114
+ return await readFile(path.join(dir, `${handlerName}.js`), 'utf8');
115
+ }
116
+ catch (err) {
117
+ if (err.code !== 'ENOENT') {
118
+ throw err;
119
+ }
120
+ const files = hasHandlers(appDir) ? await readdir(dir) : [];
121
+ const available = files
122
+ .filter(f => f.endsWith('.js'))
123
+ .map(f => path.parse(f).name)
124
+ .sort();
125
+ throw new Error(`no handler "${handlerName}" in backend/handlers/` +
126
+ (available.length ? ` (available: ${available.join(', ')})` : ' (no handlers found)'));
127
+ }
128
+ }
129
+ /**
130
+ * Runs a handler from the working tree (or `--code`) on the runner's sandbox and
131
+ * prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
132
+ */
133
+ async function backendInvokeCommand(handlerName, opts = {}, appDir = process.cwd()) {
109
134
  const appName = currentAppName();
110
135
  let input = null;
111
136
  if (opts.input) {
@@ -116,8 +141,15 @@ async function backendInvokeCommand(handlerName, opts = {}) {
116
141
  throw new Error(`--input must be valid JSON (got: ${opts.input})`);
117
142
  }
118
143
  }
119
- const res = await invokeBackend(appName, handlerName, input);
120
- output(`[z2h-cli] result (${Math.round(res.durationMs)}ms):\n${JSON.stringify(res.result, null, 2)}`, res);
144
+ const file = `${handlerName}.js`;
145
+ const code = opts.code ?? (await readHandlerSource(appDir, handlerName));
146
+ validateHandlerSource(file, code);
147
+ info(`validated ${opts.code ? '--code' : file}`);
148
+ const res = await runSandbox(appName, { code, input, handlerName });
149
+ output(formatSandboxRun(res), res);
150
+ if (!res.ok) {
151
+ process.exitCode = 1;
152
+ }
121
153
  }
122
154
 
123
155
  export { backendInvokeCommand, backendValidateCommand, deployBackend, hasHandlers };
@@ -0,0 +1,3 @@
1
+ export declare function listCustomIntegrationsCommand(): Promise<void>;
2
+ export declare function removeCustomIntegrationCommand(name: string | undefined): Promise<void>;
3
+ //# sourceMappingURL=custom-integrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-integrations.d.ts","sourceRoot":"","sources":["../../../src/commands/custom-integrations.ts"],"names":[],"mappings":"AAyBA,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC,CAsBnE;AAED,wBAAsB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB5F"}
@@ -0,0 +1,48 @@
1
+ import { output } from '../util/logger.mjs';
2
+ import { getCwdAppName, mfAppName } from '../util/app-name.mjs';
3
+ import { brokerBaseUrl, routingHeaders } from '../constants.mjs';
4
+ import { readZthAuth } from '../util/auth/auth-store.mjs';
5
+
6
+ function getTargetAppName() {
7
+ const consumerName = getCwdAppName();
8
+ if (!consumerName) {
9
+ throw new Error('Run this from inside a consumer app directory (the one with your package.json).');
10
+ }
11
+ return mfAppName(consumerName);
12
+ }
13
+ async function listCustomIntegrationsCommand() {
14
+ const appName = getTargetAppName();
15
+ const { token } = await readZthAuth();
16
+ const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/custom-integrations`, {
17
+ headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
18
+ });
19
+ if (!res.ok) {
20
+ const text = await res.text();
21
+ throw new Error(`Listing custom integrations failed (${res.status}): ${text.slice(0, 1024)}`);
22
+ }
23
+ const { connections } = (await res.json());
24
+ if (connections.length === 0) {
25
+ output(`[z2h-cli] "${appName}" has no custom integrations registered`, { ok: true, appName, connections: [] });
26
+ return;
27
+ }
28
+ const lines = connections.map(c => ` ${c.name} → ${c.allowedHost} (${c.authHeaderName})${c.inherited ? ' [inherited from base app]' : ''}`);
29
+ output(`[z2h-cli] "${appName}" custom integrations:\n${lines.join('\n')}`, { ok: true, appName, connections });
30
+ }
31
+ async function removeCustomIntegrationCommand(name) {
32
+ if (!name) {
33
+ throw new Error('Specify the connection name to remove — e.g. z2h-cli custom-integration:remove stripe');
34
+ }
35
+ const appName = getTargetAppName();
36
+ const { token } = await readZthAuth();
37
+ const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/custom-integrations/${encodeURIComponent(name)}`, {
38
+ method: 'DELETE',
39
+ headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
40
+ });
41
+ if (!res.ok) {
42
+ const text = await res.text();
43
+ throw new Error(`Removing custom integration failed (${res.status}): ${text.slice(0, 1024)}`);
44
+ }
45
+ output(`[z2h-cli] custom integration "${name}" removed from "${appName}"`, { ok: true, appName, name });
46
+ }
47
+
48
+ export { listCustomIntegrationsCommand, removeCustomIntegrationCommand };