@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
@@ -78,6 +78,7 @@ describe('edit', () => {
78
78
  version: 3,
79
79
  gitRemote: S3_REMOTE,
80
80
  entry: {},
81
+ deployed: true,
81
82
  deployedBy: 'u@x.com',
82
83
  createdBy: 'u@x.com',
83
84
  });
@@ -100,6 +101,7 @@ describe('edit', () => {
100
101
  version: 3,
101
102
  gitRemote: S3_REMOTE,
102
103
  entry: {},
104
+ deployed: true,
103
105
  deployedBy: 'u@x.com',
104
106
  createdBy: 'u@x.com',
105
107
  });
@@ -117,6 +119,7 @@ describe('edit', () => {
117
119
  version: 3,
118
120
  gitRemote: S3_REMOTE,
119
121
  entry: {},
122
+ deployed: true,
120
123
  deployedBy: 'u@x.com',
121
124
  createdBy: 'u@x.com',
122
125
  });
@@ -134,6 +137,7 @@ describe('edit', () => {
134
137
  version: 3,
135
138
  gitRemote: GITHUB_REMOTE,
136
139
  entry: {},
140
+ deployed: true,
137
141
  deployedBy: 'u@x.com',
138
142
  createdBy: 'u@x.com',
139
143
  });
@@ -151,6 +155,7 @@ describe('edit', () => {
151
155
  version: 3,
152
156
  gitRemote: S3_REMOTE,
153
157
  entry: {},
158
+ deployed: true,
154
159
  deployedBy: 'u@x.com',
155
160
  createdBy: 'u@x.com',
156
161
  });
@@ -172,6 +177,7 @@ describe('edit', () => {
172
177
  version: 3,
173
178
  gitRemote: S3_REMOTE,
174
179
  entry: {},
180
+ deployed: true,
175
181
  deployedBy: 'u@x.com',
176
182
  createdBy: 'u@x.com',
177
183
  });
@@ -4,7 +4,7 @@ import { PutObjectCommand } from '@aws-sdk/client-s3';
4
4
  import { info, output } from '../util/logger';
5
5
  import { mfAppName, getCwdAppName } from '../util/app-name';
6
6
  import { readHandlers } from '../backend/handlers';
7
- import { detectIntegrationsFromHandlers } from '../backend/detect-integrations';
7
+ import { detectIntegrations } from '../backend/detect-integrations';
8
8
  import { getS3Client } from '../util/s3/client';
9
9
  import { BUCKET_WRITE_NAME } from '../constants';
10
10
  import { invokeBackend } from '../backend/client';
@@ -44,8 +44,8 @@ async function readDetectAndUpload(
44
44
  return [];
45
45
  }
46
46
  info(`reading backend handlers ${logLabel}`);
47
- const { handlers } = await readHandlers(appDir);
48
- const integrations = detectIntegrationsFromHandlers(handlers);
47
+ const { handlers, apiDomains } = await readHandlers(appDir);
48
+ const integrations = detectIntegrations(apiDomains);
49
49
  if (Object.keys(handlers).length === 0) {
50
50
  return integrations;
51
51
  }
@@ -94,6 +94,26 @@ export async function deployBackend(opts: {
94
94
  }
95
95
  }
96
96
 
97
+ /**
98
+ * Run the same handler validation deploy runs (syntax, isolate constraints,
99
+ * handler rules, integration detection) against the working tree — without
100
+ * uploading anything. Throws on the first invalid handler.
101
+ */
102
+ export async function backendValidateCommand(appDir: string = process.cwd()): Promise<void> {
103
+ if (!hasHandlers(appDir)) {
104
+ output('[z2h-cli] no backend/handlers/ directory — nothing to validate', { handlers: [], integrations: [] });
105
+ return;
106
+ }
107
+ const { handlers, apiDomains } = await readHandlers(appDir);
108
+ const names = Object.keys(handlers).sort();
109
+ const integrations = detectIntegrations(apiDomains);
110
+ const list = (xs: string[]) => (xs.length ? xs.join(', ') : 'none');
111
+ output(`[z2h-cli] ${names.length} handler(s) valid: ${list(names)}\n[z2h-cli] integrations: ${list(integrations)}`, {
112
+ handlers: names,
113
+ integrations,
114
+ });
115
+ }
116
+
97
117
  export async function backendInvokeCommand(handlerName: string, opts: { input?: string } = {}): Promise<void> {
98
118
  const appName = currentAppName();
99
119
  let input: unknown = null;
@@ -7,7 +7,7 @@ import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from
7
7
  import { runYarnInstall } from '../util/npm/install';
8
8
  import { initRepo } from '../util/git/repo';
9
9
  import { REMOTE_TYPES, type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
10
- import { fetchApp } from '../util/broker/app';
10
+ import { fetchApp, claimApp } from '../util/broker/app';
11
11
  import { mfAppName, validateAppName } from '../util/app-name';
12
12
  import { prepareShadow } from '../shadow/prepare';
13
13
 
@@ -69,12 +69,22 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
69
69
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
70
70
  }
71
71
 
72
- // Check the broker — fail fast if the app is already registered.
72
+ // Check the broker — fail fast if the app already has a real release.
73
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past:
74
+ // either it's this same name being re-created after a local wipe (we'd only
75
+ // see it at all if we still have access to it), or it genuinely doesn't
76
+ // exist yet and we claim it below.
73
77
  const existing = await fetchApp(mfAppName(appName));
74
- if (existing) {
78
+ if (existing?.deployed) {
75
79
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
76
80
  }
77
81
 
82
+ const gitRemote = resolveRemoteUrl(opts.remote as RemoteType, appName, opts.repoUrl);
83
+
84
+ if (!existing) {
85
+ await claimApp(mfAppName(appName), gitRemote);
86
+ }
87
+
78
88
  // Compute substitutions.
79
89
  const vars = {
80
90
  __APP_NAME__: appName,
@@ -105,7 +115,6 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
105
115
  }
106
116
 
107
117
  info(`initializing git repo for ${appName}...`);
108
- const gitRemote = resolveRemoteUrl(opts.remote as RemoteType, appName, opts.repoUrl);
109
118
  await initRepo(appDir, gitRemote);
110
119
 
111
120
  info(`installing dependencies for ${appName}...`);
@@ -157,8 +157,9 @@ export async function generateCommand(appName: string, opts: GenerateOptions): P
157
157
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
158
158
  }
159
159
 
160
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past — only a real release blocks generate.
160
161
  const existing = await fetchApp(mfAppName(appName));
161
- if (existing) {
162
+ if (existing?.deployed) {
162
163
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
163
164
  }
164
165
 
package/src/constants.ts CHANGED
@@ -70,12 +70,11 @@ export const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
70
70
 
71
71
  export const PREVIEW_APP_NAME_PREFIX = 'zth-preview-';
72
72
 
73
- // ─────────── Declared (OAuth-gated) backend integrations ─────
74
- // Closed set the CLI detects from handler sources and the broker validates on
75
- // register/update. Always-on channels (`db`, `llm`, `snowflake`) are not listed —
76
- // the runner builds those regardless. Shared via `@mondaydotcomorg/z2h-cli/constants`.
77
- export const DECLARED_INTEGRATIONS = ['monday'] as const;
78
- export type DeclaredIntegration = (typeof DECLARED_INTEGRATIONS)[number];
73
+ // ─────────── Backend channels — re-exported from shared-utils ─────
74
+ // Single source of truth lives in `@mondaydotcomorg/z2h-shared-utils` so the
75
+ // CLI detector and broker stay in lockstep. Kept here for existing
76
+ // `@mondaydotcomorg/z2h-cli/constants` consumers.
77
+ export { ALWAYS_ON_CHANNELS, DECLARED_INTEGRATIONS, type DeclaredIntegration } from '@mondaydotcomorg/z2h-shared-utils';
79
78
 
80
79
  // ─────────── Broker origin + npm registry (shared §3 contract) ─────
81
80
  // The bigbrain-zth broker fronts both the STS credential route and the npm
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
17
17
  import { migrateCommand } from './commands/migrate';
18
18
  import { tagCommand } from './commands/tag';
19
19
  import { editCommand } from './commands/edit';
20
- import { backendInvokeCommand } from './commands/backend';
20
+ import { backendInvokeCommand, backendValidateCommand } from './commands/backend';
21
21
  import { doctorCommand } from './commands/doctor';
22
22
  import { errorLog, fail, getOutputMode, setOutputMode } from './util/logger';
23
23
  import { runCommand } from './util/run-command';
@@ -267,7 +267,14 @@ async function main(): Promise<void> {
267
267
 
268
268
  const backend = program
269
269
  .command('backend')
270
- .description("Inspect the app's backend: invoke a deployed handler by name");
270
+ .description("Inspect the app's backend: validate handler sources or invoke a deployed handler by name");
271
+
272
+ backend
273
+ .command('validate')
274
+ .description(
275
+ 'Validate backend/handlers/*.js exactly as deploy would (syntax, isolate rules, integrations) without uploading'
276
+ )
277
+ .action(runCommand({ data: cwdApp }, () => backendValidateCommand()));
271
278
 
272
279
  backend
273
280
  .command('invoke <handlerName>')
@@ -1,9 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { registerOrUpdateApp } from '../app';
2
+ import { registerOrUpdateApp, claimApp } from '../app';
3
3
  import * as promptVisibility from '../../prompt-visibility';
4
4
  import { DeployError } from '../../errors';
5
+ import * as authStore from '../../auth/auth-store';
5
6
  import type { ManifestEntry } from '../../manifest';
6
7
 
8
+ vi.spyOn(authStore, 'readZthAuth').mockResolvedValue({ token: 'jwt-stub' });
9
+
7
10
  vi.mock('../../prompt-visibility', () => ({
8
11
  resolveAppVisibility: vi.fn(),
9
12
  }));
@@ -105,3 +108,28 @@ describe('registerOrUpdateApp', () => {
105
108
  expect((err as DeployError).reason).toBe('register_error');
106
109
  });
107
110
  });
111
+
112
+ describe('claimApp', () => {
113
+ afterEach(() => {
114
+ vi.unstubAllGlobals();
115
+ });
116
+
117
+ it('posts the app name and gitRemote to the claim endpoint', async () => {
118
+ const fetchMock = mockFetchOnce(204);
119
+
120
+ await claimApp('mf-my-app', 's3://bucket/mf-my-app/git');
121
+
122
+ const [calledUrl, init] = fetchMock.mock.calls[0] as [string, RequestInit];
123
+ expect(calledUrl).toContain('/z2h-cli/apps/claim');
124
+ expect(JSON.parse(init.body as string)).toEqual({
125
+ appName: 'mf-my-app',
126
+ gitRemote: 's3://bucket/mf-my-app/git',
127
+ });
128
+ });
129
+
130
+ it('propagates a claim failure', async () => {
131
+ mockFetchOnce(409);
132
+
133
+ await expect(claimApp('mf-my-app', 's3://bucket/mf-my-app/git')).rejects.toThrow(/Claiming app name failed/);
134
+ });
135
+ });
@@ -8,7 +8,10 @@ import type { ManifestEntry } from '../manifest';
8
8
  export interface AppInfo {
9
9
  appName: string;
10
10
  version: number;
11
- entry: Record<string, unknown>;
11
+ /** Null for a claimed-but-unpublished app — reserved via `create`, never deployed yet. */
12
+ entry: Record<string, unknown> | null;
13
+ /** True once a real release has landed (`entry` is set) — false for a claimed-but-unpublished stub. */
14
+ deployed: boolean;
12
15
  deployedBy: string;
13
16
  gitRemote: string;
14
17
  createdBy: string;
@@ -16,7 +19,7 @@ export interface AppInfo {
16
19
 
17
20
  /**
18
21
  * Fetches a single app's current state from the broker.
19
- * Returns null if the app has never been deployed (404).
22
+ * Returns null if the app has never even been claimed (404).
20
23
  */
21
24
  export async function fetchApp(appName: string): Promise<AppInfo | null> {
22
25
  const { token } = await readZthAuth();
@@ -42,6 +45,25 @@ export async function fetchApp(appName: string): Promise<AppInfo | null> {
42
45
  return res.json() as Promise<AppInfo>;
43
46
  }
44
47
 
48
+ /**
49
+ * Reserves an app name (+ owner grant) before any deploy — called once from
50
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
51
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
52
+ * claimed the exact same name in the gap between the two calls.
53
+ */
54
+ export async function claimApp(appName: string, gitRemote: string): Promise<void> {
55
+ const { token } = await readZthAuth();
56
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/claim`, {
57
+ method: 'POST',
58
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
59
+ body: JSON.stringify({ appName, gitRemote }),
60
+ });
61
+ if (!res.ok) {
62
+ const text = await res.text();
63
+ throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
64
+ }
65
+ }
66
+
45
67
  async function registerApp(
46
68
  appName: string,
47
69
  token: string,