@mondaydotcomorg/z2h-cli 0.25.4 → 0.25.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +2 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/commands/__tests__/build.test.d.ts +2 -0
- package/dist/commands/__tests__/build.test.d.ts.map +1 -0
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +7 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +10 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +42 -19
- package/dist/commands/grant.d.ts.map +1 -1
- package/dist/commands/grant.js +8 -7
- package/dist/esm/auth.d.ts +2 -1
- package/dist/esm/auth.d.ts.map +1 -1
- package/dist/esm/auth.mjs +2 -1
- package/dist/esm/commands/__tests__/build.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/build.test.d.ts.map +1 -0
- package/dist/esm/commands/backend.d.ts.map +1 -1
- package/dist/esm/commands/backend.mjs +7 -1
- package/dist/esm/commands/build.d.ts.map +1 -1
- package/dist/esm/commands/build.mjs +10 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +43 -20
- package/dist/esm/commands/grant.d.ts.map +1 -1
- package/dist/esm/commands/grant.mjs +8 -7
- package/dist/esm/index.mjs +41 -140
- package/dist/esm/types.d.ts +23 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/util/__tests__/errors.test.d.ts +2 -0
- package/dist/esm/util/__tests__/errors.test.d.ts.map +1 -0
- package/dist/esm/util/__tests__/run-command.test.d.ts +2 -0
- package/dist/esm/util/__tests__/run-command.test.d.ts.map +1 -0
- package/dist/esm/util/auth/auth-store.d.ts +0 -4
- package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
- package/dist/esm/util/auth/auth-store.mjs +2 -9
- package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/esm/util/auth/broker-credential-provider.mjs +16 -16
- package/dist/esm/util/broker/app.d.ts.map +1 -1
- package/dist/esm/util/broker/app.mjs +17 -9
- package/dist/esm/util/errors.d.ts +48 -0
- package/dist/esm/util/errors.d.ts.map +1 -0
- package/dist/esm/util/errors.mjs +91 -0
- package/dist/esm/util/git/repo.d.ts +0 -3
- package/dist/esm/util/git/repo.d.ts.map +1 -1
- package/dist/esm/util/git/repo.mjs +4 -9
- package/dist/esm/util/logger.d.ts +4 -1
- package/dist/esm/util/logger.d.ts.map +1 -1
- package/dist/esm/util/logger.mjs +11 -3
- package/dist/esm/util/run-command.d.ts +16 -0
- package/dist/esm/util/run-command.d.ts.map +1 -0
- package/dist/esm/util/run-command.mjs +47 -0
- package/dist/esm/util/s3/upload.d.ts.map +1 -1
- package/dist/esm/util/s3/upload.mjs +22 -16
- package/dist/esm/util/tracker.d.ts +9 -2
- package/dist/esm/util/tracker.d.ts.map +1 -1
- package/dist/esm/util/tracker.mjs +18 -15
- package/dist/index.js +40 -139
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/__tests__/errors.test.d.ts +2 -0
- package/dist/util/__tests__/errors.test.d.ts.map +1 -0
- package/dist/util/__tests__/run-command.test.d.ts +2 -0
- package/dist/util/__tests__/run-command.test.d.ts.map +1 -0
- package/dist/util/auth/auth-store.d.ts +0 -4
- package/dist/util/auth/auth-store.d.ts.map +1 -1
- package/dist/util/auth/auth-store.js +2 -10
- package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/util/auth/broker-credential-provider.js +16 -16
- package/dist/util/broker/app.d.ts.map +1 -1
- package/dist/util/broker/app.js +17 -9
- package/dist/util/errors.d.ts +48 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +102 -0
- package/dist/util/git/repo.d.ts +0 -3
- package/dist/util/git/repo.d.ts.map +1 -1
- package/dist/util/git/repo.js +4 -10
- package/dist/util/logger.d.ts +4 -1
- package/dist/util/logger.d.ts.map +1 -1
- package/dist/util/logger.js +11 -3
- package/dist/util/run-command.d.ts +16 -0
- package/dist/util/run-command.d.ts.map +1 -0
- package/dist/util/run-command.js +49 -0
- package/dist/util/s3/upload.d.ts.map +1 -1
- package/dist/util/s3/upload.js +22 -16
- package/dist/util/tracker.d.ts +9 -2
- package/dist/util/tracker.d.ts.map +1 -1
- package/dist/util/tracker.js +19 -14
- package/handler-api/backend-runner/channels/llm-channel/llm.channel.ts +3 -2
- package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +7 -6
- package/handler-api/backend-runner/channels/snowflake-channel/snowflake.channel.ts +1 -1
- package/handler-api/backend-runner/channels/snowflake-channel/stateless-part-b.ts +1 -4
- package/package.json +2 -2
- package/src/auth.ts +2 -1
- package/src/commands/__tests__/backend-deploy.test.ts +18 -0
- package/src/commands/__tests__/build.test.ts +83 -0
- package/src/commands/__tests__/deploy.test.ts +107 -0
- package/src/commands/backend.ts +6 -1
- package/src/commands/build.ts +10 -0
- package/src/commands/deploy.ts +42 -20
- package/src/commands/grant.ts +8 -7
- package/src/index.ts +84 -133
- package/src/types.ts +47 -0
- package/src/util/__tests__/errors.test.ts +90 -0
- package/src/util/__tests__/run-command.test.ts +98 -0
- package/src/util/auth/__tests__/auth-store.test.ts +2 -1
- package/src/util/auth/__tests__/broker-credential-provider.test.ts +44 -8
- package/src/util/auth/auth-store.ts +1 -7
- package/src/util/auth/broker-credential-provider.ts +18 -16
- package/src/util/broker/__tests__/app.test.ts +23 -20
- package/src/util/broker/app.ts +16 -9
- package/src/util/errors.ts +98 -0
- package/src/util/git/__tests__/repo.test.ts +5 -3
- package/src/util/git/repo.ts +3 -9
- package/src/util/logger.ts +16 -3
- package/src/util/run-command.ts +59 -0
- package/src/util/s3/upload.ts +27 -22
- package/src/util/tracker.ts +19 -19
package/src/index.ts
CHANGED
|
@@ -18,10 +18,11 @@ import { editCommand } from './commands/edit';
|
|
|
18
18
|
import { backendInvokeCommand } from './commands/backend';
|
|
19
19
|
import { doctorCommand } from './commands/doctor';
|
|
20
20
|
import { errorLog, fail, getOutputMode, setOutputMode } from './util/logger';
|
|
21
|
-
import {
|
|
21
|
+
import { runCommand } from './util/run-command';
|
|
22
|
+
import { successEvent } from './util/tracker';
|
|
22
23
|
import { getCwdAppName } from './util/app-name';
|
|
23
24
|
|
|
24
|
-
const { version } = createRequire(import.meta.url)('
|
|
25
|
+
const { version } = createRequire(import.meta.url)('@mondaydotcomorg/z2h-cli/package.json') as { version: string };
|
|
25
26
|
|
|
26
27
|
const UPDATE_SCRIPT = path.join(os.homedir(), 'Development', 'bigbrain-z2h', 'z2h', 'setup', 'update.sh');
|
|
27
28
|
|
|
@@ -49,6 +50,24 @@ function collectTag(value: string, previous: string[]): string[] {
|
|
|
49
50
|
return previous.concat(value);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const cwdApp = () => ({ app_name: getCwdAppName() });
|
|
54
|
+
|
|
55
|
+
interface CreateCliOptions {
|
|
56
|
+
template?: string;
|
|
57
|
+
htmlFile?: string;
|
|
58
|
+
remote?: string;
|
|
59
|
+
repoUrl?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface DeployCliOptions {
|
|
63
|
+
dryRun?: boolean;
|
|
64
|
+
preview?: boolean | string;
|
|
65
|
+
open?: boolean;
|
|
66
|
+
remote?: string;
|
|
67
|
+
repoUrl?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
async function main(): Promise<void> {
|
|
53
72
|
stripConflictingEnvVars();
|
|
54
73
|
|
|
@@ -80,24 +99,15 @@ async function main(): Promise<void> {
|
|
|
80
99
|
program
|
|
81
100
|
.command('build')
|
|
82
101
|
.description('Build the consumer into its .zth/ shadow and copy artifacts to ./build/')
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await fail(`build failed: ${(err as Error).message}`, { command: 'build' }, 2);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
102
|
+
// A standalone build failure is the consumer's own code, not a CLI/infra problem — don't
|
|
103
|
+
// track it. buildCommand still tags the reason on its thrown error, since deploy's own
|
|
104
|
+
// (tracked) failure event benefits from knowing when a build broke it.
|
|
105
|
+
.action(runCommand({ track: false, data: cwdApp, exitCode: 2 }, () => buildCommand()));
|
|
90
106
|
|
|
91
107
|
program
|
|
92
108
|
.command('clean')
|
|
93
109
|
.description('Delete <consumer>/.zth/ and <consumer>/build/')
|
|
94
|
-
.action(
|
|
95
|
-
try {
|
|
96
|
-
await cleanCommand();
|
|
97
|
-
} catch (err) {
|
|
98
|
-
await fail(`clean failed: ${(err as Error).message}`, { command: 'clean' }, 2);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
110
|
+
.action(runCommand({ track: false, exitCode: 2 }, () => cleanCommand()));
|
|
101
111
|
|
|
102
112
|
program
|
|
103
113
|
.command('deploy')
|
|
@@ -110,7 +120,7 @@ async function main(): Promise<void> {
|
|
|
110
120
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
111
121
|
.option(
|
|
112
122
|
'--remote <type>',
|
|
113
|
-
'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.'
|
|
123
|
+
'First deploy only: source control remote to initialise — s3 (shared storage, recommended, auto-backed-up) or github (manage it yourself in your own repo). Error if the app already has a git remote.'
|
|
114
124
|
)
|
|
115
125
|
.option(
|
|
116
126
|
'--repo-url <url>',
|
|
@@ -118,53 +128,36 @@ async function main(): Promise<void> {
|
|
|
118
128
|
)
|
|
119
129
|
.option('--description <text>', 'short (<=200 char) description of the app')
|
|
120
130
|
.action(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
() => deployCommand(opts as Parameters<typeof deployCommand>[0]),
|
|
133
|
-
{
|
|
134
|
-
app_name: getCwdAppName(),
|
|
135
|
-
}
|
|
136
|
-
);
|
|
137
|
-
} catch (err) {
|
|
138
|
-
const label = opts.preview ? 'preview' : 'deploy';
|
|
139
|
-
await fail(`${label} failed: ${(err as Error).message}`, { command: label }, 2);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
131
|
+
runCommand(
|
|
132
|
+
{
|
|
133
|
+
// A preview is a real deploy for observability, tracked as its own event since
|
|
134
|
+
// the command name alone (`deploy`) can't distinguish it from a live deploy.
|
|
135
|
+
event: ([opts]: [DeployCliOptions]) => successEvent(opts.preview ? 'preview' : 'deploy'),
|
|
136
|
+
label: ([opts]: [DeployCliOptions]) => (opts.preview ? 'preview' : 'deploy'),
|
|
137
|
+
data: cwdApp,
|
|
138
|
+
exitCode: 2,
|
|
139
|
+
},
|
|
140
|
+
(opts: DeployCliOptions) => deployCommand(opts as Parameters<typeof deployCommand>[0])
|
|
141
|
+
)
|
|
142
142
|
);
|
|
143
143
|
|
|
144
144
|
program
|
|
145
145
|
.command('tag [app-name]')
|
|
146
146
|
.description("Set an app's category tags — app type & team (interactive unless --tag or --json is given)")
|
|
147
147
|
.option('--tag <tag>', 'set a tag (repeatable); full-replaces the tag set non-interactively', collectTag, [])
|
|
148
|
-
.action(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
app_name: appName ?? getCwdAppName(),
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
148
|
+
.action(
|
|
149
|
+
runCommand(
|
|
150
|
+
{
|
|
151
|
+
data: ([appName]: [string | undefined, unknown]) => ({ app_name: appName ?? getCwdAppName() }),
|
|
152
|
+
},
|
|
153
|
+
(appName: string | undefined, opts: { tag?: string[] }) => tagCommand(appName, opts)
|
|
154
|
+
)
|
|
155
|
+
);
|
|
157
156
|
|
|
158
157
|
program
|
|
159
158
|
.command('create-workspace')
|
|
160
159
|
.description(`Bootstrap the ${getWorkspaceHome()}/ yarn workspace (one-time setup)`)
|
|
161
|
-
.action(
|
|
162
|
-
try {
|
|
163
|
-
await createWorkspaceCommand();
|
|
164
|
-
} catch (err) {
|
|
165
|
-
await fail(`create-workspace failed: ${(err as Error).message}`, { command: 'create-workspace' }, 1);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
160
|
+
.action(runCommand({ track: false }, () => createWorkspaceCommand()));
|
|
168
161
|
|
|
169
162
|
program
|
|
170
163
|
.command('generate-z2h-token')
|
|
@@ -173,97 +166,69 @@ async function main(): Promise<void> {
|
|
|
173
166
|
)
|
|
174
167
|
.option('--no-browser', "don't auto-open the browser; print the URL and prompt for manual paste")
|
|
175
168
|
.option('--url <url>', 'Override the token page URL (e.g. for staging)')
|
|
176
|
-
.action(
|
|
177
|
-
try {
|
|
178
|
-
await generateZ2HTokenCommand(opts);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
await fail(`generate-z2h-token failed: ${(err as Error).message}`, { command: 'generate-z2h-token' }, 1);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
169
|
+
.action(runCommand({}, (opts: { noBrowser?: boolean; url?: string }) => generateZ2HTokenCommand(opts)));
|
|
183
170
|
|
|
184
171
|
program
|
|
185
172
|
.command('create <app-name>')
|
|
186
173
|
.description(`Scaffold a new Z2H consumer app inside ${getWorkspaceHome()}/`)
|
|
187
174
|
.option('--template <name>', 'Template name (default | html-embed)')
|
|
188
175
|
.option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
|
|
189
|
-
.option(
|
|
176
|
+
.option(
|
|
177
|
+
'--remote <type>',
|
|
178
|
+
'Source control remote: s3 (shared storage, recommended, auto-backed-up) or github (manage it yourself in your own repo)'
|
|
179
|
+
)
|
|
190
180
|
.option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
|
|
191
181
|
.action(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
runCommand(
|
|
183
|
+
{
|
|
184
|
+
data: ([appName, opts]: [string, CreateCliOptions]) => ({
|
|
195
185
|
app_name: appName,
|
|
196
186
|
template: opts.template ?? 'default',
|
|
197
187
|
remote: opts.remote ?? 'unknown',
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
(appName: string, opts: CreateCliOptions) => createCommand(appName, opts as CreateOptions)
|
|
191
|
+
)
|
|
203
192
|
);
|
|
204
193
|
|
|
205
194
|
program
|
|
206
195
|
.command('edit <app-name>')
|
|
207
196
|
.description(`Pull the latest version of an app into ${getWorkspaceHome()}/<app-name>, cloning it first if needed`)
|
|
208
|
-
.action(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
app_name: appName,
|
|
212
|
-
});
|
|
213
|
-
} catch (err) {
|
|
214
|
-
await fail(`edit failed: ${(err as Error).message}`, { command: 'edit' }, 1);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
197
|
+
.action(
|
|
198
|
+
runCommand({ data: ([appName]: [string]) => ({ app_name: appName }) }, (appName: string) => editCommand(appName))
|
|
199
|
+
);
|
|
217
200
|
|
|
218
201
|
program
|
|
219
202
|
.command('grant [email]')
|
|
220
203
|
.description('Grant access to the current app (email, --public, or both)')
|
|
221
204
|
.option('--public', 'make the app visible to all monday.com employees')
|
|
222
205
|
.option('--level <level>', 'access level for user grants: owner, editor, or viewer (default: viewer)')
|
|
223
|
-
.action(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
});
|
|
206
|
+
.action(
|
|
207
|
+
runCommand({}, (email: string | undefined, opts: { public?: boolean; level?: string }) =>
|
|
208
|
+
grantCommand(email, opts as Parameters<typeof grantCommand>[1])
|
|
209
|
+
)
|
|
210
|
+
);
|
|
230
211
|
|
|
231
212
|
program
|
|
232
213
|
.command('revoke [email]')
|
|
233
214
|
.description('Revoke access to the current app (email or --public; owners/admins only)')
|
|
234
215
|
.option('--public', 'remove the public grant, making the app visible to owners/admins only')
|
|
235
|
-
.action(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
});
|
|
216
|
+
.action(
|
|
217
|
+
runCommand({}, (email: string | undefined, opts: { public?: boolean }) =>
|
|
218
|
+
revokeCommand(email, opts as Parameters<typeof revokeCommand>[1])
|
|
219
|
+
)
|
|
220
|
+
);
|
|
242
221
|
|
|
243
222
|
program
|
|
244
223
|
.command('transfer-owner [email]')
|
|
245
224
|
.description('Transfer ownership of the current app to another monday.com user')
|
|
246
|
-
.action(
|
|
247
|
-
try {
|
|
248
|
-
await transferOwnerCommand(email);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
await fail(`transfer-owner failed: ${(err as Error).message}`, { command: 'transfer-owner' }, 1);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
225
|
+
.action(runCommand({}, (email: string | undefined) => transferOwnerCommand(email)));
|
|
253
226
|
|
|
254
227
|
program
|
|
255
228
|
.command('delete')
|
|
256
229
|
.description('Permanently delete the current app and take it offline (owners/admins only)')
|
|
257
230
|
.option('--yes', 'skip the confirmation prompt (required for --json / non-interactive)')
|
|
258
|
-
.action(
|
|
259
|
-
try {
|
|
260
|
-
await trackDuration('z2h_app_delete_success', () => deleteCommand(opts), {
|
|
261
|
-
app_name: getCwdAppName(),
|
|
262
|
-
});
|
|
263
|
-
} catch (err) {
|
|
264
|
-
await fail(`delete failed: ${(err as Error).message}`, { command: 'delete' }, 1);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
231
|
+
.action(runCommand({ data: cwdApp }, (opts: { yes?: boolean }) => deleteCommand(opts)));
|
|
267
232
|
|
|
268
233
|
const backend = program
|
|
269
234
|
.command('backend')
|
|
@@ -275,39 +240,25 @@ async function main(): Promise<void> {
|
|
|
275
240
|
'Invoke a deployed backend handler by name (the filename without .ts) — mainly for testing a handler without going through the frontend'
|
|
276
241
|
)
|
|
277
242
|
.option('--input <json>', 'JSON payload passed as ctx.input')
|
|
278
|
-
.action(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
app_name: getCwdAppName(),
|
|
282
|
-
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
});
|
|
243
|
+
.action(
|
|
244
|
+
runCommand(
|
|
245
|
+
{
|
|
246
|
+
data: ([handlerName]: [string, unknown]) => ({ app_name: getCwdAppName(), handler: handlerName }),
|
|
247
|
+
},
|
|
248
|
+
(handlerName: string, opts: { input?: string }) => backendInvokeCommand(handlerName, opts)
|
|
249
|
+
)
|
|
250
|
+
);
|
|
288
251
|
|
|
289
252
|
program
|
|
290
253
|
.command('migrate')
|
|
291
254
|
.description('Run pending workspace migrations')
|
|
292
255
|
.option('--dry-run', 'show pending migrations without applying any changes')
|
|
293
|
-
.action(
|
|
294
|
-
try {
|
|
295
|
-
await migrateCommand(version, opts);
|
|
296
|
-
} catch (err) {
|
|
297
|
-
await fail(`migrate failed: ${(err as Error).message}`, { command: 'migrate' }, 1);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
256
|
+
.action(runCommand({}, (opts: { dryRun?: boolean }) => migrateCommand(version, opts)));
|
|
300
257
|
|
|
301
258
|
program
|
|
302
259
|
.command('doctor')
|
|
303
260
|
.description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
|
|
304
|
-
.action(
|
|
305
|
-
try {
|
|
306
|
-
await doctorCommand();
|
|
307
|
-
} catch (err) {
|
|
308
|
-
await fail(`doctor failed: ${(err as Error).message}`, { command: 'doctor' }, 1);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
261
|
+
.action(runCommand({ track: false }, () => doctorCommand()));
|
|
311
262
|
|
|
312
263
|
await program.parseAsync(process.argv);
|
|
313
264
|
}
|
package/src/types.ts
CHANGED
|
@@ -62,3 +62,50 @@ export interface PreparedShadow {
|
|
|
62
62
|
port: number;
|
|
63
63
|
paths: ConsumerPaths;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
// ─────────── deploy failure reason ───────────
|
|
67
|
+
// Bounded reason codes tagged onto deploy failures — see DeployError in commands/deploy.ts.
|
|
68
|
+
export type DeployFailureReason =
|
|
69
|
+
| 'read_auth_token'
|
|
70
|
+
| 'broker_auth'
|
|
71
|
+
| 'broker_error'
|
|
72
|
+
| 'lock_timeout'
|
|
73
|
+
| 'build_error'
|
|
74
|
+
| 'git_error'
|
|
75
|
+
| 's3_error'
|
|
76
|
+
| 'deploy_backend_error'
|
|
77
|
+
| 'register_error'
|
|
78
|
+
| 'push_error'
|
|
79
|
+
| 'interrupted'
|
|
80
|
+
| 'unknown';
|
|
81
|
+
|
|
82
|
+
// ─────────── grant/revoke/transfer-owner failure reason ───────────
|
|
83
|
+
// Bounded reason codes tagged onto the shared z2h_cli_error event for these
|
|
84
|
+
// commands — see GrantError in commands/grant.ts.
|
|
85
|
+
export type GrantFailureReason = 'validation' | 'auth' | 'forbidden' | 'not_found' | 'server_error' | 'unknown';
|
|
86
|
+
|
|
87
|
+
// ─────────── command telemetry contract ───────────
|
|
88
|
+
// runCommand in util/run-command.ts reads these. It wraps every CLI command.
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A spec value that is either a constant or a function of the command's own arguments.
|
|
92
|
+
* Callbacks take the args as one tuple rather than a spread, so a callback that reads only
|
|
93
|
+
* the first arg still typechecks. Destructure what you need.
|
|
94
|
+
*/
|
|
95
|
+
export type PerArgs<A extends unknown[], T> = T | ((args: A) => T);
|
|
96
|
+
|
|
97
|
+
export interface CommandSpec<A extends unknown[]> {
|
|
98
|
+
/**
|
|
99
|
+
* Opt out of telemetry entirely — both the success event and the error event. Default `true`.
|
|
100
|
+
* Set `false` for a purely local command with no product-usage signal worth a round-trip.
|
|
101
|
+
*/
|
|
102
|
+
track?: PerArgs<A, boolean>;
|
|
103
|
+
/** Success-event name when it depends on args (e.g. `deploy --preview`); defaults to `successEvent(<command>)`. */
|
|
104
|
+
event?: PerArgs<A, string>;
|
|
105
|
+
/** Prefix in the error message and the `action` dimension. Defaults to the command's own name. */
|
|
106
|
+
label?: PerArgs<A, string>;
|
|
107
|
+
/** Extra dimensions sent with both the success and the error event. */
|
|
108
|
+
data?: (args: A) => Record<string, unknown>;
|
|
109
|
+
/** Overrides the failing error's own exitCode. */
|
|
110
|
+
exitCode?: number;
|
|
111
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Z2hError,
|
|
5
|
+
DeployError,
|
|
6
|
+
GrantError,
|
|
7
|
+
Z2hAuthMissingError,
|
|
8
|
+
GitConflictError,
|
|
9
|
+
reasonOf,
|
|
10
|
+
dataOf,
|
|
11
|
+
messageOf,
|
|
12
|
+
} from '../errors';
|
|
13
|
+
import { fail, setOutputMode } from '../logger';
|
|
14
|
+
import { trackEvent } from '../tracker';
|
|
15
|
+
|
|
16
|
+
vi.mock('../tracker', () => ({ trackEvent: vi.fn().mockResolvedValue(undefined) }));
|
|
17
|
+
|
|
18
|
+
describe('reasonOf: every classified error carries its own reason', () => {
|
|
19
|
+
it.each([
|
|
20
|
+
['DeployError', new DeployError('build_error', 'boom'), 'build_error'],
|
|
21
|
+
['GrantError', new GrantError('forbidden', 'nope'), 'forbidden'],
|
|
22
|
+
['Z2hAuthMissingError', new Z2hAuthMissingError('/tmp/auth.json'), 'read_auth_token'],
|
|
23
|
+
['GitConflictError', new GitConflictError('diverged'), 'git_error'],
|
|
24
|
+
])('%s → %s', (_name, err, expected) => {
|
|
25
|
+
expect(reasonOf(err)).toBe(expected);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('falls back to unknown for a plain Error and a non-Error throw', () => {
|
|
29
|
+
expect(reasonOf(new Error('plain'))).toBe('unknown');
|
|
30
|
+
expect(reasonOf('a string')).toBe('unknown');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps the subclass name so telemetry and stack traces stay readable', () => {
|
|
34
|
+
expect(new DeployError('s3_error', 'x').name).toBe('DeployError');
|
|
35
|
+
expect(new GitConflictError('x').name).toBe('GitConflictError');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('dataOf / messageOf', () => {
|
|
40
|
+
it('surfaces extra dimensions the throw site attached', () => {
|
|
41
|
+
expect(dataOf(new Z2hError('x', 'unknown', { stage: 'upload', attempt: 2 }))).toEqual({
|
|
42
|
+
stage: 'upload',
|
|
43
|
+
attempt: 2,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns an empty object when the error carries no data', () => {
|
|
48
|
+
expect(dataOf(new DeployError('read_auth_token', 'x'))).toEqual({});
|
|
49
|
+
expect(dataOf('not an error')).toEqual({});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('stringifies a non-Error throw instead of yielding undefined', () => {
|
|
53
|
+
expect(messageOf('boom')).toBe('boom');
|
|
54
|
+
expect(messageOf(null)).toBe('null');
|
|
55
|
+
expect(messageOf(new Error('real'))).toBe('real');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('fail: telemetry is opt-out for unobserved commands', () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.mocked(trackEvent).mockClear();
|
|
62
|
+
setOutputMode('human');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// process.exit would tear down the test run, so trap it as a throw.
|
|
66
|
+
const runFail = async (track: boolean) => {
|
|
67
|
+
const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
68
|
+
throw new Error('__exit__');
|
|
69
|
+
});
|
|
70
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
71
|
+
await expect(fail('doctor failed: x', { command: 'doctor', reason: 'unknown' }, 1, track)).rejects.toThrow(
|
|
72
|
+
'__exit__'
|
|
73
|
+
);
|
|
74
|
+
exit.mockRestore();
|
|
75
|
+
vi.mocked(process.stderr.write).mockRestore();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
it('emits z2h_cli_error when the command is observed', async () => {
|
|
79
|
+
await runFail(true);
|
|
80
|
+
expect(trackEvent).toHaveBeenCalledWith(
|
|
81
|
+
'z2h_cli_error',
|
|
82
|
+
expect.objectContaining({ action: 'doctor', reason: 'unknown' })
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('stays silent when the command has no success event', async () => {
|
|
87
|
+
await runFail(false);
|
|
88
|
+
expect(trackEvent).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { runCommand } from '../run-command';
|
|
4
|
+
import { DeployError } from '../errors';
|
|
5
|
+
import { setOutputMode } from '../logger';
|
|
6
|
+
import { trackEvent, trackDuration } from '../tracker';
|
|
7
|
+
|
|
8
|
+
vi.mock('../tracker', () => ({
|
|
9
|
+
trackEvent: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
trackDuration: vi.fn((_name: string, fn: () => Promise<unknown>) => fn()),
|
|
11
|
+
successEvent: (command: string) => `z2h_cli_${command.replace(/[\s-]+/g, '_')}_success`,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('runCommand: build_error is never reported, other reasons still are', () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.mocked(trackEvent).mockClear();
|
|
17
|
+
setOutputMode('human');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// process.exit would tear down the test run, so trap it as a throw.
|
|
21
|
+
const runFailing = async (err: unknown, cmd = new Command('deploy')) => {
|
|
22
|
+
const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
23
|
+
throw new Error('__exit__');
|
|
24
|
+
});
|
|
25
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
26
|
+
const handler = runCommand({}, () => {
|
|
27
|
+
throw err;
|
|
28
|
+
});
|
|
29
|
+
await expect(handler(cmd)).rejects.toThrow('__exit__');
|
|
30
|
+
exit.mockRestore();
|
|
31
|
+
vi.mocked(process.stderr.write).mockRestore();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
it('drops the error event entirely when the deploy failed because the build broke', async () => {
|
|
35
|
+
await runFailing(new DeployError('build_error', 'tsc exploded'));
|
|
36
|
+
expect(trackEvent).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('still reports a non-build failure reason', async () => {
|
|
40
|
+
await runFailing(new DeployError('s3_error', 'upload failed'));
|
|
41
|
+
expect(trackEvent).toHaveBeenCalledWith('z2h_cli_error', expect.objectContaining({ reason: 's3_error' }));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('runCommand: naming/tracking convention', () => {
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.mocked(trackEvent).mockClear();
|
|
48
|
+
vi.mocked(trackDuration).mockClear();
|
|
49
|
+
setOutputMode('human');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('derives its name (and label) from the Command instance, no name param needed', async () => {
|
|
53
|
+
const cmd = new Command('migrate');
|
|
54
|
+
const handler = runCommand({}, () => Promise.resolve('ok'));
|
|
55
|
+
await handler(cmd);
|
|
56
|
+
expect(trackDuration).toHaveBeenCalledWith('z2h_cli_migrate_success', expect.any(Function), {});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('composes a nested command\'s name from its parent, e.g. "backend invoke"', async () => {
|
|
60
|
+
const backend = new Command('backend');
|
|
61
|
+
const invoke = backend.command('invoke');
|
|
62
|
+
const handler = runCommand({}, () => Promise.resolve('ok'));
|
|
63
|
+
await handler(invoke);
|
|
64
|
+
expect(trackDuration).toHaveBeenCalledWith('z2h_cli_backend_invoke_success', expect.any(Function), {});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('track: false skips telemetry entirely, success or failure', async () => {
|
|
68
|
+
const cmd = new Command('doctor');
|
|
69
|
+
await runCommand({ track: false }, () => Promise.resolve('ok'))(cmd);
|
|
70
|
+
expect(trackDuration).not.toHaveBeenCalled();
|
|
71
|
+
expect(trackEvent).not.toHaveBeenCalled();
|
|
72
|
+
|
|
73
|
+
const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
74
|
+
throw new Error('__exit__');
|
|
75
|
+
});
|
|
76
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
77
|
+
const failing = runCommand({ track: false }, () => {
|
|
78
|
+
throw new Error('boom');
|
|
79
|
+
});
|
|
80
|
+
await expect(failing(cmd)).rejects.toThrow('__exit__');
|
|
81
|
+
expect(trackEvent).not.toHaveBeenCalled();
|
|
82
|
+
exit.mockRestore();
|
|
83
|
+
vi.mocked(process.stderr.write).mockRestore();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('a per-args event override beats the derived name, e.g. deploy --preview', async () => {
|
|
87
|
+
const cmd = new Command('deploy');
|
|
88
|
+
const handler = runCommand(
|
|
89
|
+
{
|
|
90
|
+
event: ([opts]: [{ preview?: boolean }]) =>
|
|
91
|
+
opts.preview ? 'z2h_cli_preview_success' : 'z2h_cli_deploy_success',
|
|
92
|
+
},
|
|
93
|
+
(_opts: { preview?: boolean }) => Promise.resolve('ok')
|
|
94
|
+
);
|
|
95
|
+
await handler({ preview: true }, cmd);
|
|
96
|
+
expect(trackDuration).toHaveBeenCalledWith('z2h_cli_preview_success', expect.any(Function), {});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
4
|
import { ensureDir, remove, writeJson } from 'fs-extra';
|
|
5
|
+
import { Z2hAuthMissingError } from '../../errors';
|
|
5
6
|
|
|
6
7
|
let scratchHome: string;
|
|
7
8
|
let originalHome: string | undefined;
|
|
@@ -29,7 +30,7 @@ afterEach(async () => {
|
|
|
29
30
|
|
|
30
31
|
describe('readZthAuth', () => {
|
|
31
32
|
it('throws Z2hAuthMissingError with a remediation hint when the file is absent', async () => {
|
|
32
|
-
const { readZthAuth
|
|
33
|
+
const { readZthAuth } = await reload();
|
|
33
34
|
await expect(readZthAuth()).rejects.toBeInstanceOf(Z2hAuthMissingError);
|
|
34
35
|
});
|
|
35
36
|
|