@mondaydotcomorg/z2h-cli 0.33.0 → 0.33.2

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 (53) 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/backend.d.ts +12 -6
  12. package/dist/commands/backend.d.ts.map +1 -1
  13. package/dist/commands/backend.js +41 -15
  14. package/dist/commands/deploy.js +29 -16
  15. package/dist/esm/backend/__tests__/format-sandbox-run.test.d.ts +2 -0
  16. package/dist/esm/backend/__tests__/format-sandbox-run.test.d.ts.map +1 -0
  17. package/dist/esm/backend/client.d.ts +32 -2
  18. package/dist/esm/backend/client.d.ts.map +1 -1
  19. package/dist/esm/backend/client.mjs +8 -5
  20. package/dist/esm/backend/format-sandbox-run.d.ts +4 -0
  21. package/dist/esm/backend/format-sandbox-run.d.ts.map +1 -0
  22. package/dist/esm/backend/format-sandbox-run.mjs +47 -0
  23. package/dist/esm/commands/__tests__/backend-invoke.test.d.ts +2 -0
  24. package/dist/esm/commands/__tests__/backend-invoke.test.d.ts.map +1 -0
  25. package/dist/esm/commands/backend.d.ts +12 -6
  26. package/dist/esm/commands/backend.d.ts.map +1 -1
  27. package/dist/esm/commands/backend.mjs +42 -16
  28. package/dist/esm/commands/deploy.mjs +29 -16
  29. package/dist/esm/index.mjs +9 -4
  30. package/dist/esm/util/broker/app.d.ts +2 -0
  31. package/dist/esm/util/broker/app.d.ts.map +1 -1
  32. package/dist/esm/util/broker/app.mjs +20 -7
  33. package/dist/index.js +9 -4
  34. package/dist/util/broker/app.d.ts +2 -0
  35. package/dist/util/broker/app.d.ts.map +1 -1
  36. package/dist/util/broker/app.js +20 -7
  37. package/handler-api/backend-runner/backend-runner.errors.ts +12 -0
  38. package/handler-api/backend-runner/backend-runner.types.ts +3 -0
  39. package/handler-api/backend-runner/channels/channel-map.ts +12 -3
  40. package/handler-api/backend-runner/channels/channel-telemetry.ts +13 -7
  41. package/handler-api/backend-runner/channels/channel-trace.ts +52 -0
  42. package/package.json +2 -2
  43. package/src/backend/__tests__/client.test.ts +17 -13
  44. package/src/backend/__tests__/format-sandbox-run.test.ts +42 -0
  45. package/src/backend/client.ts +27 -10
  46. package/src/backend/format-sandbox-run.ts +51 -0
  47. package/src/commands/__tests__/backend-deploy.test.ts +42 -21
  48. package/src/commands/__tests__/backend-invoke.test.ts +125 -0
  49. package/src/commands/__tests__/deploy.test.ts +53 -0
  50. package/src/commands/backend.ts +57 -18
  51. package/src/commands/deploy.ts +31 -17
  52. package/src/index.ts +13 -5
  53. package/src/util/broker/app.ts +59 -50
@@ -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":""}
@@ -1,9 +1,10 @@
1
1
  export declare function hasHandlers(appDir: string): boolean;
2
+ export interface ReadDetectAndUploadResult {
3
+ integrations: string[];
4
+ handlerNames: string[];
5
+ apiUsages: Record<string, unknown>;
6
+ }
2
7
  /**
3
- * Deploy the app's backend handlers to S3 and return the integrations detected
4
- * from their sources. Each `.js` handler is uploaded as its own flat file — no
5
- * compilation, no bundling, no npm deps.
6
- *
7
8
  * Integration sync note: the returned set is re-derived from the CURRENT backend
8
9
  * sources on every deploy, and the platform set is reconciled to exactly match
9
10
  * it (there is no stored declaration to fall back on). A deploy therefore always
@@ -17,13 +18,18 @@ export declare function deployBackend(opts: {
17
18
  version: number;
18
19
  deployedBy: string;
19
20
  isPreview: boolean;
20
- }): Promise<string[]>;
21
+ }): Promise<ReadDetectAndUploadResult>;
21
22
  /**
22
23
  * Runs the same checks deploy does against the working tree, refreshing the
23
24
  * local `usage.json` — no upload to S3. Throws on the first invalid handler.
24
25
  */
25
26
  export declare function backendValidateCommand(appDir?: string): Promise<void>;
27
+ /**
28
+ * Runs a handler from the working tree (or `--code`) on the runner's sandbox and
29
+ * prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
30
+ */
26
31
  export declare function backendInvokeCommand(handlerName: string, opts?: {
27
32
  input?: string;
28
- }): Promise<void>;
33
+ code?: string;
34
+ }, appDir?: string): Promise<void>;
29
35
  //# 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;AAoBD,MAAM,WAAW,yBAAyB;IACxC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAiCD;;;;;;GAMG;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,yBAAyB,CAAC,CASrC;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 };
@@ -45,21 +48,19 @@ async function writeUsageJson(appDir, patch) {
45
48
  const existing = await readExistingUsage(appDir);
46
49
  await fs$1.writeJson(path__default.default.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
47
50
  }
48
- /**
49
- * Returns `[]` when there is no `backend/handlers/` dir (or it is empty of sources)
50
- * so callers can always pass the result to `registerOrUpdateApp` without a separate gate.
51
- */
52
51
  async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel) {
53
52
  if (!hasHandlers(appDir)) {
54
- return [];
53
+ await writeUsageJson(appDir, { appName, deployedBy, apiUsages: {} });
54
+ return { integrations: [], handlerNames: [], apiUsages: {} };
55
55
  }
56
56
  util_logger.info(`reading backend handlers ${logLabel}`);
57
57
  const { handlers, apiDomains, apiUsages } = await backend_handlers.readHandlers(appDir);
58
58
  const integrations = backend_detectIntegrations.detectIntegrations(apiDomains);
59
+ const handlerNames = Object.keys(handlers);
60
+ await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
59
61
  if (Object.keys(handlers).length === 0) {
60
- return integrations;
62
+ return { integrations, handlerNames, apiUsages };
61
63
  }
62
- await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
63
64
  const s3 = util_s3_client.getS3Client();
64
65
  const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
65
66
  const put = (key, body, contentType) => s3.send(new clientS3.PutObjectCommand({ Bucket: constants.BUCKET_WRITE_NAME, Key: key, Body: body, ContentType: contentType }));
@@ -67,13 +68,9 @@ async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy,
67
68
  ...Object.entries(handlers).map(([name, code]) => put(`${prefix}/${name}.js`, code, 'application/javascript')),
68
69
  put(`${prefix}/meta.json`, meta, 'application/json'),
69
70
  ]);
70
- return integrations;
71
+ return { integrations, handlerNames, apiUsages };
71
72
  }
72
73
  /**
73
- * Deploy the app's backend handlers to S3 and return the integrations detected
74
- * from their sources. Each `.js` handler is uploaded as its own flat file — no
75
- * compilation, no bundling, no npm deps.
76
- *
77
74
  * Integration sync note: the returned set is re-derived from the CURRENT backend
78
75
  * sources on every deploy, and the platform set is reconciled to exactly match
79
76
  * it (there is no stored declaration to fall back on). A deploy therefore always
@@ -111,7 +108,29 @@ async function backendValidateCommand(appDir = process.cwd()) {
111
108
  integrations,
112
109
  });
113
110
  }
114
- async function backendInvokeCommand(handlerName, opts = {}) {
111
+ async function readHandlerSource(appDir, handlerName) {
112
+ const dir = path__default.default.join(appDir, 'backend', 'handlers');
113
+ try {
114
+ return await promises.readFile(path__default.default.join(dir, `${handlerName}.js`), 'utf8');
115
+ }
116
+ catch (err) {
117
+ if (err.code !== 'ENOENT') {
118
+ throw err;
119
+ }
120
+ const files = hasHandlers(appDir) ? await promises.readdir(dir) : [];
121
+ const available = files
122
+ .filter(f => f.endsWith('.js'))
123
+ .map(f => path__default.default.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()) {
115
134
  const appName = currentAppName();
116
135
  let input = null;
117
136
  if (opts.input) {
@@ -122,8 +141,15 @@ async function backendInvokeCommand(handlerName, opts = {}) {
122
141
  throw new Error(`--input must be valid JSON (got: ${opts.input})`);
123
142
  }
124
143
  }
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);
144
+ const file = `${handlerName}.js`;
145
+ const code = opts.code ?? (await readHandlerSource(appDir, handlerName));
146
+ backend_validate.validateHandlerSource(file, code);
147
+ util_logger.info(`validated ${opts.code ? '--code' : file}`);
148
+ const res = await backend_client.runSandbox(appName, { code, input, handlerName });
149
+ util_logger.output(backend_formatSandboxRun.formatSandboxRun(res), res);
150
+ if (!res.ok) {
151
+ process.exitCode = 1;
152
+ }
127
153
  }
128
154
 
129
155
  exports.backendInvokeCommand = backendInvokeCommand;
@@ -85,12 +85,9 @@ async function buildAndStamp(opts) {
85
85
  */
86
86
  async function uploadAndRegister(opts) {
87
87
  const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
88
- util_logger.info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
89
- await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
90
- // `integrations` is inferred from the backend sources in the working tree and
91
- // reconciled to exactly match on register — see `deployBackend` for why this
92
- // depends on deploying from the app's latest merged state.
93
- const integrations = await commands_backend.deployBackend({
88
+ // Runs before the frontend upload so a `deployBackend` failure can't leave a
89
+ // preview's unversioned `MF` dir overwritten with no rollback.
90
+ const { integrations, handlerNames, apiUsages } = await commands_backend.deployBackend({
94
91
  appDir: process.cwd(),
95
92
  appName,
96
93
  mfKeyPrefix: keyPrefix,
@@ -98,16 +95,32 @@ async function uploadAndRegister(opts) {
98
95
  deployedBy: util_machineIdentity.whoAmI(),
99
96
  isPreview,
100
97
  });
101
- await util_broker_app.registerOrUpdateApp({
102
- appName,
103
- token,
104
- nextVersion,
105
- entry,
106
- gitRemote,
107
- integrations,
108
- description,
109
- masterAppName,
110
- });
98
+ try {
99
+ util_logger.info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
100
+ await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
101
+ await util_broker_app.registerOrUpdateApp({
102
+ appName,
103
+ token,
104
+ nextVersion,
105
+ entry,
106
+ gitRemote,
107
+ integrations,
108
+ handlerNames,
109
+ apiUsages,
110
+ description,
111
+ masterAppName,
112
+ });
113
+ }
114
+ catch (err) {
115
+ // A live deploy is safe either way — RDS's version gates what's actually served, so
116
+ // an orphaned S3 object is harmless — but a preview is served straight from its
117
+ // unversioned prefix with no such gate, so only it needs the warning below.
118
+ if (isPreview) {
119
+ util_logger.warn(`preview backend was already deployed before this failed — re-run "z2h-cli deploy --preview" to retry ` +
120
+ `(every step overwrites, so a retry fully repairs it)`);
121
+ }
122
+ throw err;
123
+ }
111
124
  }
112
125
  async function deployPreview({ paths, consumerName, appName, token, opts }) {
113
126
  // A preview inherits from the live app it previews, resolved by name server-side —
@@ -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":""}
@@ -1,9 +1,10 @@
1
1
  export declare function hasHandlers(appDir: string): boolean;
2
+ export interface ReadDetectAndUploadResult {
3
+ integrations: string[];
4
+ handlerNames: string[];
5
+ apiUsages: Record<string, unknown>;
6
+ }
2
7
  /**
3
- * Deploy the app's backend handlers to S3 and return the integrations detected
4
- * from their sources. Each `.js` handler is uploaded as its own flat file — no
5
- * compilation, no bundling, no npm deps.
6
- *
7
8
  * Integration sync note: the returned set is re-derived from the CURRENT backend
8
9
  * sources on every deploy, and the platform set is reconciled to exactly match
9
10
  * it (there is no stored declaration to fall back on). A deploy therefore always
@@ -17,13 +18,18 @@ export declare function deployBackend(opts: {
17
18
  version: number;
18
19
  deployedBy: string;
19
20
  isPreview: boolean;
20
- }): Promise<string[]>;
21
+ }): Promise<ReadDetectAndUploadResult>;
21
22
  /**
22
23
  * Runs the same checks deploy does against the working tree, refreshing the
23
24
  * local `usage.json` — no upload to S3. Throws on the first invalid handler.
24
25
  */
25
26
  export declare function backendValidateCommand(appDir?: string): Promise<void>;
27
+ /**
28
+ * Runs a handler from the working tree (or `--code`) on the runner's sandbox and
29
+ * prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
30
+ */
26
31
  export declare function backendInvokeCommand(handlerName: string, opts?: {
27
32
  input?: string;
28
- }): Promise<void>;
33
+ code?: string;
34
+ }, appDir?: string): Promise<void>;
29
35
  //# 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;AAoBD,MAAM,WAAW,yBAAyB;IACxC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAiCD;;;;;;GAMG;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,yBAAyB,CAAC,CASrC;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"}