@mondaydotcomorg/z2h-cli 0.25.4 → 0.25.8

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 (120) hide show
  1. package/dist/auth.d.ts +2 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +2 -1
  4. package/dist/commands/__tests__/build.test.d.ts +2 -0
  5. package/dist/commands/__tests__/build.test.d.ts.map +1 -0
  6. package/dist/commands/backend.d.ts.map +1 -1
  7. package/dist/commands/backend.js +7 -1
  8. package/dist/commands/build.d.ts.map +1 -1
  9. package/dist/commands/build.js +10 -0
  10. package/dist/commands/deploy.d.ts.map +1 -1
  11. package/dist/commands/deploy.js +42 -19
  12. package/dist/commands/grant.d.ts.map +1 -1
  13. package/dist/commands/grant.js +8 -7
  14. package/dist/esm/auth.d.ts +2 -1
  15. package/dist/esm/auth.d.ts.map +1 -1
  16. package/dist/esm/auth.mjs +2 -1
  17. package/dist/esm/commands/__tests__/build.test.d.ts +2 -0
  18. package/dist/esm/commands/__tests__/build.test.d.ts.map +1 -0
  19. package/dist/esm/commands/backend.d.ts.map +1 -1
  20. package/dist/esm/commands/backend.mjs +7 -1
  21. package/dist/esm/commands/build.d.ts.map +1 -1
  22. package/dist/esm/commands/build.mjs +10 -0
  23. package/dist/esm/commands/deploy.d.ts.map +1 -1
  24. package/dist/esm/commands/deploy.mjs +43 -20
  25. package/dist/esm/commands/grant.d.ts.map +1 -1
  26. package/dist/esm/commands/grant.mjs +8 -7
  27. package/dist/esm/index.mjs +41 -140
  28. package/dist/esm/types.d.ts +23 -0
  29. package/dist/esm/types.d.ts.map +1 -1
  30. package/dist/esm/util/__tests__/errors.test.d.ts +2 -0
  31. package/dist/esm/util/__tests__/errors.test.d.ts.map +1 -0
  32. package/dist/esm/util/__tests__/run-command.test.d.ts +2 -0
  33. package/dist/esm/util/__tests__/run-command.test.d.ts.map +1 -0
  34. package/dist/esm/util/auth/auth-store.d.ts +0 -4
  35. package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
  36. package/dist/esm/util/auth/auth-store.mjs +2 -9
  37. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
  38. package/dist/esm/util/auth/broker-credential-provider.mjs +16 -16
  39. package/dist/esm/util/broker/app.d.ts.map +1 -1
  40. package/dist/esm/util/broker/app.mjs +17 -9
  41. package/dist/esm/util/errors.d.ts +48 -0
  42. package/dist/esm/util/errors.d.ts.map +1 -0
  43. package/dist/esm/util/errors.mjs +91 -0
  44. package/dist/esm/util/git/repo.d.ts +0 -3
  45. package/dist/esm/util/git/repo.d.ts.map +1 -1
  46. package/dist/esm/util/git/repo.mjs +4 -9
  47. package/dist/esm/util/logger.d.ts +4 -1
  48. package/dist/esm/util/logger.d.ts.map +1 -1
  49. package/dist/esm/util/logger.mjs +11 -3
  50. package/dist/esm/util/run-command.d.ts +16 -0
  51. package/dist/esm/util/run-command.d.ts.map +1 -0
  52. package/dist/esm/util/run-command.mjs +47 -0
  53. package/dist/esm/util/s3/upload.d.ts.map +1 -1
  54. package/dist/esm/util/s3/upload.mjs +22 -16
  55. package/dist/esm/util/tracker.d.ts +9 -2
  56. package/dist/esm/util/tracker.d.ts.map +1 -1
  57. package/dist/esm/util/tracker.mjs +18 -15
  58. package/dist/index.js +40 -139
  59. package/dist/types.d.ts +23 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/util/__tests__/errors.test.d.ts +2 -0
  62. package/dist/util/__tests__/errors.test.d.ts.map +1 -0
  63. package/dist/util/__tests__/run-command.test.d.ts +2 -0
  64. package/dist/util/__tests__/run-command.test.d.ts.map +1 -0
  65. package/dist/util/auth/auth-store.d.ts +0 -4
  66. package/dist/util/auth/auth-store.d.ts.map +1 -1
  67. package/dist/util/auth/auth-store.js +2 -10
  68. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
  69. package/dist/util/auth/broker-credential-provider.js +16 -16
  70. package/dist/util/broker/app.d.ts.map +1 -1
  71. package/dist/util/broker/app.js +17 -9
  72. package/dist/util/errors.d.ts +48 -0
  73. package/dist/util/errors.d.ts.map +1 -0
  74. package/dist/util/errors.js +102 -0
  75. package/dist/util/git/repo.d.ts +0 -3
  76. package/dist/util/git/repo.d.ts.map +1 -1
  77. package/dist/util/git/repo.js +4 -10
  78. package/dist/util/logger.d.ts +4 -1
  79. package/dist/util/logger.d.ts.map +1 -1
  80. package/dist/util/logger.js +11 -3
  81. package/dist/util/run-command.d.ts +16 -0
  82. package/dist/util/run-command.d.ts.map +1 -0
  83. package/dist/util/run-command.js +49 -0
  84. package/dist/util/s3/upload.d.ts.map +1 -1
  85. package/dist/util/s3/upload.js +22 -16
  86. package/dist/util/tracker.d.ts +9 -2
  87. package/dist/util/tracker.d.ts.map +1 -1
  88. package/dist/util/tracker.js +19 -14
  89. package/handler-api/backend-runner/channels/channel-map.ts +6 -12
  90. package/handler-api/backend-runner/channels/channel-telemetry.ts +79 -0
  91. package/handler-api/backend-runner/channels/llm-channel/llm.channel.ts +3 -2
  92. package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +21 -7
  93. package/handler-api/backend-runner/channels/snowflake-channel/snowflake.channel.ts +14 -2
  94. package/handler-api/backend-runner/channels/snowflake-channel/stateless-part-b.ts +1 -4
  95. package/package.json +2 -2
  96. package/src/auth.ts +2 -1
  97. package/src/commands/__tests__/backend-deploy.test.ts +18 -0
  98. package/src/commands/__tests__/build.test.ts +83 -0
  99. package/src/commands/__tests__/deploy.test.ts +107 -0
  100. package/src/commands/backend.ts +6 -1
  101. package/src/commands/build.ts +10 -0
  102. package/src/commands/deploy.ts +42 -20
  103. package/src/commands/grant.ts +8 -7
  104. package/src/index.ts +84 -133
  105. package/src/types.ts +47 -0
  106. package/src/util/__tests__/errors.test.ts +90 -0
  107. package/src/util/__tests__/run-command.test.ts +98 -0
  108. package/src/util/auth/__tests__/auth-store.test.ts +2 -1
  109. package/src/util/auth/__tests__/broker-credential-provider.test.ts +44 -8
  110. package/src/util/auth/auth-store.ts +1 -7
  111. package/src/util/auth/broker-credential-provider.ts +18 -16
  112. package/src/util/broker/__tests__/app.test.ts +23 -20
  113. package/src/util/broker/app.ts +16 -9
  114. package/src/util/errors.ts +98 -0
  115. package/src/util/git/__tests__/repo.test.ts +5 -3
  116. package/src/util/git/repo.ts +3 -9
  117. package/src/util/logger.ts +16 -3
  118. package/src/util/run-command.ts +59 -0
  119. package/src/util/s3/upload.ts +27 -22
  120. package/src/util/tracker.ts +19 -19
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { generateZ2HTokenCommand, type GenerateZ2HTokenOptions } from './commands/generate-z2h-token';
2
- export { readZthAuth, Z2hAuthMissingError, Z2H_AUTH_FILE, type ZthAuth } from './util/auth/auth-store';
2
+ export { readZthAuth, Z2H_AUTH_FILE, type ZthAuth } from './util/auth/auth-store';
3
+ export { Z2hAuthMissingError } from './util/errors';
3
4
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACtG,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACtG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
package/dist/auth.js CHANGED
@@ -2,10 +2,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const commands_generateZ2hToken = require('./commands/generate-z2h-token.js');
4
4
  const util_auth_authStore = require('./util/auth/auth-store.js');
5
+ const util_errors = require('./util/errors.js');
5
6
 
6
7
 
7
8
 
8
9
  exports.generateZ2HTokenCommand = commands_generateZ2hToken.generateZ2HTokenCommand;
9
10
  exports.Z2H_AUTH_FILE = util_auth_authStore.Z2H_AUTH_FILE;
10
- exports.Z2hAuthMissingError = util_auth_authStore.Z2hAuthMissingError;
11
11
  exports.readZthAuth = util_auth_authStore.readZthAuth;
12
+ exports.Z2hAuthMissingError = util_errors.Z2hAuthMissingError;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=build.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/build.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKpB;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
@@ -10,6 +10,7 @@ const backend_detectIntegrations = require('../backend/detect-integrations.js');
10
10
  const util_s3_client = require('../util/s3/client.js');
11
11
  const constants = require('../constants.js');
12
12
  const backend_client = require('../backend/client.js');
13
+ const util_errors = require('../util/errors.js');
13
14
 
14
15
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
15
16
 
@@ -75,7 +76,12 @@ async function deployBackend(opts) {
75
76
  const { appDir, appName, mfKeyPrefix, version, deployedBy, isPreview } = opts;
76
77
  const prefix = isPreview ? `${mfKeyPrefix.replace(/\/MF$/, '')}/MS` : `${appName}/backend-${version}`;
77
78
  const logLabel = isPreview ? 'for preview' : `for "${appName}"`;
78
- return readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel);
79
+ try {
80
+ return await readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel);
81
+ }
82
+ catch (err) {
83
+ throw util_errors.tagDeployError('deploy_backend_error', err);
84
+ }
79
85
  }
80
86
  async function backendInvokeCommand(handlerName, opts = {}) {
81
87
  const appName = currentAppName();
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAGD,wBAAsB,YAAY,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAyChF"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAGD,wBAAsB,YAAY,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAOhF"}
@@ -5,6 +5,7 @@ const fs = require('fs-extra');
5
5
  const execa = require('execa');
6
6
  const util_logger = require('../util/logger.js');
7
7
  const shadow_prepare = require('../shadow/prepare.js');
8
+ const util_errors = require('../util/errors.js');
8
9
 
9
10
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
10
11
 
@@ -12,6 +13,15 @@ const path__default = /*#__PURE__*/_interopDefault(path);
12
13
 
13
14
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
14
15
  async function buildCommand(opts = {}) {
16
+ try {
17
+ return await runBuild(opts);
18
+ }
19
+ catch (err) {
20
+ const signal = err instanceof Error ? err.signal : undefined;
21
+ throw new util_errors.DeployError(signal ? 'interrupted' : 'build_error', util_errors.messageOf(err));
22
+ }
23
+ }
24
+ async function runBuild(opts) {
15
25
  const ctx = await shadow_prepare.prepareShadow();
16
26
  // Typecheck against the shadow's tsconfig (the consumer dir has none).
17
27
  // Errors fail the build before we waste time on the Trident pipeline.
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAiBtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAkBtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E"}
@@ -18,6 +18,7 @@ const util_machineIdentity = require('../util/machine-identity.js');
18
18
  const util_broker_app = require('../util/broker/app.js');
19
19
  const util_openBrowser = require('../util/open-browser.js');
20
20
  const shadow_prepare = require('../shadow/prepare.js');
21
+ const util_errors = require('../util/errors.js');
21
22
  const commands_backend = require('./backend.js');
22
23
  const commands_build = require('./build.js');
23
24
  const commands_tag = require('./tag.js');
@@ -41,7 +42,13 @@ async function deployCommand(opts = {}) {
41
42
  const appName = util_appName.mfAppName(consumerPkg.name);
42
43
  // Validate the user's deploy token is on disk before doing any expensive
43
44
  // work. Throws Z2hAuthMissingError with a remediation hint if not.
44
- const { token } = await util_auth_authStore.readZthAuth();
45
+ let token;
46
+ try {
47
+ ({ token } = await util_auth_authStore.readZthAuth());
48
+ }
49
+ catch (err) {
50
+ throw util_errors.tagDeployError('read_auth_token', err);
51
+ }
45
52
  const ctx = { paths, consumerName: consumerPkg.name, appName, token, opts };
46
53
  if (opts.preview) {
47
54
  await deployPreview(ctx);
@@ -98,6 +105,8 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
98
105
  // Full CDN URL, not a relative path — trident-skeleton uses a manifest
99
106
  // entry's `url` verbatim as the asset base, never joined with its own mfUrl.
100
107
  const url = `${constants.BUCKET_READ_DNS_URL}/${keyPrefix}`;
108
+ // No catch-all: every stage below tags its own DeployError reason at the throw site,
109
+ // and anything unclassified reports as `unknown` via reasonOf() in runCommand.
101
110
  const nextVersion = (await util_auth_brokerCredentialProvider.fetchDeployVersion(previewAppName)) + 1;
102
111
  util_logger.info('building');
103
112
  const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
@@ -140,10 +149,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
140
149
  else {
141
150
  if (!opts.remote) {
142
151
  throw new Error(`This app has no git remote. On first deploy, tell Z2H how to track source:\n\n` +
143
- ` z2h-cli deploy --remote s3 (default)\n` +
144
- ` Stores source history in S3 alongside your app. No extra setup.\n\n` +
152
+ ` z2h-cli deploy --remote s3 (default, recommended)\n` +
153
+ ` We store and back up your code for you automatically. No extra setup.\n\n` +
145
154
  ` z2h-cli deploy --remote github --repo-url https://github.com/org/repo.git\n` +
146
- ` Uses an existing GitHub repo (must exist before deploying).`);
155
+ ` Manage it yourself in an existing GitHub repo (must exist before deploying).`);
147
156
  }
148
157
  util_git_remoteOptions.validateRemoteOptions(opts);
149
158
  // Pass the raw consumer name (not the mf-prefixed appName) so defaultS3Remote
@@ -160,9 +169,14 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
160
169
  // The skill commits the working tree (with an LLM-authored message) before
161
170
  // invoking deploy, so the tree should be clean here.
162
171
  if (gitRemote) {
163
- await util_git_repo.guardCleanTree(process.cwd());
164
- util_logger.info('syncing with git remote');
165
- await util_git_repo.pullAndResolve(process.cwd(), gitEnv);
172
+ try {
173
+ await util_git_repo.guardCleanTree(process.cwd());
174
+ util_logger.info('syncing with git remote');
175
+ await util_git_repo.pullAndResolve(process.cwd(), gitEnv);
176
+ }
177
+ catch (err) {
178
+ throw util_errors.tagDeployError('git_error', err);
179
+ }
166
180
  }
167
181
  util_logger.info('reading current Z2H manifest');
168
182
  const keyPrefix = `${appName}/${nextVersion}`;
@@ -182,7 +196,6 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
182
196
  });
183
197
  return;
184
198
  }
185
- // lock → git push → bundle upload → manifest PUT (the manifest write is the single commit point).
186
199
  try {
187
200
  // Acquire the deploy lock explicitly. Done after the build
188
201
  // so the lock hold stays tiny — it wraps only push → upload → manifest.
@@ -211,6 +224,8 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
211
224
  });
212
225
  }
213
226
  finally {
227
+ // Same as preview: each stage tags its own reason, so no catch-all here — the try
228
+ // exists only so the lock is released on every exit path.
214
229
  util_logger.info('releasing deploy lock');
215
230
  await releaseLockBestEffort(appName, token);
216
231
  }
@@ -256,31 +271,39 @@ async function handleDeployTags(appSlug) {
256
271
  util_logger.warn(`could not update tags: ${err.message}`);
257
272
  }
258
273
  }
259
- // Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
260
- // so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
261
- async function acquireDeployLock(appName, token) {
262
- const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/lock/acquire`, {
274
+ function postToLockEndpoint(path, appName, token) {
275
+ return fetch(`${constants.brokerBaseUrl()}${path}`, {
263
276
  method: 'POST',
264
277
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
265
278
  body: JSON.stringify({ appName }),
266
279
  });
280
+ }
281
+ // Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
282
+ // so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
283
+ async function acquireDeployLock(appName, token) {
284
+ let res;
285
+ try {
286
+ res = await postToLockEndpoint('/z2h-cli/lock/acquire', appName, token);
287
+ }
288
+ catch (err) {
289
+ throw util_errors.tagDeployError('broker_error', err);
290
+ }
267
291
  if (res.status === 409) {
268
- throw new Error(`Someone is deploying "${appName}" right now — try again in a moment.`);
292
+ throw new util_errors.DeployError('lock_timeout', `Someone is deploying "${appName}" right now — try again in a moment.`);
269
293
  }
270
294
  if (!res.ok) {
295
+ // Only 409 is contention — don't report a broker outage or a stale token as lock contention.
271
296
  const text = await res.text();
272
- throw new Error(`Failed to acquire deploy lock (${res.status}): ${text.slice(0, 512)}`);
297
+ const grantReason = util_errors.grantReasonForStatus(res.status);
298
+ const reason = grantReason === 'auth' || grantReason === 'forbidden' ? 'broker_auth' : 'broker_error';
299
+ throw new util_errors.DeployError(reason, `Failed to acquire deploy lock (${res.status}): ${text.slice(0, 512)}`);
273
300
  }
274
301
  util_logger.info(`deploy lock acquired for ${appName}`);
275
302
  }
276
303
  // best-effort — the 2-min TTL backstop handles failures
277
304
  async function releaseLockBestEffort(appName, token) {
278
305
  try {
279
- const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/lock/release`, {
280
- method: 'POST',
281
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
282
- body: JSON.stringify({ appName }),
283
- });
306
+ const res = await postToLockEndpoint('/z2h-cli/lock/release', appName, token);
284
307
  if (!res.ok) {
285
308
  util_logger.warn(`failed to release lock for ${appName} (${res.status})`);
286
309
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/commands/grant.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACvC;AAkBD,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B/F;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhG;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnF"}
1
+ {"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/commands/grant.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACvC;AAkBD,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B/F;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhG;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnF"}
@@ -4,6 +4,7 @@ const util_logger = require('../util/logger.js');
4
4
  const util_appName = require('../util/app-name.js');
5
5
  const constants = require('../constants.js');
6
6
  const util_auth_authStore = require('../util/auth/auth-store.js');
7
+ const util_errors = require('../util/errors.js');
7
8
 
8
9
  async function postGrant(appName, token, body) {
9
10
  const res = await fetch(`${constants.brokerBaseUrl()}/z2h-apps/${appName}/grants`, {
@@ -13,16 +14,16 @@ async function postGrant(appName, token, body) {
13
14
  });
14
15
  if (!res.ok) {
15
16
  const text = await res.text();
16
- throw new Error(`Grant failed (${res.status}): ${text.slice(0, 1024)}`);
17
+ throw new util_errors.GrantError(res.status, `Grant failed (${res.status}): ${text.slice(0, 1024)}`);
17
18
  }
18
19
  }
19
20
  async function grantCommand(email, opts) {
20
21
  if (!opts.public && !email) {
21
- throw new Error('Specify what to grant — e.g. --public or provide an email');
22
+ throw new util_errors.GrantError('validation', 'Specify what to grant — e.g. --public or provide an email');
22
23
  }
23
24
  const level = opts.level ?? 'viewer';
24
25
  if (!['owner', 'editor', 'viewer'].includes(level)) {
25
- throw new Error(`Invalid --level "${level}". Must be owner, editor, or viewer`);
26
+ throw new util_errors.GrantError('validation', `Invalid --level "${level}". Must be owner, editor, or viewer`);
26
27
  }
27
28
  const appName = util_appName.mfAppName(util_appName.getCwdAppName());
28
29
  const { token } = await util_auth_authStore.readZthAuth();
@@ -43,7 +44,7 @@ async function grantCommand(email, opts) {
43
44
  }
44
45
  async function revokeCommand(email, opts) {
45
46
  if (!opts.public && !email) {
46
- throw new Error('Specify what to revoke — e.g. --public or provide an email');
47
+ throw new util_errors.GrantError('validation', 'Specify what to revoke — e.g. --public or provide an email');
47
48
  }
48
49
  const appName = util_appName.mfAppName(util_appName.getCwdAppName());
49
50
  const { token } = await util_auth_authStore.readZthAuth();
@@ -54,7 +55,7 @@ async function revokeCommand(email, opts) {
54
55
  });
55
56
  if (!res.ok) {
56
57
  const text = await res.text();
57
- throw new Error(`Revoke failed (${res.status}): ${text.slice(0, 1024)}`);
58
+ throw new util_errors.GrantError(res.status, `Revoke failed (${res.status}): ${text.slice(0, 1024)}`);
58
59
  }
59
60
  const revoked = email ?? 'public';
60
61
  const msg = email
@@ -64,7 +65,7 @@ async function revokeCommand(email, opts) {
64
65
  }
65
66
  async function transferOwnerCommand(email) {
66
67
  if (!email) {
67
- throw new Error('Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
68
+ throw new util_errors.GrantError('validation', 'Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
68
69
  }
69
70
  const appName = util_appName.mfAppName(util_appName.getCwdAppName());
70
71
  const { token } = await util_auth_authStore.readZthAuth();
@@ -75,7 +76,7 @@ async function transferOwnerCommand(email) {
75
76
  });
76
77
  if (!res.ok) {
77
78
  const text = await res.text();
78
- throw new Error(`Transfer owner failed (${res.status}): ${text.slice(0, 1024)}`);
79
+ throw new util_errors.GrantError(res.status, `Transfer owner failed (${res.status}): ${text.slice(0, 1024)}`);
79
80
  }
80
81
  util_logger.output(`[z2h-cli] "${appName}" ownership transferred to ${email}`, { ok: true, appName, newOwner: email });
81
82
  }
@@ -1,3 +1,4 @@
1
1
  export { generateZ2HTokenCommand, type GenerateZ2HTokenOptions } from './commands/generate-z2h-token';
2
- export { readZthAuth, Z2hAuthMissingError, Z2H_AUTH_FILE, type ZthAuth } from './util/auth/auth-store';
2
+ export { readZthAuth, Z2H_AUTH_FILE, type ZthAuth } from './util/auth/auth-store';
3
+ export { Z2hAuthMissingError } from './util/errors';
3
4
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACtG,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACtG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
package/dist/esm/auth.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  export { generateZ2HTokenCommand } from './commands/generate-z2h-token.mjs';
2
- export { Z2H_AUTH_FILE, Z2hAuthMissingError, readZthAuth } from './util/auth/auth-store.mjs';
2
+ export { Z2H_AUTH_FILE, readZthAuth } from './util/auth/auth-store.mjs';
3
+ export { Z2hAuthMissingError } from './util/errors.mjs';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=build.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/build.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/commands/backend.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKpB;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/commands/backend.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
@@ -8,6 +8,7 @@ import { detectIntegrationsFromHandlers } from '../backend/detect-integrations.m
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';
11
+ import { tagDeployError } from '../util/errors.mjs';
11
12
 
12
13
  function currentAppName() {
13
14
  const raw = getCwdAppName();
@@ -69,7 +70,12 @@ async function deployBackend(opts) {
69
70
  const { appDir, appName, mfKeyPrefix, version, deployedBy, isPreview } = opts;
70
71
  const prefix = isPreview ? `${mfKeyPrefix.replace(/\/MF$/, '')}/MS` : `${appName}/backend-${version}`;
71
72
  const logLabel = isPreview ? 'for preview' : `for "${appName}"`;
72
- return readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel);
73
+ try {
74
+ return await readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel);
75
+ }
76
+ catch (err) {
77
+ throw tagDeployError('deploy_backend_error', err);
78
+ }
73
79
  }
74
80
  async function backendInvokeCommand(handlerName, opts = {}) {
75
81
  const appName = currentAppName();
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAGD,wBAAsB,YAAY,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAyChF"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAGD,wBAAsB,YAAY,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAOhF"}
@@ -3,9 +3,19 @@ import { pathExists, readJson } from 'fs-extra';
3
3
  import { execa } from 'execa';
4
4
  import { info, output } from '../util/logger.mjs';
5
5
  import { prepareShadow } from '../shadow/prepare.mjs';
6
+ import { DeployError, messageOf } from '../util/errors.mjs';
6
7
 
7
8
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
8
9
  async function buildCommand(opts = {}) {
10
+ try {
11
+ return await runBuild(opts);
12
+ }
13
+ catch (err) {
14
+ const signal = err instanceof Error ? err.signal : undefined;
15
+ throw new DeployError(signal ? 'interrupted' : 'build_error', messageOf(err));
16
+ }
17
+ }
18
+ async function runBuild(opts) {
9
19
  const ctx = await prepareShadow();
10
20
  // Typecheck against the shadow's tsconfig (the consumer dir has none).
11
21
  // Errors fail the build before we waste time on the Trident pipeline.
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAiBtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAkBtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E"}
@@ -4,7 +4,7 @@ import { info, output, getOutputMode, warn } from '../util/logger.mjs';
4
4
  import { resolveConsumerPaths } from '../shadow/paths.mjs';
5
5
  import { fetchAppTags, fetchTagCatalog, setAppTags } from '../util/tags.mjs';
6
6
  import { uploadDir } from '../util/s3/upload.mjs';
7
- import { BUCKET_WRITE_NAME, brokerBaseUrl, routingHeaders, S3_PREVIEWS_FOLDER, BUCKET_READ_DNS_URL } from '../constants.mjs';
7
+ import { BUCKET_WRITE_NAME, S3_PREVIEWS_FOLDER, BUCKET_READ_DNS_URL, brokerBaseUrl, routingHeaders } from '../constants.mjs';
8
8
  import { fetchDeployVersion, brokerCredentialProvider } from '../util/auth/broker-credential-provider.mjs';
9
9
  import { readZthAuth } from '../util/auth/auth-store.mjs';
10
10
  import { validateAppName, cleanAppName, mfAppName, buildPreviewSlug } from '../util/app-name.mjs';
@@ -16,6 +16,7 @@ import { getMachineAppName } from '../util/machine-identity.mjs';
16
16
  import { registerOrUpdateApp } from '../util/broker/app.mjs';
17
17
  import { openInBrowser } from '../util/open-browser.mjs';
18
18
  import { resolvePortAndMetadata } from '../shadow/prepare.mjs';
19
+ import { tagDeployError, DeployError, grantReasonForStatus } from '../util/errors.mjs';
19
20
  import { deployBackend } from './backend.mjs';
20
21
  import { buildCommand } from './build.mjs';
21
22
  import { promptForTags } from './tag.mjs';
@@ -35,7 +36,13 @@ async function deployCommand(opts = {}) {
35
36
  const appName = mfAppName(consumerPkg.name);
36
37
  // Validate the user's deploy token is on disk before doing any expensive
37
38
  // work. Throws Z2hAuthMissingError with a remediation hint if not.
38
- const { token } = await readZthAuth();
39
+ let token;
40
+ try {
41
+ ({ token } = await readZthAuth());
42
+ }
43
+ catch (err) {
44
+ throw tagDeployError('read_auth_token', err);
45
+ }
39
46
  const ctx = { paths, consumerName: consumerPkg.name, appName, token, opts };
40
47
  if (opts.preview) {
41
48
  await deployPreview(ctx);
@@ -92,6 +99,8 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
92
99
  // Full CDN URL, not a relative path — trident-skeleton uses a manifest
93
100
  // entry's `url` verbatim as the asset base, never joined with its own mfUrl.
94
101
  const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
102
+ // No catch-all: every stage below tags its own DeployError reason at the throw site,
103
+ // and anything unclassified reports as `unknown` via reasonOf() in runCommand.
95
104
  const nextVersion = (await fetchDeployVersion(previewAppName)) + 1;
96
105
  info('building');
97
106
  const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
@@ -134,10 +143,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
134
143
  else {
135
144
  if (!opts.remote) {
136
145
  throw new Error(`This app has no git remote. On first deploy, tell Z2H how to track source:\n\n` +
137
- ` z2h-cli deploy --remote s3 (default)\n` +
138
- ` Stores source history in S3 alongside your app. No extra setup.\n\n` +
146
+ ` z2h-cli deploy --remote s3 (default, recommended)\n` +
147
+ ` We store and back up your code for you automatically. No extra setup.\n\n` +
139
148
  ` z2h-cli deploy --remote github --repo-url https://github.com/org/repo.git\n` +
140
- ` Uses an existing GitHub repo (must exist before deploying).`);
149
+ ` Manage it yourself in an existing GitHub repo (must exist before deploying).`);
141
150
  }
142
151
  validateRemoteOptions(opts);
143
152
  // Pass the raw consumer name (not the mf-prefixed appName) so defaultS3Remote
@@ -154,9 +163,14 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
154
163
  // The skill commits the working tree (with an LLM-authored message) before
155
164
  // invoking deploy, so the tree should be clean here.
156
165
  if (gitRemote) {
157
- await guardCleanTree(process.cwd());
158
- info('syncing with git remote');
159
- await pullAndResolve(process.cwd(), gitEnv);
166
+ try {
167
+ await guardCleanTree(process.cwd());
168
+ info('syncing with git remote');
169
+ await pullAndResolve(process.cwd(), gitEnv);
170
+ }
171
+ catch (err) {
172
+ throw tagDeployError('git_error', err);
173
+ }
160
174
  }
161
175
  info('reading current Z2H manifest');
162
176
  const keyPrefix = `${appName}/${nextVersion}`;
@@ -176,7 +190,6 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
176
190
  });
177
191
  return;
178
192
  }
179
- // lock → git push → bundle upload → manifest PUT (the manifest write is the single commit point).
180
193
  try {
181
194
  // Acquire the deploy lock explicitly. Done after the build
182
195
  // so the lock hold stays tiny — it wraps only push → upload → manifest.
@@ -205,6 +218,8 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
205
218
  });
206
219
  }
207
220
  finally {
221
+ // Same as preview: each stage tags its own reason, so no catch-all here — the try
222
+ // exists only so the lock is released on every exit path.
208
223
  info('releasing deploy lock');
209
224
  await releaseLockBestEffort(appName, token);
210
225
  }
@@ -250,31 +265,39 @@ async function handleDeployTags(appSlug) {
250
265
  warn(`could not update tags: ${err.message}`);
251
266
  }
252
267
  }
253
- // Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
254
- // so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
255
- async function acquireDeployLock(appName, token) {
256
- const res = await fetch(`${brokerBaseUrl()}/z2h-cli/lock/acquire`, {
268
+ function postToLockEndpoint(path, appName, token) {
269
+ return fetch(`${brokerBaseUrl()}${path}`, {
257
270
  method: 'POST',
258
271
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
259
272
  body: JSON.stringify({ appName }),
260
273
  });
274
+ }
275
+ // Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
276
+ // so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
277
+ async function acquireDeployLock(appName, token) {
278
+ let res;
279
+ try {
280
+ res = await postToLockEndpoint('/z2h-cli/lock/acquire', appName, token);
281
+ }
282
+ catch (err) {
283
+ throw tagDeployError('broker_error', err);
284
+ }
261
285
  if (res.status === 409) {
262
- throw new Error(`Someone is deploying "${appName}" right now — try again in a moment.`);
286
+ throw new DeployError('lock_timeout', `Someone is deploying "${appName}" right now — try again in a moment.`);
263
287
  }
264
288
  if (!res.ok) {
289
+ // Only 409 is contention — don't report a broker outage or a stale token as lock contention.
265
290
  const text = await res.text();
266
- throw new Error(`Failed to acquire deploy lock (${res.status}): ${text.slice(0, 512)}`);
291
+ const grantReason = grantReasonForStatus(res.status);
292
+ const reason = grantReason === 'auth' || grantReason === 'forbidden' ? 'broker_auth' : 'broker_error';
293
+ throw new DeployError(reason, `Failed to acquire deploy lock (${res.status}): ${text.slice(0, 512)}`);
267
294
  }
268
295
  info(`deploy lock acquired for ${appName}`);
269
296
  }
270
297
  // best-effort — the 2-min TTL backstop handles failures
271
298
  async function releaseLockBestEffort(appName, token) {
272
299
  try {
273
- const res = await fetch(`${brokerBaseUrl()}/z2h-cli/lock/release`, {
274
- method: 'POST',
275
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
276
- body: JSON.stringify({ appName }),
277
- });
300
+ const res = await postToLockEndpoint('/z2h-cli/lock/release', appName, token);
278
301
  if (!res.ok) {
279
302
  warn(`failed to release lock for ${appName} (${res.status})`);
280
303
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../../src/commands/grant.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACvC;AAkBD,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B/F;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhG;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnF"}
1
+ {"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../../src/commands/grant.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACvC;AAkBD,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B/F;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhG;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnF"}
@@ -2,6 +2,7 @@ import { output } from '../util/logger.mjs';
2
2
  import { mfAppName, getCwdAppName } from '../util/app-name.mjs';
3
3
  import { brokerBaseUrl, routingHeaders } from '../constants.mjs';
4
4
  import { readZthAuth } from '../util/auth/auth-store.mjs';
5
+ import { GrantError } from '../util/errors.mjs';
5
6
 
6
7
  async function postGrant(appName, token, body) {
7
8
  const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/grants`, {
@@ -11,16 +12,16 @@ async function postGrant(appName, token, body) {
11
12
  });
12
13
  if (!res.ok) {
13
14
  const text = await res.text();
14
- throw new Error(`Grant failed (${res.status}): ${text.slice(0, 1024)}`);
15
+ throw new GrantError(res.status, `Grant failed (${res.status}): ${text.slice(0, 1024)}`);
15
16
  }
16
17
  }
17
18
  async function grantCommand(email, opts) {
18
19
  if (!opts.public && !email) {
19
- throw new Error('Specify what to grant — e.g. --public or provide an email');
20
+ throw new GrantError('validation', 'Specify what to grant — e.g. --public or provide an email');
20
21
  }
21
22
  const level = opts.level ?? 'viewer';
22
23
  if (!['owner', 'editor', 'viewer'].includes(level)) {
23
- throw new Error(`Invalid --level "${level}". Must be owner, editor, or viewer`);
24
+ throw new GrantError('validation', `Invalid --level "${level}". Must be owner, editor, or viewer`);
24
25
  }
25
26
  const appName = mfAppName(getCwdAppName());
26
27
  const { token } = await readZthAuth();
@@ -41,7 +42,7 @@ async function grantCommand(email, opts) {
41
42
  }
42
43
  async function revokeCommand(email, opts) {
43
44
  if (!opts.public && !email) {
44
- throw new Error('Specify what to revoke — e.g. --public or provide an email');
45
+ throw new GrantError('validation', 'Specify what to revoke — e.g. --public or provide an email');
45
46
  }
46
47
  const appName = mfAppName(getCwdAppName());
47
48
  const { token } = await readZthAuth();
@@ -52,7 +53,7 @@ async function revokeCommand(email, opts) {
52
53
  });
53
54
  if (!res.ok) {
54
55
  const text = await res.text();
55
- throw new Error(`Revoke failed (${res.status}): ${text.slice(0, 1024)}`);
56
+ throw new GrantError(res.status, `Revoke failed (${res.status}): ${text.slice(0, 1024)}`);
56
57
  }
57
58
  const revoked = email ?? 'public';
58
59
  const msg = email
@@ -62,7 +63,7 @@ async function revokeCommand(email, opts) {
62
63
  }
63
64
  async function transferOwnerCommand(email) {
64
65
  if (!email) {
65
- throw new Error('Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
66
+ throw new GrantError('validation', 'Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
66
67
  }
67
68
  const appName = mfAppName(getCwdAppName());
68
69
  const { token } = await readZthAuth();
@@ -73,7 +74,7 @@ async function transferOwnerCommand(email) {
73
74
  });
74
75
  if (!res.ok) {
75
76
  const text = await res.text();
76
- throw new Error(`Transfer owner failed (${res.status}): ${text.slice(0, 1024)}`);
77
+ throw new GrantError(res.status, `Transfer owner failed (${res.status}): ${text.slice(0, 1024)}`);
77
78
  }
78
79
  output(`[z2h-cli] "${appName}" ownership transferred to ${email}`, { ok: true, appName, newOwner: email });
79
80
  }