@mondaydotcomorg/z2h-cli 0.5.2 → 0.6.1
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/create-workspace.d.ts.map +1 -1
- package/dist/commands/create-workspace.js +2 -5
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +2 -5
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +41 -0
- package/dist/esm/commands/create-workspace.d.ts.map +1 -1
- package/dist/esm/commands/create-workspace.mjs +2 -5
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +2 -5
- package/dist/esm/constants.d.ts +18 -0
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +35 -1
- package/dist/esm/index.mjs +18 -6
- package/dist/esm/migrations/003-npm-broker-scope-split.d.ts +14 -0
- package/dist/esm/migrations/003-npm-broker-scope-split.d.ts.map +1 -0
- package/dist/esm/migrations/003-npm-broker-scope-split.mjs +49 -0
- package/dist/esm/migrations/__tests__/003-npm-broker-scope-split.test.d.ts +2 -0
- package/dist/esm/migrations/__tests__/003-npm-broker-scope-split.test.d.ts.map +1 -0
- package/dist/esm/migrations/auto-migrate.mjs +4 -4
- package/dist/esm/shadow/sync.d.ts.map +1 -1
- package/dist/esm/shadow/sync.mjs +4 -6
- package/dist/esm/util/auth/auth-store.d.ts +10 -0
- package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
- package/dist/esm/util/auth/auth-store.mjs +20 -2
- package/dist/esm/util/auth/broker-credential-provider.d.ts +0 -1
- package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/esm/util/auth/broker-credential-provider.mjs +2 -5
- package/dist/esm/util/logger.d.ts.map +1 -1
- package/dist/esm/util/logger.mjs +2 -0
- package/dist/esm/util/npm/__tests__/broker-config.test.d.ts +2 -0
- package/dist/esm/util/npm/__tests__/broker-config.test.d.ts.map +1 -0
- package/dist/esm/util/npm/__tests__/install.test.d.ts +2 -0
- package/dist/esm/util/npm/__tests__/install.test.d.ts.map +1 -0
- package/dist/esm/util/npm/broker-config.d.ts +20 -0
- package/dist/esm/util/npm/broker-config.d.ts.map +1 -0
- package/dist/esm/util/npm/broker-config.mjs +59 -0
- package/dist/esm/util/npm/ensure-token.d.ts +19 -0
- package/dist/esm/util/npm/ensure-token.d.ts.map +1 -0
- package/dist/esm/util/npm/ensure-token.mjs +22 -0
- package/dist/esm/util/npm/install.d.ts +18 -0
- package/dist/esm/util/npm/install.d.ts.map +1 -0
- package/dist/esm/util/npm/install.mjs +74 -0
- package/dist/esm/util/tracker.d.ts +3 -0
- package/dist/esm/util/tracker.d.ts.map +1 -0
- package/dist/esm/util/tracker.mjs +20 -0
- package/dist/index.js +18 -6
- package/dist/migrations/003-npm-broker-scope-split.d.ts +14 -0
- package/dist/migrations/003-npm-broker-scope-split.d.ts.map +1 -0
- package/dist/migrations/003-npm-broker-scope-split.js +51 -0
- package/dist/migrations/__tests__/003-npm-broker-scope-split.test.d.ts +2 -0
- package/dist/migrations/__tests__/003-npm-broker-scope-split.test.d.ts.map +1 -0
- package/dist/migrations/auto-migrate.js +3 -3
- package/dist/shadow/sync.d.ts.map +1 -1
- package/dist/shadow/sync.js +4 -6
- package/dist/util/auth/auth-store.d.ts +10 -0
- package/dist/util/auth/auth-store.d.ts.map +1 -1
- package/dist/util/auth/auth-store.js +19 -0
- package/dist/util/auth/broker-credential-provider.d.ts +0 -1
- package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/util/auth/broker-credential-provider.js +2 -6
- package/dist/util/logger.d.ts.map +1 -1
- package/dist/util/logger.js +2 -0
- package/dist/util/npm/__tests__/broker-config.test.d.ts +2 -0
- package/dist/util/npm/__tests__/broker-config.test.d.ts.map +1 -0
- package/dist/util/npm/__tests__/install.test.d.ts +2 -0
- package/dist/util/npm/__tests__/install.test.d.ts.map +1 -0
- package/dist/util/npm/broker-config.d.ts +20 -0
- package/dist/util/npm/broker-config.d.ts.map +1 -0
- package/dist/util/npm/broker-config.js +63 -0
- package/dist/util/npm/ensure-token.d.ts +19 -0
- package/dist/util/npm/ensure-token.d.ts.map +1 -0
- package/dist/util/npm/ensure-token.js +24 -0
- package/dist/util/npm/install.d.ts +18 -0
- package/dist/util/npm/install.d.ts.map +1 -0
- package/dist/util/npm/install.js +76 -0
- package/dist/util/tracker.d.ts +3 -0
- package/dist/util/tracker.d.ts.map +1 -0
- package/dist/util/tracker.js +27 -0
- package/migrations.json +5 -0
- package/package.json +1 -1
- package/src/commands/__tests__/create-workspace.test.ts +9 -11
- package/src/commands/__tests__/create.test.ts +9 -6
- package/src/commands/create-workspace.ts +2 -5
- package/src/commands/create.ts +2 -5
- package/src/constants.ts +41 -0
- package/src/index.ts +18 -6
- package/src/migrations/003-npm-broker-scope-split.ts +52 -0
- package/src/migrations/__tests__/003-npm-broker-scope-split.test.ts +99 -0
- package/src/migrations/auto-migrate.ts +4 -4
- package/src/shadow/sync.ts +4 -6
- package/src/templates/workspace/npmrc +3 -2
- package/src/templates/workspace/yarnrc.yml +6 -2
- package/src/util/__tests__/workspace-templates-shipped.test.ts +19 -0
- package/src/util/auth/__tests__/broker-credential-provider.test.ts +2 -1
- package/src/util/auth/auth-store.ts +19 -1
- package/src/util/auth/broker-credential-provider.ts +1 -5
- package/src/util/logger.ts +2 -0
- package/src/util/npm/__tests__/broker-config.test.ts +48 -0
- package/src/util/npm/__tests__/install.test.ts +94 -0
- package/src/util/npm/broker-config.ts +63 -0
- package/src/util/npm/ensure-token.ts +28 -0
- package/src/util/npm/install.ts +84 -0
- package/src/util/tracker.ts +25 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface EnsureZ2hTokenOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Re-run the token flow even if a token already exists on disk. Used after a
|
|
4
|
+
* broker 401, where the stored token is present but expired/invalid.
|
|
5
|
+
*/
|
|
6
|
+
forceRefresh?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Ensure a z2h JWT exists at `~/.zth/auth.json` before an install-bearing
|
|
10
|
+
* command runs. If none is present (or `forceRefresh` is set), drive the
|
|
11
|
+
* existing `generate-z2h-token` flow — loopback capture with manual-paste
|
|
12
|
+
* fallback — which writes the token. No-ops when a token already exists and no
|
|
13
|
+
* refresh was requested.
|
|
14
|
+
*
|
|
15
|
+
* This is the install preflight: it keeps a friendly auth prompt in front of
|
|
16
|
+
* the raw npm error a non-technical user would otherwise hit.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureZ2hToken(opts?: EnsureZ2hTokenOptions): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=ensure-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure-token.d.ts","sourceRoot":"","sources":["../../../src/util/npm/ensure-token.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAKpF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const util_auth_authStore = require('../auth/auth-store.js');
|
|
4
|
+
const util_logger = require('../logger.js');
|
|
5
|
+
const commands_generateZ2hToken = require('../../commands/generate-z2h-token.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensure a z2h JWT exists at `~/.zth/auth.json` before an install-bearing
|
|
9
|
+
* command runs. If none is present (or `forceRefresh` is set), drive the
|
|
10
|
+
* existing `generate-z2h-token` flow — loopback capture with manual-paste
|
|
11
|
+
* fallback — which writes the token. No-ops when a token already exists and no
|
|
12
|
+
* refresh was requested.
|
|
13
|
+
*
|
|
14
|
+
* This is the install preflight: it keeps a friendly auth prompt in front of
|
|
15
|
+
* the raw npm error a non-technical user would otherwise hit.
|
|
16
|
+
*/
|
|
17
|
+
async function ensureZ2hToken(opts = {}) {
|
|
18
|
+
if (opts.forceRefresh || !util_auth_authStore.tryReadAuthTokenSync()) {
|
|
19
|
+
util_logger.info('you need to sign in to install monday packages — opening the sign-in page');
|
|
20
|
+
await commands_generateZ2hToken.generateZ2HTokenCommand();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.ensureZ2hToken = ensureZ2hToken;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RunYarnInstallOptions {
|
|
2
|
+
/** Directory to run `yarn install` in. */
|
|
3
|
+
cwd: string;
|
|
4
|
+
/** Extra env to merge on top of `process.env` + the broker token. */
|
|
5
|
+
extraEnv?: NodeJS.ProcessEnv;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Run `yarn install` for an install-bearing command:
|
|
9
|
+
* 1. Preflight — ensure a z2h token exists (prompt `generate-z2h-token` if not).
|
|
10
|
+
* 2. Install through the broker.
|
|
11
|
+
* 3. On a broker 401, surface a friendly "your access expired — refreshing it"
|
|
12
|
+
* message, re-auth, and retry the install exactly once.
|
|
13
|
+
*
|
|
14
|
+
* A non-auth failure (real dependency error, network, etc.) propagates as-is so
|
|
15
|
+
* the command's normal error path reports it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runYarnInstall(opts: RunYarnInstallOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/util/npm/install.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,qBAAqB;IACpC,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC9B;AAqCD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/E"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const execa = require('execa');
|
|
4
|
+
const util_logger = require('../logger.js');
|
|
5
|
+
const util_auth_authStore = require('../auth/auth-store.js');
|
|
6
|
+
const constants = require('../../constants.js');
|
|
7
|
+
const util_npm_ensureToken = require('./ensure-token.js');
|
|
8
|
+
|
|
9
|
+
// Substrings that, in a failed install's output, indicate the broker rejected
|
|
10
|
+
// the z2h JWT (expired/invalid). npm surfaces `E401`/`code E401`; yarn berry
|
|
11
|
+
// surfaces an HTTP 401 line. We re-auth on any of these rather than dumping a
|
|
12
|
+
// raw npm error on a non-technical user.
|
|
13
|
+
const AUTH_FAILURE_MARKERS = ['401', 'e401', 'unauthorized', 'authentication'];
|
|
14
|
+
function looksLikeAuthFailure(text) {
|
|
15
|
+
const haystack = text.toLowerCase();
|
|
16
|
+
return AUTH_FAILURE_MARKERS.some(marker => haystack.includes(marker));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Spawn `yarn install` in `cwd` with the npm-broker credential injected, while
|
|
20
|
+
* streaming yarn's output live to the user. stdout is inherited; stderr is
|
|
21
|
+
* piped so we can both echo it and inspect it for a broker auth failure.
|
|
22
|
+
*
|
|
23
|
+
* Returns the captured stderr on success; throws an Error tagged
|
|
24
|
+
* `isAuthFailure` when the failure output looks like a broker 401 so the
|
|
25
|
+
* caller can re-auth, or rethrows the raw execa error otherwise.
|
|
26
|
+
*/
|
|
27
|
+
async function spawnYarnInstall(opts) {
|
|
28
|
+
const token = util_auth_authStore.tryReadAuthTokenSync();
|
|
29
|
+
const env = { ...process.env, ...(token && { [constants.Z2H_NPM_TOKEN_ENV]: token }), ...opts.extraEnv };
|
|
30
|
+
const child = execa.execa('yarn', ['install'], {
|
|
31
|
+
cwd: opts.cwd,
|
|
32
|
+
stdout: 'inherit',
|
|
33
|
+
stderr: ['inherit', 'pipe'],
|
|
34
|
+
env,
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await child;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const e = err;
|
|
41
|
+
const combined = `${e.stderr}\n${e.message ?? ''}`;
|
|
42
|
+
if (looksLikeAuthFailure(combined)) {
|
|
43
|
+
const authErr = new Error('broker rejected the z2h token during install');
|
|
44
|
+
authErr.isAuthFailure = true;
|
|
45
|
+
throw authErr;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run `yarn install` for an install-bearing command:
|
|
52
|
+
* 1. Preflight — ensure a z2h token exists (prompt `generate-z2h-token` if not).
|
|
53
|
+
* 2. Install through the broker.
|
|
54
|
+
* 3. On a broker 401, surface a friendly "your access expired — refreshing it"
|
|
55
|
+
* message, re-auth, and retry the install exactly once.
|
|
56
|
+
*
|
|
57
|
+
* A non-auth failure (real dependency error, network, etc.) propagates as-is so
|
|
58
|
+
* the command's normal error path reports it.
|
|
59
|
+
*/
|
|
60
|
+
async function runYarnInstall(opts) {
|
|
61
|
+
await util_npm_ensureToken.ensureZ2hToken();
|
|
62
|
+
try {
|
|
63
|
+
await spawnYarnInstall(opts);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (!err.isAuthFailure) {
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
util_logger.warn('your access expired — refreshing it now…');
|
|
70
|
+
await util_npm_ensureToken.ensureZ2hToken({ forceRefresh: true });
|
|
71
|
+
util_logger.info('access refreshed — retrying install');
|
|
72
|
+
await spawnYarnInstall(opts);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.runYarnInstall = runYarnInstall;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/util/tracker.ts"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAO5E;AAED,wBAAsB,aAAa,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CAKZ"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
|
|
5
|
+
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
6
|
+
|
|
7
|
+
const os__default = /*#__PURE__*/_interopDefault(os);
|
|
8
|
+
|
|
9
|
+
const DOORMAN_URL = 'https://track.bigbrain.me/prod/event';
|
|
10
|
+
const username = os__default.default.userInfo().username;
|
|
11
|
+
function trackEvent(name, data) {
|
|
12
|
+
void fetch(DOORMAN_URL, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ name, source: 'z2h-cli', data: { username, ...data } }),
|
|
16
|
+
signal: AbortSignal.timeout(5000),
|
|
17
|
+
}).catch(() => { });
|
|
18
|
+
}
|
|
19
|
+
async function trackDuration(eventName, fn, data) {
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
const result = await fn();
|
|
22
|
+
trackEvent(eventName, { ...data, duration_ms: Date.now() - start });
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
exports.trackDuration = trackDuration;
|
|
27
|
+
exports.trackEvent = trackEvent;
|
package/migrations.json
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
"version": "0.4.1",
|
|
10
10
|
"description": "Remove z2h-cli from workspace and consumer devDependencies (managed globally)",
|
|
11
11
|
"migrate": "./dist/migrations/002-remove-z2h-cli-from-workspace-deps"
|
|
12
|
+
},
|
|
13
|
+
"npm-broker-scope-split": {
|
|
14
|
+
"version": "0.6.0",
|
|
15
|
+
"description": "Rewrite workspace .yarnrc.yml/.npmrc from ${NPM_TOKEN} to the @mondaydotcomorg broker scope-split",
|
|
16
|
+
"migrate": "./dist/migrations/003-npm-broker-scope-split"
|
|
12
17
|
}
|
|
13
18
|
}
|
|
14
19
|
}
|
package/package.json
CHANGED
|
@@ -3,12 +3,14 @@ import os from 'node:os';
|
|
|
3
3
|
import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists } from 'fs-extra';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { runYarnInstall } from '../../util/npm/install';
|
|
7
7
|
import { createWorkspaceCommand } from '../create-workspace';
|
|
8
8
|
|
|
9
|
-
// Mock
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Mock the install helper so `yarn install` (with its broker preflight) doesn't
|
|
10
|
+
// actually run. The token-injection + 401 re-auth behavior is covered in
|
|
11
|
+
// util/npm/__tests__/install.test.ts.
|
|
12
|
+
vi.mock('../../util/npm/install', () => ({
|
|
13
|
+
runYarnInstall: vi.fn().mockResolvedValue(undefined),
|
|
12
14
|
}));
|
|
13
15
|
|
|
14
16
|
describe('create-workspace', () => {
|
|
@@ -17,7 +19,7 @@ describe('create-workspace', () => {
|
|
|
17
19
|
beforeEach(async () => {
|
|
18
20
|
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-cw-'));
|
|
19
21
|
process.env.Z2H_WORKSPACE_HOME = tmp;
|
|
20
|
-
vi.mocked(
|
|
22
|
+
vi.mocked(runYarnInstall).mockClear();
|
|
21
23
|
// After mkdtemp the dir exists and is empty; we want absent for the
|
|
22
24
|
// "happy path" tests, so remove it.
|
|
23
25
|
await rm(tmp, { recursive: true, force: true });
|
|
@@ -46,11 +48,7 @@ describe('create-workspace', () => {
|
|
|
46
48
|
expect(await pathExists(path.join(tmp, '.npmrc'))).toBe(true);
|
|
47
49
|
expect(await pathExists(path.join(tmp, 'README.md'))).toBe(true);
|
|
48
50
|
|
|
49
|
-
expect(
|
|
50
|
-
'yarn',
|
|
51
|
-
expect.arrayContaining(['install']),
|
|
52
|
-
expect.objectContaining({ cwd: tmp })
|
|
53
|
-
);
|
|
51
|
+
expect(runYarnInstall).toHaveBeenCalledWith(expect.objectContaining({ cwd: tmp }));
|
|
54
52
|
});
|
|
55
53
|
|
|
56
54
|
it('refuses if the target dir is non-empty', async () => {
|
|
@@ -58,7 +56,7 @@ describe('create-workspace', () => {
|
|
|
58
56
|
await writeJson(path.join(tmp, 'somefile.json'), { hello: 'world' });
|
|
59
57
|
|
|
60
58
|
await expect(createWorkspaceCommand({})).rejects.toThrow(/Workspace already exists/);
|
|
61
|
-
expect(
|
|
59
|
+
expect(runYarnInstall).not.toHaveBeenCalled();
|
|
62
60
|
});
|
|
63
61
|
|
|
64
62
|
it('does not include z2h-cli in workspace devDeps (managed globally)', async () => {
|
|
@@ -3,12 +3,15 @@ import os from 'node:os';
|
|
|
3
3
|
import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists, writeFile } from 'fs-extra';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { runYarnInstall } from '../../util/npm/install';
|
|
7
7
|
import { createCommand } from '../create';
|
|
8
8
|
import * as manifest from '../../util/manifest';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Mock the install helper so `yarn install` (with its broker preflight) doesn't
|
|
11
|
+
// actually run. Token-injection + 401 re-auth is covered in
|
|
12
|
+
// util/npm/__tests__/install.test.ts.
|
|
13
|
+
vi.mock('../../util/npm/install', () => ({
|
|
14
|
+
runYarnInstall: vi.fn().mockResolvedValue(undefined),
|
|
12
15
|
}));
|
|
13
16
|
|
|
14
17
|
vi.spyOn(manifest, 'readManifest').mockResolvedValue({});
|
|
@@ -29,7 +32,7 @@ describe('create', () => {
|
|
|
29
32
|
beforeEach(async () => {
|
|
30
33
|
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-c-'));
|
|
31
34
|
process.env.Z2H_WORKSPACE_HOME = tmp;
|
|
32
|
-
vi.mocked(
|
|
35
|
+
vi.mocked(runYarnInstall).mockClear();
|
|
33
36
|
});
|
|
34
37
|
|
|
35
38
|
afterEach(async () => {
|
|
@@ -39,7 +42,7 @@ describe('create', () => {
|
|
|
39
42
|
|
|
40
43
|
it('refuses when there is no workspace marker', async () => {
|
|
41
44
|
await expect(createCommand('demo-app', {})).rejects.toThrow(/create-workspace/);
|
|
42
|
-
expect(
|
|
45
|
+
expect(runYarnInstall).not.toHaveBeenCalled();
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
it('rejects invalid app names', async () => {
|
|
@@ -76,7 +79,7 @@ describe('create', () => {
|
|
|
76
79
|
expect(content).toContain('export default function DemoApp()');
|
|
77
80
|
expect(content).not.toContain('__APP_COMPONENT__');
|
|
78
81
|
|
|
79
|
-
expect(
|
|
82
|
+
expect(runYarnInstall).toHaveBeenCalledWith(expect.objectContaining({ cwd: tmp }));
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
it('scaffolds an html-embed app and copies the html file', async () => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { ensureDir, writeFile, pathExists, readdir, copy } from 'fs-extra';
|
|
4
|
-
import { execa } from 'execa';
|
|
5
4
|
import { info, output, setOutputMode } from '../util/logger';
|
|
6
5
|
import { getWorkspaceHome } from '../util/workspace';
|
|
7
6
|
import { getTemplatesDir } from '../util/templates';
|
|
7
|
+
import { runYarnInstall } from '../util/npm/install';
|
|
8
8
|
|
|
9
9
|
export interface CreateWorkspaceOptions {
|
|
10
10
|
json?: boolean;
|
|
@@ -46,10 +46,7 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
|
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
48
|
info(`setting up your Z2H workspace — this may take a minute...`);
|
|
49
|
-
await
|
|
50
|
-
cwd: home,
|
|
51
|
-
stdio: 'inherit',
|
|
52
|
-
});
|
|
49
|
+
await runYarnInstall({ cwd: home });
|
|
53
50
|
|
|
54
51
|
output(`Your workspace is ready! 🚀 Apps will live at ${home}`, {
|
|
55
52
|
ok: true,
|
package/src/commands/create.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { ensureDir, pathExists, readdir, writeFile, copy } from 'fs-extra';
|
|
4
|
-
import { execa } from 'execa';
|
|
5
4
|
import { info, output, setOutputMode } from '../util/logger';
|
|
6
5
|
import { assertWorkspace } from '../util/workspace';
|
|
7
6
|
import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
|
|
8
7
|
import { readManifest } from '../util/manifest';
|
|
9
8
|
import { mfAppName } from '../util/app-name';
|
|
10
9
|
import { APP_NAME_PATTERN } from '../constants';
|
|
10
|
+
import { runYarnInstall } from '../util/npm/install';
|
|
11
11
|
|
|
12
12
|
export interface CreateOptions {
|
|
13
13
|
template?: string;
|
|
@@ -116,10 +116,7 @@ export async function createCommand(appName: string, opts: CreateOptions): Promi
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
info(`installing dependencies for ${appName}...`);
|
|
119
|
-
await
|
|
120
|
-
cwd: workspace,
|
|
121
|
-
stdio: 'inherit',
|
|
122
|
-
});
|
|
119
|
+
await runYarnInstall({ cwd: workspace });
|
|
123
120
|
|
|
124
121
|
output(`[z2h-cli] app ready at ${appDir}`, {
|
|
125
122
|
ok: true,
|
package/src/constants.ts
CHANGED
|
@@ -56,3 +56,44 @@ export const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
|
|
|
56
56
|
// backend STS broker rejects anything else with 400; the CLI can validate
|
|
57
57
|
// upfront for friendlier errors.
|
|
58
58
|
export const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
|
|
59
|
+
|
|
60
|
+
// ─────────── Broker origin + npm registry (shared §3 contract) ─────
|
|
61
|
+
// The bigbrain-zth broker fronts both the STS credential route and the npm
|
|
62
|
+
// proxy. `Z2H_BROKER_BASE_URL` overrides the origin for tests/staging.
|
|
63
|
+
export const DEFAULT_BROKER_BASE_URL = 'https://bigbrain-zth.bigbrain.me';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the broker origin (no trailing slash). Honors
|
|
67
|
+
* `Z2H_BROKER_BASE_URL` (tests/staging), else the prod default. Shared by the
|
|
68
|
+
* STS credential provider and the npm-broker registry URL.
|
|
69
|
+
*/
|
|
70
|
+
export function brokerBaseUrl(): string {
|
|
71
|
+
return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The authenticated npm reverse-proxy is mounted here on the broker. Only the
|
|
75
|
+
// `@mondaydotcomorg` scope is served; everything else resolves from npmjs
|
|
76
|
+
// directly. See docs/superpowers/specs/2026-06-03-npm-brokering-design.md §3.
|
|
77
|
+
export const NPM_BROKER_PATH = '/z2h-cli/npm';
|
|
78
|
+
|
|
79
|
+
// The scope brokered through the proxy — the ONLY scope it serves.
|
|
80
|
+
export const BROKER_NPM_SCOPE = '@mondaydotcomorg';
|
|
81
|
+
|
|
82
|
+
// Public registry for everything outside the brokered scope (unchanged speed).
|
|
83
|
+
export const PUBLIC_NPM_REGISTRY_URL = 'https://registry.npmjs.org/';
|
|
84
|
+
|
|
85
|
+
// Env var the client config (`~/.npmrc`, `.yarnrc.yml`) interpolates at
|
|
86
|
+
// invocation time; its value is the z2h JWT, injected per-spawn by the CLI.
|
|
87
|
+
// Deliberately NOT `NPM_TOKEN` — avoids colliding with the monorepo's own dev
|
|
88
|
+
// token and makes clear it is the z2h JWT, not a real npm token.
|
|
89
|
+
export const Z2H_NPM_TOKEN_ENV = 'Z2H_NPM_TOKEN' as const;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Broker npm registry URL (no trailing slash), e.g. prod
|
|
93
|
+
* `https://bigbrain-zth.bigbrain.me/z2h-cli/npm`. Built on `brokerBaseUrl()`
|
|
94
|
+
* so the staging override flows through. The trailing-slash forms required by
|
|
95
|
+
* the `.npmrc`/`.yarnrc.yml` configs are added where those files are written.
|
|
96
|
+
*/
|
|
97
|
+
export function brokerNpmRegistryUrl(): string {
|
|
98
|
+
return `${brokerBaseUrl()}${NPM_BROKER_PATH}`;
|
|
99
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import { devCommand } from './commands/dev';
|
|
4
5
|
import { buildCommand } from './commands/build';
|
|
@@ -9,9 +10,18 @@ import { createCommand } from './commands/create';
|
|
|
9
10
|
import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
|
|
10
11
|
import { migrateCommand } from './commands/migrate';
|
|
11
12
|
import { errorLog, fail } from './util/logger';
|
|
13
|
+
import { trackDuration } from './util/tracker';
|
|
12
14
|
|
|
13
15
|
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
14
16
|
|
|
17
|
+
function getCwdAppName(): string | undefined {
|
|
18
|
+
try {
|
|
19
|
+
return (JSON.parse(readFileSync('package.json', 'utf8')) as { name?: string }).name;
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
async function main(): Promise<void> {
|
|
16
26
|
const program = new Command();
|
|
17
27
|
program
|
|
@@ -38,11 +48,8 @@ async function main(): Promise<void> {
|
|
|
38
48
|
.option('--json', 'emit structured JSON on stdout')
|
|
39
49
|
.action(async (opts: { json?: boolean }) => {
|
|
40
50
|
try {
|
|
41
|
-
await buildCommand(opts);
|
|
51
|
+
await trackDuration('z2h_app_build_success', () => buildCommand(opts), { app_name: getCwdAppName() });
|
|
42
52
|
} catch (err) {
|
|
43
|
-
// fail() emits a structured JSON error on stdout in --json mode and
|
|
44
|
-
// a red line on stderr in human mode, then exits. errorLog alone is
|
|
45
|
-
// silent in JSON mode, which leaves machine consumers with no signal.
|
|
46
53
|
fail(`build failed: ${(err as Error).message}`, { command: 'build' }, 2);
|
|
47
54
|
}
|
|
48
55
|
});
|
|
@@ -78,7 +85,9 @@ async function main(): Promise<void> {
|
|
|
78
85
|
.option('--json', 'emit structured JSON on stdout')
|
|
79
86
|
.action(async (opts: { dryRun?: boolean; json?: boolean }) => {
|
|
80
87
|
try {
|
|
81
|
-
await deployCommand(opts)
|
|
88
|
+
await trackDuration('z2h_app_deploy_success', () => deployCommand(opts), {
|
|
89
|
+
app_name: getCwdAppName(),
|
|
90
|
+
});
|
|
82
91
|
} catch (err) {
|
|
83
92
|
fail(`deploy failed: ${(err as Error).message}`, { command: 'deploy' }, 2);
|
|
84
93
|
}
|
|
@@ -120,7 +129,10 @@ async function main(): Promise<void> {
|
|
|
120
129
|
.option('--json', 'emit structured JSON on stdout')
|
|
121
130
|
.action(async (appName: string, opts: { template?: string; htmlFile?: string; json?: boolean }) => {
|
|
122
131
|
try {
|
|
123
|
-
await createCommand(appName, opts)
|
|
132
|
+
await trackDuration('z2h_app_created', () => createCommand(appName, opts), {
|
|
133
|
+
app_name: appName,
|
|
134
|
+
template: opts.template ?? 'default',
|
|
135
|
+
});
|
|
124
136
|
} catch (err) {
|
|
125
137
|
fail(`create failed: ${(err as Error).message}`, { command: 'create' }, 1);
|
|
126
138
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { workspaceYarnrcContents, workspaceNpmrcContents } from '../util/npm/broker-config';
|
|
2
|
+
import type { Tree } from '@nx/devkit';
|
|
3
|
+
|
|
4
|
+
// Legacy marker: the old configs interpolated a real npm token via
|
|
5
|
+
// `${NPM_TOKEN}`. Its presence means the file predates npm brokering and must
|
|
6
|
+
// be rewritten to the scope-split. Once rewritten (placeholder is now
|
|
7
|
+
// `${Z2H_NPM_TOKEN}`) this no longer matches, so the migration is idempotent.
|
|
8
|
+
const LEGACY_TOKEN_PLACEHOLDER = '${NPM_TOKEN}';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rewrite a single rc file to `contents` iff it exists and still carries the
|
|
12
|
+
* legacy `${NPM_TOKEN}` placeholder. Returns nothing; mutates the Tree.
|
|
13
|
+
*/
|
|
14
|
+
function rewriteIfLegacy(tree: Tree, filePath: string, contents: string): void {
|
|
15
|
+
if (!tree.exists(filePath)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const current = tree.read(filePath, 'utf-8') ?? '';
|
|
19
|
+
if (!current.includes(LEGACY_TOKEN_PLACEHOLDER)) {
|
|
20
|
+
return; // already migrated (or never legacy) — leave untouched
|
|
21
|
+
}
|
|
22
|
+
tree.write(filePath, contents);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Migrate the workspace npm/yarn config from the legacy single-registry
|
|
27
|
+
* `${NPM_TOKEN}` form to the `@mondaydotcomorg`→broker scope-split
|
|
28
|
+
* (`${Z2H_NPM_TOKEN}` placeholder). Rewrites the workspace-root `.yarnrc.yml`
|
|
29
|
+
* and `.npmrc`, plus any consumer-app copies that still carry the legacy
|
|
30
|
+
* placeholder (migration 001 normally removes per-consumer `.yarnrc.yml`, so
|
|
31
|
+
* this is just defensive cleanup).
|
|
32
|
+
*
|
|
33
|
+
* The global `~/.npmrc` broker block is written separately by `migrateCommand`
|
|
34
|
+
* (it lives outside the workspace Tree).
|
|
35
|
+
*/
|
|
36
|
+
export default function npmBrokerScopeSplit(tree: Tree): void {
|
|
37
|
+
const yarnrc = workspaceYarnrcContents();
|
|
38
|
+
const npmrc = workspaceNpmrcContents();
|
|
39
|
+
|
|
40
|
+
// Workspace root.
|
|
41
|
+
rewriteIfLegacy(tree, '.yarnrc.yml', yarnrc);
|
|
42
|
+
rewriteIfLegacy(tree, '.npmrc', npmrc);
|
|
43
|
+
|
|
44
|
+
// Any consumer apps that still carry their own legacy rc files.
|
|
45
|
+
for (const entry of tree.children('.')) {
|
|
46
|
+
if (entry.startsWith('.')) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
rewriteIfLegacy(tree, `${entry}/.yarnrc.yml`, yarnrc);
|
|
50
|
+
rewriteIfLegacy(tree, `${entry}/.npmrc`, npmrc);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createTree } from '@nx/devkit/testing';
|
|
3
|
+
import npmBrokerScopeSplit from '../003-npm-broker-scope-split';
|
|
4
|
+
import type { Tree } from '@nx/devkit';
|
|
5
|
+
|
|
6
|
+
// The legacy workspace files brokering replaces.
|
|
7
|
+
const LEGACY_YARNRC = [
|
|
8
|
+
'nodeLinker: node-modules',
|
|
9
|
+
'',
|
|
10
|
+
"npmAuthToken: '${NPM_TOKEN}'",
|
|
11
|
+
'',
|
|
12
|
+
"npmRegistryServer: 'https://registry.npmjs.org/'",
|
|
13
|
+
'',
|
|
14
|
+
'yarnPath: .yarn/releases/yarn-4.5.1.cjs',
|
|
15
|
+
'',
|
|
16
|
+
].join('\n');
|
|
17
|
+
|
|
18
|
+
const LEGACY_NPMRC = [
|
|
19
|
+
'always-auth=true',
|
|
20
|
+
'//registry.npmjs.org/:_authToken=${NPM_TOKEN}',
|
|
21
|
+
'registry=https://registry.npmjs.org/',
|
|
22
|
+
'',
|
|
23
|
+
].join('\n');
|
|
24
|
+
|
|
25
|
+
function makeTree(): Tree {
|
|
26
|
+
return createTree();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('migration 003 — npm broker scope split', () => {
|
|
30
|
+
it('rewrites a legacy ${NPM_TOKEN} workspace .yarnrc.yml to the scope-split', () => {
|
|
31
|
+
const tree = makeTree();
|
|
32
|
+
tree.write('.yarnrc.yml', LEGACY_YARNRC);
|
|
33
|
+
|
|
34
|
+
npmBrokerScopeSplit(tree);
|
|
35
|
+
|
|
36
|
+
const out = tree.read('.yarnrc.yml', 'utf-8')!;
|
|
37
|
+
expect(out).not.toContain('${NPM_TOKEN}');
|
|
38
|
+
expect(out).toContain(' mondaydotcomorg:');
|
|
39
|
+
expect(out).toContain(' npmRegistryServer: "https://bigbrain-zth.bigbrain.me/z2h-cli/npm"');
|
|
40
|
+
expect(out).toContain(' npmAuthToken: "${Z2H_NPM_TOKEN}"');
|
|
41
|
+
expect(out).toContain('npmRegistryServer: "https://registry.npmjs.org/"');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rewrites a legacy ${NPM_TOKEN} workspace .npmrc to the scope-split', () => {
|
|
45
|
+
const tree = makeTree();
|
|
46
|
+
tree.write('.npmrc', LEGACY_NPMRC);
|
|
47
|
+
|
|
48
|
+
npmBrokerScopeSplit(tree);
|
|
49
|
+
|
|
50
|
+
const out = tree.read('.npmrc', 'utf-8')!;
|
|
51
|
+
expect(out).not.toContain('${NPM_TOKEN}');
|
|
52
|
+
expect(out).toContain('@mondaydotcomorg:registry=https://bigbrain-zth.bigbrain.me/z2h-cli/npm/');
|
|
53
|
+
expect(out).toContain('//bigbrain-zth.bigbrain.me/z2h-cli/npm/:_authToken=${Z2H_NPM_TOKEN}');
|
|
54
|
+
expect(out).toContain('registry=https://registry.npmjs.org/');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('is idempotent — a second run on already-migrated files is a no-op', () => {
|
|
58
|
+
const tree = makeTree();
|
|
59
|
+
tree.write('.yarnrc.yml', LEGACY_YARNRC);
|
|
60
|
+
tree.write('.npmrc', LEGACY_NPMRC);
|
|
61
|
+
|
|
62
|
+
npmBrokerScopeSplit(tree);
|
|
63
|
+
const yarnrcAfterFirst = tree.read('.yarnrc.yml', 'utf-8')!;
|
|
64
|
+
const npmrcAfterFirst = tree.read('.npmrc', 'utf-8')!;
|
|
65
|
+
|
|
66
|
+
npmBrokerScopeSplit(tree);
|
|
67
|
+
expect(tree.read('.yarnrc.yml', 'utf-8')!).toBe(yarnrcAfterFirst);
|
|
68
|
+
expect(tree.read('.npmrc', 'utf-8')!).toBe(npmrcAfterFirst);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('leaves non-legacy files untouched (no ${NPM_TOKEN} marker)', () => {
|
|
72
|
+
const tree = makeTree();
|
|
73
|
+
const custom = 'nodeLinker: node-modules\nnpmRegistryServer: "https://my.custom/"\n';
|
|
74
|
+
tree.write('.yarnrc.yml', custom);
|
|
75
|
+
|
|
76
|
+
npmBrokerScopeSplit(tree);
|
|
77
|
+
|
|
78
|
+
expect(tree.read('.yarnrc.yml', 'utf-8')!).toBe(custom);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('rewrites a consumer-app legacy .yarnrc.yml it finds', () => {
|
|
82
|
+
const tree = makeTree();
|
|
83
|
+
tree.write('my-app/package.json', '{"name":"my-app"}');
|
|
84
|
+
tree.write('my-app/.yarnrc.yml', LEGACY_YARNRC);
|
|
85
|
+
|
|
86
|
+
npmBrokerScopeSplit(tree);
|
|
87
|
+
|
|
88
|
+
const out = tree.read('my-app/.yarnrc.yml', 'utf-8')!;
|
|
89
|
+
expect(out).not.toContain('${NPM_TOKEN}');
|
|
90
|
+
expect(out).toContain(' npmAuthToken: "${Z2H_NPM_TOKEN}"');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does nothing when no rc files exist', () => {
|
|
94
|
+
const tree = makeTree();
|
|
95
|
+
expect(() => npmBrokerScopeSplit(tree)).not.toThrow();
|
|
96
|
+
expect(tree.exists('.yarnrc.yml')).toBe(false);
|
|
97
|
+
expect(tree.exists('.npmrc')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { pathExists, readJson, writeJson, writeFile } from 'fs-extra';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import semver from 'semver';
|
|
7
|
-
import { info, warn } from '../util/logger';
|
|
7
|
+
import { info, warn, errorLog } from '../util/logger';
|
|
8
8
|
import { getWorkspaceHome, isZ2hWorkspace } from '../util/workspace';
|
|
9
9
|
|
|
10
10
|
const STATE_FILE = '.z2h-migrations-state.json';
|
|
@@ -199,9 +199,9 @@ export async function autoMigrate(cliVersion: string, dryRun = false): Promise<v
|
|
|
199
199
|
info(` ○ ${entry.description} (no changes needed)`);
|
|
200
200
|
}
|
|
201
201
|
} catch (err) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
const errorMessage = `migration "${name}" failed: ${(err as Error).message}`;
|
|
203
|
+
errorLog(`${errorMessage}\nSkipping remaining migrations — run "z2h-cli migrate" to retry.`);
|
|
204
|
+
throw new Error(errorMessage);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|