@mondaydotcomorg/z2h-cli 0.25.3 → 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.
Files changed (118) 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/llm-channel/llm.channel.ts +3 -2
  90. package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +7 -6
  91. package/handler-api/backend-runner/channels/snowflake-channel/snowflake.channel.ts +1 -1
  92. package/handler-api/backend-runner/channels/snowflake-channel/stateless-part-b.ts +1 -4
  93. package/package.json +2 -2
  94. package/src/auth.ts +2 -1
  95. package/src/commands/__tests__/backend-deploy.test.ts +18 -0
  96. package/src/commands/__tests__/build.test.ts +83 -0
  97. package/src/commands/__tests__/deploy.test.ts +107 -0
  98. package/src/commands/backend.ts +6 -1
  99. package/src/commands/build.ts +10 -0
  100. package/src/commands/deploy.ts +42 -20
  101. package/src/commands/grant.ts +8 -7
  102. package/src/index.ts +84 -133
  103. package/src/types.ts +47 -0
  104. package/src/util/__tests__/errors.test.ts +90 -0
  105. package/src/util/__tests__/run-command.test.ts +98 -0
  106. package/src/util/auth/__tests__/auth-store.test.ts +2 -1
  107. package/src/util/auth/__tests__/broker-credential-provider.test.ts +44 -8
  108. package/src/util/auth/auth-store.ts +1 -7
  109. package/src/util/auth/broker-credential-provider.ts +18 -16
  110. package/src/util/broker/__tests__/app.test.ts +23 -20
  111. package/src/util/broker/app.ts +16 -9
  112. package/src/util/errors.ts +98 -0
  113. package/src/util/git/__tests__/repo.test.ts +5 -3
  114. package/src/util/git/repo.ts +3 -9
  115. package/src/util/logger.ts +16 -3
  116. package/src/util/run-command.ts +59 -0
  117. package/src/util/s3/upload.ts +27 -22
  118. package/src/util/tracker.ts +19 -19
@@ -0,0 +1,102 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ /**
4
+ * Base for every error the CLI classifies. runCommand in ./run-command.ts reads `reason`
5
+ * and `data` straight off the error, so the throw site is the only place that needs to know
6
+ * why it failed. Nothing re-derives it from the error type at the top level.
7
+ *
8
+ * `exitCode` lets a throw site override the CLI's --help contract, 1 for user-resolvable
9
+ * and 2 for system failures, per error instead of per command. Nothing sets it yet. The
10
+ * command table still pins exit codes, which keeps today's behavior.
11
+ */
12
+ class Z2hError extends Error {
13
+ reason;
14
+ data;
15
+ exitCode;
16
+ constructor(message, reason, data, exitCode = 1) {
17
+ super(message);
18
+ this.reason = reason;
19
+ this.data = data;
20
+ this.exitCode = exitCode;
21
+ this.name = new.target.name;
22
+ }
23
+ }
24
+ /** Tags a deploy/preview stage failure with a bounded reason code instead of a freeform message. */
25
+ class DeployError extends Z2hError {
26
+ reason;
27
+ constructor(reason, message, data) {
28
+ super(message, reason, data);
29
+ this.reason = reason;
30
+ }
31
+ }
32
+ /**
33
+ * Tags a grant/revoke/transfer-owner failure with a bounded reason code instead of a freeform message.
34
+ * `reason` also accepts a raw HTTP status, which gets mapped to a bounded reason internally.
35
+ */
36
+ class GrantError extends Z2hError {
37
+ constructor(reason, message, data) {
38
+ super(message, typeof reason === 'number' ? grantReasonForStatus(reason) : reason, data);
39
+ }
40
+ }
41
+ /** No z2h JWT found — see auth-store.ts for the discovery order (env var, then ~/.zth/auth.json). */
42
+ class Z2hAuthMissingError extends Z2hError {
43
+ authFilePath;
44
+ constructor(authFilePath) {
45
+ super(`No Z2H deploy token found at ${authFilePath}. Please run 'z2h-cli generate-z2h-token' command.`, 'read_auth_token');
46
+ this.authFilePath = authFilePath;
47
+ }
48
+ }
49
+ /** A git pull diverged or a push was rejected; `reason` distinguishes which stage hit it. */
50
+ class GitConflictError extends Z2hError {
51
+ constructor(message, reason = 'git_error') {
52
+ super(message, reason);
53
+ }
54
+ }
55
+ /** Maps an HTTP status to a bounded grant/revoke/transfer-owner failure reason. */
56
+ function grantReasonForStatus(status) {
57
+ // 401 is an expired/invalid token (re-auth fixes it); 403 is a real permission denial.
58
+ if (status === 401) {
59
+ return 'auth';
60
+ }
61
+ if (status === 403) {
62
+ return 'forbidden';
63
+ }
64
+ if (status === 404) {
65
+ return 'not_found';
66
+ }
67
+ if (status >= 500) {
68
+ return 'server_error';
69
+ }
70
+ return 'unknown';
71
+ }
72
+ /** Tags a failure with `reason`, unless `err` is already classified; `signal` overrides `reason` for a killed child process. */
73
+ function tagDeployError(reason, err) {
74
+ if (err instanceof Z2hError) {
75
+ return err;
76
+ }
77
+ const signal = err instanceof Error ? err.signal : undefined;
78
+ return new DeployError(signal ? 'interrupted' : reason, messageOf(err));
79
+ }
80
+ /** Reads the bounded reason off a classified error; anything unclassified is `unknown`. */
81
+ function reasonOf(err) {
82
+ return err instanceof Z2hError ? err.reason : 'unknown';
83
+ }
84
+ /** Extra telemetry dimensions the throw site attached, if any. */
85
+ function dataOf(err) {
86
+ return err instanceof Z2hError ? err.data ?? {} : {};
87
+ }
88
+ /** Safe message for any thrown value. A non-Error throw must not break the error path. */
89
+ function messageOf(err) {
90
+ return err instanceof Error ? err.message : String(err);
91
+ }
92
+
93
+ exports.DeployError = DeployError;
94
+ exports.GitConflictError = GitConflictError;
95
+ exports.GrantError = GrantError;
96
+ exports.Z2hAuthMissingError = Z2hAuthMissingError;
97
+ exports.Z2hError = Z2hError;
98
+ exports.dataOf = dataOf;
99
+ exports.grantReasonForStatus = grantReasonForStatus;
100
+ exports.messageOf = messageOf;
101
+ exports.reasonOf = reasonOf;
102
+ exports.tagDeployError = tagDeployError;
@@ -1,6 +1,3 @@
1
- export declare class GitConflictError extends Error {
2
- constructor(message: string);
3
- }
4
1
  export interface ConflictInfo {
5
2
  localCommits: string[];
6
3
  remoteCommits: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAkBA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D"}
@@ -5,6 +5,7 @@ const fs = require('fs-extra');
5
5
  const execa = require('execa');
6
6
  const constants = require('../../constants.js');
7
7
  const util_appName = require('../app-name.js');
8
+ const util_errors = require('../errors.js');
8
9
 
9
10
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
10
11
 
@@ -19,12 +20,6 @@ async function execGitLines(args, dir, env) {
19
20
  .then(r => r.stdout.split('\n').filter(Boolean))
20
21
  .catch(() => []);
21
22
  }
22
- class GitConflictError extends Error {
23
- constructor(message) {
24
- super(message);
25
- this.name = 'GitConflictError';
26
- }
27
- }
28
23
  const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
29
24
  const LOCAL_HOOKS_DIR = '.zth/hooks';
30
25
  function defaultS3Remote(app) {
@@ -98,7 +93,7 @@ async function pullAndResolve(dir, env) {
98
93
  }
99
94
  await safeBackupIfDirty(dir);
100
95
  const info = await gatherConflictInfo(dir, gitEnv);
101
- throw new GitConflictError(`Pull rejected: Git conflict detected: ${JSON.stringify(info)}`);
96
+ throw new util_errors.GitConflictError(`Pull rejected: Git conflict detected: ${JSON.stringify(info)}`);
102
97
  }
103
98
  }
104
99
  /**
@@ -126,9 +121,9 @@ async function pushToRemote(dir, env) {
126
121
  }
127
122
  if (isRejected) {
128
123
  const info = await gatherConflictInfo(dir, gitEnv);
129
- throw new GitConflictError(`Push rejected: Git conflict detected: ${JSON.stringify(info)}`);
124
+ throw new util_errors.GitConflictError(`Push rejected: Git conflict detected: ${JSON.stringify(info)}`, 'push_error');
130
125
  }
131
- throw err;
126
+ throw new util_errors.DeployError('push_error', util_errors.messageOf(err));
132
127
  }
133
128
  }
134
129
  async function remoteHead(dir) {
@@ -183,7 +178,6 @@ async function ensureGitignore(dir) {
183
178
  await fs__default.default.outputFile(gitignorePath, content + separator + missing.join('\n') + '\n');
184
179
  }
185
180
 
186
- exports.GitConflictError = GitConflictError;
187
181
  exports.cloneRepo = cloneRepo;
188
182
  exports.defaultS3Remote = defaultS3Remote;
189
183
  exports.getRemoteUrl = getRemoteUrl;
@@ -17,6 +17,9 @@ export declare function output(human: string, data: unknown): void;
17
17
  /**
18
18
  * Emit a structured failure payload (JSON mode) or a red error line (human mode),
19
19
  * then exit with the given code.
20
+ *
21
+ * `track: false` skips the telemetry event but still prints and exits. Commands with no
22
+ * success event pass it, so their failures don't skew per-command error rates.
20
23
  */
21
- export declare function fail(message: string, payload: Record<string, unknown>, exitCode?: number): Promise<never>;
24
+ export declare function fail(message: string, payload: Record<string, unknown>, exitCode?: number, track?: boolean): Promise<never>;
22
25
  //# sourceMappingURL=logger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/util/logger.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAI1C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAEpD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI1C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAMzD;AAED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAW1G"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/util/logger.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAI1C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAEpD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI1C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAMzD;AAED;;;;;;GAMG;AACH,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,QAAQ,SAAI,EACZ,KAAK,UAAO,GACX,OAAO,CAAC,KAAK,CAAC,CAehB"}
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const pc = require('picocolors');
4
+ const observability = require('@mondaydotcomorg/z2h-shared-utils/observability');
4
5
  const util_tracker = require('./tracker.js');
5
6
 
6
7
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -49,10 +50,15 @@ function output(human, data) {
49
50
  /**
50
51
  * Emit a structured failure payload (JSON mode) or a red error line (human mode),
51
52
  * then exit with the given code.
53
+ *
54
+ * `track: false` skips the telemetry event but still prints and exits. Commands with no
55
+ * success event pass it, so their failures don't skew per-command error rates.
52
56
  */
53
- async function fail(message, payload, exitCode = 1) {
57
+ async function fail(message, payload, exitCode = 1, track = true) {
58
+ // Canonical fields go last so a payload key can never shadow `error`/`action`.
59
+ const { command, ...rest } = payload;
54
60
  if (mode === 'json') {
55
- process.stdout.write(`${JSON.stringify({ ok: false, error: message, ...payload })}\n`);
61
+ process.stdout.write(`${JSON.stringify({ ok: false, ...payload, error: message })}\n`);
56
62
  }
57
63
  else {
58
64
  process.stderr.write(pc__default.default.red(`[z2h-cli] ${message}\n`));
@@ -60,7 +66,9 @@ async function fail(message, payload, exitCode = 1) {
60
66
  process.stderr.write(pc__default.default.dim(` → ${payload.suggestedCommand}\n`));
61
67
  }
62
68
  }
63
- await util_tracker.trackEvent('z2h_cli_error', { action: payload.command ?? 'unknown', error: message });
69
+ if (track) {
70
+ await util_tracker.trackEvent(observability.Z2hCliEvents.error, { ...rest, action: command ?? 'unknown', error: message });
71
+ }
64
72
  process.exit(exitCode);
65
73
  }
66
74
 
@@ -0,0 +1,16 @@
1
+ import type { Command } from 'commander';
2
+ import type { CommandSpec } from '../types';
3
+ /**
4
+ * Wraps a command body with the whole telemetry and failure contract so no command
5
+ * hand-rolls it. It times the happy path. On a throw it reads `reason` and `data` straight
6
+ * off the error, defined in ./errors, instead of re-deriving them per command.
7
+ *
8
+ * Returns a commander action handler, chainable straight off `.command(...)` — commander always
9
+ * appends the Command instance as the action handler's last argument, so this splits it back off
10
+ * to read the command's own name/parent instead of asking the caller to pass it in.
11
+ *
12
+ * NoInfer keeps the arg types coming from `fn` alone. A `data` or `label` callback that
13
+ * only needs the first arg must not narrow the handler's own signature.
14
+ */
15
+ export declare function runCommand<A extends unknown[]>(spec: CommandSpec<NoInfer<A>>, fn: (...args: A) => Promise<unknown>): (...args: [...A, Command]) => Promise<void>;
16
+ //# sourceMappingURL=run-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../src/util/run-command.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,UAAU,CAAC;AAcrD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,EAAE,EAC5C,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAC7B,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GACnC,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAyB7C"}
@@ -0,0 +1,49 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const util_logger = require('./logger.js');
4
+ const util_tracker = require('./tracker.js');
5
+ const util_errors = require('./errors.js');
6
+
7
+ /** A spec field is a constant, a callback over the args, or absent — in which case `fallback` applies. */
8
+ const resolveSpecField = (field, args, fallback) => {
9
+ if (field === undefined) {
10
+ return fallback;
11
+ }
12
+ return typeof field === 'function' ? field(args) : field;
13
+ };
14
+ /**
15
+ * Wraps a command body with the whole telemetry and failure contract so no command
16
+ * hand-rolls it. It times the happy path. On a throw it reads `reason` and `data` straight
17
+ * off the error, defined in ./errors, instead of re-deriving them per command.
18
+ *
19
+ * Returns a commander action handler, chainable straight off `.command(...)` — commander always
20
+ * appends the Command instance as the action handler's last argument, so this splits it back off
21
+ * to read the command's own name/parent instead of asking the caller to pass it in.
22
+ *
23
+ * NoInfer keeps the arg types coming from `fn` alone. A `data` or `label` callback that
24
+ * only needs the first arg must not narrow the handler's own signature.
25
+ */
26
+ function runCommand(spec, fn) {
27
+ return async (...allArgs) => {
28
+ const cmd = allArgs[allArgs.length - 1];
29
+ const args = allArgs.slice(0, -1);
30
+ const name = cmd.parent ? `${cmd.parent.name()} ${cmd.name()}` : cmd.name();
31
+ const resolve = (v, fallback) => resolveSpecField(v, args, fallback);
32
+ const track = resolve(spec.track, true);
33
+ const label = resolve(spec.label, name);
34
+ const event = resolve(spec.event, util_tracker.successEvent(name));
35
+ const data = spec.data?.(args) ?? {};
36
+ try {
37
+ await (track ? util_tracker.trackDuration(event, () => fn(...args), data) : fn(...args));
38
+ }
39
+ catch (err) {
40
+ const reason = util_errors.reasonOf(err);
41
+ // A deploy/preview that failed because the consumer's own build broke isn't a CLI/infra
42
+ // problem — don't report it at all, not even as `unknown`.
43
+ const shouldTrack = track && reason !== 'build_error';
44
+ await util_logger.fail(`${label} failed: ${util_errors.messageOf(err)}`, { command: label, reason, ...data, ...util_errors.dataOf(err) }, spec.exitCode ?? (err instanceof util_errors.Z2hError ? err.exitCode : 1), shouldTrack);
45
+ }
46
+ };
47
+ }
48
+
49
+ exports.runCommand = runCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/util/s3/upload.ts"],"names":[],"mappings":"AAeA,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAA0B,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BnB"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/util/s3/upload.ts"],"names":[],"mappings":"AAgBA,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAA0B,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CA8BnB"}
@@ -7,6 +7,7 @@ const mimeTypes = require('mime-types');
7
7
  const pMap = require('p-map');
8
8
  const clientS3 = require('@aws-sdk/client-s3');
9
9
  const constants = require('../../constants.js');
10
+ const util_errors = require('../errors.js');
10
11
  const util_s3_client = require('./client.js');
11
12
 
12
13
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -19,22 +20,27 @@ const pMap__default = /*#__PURE__*/_interopDefault(pMap);
19
20
  const FOREVER = 'public, immutable';
20
21
  const UPLOAD_CONCURRENCY = 5;
21
22
  async function uploadDir(localDir, keyPrefix, bucket = constants.BUCKET_WRITE_NAME) {
22
- const entries = await promises.readdir(localDir, { recursive: true, withFileTypes: true });
23
- const files = entries.filter(e => e.isFile()).map(e => path__default.default.relative(localDir, path__default.default.join(e.parentPath, e.name)));
24
- const s3 = util_s3_client.getS3Client();
25
- await pMap__default.default(files, async (rel) => {
26
- const body = await fs.readFile(path__default.default.join(localDir, rel));
27
- const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
28
- const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
29
- await s3.send(new clientS3.PutObjectCommand({
30
- Bucket: bucket,
31
- Key: key,
32
- Body: body,
33
- ContentType: contentType,
34
- CacheControl: FOREVER,
35
- }));
36
- }, { concurrency: UPLOAD_CONCURRENCY });
37
- return files.map(f => f.split(path__default.default.sep).join('/'));
23
+ try {
24
+ const entries = await promises.readdir(localDir, { recursive: true, withFileTypes: true });
25
+ const files = entries.filter(e => e.isFile()).map(e => path__default.default.relative(localDir, path__default.default.join(e.parentPath, e.name)));
26
+ const s3 = util_s3_client.getS3Client();
27
+ await pMap__default.default(files, async (rel) => {
28
+ const body = await fs.readFile(path__default.default.join(localDir, rel));
29
+ const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
30
+ const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
31
+ await s3.send(new clientS3.PutObjectCommand({
32
+ Bucket: bucket,
33
+ Key: key,
34
+ Body: body,
35
+ ContentType: contentType,
36
+ CacheControl: FOREVER,
37
+ }));
38
+ }, { concurrency: UPLOAD_CONCURRENCY });
39
+ return files.map(f => f.split(path__default.default.sep).join('/'));
40
+ }
41
+ catch (err) {
42
+ throw util_errors.tagDeployError('s3_error', err);
43
+ }
38
44
  }
39
45
 
40
46
  exports.uploadDir = uploadDir;
@@ -1,3 +1,10 @@
1
- export declare function trackEvent(name: string, data: Record<string, unknown>): Promise<void>;
2
- export declare function trackDuration<T>(eventName: string, fn: () => Promise<T>, data?: Record<string, unknown>): Promise<T>;
1
+ export declare const trackEvent: (name: string, data: Record<string, unknown>) => Promise<void>, trackDuration: <T>(name: string, fn: () => Promise<T>, data?: Record<string, unknown>) => Promise<T>;
2
+ /**
3
+ * `z2h_cli_<command>_success` — the success-event name for every command, derived by
4
+ * util/run-command.ts's `runCommand()` so no command has to choose one. Nested commands join
5
+ * their path with `_` (`backend invoke` → `z2h_cli_backend_invoke_success`). The one place a
6
+ * command names its own event, `deploy --preview`, still goes through this function so the
7
+ * convention holds everywhere. See CommandSpec's `event` field in types.ts.
8
+ */
9
+ export declare function successEvent(command: string): string;
3
10
  //# sourceMappingURL=tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/util/tracker.ts"],"names":[],"mappings":"AASA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3F;AAED,wBAAsB,aAAa,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CAKZ"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/util/tracker.ts"],"names":[],"mappings":"AAcA,eAAO,MAAQ,UAAU,kEAAE,aAAa,uFAItC,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -1,7 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const os = require('node:os');
4
+ const node_module = require('node:module');
5
+ const observability = require('@mondaydotcomorg/z2h-shared-utils/observability');
4
6
 
7
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
5
8
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
6
9
 
7
10
  const os__default = /*#__PURE__*/_interopDefault(os);
@@ -9,22 +12,24 @@ const os__default = /*#__PURE__*/_interopDefault(os);
9
12
  const DOORMAN_URL = 'https://track-int.bigbrain.me/prod/event';
10
13
  const DOORMAN_URL_STAGING = 'https://track-int.bigbrainstaging.me/stage/event';
11
14
  const username = os__default.default.userInfo().username;
15
+ const { version: cliVersion } = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('util/tracker.js', document.baseURI).href)))('@mondaydotcomorg/z2h-cli/package.json');
12
16
  const isStaging = process.env.NODE_ENV === 'staging';
13
- async function trackEvent(name, data) {
14
- const doormanUrl = isStaging ? DOORMAN_URL_STAGING : DOORMAN_URL;
15
- await fetch(doormanUrl, {
16
- method: 'POST',
17
- headers: { 'Content-Type': 'application/json' },
18
- body: JSON.stringify({ name, source: 'z2h-cli', data: { user_name: username, ...data } }),
19
- signal: AbortSignal.timeout(5000),
20
- }).catch(() => { });
21
- }
22
- async function trackDuration(eventName, fn, data) {
23
- const start = Date.now();
24
- const result = await fn();
25
- await trackEvent(eventName, { ...data, duration_ms: Date.now() - start });
26
- return result;
17
+ const { trackEvent, trackDuration } = observability.createHttpTracker({
18
+ url: isStaging ? DOORMAN_URL_STAGING : DOORMAN_URL,
19
+ source: 'z2h-cli',
20
+ getContext: () => ({ user_name: username, cli_version: cliVersion }),
21
+ });
22
+ /**
23
+ * `z2h_cli_<command>_success` — the success-event name for every command, derived by
24
+ * util/run-command.ts's `runCommand()` so no command has to choose one. Nested commands join
25
+ * their path with `_` (`backend invoke` → `z2h_cli_backend_invoke_success`). The one place a
26
+ * command names its own event, `deploy --preview`, still goes through this function so the
27
+ * convention holds everywhere. See CommandSpec's `event` field in types.ts.
28
+ */
29
+ function successEvent(command) {
30
+ return `z2h_cli_${command.replace(/[\s-]+/g, '_')}_success`;
27
31
  }
28
32
 
33
+ exports.successEvent = successEvent;
29
34
  exports.trackDuration = trackDuration;
30
35
  exports.trackEvent = trackEvent;
@@ -1,6 +1,7 @@
1
1
  import { logger } from '@mondaydotcomorg/trident-backend-runtime';
2
2
  import { createMondayModel } from '@mondaydotcomorg/kong-ai-gateway-sdk/langchain';
3
- import { BackendRunnerEvents, trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability';
3
+ import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
4
+ import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
4
5
 
5
6
  import { ChannelError } from '../../backend-runner.errors';
6
7
  import { BI_SOURCE } from '../../constants';
@@ -209,7 +210,7 @@ export function makeLlmChannel(appName: string, caller: Caller) {
209
210
  LLM_TIMEOUT_MS,
210
211
  'LLM gateway',
211
212
  );
212
- trackEvent(BackendRunnerEvents.llm.call, { appName, email: caller.email, modelId: model }, BI_SOURCE);
213
+ void trackEvent(BackendRunnerEvents.llm.call, { appName, email: caller.email, modelId: model }, BI_SOURCE);
213
214
  return { content: extractText(response.content) };
214
215
  } catch (err) {
215
216
  throw toLlmChannelError(err);
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
3
  import { context, logger } from '@mondaydotcomorg/trident-backend-runtime';
4
- import { BackendRunnerEvents, trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability';
4
+ import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
5
+ import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
5
6
  import { getDefaultClient } from 'src/utils/elasticache';
6
7
 
7
8
  import { ChannelError } from '../../backend-runner.errors';
@@ -109,8 +110,8 @@ function resolveTimeout(timeoutMs: number | undefined): number {
109
110
  */
110
111
  export async function runSnowflakeQuery(
111
112
  sql: string,
113
+ appName: string,
112
114
  params?: Record<string, SnowflakeParamValue> | null,
113
- appName?: string,
114
115
  options?: RunQueryOptions | null,
115
116
  ): Promise<SnowflakeRow[]> {
116
117
  const timeoutInMs = resolveTimeout(options?.timeoutMs);
@@ -138,7 +139,7 @@ export async function runSnowflakeQuery(
138
139
  const cached = scope ? await readCache(queryCacheKey, scope) : null;
139
140
  if (cached) {
140
141
  const duration_ms = Date.now() - startMs;
141
- trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: true }, BI_SOURCE);
142
+ void trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: true }, BI_SOURCE);
142
143
  logger.info(
143
144
  {
144
145
  tag: LOG_TAG,
@@ -155,7 +156,7 @@ export async function runSnowflakeQuery(
155
156
 
156
157
  const queryOptions = {
157
158
  query: boundSql,
158
- identifier: `z2h-query:${queryIdentifier}`,
159
+ identifier: `z2h-query:${appName}:${queryIdentifier}`,
159
160
  fallback: [] as SnowflakeRow[],
160
161
  timeoutInMs,
161
162
  convertToSingularResult: false,
@@ -182,7 +183,7 @@ export async function runSnowflakeQuery(
182
183
  );
183
184
  } catch (err) {
184
185
  // Failure BI tracking already happens generically at the invoke level
185
- // (runner.service.ts's z2h_backend_invoke_failed, tagged with this
186
+ // (runner.service.ts's BackendRunnerEvents.invoke.failed, tagged with this
186
187
  // ChannelError's `code`) — this stays a log only, for the queryHash/
187
188
  // queryIdentifier that generic event can't know about.
188
189
  const duration_ms = Date.now() - startMs;
@@ -195,7 +196,7 @@ export async function runSnowflakeQuery(
195
196
  await writeCache(queryCacheKey, scope, rows);
196
197
  }
197
198
  const duration_ms = Date.now() - startMs;
198
- trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: false }, BI_SOURCE);
199
+ void trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: false }, BI_SOURCE);
199
200
  logger.info(
200
201
  { tag: LOG_TAG, queryHash: queryCacheKey, queryIdentifier, duration_ms, rowCount: rows.length, cache_hit: false },
201
202
  'query executed',
@@ -96,8 +96,8 @@ export function makeSnowflakeChannel(appName: string) {
96
96
  }
97
97
  return runSnowflakeQuery(
98
98
  query,
99
- params as Record<string, SnowflakeParamValue> | null | undefined,
100
99
  appName,
100
+ params as Record<string, SnowflakeParamValue> | null | undefined,
101
101
  options as RunQueryOptions | null | undefined,
102
102
  );
103
103
  },
@@ -23,10 +23,7 @@ export interface SnowflakeConnectionDetails {
23
23
  defaultDatabase?: string;
24
24
  }
25
25
 
26
- export function isStatelessPartBApp(appName: string | undefined): boolean {
27
- if (!appName) {
28
- return false;
29
- }
26
+ export function isStatelessPartBApp(appName: string): boolean {
30
27
  const { allowed } = getConfiguration(ConfigurationKey.Z2H_STATELESS_PART_B_APPS);
31
28
  return Array.isArray(allowed) && allowed.includes(appName);
32
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.25.3",
3
+ "version": "0.25.7",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -51,7 +51,7 @@
51
51
  ],
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-s3": "^3.706.0",
54
- "@mondaydotcomorg/z2h-shared-utils": "^0.3.3",
54
+ "@mondaydotcomorg/z2h-shared-utils": "^0.4.1",
55
55
  "chokidar": "^3.6.0",
56
56
  "commander": "^12.1.0",
57
57
  "cors": "^2.8.6",
package/src/auth.ts CHANGED
@@ -1,2 +1,3 @@
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';
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import { deployBackend, hasHandlers } from '../backend';
7
7
  import { readHandlers } from '../../backend/handlers';
8
+ import { DeployError } from '../../util/errors';
8
9
 
9
10
  vi.mock('../../backend/handlers', () => ({
10
11
  readHandlers: vi.fn(),
@@ -96,6 +97,23 @@ describe('deployBackend (live)', () => {
96
97
  const keys: string[] = mockSend.mock.calls.map((c: unknown[]) => (c[0] as { input: { Key: string } }).input.Key);
97
98
  expect(keys.some(k => k.includes('/backend-1/'))).toBe(true);
98
99
  });
100
+
101
+ it('tags a handler upload failure as deploy_backend_error, not register_error', async () => {
102
+ await ensureDir(path.join(tmp, 'backend', 'handlers'));
103
+ mockSend.mockRejectedValueOnce(new Error('AccessDenied'));
104
+
105
+ const err = await deployBackend({
106
+ appDir: tmp,
107
+ appName: 'mf-my-app',
108
+ mfKeyPrefix: 'mf-my-app/1',
109
+ version: 1,
110
+ deployedBy: 'test-user',
111
+ isPreview: false,
112
+ }).catch((e: unknown) => e);
113
+ expect(err).toBeInstanceOf(DeployError);
114
+ expect((err as DeployError).reason).toBe('deploy_backend_error');
115
+ expect((err as DeployError).message).toBe('AccessDenied');
116
+ });
99
117
  });
100
118
 
101
119
  describe('deployBackend (preview)', () => {