@mondaydotcomorg/z2h-cli 0.27.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/backend/__tests__/ast.test.d.ts +2 -0
  2. package/dist/backend/__tests__/ast.test.d.ts.map +1 -0
  3. package/dist/backend/__tests__/parse.test.d.ts +2 -0
  4. package/dist/backend/__tests__/parse.test.d.ts.map +1 -0
  5. package/dist/backend/__tests__/validate.test.d.ts +2 -0
  6. package/dist/backend/__tests__/validate.test.d.ts.map +1 -0
  7. package/dist/backend/ast.d.ts +41 -0
  8. package/dist/backend/ast.d.ts.map +1 -0
  9. package/dist/backend/ast.js +152 -0
  10. package/dist/backend/detect-integrations.d.ts +8 -4
  11. package/dist/backend/detect-integrations.d.ts.map +1 -1
  12. package/dist/backend/detect-integrations.js +12 -15
  13. package/dist/backend/handler-declaration.d.ts +17 -0
  14. package/dist/backend/handler-declaration.d.ts.map +1 -0
  15. package/dist/backend/handler-declaration.js +83 -0
  16. package/dist/backend/handlers.d.ts +2 -0
  17. package/dist/backend/handlers.d.ts.map +1 -1
  18. package/dist/backend/handlers.js +7 -36
  19. package/dist/backend/messages.d.ts +10 -0
  20. package/dist/backend/messages.d.ts.map +1 -0
  21. package/dist/backend/messages.js +14 -0
  22. package/dist/backend/parse.d.ts +11 -0
  23. package/dist/backend/parse.d.ts.map +1 -0
  24. package/dist/backend/parse.js +66 -0
  25. package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts +2 -0
  26. package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +1 -0
  27. package/dist/backend/rules/index.d.ts +5 -0
  28. package/dist/backend/rules/index.d.ts.map +1 -0
  29. package/dist/backend/rules/index.js +8 -0
  30. package/dist/backend/rules/snowflake-static-sql.d.ts +15 -0
  31. package/dist/backend/rules/snowflake-static-sql.d.ts.map +1 -0
  32. package/dist/backend/rules/snowflake-static-sql.js +75 -0
  33. package/dist/backend/rules/types.d.ts +29 -0
  34. package/dist/backend/rules/types.d.ts.map +1 -0
  35. package/dist/backend/rules/types.js +1 -0
  36. package/dist/backend/validate.d.ts +23 -0
  37. package/dist/backend/validate.d.ts.map +1 -0
  38. package/dist/backend/validate.js +64 -0
  39. package/dist/commands/__tests__/backend-validate.test.d.ts +2 -0
  40. package/dist/commands/__tests__/backend-validate.test.d.ts.map +1 -0
  41. package/dist/commands/backend.d.ts +6 -0
  42. package/dist/commands/backend.d.ts.map +1 -1
  43. package/dist/commands/backend.js +22 -2
  44. package/dist/commands/create.d.ts.map +1 -1
  45. package/dist/commands/create.js +10 -3
  46. package/dist/commands/generate.d.ts.map +1 -1
  47. package/dist/commands/generate.js +2 -1
  48. package/dist/constants.d.ts +1 -2
  49. package/dist/constants.d.ts.map +1 -1
  50. package/dist/constants.js +10 -6
  51. package/dist/esm/backend/__tests__/ast.test.d.ts +2 -0
  52. package/dist/esm/backend/__tests__/ast.test.d.ts.map +1 -0
  53. package/dist/esm/backend/__tests__/parse.test.d.ts +2 -0
  54. package/dist/esm/backend/__tests__/parse.test.d.ts.map +1 -0
  55. package/dist/esm/backend/__tests__/validate.test.d.ts +2 -0
  56. package/dist/esm/backend/__tests__/validate.test.d.ts.map +1 -0
  57. package/dist/esm/backend/ast.d.ts +41 -0
  58. package/dist/esm/backend/ast.d.ts.map +1 -0
  59. package/dist/esm/backend/ast.mjs +146 -0
  60. package/dist/esm/backend/detect-integrations.d.ts +8 -4
  61. package/dist/esm/backend/detect-integrations.d.ts.map +1 -1
  62. package/dist/esm/backend/detect-integrations.mjs +12 -15
  63. package/dist/esm/backend/handler-declaration.d.ts +17 -0
  64. package/dist/esm/backend/handler-declaration.d.ts.map +1 -0
  65. package/dist/esm/backend/handler-declaration.mjs +78 -0
  66. package/dist/esm/backend/handlers.d.ts +2 -0
  67. package/dist/esm/backend/handlers.d.ts.map +1 -1
  68. package/dist/esm/backend/handlers.mjs +7 -36
  69. package/dist/esm/backend/messages.d.ts +10 -0
  70. package/dist/esm/backend/messages.d.ts.map +1 -0
  71. package/dist/esm/backend/messages.mjs +12 -0
  72. package/dist/esm/backend/parse.d.ts +11 -0
  73. package/dist/esm/backend/parse.d.ts.map +1 -0
  74. package/dist/esm/backend/parse.mjs +64 -0
  75. package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts +2 -0
  76. package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +1 -0
  77. package/dist/esm/backend/rules/index.d.ts +5 -0
  78. package/dist/esm/backend/rules/index.d.ts.map +1 -0
  79. package/dist/esm/backend/rules/index.mjs +6 -0
  80. package/dist/esm/backend/rules/snowflake-static-sql.d.ts +15 -0
  81. package/dist/esm/backend/rules/snowflake-static-sql.d.ts.map +1 -0
  82. package/dist/esm/backend/rules/snowflake-static-sql.mjs +73 -0
  83. package/dist/esm/backend/rules/types.d.ts +29 -0
  84. package/dist/esm/backend/rules/types.d.ts.map +1 -0
  85. package/dist/esm/backend/rules/types.mjs +1 -0
  86. package/dist/esm/backend/validate.d.ts +23 -0
  87. package/dist/esm/backend/validate.d.ts.map +1 -0
  88. package/dist/esm/backend/validate.mjs +62 -0
  89. package/dist/esm/commands/__tests__/backend-validate.test.d.ts +2 -0
  90. package/dist/esm/commands/__tests__/backend-validate.test.d.ts.map +1 -0
  91. package/dist/esm/commands/backend.d.ts +6 -0
  92. package/dist/esm/commands/backend.d.ts.map +1 -1
  93. package/dist/esm/commands/backend.mjs +23 -4
  94. package/dist/esm/commands/create.d.ts.map +1 -1
  95. package/dist/esm/commands/create.mjs +11 -4
  96. package/dist/esm/commands/generate.d.ts.map +1 -1
  97. package/dist/esm/commands/generate.mjs +2 -1
  98. package/dist/esm/constants.d.ts +1 -2
  99. package/dist/esm/constants.d.ts.map +1 -1
  100. package/dist/esm/constants.mjs +3 -6
  101. package/dist/esm/index.mjs +6 -2
  102. package/dist/esm/util/broker/app.d.ts +12 -2
  103. package/dist/esm/util/broker/app.d.ts.map +1 -1
  104. package/dist/esm/util/broker/app.mjs +20 -2
  105. package/dist/index.js +5 -1
  106. package/dist/util/broker/app.d.ts +12 -2
  107. package/dist/util/broker/app.d.ts.map +1 -1
  108. package/dist/util/broker/app.js +20 -1
  109. package/package.json +4 -2
  110. package/src/backend/__tests__/ast.test.ts +53 -0
  111. package/src/backend/__tests__/detect-integrations.test.ts +12 -30
  112. package/src/backend/__tests__/handlers.test.ts +21 -34
  113. package/src/backend/__tests__/parse.test.ts +25 -0
  114. package/src/backend/__tests__/validate.test.ts +246 -0
  115. package/src/backend/ast.ts +204 -0
  116. package/src/backend/detect-integrations.ts +12 -14
  117. package/src/backend/handler-declaration.ts +104 -0
  118. package/src/backend/handlers.ts +9 -44
  119. package/src/backend/messages.ts +16 -0
  120. package/src/backend/parse.ts +82 -0
  121. package/src/backend/rules/__tests__/snowflake-static-sql.test.ts +175 -0
  122. package/src/backend/rules/index.ts +7 -0
  123. package/src/backend/rules/snowflake-static-sql.ts +78 -0
  124. package/src/backend/rules/types.ts +30 -0
  125. package/src/backend/validate.ts +73 -0
  126. package/src/commands/__tests__/backend-deploy.test.ts +5 -3
  127. package/src/commands/__tests__/backend-validate.test.ts +71 -0
  128. package/src/commands/__tests__/create.test.ts +46 -0
  129. package/src/commands/__tests__/edit.test.ts +6 -0
  130. package/src/commands/backend.ts +23 -3
  131. package/src/commands/create.ts +13 -4
  132. package/src/commands/generate.ts +2 -1
  133. package/src/constants.ts +5 -6
  134. package/src/index.ts +9 -2
  135. package/src/util/broker/__tests__/app.test.ts +29 -1
  136. package/src/util/broker/app.ts +24 -2
@@ -4,7 +4,7 @@ import { PutObjectCommand } from '@aws-sdk/client-s3';
4
4
  import { output, info } from '../util/logger.mjs';
5
5
  import { getCwdAppName, mfAppName } from '../util/app-name.mjs';
6
6
  import { readHandlers } from '../backend/handlers.mjs';
7
- import { detectIntegrationsFromHandlers } from '../backend/detect-integrations.mjs';
7
+ import { detectIntegrations } from '../backend/detect-integrations.mjs';
8
8
  import { getS3Client } from '../util/s3/client.mjs';
9
9
  import { BUCKET_WRITE_NAME } from '../constants.mjs';
10
10
  import { invokeBackend } from '../backend/client.mjs';
@@ -36,8 +36,8 @@ async function readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel
36
36
  return [];
37
37
  }
38
38
  info(`reading backend handlers ${logLabel}`);
39
- const { handlers } = await readHandlers(appDir);
40
- const integrations = detectIntegrationsFromHandlers(handlers);
39
+ const { handlers, apiDomains } = await readHandlers(appDir);
40
+ const integrations = detectIntegrations(apiDomains);
41
41
  if (Object.keys(handlers).length === 0) {
42
42
  return integrations;
43
43
  }
@@ -77,6 +77,25 @@ async function deployBackend(opts) {
77
77
  throw tagDeployError('deploy_backend_error', err);
78
78
  }
79
79
  }
80
+ /**
81
+ * Run the same handler validation deploy runs (syntax, isolate constraints,
82
+ * handler rules, integration detection) against the working tree — without
83
+ * uploading anything. Throws on the first invalid handler.
84
+ */
85
+ async function backendValidateCommand(appDir = process.cwd()) {
86
+ if (!hasHandlers(appDir)) {
87
+ output('[z2h-cli] no backend/handlers/ directory — nothing to validate', { handlers: [], integrations: [] });
88
+ return;
89
+ }
90
+ const { handlers, apiDomains } = await readHandlers(appDir);
91
+ const names = Object.keys(handlers).sort();
92
+ const integrations = detectIntegrations(apiDomains);
93
+ const list = (xs) => (xs.length ? xs.join(', ') : 'none');
94
+ output(`[z2h-cli] ${names.length} handler(s) valid: ${list(names)}\n[z2h-cli] integrations: ${list(integrations)}`, {
95
+ handlers: names,
96
+ integrations,
97
+ });
98
+ }
80
99
  async function backendInvokeCommand(handlerName, opts = {}) {
81
100
  const appName = currentAppName();
82
101
  let input = null;
@@ -92,4 +111,4 @@ async function backendInvokeCommand(handlerName, opts = {}) {
92
111
  output(`[z2h-cli] result (${Math.round(res.durationMs)}ms):\n${JSON.stringify(res.result, null, 2)}`, res);
93
112
  }
94
113
 
95
- export { backendInvokeCommand, deployBackend, hasHandlers };
114
+ export { backendInvokeCommand, backendValidateCommand, deployBackend, hasHandlers };
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF5F"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6F5F"}
@@ -8,7 +8,7 @@ import { runYarnInstall } from '../util/npm/install.mjs';
8
8
  import { initRepo } from '../util/git/repo.mjs';
9
9
  import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
10
10
  export { REMOTE_TYPES } from '../util/git/remote-options.mjs';
11
- import { fetchApp } from '../util/broker/app.mjs';
11
+ import { fetchApp, claimApp } from '../util/broker/app.mjs';
12
12
  import { validateAppName, mfAppName } from '../util/app-name.mjs';
13
13
  import { prepareShadow } from '../shadow/prepare.mjs';
14
14
 
@@ -53,11 +53,19 @@ async function createCommand(appName, opts = {}) {
53
53
  if (!(await isDirEmpty(appDir))) {
54
54
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
55
55
  }
56
- // Check the broker — fail fast if the app is already registered.
56
+ // Check the broker — fail fast if the app already has a real release.
57
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past:
58
+ // either it's this same name being re-created after a local wipe (we'd only
59
+ // see it at all if we still have access to it), or it genuinely doesn't
60
+ // exist yet and we claim it below.
57
61
  const existing = await fetchApp(mfAppName(appName));
58
- if (existing) {
62
+ if (existing?.deployed) {
59
63
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
60
64
  }
65
+ const gitRemote = resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
66
+ if (!existing) {
67
+ await claimApp(mfAppName(appName), gitRemote);
68
+ }
61
69
  // Compute substitutions.
62
70
  const vars = {
63
71
  __APP_NAME__: appName,
@@ -82,7 +90,6 @@ async function createCommand(appName, opts = {}) {
82
90
  await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
83
91
  }
84
92
  info(`initializing git repo for ${appName}...`);
85
- const gitRemote = resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
86
93
  await initRepo(appDir, gitRemote);
87
94
  info(`installing dependencies for ${appName}...`);
88
95
  await runYarnInstall({ cwd: workspace });
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA6E3F"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA8E3F"}
@@ -128,8 +128,9 @@ async function generateCommand(appName, opts) {
128
128
  if (!(await isDirEmpty(appDir))) {
129
129
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
130
130
  }
131
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past — only a real release blocks generate.
131
132
  const existing = await fetchApp(mfAppName(appName));
132
- if (existing) {
133
+ if (existing?.deployed) {
133
134
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
134
135
  }
135
136
  info(`copying template "${template.id}" → ${appDir}`);
@@ -20,8 +20,7 @@ export declare const S3_SKILLS_BUCKET_NAME = "prod-use1-bigbrain-zth-skills";
20
20
  export declare const DATAVIZ_SKILLS_PREFIX = "dataviz/";
21
21
  export declare const APP_NAME_PATTERN: RegExp;
22
22
  export declare const PREVIEW_APP_NAME_PREFIX = "zth-preview-";
23
- export declare const DECLARED_INTEGRATIONS: readonly ["monday"];
24
- export type DeclaredIntegration = (typeof DECLARED_INTEGRATIONS)[number];
23
+ export { ALWAYS_ON_CHANNELS, DECLARED_INTEGRATIONS, type DeclaredIntegration } from '@mondaydotcomorg/z2h-shared-utils';
25
24
  export declare const DEFAULT_BROKER_BASE_URL = "https://bigbrain-zth.bigbrain.me";
26
25
  /**
27
26
  * Resolves the broker origin (no trailing slash). Honors
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,SAAS,CAAC;AAGtC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEjD,CAAC;AAKF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAElD,CAAC;AAMF,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAC9B,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAG9B,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAQ3C,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAK7C,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAM1E,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAIrE,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAMhD,eAAO,MAAM,gBAAgB,QAA2B,CAAC;AAEzD,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAMtD,eAAO,MAAM,qBAAqB,qBAAsB,CAAC;AACzD,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAKzE,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAID,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGvD;AAKD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAG9C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAGnD,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAMrE,eAAO,MAAM,iBAAiB,EAAG,eAAwB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,SAAS,CAAC;AAGtC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEjD,CAAC;AAKF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAElD,CAAC;AAMF,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAC9B,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAG9B,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAQ3C,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAK7C,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAM1E,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAIrE,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAMhD,eAAO,MAAM,gBAAgB,QAA2B,CAAC;AAEzD,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAMtD,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxH,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAID,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGvD;AAKD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAG9C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAGnD,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAMrE,eAAO,MAAM,iBAAiB,EAAG,eAAwB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
@@ -1,3 +1,5 @@
1
+ export { ALWAYS_ON_CHANNELS, DECLARED_INTEGRATIONS } from '@mondaydotcomorg/z2h-shared-utils';
2
+
1
3
  /**
2
4
  * Cross-file constants. Narrowly-used literals (templates, prefixes used in
3
5
  * one place) stay local to their file to keep this index small.
@@ -55,11 +57,6 @@ const DATAVIZ_SKILLS_PREFIX = 'dataviz/';
55
57
  // upfront for friendlier errors.
56
58
  const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
57
59
  const PREVIEW_APP_NAME_PREFIX = 'zth-preview-';
58
- // ─────────── Declared (OAuth-gated) backend integrations ─────
59
- // Closed set the CLI detects from handler sources and the broker validates on
60
- // register/update. Always-on channels (`db`, `llm`, `snowflake`) are not listed —
61
- // the runner builds those regardless. Shared via `@mondaydotcomorg/z2h-cli/constants`.
62
- const DECLARED_INTEGRATIONS = ['monday'];
63
60
  // ─────────── Broker origin + npm registry (shared §3 contract) ─────
64
61
  // The bigbrain-zth broker fronts both the STS credential route and the npm
65
62
  // proxy. `Z2H_BROKER_BASE_URL` overrides the origin for tests/staging.
@@ -101,4 +98,4 @@ function brokerNpmRegistryUrl() {
101
98
  return `${brokerBaseUrl()}${NPM_BROKER_PATH}`;
102
99
  }
103
100
 
104
- export { APP_NAME_PATTERN, BROKER_NPM_SCOPE, BUCKET_READ_DNS_NAME, BUCKET_READ_DNS_URL, BUCKET_WRITE_NAME, BUCKET_WRITE_URL, DATAVIZ_SKILLS_PREFIX, DECLARED_INTEGRATIONS, DEFAULT_BROKER_BASE_URL, GITIGNORE_ENTRIES, MANIFEST_KEY, MANIFEST_READ_URL, MAX_PORT, MIN_PORT, NPM_BROKER_PATH, PREVIEW_APP_NAME_PREFIX, PUBLIC_NPM_REGISTRY_URL, REGION, S3_PREVIEWS_FOLDER, S3_SKILLS_BUCKET_NAME, SHADOW_DEV_DEPS, SHADOW_DIR_NAME, SHADOW_SCRIPTS, Z2H_NPM_TOKEN_ENV, brokerBaseUrl, brokerNpmRegistryUrl, routingHeaders };
101
+ export { APP_NAME_PATTERN, BROKER_NPM_SCOPE, BUCKET_READ_DNS_NAME, BUCKET_READ_DNS_URL, BUCKET_WRITE_NAME, BUCKET_WRITE_URL, DATAVIZ_SKILLS_PREFIX, DEFAULT_BROKER_BASE_URL, GITIGNORE_ENTRIES, MANIFEST_KEY, MANIFEST_READ_URL, MAX_PORT, MIN_PORT, NPM_BROKER_PATH, PREVIEW_APP_NAME_PREFIX, PUBLIC_NPM_REGISTRY_URL, REGION, S3_PREVIEWS_FOLDER, S3_SKILLS_BUCKET_NAME, SHADOW_DEV_DEPS, SHADOW_DIR_NAME, SHADOW_SCRIPTS, Z2H_NPM_TOKEN_ENV, brokerBaseUrl, brokerNpmRegistryUrl, routingHeaders };
@@ -17,7 +17,7 @@ import { generateZ2HTokenCommand } from './commands/generate-z2h-token.mjs';
17
17
  import { migrateCommand } from './commands/migrate.mjs';
18
18
  import { tagCommand } from './commands/tag.mjs';
19
19
  import { editCommand } from './commands/edit.mjs';
20
- import { backendInvokeCommand } from './commands/backend.mjs';
20
+ import { backendValidateCommand, backendInvokeCommand } from './commands/backend.mjs';
21
21
  import { doctorCommand } from './commands/doctor.mjs';
22
22
  import { fail, errorLog, setOutputMode, getOutputMode } from './util/logger.mjs';
23
23
  import { runCommand } from './util/run-command.mjs';
@@ -170,7 +170,11 @@ async function main() {
170
170
  .action(runCommand({ data: cwdApp }, (opts) => deleteCommand(opts)));
171
171
  const backend = program
172
172
  .command('backend')
173
- .description("Inspect the app's backend: invoke a deployed handler by name");
173
+ .description("Inspect the app's backend: validate handler sources or invoke a deployed handler by name");
174
+ backend
175
+ .command('validate')
176
+ .description('Validate backend/handlers/*.js exactly as deploy would (syntax, isolate rules, integrations) without uploading')
177
+ .action(runCommand({ data: cwdApp }, () => backendValidateCommand()));
174
178
  backend
175
179
  .command('invoke <handlerName>')
176
180
  .description('Invoke a deployed backend handler by name (the filename without .ts) — mainly for testing a handler without going through the frontend')
@@ -2,16 +2,26 @@ import type { ManifestEntry } from '../manifest';
2
2
  export interface AppInfo {
3
3
  appName: string;
4
4
  version: number;
5
- entry: Record<string, unknown>;
5
+ /** Null for a claimed-but-unpublished app — reserved via `create`, never deployed yet. */
6
+ entry: Record<string, unknown> | null;
7
+ /** True once a real release has landed (`entry` is set) — false for a claimed-but-unpublished stub. */
8
+ deployed: boolean;
6
9
  deployedBy: string;
7
10
  gitRemote: string;
8
11
  createdBy: string;
9
12
  }
10
13
  /**
11
14
  * Fetches a single app's current state from the broker.
12
- * Returns null if the app has never been deployed (404).
15
+ * Returns null if the app has never even been claimed (404).
13
16
  */
14
17
  export declare function fetchApp(appName: string): Promise<AppInfo | null>;
18
+ /**
19
+ * Reserves an app name (+ owner grant) before any deploy — called once from
20
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
21
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
22
+ * claimed the exact same name in the gap between the two calls.
23
+ */
24
+ export declare function claimApp(appName: string, gitRemote: string): Promise<void>;
15
25
  export interface RegisterOrUpdateOptions {
16
26
  appName: string;
17
27
  token: string;
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
@@ -6,7 +6,7 @@ import { tagDeployError, DeployError } from '../errors.mjs';
6
6
 
7
7
  /**
8
8
  * Fetches a single app's current state from the broker.
9
- * Returns null if the app has never been deployed (404).
9
+ * Returns null if the app has never even been claimed (404).
10
10
  */
11
11
  async function fetchApp(appName) {
12
12
  const { token } = await readZthAuth();
@@ -29,6 +29,24 @@ async function fetchApp(appName) {
29
29
  }
30
30
  return res.json();
31
31
  }
32
+ /**
33
+ * Reserves an app name (+ owner grant) before any deploy — called once from
34
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
35
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
36
+ * claimed the exact same name in the gap between the two calls.
37
+ */
38
+ async function claimApp(appName, gitRemote) {
39
+ const { token } = await readZthAuth();
40
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/claim`, {
41
+ method: 'POST',
42
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
43
+ body: JSON.stringify({ appName, gitRemote }),
44
+ });
45
+ if (!res.ok) {
46
+ const text = await res.text();
47
+ throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
48
+ }
49
+ }
32
50
  async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
33
51
  const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
34
52
  method: 'POST',
@@ -78,4 +96,4 @@ async function registerOrUpdateApp(opts) {
78
96
  }
79
97
  }
80
98
 
81
- export { fetchApp, registerOrUpdateApp };
99
+ export { claimApp, fetchApp, registerOrUpdateApp };
package/dist/index.js CHANGED
@@ -176,7 +176,11 @@ async function main() {
176
176
  .action(util_runCommand.runCommand({ data: cwdApp }, (opts) => commands_delete.deleteCommand(opts)));
177
177
  const backend = program
178
178
  .command('backend')
179
- .description("Inspect the app's backend: invoke a deployed handler by name");
179
+ .description("Inspect the app's backend: validate handler sources or invoke a deployed handler by name");
180
+ backend
181
+ .command('validate')
182
+ .description('Validate backend/handlers/*.js exactly as deploy would (syntax, isolate rules, integrations) without uploading')
183
+ .action(util_runCommand.runCommand({ data: cwdApp }, () => commands_backend.backendValidateCommand()));
180
184
  backend
181
185
  .command('invoke <handlerName>')
182
186
  .description('Invoke a deployed backend handler by name (the filename without .ts) — mainly for testing a handler without going through the frontend')
@@ -2,16 +2,26 @@ import type { ManifestEntry } from '../manifest';
2
2
  export interface AppInfo {
3
3
  appName: string;
4
4
  version: number;
5
- entry: Record<string, unknown>;
5
+ /** Null for a claimed-but-unpublished app — reserved via `create`, never deployed yet. */
6
+ entry: Record<string, unknown> | null;
7
+ /** True once a real release has landed (`entry` is set) — false for a claimed-but-unpublished stub. */
8
+ deployed: boolean;
6
9
  deployedBy: string;
7
10
  gitRemote: string;
8
11
  createdBy: string;
9
12
  }
10
13
  /**
11
14
  * Fetches a single app's current state from the broker.
12
- * Returns null if the app has never been deployed (404).
15
+ * Returns null if the app has never even been claimed (404).
13
16
  */
14
17
  export declare function fetchApp(appName: string): Promise<AppInfo | null>;
18
+ /**
19
+ * Reserves an app name (+ owner grant) before any deploy — called once from
20
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
21
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
22
+ * claimed the exact same name in the gap between the two calls.
23
+ */
24
+ export declare function claimApp(appName: string, gitRemote: string): Promise<void>;
15
25
  export interface RegisterOrUpdateOptions {
16
26
  appName: string;
17
27
  token: string;
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
@@ -8,7 +8,7 @@ const util_errors = require('../errors.js');
8
8
 
9
9
  /**
10
10
  * Fetches a single app's current state from the broker.
11
- * Returns null if the app has never been deployed (404).
11
+ * Returns null if the app has never even been claimed (404).
12
12
  */
13
13
  async function fetchApp(appName) {
14
14
  const { token } = await util_auth_authStore.readZthAuth();
@@ -31,6 +31,24 @@ async function fetchApp(appName) {
31
31
  }
32
32
  return res.json();
33
33
  }
34
+ /**
35
+ * Reserves an app name (+ owner grant) before any deploy — called once from
36
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
37
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
38
+ * claimed the exact same name in the gap between the two calls.
39
+ */
40
+ async function claimApp(appName, gitRemote) {
41
+ const { token } = await util_auth_authStore.readZthAuth();
42
+ const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/claim`, {
43
+ method: 'POST',
44
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
45
+ body: JSON.stringify({ appName, gitRemote }),
46
+ });
47
+ if (!res.ok) {
48
+ const text = await res.text();
49
+ throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
50
+ }
51
+ }
34
52
  async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
35
53
  const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
36
54
  method: 'POST',
@@ -80,5 +98,6 @@ async function registerOrUpdateApp(opts) {
80
98
  }
81
99
  }
82
100
 
101
+ exports.claimApp = claimApp;
83
102
  exports.fetchApp = fetchApp;
84
103
  exports.registerOrUpdateApp = registerOrUpdateApp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -52,7 +52,9 @@
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-s3": "^3.706.0",
54
54
  "@mondaydotcomorg/z2h-dashboard-templates": "^0.3.4",
55
- "@mondaydotcomorg/z2h-shared-utils": "^0.5.0",
55
+ "@mondaydotcomorg/z2h-shared-utils": "^0.6.0",
56
+ "acorn": "^8.14.0",
57
+ "acorn-walk": "^8.3.4",
56
58
  "chokidar": "^3.6.0",
57
59
  "commander": "^12.1.0",
58
60
  "cors": "^2.8.6",
@@ -0,0 +1,53 @@
1
+ import { parse, type ExpressionStatement, type Program } from 'acorn';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { collectBindings, createPathResolver } from '../ast';
4
+
5
+ const program = (source: string): Program => parse(source, { ecmaVersion: 'latest', sourceType: 'script' });
6
+
7
+ /** Resolves the expression in the program's last statement. */
8
+ const resolveLast = (source: string, contextParam?: string) => {
9
+ const p = program(source);
10
+ const last = p.body[p.body.length - 1] as ExpressionStatement;
11
+ return createPathResolver(collectBindings(p), contextParam).pathOf(last.expression);
12
+ };
13
+
14
+ describe('createPathResolver', () => {
15
+ it('returns the literal chain when nothing is aliased', () => {
16
+ expect(resolveLast('ctx.api.v1.db.get;')).toBe('ctx.api.v1.db.get');
17
+ });
18
+
19
+ it('treats computed string literals as static keys', () => {
20
+ expect(resolveLast('ctx.api.v1["db"].get;')).toBe('ctx.api.v1.db.get');
21
+ expect(resolveLast("const { ['db']: store } = ctx.api.v1; store.get;")).toBe('ctx.api.v1.db.get');
22
+ });
23
+
24
+ it('returns undefined for dynamic links and non-identifier roots', () => {
25
+ expect(resolveLast('ctx.api.v1[db].get;')).toBeUndefined();
26
+ expect(resolveLast('fn().db;')).toBeUndefined();
27
+ expect(resolveLast('"x";')).toBeUndefined();
28
+ });
29
+
30
+ it('rewrites the handler context parameter to ctx', () => {
31
+ expect(resolveLast('context.api.v1.db.get;', 'context')).toBe('ctx.api.v1.db.get');
32
+ expect(resolveLast('const sf = context.api.v1.snowflake; sf.query;', 'context')).toBe('ctx.api.v1.snowflake.query');
33
+ });
34
+
35
+ it('follows const aliases, including destructuring and chains', () => {
36
+ expect(resolveLast('const sf = ctx.api.v1.snowflake; sf.query;')).toBe('ctx.api.v1.snowflake.query');
37
+ expect(resolveLast('const { v1 } = ctx.api; v1.db;')).toBe('ctx.api.v1.db');
38
+ expect(resolveLast('const { v1: { db: store } } = ctx.api; store.get;')).toBe('ctx.api.v1.db.get');
39
+ expect(resolveLast('const a = ctx.api; const b = a.v1; const c = b.db; c;')).toBe('ctx.api.v1.db');
40
+ });
41
+
42
+ it('leaves unpinnable names as written', () => {
43
+ expect(resolveLast('let sf = ctx.api.v1.snowflake; sf.query;')).toBe('sf.query');
44
+ expect(resolveLast('const sf = ctx.api.v1.snowflake; sf = 1; sf.query;')).toBe('sf.query');
45
+ expect(resolveLast('const sf = ctx.api.v1.snowflake; function f(sf) {} sf.query;')).toBe('sf.query');
46
+ expect(resolveLast('const sf = ctx.api.v1.snowflake; { const sf = 1; } sf.query;')).toBe('sf.query');
47
+ expect(resolveLast('const [sf] = ctx.api; sf.query;')).toBe('sf.query');
48
+ });
49
+
50
+ it('does not loop on circular aliases', () => {
51
+ expect(resolveLast('const a = b.x; const b = a.y; a.z;')).toBe('a.y.x.z');
52
+ });
53
+ });
@@ -1,42 +1,24 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { detectIntegrationsFromHandlers } from '../detect-integrations';
2
+ import { detectIntegrations } from '../detect-integrations';
3
3
 
4
- describe('detectIntegrationsFromHandlers', () => {
5
- it('returns [] when handlers are empty', () => {
6
- expect(detectIntegrationsFromHandlers({})).toEqual([]);
4
+ describe('detectIntegrations', () => {
5
+ it('returns [] when no domains are touched', () => {
6
+ expect(detectIntegrations([])).toEqual([]);
7
7
  });
8
8
 
9
- it('returns [] when sources never touch ctx.api.v1.monday', () => {
10
- expect(
11
- detectIntegrationsFromHandlers({
12
- ping: 'async function handler(ctx) { return ctx.db.get("x"); }',
13
- q: 'async function handler(ctx) { return ctx.api.v1.snowflake.runQuery("select 1"); }',
14
- })
15
- ).toEqual([]);
9
+ it('ignores always-on channels', () => {
10
+ expect(detectIntegrations(['db', 'llm', 'snowflake'])).toEqual([]);
16
11
  });
17
12
 
18
- it('detects monday from ctx.api.v1.monday', () => {
19
- expect(
20
- detectIntegrationsFromHandlers({
21
- boards: 'async function handler(ctx) {\n return ctx.api.v1.monday.query("{ boards { id } }");\n}',
22
- })
23
- ).toEqual(['monday']);
13
+ it('ignores unknown domains', () => {
14
+ expect(detectIntegrations(['totallyMadeUp'])).toEqual([]);
24
15
  });
25
16
 
26
- it('dedupes across handlers', () => {
27
- expect(
28
- detectIntegrationsFromHandlers({
29
- a: 'ctx.api.v1.monday.query("x")',
30
- b: 'ctx.api.v1.monday.query("y"); ctx.api.v1.snowflake.runQuery("z")',
31
- })
32
- ).toEqual(['monday']);
17
+ it('keeps declared integrations', () => {
18
+ expect(detectIntegrations(['snowflake', 'monday', 'db'])).toEqual(['monday']);
33
19
  });
34
20
 
35
- it('ignores lookalike identifiers that are not ctx.api.v1.monday', () => {
36
- expect(
37
- detectIntegrationsFromHandlers({
38
- a: 'const monday = 1; api.v1.monday; ctx.api.v2.monday',
39
- })
40
- ).toEqual([]);
21
+ it('dedupes', () => {
22
+ expect(detectIntegrations(['monday', 'monday'])).toEqual(['monday']);
41
23
  });
42
24
  });
@@ -1,10 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { mkdtemp, rm, ensureDir, writeFile } from 'fs-extra';
4
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import { readHandlers } from '../handlers';
7
7
 
8
+ vi.mock('../../util/tracker', () => ({
9
+ trackEvent: vi.fn().mockResolvedValue(undefined),
10
+ trackDuration: vi.fn(),
11
+ }));
12
+
8
13
  let appDir: string;
9
14
 
10
15
  const writeHandler = (name: string, source: string) =>
@@ -23,16 +28,14 @@ describe('readHandlers — discovery', () => {
23
28
  it('returns empty handlers when backend/handlers does not exist', async () => {
24
29
  const dir = await mkdtemp(path.join(os.tmpdir(), 'z2h-empty-'));
25
30
  try {
26
- const result = await readHandlers(dir);
27
- expect(result.handlers).toEqual({});
31
+ await expect(readHandlers(dir)).resolves.toEqual({ handlers: {}, apiDomains: [] });
28
32
  } finally {
29
33
  await rm(dir, { recursive: true, force: true });
30
34
  }
31
35
  });
32
36
 
33
37
  it('returns empty handlers when the directory is empty', async () => {
34
- const result = await readHandlers(appDir);
35
- expect(result.handlers).toEqual({});
38
+ await expect(readHandlers(appDir)).resolves.toEqual({ handlers: {}, apiDomains: [] });
36
39
  });
37
40
 
38
41
  it('uses the filename (without .js) as the handler name', async () => {
@@ -57,43 +60,27 @@ describe('readHandlers — output contract', () => {
57
60
  expect(handlers['my_handler']).toBe(source);
58
61
  });
59
62
 
60
- it.each([
61
- ['a function declaration', 'async function handler(ctx) { return 1; }'],
62
- ['a const arrow', 'const handler = async (ctx) => 1;'],
63
- ['a var assignment', 'var handler = async function(ctx) { return 1; };'],
64
- ])('accepts %s', async (_label, source) => {
65
- await writeHandler('h.js', source);
66
- await expect(readHandlers(appDir)).resolves.toMatchObject({ handlers: { h: expect.any(String) } });
63
+ it('merges the ctx.api.v1 domains touched across all handlers, sorted and unique', async () => {
64
+ await writeHandler('a.js', 'async function handler(ctx) { return ctx.api.v1.monday.query("x"); }');
65
+ await writeHandler(
66
+ 'b.js',
67
+ 'async function handler(ctx) { ctx.api.v1.db.get("k"); return ctx.api.v1.monday.query("y"); }'
68
+ );
69
+ const { apiDomains } = await readHandlers(appDir);
70
+ expect(apiDomains).toEqual(['db', 'monday']);
67
71
  });
68
72
  });
69
73
 
74
+ // Per-shape validation rules are covered in validate.test.ts; here we only prove
75
+ // that readHandlers runs the validator and names the offending file.
70
76
  describe('readHandlers — validation', () => {
71
- it('rejects a file that does not define handler', async () => {
72
- await writeHandler('bad.js', 'async function main(ctx) { return 1; }');
73
- await expect(readHandlers(appDir)).rejects.toThrow(/must define a handler/);
74
- });
75
-
76
- it('rejects import statements', async () => {
77
- await writeHandler('bad.js', "import { foo } from './utils';\nasync function handler(ctx) { return foo; }");
78
- await expect(readHandlers(appDir)).rejects.toThrow(/must not import/);
79
- });
80
-
81
- it('rejects require calls', async () => {
82
- await writeHandler('bad.js', "const foo = require('./utils');\nasync function handler(ctx) { return foo; }");
83
- await expect(readHandlers(appDir)).rejects.toThrow(/must not import/);
84
- });
85
-
86
77
  it('rejects .ts handler files with a clear message', async () => {
87
78
  await writeHandler('call_snowflake.ts', 'async function handler(ctx) { return 1; }');
88
79
  await expect(readHandlers(appDir)).rejects.toThrow(/TypeScript handlers are not yet supported/);
89
80
  });
90
81
 
91
- it.each([
92
- ['an exported function declaration', 'export async function handler(ctx) { return 1; }'],
93
- ['an exported const', 'export const handler = async (ctx) => 1;'],
94
- ['a default-exported function', 'export default async function handler(ctx) { return 1; }'],
95
- ])('rejects %s — the isolate has no module system', async (_label, source) => {
96
- await writeHandler('bad.js', source);
97
- await expect(readHandlers(appDir)).rejects.toThrow(/must not export handler/);
82
+ it('surfaces validation errors with the filename', async () => {
83
+ await writeHandler('bad.js', 'async function handler(ctx) {\n return ctx.input.(;\n}');
84
+ await expect(readHandlers(appDir)).rejects.toThrow(/^bad\.js:2:19: not valid JavaScript/);
98
85
  });
99
86
  });
@@ -0,0 +1,25 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { trackEvent } from '../../util/tracker';
3
+ import { parseOrThrow } from '../parse';
4
+
5
+ vi.mock('../../util/tracker', () => ({
6
+ trackEvent: vi.fn().mockResolvedValue(undefined),
7
+ trackDuration: vi.fn(),
8
+ }));
9
+
10
+ const track = vi.mocked(trackEvent);
11
+
12
+ describe('parseOrThrow — telemetry', () => {
13
+ afterEach(() => {
14
+ track.mockClear();
15
+ });
16
+
17
+ it('tracks the same message it throws', () => {
18
+ expect(() => parseOrThrow('h.js', 'async function handler(ctx) {\n return ctx.input.(;\n}')).toThrow(
19
+ 'h.js:2:19: not valid JavaScript — Unexpected token'
20
+ );
21
+ expect(track).toHaveBeenCalledWith('z2h_cli_handler_parse_error', {
22
+ error: 'h.js:2:19: not valid JavaScript — Unexpected token',
23
+ });
24
+ });
25
+ });