@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,14 +4,15 @@ import { pathExists, ensureDir, writeFile, copy, readdir } from 'fs-extra';
4
4
  import { setOutputMode, info, warn, output } from '../util/logger.mjs';
5
5
  import { assertWorkspace } from '../util/workspace.mjs';
6
6
  import { APP_TEMPLATES, getTemplatesDir, renderTemplate } from '../util/templates.mjs';
7
- import { readManifest } from '../util/manifest.mjs';
8
- import { mfAppName } from '../util/app-name.mjs';
9
7
  import { APP_NAME_PATTERN } from '../constants.mjs';
10
8
  import { runYarnInstall } from '../util/npm/install.mjs';
11
- import { defaultS3Remote, initRepo } from '../util/git/repo.mjs';
12
- import { buildCommand } from './build.mjs';
9
+ import { initRepo } from '../util/git/repo.mjs';
10
+ import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
11
+ export { REMOTE_TYPES } from '../util/git/remote-options.mjs';
12
+ import { fetchApp } from '../util/broker/app.mjs';
13
+ import { mfAppName } from '../util/app-name.mjs';
14
+ import { prepareShadow } from '../shadow/prepare.mjs';
13
15
 
14
- const REMOTE_TYPES = ['s3', 'github'];
15
16
  // Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
16
17
  // /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
17
18
  // platform page in the URL, so we refuse upfront.
@@ -51,15 +52,7 @@ async function createCommand(appName, opts = {}) {
51
52
  `2-50 chars, starting with a letter.`);
52
53
  }
53
54
  // Validate --remote up front before any file I/O.
54
- if (!opts.remote) {
55
- throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
56
- }
57
- if (!REMOTE_TYPES.includes(opts.remote)) {
58
- throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
59
- }
60
- if (opts.remote === 'github' && !opts.repoUrl) {
61
- throw new Error('--repo-url is required when using --remote=github.');
62
- }
55
+ validateRemoteOptions(opts, true);
63
56
  // Resolve template.
64
57
  // `--html-file` implies `--template html-embed` unless the user already picked one.
65
58
  const template = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default'));
@@ -74,9 +67,9 @@ async function createCommand(appName, opts = {}) {
74
67
  if (!(await isDirEmpty(appDir))) {
75
68
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
76
69
  }
77
- // Check for manifest collision (network call — after all local checks).
78
- const manifest = await readManifest();
79
- if (manifest[mfAppName(appName)]) {
70
+ // Check the broker — fail fast if the app is already registered.
71
+ const existing = await fetchApp(mfAppName(appName));
72
+ if (existing) {
80
73
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
81
74
  }
82
75
  // Compute substitutions.
@@ -103,28 +96,16 @@ async function createCommand(appName, opts = {}) {
103
96
  await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
104
97
  }
105
98
  info(`initializing git repo for ${appName}...`);
106
- let gitRemote;
107
- if (opts.remote === 's3') {
108
- gitRemote = defaultS3Remote(appName);
109
- }
110
- else {
111
- gitRemote = opts.repoUrl;
112
- }
99
+ const gitRemote = resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
113
100
  await initRepo(appDir, gitRemote);
114
101
  info(`installing dependencies for ${appName}...`);
115
102
  await runYarnInstall({ cwd: workspace });
116
- // build/dev/deploy all locate the consumer via process.cwd() — chdir before calling them.
117
- const prevCwd = process.cwd();
103
+ // Best-effort workspace wiring (.zth shadow). Non-fatal — dev/deploy re-run it.
118
104
  try {
119
- info(`pre-building ${appName}`);
120
- process.chdir(appDir);
121
- await buildCommand();
122
- }
123
- catch {
124
- warn(`pre-build failed. No worries — it will run again when needed.`);
105
+ await prepareShadow(appDir);
125
106
  }
126
- finally {
127
- process.chdir(prevCwd);
107
+ catch (err) {
108
+ warn(`prepare shadow failed: ${err.message} — it will run again when needed.`);
128
109
  }
129
110
  output(`[z2h-cli] app ready at ${appDir}`, {
130
111
  ok: true,
@@ -136,4 +117,4 @@ async function createCommand(appName, opts = {}) {
136
117
  });
137
118
  }
138
119
 
139
- export { REMOTE_TYPES, createCommand };
120
+ export { createCommand };
@@ -1,6 +1,9 @@
1
+ import { type RemoteType } from '../util/git/remote-options';
1
2
  export interface DeployOptions {
2
3
  dryRun?: boolean;
3
4
  json?: boolean;
5
+ remote?: RemoteType;
6
+ repoUrl?: string;
4
7
  }
5
8
  export declare function deployCommand(opts?: DeployOptions): Promise<void>;
6
9
  //# sourceMappingURL=deploy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0F3E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAItG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmJ3E"}
@@ -1,16 +1,20 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
+ import { createInterface } from 'node:readline';
3
4
  import { readJson, pathExists } from 'fs-extra';
4
5
  import { setOutputMode, info, output, warn } from '../util/logger.mjs';
5
6
  import { resolveConsumerPaths } from '../shadow/paths.mjs';
6
- import { readManifest, nextVersion, writeManifest, upsertEntry } from '../util/manifest.mjs';
7
7
  import { uploadDir } from '../util/s3/upload.mjs';
8
- import { BUCKET_WRITE_NAME, brokerBaseUrl, BUCKET_READ_DNS_URL } from '../constants.mjs';
9
- import { setupS3Client } from '../util/s3/client.mjs';
8
+ import { BUCKET_WRITE_NAME, brokerBaseUrl, routingHeaders, BUCKET_READ_DNS_URL } from '../constants.mjs';
9
+ import { fetchDeployVersion, brokerCredentialProvider } from '../util/auth/broker-credential-provider.mjs';
10
10
  import { readZthAuth } from '../util/auth/auth-store.mjs';
11
11
  import { mfAppName, cleanAppName } from '../util/app-name.mjs';
12
+ import { getMachineAppName } from '../util/machine-identity.mjs';
13
+ import { getRemoteUrl, initRepo, guardCleanTree, pullAndResolve, pushToRemote } from '../util/git/repo.mjs';
14
+ import { resolveGitEnv } from '../util/git/git-env.mjs';
15
+ import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
16
+ import { setupS3Client } from '../util/s3/client.mjs';
12
17
  import { buildCommand } from './build.mjs';
13
- import { getRemoteUrl } from '../util/git/repo.mjs';
14
18
 
15
19
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
16
20
  async function deployCommand(opts = {}) {
@@ -26,11 +30,42 @@ async function deployCommand(opts = {}) {
26
30
  // Validate the user's deploy token is on disk before doing any expensive
27
31
  // work. Throws Z2hAuthMissingError with a remediation hint if not.
28
32
  const { token } = await readZthAuth();
29
- setupS3Client({ appName });
33
+ let gitRemote = await getRemoteUrl(process.cwd());
34
+ if (gitRemote) {
35
+ if (opts.remote || opts.repoUrl) {
36
+ throw new Error(`--remote and --repo-url only apply on the first deploy of an app with no git remote.\n` +
37
+ `This app already tracks source at: ${gitRemote}`);
38
+ }
39
+ }
40
+ else {
41
+ if (!opts.remote) {
42
+ throw new Error(`This app has no git remote. On first deploy, tell Z2H how to track source:\n\n` +
43
+ ` z2h-cli deploy --remote s3 (default)\n` +
44
+ ` Stores source history in S3 alongside your app. No extra setup.\n\n` +
45
+ ` z2h-cli deploy --remote github --repo-url https://github.com/org/repo.git\n` +
46
+ ` Uses an existing GitHub repo (must exist before deploying).`);
47
+ }
48
+ validateRemoteOptions(opts);
49
+ // Pass the raw consumer name (not the mf-prefixed appName) so defaultS3Remote
50
+ // produces the correct s3://bucket/mf-<name>/git path without double prefix.
51
+ const remote = resolveRemoteUrl(opts.remote, consumerPkg.name, opts.repoUrl);
52
+ info(`initializing git repo with ${opts.remote} remote...`);
53
+ await initRepo(process.cwd(), remote);
54
+ gitRemote = remote;
55
+ }
56
+ const version = await fetchDeployVersion(appName);
57
+ const nextVersion = version + 1;
58
+ const gitEnv = await resolveGitEnv(appName, gitRemote);
59
+ // Sync git first so a divergence (GitConflictError) stops us.
60
+ // The skill commits the working tree (with an LLM-authored message) before
61
+ // invoking deploy, so the tree should be clean here.
62
+ if (gitRemote) {
63
+ await guardCleanTree(process.cwd());
64
+ info('syncing with git remote');
65
+ await pullAndResolve(process.cwd(), gitEnv);
66
+ }
30
67
  info('reading current Z2H manifest');
31
- const manifest = await readManifest();
32
- const version = nextVersion(manifest, appName);
33
- const keyPrefix = `${appName}/${version}`;
68
+ const keyPrefix = `${appName}/${nextVersion}`;
34
69
  const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
35
70
  info('building');
36
71
  await buildCommand({ json: opts.json, publicUrl: url });
@@ -43,11 +78,10 @@ async function deployCommand(opts = {}) {
43
78
  throw new Error(`Missing asset-manifest.json at ${assetManifestPath}.`);
44
79
  }
45
80
  const assetManifest = (await readJson(assetManifestPath));
46
- const deployedBy = os.userInfo().username;
47
- const gitRemote = await getRemoteUrl(process.cwd());
81
+ const deployedBy = getMachineAppName() ?? os.userInfo().username;
48
82
  const entry = {
49
83
  ...assetManifest,
50
- version,
84
+ version: nextVersion,
51
85
  url,
52
86
  deployedBy,
53
87
  gitRemote,
@@ -65,14 +99,34 @@ async function deployCommand(opts = {}) {
65
99
  });
66
100
  return;
67
101
  }
102
+ // lock → git push → bundle upload → manifest PUT (the manifest write is the single commit point).
68
103
  try {
69
- // Lock is acquired here — uploadDir triggers the STS credential provider on first S3 call
104
+ // Acquire the deploy lock explicitly. Done after the build
105
+ // so the lock hold stays tiny — it wraps only push → upload → manifest.
106
+ // A 409 here means someone else is mid-deploy;
107
+ const creds = await brokerCredentialProvider({ appName })();
108
+ setupS3Client(creds);
109
+ await acquireDeployLock(appName, token);
110
+ // Push before any S3 write so a rejected push (someone deployed during our
111
+ // build) stops the deploy before the bundle/manifest go live
112
+ if (gitRemote) {
113
+ info('pushing to git remote');
114
+ await pushToRemote(process.cwd(), gitEnv);
115
+ }
70
116
  info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
71
117
  await uploadDir(paths.shadowBuild, keyPrefix);
72
- info(`updating manifest entry "${appName}" → v${version}`);
73
- await writeManifest(upsertEntry(manifest, appName, entry));
74
- info(`registering app "${appName}" → v${version}`);
75
- await registerApp(appName, token, version, entry);
118
+ // v1 creates the app row (+ owner grant); later versions release onto it.
119
+ // The two are separate endpoints, so the CLI picks based on the version it
120
+ // was handed.
121
+ if (nextVersion === 1) {
122
+ const visibility = await resolveFirstDeployVisibility(appName, opts.json);
123
+ info(`registering app "${appName}" → v${nextVersion} (${visibility})`);
124
+ await registerApp(appName, token, nextVersion, entry, visibility, gitRemote);
125
+ }
126
+ else {
127
+ info(`releasing "${appName}" → v${nextVersion}`);
128
+ await updateApp(appName, token, nextVersion, entry);
129
+ }
76
130
  }
77
131
  finally {
78
132
  await releaseLockBestEffort(appName, token);
@@ -81,10 +135,10 @@ async function deployCommand(opts = {}) {
81
135
  // before looking it up in the manifest. So the user-facing URL must use
82
136
  // the consumer's de-scoped package name (no `mf-` prefix, no `@scope/`).
83
137
  const liveUrl = `https://bigbrain.me/bigbrain-vibe/${cleanAppName(consumerPkg.name)}`;
84
- output(`[z2h-cli] deployed ✅\n app: ${appName}\n version: ${version}\n open: ${liveUrl}`, {
138
+ output(`[z2h-cli] deployed ✅\n app: ${appName}\n version: ${nextVersion}\n open: ${liveUrl}`, {
85
139
  ok: true,
86
140
  appName,
87
- version,
141
+ version: nextVersion,
88
142
  bucket: BUCKET_WRITE_NAME,
89
143
  keyPrefix,
90
144
  url,
@@ -92,25 +146,71 @@ async function deployCommand(opts = {}) {
92
146
  entry,
93
147
  });
94
148
  }
95
- async function registerApp(appName, token, version, entry) {
149
+ async function registerApp(appName, token, version, entry, visibility, gitRemote) {
96
150
  const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
97
151
  method: 'POST',
98
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
99
- body: JSON.stringify({ appName, version, entry }),
152
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
153
+ body: JSON.stringify({ appName, version, entry, visibility, gitRemote }),
100
154
  });
101
155
  if (!res.ok) {
102
156
  const text = await res.text();
103
157
  throw new Error(`App registration failed (${res.status}): ${text.slice(0, 1024)}`);
104
158
  }
105
159
  }
106
- // best-effort — the 5-min TTL backstop handles failures
160
+ // Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
161
+ // so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
162
+ async function acquireDeployLock(appName, token) {
163
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/lock/acquire`, {
164
+ method: 'POST',
165
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
166
+ body: JSON.stringify({ appName }),
167
+ });
168
+ if (res.status === 409) {
169
+ throw new Error(`Someone is deploying "${appName}" right now — try again in a moment.`);
170
+ }
171
+ if (!res.ok) {
172
+ const text = await res.text();
173
+ throw new Error(`Failed to acquire deploy lock (${res.status}): ${text.slice(0, 512)}`);
174
+ }
175
+ info(`deploy lock acquired for ${appName}`);
176
+ }
177
+ async function updateApp(appName, token, version, entry) {
178
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/update`, {
179
+ method: 'POST',
180
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
181
+ body: JSON.stringify({ appName, version, entry }),
182
+ });
183
+ if (!res.ok) {
184
+ const text = await res.text();
185
+ throw new Error(`App update failed (${res.status}): ${text.slice(0, 1024)}`);
186
+ }
187
+ }
188
+ // First deploy only: decide whether the app is born public or private. The
189
+ // choice is passed to the register endpoint (default private). Non-interactive
190
+ // runs (--json or no TTY) always default to private.
191
+ async function resolveFirstDeployVisibility(appName, json) {
192
+ if (json || !process.stdout.isTTY) {
193
+ return 'private';
194
+ }
195
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
196
+ const answer = await new Promise(resolve => {
197
+ rl.question(`Make "${appName}" public to all monday.com employees? (y/N) `, resolve);
198
+ });
199
+ rl.close();
200
+ return answer.trim().toLowerCase() === 'y' ? 'public' : 'private';
201
+ }
202
+ // best-effort — the 2-min TTL backstop handles failures
107
203
  async function releaseLockBestEffort(appName, token) {
108
204
  try {
109
- await fetch(`${brokerBaseUrl()}/z2h-cli/lock/release`, {
205
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/lock/release`, {
110
206
  method: 'POST',
111
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
207
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
112
208
  body: JSON.stringify({ appName }),
113
209
  });
210
+ if (!res.ok) {
211
+ warn(`failed to release lock for ${appName} (${res.status})`);
212
+ return;
213
+ }
114
214
  info(`lock released for ${appName}`);
115
215
  }
116
216
  catch {
@@ -0,0 +1,5 @@
1
+ export interface EditOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function editCommand(appName: string, opts?: EditOptions): Promise<void>;
5
+ //# sourceMappingURL=edit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../../src/commands/edit.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CxF"}
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { pathExists } from 'fs-extra';
3
+ import { setOutputMode, output, info, warn } from '../util/logger.mjs';
4
+ import { assertWorkspace } from '../util/workspace.mjs';
5
+ import { mfAppName } from '../util/app-name.mjs';
6
+ import { fetchApp } from '../util/broker/app.mjs';
7
+ import { resolveGitEnv } from '../util/git/git-env.mjs';
8
+ import { cloneRepo, pullAndResolve } from '../util/git/repo.mjs';
9
+ import { runYarnInstall } from '../util/npm/install.mjs';
10
+ import { prepareShadow } from '../shadow/prepare.mjs';
11
+
12
+ async function editCommand(appName, opts = {}) {
13
+ if (opts.json) {
14
+ setOutputMode('json');
15
+ }
16
+ const workspace = await assertWorkspace();
17
+ const mfName = mfAppName(appName);
18
+ const app = await fetchApp(mfName);
19
+ if (!app?.gitRemote) {
20
+ output(`[z2h-cli] ${appName} was deployed without git tracking — run "z2h-cli deploy" to set up a git remote`, {
21
+ ok: true,
22
+ appName,
23
+ });
24
+ return;
25
+ }
26
+ const { gitRemote } = app;
27
+ const gitEnv = await resolveGitEnv(mfName, gitRemote);
28
+ const appDir = path.join(workspace, appName);
29
+ const hasLocal = await pathExists(appDir);
30
+ if (!hasLocal) {
31
+ info(`cloning ${appName}...`);
32
+ await cloneRepo(gitRemote, appDir, gitEnv);
33
+ info(`installing dependencies for ${appName}...`);
34
+ await runYarnInstall({ cwd: workspace });
35
+ // Best-effort workspace wiring (.zth shadow). Non-fatal — dev/deploy re-run it.
36
+ try {
37
+ await prepareShadow(appDir);
38
+ }
39
+ catch (err) {
40
+ warn(`prepare shadow failed: ${err.message} — it will run again when needed.`);
41
+ }
42
+ }
43
+ else {
44
+ info(`pulling latest ${appName}...`);
45
+ await pullAndResolve(appDir, gitEnv);
46
+ }
47
+ output(`[z2h-cli] ${appName} is ready at ${appDir}`, {
48
+ ok: true,
49
+ appDir,
50
+ appName,
51
+ gitRemote,
52
+ });
53
+ }
54
+
55
+ export { editCommand };
@@ -0,0 +1,12 @@
1
+ export interface GrantOptions {
2
+ public?: boolean;
3
+ level?: 'owner' | 'editor' | 'viewer';
4
+ json?: boolean;
5
+ }
6
+ export declare function grantCommand(email: string | undefined, opts: GrantOptions): Promise<void>;
7
+ export declare function revokeCommand(email: string | undefined, opts: GrantOptions): Promise<void>;
8
+ export interface TransferOwnerOptions {
9
+ json?: boolean;
10
+ }
11
+ export declare function transferOwnerCommand(email: string | undefined, opts: TransferOwnerOptions): Promise<void>;
12
+ //# sourceMappingURL=grant.d.ts.map
@@ -0,0 +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;IACtC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAkBD,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B/F;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BhG;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB/G"}
@@ -0,0 +1,90 @@
1
+ import { setOutputMode, output } from '../util/logger.mjs';
2
+ import { mfAppName, getCwdAppName } from '../util/app-name.mjs';
3
+ import { brokerBaseUrl, routingHeaders } from '../constants.mjs';
4
+ import { readZthAuth } from '../util/auth/auth-store.mjs';
5
+
6
+ async function postGrant(appName, token, body) {
7
+ const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/grants`, {
8
+ method: 'POST',
9
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
10
+ body: JSON.stringify(body),
11
+ });
12
+ if (!res.ok) {
13
+ const text = await res.text();
14
+ throw new Error(`Grant failed (${res.status}): ${text.slice(0, 1024)}`);
15
+ }
16
+ }
17
+ async function grantCommand(email, opts) {
18
+ if (opts.json) {
19
+ setOutputMode('json');
20
+ }
21
+ if (!opts.public && !email) {
22
+ throw new Error('Specify what to grant — e.g. --public or provide an email');
23
+ }
24
+ const level = opts.level ?? 'viewer';
25
+ if (!['owner', 'editor', 'viewer'].includes(level)) {
26
+ throw new Error(`Invalid --level "${level}". Must be owner, editor, or viewer`);
27
+ }
28
+ const appName = mfAppName(getCwdAppName());
29
+ const { token } = await readZthAuth();
30
+ if (email) {
31
+ await postGrant(appName, token, { type: 'user', value: email, accessLevel: level });
32
+ }
33
+ if (opts.public) {
34
+ await postGrant(appName, token, { type: 'global', accessLevel: 'viewer' });
35
+ }
36
+ const parts = [];
37
+ if (email) {
38
+ parts.push(`${email} (${level})`);
39
+ }
40
+ if (opts.public) {
41
+ parts.push('public');
42
+ }
43
+ output(`[z2h-cli] "${appName}" granted to: ${parts.join(', ')}`, { ok: true, appName, granted: parts });
44
+ }
45
+ async function revokeCommand(email, opts) {
46
+ if (opts.json) {
47
+ setOutputMode('json');
48
+ }
49
+ if (!opts.public && !email) {
50
+ throw new Error('Specify what to revoke — e.g. --public or provide an email');
51
+ }
52
+ const appName = mfAppName(getCwdAppName());
53
+ const { token } = await readZthAuth();
54
+ const target = email ? `user/${encodeURIComponent(email)}` : 'global';
55
+ const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/grants/${target}`, {
56
+ method: 'DELETE',
57
+ headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
58
+ });
59
+ if (!res.ok) {
60
+ const text = await res.text();
61
+ throw new Error(`Revoke failed (${res.status}): ${text.slice(0, 1024)}`);
62
+ }
63
+ const revoked = email ?? 'public';
64
+ const msg = email
65
+ ? `[z2h-cli] "${appName}" access revoked from ${email}`
66
+ : `[z2h-cli] "${appName}" is now private (owners/admins only)`;
67
+ output(msg, { ok: true, appName, revoked });
68
+ }
69
+ async function transferOwnerCommand(email, opts) {
70
+ if (opts.json) {
71
+ setOutputMode('json');
72
+ }
73
+ if (!email) {
74
+ throw new Error('Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
75
+ }
76
+ const appName = mfAppName(getCwdAppName());
77
+ const { token } = await readZthAuth();
78
+ const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/transfer-owner`, {
79
+ method: 'POST',
80
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
81
+ body: JSON.stringify({ email }),
82
+ });
83
+ if (!res.ok) {
84
+ const text = await res.text();
85
+ throw new Error(`Transfer owner failed (${res.status}): ${text.slice(0, 1024)}`);
86
+ }
87
+ output(`[z2h-cli] "${appName}" ownership transferred to ${email}`, { ok: true, appName, newOwner: email });
88
+ }
89
+
90
+ export { grantCommand, revokeCommand, transferOwnerCommand };
@@ -23,6 +23,7 @@ export declare const DEFAULT_BROKER_BASE_URL = "https://bigbrain-zth.bigbrain.me
23
23
  * STS credential provider and the npm-broker registry URL.
24
24
  */
25
25
  export declare function brokerBaseUrl(): string;
26
+ export declare function routingHeaders(): Record<string, string>;
26
27
  export declare const NPM_BROKER_PATH = "/z2h-cli/npm";
27
28
  export declare const BROKER_NPM_SCOPE = "@mondaydotcomorg";
28
29
  export declare const PUBLIC_NPM_REGISTRY_URL = "https://registry.npmjs.org/";
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,SAAS,CAAC;AAGtC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGjD,CAAC;AAQF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAElD,CAAC;AAMF,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAC9B,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAG9B,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAQ3C,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAM1E,eAAO,MAAM,gBAAgB,QAA2B,CAAC;AAKzD,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAKD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAG9C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAGnD,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAMrE,eAAO,MAAM,iBAAiB,EAAG,eAAwB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,SAAS,CAAC;AAGtC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGjD,CAAC;AAQF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAElD,CAAC;AAMF,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAC9B,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAG9B,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAQ3C,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAM1E,eAAO,MAAM,gBAAgB,QAA2B,CAAC;AAKzD,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAID,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGvD;AAKD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAG9C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAGnD,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAMrE,eAAO,MAAM,iBAAiB,EAAG,eAAwB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
@@ -58,6 +58,12 @@ const DEFAULT_BROKER_BASE_URL = 'https://bigbrain-zth.bigbrain.me';
58
58
  function brokerBaseUrl() {
59
59
  return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
60
60
  }
61
+ // Returns Baggage header for monday-mirror routing when Z2H_ROUTING_KEY is set.
62
+ // No-op in production (returns {}) — only activates when the env var is set.
63
+ function routingHeaders() {
64
+ const key = process.env.Z2H_ROUTING_KEY;
65
+ return key ? { Baggage: `routingKey=${key}` } : {};
66
+ }
61
67
  // The authenticated npm reverse-proxy is mounted here on the broker. Only the
62
68
  // `@mondaydotcomorg` scope is served; everything else resolves from npmjs
63
69
  // directly. See docs/superpowers/specs/2026-06-03-npm-brokering-design.md §3.
@@ -81,4 +87,4 @@ function brokerNpmRegistryUrl() {
81
87
  return `${brokerBaseUrl()}${NPM_BROKER_PATH}`;
82
88
  }
83
89
 
84
- export { APP_NAME_PATTERN, BROKER_NPM_SCOPE, BUCKET_READ_DNS_NAME, BUCKET_READ_DNS_URL, BUCKET_WRITE_NAME, BUCKET_WRITE_URL, DEFAULT_BROKER_BASE_URL, GITIGNORE_ENTRIES, MANIFEST_KEY, MANIFEST_READ_URL, MAX_PORT, MIN_PORT, NPM_BROKER_PATH, PUBLIC_NPM_REGISTRY_URL, REGION, SHADOW_DEV_DEPS, SHADOW_DIR_NAME, SHADOW_SCRIPTS, Z2H_NPM_TOKEN_ENV, brokerBaseUrl, brokerNpmRegistryUrl };
90
+ export { APP_NAME_PATTERN, BROKER_NPM_SCOPE, BUCKET_READ_DNS_NAME, BUCKET_READ_DNS_URL, BUCKET_WRITE_NAME, BUCKET_WRITE_URL, DEFAULT_BROKER_BASE_URL, GITIGNORE_ENTRIES, MANIFEST_KEY, MANIFEST_READ_URL, MAX_PORT, MIN_PORT, NPM_BROKER_PATH, PUBLIC_NPM_REGISTRY_URL, REGION, SHADOW_DEV_DEPS, SHADOW_DIR_NAME, SHADOW_SCRIPTS, Z2H_NPM_TOKEN_ENV, brokerBaseUrl, brokerNpmRegistryUrl, routingHeaders };