@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.
Files changed (105) hide show
  1. package/dist/commands/create-workspace.d.ts.map +1 -1
  2. package/dist/commands/create-workspace.js +2 -5
  3. package/dist/commands/create.d.ts.map +1 -1
  4. package/dist/commands/create.js +2 -5
  5. package/dist/constants.d.ts +18 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +41 -0
  8. package/dist/esm/commands/create-workspace.d.ts.map +1 -1
  9. package/dist/esm/commands/create-workspace.mjs +2 -5
  10. package/dist/esm/commands/create.d.ts.map +1 -1
  11. package/dist/esm/commands/create.mjs +2 -5
  12. package/dist/esm/constants.d.ts +18 -0
  13. package/dist/esm/constants.d.ts.map +1 -1
  14. package/dist/esm/constants.mjs +35 -1
  15. package/dist/esm/index.mjs +18 -6
  16. package/dist/esm/migrations/003-npm-broker-scope-split.d.ts +14 -0
  17. package/dist/esm/migrations/003-npm-broker-scope-split.d.ts.map +1 -0
  18. package/dist/esm/migrations/003-npm-broker-scope-split.mjs +49 -0
  19. package/dist/esm/migrations/__tests__/003-npm-broker-scope-split.test.d.ts +2 -0
  20. package/dist/esm/migrations/__tests__/003-npm-broker-scope-split.test.d.ts.map +1 -0
  21. package/dist/esm/migrations/auto-migrate.mjs +4 -4
  22. package/dist/esm/shadow/sync.d.ts.map +1 -1
  23. package/dist/esm/shadow/sync.mjs +4 -6
  24. package/dist/esm/util/auth/auth-store.d.ts +10 -0
  25. package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
  26. package/dist/esm/util/auth/auth-store.mjs +20 -2
  27. package/dist/esm/util/auth/broker-credential-provider.d.ts +0 -1
  28. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
  29. package/dist/esm/util/auth/broker-credential-provider.mjs +2 -5
  30. package/dist/esm/util/logger.d.ts.map +1 -1
  31. package/dist/esm/util/logger.mjs +2 -0
  32. package/dist/esm/util/npm/__tests__/broker-config.test.d.ts +2 -0
  33. package/dist/esm/util/npm/__tests__/broker-config.test.d.ts.map +1 -0
  34. package/dist/esm/util/npm/__tests__/install.test.d.ts +2 -0
  35. package/dist/esm/util/npm/__tests__/install.test.d.ts.map +1 -0
  36. package/dist/esm/util/npm/broker-config.d.ts +20 -0
  37. package/dist/esm/util/npm/broker-config.d.ts.map +1 -0
  38. package/dist/esm/util/npm/broker-config.mjs +59 -0
  39. package/dist/esm/util/npm/ensure-token.d.ts +19 -0
  40. package/dist/esm/util/npm/ensure-token.d.ts.map +1 -0
  41. package/dist/esm/util/npm/ensure-token.mjs +22 -0
  42. package/dist/esm/util/npm/install.d.ts +18 -0
  43. package/dist/esm/util/npm/install.d.ts.map +1 -0
  44. package/dist/esm/util/npm/install.mjs +74 -0
  45. package/dist/esm/util/tracker.d.ts +3 -0
  46. package/dist/esm/util/tracker.d.ts.map +1 -0
  47. package/dist/esm/util/tracker.mjs +20 -0
  48. package/dist/index.js +18 -6
  49. package/dist/migrations/003-npm-broker-scope-split.d.ts +14 -0
  50. package/dist/migrations/003-npm-broker-scope-split.d.ts.map +1 -0
  51. package/dist/migrations/003-npm-broker-scope-split.js +51 -0
  52. package/dist/migrations/__tests__/003-npm-broker-scope-split.test.d.ts +2 -0
  53. package/dist/migrations/__tests__/003-npm-broker-scope-split.test.d.ts.map +1 -0
  54. package/dist/migrations/auto-migrate.js +3 -3
  55. package/dist/shadow/sync.d.ts.map +1 -1
  56. package/dist/shadow/sync.js +4 -6
  57. package/dist/util/auth/auth-store.d.ts +10 -0
  58. package/dist/util/auth/auth-store.d.ts.map +1 -1
  59. package/dist/util/auth/auth-store.js +19 -0
  60. package/dist/util/auth/broker-credential-provider.d.ts +0 -1
  61. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
  62. package/dist/util/auth/broker-credential-provider.js +2 -6
  63. package/dist/util/logger.d.ts.map +1 -1
  64. package/dist/util/logger.js +2 -0
  65. package/dist/util/npm/__tests__/broker-config.test.d.ts +2 -0
  66. package/dist/util/npm/__tests__/broker-config.test.d.ts.map +1 -0
  67. package/dist/util/npm/__tests__/install.test.d.ts +2 -0
  68. package/dist/util/npm/__tests__/install.test.d.ts.map +1 -0
  69. package/dist/util/npm/broker-config.d.ts +20 -0
  70. package/dist/util/npm/broker-config.d.ts.map +1 -0
  71. package/dist/util/npm/broker-config.js +63 -0
  72. package/dist/util/npm/ensure-token.d.ts +19 -0
  73. package/dist/util/npm/ensure-token.d.ts.map +1 -0
  74. package/dist/util/npm/ensure-token.js +24 -0
  75. package/dist/util/npm/install.d.ts +18 -0
  76. package/dist/util/npm/install.d.ts.map +1 -0
  77. package/dist/util/npm/install.js +76 -0
  78. package/dist/util/tracker.d.ts +3 -0
  79. package/dist/util/tracker.d.ts.map +1 -0
  80. package/dist/util/tracker.js +27 -0
  81. package/migrations.json +5 -0
  82. package/package.json +1 -1
  83. package/src/commands/__tests__/create-workspace.test.ts +9 -11
  84. package/src/commands/__tests__/create.test.ts +9 -6
  85. package/src/commands/create-workspace.ts +2 -5
  86. package/src/commands/create.ts +2 -5
  87. package/src/constants.ts +41 -0
  88. package/src/index.ts +18 -6
  89. package/src/migrations/003-npm-broker-scope-split.ts +52 -0
  90. package/src/migrations/__tests__/003-npm-broker-scope-split.test.ts +99 -0
  91. package/src/migrations/auto-migrate.ts +4 -4
  92. package/src/shadow/sync.ts +4 -6
  93. package/src/templates/workspace/npmrc +3 -2
  94. package/src/templates/workspace/yarnrc.yml +6 -2
  95. package/src/util/__tests__/workspace-templates-shipped.test.ts +19 -0
  96. package/src/util/auth/__tests__/broker-credential-provider.test.ts +2 -1
  97. package/src/util/auth/auth-store.ts +19 -1
  98. package/src/util/auth/broker-credential-provider.ts +1 -5
  99. package/src/util/logger.ts +2 -0
  100. package/src/util/npm/__tests__/broker-config.test.ts +48 -0
  101. package/src/util/npm/__tests__/install.test.ts +94 -0
  102. package/src/util/npm/broker-config.ts +63 -0
  103. package/src/util/npm/ensure-token.ts +28 -0
  104. package/src/util/npm/install.ts +84 -0
  105. package/src/util/tracker.ts +25 -0
@@ -1,6 +1,6 @@
1
1
  import { pathExists, readFile, writeFile } from 'fs-extra';
2
- import { execa } from 'execa';
3
2
  import { info } from '../util/logger';
3
+ import { runYarnInstall } from '../util/npm/install';
4
4
  import { buildShadowPkg, hashConsumerDeps, writeShadowPkg } from './merge-package-json';
5
5
  import type { ConsumerPaths, ConsumerPkg } from '../types';
6
6
 
@@ -20,11 +20,9 @@ export async function syncShadow(paths: ConsumerPaths, consumerPkg: ConsumerPkg)
20
20
  if (oldHash !== newHash) {
21
21
  info(oldHash ? 'consumer deps changed — yarn install' : 'first run — yarn install');
22
22
  // Yarn 4 is cache-first and non-interactive by default in non-TTY
23
- // contexts, so plain `yarn install` is all we need.
24
- await execa('yarn', ['install'], {
25
- cwd: paths.consumerDir,
26
- stdio: 'inherit',
27
- });
23
+ // contexts, so plain `yarn install` is all we need. runYarnInstall injects
24
+ // the npm-broker token, preflights auth, and re-auths on a broker 401.
25
+ await runYarnInstall({ cwd: paths.consumerDir });
28
26
  await writeFile(paths.shadowDepsHash, newHash);
29
27
  }
30
28
  }
@@ -1,3 +1,4 @@
1
- always-auth=true
2
- //registry.npmjs.org/:_authToken=${NPM_TOKEN}
1
+ @mondaydotcomorg:registry=https://bigbrain-zth.bigbrain.me/z2h-cli/npm/
2
+ //bigbrain-zth.bigbrain.me/z2h-cli/npm/:_authToken=${Z2H_NPM_TOKEN}
3
3
  registry=https://registry.npmjs.org/
4
+ always-auth=true
@@ -1,7 +1,11 @@
1
1
  nodeLinker: node-modules
2
2
 
3
- npmAuthToken: '${NPM_TOKEN}'
3
+ npmScopes:
4
+ mondaydotcomorg:
5
+ npmRegistryServer: "https://bigbrain-zth.bigbrain.me/z2h-cli/npm"
6
+ npmAuthToken: "${Z2H_NPM_TOKEN}"
7
+ npmAlwaysAuth: true
4
8
 
5
- npmRegistryServer: 'https://registry.npmjs.org/'
9
+ npmRegistryServer: "https://registry.npmjs.org/"
6
10
 
7
11
  yarnPath: .yarn/releases/yarn-4.5.1.cjs
@@ -33,4 +33,23 @@ describe('workspace templates ship with the package', () => {
33
33
  const raw = await readFile(path.join(getTemplatesDir(), 'workspace', 'yarnrc.yml'), 'utf8');
34
34
  expect(raw).toMatch(/yarnPath:\s*\.yarn\/releases\/yarn-4\.5\.1\.cjs/);
35
35
  });
36
+
37
+ it('workspace yarnrc scope-splits @mondaydotcomorg to the broker with the Z2H placeholder, no legacy token', async () => {
38
+ const raw = await readFile(path.join(getTemplatesDir(), 'workspace', 'yarnrc.yml'), 'utf8');
39
+ expect(raw).not.toContain('${NPM_TOKEN}');
40
+ expect(raw).toContain('npmScopes:');
41
+ expect(raw).toContain('mondaydotcomorg:');
42
+ expect(raw).toContain('npmRegistryServer: "https://bigbrain-zth.bigbrain.me/z2h-cli/npm"');
43
+ expect(raw).toContain('npmAuthToken: "${Z2H_NPM_TOKEN}"');
44
+ // Default registry stays public/direct.
45
+ expect(raw).toContain('npmRegistryServer: "https://registry.npmjs.org/"');
46
+ });
47
+
48
+ it('workspace npmrc routes the broker scope + keeps a public default registry, no legacy token', async () => {
49
+ const raw = await readFile(path.join(getTemplatesDir(), 'workspace', 'npmrc'), 'utf8');
50
+ expect(raw).not.toContain('${NPM_TOKEN}');
51
+ expect(raw).toContain('@mondaydotcomorg:registry=https://bigbrain-zth.bigbrain.me/z2h-cli/npm/');
52
+ expect(raw).toContain('//bigbrain-zth.bigbrain.me/z2h-cli/npm/:_authToken=${Z2H_NPM_TOKEN}');
53
+ expect(raw).toContain('registry=https://registry.npmjs.org/');
54
+ });
36
55
  });
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { brokerCredentialProvider, DEFAULT_BROKER_BASE_URL, STS_BROKER_PATH } from '../broker-credential-provider';
2
+ import { brokerCredentialProvider, STS_BROKER_PATH } from '../broker-credential-provider';
3
+ import { DEFAULT_BROKER_BASE_URL } from '../../../constants';
3
4
  import * as authStore from '../auth-store';
4
5
 
5
6
  const STUB_CREDS = {
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
- import { pathExists, readJson } from 'fs-extra';
3
+ import { pathExists, readJson, readJsonSync } from 'fs-extra';
4
4
 
5
5
  // npm-style convention: per-user config under ~/.zth (mirrors ~/.npmrc, ~/.aws/).
6
6
  // Kept out of the workspace dir so accidental `git init` in ~/zth-projects can't
@@ -29,3 +29,21 @@ export async function readZthAuth(): Promise<ZthAuth> {
29
29
  }
30
30
  return { token: parsed.token };
31
31
  }
32
+
33
+ /**
34
+ * Synchronous, never-throws read of the z2h JWT from `~/.zth/auth.json`.
35
+ * Returns the token string, or `null` when the file is absent, unreadable,
36
+ * malformed, or missing the `token` field.
37
+ *
38
+ * Used when building the env for a spawned package-manager process, where a
39
+ * missing token must degrade gracefully (best-effort self-update) rather than
40
+ * blow up — and where an inline sync read keeps the spawn call site simple.
41
+ */
42
+ export function tryReadAuthTokenSync(): string | null {
43
+ try {
44
+ const parsed = readJsonSync(Z2H_AUTH_FILE) as Partial<ZthAuth> | null;
45
+ return parsed?.token ?? null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
@@ -1,7 +1,7 @@
1
+ import { brokerBaseUrl } from '../../constants';
1
2
  import { readZthAuth } from './auth-store';
2
3
  import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types';
3
4
 
4
- export const DEFAULT_BROKER_BASE_URL = 'https://bigbrain-zth.bigbrain.me';
5
5
  export const STS_BROKER_PATH = '/z2h-cli/sts/credentials';
6
6
 
7
7
  // Refresh the cached credentials this many ms before they expire so the next
@@ -15,10 +15,6 @@ interface BrokerResponse {
15
15
  expiration?: string;
16
16
  }
17
17
 
18
- function brokerBaseUrl(): string {
19
- return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
20
- }
21
-
22
18
  async function fetchCredentialsOnce(appName: string): Promise<AwsCredentialIdentity> {
23
19
  const { token } = await readZthAuth();
24
20
  const url = `${brokerBaseUrl()}${STS_BROKER_PATH}`;
@@ -1,4 +1,5 @@
1
1
  import pc from 'picocolors';
2
+ import { trackEvent } from './tracker';
2
3
 
3
4
  export type OutputMode = 'human' | 'json';
4
5
 
@@ -60,5 +61,6 @@ export function fail(message: string, payload: Record<string, unknown>, exitCode
60
61
  process.stderr.write(pc.dim(` → ${payload.suggestedCommand}\n`));
61
62
  }
62
63
  }
64
+ trackEvent('z2h_cli_error', { action: (payload.command as string) ?? 'unknown', error: message });
63
65
  process.exit(exitCode);
64
66
  }
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import { brokerNpmrcLines, workspaceNpmrcContents, workspaceYarnrcContents } from '../broker-config';
3
+
4
+ afterEach(() => {
5
+ vi.unstubAllEnvs();
6
+ });
7
+
8
+ describe('broker-config (prod defaults)', () => {
9
+ it('npmrc lines route the brokered scope and supply the placeholder only', () => {
10
+ const lines = brokerNpmrcLines();
11
+ expect(lines).toEqual([
12
+ '@mondaydotcomorg:registry=https://bigbrain-zth.bigbrain.me/z2h-cli/npm/',
13
+ '//bigbrain-zth.bigbrain.me/z2h-cli/npm/:_authToken=${Z2H_NPM_TOKEN}',
14
+ ]);
15
+ });
16
+
17
+ it('workspace .npmrc keeps a public default registry + always-auth and no legacy token', () => {
18
+ const npmrc = workspaceNpmrcContents();
19
+ expect(npmrc).toContain('@mondaydotcomorg:registry=https://bigbrain-zth.bigbrain.me/z2h-cli/npm/');
20
+ expect(npmrc).toContain('registry=https://registry.npmjs.org/');
21
+ expect(npmrc).toContain('always-auth=true');
22
+ expect(npmrc).not.toContain('${NPM_TOKEN}');
23
+ expect(npmrc).toContain('${Z2H_NPM_TOKEN}');
24
+ });
25
+
26
+ it('workspace .yarnrc.yml scope-splits the brokered scope and leaves the default registry public', () => {
27
+ const yarnrc = workspaceYarnrcContents();
28
+ expect(yarnrc).toContain('npmScopes:');
29
+ expect(yarnrc).toContain(' mondaydotcomorg:');
30
+ expect(yarnrc).toContain(' npmRegistryServer: "https://bigbrain-zth.bigbrain.me/z2h-cli/npm"');
31
+ expect(yarnrc).toContain(' npmAuthToken: "${Z2H_NPM_TOKEN}"');
32
+ expect(yarnrc).toContain(' npmAlwaysAuth: true');
33
+ expect(yarnrc).toContain('npmRegistryServer: "https://registry.npmjs.org/"');
34
+ expect(yarnrc).toContain('yarnPath: .yarn/releases/yarn-4.5.1.cjs');
35
+ expect(yarnrc).not.toContain('${NPM_TOKEN}');
36
+ });
37
+ });
38
+
39
+ describe('broker-config (staging override)', () => {
40
+ it('honors Z2H_BROKER_BASE_URL for the registry host in every form', () => {
41
+ vi.stubEnv('Z2H_BROKER_BASE_URL', 'https://bigbrainstaging.me');
42
+ expect(brokerNpmrcLines()).toEqual([
43
+ '@mondaydotcomorg:registry=https://bigbrainstaging.me/z2h-cli/npm/',
44
+ '//bigbrainstaging.me/z2h-cli/npm/:_authToken=${Z2H_NPM_TOKEN}',
45
+ ]);
46
+ expect(workspaceYarnrcContents()).toContain(' npmRegistryServer: "https://bigbrainstaging.me/z2h-cli/npm"');
47
+ });
48
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { execa } from 'execa';
3
+ import { runYarnInstall } from '../install';
4
+ import * as ensureToken from '../ensure-token';
5
+ import * as authStore from '../../auth/auth-store';
6
+
7
+ vi.mock('execa', () => ({
8
+ execa: vi.fn(),
9
+ }));
10
+
11
+ function makeChild({
12
+ rejectWith,
13
+ stderrChunks = [],
14
+ }: {
15
+ rejectWith?: Error;
16
+ stderrChunks?: string[];
17
+ } = {}) {
18
+ if (rejectWith) {
19
+ (rejectWith as Error & { stderr?: string }).stderr = stderrChunks.join('');
20
+ }
21
+ const promise = rejectWith ? Promise.reject(rejectWith) : Promise.resolve({ exitCode: 0 });
22
+ promise.catch(() => undefined);
23
+ return {
24
+ then: promise.then.bind(promise),
25
+ catch: promise.catch.bind(promise),
26
+ finally: promise.finally.bind(promise),
27
+ };
28
+ }
29
+
30
+ beforeEach(() => {
31
+ vi.restoreAllMocks();
32
+ vi.mocked(execa).mockReset();
33
+ vi.spyOn(ensureToken, 'ensureZ2hToken').mockResolvedValue();
34
+ vi.spyOn(authStore, 'tryReadAuthTokenSync').mockReturnValue('jwt-stub');
35
+ });
36
+
37
+ describe('runYarnInstall', () => {
38
+ it('preflights the token then spawns `yarn install` with Z2H_NPM_TOKEN in env', async () => {
39
+ vi.mocked(execa).mockReturnValueOnce(makeChild() as never);
40
+
41
+ await runYarnInstall({ cwd: '/ws' });
42
+
43
+ expect(ensureToken.ensureZ2hToken).toHaveBeenCalledTimes(1);
44
+ expect(execa).toHaveBeenCalledWith(
45
+ 'yarn',
46
+ ['install'],
47
+ expect.objectContaining({
48
+ cwd: '/ws',
49
+ env: expect.objectContaining({ Z2H_NPM_TOKEN: 'jwt-stub' }),
50
+ })
51
+ );
52
+ });
53
+
54
+ it('merges extraEnv on top of the broker token', async () => {
55
+ vi.mocked(execa).mockReturnValueOnce(makeChild() as never);
56
+
57
+ await runYarnInstall({ cwd: '/ws', extraEnv: { PUBLIC_URL: 'https://x' } });
58
+
59
+ expect(execa).toHaveBeenCalledWith(
60
+ 'yarn',
61
+ ['install'],
62
+ expect.objectContaining({
63
+ env: expect.objectContaining({ Z2H_NPM_TOKEN: 'jwt-stub', PUBLIC_URL: 'https://x' }),
64
+ })
65
+ );
66
+ });
67
+
68
+ it('on a broker 401, re-auths (forceRefresh) and retries the install once', async () => {
69
+ const e401 = new Error('Process exited with code 1');
70
+ vi.mocked(execa)
71
+ .mockReturnValueOnce(makeChild({ rejectWith: e401, stderrChunks: ['npm error code E401\n'] }) as never)
72
+ .mockReturnValueOnce(makeChild() as never);
73
+
74
+ await runYarnInstall({ cwd: '/ws' });
75
+
76
+ // Preflight + forced refresh after the 401.
77
+ expect(ensureToken.ensureZ2hToken).toHaveBeenCalledTimes(2);
78
+ expect(vi.mocked(ensureToken.ensureZ2hToken).mock.calls[1][0]).toEqual({ forceRefresh: true });
79
+ // Two install attempts.
80
+ expect(execa).toHaveBeenCalledTimes(2);
81
+ });
82
+
83
+ it('rethrows a non-auth failure without retrying', async () => {
84
+ const depErr = new Error('ENOTFOUND some-package');
85
+ vi.mocked(execa).mockReturnValueOnce(
86
+ makeChild({ rejectWith: depErr, stderrChunks: ["error Couldn't find package\n"] }) as never
87
+ );
88
+
89
+ await expect(runYarnInstall({ cwd: '/ws' })).rejects.toThrow(/ENOTFOUND/);
90
+ expect(execa).toHaveBeenCalledTimes(1);
91
+ // No forced re-auth on a non-auth error.
92
+ expect(ensureToken.ensureZ2hToken).toHaveBeenCalledTimes(1);
93
+ });
94
+ });
@@ -0,0 +1,63 @@
1
+ import { BROKER_NPM_SCOPE, PUBLIC_NPM_REGISTRY_URL, Z2H_NPM_TOKEN_ENV, brokerNpmRegistryUrl } from '../../constants';
2
+
3
+ /** `https://host/path/` (with trailing slash) for the registry= line. */
4
+ function registryUrlWithSlash(): string {
5
+ const url = brokerNpmRegistryUrl();
6
+ return url.endsWith('/') ? url : `${url}/`;
7
+ }
8
+
9
+ /**
10
+ * The `//host/path/` prefix (scheme stripped, trailing slash kept) that npm
11
+ * requires the `:_authToken` key to be keyed under so it aligns with the
12
+ * registry path. Derived from the same registry URL so staging overrides flow
13
+ * through.
14
+ */
15
+ function authKeyPrefix(): string {
16
+ return registryUrlWithSlash().replace(/^https?:/, '');
17
+ }
18
+
19
+ /**
20
+ * The two npm config lines (§3.1 / §3.3) that route the brokered scope to the
21
+ * proxy and supply the `${Z2H_NPM_TOKEN}` placeholder. No literal token — npm
22
+ * interpolates the placeholder from the env at invocation time.
23
+ */
24
+ export function brokerNpmrcLines(): string[] {
25
+ return [
26
+ `${BROKER_NPM_SCOPE}:registry=${registryUrlWithSlash()}`,
27
+ `${authKeyPrefix()}:_authToken=\${${Z2H_NPM_TOKEN_ENV}}`,
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * Full contents for a **workspace** `.npmrc` (§3.3): broker scope-split plus an
33
+ * explicit public default registry and always-auth. Used by the template and
34
+ * the migration so both stay byte-identical.
35
+ */
36
+ export function workspaceNpmrcContents(): string {
37
+ return [...brokerNpmrcLines(), `registry=${PUBLIC_NPM_REGISTRY_URL}`, 'always-auth=true'].join('\n') + '\n';
38
+ }
39
+
40
+ /**
41
+ * Full contents for a **workspace** `.yarnrc.yml` (§3.2): the brokered scope
42
+ * points at the proxy with the `${Z2H_NPM_TOKEN}` placeholder + always-auth;
43
+ * the default `npmRegistryServer` stays the public registry (direct, unchanged
44
+ * speed). `yarnPath` keeps the bundled Berry release so any `yarn` runs 4.x.
45
+ */
46
+ export function workspaceYarnrcContents(): string {
47
+ const scopeKey = BROKER_NPM_SCOPE.replace(/^@/, '');
48
+ return (
49
+ [
50
+ 'nodeLinker: node-modules',
51
+ '',
52
+ 'npmScopes:',
53
+ ` ${scopeKey}:`,
54
+ ` npmRegistryServer: "${brokerNpmRegistryUrl()}"`,
55
+ ` npmAuthToken: "\${${Z2H_NPM_TOKEN_ENV}}"`,
56
+ ' npmAlwaysAuth: true',
57
+ '',
58
+ `npmRegistryServer: "${PUBLIC_NPM_REGISTRY_URL}"`,
59
+ '',
60
+ 'yarnPath: .yarn/releases/yarn-4.5.1.cjs',
61
+ ].join('\n') + '\n'
62
+ );
63
+ }
@@ -0,0 +1,28 @@
1
+ import { tryReadAuthTokenSync } from '../auth/auth-store';
2
+ import { info } from '../logger';
3
+ import { generateZ2HTokenCommand } from '../../commands/generate-z2h-token';
4
+
5
+ export interface EnsureZ2hTokenOptions {
6
+ /**
7
+ * Re-run the token flow even if a token already exists on disk. Used after a
8
+ * broker 401, where the stored token is present but expired/invalid.
9
+ */
10
+ forceRefresh?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Ensure a z2h JWT exists at `~/.zth/auth.json` before an install-bearing
15
+ * command runs. If none is present (or `forceRefresh` is set), drive the
16
+ * existing `generate-z2h-token` flow — loopback capture with manual-paste
17
+ * fallback — which writes the token. No-ops when a token already exists and no
18
+ * refresh was requested.
19
+ *
20
+ * This is the install preflight: it keeps a friendly auth prompt in front of
21
+ * the raw npm error a non-technical user would otherwise hit.
22
+ */
23
+ export async function ensureZ2hToken(opts: EnsureZ2hTokenOptions = {}): Promise<void> {
24
+ if (opts.forceRefresh || !tryReadAuthTokenSync()) {
25
+ info('you need to sign in to install monday packages — opening the sign-in page');
26
+ await generateZ2HTokenCommand();
27
+ }
28
+ }
@@ -0,0 +1,84 @@
1
+ import { execa, type Options as ExecaOptions } from 'execa';
2
+ import { info, warn } from '../logger';
3
+ import { tryReadAuthTokenSync } from '../auth/auth-store';
4
+ import { Z2H_NPM_TOKEN_ENV } from '../../constants';
5
+ import { ensureZ2hToken } from './ensure-token';
6
+
7
+ // Substrings that, in a failed install's output, indicate the broker rejected
8
+ // the z2h JWT (expired/invalid). npm surfaces `E401`/`code E401`; yarn berry
9
+ // surfaces an HTTP 401 line. We re-auth on any of these rather than dumping a
10
+ // raw npm error on a non-technical user.
11
+ const AUTH_FAILURE_MARKERS = ['401', 'e401', 'unauthorized', 'authentication'];
12
+
13
+ function looksLikeAuthFailure(text: string): boolean {
14
+ const haystack = text.toLowerCase();
15
+ return AUTH_FAILURE_MARKERS.some(marker => haystack.includes(marker));
16
+ }
17
+
18
+ export interface RunYarnInstallOptions {
19
+ /** Directory to run `yarn install` in. */
20
+ cwd: string;
21
+ /** Extra env to merge on top of `process.env` + the broker token. */
22
+ extraEnv?: NodeJS.ProcessEnv;
23
+ }
24
+
25
+ /**
26
+ * Spawn `yarn install` in `cwd` with the npm-broker credential injected, while
27
+ * streaming yarn's output live to the user. stdout is inherited; stderr is
28
+ * piped so we can both echo it and inspect it for a broker auth failure.
29
+ *
30
+ * Returns the captured stderr on success; throws an Error tagged
31
+ * `isAuthFailure` when the failure output looks like a broker 401 so the
32
+ * caller can re-auth, or rethrows the raw execa error otherwise.
33
+ */
34
+ async function spawnYarnInstall(opts: RunYarnInstallOptions): Promise<void> {
35
+ const token = tryReadAuthTokenSync();
36
+ const env = { ...process.env, ...(token && { [Z2H_NPM_TOKEN_ENV]: token }), ...opts.extraEnv };
37
+ const child = execa('yarn', ['install'], {
38
+ cwd: opts.cwd,
39
+ stdout: 'inherit',
40
+ stderr: ['inherit', 'pipe'],
41
+ env,
42
+ } satisfies ExecaOptions);
43
+
44
+ try {
45
+ await child;
46
+ } catch (err) {
47
+ const e = err as Error & { stderr?: string };
48
+ const combined = `${e.stderr}\n${e.message ?? ''}`;
49
+ if (looksLikeAuthFailure(combined)) {
50
+ const authErr = new Error('broker rejected the z2h token during install') as Error & {
51
+ isAuthFailure: true;
52
+ };
53
+ authErr.isAuthFailure = true;
54
+ throw authErr;
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Run `yarn install` for an install-bearing command:
62
+ * 1. Preflight — ensure a z2h token exists (prompt `generate-z2h-token` if not).
63
+ * 2. Install through the broker.
64
+ * 3. On a broker 401, surface a friendly "your access expired — refreshing it"
65
+ * message, re-auth, and retry the install exactly once.
66
+ *
67
+ * A non-auth failure (real dependency error, network, etc.) propagates as-is so
68
+ * the command's normal error path reports it.
69
+ */
70
+ export async function runYarnInstall(opts: RunYarnInstallOptions): Promise<void> {
71
+ await ensureZ2hToken();
72
+
73
+ try {
74
+ await spawnYarnInstall(opts);
75
+ } catch (err) {
76
+ if (!(err as { isAuthFailure?: boolean }).isAuthFailure) {
77
+ throw err;
78
+ }
79
+ warn('your access expired — refreshing it now…');
80
+ await ensureZ2hToken({ forceRefresh: true });
81
+ info('access refreshed — retrying install');
82
+ await spawnYarnInstall(opts);
83
+ }
84
+ }
@@ -0,0 +1,25 @@
1
+ import os from 'node:os';
2
+
3
+ const DOORMAN_URL = 'https://track.bigbrain.me/prod/event';
4
+
5
+ const username = os.userInfo().username;
6
+
7
+ export function trackEvent(name: string, data: Record<string, unknown>): void {
8
+ void fetch(DOORMAN_URL, {
9
+ method: 'POST',
10
+ headers: { 'Content-Type': 'application/json' },
11
+ body: JSON.stringify({ name, source: 'z2h-cli', data: { username, ...data } }),
12
+ signal: AbortSignal.timeout(5000),
13
+ }).catch(() => {});
14
+ }
15
+
16
+ export async function trackDuration<T>(
17
+ eventName: string,
18
+ fn: () => Promise<T>,
19
+ data?: Record<string, unknown>
20
+ ): Promise<T> {
21
+ const start = Date.now();
22
+ const result = await fn();
23
+ trackEvent(eventName, { ...data, duration_ms: Date.now() - start });
24
+ return result;
25
+ }