@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.
- package/dist/commands/__tests__/edit.test.d.ts +2 -0
- package/dist/commands/__tests__/edit.test.d.ts.map +1 -0
- package/dist/commands/create.d.ts +2 -2
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +14 -34
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +123 -23
- package/dist/commands/edit.d.ts +5 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +61 -0
- package/dist/commands/grant.d.ts +12 -0
- package/dist/commands/grant.d.ts.map +1 -0
- package/dist/commands/grant.js +94 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/esm/commands/__tests__/edit.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/edit.test.d.ts.map +1 -0
- package/dist/esm/commands/create.d.ts +2 -2
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +16 -35
- package/dist/esm/commands/deploy.d.ts +3 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +124 -24
- package/dist/esm/commands/edit.d.ts +5 -0
- package/dist/esm/commands/edit.d.ts.map +1 -0
- package/dist/esm/commands/edit.mjs +55 -0
- package/dist/esm/commands/grant.d.ts +12 -0
- package/dist/esm/commands/grant.d.ts.map +1 -0
- package/dist/esm/commands/grant.mjs +90 -0
- package/dist/esm/constants.d.ts +1 -0
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +7 -1
- package/dist/esm/index.mjs +67 -10
- package/dist/esm/shadow/prepare.d.ts +1 -1
- package/dist/esm/shadow/prepare.d.ts.map +1 -1
- package/dist/esm/shadow/prepare.mjs +2 -2
- package/dist/esm/util/app-name.d.ts +1 -0
- package/dist/esm/util/app-name.d.ts.map +1 -1
- package/dist/esm/util/app-name.mjs +11 -1
- package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
- package/dist/esm/util/auth/auth-store.mjs +10 -0
- package/dist/esm/util/auth/broker-credential-provider.d.ts +10 -2
- package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/esm/util/auth/broker-credential-provider.mjs +35 -13
- package/dist/esm/util/broker/app.d.ts +14 -0
- package/dist/esm/util/broker/app.d.ts.map +1 -0
- package/dist/esm/util/broker/app.mjs +27 -0
- package/dist/esm/util/git/git-env.d.ts +9 -0
- package/dist/esm/util/git/git-env.d.ts.map +1 -0
- package/dist/esm/util/git/git-env.mjs +22 -0
- package/dist/esm/util/git/remote-options.d.ts +17 -0
- package/dist/esm/util/git/remote-options.d.ts.map +1 -0
- package/dist/esm/util/git/remote-options.mjs +30 -0
- package/dist/esm/util/git/repo.d.ts +21 -3
- package/dist/esm/util/git/repo.d.ts.map +1 -1
- package/dist/esm/util/git/repo.mjs +103 -47
- package/dist/esm/util/machine-identity.d.ts +9 -0
- package/dist/esm/util/machine-identity.d.ts.map +1 -0
- package/dist/esm/util/machine-identity.mjs +12 -0
- package/dist/esm/util/s3/client.d.ts +2 -7
- package/dist/esm/util/s3/client.d.ts.map +1 -1
- package/dist/esm/util/s3/client.mjs +3 -11
- package/dist/index.js +69 -12
- package/dist/shadow/prepare.d.ts +1 -1
- package/dist/shadow/prepare.d.ts.map +1 -1
- package/dist/shadow/prepare.js +2 -2
- package/dist/util/app-name.d.ts +1 -0
- package/dist/util/app-name.d.ts.map +1 -1
- package/dist/util/app-name.js +11 -0
- package/dist/util/auth/auth-store.d.ts.map +1 -1
- package/dist/util/auth/auth-store.js +10 -0
- package/dist/util/auth/broker-credential-provider.d.ts +10 -2
- package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/util/auth/broker-credential-provider.js +35 -11
- package/dist/util/broker/app.d.ts +14 -0
- package/dist/util/broker/app.d.ts.map +1 -0
- package/dist/util/broker/app.js +29 -0
- package/dist/util/git/git-env.d.ts +9 -0
- package/dist/util/git/git-env.d.ts.map +1 -0
- package/dist/util/git/git-env.js +24 -0
- package/dist/util/git/remote-options.d.ts +17 -0
- package/dist/util/git/remote-options.d.ts.map +1 -0
- package/dist/util/git/remote-options.js +34 -0
- package/dist/util/git/repo.d.ts +21 -3
- package/dist/util/git/repo.d.ts.map +1 -1
- package/dist/util/git/repo.js +104 -47
- package/dist/util/machine-identity.d.ts +9 -0
- package/dist/util/machine-identity.d.ts.map +1 -0
- package/dist/util/machine-identity.js +14 -0
- package/dist/util/s3/client.d.ts +2 -7
- package/dist/util/s3/client.d.ts.map +1 -1
- package/dist/util/s3/client.js +3 -11
- package/package.json +1 -1
- package/src/commands/__tests__/create.test.ts +9 -7
- package/src/commands/__tests__/edit.test.ts +184 -0
- package/src/commands/create.ts +15 -33
- package/src/commands/deploy.ts +147 -33
- package/src/commands/edit.ts +63 -0
- package/src/commands/grant.ts +115 -0
- package/src/constants.ts +7 -0
- package/src/index.ts +83 -13
- package/src/shadow/prepare.ts +2 -2
- package/src/templates/app/CLAUDE.md +15 -0
- package/src/util/app-name.ts +10 -0
- package/src/util/auth/__tests__/broker-credential-provider.test.ts +34 -2
- package/src/util/auth/auth-store.ts +12 -0
- package/src/util/auth/broker-credential-provider.ts +43 -15
- package/src/util/broker/app.ts +34 -0
- package/src/util/git/__tests__/repo.test.ts +108 -90
- package/src/util/git/git-env.ts +23 -0
- package/src/util/git/remote-options.ts +37 -0
- package/src/util/git/repo.ts +121 -47
- package/src/util/machine-identity.ts +10 -0
- package/src/util/s3/client.ts +4 -11
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { output, setOutputMode } from '../util/logger';
|
|
2
|
+
import { getCwdAppName, mfAppName } from '../util/app-name';
|
|
3
|
+
import { brokerBaseUrl, routingHeaders } from '../constants';
|
|
4
|
+
import { readZthAuth } from '../util/auth/auth-store';
|
|
5
|
+
|
|
6
|
+
export interface GrantOptions {
|
|
7
|
+
public?: boolean;
|
|
8
|
+
level?: 'owner' | 'editor' | 'viewer';
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function postGrant(
|
|
13
|
+
appName: string,
|
|
14
|
+
token: string,
|
|
15
|
+
body: { type: string; value?: string; accessLevel: string }
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/grants`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
throw new Error(`Grant failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function grantCommand(email: string | undefined, opts: GrantOptions): Promise<void> {
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
setOutputMode('json');
|
|
31
|
+
}
|
|
32
|
+
if (!opts.public && !email) {
|
|
33
|
+
throw new Error('Specify what to grant — e.g. --public or provide an email');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const level = opts.level ?? 'viewer';
|
|
37
|
+
if (!['owner', 'editor', 'viewer'].includes(level)) {
|
|
38
|
+
throw new Error(`Invalid --level "${level}". Must be owner, editor, or viewer`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const appName = mfAppName(getCwdAppName()!);
|
|
42
|
+
const { token } = await readZthAuth();
|
|
43
|
+
|
|
44
|
+
if (email) {
|
|
45
|
+
await postGrant(appName, token, { type: 'user', value: email, accessLevel: level });
|
|
46
|
+
}
|
|
47
|
+
if (opts.public) {
|
|
48
|
+
await postGrant(appName, token, { type: 'global', accessLevel: 'viewer' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
if (email) {
|
|
53
|
+
parts.push(`${email} (${level})`);
|
|
54
|
+
}
|
|
55
|
+
if (opts.public) {
|
|
56
|
+
parts.push('public');
|
|
57
|
+
}
|
|
58
|
+
output(`[z2h-cli] "${appName}" granted to: ${parts.join(', ')}`, { ok: true, appName, granted: parts });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function revokeCommand(email: string | undefined, opts: GrantOptions): Promise<void> {
|
|
62
|
+
if (opts.json) {
|
|
63
|
+
setOutputMode('json');
|
|
64
|
+
}
|
|
65
|
+
if (!opts.public && !email) {
|
|
66
|
+
throw new Error('Specify what to revoke — e.g. --public or provide an email');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const appName = mfAppName(getCwdAppName()!);
|
|
70
|
+
const { token } = await readZthAuth();
|
|
71
|
+
|
|
72
|
+
const target = email ? `user/${encodeURIComponent(email)}` : 'global';
|
|
73
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/grants/${target}`, {
|
|
74
|
+
method: 'DELETE',
|
|
75
|
+
headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const text = await res.text();
|
|
79
|
+
throw new Error(`Revoke failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const revoked = email ?? 'public';
|
|
83
|
+
const msg = email
|
|
84
|
+
? `[z2h-cli] "${appName}" access revoked from ${email}`
|
|
85
|
+
: `[z2h-cli] "${appName}" is now private (owners/admins only)`;
|
|
86
|
+
output(msg, { ok: true, appName, revoked });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TransferOwnerOptions {
|
|
90
|
+
json?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function transferOwnerCommand(email: string | undefined, opts: TransferOwnerOptions): Promise<void> {
|
|
94
|
+
if (opts.json) {
|
|
95
|
+
setOutputMode('json');
|
|
96
|
+
}
|
|
97
|
+
if (!email) {
|
|
98
|
+
throw new Error('Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const appName = mfAppName(getCwdAppName()!);
|
|
102
|
+
const { token } = await readZthAuth();
|
|
103
|
+
|
|
104
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${appName}/transfer-owner`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
107
|
+
body: JSON.stringify({ email }),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
const text = await res.text();
|
|
111
|
+
throw new Error(`Transfer owner failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
output(`[z2h-cli] "${appName}" ownership transferred to ${email}`, { ok: true, appName, newOwner: email });
|
|
115
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -71,6 +71,13 @@ export function brokerBaseUrl(): string {
|
|
|
71
71
|
return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Returns Baggage header for monday-mirror routing when Z2H_ROUTING_KEY is set.
|
|
75
|
+
// No-op in production (returns {}) — only activates when the env var is set.
|
|
76
|
+
export function routingHeaders(): Record<string, string> {
|
|
77
|
+
const key = process.env.Z2H_ROUTING_KEY;
|
|
78
|
+
return key ? { Baggage: `routingKey=${key}` } : {};
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
// The authenticated npm reverse-proxy is mounted here on the broker. Only the
|
|
75
82
|
// `@mondaydotcomorg` scope is served; everything else resolves from npmjs
|
|
76
83
|
// directly. See docs/superpowers/specs/2026-06-03-npm-brokering-design.md §3.
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
2
|
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { Command } from 'commander';
|
|
6
5
|
import { execa } from 'execa';
|
|
6
|
+
import { getWorkspaceHome } from './util/workspace';
|
|
7
7
|
import { devCommand } from './commands/dev';
|
|
8
8
|
import { buildCommand } from './commands/build';
|
|
9
9
|
import { cleanCommand } from './commands/clean';
|
|
10
10
|
import { deployCommand } from './commands/deploy';
|
|
11
|
+
import { grantCommand, revokeCommand, transferOwnerCommand } from './commands/grant';
|
|
11
12
|
import { createWorkspaceCommand } from './commands/create-workspace';
|
|
12
13
|
import { createCommand, CreateOptions } from './commands/create';
|
|
13
14
|
import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
|
|
14
15
|
import { migrateCommand } from './commands/migrate';
|
|
16
|
+
import { editCommand } from './commands/edit';
|
|
15
17
|
import { errorLog, fail, getOutputMode } from './util/logger';
|
|
16
18
|
import { trackDuration } from './util/tracker';
|
|
19
|
+
import { getCwdAppName } from './util/app-name';
|
|
17
20
|
|
|
18
21
|
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
19
22
|
|
|
@@ -30,15 +33,17 @@ function selfUpdate(): void {
|
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const CONFLICTING_ENV_VARS = ['AWS_PROFILE'];
|
|
37
|
+
|
|
38
|
+
function stripConflictingEnvVars(): void {
|
|
39
|
+
for (const name of CONFLICTING_ENV_VARS) {
|
|
40
|
+
delete process.env[name];
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
async function main(): Promise<void> {
|
|
45
|
+
stripConflictingEnvVars();
|
|
46
|
+
|
|
42
47
|
const program = new Command();
|
|
43
48
|
program
|
|
44
49
|
.name('z2h-cli')
|
|
@@ -96,14 +101,26 @@ async function main(): Promise<void> {
|
|
|
96
101
|
|
|
97
102
|
program
|
|
98
103
|
.command('deploy')
|
|
99
|
-
.description('Build, upload
|
|
104
|
+
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
100
105
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
101
106
|
.option('--json', 'emit structured JSON on stdout')
|
|
102
|
-
.
|
|
107
|
+
.option(
|
|
108
|
+
'--remote <type>',
|
|
109
|
+
'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.'
|
|
110
|
+
)
|
|
111
|
+
.option(
|
|
112
|
+
'--repo-url <url>',
|
|
113
|
+
'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.'
|
|
114
|
+
)
|
|
115
|
+
.action(async (opts: { dryRun?: boolean; json?: boolean; remote?: string; repoUrl?: string }) => {
|
|
103
116
|
try {
|
|
104
|
-
await trackDuration(
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
await trackDuration(
|
|
118
|
+
'z2h_app_deploy_success',
|
|
119
|
+
() => deployCommand(opts as Parameters<typeof deployCommand>[0]),
|
|
120
|
+
{
|
|
121
|
+
app_name: getCwdAppName(),
|
|
122
|
+
}
|
|
123
|
+
);
|
|
107
124
|
} catch (err) {
|
|
108
125
|
await fail(`deploy failed: ${(err as Error).message}`, { command: 'deploy' }, 2);
|
|
109
126
|
}
|
|
@@ -111,7 +128,7 @@ async function main(): Promise<void> {
|
|
|
111
128
|
|
|
112
129
|
program
|
|
113
130
|
.command('create-workspace')
|
|
114
|
-
.description(
|
|
131
|
+
.description(`Bootstrap the ${getWorkspaceHome()}/ yarn workspace (one-time setup)`)
|
|
115
132
|
.option('--json', 'emit structured JSON on stdout')
|
|
116
133
|
.action(async (opts: { json?: boolean }) => {
|
|
117
134
|
try {
|
|
@@ -139,7 +156,7 @@ async function main(): Promise<void> {
|
|
|
139
156
|
|
|
140
157
|
program
|
|
141
158
|
.command('create <app-name>')
|
|
142
|
-
.description(
|
|
159
|
+
.description(`Scaffold a new Z2H consumer app inside ${getWorkspaceHome()}/`)
|
|
143
160
|
.option('--template <name>', 'Template name (default | html-embed)')
|
|
144
161
|
.option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
|
|
145
162
|
.option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
|
|
@@ -162,6 +179,59 @@ async function main(): Promise<void> {
|
|
|
162
179
|
}
|
|
163
180
|
);
|
|
164
181
|
|
|
182
|
+
program
|
|
183
|
+
.command('edit <app-name>')
|
|
184
|
+
.description(`Pull the latest version of an app into ${getWorkspaceHome()}/<app-name>, cloning it first if needed`)
|
|
185
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
186
|
+
.action(async (appName: string, opts: { json?: boolean }) => {
|
|
187
|
+
try {
|
|
188
|
+
await trackDuration('z2h_app_edit_pulled', () => editCommand(appName, opts), {
|
|
189
|
+
app_name: appName,
|
|
190
|
+
});
|
|
191
|
+
} catch (err) {
|
|
192
|
+
await fail(`edit failed: ${(err as Error).message}`, { command: 'edit' }, 1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
program
|
|
197
|
+
.command('grant [email]')
|
|
198
|
+
.description('Grant access to the current app (email, --public, or both)')
|
|
199
|
+
.option('--public', 'make the app visible to all monday.com employees')
|
|
200
|
+
.option('--level <level>', 'access level for user grants: owner, editor, or viewer (default: viewer)')
|
|
201
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
202
|
+
.action(async (email: string | undefined, opts: { public?: boolean; level?: string; json?: boolean }) => {
|
|
203
|
+
try {
|
|
204
|
+
await grantCommand(email, opts as Parameters<typeof grantCommand>[1]);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
await fail(`grant failed: ${(err as Error).message}`, { command: 'grant' }, 1);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
program
|
|
211
|
+
.command('revoke [email]')
|
|
212
|
+
.description('Revoke access to the current app (email or --public; owners/admins only)')
|
|
213
|
+
.option('--public', 'remove the public grant, making the app visible to owners/admins only')
|
|
214
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
215
|
+
.action(async (email: string | undefined, opts: { public?: boolean; json?: boolean }) => {
|
|
216
|
+
try {
|
|
217
|
+
await revokeCommand(email, opts as Parameters<typeof revokeCommand>[1]);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
await fail(`revoke failed: ${(err as Error).message}`, { command: 'revoke' }, 1);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
program
|
|
224
|
+
.command('transfer-owner [email]')
|
|
225
|
+
.description('Transfer ownership of the current app to another monday.com user')
|
|
226
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
227
|
+
.action(async (email: string | undefined, opts: { json?: boolean }) => {
|
|
228
|
+
try {
|
|
229
|
+
await transferOwnerCommand(email, opts);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
await fail(`transfer-owner failed: ${(err as Error).message}`, { command: 'transfer-owner' }, 1);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
165
235
|
program
|
|
166
236
|
.command('migrate')
|
|
167
237
|
.description('Run pending workspace migrations')
|
package/src/shadow/prepare.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { isPortFree, pickPort } from './port';
|
|
|
10
10
|
import { syncShadow } from './sync';
|
|
11
11
|
import type { ConsumerPaths, ConsumerPkg, PreparedShadow, ShadowMetadata } from '../types';
|
|
12
12
|
|
|
13
|
-
export async function prepareShadow(): Promise<PreparedShadow> {
|
|
14
|
-
const paths = resolveConsumerPaths(
|
|
13
|
+
export async function prepareShadow(consumerDir: string = process.cwd()): Promise<PreparedShadow> {
|
|
14
|
+
const paths = resolveConsumerPaths(consumerDir);
|
|
15
15
|
|
|
16
16
|
if (!(await pathExists(paths.consumerPkg))) {
|
|
17
17
|
throw new Error(`No package.json found in ${paths.consumerDir}`);
|
|
@@ -45,6 +45,21 @@ When a request needs data, follow `z2h:coding-standards-mf` §0: write a `runQue
|
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
+
## App Visibility
|
|
49
|
+
|
|
50
|
+
Apps are **private by default** — only you can view them. After the first deploy the `z2h:feature-development` skill automatically asks whether to open it up:
|
|
51
|
+
|
|
52
|
+
> "Should this app be visible to everyone at monday.com, or just you for now?"
|
|
53
|
+
|
|
54
|
+
To change visibility at any time:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
npx z2h-cli grant --public # open to all monday.com employees
|
|
58
|
+
npx z2h-cli revoke --public # back to private
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
48
63
|
## ⛔ No Agent Visual Testing
|
|
49
64
|
|
|
50
65
|
When you make a change, **do not screenshot, do not run `axcli chrome`, do not run `playwright-cli`, do not open browser tabs**. The user is sitting in front of the live preview and verifies with their own eyes — HMR updates instantly. Edit → tell the user → wait for their approval. Agent-side visual checks have proven slow and unreliable in this flow.
|
package/src/util/app-name.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Strip any npm scope (`@scope/`) from a package name.
|
|
3
5
|
* e.g. `@trident/customer-viewer` → `customer-viewer`, `foo` → `foo`.
|
|
@@ -6,6 +8,14 @@ export function cleanAppName(name: string): string {
|
|
|
6
8
|
return name.replace(/^@[^/]+\//, '');
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
export function getCwdAppName(): string | undefined {
|
|
12
|
+
try {
|
|
13
|
+
return (JSON.parse(readFileSync('package.json', 'utf8')) as { name?: string }).name;
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* Canonical MF bundle name: the de-scoped app name with an `mf-` prefix.
|
|
11
21
|
* This matches the trident convention (shadow pkg is `@trident/mf-<app>`,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
brokerCredentialProvider,
|
|
4
|
+
fetchDeployVersion,
|
|
5
|
+
DEPLOY_VERSION_PATH,
|
|
6
|
+
STS_BROKER_PATH,
|
|
7
|
+
} from '../broker-credential-provider';
|
|
3
8
|
import { DEFAULT_BROKER_BASE_URL } from '../../../constants';
|
|
4
9
|
import * as authStore from '../auth-store';
|
|
5
10
|
|
|
@@ -32,8 +37,35 @@ function mockFetchOnce(body: unknown, init: { status?: number; text?: string } =
|
|
|
32
37
|
return fetchMock;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
describe('fetchDeployVersion', () => {
|
|
41
|
+
it('POSTs the appName to the version endpoint and returns the version', async () => {
|
|
42
|
+
const fetchMock = mockFetchOnce({ version: 4 });
|
|
43
|
+
|
|
44
|
+
const version = await fetchDeployVersion('my-app');
|
|
45
|
+
|
|
46
|
+
expect(version).toBe(4);
|
|
47
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
48
|
+
`${DEFAULT_BROKER_BASE_URL}${DEPLOY_VERSION_PATH}`,
|
|
49
|
+
expect.objectContaining({
|
|
50
|
+
method: 'POST',
|
|
51
|
+
body: JSON.stringify({ appName: 'my-app' }),
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns 0 when the broker returns no version field (new app)', async () => {
|
|
57
|
+
mockFetchOnce({});
|
|
58
|
+
await expect(fetchDeployVersion('my-app')).resolves.toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('surfaces a remediation hint on 401', async () => {
|
|
62
|
+
mockFetchOnce({}, { status: 401, text: 'Unauthorized' });
|
|
63
|
+
await expect(fetchDeployVersion('my-app')).rejects.toThrow(/token rejected by the broker/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
35
67
|
describe('brokerCredentialProvider', () => {
|
|
36
|
-
it('POSTs the appName to the broker with a Bearer token + parses the response', async () => {
|
|
68
|
+
it('POSTs the appName + version to the broker with a Bearer token + parses the response', async () => {
|
|
37
69
|
const fetchMock = mockFetchOnce({ ...STUB_CREDS, expiration: FAR_FUTURE_EXPIRY });
|
|
38
70
|
const provider = brokerCredentialProvider({ appName: 'my-app' });
|
|
39
71
|
const creds = await provider();
|
|
@@ -20,6 +20,15 @@ export class Z2hAuthMissingError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export async function readZthAuth(): Promise<ZthAuth> {
|
|
23
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
24
|
+
const token = process.env.Z2H_AUTH_TOKEN;
|
|
25
|
+
if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return { token };
|
|
31
|
+
}
|
|
23
32
|
if (!(await pathExists(Z2H_AUTH_FILE))) {
|
|
24
33
|
throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
|
|
25
34
|
}
|
|
@@ -40,6 +49,9 @@ export async function readZthAuth(): Promise<ZthAuth> {
|
|
|
40
49
|
* blow up — and where an inline sync read keeps the spawn call site simple.
|
|
41
50
|
*/
|
|
42
51
|
export function tryReadAuthTokenSync(): string | null {
|
|
52
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
53
|
+
return process.env.Z2H_AUTH_TOKEN;
|
|
54
|
+
}
|
|
43
55
|
try {
|
|
44
56
|
const parsed = readJsonSync(Z2H_AUTH_FILE) as Partial<ZthAuth> | null;
|
|
45
57
|
return parsed?.token ?? null;
|
|
@@ -1,51 +1,78 @@
|
|
|
1
|
-
import { brokerBaseUrl } from '../../constants';
|
|
1
|
+
import { brokerBaseUrl, routingHeaders } from '../../constants';
|
|
2
2
|
import { readZthAuth } from './auth-store';
|
|
3
3
|
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types';
|
|
4
4
|
|
|
5
5
|
export const STS_BROKER_PATH = '/z2h-cli/sts/credentials';
|
|
6
|
+
export const DEPLOY_VERSION_PATH = '/z2h-cli/apps/version';
|
|
6
7
|
|
|
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
11
|
|
|
11
|
-
interface
|
|
12
|
+
interface CredentialsResponse {
|
|
12
13
|
accessKeyId?: string;
|
|
13
14
|
secretAccessKey?: string;
|
|
14
15
|
sessionToken?: string;
|
|
15
16
|
expiration?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
interface VersionResponse {
|
|
20
|
+
version?: number;
|
|
21
|
+
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
async function postToBroker(path: string, body: unknown): Promise<Response> {
|
|
24
|
+
const { token } = await readZthAuth();
|
|
23
25
|
try {
|
|
24
|
-
|
|
26
|
+
return await fetch(`${brokerBaseUrl()}${path}`, {
|
|
25
27
|
method: 'POST',
|
|
26
28
|
headers: {
|
|
27
29
|
Authorization: `Bearer ${token}`,
|
|
28
30
|
'Content-Type': 'application/json',
|
|
29
31
|
Accept: 'application/json',
|
|
32
|
+
...routingHeaders(),
|
|
30
33
|
},
|
|
31
|
-
body: JSON.stringify(
|
|
34
|
+
body: JSON.stringify(body),
|
|
32
35
|
});
|
|
33
36
|
} catch (err) {
|
|
34
37
|
const message = err instanceof Error ? err.message : String(err);
|
|
35
38
|
throw new Error(`Z2H deploy broker request failed: ${message}`);
|
|
36
39
|
}
|
|
40
|
+
}
|
|
37
41
|
|
|
42
|
+
function assertBrokerOk(res: Response): void {
|
|
38
43
|
if (res.status === 401 || res.status === 403) {
|
|
39
44
|
throw new Error(
|
|
40
45
|
`Z2H deploy token rejected by the broker (${res.status}). Refresh user token by running 'z2h-cli generate-z2h-token'.`
|
|
41
46
|
);
|
|
42
47
|
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reserve the version this deploy will write to. Split out of the STS
|
|
52
|
+
* credentials call so version derivation lives on its own endpoint — the CLI
|
|
53
|
+
* asks for the version first, then requests credentials scoped to it.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchDeployVersion(appName: string): Promise<number> {
|
|
56
|
+
const res = await postToBroker(DEPLOY_VERSION_PATH, { appName });
|
|
57
|
+
assertBrokerOk(res);
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
throw new Error(`Z2H deploy broker returned ${res.status}: ${text.slice(0, 1024)}`);
|
|
61
|
+
}
|
|
62
|
+
const body = (await res.json()) as VersionResponse;
|
|
63
|
+
|
|
64
|
+
return body.version ?? 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function fetchStsOnce(appName: string): Promise<AwsCredentialIdentity> {
|
|
68
|
+
const res = await postToBroker(STS_BROKER_PATH, { appName });
|
|
69
|
+
assertBrokerOk(res);
|
|
43
70
|
if (!res.ok) {
|
|
44
71
|
const text = await res.text();
|
|
45
72
|
throw new Error(`Z2H deploy broker returned ${res.status}: ${text.slice(0, 1024)}`);
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
const body = (await res.json()) as
|
|
75
|
+
const body = (await res.json()) as CredentialsResponse;
|
|
49
76
|
if (!body.accessKeyId || !body.secretAccessKey || !body.sessionToken || !body.expiration) {
|
|
50
77
|
throw new Error('Z2H deploy broker response missing required credential fields');
|
|
51
78
|
}
|
|
@@ -60,8 +87,9 @@ async function fetchCredentialsOnce(appName: string): Promise<AwsCredentialIdent
|
|
|
60
87
|
|
|
61
88
|
/**
|
|
62
89
|
* AWS SDK credential provider that fetches short-lived deploy credentials
|
|
63
|
-
* from the bigbrain-zth STS broker
|
|
64
|
-
* 5 minutes before expiry; concurrent calls share a
|
|
90
|
+
* from the bigbrain-zth STS broker for a specific version. Credentials are
|
|
91
|
+
* cached in memory until 5 minutes before expiry; concurrent calls share a
|
|
92
|
+
* single in-flight fetch.
|
|
65
93
|
*/
|
|
66
94
|
export function brokerCredentialProvider({ appName }: { appName: string }): AwsCredentialIdentityProvider {
|
|
67
95
|
let cached: AwsCredentialIdentity | undefined;
|
|
@@ -76,10 +104,10 @@ export function brokerCredentialProvider({ appName }: { appName: string }): AwsC
|
|
|
76
104
|
return inflight;
|
|
77
105
|
}
|
|
78
106
|
|
|
79
|
-
inflight =
|
|
80
|
-
.then(
|
|
81
|
-
cached =
|
|
82
|
-
return
|
|
107
|
+
inflight = fetchStsOnce(appName)
|
|
108
|
+
.then(credentials => {
|
|
109
|
+
cached = credentials;
|
|
110
|
+
return credentials;
|
|
83
111
|
})
|
|
84
112
|
.finally(() => {
|
|
85
113
|
inflight = undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { brokerBaseUrl, routingHeaders } from '../../constants';
|
|
2
|
+
import { readZthAuth } from '../auth/auth-store';
|
|
3
|
+
|
|
4
|
+
export interface AppInfo {
|
|
5
|
+
appName: string;
|
|
6
|
+
version: number;
|
|
7
|
+
entry: Record<string, unknown>;
|
|
8
|
+
deployedBy: string;
|
|
9
|
+
gitRemote: string;
|
|
10
|
+
createdBy: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetches a single app's current state from the broker.
|
|
15
|
+
* Returns null if the app has never been deployed (404).
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchApp(appName: string): Promise<AppInfo | null> {
|
|
18
|
+
const { token } = await readZthAuth();
|
|
19
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/${encodeURIComponent(appName)}`, {
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bearer ${token}`,
|
|
22
|
+
Accept: 'application/json',
|
|
23
|
+
...routingHeaders(),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (res.status === 404) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
throw new Error(`Z2H app lookup failed (${res.status}): ${text.slice(0, 512)}`);
|
|
32
|
+
}
|
|
33
|
+
return res.json() as Promise<AppInfo>;
|
|
34
|
+
}
|