@mondaydotcomorg/z2h-cli 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/commands/build.d.ts +1 -0
  2. package/dist/commands/build.d.ts.map +1 -1
  3. package/dist/commands/build.js +5 -1
  4. package/dist/commands/create-workspace.d.ts.map +1 -1
  5. package/dist/commands/create-workspace.js +1 -0
  6. package/dist/commands/create.d.ts.map +1 -1
  7. package/dist/commands/create.js +16 -2
  8. package/dist/commands/deploy.d.ts.map +1 -1
  9. package/dist/commands/deploy.js +27 -28
  10. package/dist/commands/dev.d.ts.map +1 -1
  11. package/dist/commands/dev.js +29 -0
  12. package/dist/commands/generate-z2h-token.d.ts +16 -0
  13. package/dist/commands/generate-z2h-token.d.ts.map +1 -0
  14. package/dist/commands/generate-z2h-token.js +107 -0
  15. package/dist/constants.d.ts +8 -0
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +28 -0
  18. package/dist/esm/commands/build.d.ts +1 -0
  19. package/dist/esm/commands/build.d.ts.map +1 -1
  20. package/dist/esm/commands/build.mjs +5 -1
  21. package/dist/esm/commands/create-workspace.d.ts.map +1 -1
  22. package/dist/esm/commands/create-workspace.mjs +1 -0
  23. package/dist/esm/commands/create.d.ts.map +1 -1
  24. package/dist/esm/commands/create.mjs +16 -2
  25. package/dist/esm/commands/deploy.d.ts.map +1 -1
  26. package/dist/esm/commands/deploy.mjs +24 -26
  27. package/dist/esm/commands/dev.d.ts.map +1 -1
  28. package/dist/esm/commands/dev.mjs +25 -1
  29. package/dist/esm/commands/generate-z2h-token.d.ts +16 -0
  30. package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -0
  31. package/dist/esm/commands/generate-z2h-token.mjs +99 -0
  32. package/dist/esm/constants.d.ts +8 -0
  33. package/dist/esm/constants.d.ts.map +1 -1
  34. package/dist/esm/constants.mjs +21 -1
  35. package/dist/esm/index.mjs +15 -0
  36. package/dist/esm/shadow/scaffold.d.ts.map +1 -1
  37. package/dist/esm/shadow/scaffold.mjs +5 -0
  38. package/dist/esm/templates/app/claude-settings.mjs +15 -2
  39. package/dist/esm/util/auth/__tests__/auth-store.test.d.ts +2 -0
  40. package/dist/esm/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
  41. package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
  42. package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
  43. package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts +2 -0
  44. package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
  45. package/dist/esm/util/auth/__tests__/write-auth.test.d.ts +2 -0
  46. package/dist/esm/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
  47. package/dist/esm/util/auth/auth-store.d.ts +11 -0
  48. package/dist/esm/util/auth/auth-store.d.ts.map +1 -0
  49. package/dist/esm/util/auth/auth-store.mjs +29 -0
  50. package/dist/esm/util/auth/broker-credential-provider.d.ts +12 -0
  51. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -0
  52. package/dist/esm/util/auth/broker-credential-provider.mjs +76 -0
  53. package/dist/esm/util/auth/loopback-server.d.ts +12 -0
  54. package/dist/esm/util/auth/loopback-server.d.ts.map +1 -0
  55. package/dist/esm/util/auth/loopback-server.mjs +87 -0
  56. package/dist/esm/util/auth/write-auth.d.ts +11 -0
  57. package/dist/esm/util/auth/write-auth.d.ts.map +1 -0
  58. package/dist/esm/util/auth/write-auth.mjs +22 -0
  59. package/dist/esm/util/manifest.d.ts +1 -0
  60. package/dist/esm/util/manifest.d.ts.map +1 -1
  61. package/dist/esm/util/manifest.mjs +2 -1
  62. package/dist/esm/util/s3/client.d.ts +7 -8
  63. package/dist/esm/util/s3/client.d.ts.map +1 -1
  64. package/dist/esm/util/s3/client.mjs +15 -30
  65. package/dist/esm/util/s3/upload.d.ts.map +1 -1
  66. package/dist/esm/util/s3/upload.mjs +2 -1
  67. package/dist/index.js +15 -0
  68. package/dist/shadow/scaffold.d.ts.map +1 -1
  69. package/dist/shadow/scaffold.js +5 -0
  70. package/dist/templates/app/claude-settings.js +15 -1
  71. package/dist/util/auth/__tests__/auth-store.test.d.ts +2 -0
  72. package/dist/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
  73. package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
  74. package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
  75. package/dist/util/auth/__tests__/loopback-server.test.d.ts +2 -0
  76. package/dist/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
  77. package/dist/util/auth/__tests__/write-auth.test.d.ts +2 -0
  78. package/dist/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
  79. package/dist/util/auth/auth-store.d.ts +11 -0
  80. package/dist/util/auth/auth-store.d.ts.map +1 -0
  81. package/dist/util/auth/auth-store.js +39 -0
  82. package/dist/util/auth/broker-credential-provider.d.ts +12 -0
  83. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -0
  84. package/dist/util/auth/broker-credential-provider.js +80 -0
  85. package/dist/util/auth/loopback-server.d.ts +12 -0
  86. package/dist/util/auth/loopback-server.d.ts.map +1 -0
  87. package/dist/util/auth/loopback-server.js +94 -0
  88. package/dist/util/auth/write-auth.d.ts +11 -0
  89. package/dist/util/auth/write-auth.d.ts.map +1 -0
  90. package/dist/util/auth/write-auth.js +26 -0
  91. package/dist/util/manifest.d.ts +1 -0
  92. package/dist/util/manifest.d.ts.map +1 -1
  93. package/dist/util/manifest.js +5 -4
  94. package/dist/util/s3/client.d.ts +7 -8
  95. package/dist/util/s3/client.d.ts.map +1 -1
  96. package/dist/util/s3/client.js +14 -36
  97. package/dist/util/s3/upload.d.ts.map +1 -1
  98. package/dist/util/s3/upload.js +2 -1
  99. package/package.json +10 -1
  100. package/src/commands/__tests__/create.test.ts +3 -0
  101. package/src/commands/build.ts +6 -1
  102. package/src/commands/create-workspace.ts +1 -0
  103. package/src/commands/create.ts +19 -2
  104. package/src/commands/deploy.ts +27 -36
  105. package/src/commands/dev.ts +26 -1
  106. package/src/commands/generate-z2h-token.ts +124 -0
  107. package/src/constants.ts +25 -0
  108. package/src/index.ts +17 -0
  109. package/src/shadow/scaffold.ts +5 -0
  110. package/src/templates/app/claude-settings.json +12 -0
  111. package/src/templates/workspace/yarnlock +0 -0
  112. package/src/util/auth/__tests__/auth-store.test.ts +50 -0
  113. package/src/util/auth/__tests__/broker-credential-provider.test.ts +115 -0
  114. package/src/util/auth/__tests__/loopback-server.test.ts +85 -0
  115. package/src/util/auth/__tests__/write-auth.test.ts +79 -0
  116. package/src/util/auth/auth-store.ts +31 -0
  117. package/src/util/auth/broker-credential-provider.ts +93 -0
  118. package/src/util/auth/loopback-server.ts +103 -0
  119. package/src/util/auth/write-auth.ts +24 -0
  120. package/src/util/manifest.ts +3 -1
  121. package/src/util/s3/client.ts +15 -32
  122. package/src/util/s3/upload.ts +2 -1
@@ -6,6 +6,7 @@ const fsExtra = require('fs-extra');
6
6
  const mimeTypes = require('mime-types');
7
7
  const pMap = require('p-map');
8
8
  const clientS3 = require('@aws-sdk/client-s3');
9
+ const constants = require('../../constants.js');
9
10
  const util_s3_client = require('./client.js');
10
11
 
11
12
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -26,7 +27,7 @@ async function uploadDir(localDir, keyPrefix) {
26
27
  const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
27
28
  const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
28
29
  await s3.send(new clientS3.PutObjectCommand({
29
- Bucket: util_s3_client.BUCKET_WRITE_NAME,
30
+ Bucket: constants.BUCKET_WRITE_NAME,
30
31
  Key: key,
31
32
  Body: body,
32
33
  ContentType: contentType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,6 +10,11 @@
10
10
  "require": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
12
  },
13
+ "./constants": {
14
+ "import": "./dist/esm/constants.mjs",
15
+ "require": "./dist/constants.js",
16
+ "types": "./dist/constants.d.ts"
17
+ },
13
18
  "./package.json": "./package.json"
14
19
  },
15
20
  "nx": {
@@ -35,7 +40,9 @@
35
40
  "@aws-sdk/client-s3": "^3.706.0",
36
41
  "chokidar": "^3.6.0",
37
42
  "commander": "^12.1.0",
43
+ "cors": "^2.8.6",
38
44
  "execa": "^9.5.1",
45
+ "express": "^5.2.1",
39
46
  "fs-extra": "^11.2.0",
40
47
  "mime-types": "^2.1.35",
41
48
  "p-map": "^7.0.2",
@@ -44,6 +51,8 @@
44
51
  },
45
52
  "devDependencies": {
46
53
  "@mondaydotcomorg/trident-library": "^1.2.7",
54
+ "@types/cors": "^2.8.19",
55
+ "@types/express": "^5.0.6",
47
56
  "@types/fs-extra": "^11.0.4",
48
57
  "@types/mime-types": "^2.1.4",
49
58
  "@types/node": "^22.19.0",
@@ -5,11 +5,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import { execa } from 'execa';
7
7
  import { createCommand } from '../create';
8
+ import * as manifest from '../../util/manifest';
8
9
 
9
10
  vi.mock('execa', () => ({
10
11
  execa: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }),
11
12
  }));
12
13
 
14
+ vi.spyOn(manifest, 'readManifest').mockResolvedValue({});
15
+
13
16
  async function makeWorkspace(tmp: string): Promise<void> {
14
17
  await ensureDir(tmp);
15
18
  await writeJson(path.join(tmp, 'package.json'), {
@@ -5,6 +5,7 @@ import { prepareShadow } from '../shadow/prepare';
5
5
 
6
6
  export interface BuildOptions {
7
7
  json?: boolean;
8
+ publicUrl?: string;
8
9
  }
9
10
 
10
11
  export async function buildCommand(opts: BuildOptions): Promise<void> {
@@ -27,7 +28,11 @@ export async function buildCommand(opts: BuildOptions): Promise<void> {
27
28
  });
28
29
 
29
30
  info(`building "${ctx.appName}" via trident`);
30
- await execa('yarn', ['build'], { cwd: ctx.paths.shadowDir, stdio: 'inherit' });
31
+ await execa('yarn', ['build'], {
32
+ cwd: ctx.paths.shadowDir,
33
+ stdio: 'inherit',
34
+ env: { ...process.env, ...(opts.publicUrl ? { PUBLIC_URL: opts.publicUrl } : {}) },
35
+ });
31
36
 
32
37
  if (!(await pathExists(ctx.paths.shadowBuild))) {
33
38
  throw new Error(`Expected build output at ${ctx.paths.shadowBuild} — not found.`);
@@ -62,6 +62,7 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
62
62
  writeFile(path.join(home, 'package.json'), renderTemplate(pkgRaw, { __Z2H_CLI_VERSION__: cliVersion })),
63
63
  copy(path.join(wsTemplatesDir, 'gitignore'), path.join(home, '.gitignore')),
64
64
  copy(path.join(wsTemplatesDir, 'yarnrc.yml'), path.join(home, '.yarnrc.yml')),
65
+ copy(path.join(wsTemplatesDir, 'yarnlock'), path.join(home, 'yarn.lock')),
65
66
  copy(path.join(wsTemplatesDir, 'nvmrc'), path.join(home, '.nvmrc')),
66
67
  copy(path.join(wsTemplatesDir, 'npmrc'), path.join(home, '.npmrc')),
67
68
  copy(path.join(wsTemplatesDir, 'README.md'), path.join(home, 'README.md')),
@@ -5,6 +5,9 @@ import { execa } from 'execa';
5
5
  import { info, output, setOutputMode } from '../util/logger';
6
6
  import { assertWorkspace } from '../util/workspace';
7
7
  import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
8
+ import { readManifest } from '../util/manifest';
9
+ import { mfAppName } from '../util/app-name';
10
+ import { APP_NAME_PATTERN } from '../constants';
8
11
 
9
12
  export interface CreateOptions {
10
13
  template?: string;
@@ -12,7 +15,10 @@ export interface CreateOptions {
12
15
  json?: boolean;
13
16
  }
14
17
 
15
- const APP_NAME_RE = /^[a-z][a-z0-9-]{1,49}$/;
18
+ // Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
19
+ // /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
20
+ // platform page in the URL, so we refuse upfront.
21
+ const RESERVED_APP_NAMES = new Set(['generate-z2h-token']);
16
22
 
17
23
  /**
18
24
  * Convert kebab-case to PascalCase. `demo-app` → `DemoApp`.
@@ -45,7 +51,12 @@ export async function createCommand(appName: string, opts: CreateOptions): Promi
45
51
  const workspace = await assertWorkspace();
46
52
 
47
53
  // Validate app name.
48
- if (!APP_NAME_RE.test(appName)) {
54
+ if (RESERVED_APP_NAMES.has(appName)) {
55
+ throw new Error(
56
+ `App name "${appName}" is reserved by the Z2H host for a platform page under /bigbrain-vibe/${appName}. Pick a different name.`
57
+ );
58
+ }
59
+ if (!APP_NAME_PATTERN.test(appName)) {
49
60
  throw new Error(
50
61
  `App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
51
62
  `2-50 chars, starting with a letter.`
@@ -69,6 +80,12 @@ export async function createCommand(appName: string, opts: CreateOptions): Promi
69
80
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
70
81
  }
71
82
 
83
+ // Check for manifest collision (network call — after all local checks).
84
+ const manifest = await readManifest();
85
+ if (manifest[mfAppName(appName)]) {
86
+ throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
87
+ }
88
+
72
89
  // Compute substitutions.
73
90
  const vars = {
74
91
  __APP_NAME__: appName,
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
+ import os from 'node:os';
2
3
  import { pathExists, readJson } from 'fs-extra';
3
- import { execa } from 'execa';
4
- import { info, warn, output, setOutputMode } from '../util/logger';
4
+ import { info, output, setOutputMode } from '../util/logger';
5
5
  import { resolveConsumerPaths } from '../shadow/paths';
6
6
  import {
7
7
  nextVersion,
@@ -12,39 +12,22 @@ import {
12
12
  type ManifestEntry,
13
13
  } from '../util/manifest';
14
14
  import { uploadDir } from '../util/s3/upload';
15
- import { assertBucketExists, BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL } from '../util/s3/client';
15
+ import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL } from '../constants';
16
+ import { setupS3Client } from '../util/s3/client';
17
+ import { readZthAuth } from '../util/auth/auth-store';
16
18
  import { cleanAppName, mfAppName } from '../util/app-name';
17
19
  import { buildCommand } from './build';
18
20
 
19
- const APONO_BUNDLE = '<APONO_BUNDLE_NAME>'; // TODO: replace with actual bundle name once infra setup is complete
20
-
21
21
  export interface DeployOptions {
22
22
  dryRun?: boolean;
23
23
  json?: boolean;
24
24
  }
25
25
 
26
- async function requestAponoAccess(): Promise<void> {
27
- if (APONO_BUNDLE === '<APONO_BUNDLE_NAME>') {
28
- warn(
29
- `skipping Apono request — APONO_BUNDLE is still a placeholder ("${APONO_BUNDLE}"). Set it in deploy.ts before relying on auto-grant.`
30
- );
31
- return;
32
- }
33
- info(`requesting Apono access for bundle "${APONO_BUNDLE}"`);
34
- await execa('apono', ['bundles', 'grant', APONO_BUNDLE], { stdio: 'inherit' });
35
- }
36
-
37
26
  export async function deployCommand(opts: DeployOptions): Promise<void> {
38
27
  if (opts.json) {
39
28
  setOutputMode('json');
40
29
  }
41
30
 
42
- info('checking S3 bucket');
43
- await assertBucketExists();
44
-
45
- info('building');
46
- await buildCommand({ json: opts.json });
47
-
48
31
  const paths = resolveConsumerPaths(process.cwd());
49
32
  const consumerPkg = (await readJson(paths.consumerPkg)) as { name?: string };
50
33
  if (!consumerPkg.name) {
@@ -52,6 +35,20 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
52
35
  }
53
36
  const appName = mfAppName(consumerPkg.name);
54
37
 
38
+ // Validate the user's deploy token is on disk before doing any expensive
39
+ // work. Throws Z2hAuthMissingError with a remediation hint if not.
40
+ await readZthAuth();
41
+ setupS3Client({ appName });
42
+
43
+ info('reading current Z2H manifest');
44
+ const manifest = await readManifest();
45
+ const version = nextVersion(manifest, appName);
46
+ const keyPrefix = `${appName}/${version}`;
47
+ const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
48
+
49
+ info('building');
50
+ await buildCommand({ json: opts.json, publicUrl: url });
51
+
55
52
  if (!(await pathExists(paths.shadowBuild))) {
56
53
  throw new Error(`No build output at ${paths.shadowBuild} after build step.`);
57
54
  }
@@ -63,11 +60,13 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
63
60
  }
64
61
  const assetManifest = (await readJson(assetManifestPath)) as AssetManifestFile;
65
62
 
66
- info('reading current Z2H manifest');
67
- const manifest = await readManifest();
68
- const version = nextVersion(manifest, appName);
69
- const keyPrefix = `${appName}/${version}`;
70
- const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
63
+ const deployedBy = os.userInfo().username;
64
+ const entry: ManifestEntry = {
65
+ ...assetManifest,
66
+ version,
67
+ url,
68
+ deployedBy,
69
+ };
71
70
 
72
71
  if (opts.dryRun) {
73
72
  output(`[z2h-cli] dry-run: would upload to s3://${BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
@@ -78,22 +77,14 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
78
77
  bucket: BUCKET_WRITE_NAME,
79
78
  keyPrefix,
80
79
  url,
81
- entry: { ...assetManifest, version, url },
80
+ entry,
82
81
  });
83
82
  return;
84
83
  }
85
84
 
86
- await requestAponoAccess();
87
-
88
85
  info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
89
86
  await uploadDir(paths.shadowBuild, keyPrefix);
90
87
 
91
- const entry: ManifestEntry = {
92
- ...assetManifest,
93
- version,
94
- url,
95
- };
96
-
97
88
  info(`updating manifest entry "${appName}" → v${version}`);
98
89
  await writeManifest(upsertEntry(manifest, appName, entry));
99
90
 
@@ -1,4 +1,6 @@
1
- import { readJson } from 'fs-extra';
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { readJson, writeFile } from 'fs-extra';
2
4
  import { watch } from 'chokidar';
3
5
  import { execa } from 'execa';
4
6
  import { info, warn, errorLog, output, setOutputMode } from '../util/logger';
@@ -31,6 +33,29 @@ export async function devCommand(opts: DevOptions): Promise<void> {
31
33
  stdio: ['inherit', 'pipe', 'inherit'],
32
34
  });
33
35
 
36
+ const pidFile = path.join(ctx.paths.shadowDir, 'dev.pid');
37
+ if (server.pid) {
38
+ await writeFile(pidFile, String(server.pid));
39
+ }
40
+
41
+ const cleanupPid = () => {
42
+ try {
43
+ fs.unlinkSync(pidFile);
44
+ } catch (_) {
45
+ // file already gone — ignore
46
+ }
47
+ };
48
+ server.on('exit', cleanupPid);
49
+ process.on('exit', cleanupPid);
50
+ process.on('SIGINT', () => {
51
+ server.kill();
52
+ process.exit(0);
53
+ });
54
+ process.on('SIGTERM', () => {
55
+ server.kill();
56
+ process.exit(0);
57
+ });
58
+
34
59
  let readyPrinted = false;
35
60
  server.stdout?.on('data', (chunk: Buffer) => {
36
61
  process.stdout.write(chunk);
@@ -0,0 +1,124 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { execa } from 'execa';
4
+ import { info, warn, output, setOutputMode } from '../util/logger';
5
+ import { writeZthAuth, validateJwtPattern } from '../util/auth/write-auth';
6
+ import { startLoopbackServer } from '../util/auth/loopback-server';
7
+
8
+ export const DEFAULT_TOKEN_PAGE_URL = 'https://bigbrain.me/bigbrain-vibe/generate-z2h-token';
9
+ export const LOOPBACK_TIMEOUT_MS = 120_000;
10
+
11
+ export interface GenerateZ2HTokenOptions {
12
+ /**
13
+ * Skip browser auto-open AND skip the loopback callback. Prints the URL
14
+ * for the user to open manually, then waits for them to paste the token
15
+ * into the prompt. Use for SSH sessions, CI, or when the loopback can't
16
+ * reach the CLI for any reason.
17
+ */
18
+ noBrowser?: boolean;
19
+ /** Override the page URL — useful when pointing at staging. */
20
+ url?: string;
21
+ json?: boolean;
22
+ }
23
+
24
+ function browserOpenCommand(): string {
25
+ switch (process.platform) {
26
+ case 'darwin':
27
+ return 'open';
28
+ case 'win32':
29
+ return 'start';
30
+ default:
31
+ return 'xdg-open';
32
+ }
33
+ }
34
+
35
+ async function openInBrowser(url: string): Promise<void> {
36
+ await execa(browserOpenCommand(), [url], { stdio: 'ignore' });
37
+ }
38
+
39
+ async function promptForToken(): Promise<string> {
40
+ const rl = readline.createInterface({ input: stdin, output: stdout });
41
+ try {
42
+ const raw = await rl.question('Paste your token here, then press Enter:\n> ');
43
+ return raw.trim();
44
+ } finally {
45
+ rl.close();
46
+ }
47
+ }
48
+
49
+ function withQueryParam(url: string, key: string, value: string): string {
50
+ const sep = url.includes('?') ? '&' : '?';
51
+ return `${url}${sep}${key}=${encodeURIComponent(value)}`;
52
+ }
53
+
54
+ async function runManualFlow(pageUrl: string): Promise<string> {
55
+ info(
56
+ `open this URL in your browser to mint a Z2H deploy token:\n` +
57
+ ` ${pageUrl}\n\n` +
58
+ `1. Click "Generate token" on the page.\n` +
59
+ `2. Copy the token shown.\n` +
60
+ `3. Paste it below and press Enter.\n`
61
+ );
62
+ const token = await promptForToken();
63
+ if (!token) {
64
+ throw new Error('No token entered.');
65
+ }
66
+ return token;
67
+ }
68
+
69
+ async function runLoopbackFlow(pageUrl: string): Promise<string | null> {
70
+ const loopback = await startLoopbackServer();
71
+ const fullUrl = withQueryParam(pageUrl, 'cli_callback', loopback.callbackUrl);
72
+
73
+ info('opening your browser to mint a Z2H deploy token');
74
+ info(` ${fullUrl}`);
75
+
76
+ try {
77
+ await openInBrowser(fullUrl);
78
+ } catch {
79
+ warn('could not auto-open the browser. please visit the URL above manually.');
80
+ }
81
+
82
+ info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
83
+
84
+ try {
85
+ return await loopback.waitForToken(LOOPBACK_TIMEOUT_MS);
86
+ } catch {
87
+ return null;
88
+ } finally {
89
+ loopback.close();
90
+ }
91
+ }
92
+
93
+ export async function generateZ2HTokenCommand(opts: GenerateZ2HTokenOptions = {}): Promise<void> {
94
+ if (opts.json) {
95
+ setOutputMode('json');
96
+ }
97
+
98
+ const pageUrl = opts.url ?? process.env.Z2H_TOKEN_PAGE_URL ?? DEFAULT_TOKEN_PAGE_URL;
99
+
100
+ // --no-browser bypasses both auto-open and the loopback round-trip — the
101
+ // user opens the URL themselves and pastes the token. Useful when the
102
+ // loopback can't be reached (SSH, CI, locked-down browsers).
103
+ let token: string | null = null;
104
+ if (!opts.noBrowser) {
105
+ token = await runLoopbackFlow(pageUrl);
106
+ if (!token) {
107
+ warn('loopback flow timed out or was cancelled — falling back to manual paste.');
108
+ }
109
+ }
110
+ if (!token) {
111
+ token = await runManualFlow(pageUrl);
112
+ }
113
+
114
+ if (!validateJwtPattern(token)) {
115
+ throw new Error('That does not look like a JWT (expected three dot-separated segments).');
116
+ }
117
+
118
+ const { filePath } = await writeZthAuth({ token });
119
+
120
+ output(`[z2h-cli] token saved ✅ ${filePath}`, {
121
+ ok: true,
122
+ filePath,
123
+ });
124
+ }
package/src/constants.ts CHANGED
@@ -38,3 +38,28 @@ export const YARNRC_LINES = [
38
38
  "npmAuthToken: '${NPM_TOKEN}'",
39
39
  "npmRegistryServer: 'https://registry.npmjs.org/'",
40
40
  ];
41
+
42
+ // ─────────── Z2H deploy bucket / contract ─────────
43
+ // Source of truth for the bucket layout. The CLI uses these when configuring
44
+ // the S3 client + uploading; bigbrain-zth's STS broker imports them via
45
+ // `@mondaydotcomorg/z2h-cli/constants` to scope the per-deploy session
46
+ // policy to the same bucket + manifest key.
47
+
48
+ export const BUCKET_WRITE_NAME = 'prod-use1-bigbrain-zth-mf-assets';
49
+ export const REGION = 'us-east-1';
50
+ export const BUCKET_WRITE_URL = `https://${BUCKET_WRITE_NAME}.s3.${REGION}.amazonaws.com`;
51
+
52
+ export const BUCKET_READ_DNS_NAME = 'bigbrain-zth-mf-assets';
53
+ export const BUCKET_READ_DNS_URL = `https://${BUCKET_READ_DNS_NAME}.bigbrain.me`;
54
+
55
+ // No `.json` extension — TridentSkeleton's `mfUrl` API appends `/mf-assets`
56
+ // to the bucket base when fetching, so the manifest must live at this exact
57
+ // key. See microfrontends/mf-bigbrain-zth/.../ZeroToHeroHost.tsx.
58
+ export const MANIFEST_KEY = 'mf-assets';
59
+ export const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
60
+
61
+ // ─────────── App-name slug — backend & CLI both honor this ─────
62
+ // Lowercase kebab-case, must start with a letter, 2-50 chars total. The
63
+ // backend STS broker rejects anything else with 400; the CLI can validate
64
+ // upfront for friendlier errors.
65
+ export const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { cleanCommand } from './commands/clean';
5
5
  import { deployCommand } from './commands/deploy';
6
6
  import { createWorkspaceCommand } from './commands/create-workspace';
7
7
  import { createCommand } from './commands/create';
8
+ import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
8
9
  import { errorLog, fail } from './util/logger';
9
10
 
10
11
  async function main(): Promise<void> {
@@ -91,6 +92,22 @@ async function main(): Promise<void> {
91
92
  }
92
93
  });
93
94
 
95
+ program
96
+ .command('generate-z2h-token')
97
+ .description(
98
+ 'Open the bigbrain.me token page, capture the token via a loopback callback, and persist it to ~/.zth/auth.json'
99
+ )
100
+ .option('--no-browser', "don't auto-open the browser; print the URL and prompt for manual paste")
101
+ .option('--url <url>', 'Override the token page URL (e.g. for staging)')
102
+ .option('--json', 'emit structured JSON on stdout')
103
+ .action(async (opts: { noBrowser?: boolean; url?: string; json?: boolean }) => {
104
+ try {
105
+ await generateZ2HTokenCommand(opts);
106
+ } catch (err) {
107
+ fail(`generate-z2h-token failed: ${(err as Error).message}`, { command: 'generate-z2h-token' }, 1);
108
+ }
109
+ });
110
+
94
111
  program
95
112
  .command('create <app-name>')
96
113
  .description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
@@ -78,6 +78,11 @@ declare module '*.wasm' {
78
78
  const e: string;
79
79
  export default e;
80
80
  }
81
+
82
+ declare module '*.html?url' {
83
+ const src: string;
84
+ export default src;
85
+ }
81
86
  `;
82
87
 
83
88
  const TSCONFIG_TEMPLATE = {
@@ -3,5 +3,17 @@
3
3
  "allow": [
4
4
  "Skill(z2h:*)"
5
5
  ]
6
+ },
7
+ "hooks": {
8
+ "Stop": [
9
+ {
10
+ "hooks": [
11
+ {
12
+ "type": "command",
13
+ "command": "if [ -f .zth/dev.pid ]; then kill $(cat .zth/dev.pid) 2>/dev/null; rm .zth/dev.pid; fi"
14
+ }
15
+ ]
16
+ }
17
+ ]
6
18
  }
7
19
  }
File without changes
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
4
+ import { ensureDir, remove, writeJson } from 'fs-extra';
5
+
6
+ let scratchHome: string;
7
+ let originalHome: string | undefined;
8
+
9
+ async function reload() {
10
+ // Re-import the module so HOME-derived constants pick up our scratch dir.
11
+ return import(`../auth-store?bust=${Date.now()}`);
12
+ }
13
+
14
+ beforeEach(async () => {
15
+ originalHome = process.env.HOME;
16
+ scratchHome = path.join(os.tmpdir(), `z2h-auth-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
17
+ await ensureDir(scratchHome);
18
+ process.env.HOME = scratchHome;
19
+ });
20
+
21
+ afterEach(async () => {
22
+ if (originalHome === undefined) {
23
+ delete process.env.HOME;
24
+ } else {
25
+ process.env.HOME = originalHome;
26
+ }
27
+ await remove(scratchHome);
28
+ });
29
+
30
+ describe('readZthAuth', () => {
31
+ it('throws Z2hAuthMissingError with a remediation hint when the file is absent', async () => {
32
+ const { readZthAuth, Z2hAuthMissingError } = await reload();
33
+ await expect(readZthAuth()).rejects.toBeInstanceOf(Z2hAuthMissingError);
34
+ });
35
+
36
+ it('throws when the file exists but lacks a token', async () => {
37
+ const { readZthAuth, Z2H_AUTH_FILE } = await reload();
38
+ await ensureDir(path.dirname(Z2H_AUTH_FILE));
39
+ await writeJson(Z2H_AUTH_FILE, { somethingElse: true });
40
+ await expect(readZthAuth()).rejects.toThrow(/missing a "token"/);
41
+ });
42
+
43
+ it('returns the parsed token', async () => {
44
+ const { readZthAuth, Z2H_AUTH_FILE } = await reload();
45
+ await ensureDir(path.dirname(Z2H_AUTH_FILE));
46
+ await writeJson(Z2H_AUTH_FILE, { token: 'abc.def.ghi' });
47
+ const result = await readZthAuth();
48
+ expect(result).toEqual({ token: 'abc.def.ghi' });
49
+ });
50
+ });