@mondaydotcomorg/z2h-cli 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/commands/__tests__/edit.test.d.ts +2 -0
  2. package/dist/commands/__tests__/edit.test.d.ts.map +1 -0
  3. package/dist/commands/create.d.ts +2 -2
  4. package/dist/commands/create.d.ts.map +1 -1
  5. package/dist/commands/create.js +14 -34
  6. package/dist/commands/deploy.d.ts +3 -0
  7. package/dist/commands/deploy.d.ts.map +1 -1
  8. package/dist/commands/deploy.js +123 -23
  9. package/dist/commands/edit.d.ts +5 -0
  10. package/dist/commands/edit.d.ts.map +1 -0
  11. package/dist/commands/edit.js +61 -0
  12. package/dist/commands/grant.d.ts +12 -0
  13. package/dist/commands/grant.d.ts.map +1 -0
  14. package/dist/commands/grant.js +94 -0
  15. package/dist/constants.d.ts +1 -0
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +7 -0
  18. package/dist/esm/commands/__tests__/edit.test.d.ts +2 -0
  19. package/dist/esm/commands/__tests__/edit.test.d.ts.map +1 -0
  20. package/dist/esm/commands/create.d.ts +2 -2
  21. package/dist/esm/commands/create.d.ts.map +1 -1
  22. package/dist/esm/commands/create.mjs +16 -35
  23. package/dist/esm/commands/deploy.d.ts +3 -0
  24. package/dist/esm/commands/deploy.d.ts.map +1 -1
  25. package/dist/esm/commands/deploy.mjs +124 -24
  26. package/dist/esm/commands/edit.d.ts +5 -0
  27. package/dist/esm/commands/edit.d.ts.map +1 -0
  28. package/dist/esm/commands/edit.mjs +55 -0
  29. package/dist/esm/commands/grant.d.ts +12 -0
  30. package/dist/esm/commands/grant.d.ts.map +1 -0
  31. package/dist/esm/commands/grant.mjs +90 -0
  32. package/dist/esm/constants.d.ts +1 -0
  33. package/dist/esm/constants.d.ts.map +1 -1
  34. package/dist/esm/constants.mjs +7 -1
  35. package/dist/esm/index.mjs +67 -10
  36. package/dist/esm/shadow/prepare.d.ts +1 -1
  37. package/dist/esm/shadow/prepare.d.ts.map +1 -1
  38. package/dist/esm/shadow/prepare.mjs +2 -2
  39. package/dist/esm/util/app-name.d.ts +1 -0
  40. package/dist/esm/util/app-name.d.ts.map +1 -1
  41. package/dist/esm/util/app-name.mjs +11 -1
  42. package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
  43. package/dist/esm/util/auth/auth-store.mjs +10 -0
  44. package/dist/esm/util/auth/broker-credential-provider.d.ts +10 -2
  45. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
  46. package/dist/esm/util/auth/broker-credential-provider.mjs +35 -13
  47. package/dist/esm/util/broker/app.d.ts +14 -0
  48. package/dist/esm/util/broker/app.d.ts.map +1 -0
  49. package/dist/esm/util/broker/app.mjs +27 -0
  50. package/dist/esm/util/git/git-env.d.ts +9 -0
  51. package/dist/esm/util/git/git-env.d.ts.map +1 -0
  52. package/dist/esm/util/git/git-env.mjs +22 -0
  53. package/dist/esm/util/git/remote-options.d.ts +17 -0
  54. package/dist/esm/util/git/remote-options.d.ts.map +1 -0
  55. package/dist/esm/util/git/remote-options.mjs +30 -0
  56. package/dist/esm/util/git/repo.d.ts +21 -3
  57. package/dist/esm/util/git/repo.d.ts.map +1 -1
  58. package/dist/esm/util/git/repo.mjs +103 -47
  59. package/dist/esm/util/machine-identity.d.ts +9 -0
  60. package/dist/esm/util/machine-identity.d.ts.map +1 -0
  61. package/dist/esm/util/machine-identity.mjs +12 -0
  62. package/dist/esm/util/s3/client.d.ts +2 -7
  63. package/dist/esm/util/s3/client.d.ts.map +1 -1
  64. package/dist/esm/util/s3/client.mjs +3 -11
  65. package/dist/index.js +69 -12
  66. package/dist/shadow/prepare.d.ts +1 -1
  67. package/dist/shadow/prepare.d.ts.map +1 -1
  68. package/dist/shadow/prepare.js +2 -2
  69. package/dist/util/app-name.d.ts +1 -0
  70. package/dist/util/app-name.d.ts.map +1 -1
  71. package/dist/util/app-name.js +11 -0
  72. package/dist/util/auth/auth-store.d.ts.map +1 -1
  73. package/dist/util/auth/auth-store.js +10 -0
  74. package/dist/util/auth/broker-credential-provider.d.ts +10 -2
  75. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
  76. package/dist/util/auth/broker-credential-provider.js +35 -11
  77. package/dist/util/broker/app.d.ts +14 -0
  78. package/dist/util/broker/app.d.ts.map +1 -0
  79. package/dist/util/broker/app.js +29 -0
  80. package/dist/util/git/git-env.d.ts +9 -0
  81. package/dist/util/git/git-env.d.ts.map +1 -0
  82. package/dist/util/git/git-env.js +24 -0
  83. package/dist/util/git/remote-options.d.ts +17 -0
  84. package/dist/util/git/remote-options.d.ts.map +1 -0
  85. package/dist/util/git/remote-options.js +34 -0
  86. package/dist/util/git/repo.d.ts +21 -3
  87. package/dist/util/git/repo.d.ts.map +1 -1
  88. package/dist/util/git/repo.js +104 -47
  89. package/dist/util/machine-identity.d.ts +9 -0
  90. package/dist/util/machine-identity.d.ts.map +1 -0
  91. package/dist/util/machine-identity.js +14 -0
  92. package/dist/util/s3/client.d.ts +2 -7
  93. package/dist/util/s3/client.d.ts.map +1 -1
  94. package/dist/util/s3/client.js +3 -11
  95. package/package.json +1 -1
  96. package/src/commands/__tests__/create.test.ts +9 -7
  97. package/src/commands/__tests__/edit.test.ts +184 -0
  98. package/src/commands/create.ts +15 -33
  99. package/src/commands/deploy.ts +147 -33
  100. package/src/commands/edit.ts +63 -0
  101. package/src/commands/grant.ts +115 -0
  102. package/src/constants.ts +7 -0
  103. package/src/index.ts +83 -13
  104. package/src/shadow/prepare.ts +2 -2
  105. package/src/templates/app/CLAUDE.md +15 -0
  106. package/src/util/app-name.ts +10 -0
  107. package/src/util/auth/__tests__/broker-credential-provider.test.ts +34 -2
  108. package/src/util/auth/auth-store.ts +12 -0
  109. package/src/util/auth/broker-credential-provider.ts +43 -15
  110. package/src/util/broker/app.ts +34 -0
  111. package/src/util/git/__tests__/repo.test.ts +108 -90
  112. package/src/util/git/git-env.ts +23 -0
  113. package/src/util/git/remote-options.ts +37 -0
  114. package/src/util/git/repo.ts +121 -47
  115. package/src/util/machine-identity.ts +10 -0
  116. package/src/util/s3/client.ts +4 -11
@@ -4,6 +4,14 @@ import { execa } from 'execa';
4
4
  import { BUCKET_WRITE_NAME } from '../../constants.mjs';
5
5
  import { mfAppName } from '../app-name.mjs';
6
6
 
7
+ function execGit(args, dir, env) {
8
+ return execa('git', args, { cwd: dir, env });
9
+ }
10
+ async function execGitLines(args, dir, env) {
11
+ return execGit(args, dir, env)
12
+ .then(r => r.stdout.split('\n').filter(Boolean))
13
+ .catch(() => []);
14
+ }
7
15
  class GitConflictError extends Error {
8
16
  constructor(message) {
9
17
  super(message);
@@ -11,28 +19,42 @@ class GitConflictError extends Error {
11
19
  }
12
20
  }
13
21
  const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
22
+ const LOCAL_HOOKS_DIR = '.zth/hooks';
14
23
  function defaultS3Remote(app) {
15
24
  return `s3://${BUCKET_WRITE_NAME}/${mfAppName(app)}/git`;
16
25
  }
17
26
  async function initRepo(dir, remote) {
18
27
  await ensureGitignore(dir);
19
- await execa('git', ['init', '-b', 'master'], { cwd: dir });
20
- await execa('git', ['add', '.'], { cwd: dir });
21
- await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
22
- await execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
28
+ await execGit(['init', '-b', 'master'], dir);
29
+ await overrideGlobalHooks(dir);
30
+ await execGit(['add', '.'], dir);
31
+ await execGit(['commit', '-m', 'Initial commit'], dir);
32
+ await execGit(['remote', 'add', 'origin', remote], dir);
23
33
  }
24
34
  async function getRemoteUrl(dir) {
25
- return execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
35
+ return execGit(['remote', 'get-url', 'origin'], dir)
26
36
  .then(r => r.stdout.trim() || undefined)
27
37
  .catch(() => undefined);
28
38
  }
29
- async function cloneRepo(remote, dir) {
30
- await execa('git', ['clone', remote, dir]);
39
+ /**
40
+ * Throws if the working tree has uncommitted changes. Call only on a known git
41
+ * repo (e.g. guarded by a resolved remote) — a failing `git status` propagates
42
+ * rather than being silently treated as "clean".
43
+ */
44
+ async function guardCleanTree(dir) {
45
+ const { stdout } = await execGit(['status', '--porcelain'], dir);
46
+ if (stdout.trim()) {
47
+ throw new Error('You have uncommitted changes — commit your changes before deploying.');
48
+ }
49
+ }
50
+ async function cloneRepo(remote, dir, env = {}) {
51
+ await execa('git', ['clone', remote, dir], { env: { ...process.env, ...env } });
52
+ await overrideGlobalHooks(dir);
31
53
  }
32
54
  async function safeBackupIfDirty(dir) {
33
55
  let status;
34
56
  try {
35
- ({ stdout: status } = await execa('git', ['status', '--porcelain'], { cwd: dir }));
57
+ ({ stdout: status } = await execGit(['status', '--porcelain'], dir));
36
58
  }
37
59
  catch {
38
60
  return;
@@ -45,17 +67,55 @@ async function safeBackupIfDirty(dir) {
45
67
  const backupDir = path.join(path.dirname(dir), `${appName}.backup-${sha}`);
46
68
  await fs.copy(dir, backupDir, { overwrite: false });
47
69
  }
48
- async function pullAndResolve(dir) {
70
+ async function pullAndResolve(dir, env) {
71
+ const gitEnv = env ? { ...process.env, ...env } : undefined;
49
72
  try {
50
- await execa('git', ['pull', '--ff-only'], { cwd: dir });
73
+ await execGit(['pull', '--ff-only'], dir, gitEnv);
51
74
  }
52
- catch {
53
- // TODO(Phase 7): safeBackupIfDirty(dir) → resolveDivergence(dir) instead of throwing
54
- throw new GitConflictError('This app changed while you were working — please re-open it to get the latest version.');
75
+ catch (err) {
76
+ // First deploy: remote exists but has no commits yet — not a conflict.
77
+ const e = err;
78
+ const msg = `${e.message ?? ''} ${e.stderr ?? ''}`;
79
+ if (/couldn't find remote ref|no tracking information|does not appear to be a git repository/i.test(msg)) {
80
+ return;
81
+ }
82
+ await safeBackupIfDirty(dir);
83
+ const info = await gatherConflictInfo(dir, gitEnv);
84
+ throw new GitConflictError(JSON.stringify(info));
85
+ }
86
+ }
87
+ /**
88
+ * Pushes the local master branch to origin. On a rejected (non-fast-forward) push —
89
+ * i.e. a deploy landed while we were building — gathers structured conflict info and
90
+ * throws GitConflictError so the deploy stops before anything goes live (spec: push
91
+ * must stop the deploy before the bundle upload). The skill re-resolves and re-runs,
92
+ * which rebuilds against the latest. Non-conflict push errors re-throw as-is.
93
+ */
94
+ async function pushToRemote(dir, env) {
95
+ const gitEnv = env ? { ...process.env, ...env } : undefined;
96
+ try {
97
+ await execGit(['push', '-u', 'origin', 'master'], dir, gitEnv);
98
+ }
99
+ catch (err) {
100
+ // execa puts the command in message but stderr in a separate field.
101
+ const e = err;
102
+ const msg = `${e.message ?? ''} ${e.stderr ?? ''}`;
103
+ const isRejected = /\[rejected\]|non-fast-forward/.test(msg);
104
+ // git-remote-s3 emits EPIPE when git closes its stdin after a completed
105
+ // push (new commits or already-up-to-date). Treat EPIPE without a
106
+ // rejection marker as success — the push went through.
107
+ if (/EPIPE/.test(msg) && !isRejected) {
108
+ return;
109
+ }
110
+ if (isRejected) {
111
+ const info = await gatherConflictInfo(dir, gitEnv);
112
+ throw new GitConflictError(JSON.stringify(info));
113
+ }
114
+ throw err;
55
115
  }
56
116
  }
57
117
  async function remoteHead(dir) {
58
- const { stdout } = await execa('git', ['ls-remote', 'origin', 'refs/heads/master'], { cwd: dir });
118
+ const { stdout } = await execGit(['ls-remote', 'origin', 'refs/heads/master'], dir);
59
119
  const sha = stdout.split('\t')[0]?.trim();
60
120
  if (!sha) {
61
121
  throw new Error('could not read remote head — is the remote reachable?');
@@ -63,42 +123,38 @@ async function remoteHead(dir) {
63
123
  return sha;
64
124
  }
65
125
  async function localHead(dir) {
66
- const { stdout } = await execa('git', ['rev-parse', 'HEAD'], { cwd: dir });
126
+ const { stdout } = await execGit(['rev-parse', 'HEAD'], dir);
67
127
  return stdout.trim();
68
128
  }
69
- async function commitDetailedAndPush(dir, version) {
70
- const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
71
- if (branch.trim() !== 'master') {
72
- throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
73
- }
74
- await execa('git', ['add', '.'], { cwd: dir });
75
- const { stdout: staged } = await execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
76
- if (!staged.trim()) {
77
- throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
78
- }
79
- const message = await buildCommitMessage(dir, version);
80
- await execa('git', ['commit', '-m', message], { cwd: dir });
81
- try {
82
- await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
129
+ async function gatherConflictInfo(dir, gitEnv) {
130
+ await execGit(['fetch', 'origin'], dir, gitEnv).catch(() => { });
131
+ const [localCommits, remoteCommits, conflictingFiles] = await Promise.all([
132
+ execGitLines(['log', 'origin/master..HEAD', '--oneline'], dir, gitEnv),
133
+ execGitLines(['log', 'HEAD..origin/master', '--oneline'], dir, gitEnv),
134
+ execGitLines(['diff', '--name-only', 'HEAD', 'origin/master'], dir, gitEnv),
135
+ ]);
136
+ return { localCommits, remoteCommits, conflictingFiles };
137
+ }
138
+ /**
139
+ * If the user has a global core.hooksPath (e.g. ~/.global-git-hooks with a
140
+ * prevent-push-to-master script), override it locally so Z2H's non-interactive
141
+ * pushes aren't blocked. Other repos on the machine stay protected.
142
+ */
143
+ async function overrideGlobalHooks(dir) {
144
+ if (await fs.pathExists(path.join(dir, LOCAL_HOOKS_DIR))) {
145
+ return;
83
146
  }
84
- catch (err) {
85
- if (/\[rejected\]|non-fast-forward/.test(err.message)) {
86
- throw new GitConflictError('push rejected — remote has new commits');
87
- }
88
- throw err;
147
+ const globalHooksPath = await execGit(['config', '--global', 'core.hooksPath'], dir)
148
+ .then(r => r.stdout.trim())
149
+ .catch(() => '');
150
+ if (!globalHooksPath) {
151
+ return;
89
152
  }
90
- }
91
- async function buildCommitMessage(dir, version) {
92
- // TODO(Phase 7): replace with LLM-generated summary using git diff + commit messages
93
- const { stdout } = await execa('git', ['status', '--short'], { cwd: dir });
94
- const files = stdout
95
- .split('\n')
96
- .map(line => line.slice(3).trim())
97
- .filter(Boolean);
98
- const fileList = files.length
99
- ? files.slice(0, 5).join(', ') + (files.length > 5 ? ` (+${files.length - 5} more)` : '')
100
- : 'no changes';
101
- return `v${version}: deploy — ${fileList}`;
153
+ await fs.ensureDir(path.join(dir, LOCAL_HOOKS_DIR));
154
+ await execGit(['config', '--local', 'core.hooksPath', LOCAL_HOOKS_DIR], dir);
155
+ // eslint-disable-next-line no-console
156
+ console.warn(`⚠️ Detected global git hooks at ${globalHooksPath}\n` +
157
+ ` Overriding locally for this Z2H project (your other repos are unaffected).`);
102
158
  }
103
159
  async function ensureGitignore(dir) {
104
160
  const gitignorePath = path.join(dir, '.gitignore');
@@ -118,4 +174,4 @@ async function ensureGitignore(dir) {
118
174
  await fs.outputFile(gitignorePath, content + separator + missing.join('\n') + '\n');
119
175
  }
120
176
 
121
- export { GitConflictError, cloneRepo, commitDetailedAndPush, defaultS3Remote, getRemoteUrl, initRepo, localHead, pullAndResolve, remoteHead, safeBackupIfDirty };
177
+ export { GitConflictError, cloneRepo, defaultS3Remote, getRemoteUrl, guardCleanTree, initRepo, localHead, pullAndResolve, pushToRemote, remoteHead, safeBackupIfDirty };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns the machine app name when running as a service (e.g. agent-runner).
3
+ * Available when the process is a monday.com microservice with `APP_NAME` set
4
+ * by the platform runtime.
5
+ *
6
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
7
+ */
8
+ export declare function getMachineAppName(): string | undefined;
9
+ //# sourceMappingURL=machine-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns the machine app name when running as a service (e.g. agent-runner).
3
+ * Available when the process is a monday.com microservice with `APP_NAME` set
4
+ * by the platform runtime.
5
+ *
6
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
7
+ */
8
+ function getMachineAppName() {
9
+ return process.env.APP_NAME || undefined;
10
+ }
11
+
12
+ export { getMachineAppName };
@@ -1,10 +1,5 @@
1
1
  import { S3Client } from '@aws-sdk/client-s3';
2
- /**
3
- * Initialize the per-process S3 client with broker-issued credentials scoped
4
- * to the given app's S3 prefix. Must be called once before any `getS3Client`.
5
- */
6
- export declare function setupS3Client({ appName }: {
7
- appName: string;
8
- }): void;
2
+ import type { AwsCredentialIdentity } from '@aws-sdk/types';
3
+ export declare function setupS3Client(credentials: AwsCredentialIdentity): void;
9
4
  export declare function getS3Client(): S3Client;
10
5
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/util/s3/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAM9C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKpE;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAKtC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/util/s3/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAI5D,wBAAgB,aAAa,CAAC,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAEtE;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAKtC"}
@@ -1,21 +1,13 @@
1
1
  import { S3Client } from '@aws-sdk/client-s3';
2
- import { brokerCredentialProvider } from '../auth/broker-credential-provider.mjs';
3
2
  import { REGION } from '../../constants.mjs';
4
3
 
5
4
  let client = null;
6
- /**
7
- * Initialize the per-process S3 client with broker-issued credentials scoped
8
- * to the given app's S3 prefix. Must be called once before any `getS3Client`.
9
- */
10
- function setupS3Client({ appName }) {
11
- client = new S3Client({
12
- region: REGION,
13
- credentials: brokerCredentialProvider({ appName }),
14
- });
5
+ function setupS3Client(credentials) {
6
+ client = new S3Client({ region: REGION, credentials });
15
7
  }
16
8
  function getS3Client() {
17
9
  if (!client) {
18
- throw new Error('S3 client not initialized — call setupS3Client({ appName }) first.');
10
+ throw new Error('S3 client not initialized — call setupS3Client(credentials) first.');
19
11
  }
20
12
  return client;
21
13
  }
package/dist/index.js CHANGED
@@ -1,19 +1,22 @@
1
1
  const node_module = require('node:module');
2
- const fs = require('node:fs');
3
2
  const os = require('node:os');
4
3
  const path = require('node:path');
5
4
  const commander = require('commander');
6
5
  const execa = require('execa');
6
+ const util_workspace = require('./util/workspace.js');
7
7
  const commands_dev = require('./commands/dev.js');
8
8
  const commands_build = require('./commands/build.js');
9
9
  const commands_clean = require('./commands/clean.js');
10
10
  const commands_deploy = require('./commands/deploy.js');
11
+ const commands_grant = require('./commands/grant.js');
11
12
  const commands_createWorkspace = require('./commands/create-workspace.js');
12
13
  const commands_create = require('./commands/create.js');
13
14
  const commands_generateZ2hToken = require('./commands/generate-z2h-token.js');
14
15
  const commands_migrate = require('./commands/migrate.js');
16
+ const commands_edit = require('./commands/edit.js');
15
17
  const util_logger = require('./util/logger.js');
16
18
  const util_tracker = require('./util/tracker.js');
19
+ const util_appName = require('./util/app-name.js');
17
20
 
18
21
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
19
22
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -34,15 +37,14 @@ function selfUpdate() {
34
37
  util_logger.errorLog(`Z2H auto-update failed to start: ${err.message}`);
35
38
  }
36
39
  }
37
- function getCwdAppName() {
38
- try {
39
- return JSON.parse(fs.readFileSync('package.json', 'utf8')).name;
40
- }
41
- catch {
42
- return undefined;
40
+ const CONFLICTING_ENV_VARS = ['AWS_PROFILE'];
41
+ function stripConflictingEnvVars() {
42
+ for (const name of CONFLICTING_ENV_VARS) {
43
+ delete process.env[name];
43
44
  }
44
45
  }
45
46
  async function main() {
47
+ stripConflictingEnvVars();
46
48
  const program = new commander.Command();
47
49
  program
48
50
  .name('z2h-cli')
@@ -65,7 +67,7 @@ async function main() {
65
67
  .option('--json', 'emit structured JSON on stdout')
66
68
  .action(async (opts) => {
67
69
  try {
68
- await util_tracker.trackDuration('z2h_app_build_success', () => commands_build.buildCommand(opts), { app_name: getCwdAppName() });
70
+ await util_tracker.trackDuration('z2h_app_build_success', () => commands_build.buildCommand(opts), { app_name: util_appName.getCwdAppName() });
69
71
  }
70
72
  catch (err) {
71
73
  await util_logger.fail(`build failed: ${err.message}`, { command: 'build' }, 2);
@@ -97,13 +99,15 @@ async function main() {
97
99
  });
98
100
  program
99
101
  .command('deploy')
100
- .description('Build, upload artifacts to S3, and update the Z2H manifest')
102
+ .description('Build, upload to S3, and register the app with the Z2H backend')
101
103
  .option('--dry-run', 'print what would happen without uploading anything')
102
104
  .option('--json', 'emit structured JSON on stdout')
105
+ .option('--remote <type>', 'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.')
106
+ .option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
103
107
  .action(async (opts) => {
104
108
  try {
105
109
  await util_tracker.trackDuration('z2h_app_deploy_success', () => commands_deploy.deployCommand(opts), {
106
- app_name: getCwdAppName(),
110
+ app_name: util_appName.getCwdAppName(),
107
111
  });
108
112
  }
109
113
  catch (err) {
@@ -112,7 +116,7 @@ async function main() {
112
116
  });
113
117
  program
114
118
  .command('create-workspace')
115
- .description('Bootstrap the ~/zth-projects/ yarn workspace (one-time setup)')
119
+ .description(`Bootstrap the ${util_workspace.getWorkspaceHome()}/ yarn workspace (one-time setup)`)
116
120
  .option('--json', 'emit structured JSON on stdout')
117
121
  .action(async (opts) => {
118
122
  try {
@@ -138,7 +142,7 @@ async function main() {
138
142
  });
139
143
  program
140
144
  .command('create <app-name>')
141
- .description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
145
+ .description(`Scaffold a new Z2H consumer app inside ${util_workspace.getWorkspaceHome()}/`)
142
146
  .option('--template <name>', 'Template name (default | html-embed)')
143
147
  .option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
144
148
  .option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
@@ -156,6 +160,59 @@ async function main() {
156
160
  await util_logger.fail(`create failed: ${err.message}`, { command: 'create' }, 1);
157
161
  }
158
162
  });
163
+ program
164
+ .command('edit <app-name>')
165
+ .description(`Pull the latest version of an app into ${util_workspace.getWorkspaceHome()}/<app-name>, cloning it first if needed`)
166
+ .option('--json', 'emit structured JSON on stdout')
167
+ .action(async (appName, opts) => {
168
+ try {
169
+ await util_tracker.trackDuration('z2h_app_edit_pulled', () => commands_edit.editCommand(appName, opts), {
170
+ app_name: appName,
171
+ });
172
+ }
173
+ catch (err) {
174
+ await util_logger.fail(`edit failed: ${err.message}`, { command: 'edit' }, 1);
175
+ }
176
+ });
177
+ program
178
+ .command('grant [email]')
179
+ .description('Grant access to the current app (email, --public, or both)')
180
+ .option('--public', 'make the app visible to all monday.com employees')
181
+ .option('--level <level>', 'access level for user grants: owner, editor, or viewer (default: viewer)')
182
+ .option('--json', 'emit structured JSON on stdout')
183
+ .action(async (email, opts) => {
184
+ try {
185
+ await commands_grant.grantCommand(email, opts);
186
+ }
187
+ catch (err) {
188
+ await util_logger.fail(`grant failed: ${err.message}`, { command: 'grant' }, 1);
189
+ }
190
+ });
191
+ program
192
+ .command('revoke [email]')
193
+ .description('Revoke access to the current app (email or --public; owners/admins only)')
194
+ .option('--public', 'remove the public grant, making the app visible to owners/admins only')
195
+ .option('--json', 'emit structured JSON on stdout')
196
+ .action(async (email, opts) => {
197
+ try {
198
+ await commands_grant.revokeCommand(email, opts);
199
+ }
200
+ catch (err) {
201
+ await util_logger.fail(`revoke failed: ${err.message}`, { command: 'revoke' }, 1);
202
+ }
203
+ });
204
+ program
205
+ .command('transfer-owner [email]')
206
+ .description('Transfer ownership of the current app to another monday.com user')
207
+ .option('--json', 'emit structured JSON on stdout')
208
+ .action(async (email, opts) => {
209
+ try {
210
+ await commands_grant.transferOwnerCommand(email, opts);
211
+ }
212
+ catch (err) {
213
+ await util_logger.fail(`transfer-owner failed: ${err.message}`, { command: 'transfer-owner' }, 1);
214
+ }
215
+ });
159
216
  program
160
217
  .command('migrate')
161
218
  .description('Run pending workspace migrations')
@@ -1,3 +1,3 @@
1
1
  import type { PreparedShadow } from '../types';
2
- export declare function prepareShadow(): Promise<PreparedShadow>;
2
+ export declare function prepareShadow(consumerDir?: string): Promise<PreparedShadow>;
3
3
  //# sourceMappingURL=prepare.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../src/shadow/prepare.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAA8B,cAAc,EAAkB,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,IAAI,OAAO,CAAC,cAAc,CAAC,CAgC7D"}
1
+ {"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../src/shadow/prepare.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAA8B,cAAc,EAAkB,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG"}
@@ -11,8 +11,8 @@ const shadow_metadata = require('./metadata.js');
11
11
  const shadow_port = require('./port.js');
12
12
  const shadow_sync = require('./sync.js');
13
13
 
14
- async function prepareShadow() {
15
- const paths = shadow_paths.resolveConsumerPaths(process.cwd());
14
+ async function prepareShadow(consumerDir = process.cwd()) {
15
+ const paths = shadow_paths.resolveConsumerPaths(consumerDir);
16
16
  if (!(await fs.pathExists(paths.consumerPkg))) {
17
17
  throw new Error(`No package.json found in ${paths.consumerDir}`);
18
18
  }
@@ -3,6 +3,7 @@
3
3
  * e.g. `@trident/customer-viewer` → `customer-viewer`, `foo` → `foo`.
4
4
  */
5
5
  export declare function cleanAppName(name: string): string;
6
+ export declare function getCwdAppName(): string | undefined;
6
7
  /**
7
8
  * Canonical MF bundle name: the de-scoped app name with an `mf-` prefix.
8
9
  * This matches the trident convention (shadow pkg is `@trident/mf-<app>`,
@@ -1 +1 @@
1
- {"version":3,"file":"app-name.d.ts","sourceRoot":"","sources":["../../src/util/app-name.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
1
+ {"version":3,"file":"app-name.d.ts","sourceRoot":"","sources":["../../src/util/app-name.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAMlD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
+ const fs = require('node:fs');
4
+
3
5
  /**
4
6
  * Strip any npm scope (`@scope/`) from a package name.
5
7
  * e.g. `@trident/customer-viewer` → `customer-viewer`, `foo` → `foo`.
@@ -7,6 +9,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
7
9
  function cleanAppName(name) {
8
10
  return name.replace(/^@[^/]+\//, '');
9
11
  }
12
+ function getCwdAppName() {
13
+ try {
14
+ return JSON.parse(fs.readFileSync('package.json', 'utf8')).name;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
10
20
  /**
11
21
  * Canonical MF bundle name: the de-scoped app name with an `mf-` prefix.
12
22
  * This matches the trident convention (shadow pkg is `@trident/mf-<app>`,
@@ -17,4 +27,5 @@ function mfAppName(name) {
17
27
  }
18
28
 
19
29
  exports.cleanAppName = cleanAppName;
30
+ exports.getCwdAppName = getCwdAppName;
20
31
  exports.mfAppName = mfAppName;
@@ -1 +1 @@
1
- {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CASpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAOpD"}
1
+ {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAkBpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAUpD"}
@@ -23,6 +23,13 @@ class Z2hAuthMissingError extends Error {
23
23
  }
24
24
  }
25
25
  async function readZthAuth() {
26
+ if (process.env.Z2H_AUTH_TOKEN) {
27
+ const token = process.env.Z2H_AUTH_TOKEN;
28
+ if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
29
+ throw new Error('Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.');
30
+ }
31
+ return { token };
32
+ }
26
33
  if (!(await fs.pathExists(Z2H_AUTH_FILE))) {
27
34
  throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
28
35
  }
@@ -42,6 +49,9 @@ async function readZthAuth() {
42
49
  * blow up — and where an inline sync read keeps the spawn call site simple.
43
50
  */
44
51
  function tryReadAuthTokenSync() {
52
+ if (process.env.Z2H_AUTH_TOKEN) {
53
+ return process.env.Z2H_AUTH_TOKEN;
54
+ }
45
55
  try {
46
56
  const parsed = fs.readJsonSync(Z2H_AUTH_FILE);
47
57
  return parsed?.token ?? null;
@@ -1,9 +1,17 @@
1
1
  import type { AwsCredentialIdentityProvider } from '@aws-sdk/types';
2
2
  export declare const STS_BROKER_PATH = "/z2h-cli/sts/credentials";
3
+ export declare const DEPLOY_VERSION_PATH = "/z2h-cli/apps/version";
4
+ /**
5
+ * Reserve the version this deploy will write to. Split out of the STS
6
+ * credentials call so version derivation lives on its own endpoint — the CLI
7
+ * asks for the version first, then requests credentials scoped to it.
8
+ */
9
+ export declare function fetchDeployVersion(appName: string): Promise<number>;
3
10
  /**
4
11
  * AWS SDK credential provider that fetches short-lived deploy credentials
5
- * from the bigbrain-zth STS broker. Credentials are cached in memory until
6
- * 5 minutes before expiry; concurrent calls share a single in-flight fetch.
12
+ * from the bigbrain-zth STS broker for a specific version. Credentials are
13
+ * cached in memory until 5 minutes before expiry; concurrent calls share a
14
+ * single in-flight fetch.
7
15
  */
8
16
  export declare function brokerCredentialProvider({ appName }: {
9
17
  appName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"broker-credential-provider.d.ts","sourceRoot":"","sources":["../../../src/util/auth/broker-credential-provider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAyB,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAE3F,eAAO,MAAM,eAAe,6BAA6B,CAAC;AAwD1D;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,6BAA6B,CAuBxG"}
1
+ {"version":3,"file":"broker-credential-provider.d.ts","sourceRoot":"","sources":["../../../src/util/auth/broker-credential-provider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAyB,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAE3F,eAAO,MAAM,eAAe,6BAA6B,CAAC;AAC1D,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AA4C3D;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUzE;AAuBD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,6BAA6B,CAuBxG"}
@@ -4,31 +4,52 @@ const constants = require('../../constants.js');
4
4
  const util_auth_authStore = require('./auth-store.js');
5
5
 
6
6
  const STS_BROKER_PATH = '/z2h-cli/sts/credentials';
7
+ const DEPLOY_VERSION_PATH = '/z2h-cli/apps/version';
7
8
  // Refresh the cached credentials this many ms before they expire so the next
8
9
  // AWS API call doesn't race the broker round-trip against a hard expiry.
9
10
  const REFRESH_BUFFER_MS = 5 * 60 * 1000;
10
- async function fetchCredentialsOnce(appName) {
11
+ async function postToBroker(path, body) {
11
12
  const { token } = await util_auth_authStore.readZthAuth();
12
- const url = `${constants.brokerBaseUrl()}${STS_BROKER_PATH}`;
13
- let res;
14
13
  try {
15
- res = await fetch(url, {
14
+ return await fetch(`${constants.brokerBaseUrl()}${path}`, {
16
15
  method: 'POST',
17
16
  headers: {
18
17
  Authorization: `Bearer ${token}`,
19
18
  'Content-Type': 'application/json',
20
19
  Accept: 'application/json',
20
+ ...constants.routingHeaders(),
21
21
  },
22
- body: JSON.stringify({ appName }),
22
+ body: JSON.stringify(body),
23
23
  });
24
24
  }
25
25
  catch (err) {
26
26
  const message = err instanceof Error ? err.message : String(err);
27
27
  throw new Error(`Z2H deploy broker request failed: ${message}`);
28
28
  }
29
+ }
30
+ function assertBrokerOk(res) {
29
31
  if (res.status === 401 || res.status === 403) {
30
32
  throw new Error(`Z2H deploy token rejected by the broker (${res.status}). Refresh user token by running 'z2h-cli generate-z2h-token'.`);
31
33
  }
34
+ }
35
+ /**
36
+ * Reserve the version this deploy will write to. Split out of the STS
37
+ * credentials call so version derivation lives on its own endpoint — the CLI
38
+ * asks for the version first, then requests credentials scoped to it.
39
+ */
40
+ async function fetchDeployVersion(appName) {
41
+ const res = await postToBroker(DEPLOY_VERSION_PATH, { appName });
42
+ assertBrokerOk(res);
43
+ if (!res.ok) {
44
+ const text = await res.text();
45
+ throw new Error(`Z2H deploy broker returned ${res.status}: ${text.slice(0, 1024)}`);
46
+ }
47
+ const body = (await res.json());
48
+ return body.version ?? 0;
49
+ }
50
+ async function fetchStsOnce(appName) {
51
+ const res = await postToBroker(STS_BROKER_PATH, { appName });
52
+ assertBrokerOk(res);
32
53
  if (!res.ok) {
33
54
  const text = await res.text();
34
55
  throw new Error(`Z2H deploy broker returned ${res.status}: ${text.slice(0, 1024)}`);
@@ -46,8 +67,9 @@ async function fetchCredentialsOnce(appName) {
46
67
  }
47
68
  /**
48
69
  * AWS SDK credential provider that fetches short-lived deploy credentials
49
- * from the bigbrain-zth STS broker. Credentials are cached in memory until
50
- * 5 minutes before expiry; concurrent calls share a single in-flight fetch.
70
+ * from the bigbrain-zth STS broker for a specific version. Credentials are
71
+ * cached in memory until 5 minutes before expiry; concurrent calls share a
72
+ * single in-flight fetch.
51
73
  */
52
74
  function brokerCredentialProvider({ appName }) {
53
75
  let cached;
@@ -60,10 +82,10 @@ function brokerCredentialProvider({ appName }) {
60
82
  if (inflight) {
61
83
  return inflight;
62
84
  }
63
- inflight = fetchCredentialsOnce(appName)
64
- .then(creds => {
65
- cached = creds;
66
- return creds;
85
+ inflight = fetchStsOnce(appName)
86
+ .then(credentials => {
87
+ cached = credentials;
88
+ return credentials;
67
89
  })
68
90
  .finally(() => {
69
91
  inflight = undefined;
@@ -72,5 +94,7 @@ function brokerCredentialProvider({ appName }) {
72
94
  };
73
95
  }
74
96
 
97
+ exports.DEPLOY_VERSION_PATH = DEPLOY_VERSION_PATH;
75
98
  exports.STS_BROKER_PATH = STS_BROKER_PATH;
76
99
  exports.brokerCredentialProvider = brokerCredentialProvider;
100
+ exports.fetchDeployVersion = fetchDeployVersion;