@mondaydotcomorg/z2h-cli 0.29.4 → 0.30.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.
@@ -90,7 +90,7 @@ async function buildAndStamp(opts) {
90
90
  * register the release with the platform in one server-side transaction.
91
91
  */
92
92
  async function uploadAndRegister(opts) {
93
- const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } = opts;
93
+ const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
94
94
  util_logger.info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
95
95
  await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
96
96
  // `integrations` is inferred from the backend sources in the working tree and
@@ -104,9 +104,25 @@ async function uploadAndRegister(opts) {
104
104
  deployedBy: whoDeployed(),
105
105
  isPreview,
106
106
  });
107
- await util_broker_app.registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, integrations, description });
107
+ await util_broker_app.registerOrUpdateApp({
108
+ appName,
109
+ token,
110
+ nextVersion,
111
+ entry,
112
+ gitRemote,
113
+ integrations,
114
+ description,
115
+ masterAppName,
116
+ });
108
117
  }
109
118
  async function deployPreview({ paths, consumerName, appName, token, opts }) {
119
+ // A preview inherits from the live app it previews, resolved by name server-side —
120
+ // fail fast with an actionable message if that app was never claimed/deployed (e.g. a
121
+ // local checkout scaffolded before `create` started claiming names), rather than
122
+ // letting a deep backend 404 surface as a generic "App registration failed".
123
+ if (!(await util_broker_app.fetchApp(appName))) {
124
+ throw new util_errors.DeployError('register_error', `Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`);
125
+ }
110
126
  await fs.ensureDir(paths.shadowDir);
111
127
  const { previewId } = await shadow_prepare.resolvePortAndMetadata(paths, util_appName.cleanAppName(consumerName));
112
128
  const previewSlug = util_appName.buildPreviewSlug(consumerName, previewId);
@@ -138,6 +154,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
138
154
  entry,
139
155
  gitRemote: '',
140
156
  isPreview: true,
157
+ masterAppName: appName,
141
158
  });
142
159
  const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
143
160
  util_logger.output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA8E3F"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqF3F"}
@@ -141,6 +141,12 @@ async function generateCommand(appName, opts) {
141
141
  if (existing?.deployed) {
142
142
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
143
143
  }
144
+ // generate skips `create` entirely, but the preview it deploys below still resolves this
145
+ // app's own row by name server-side — claim it now so that lookup finds something, same as
146
+ // `create` does for the normal flow.
147
+ if (!existing) {
148
+ await util_broker_app.claimApp(util_appName.mfAppName(appName), util_git_repo.defaultS3Remote(appName));
149
+ }
144
150
  util_logger.info(`copying template "${template.id}" → ${appDir}`);
145
151
  // Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
146
152
  // needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
@@ -14,7 +14,7 @@ import { resolveGitEnv } from '../util/git/git-env.mjs';
14
14
  import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
15
15
  import { setupS3Client } from '../util/s3/client.mjs';
16
16
  import { getMachineAppName } from '../util/machine-identity.mjs';
17
- import { registerOrUpdateApp } from '../util/broker/app.mjs';
17
+ import { fetchApp, registerOrUpdateApp } from '../util/broker/app.mjs';
18
18
  import { openInBrowser } from '../util/open-browser.mjs';
19
19
  import { resolvePortAndMetadata } from '../shadow/prepare.mjs';
20
20
  import { tagDeployError, DeployError, grantReasonForStatus } from '../util/errors.mjs';
@@ -83,7 +83,7 @@ async function buildAndStamp(opts) {
83
83
  * register the release with the platform in one server-side transaction.
84
84
  */
85
85
  async function uploadAndRegister(opts) {
86
- const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } = opts;
86
+ const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
87
87
  info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
88
88
  await uploadDir(paths.shadowBuild, keyPrefix);
89
89
  // `integrations` is inferred from the backend sources in the working tree and
@@ -97,9 +97,25 @@ async function uploadAndRegister(opts) {
97
97
  deployedBy: whoDeployed(),
98
98
  isPreview,
99
99
  });
100
- await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, integrations, description });
100
+ await registerOrUpdateApp({
101
+ appName,
102
+ token,
103
+ nextVersion,
104
+ entry,
105
+ gitRemote,
106
+ integrations,
107
+ description,
108
+ masterAppName,
109
+ });
101
110
  }
102
111
  async function deployPreview({ paths, consumerName, appName, token, opts }) {
112
+ // A preview inherits from the live app it previews, resolved by name server-side —
113
+ // fail fast with an actionable message if that app was never claimed/deployed (e.g. a
114
+ // local checkout scaffolded before `create` started claiming names), rather than
115
+ // letting a deep backend 404 surface as a generic "App registration failed".
116
+ if (!(await fetchApp(appName))) {
117
+ throw new DeployError('register_error', `Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`);
118
+ }
103
119
  await ensureDir(paths.shadowDir);
104
120
  const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
105
121
  const previewSlug = buildPreviewSlug(consumerName, previewId);
@@ -131,6 +147,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
131
147
  entry,
132
148
  gitRemote: '',
133
149
  isPreview: true,
150
+ masterAppName: appName,
134
151
  });
135
152
  const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
136
153
  output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA8E3F"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqF3F"}
@@ -5,10 +5,10 @@ import { execa } from 'execa';
5
5
  import { pathExists, readdir } from 'fs-extra';
6
6
  import { assertWorkspace } from '../util/workspace.mjs';
7
7
  import { validateAppName, mfAppName, cleanAppName } from '../util/app-name.mjs';
8
- import { fetchApp } from '../util/broker/app.mjs';
8
+ import { fetchApp, claimApp } from '../util/broker/app.mjs';
9
9
  import { patchBuiltBundle, patchOneFile } from '../util/dashboard-templates/patch-bundle.mjs';
10
10
  import { findTemplate } from '../util/dashboard-templates/load-catalog.mjs';
11
- import { initRepo, defaultS3Remote } from '../util/git/repo.mjs';
11
+ import { defaultS3Remote, initRepo } from '../util/git/repo.mjs';
12
12
  import { info, output } from '../util/logger.mjs';
13
13
 
14
14
  // Must match zth-backend-runner's route pattern — fail at generate time, not at first invocation.
@@ -133,6 +133,12 @@ async function generateCommand(appName, opts) {
133
133
  if (existing?.deployed) {
134
134
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
135
135
  }
136
+ // generate skips `create` entirely, but the preview it deploys below still resolves this
137
+ // app's own row by name server-side — claim it now so that lookup finds something, same as
138
+ // `create` does for the normal flow.
139
+ if (!existing) {
140
+ await claimApp(mfAppName(appName), defaultS3Remote(appName));
141
+ }
136
142
  info(`copying template "${template.id}" → ${appDir}`);
137
143
  // Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
138
144
  // needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
@@ -30,6 +30,8 @@ export interface RegisterOrUpdateOptions {
30
30
  gitRemote: string;
31
31
  integrations?: string[];
32
32
  description?: string;
33
+ /** For a preview app's first registration, the real app it previews. Ignored past v1. */
34
+ masterAppName?: string;
33
35
  }
34
36
  /**
35
37
  * v1 creates the app row (+ owner grant, prompting for visibility); later
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA6CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtF"}
@@ -47,11 +47,11 @@ async function claimApp(appName, gitRemote) {
47
47
  throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
48
48
  }
49
49
  }
50
- async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
50
+ async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description, masterAppName) {
51
51
  const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
52
52
  method: 'POST',
53
53
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
54
- body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
54
+ body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
55
55
  });
56
56
  if (!res.ok) {
57
57
  const text = await res.text();
@@ -77,12 +77,12 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
77
77
  * deploy and preview so this branching logic lives in exactly one place.
78
78
  */
79
79
  async function registerOrUpdateApp(opts) {
80
- const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
80
+ const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
81
81
  try {
82
82
  if (nextVersion === 1) {
83
83
  const visibility = await resolveAppVisibility(appName);
84
84
  info(`registering "${appName}" → v${nextVersion} (${visibility})`);
85
- await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
85
+ await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description, masterAppName);
86
86
  }
87
87
  else {
88
88
  info(`updating "${appName}" → v${nextVersion}`);
@@ -30,6 +30,8 @@ export interface RegisterOrUpdateOptions {
30
30
  gitRemote: string;
31
31
  integrations?: string[];
32
32
  description?: string;
33
+ /** For a preview app's first registration, the real app it previews. Ignored past v1. */
34
+ masterAppName?: string;
33
35
  }
34
36
  /**
35
37
  * v1 creates the app row (+ owner grant, prompting for visibility); later
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA6CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtF"}
@@ -49,11 +49,11 @@ async function claimApp(appName, gitRemote) {
49
49
  throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
50
50
  }
51
51
  }
52
- async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
52
+ async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description, masterAppName) {
53
53
  const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
54
54
  method: 'POST',
55
55
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
56
- body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
56
+ body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
57
57
  });
58
58
  if (!res.ok) {
59
59
  const text = await res.text();
@@ -79,12 +79,12 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
79
79
  * deploy and preview so this branching logic lives in exactly one place.
80
80
  */
81
81
  async function registerOrUpdateApp(opts) {
82
- const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
82
+ const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
83
83
  try {
84
84
  if (nextVersion === 1) {
85
85
  const visibility = await util_promptVisibility.resolveAppVisibility(appName);
86
86
  util_logger.info(`registering "${appName}" → v${nextVersion} (${visibility})`);
87
- await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
87
+ await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description, masterAppName);
88
88
  }
89
89
  else {
90
90
  util_logger.info(`updating "${appName}" → v${nextVersion}`);
@@ -58,35 +58,32 @@ function hashQueryShape(rawSql: string): string {
58
58
  }
59
59
 
60
60
  /**
61
- * The permissions-aware pool returns different rows to different people, so the
62
- * cache key has to carry whatever decides that — the caller's roles and user
63
- * group, exactly as bigbrain-zth keys it. Get this wrong and one user is served
64
- * another's rows. The stateless pool reads as a fixed service account, so every
65
- * caller shares one entry there.
61
+ * The permissions-aware pool returns different rows to different people —
62
+ * including different rows to two people with the same roles/user_group, since
63
+ * Snowflake now enforces column/row-level grants per user. The cache key must
64
+ * therefore scope to the individual caller, exactly as bigbrain-zth keys it.
65
+ * The stateless pool reads as a fixed service account, so every caller shares
66
+ * one entry there.
66
67
  */
67
68
  /**
68
69
  * Returns the cache scope key for the current caller, or null when we must not
69
70
  * cache at all.
70
71
  *
71
- * On the permissions-aware path we hash the caller's roles + user_group, mirroring
72
+ * On the permissions-aware path we scope to the caller's user id, mirroring
72
73
  * bigbrain-zth exactly. If there is no execution context (e.g. the call arrived
73
74
  * outside a normal invoke flow) we fail **closed**: skipping both the cache read
74
75
  * and the write prevents an unauthenticated scope-collapse where every caller
75
- * would share one entry keyed by `hash('|')`.
76
+ * would share one entry.
76
77
  */
77
78
  function cacheScope(useStateless: boolean): string | null {
78
79
  if (useStateless) {
79
80
  return 'stateless';
80
81
  }
81
- const user = context.execution.get()?.user as { roles?: string[]; user_group?: string } | undefined;
82
- if (!user) {
82
+ const user = context.execution.get()?.user as { id?: number } | undefined;
83
+ if (!user?.id) {
83
84
  return null;
84
85
  }
85
- const roles = [...(user.roles ?? [])].sort((a, b) => a.localeCompare(b)).join(',');
86
- return createHash('sha256')
87
- .update(`${roles}|${user.user_group ?? ''}`)
88
- .digest('hex')
89
- .slice(0, 16);
86
+ return `user:${user.id}`;
90
87
  }
91
88
 
92
89
  /**
@@ -1,6 +1,6 @@
1
1
  import { ChannelError } from '../backend-runner.errors';
2
2
 
3
- export const UPSTREAM_TIMEOUT_MS = 15_000;
3
+ export const UPSTREAM_TIMEOUT_MS = 60_000;
4
4
  export const LLM_TIMEOUT_MS = 60_000;
5
5
  /**
6
6
  * Head-room added on top of an upstream's *own* budget when racing it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.29.4",
3
+ "version": "0.30.0",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -51,7 +51,7 @@
51
51
  ],
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-s3": "^3.706.0",
54
- "@mondaydotcomorg/z2h-dashboard-templates": "^0.3.5",
54
+ "@mondaydotcomorg/z2h-dashboard-templates": "^0.3.6",
55
55
  "@mondaydotcomorg/z2h-shared-utils": "^0.6.2",
56
56
  "acorn": "^8.14.0",
57
57
  "acorn-walk": "^8.3.4",
@@ -39,8 +39,21 @@ vi.mock('../../util/auth/broker-credential-provider', () => ({
39
39
  fetchDeployVersion: vi.fn(),
40
40
  }));
41
41
 
42
+ const { STUB_MASTER_APP } = vi.hoisted(() => ({
43
+ STUB_MASTER_APP: {
44
+ appName: 'mf-my-app',
45
+ version: 1,
46
+ entry: {},
47
+ deployed: true,
48
+ deployedBy: 'someone@x.com',
49
+ gitRemote: 's3://bucket/my-app/git',
50
+ createdBy: 'someone@x.com',
51
+ },
52
+ }));
53
+
42
54
  vi.mock('../../util/broker/app', () => ({
43
55
  registerOrUpdateApp: vi.fn().mockResolvedValue(undefined),
56
+ fetchApp: vi.fn().mockResolvedValue(STUB_MASTER_APP),
44
57
  }));
45
58
 
46
59
  vi.mock('../../util/git/repo', () => ({
@@ -90,6 +103,7 @@ describe('deploy --preview', () => {
90
103
  vi.mocked(brokerProvider.brokerCredentialProvider).mockClear();
91
104
  vi.mocked(brokerProvider.fetchDeployVersion).mockReset().mockResolvedValue(0);
92
105
  vi.mocked(brokerApp.registerOrUpdateApp).mockClear().mockResolvedValue(undefined);
106
+ vi.mocked(brokerApp.fetchApp).mockClear().mockResolvedValue(STUB_MASTER_APP);
93
107
  vi.mocked(gitRepo.guardCleanTree).mockClear();
94
108
  vi.mocked(gitRepo.pullAndResolve).mockClear();
95
109
  vi.mocked(gitRepo.pushToRemote).mockClear();
@@ -148,6 +162,7 @@ describe('deploy --preview', () => {
148
162
  entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 3, gitRemote: '' }),
149
163
  gitRemote: '',
150
164
  integrations: [],
165
+ masterAppName: 'mf-my-app',
151
166
  });
152
167
  });
153
168
 
@@ -165,6 +180,7 @@ describe('deploy --preview', () => {
165
180
  entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 1, gitRemote: '' }),
166
181
  gitRemote: '',
167
182
  integrations: [],
183
+ masterAppName: 'mf-my-app',
168
184
  });
169
185
  });
170
186
 
@@ -220,6 +236,18 @@ describe('deploy --preview', () => {
220
236
  expect((err as DeployError).reason).toBe('register_error');
221
237
  });
222
238
 
239
+ it('fails fast with an actionable message when the live app was never created/deployed', async () => {
240
+ vi.mocked(brokerApp.fetchApp).mockResolvedValue(null);
241
+
242
+ const err = await deployCommand({ preview: true }).catch((e: unknown) => e);
243
+
244
+ expect(err).toBeInstanceOf(DeployError);
245
+ expect((err as DeployError).reason).toBe('register_error');
246
+ expect((err as Error).message).toMatch(/never been created or deployed/);
247
+ expect(buildCommand).not.toHaveBeenCalled();
248
+ expect(brokerApp.registerOrUpdateApp).not.toHaveBeenCalled();
249
+ });
250
+
223
251
  it('rejects deploying a preview for an app already named with the preview prefix', async () => {
224
252
  await writeJson(path.join(tmp, 'package.json'), { name: 'zth-preview-abc123-my-app' });
225
253
  await expect(deployCommand({ preview: true })).rejects.toThrow(/reserved for the ephemeral preview mechanism/);
@@ -20,7 +20,7 @@ import { resolveGitEnv } from '../util/git/git-env';
20
20
  import { type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
21
21
  import { setupS3Client } from '../util/s3/client';
22
22
  import { getMachineAppName } from '../util/machine-identity';
23
- import { registerOrUpdateApp } from '../util/broker/app';
23
+ import { fetchApp, registerOrUpdateApp } from '../util/broker/app';
24
24
  import { openInBrowser } from '../util/open-browser';
25
25
  import { resolvePortAndMetadata } from '../shadow/prepare';
26
26
  import { DeployError, tagDeployError, grantReasonForStatus } from '../util/errors';
@@ -132,8 +132,10 @@ async function uploadAndRegister(opts: {
132
132
  gitRemote: string;
133
133
  isPreview: boolean;
134
134
  description?: string;
135
+ masterAppName?: string;
135
136
  }): Promise<void> {
136
- const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } = opts;
137
+ const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } =
138
+ opts;
137
139
 
138
140
  info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
139
141
  await uploadDir(paths.shadowBuild, keyPrefix);
@@ -150,10 +152,30 @@ async function uploadAndRegister(opts: {
150
152
  isPreview,
151
153
  });
152
154
 
153
- await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, integrations, description });
155
+ await registerOrUpdateApp({
156
+ appName,
157
+ token,
158
+ nextVersion,
159
+ entry,
160
+ gitRemote,
161
+ integrations,
162
+ description,
163
+ masterAppName,
164
+ });
154
165
  }
155
166
 
156
167
  async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
168
+ // A preview inherits from the live app it previews, resolved by name server-side —
169
+ // fail fast with an actionable message if that app was never claimed/deployed (e.g. a
170
+ // local checkout scaffolded before `create` started claiming names), rather than
171
+ // letting a deep backend 404 surface as a generic "App registration failed".
172
+ if (!(await fetchApp(appName))) {
173
+ throw new DeployError(
174
+ 'register_error',
175
+ `Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`
176
+ );
177
+ }
178
+
157
179
  await ensureDir(paths.shadowDir);
158
180
  const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
159
181
 
@@ -190,6 +212,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }: Depl
190
212
  entry,
191
213
  gitRemote: '',
192
214
  isPreview: true,
215
+ masterAppName: appName,
193
216
  });
194
217
 
195
218
  const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
@@ -5,7 +5,7 @@ import { execa } from 'execa';
5
5
  import { pathExists, readdir } from 'fs-extra';
6
6
  import { assertWorkspace } from '../util/workspace';
7
7
  import { validateAppName, cleanAppName, mfAppName } from '../util/app-name';
8
- import { fetchApp } from '../util/broker/app';
8
+ import { claimApp, fetchApp } from '../util/broker/app';
9
9
  import { patchBuiltBundle, patchOneFile, type PatchedConfig } from '../util/dashboard-templates/patch-bundle';
10
10
  import { findTemplate, type DashboardTemplateEntry } from '../util/dashboard-templates/load-catalog';
11
11
  import { initRepo, defaultS3Remote } from '../util/git/repo';
@@ -163,6 +163,13 @@ export async function generateCommand(appName: string, opts: GenerateOptions): P
163
163
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
164
164
  }
165
165
 
166
+ // generate skips `create` entirely, but the preview it deploys below still resolves this
167
+ // app's own row by name server-side — claim it now so that lookup finds something, same as
168
+ // `create` does for the normal flow.
169
+ if (!existing) {
170
+ await claimApp(mfAppName(appName), defaultS3Remote(appName));
171
+ }
172
+
166
173
  info(`copying template "${template.id}" → ${appDir}`);
167
174
  // Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
168
175
  // needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
@@ -72,12 +72,13 @@ async function registerApp(
72
72
  visibility: 'public' | 'private',
73
73
  gitRemote: string,
74
74
  integrations?: string[],
75
- description?: string
75
+ description?: string,
76
+ masterAppName?: string
76
77
  ): Promise<void> {
77
78
  const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
78
79
  method: 'POST',
79
80
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
80
- body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
81
+ body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
81
82
  });
82
83
  if (!res.ok) {
83
84
  const text = await res.text();
@@ -114,6 +115,8 @@ export interface RegisterOrUpdateOptions {
114
115
  gitRemote: string;
115
116
  integrations?: string[];
116
117
  description?: string;
118
+ /** For a preview app's first registration, the real app it previews. Ignored past v1. */
119
+ masterAppName?: string;
117
120
  }
118
121
 
119
122
  /**
@@ -123,13 +126,23 @@ export interface RegisterOrUpdateOptions {
123
126
  * deploy and preview so this branching logic lives in exactly one place.
124
127
  */
125
128
  export async function registerOrUpdateApp(opts: RegisterOrUpdateOptions): Promise<void> {
126
- const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
129
+ const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
127
130
 
128
131
  try {
129
132
  if (nextVersion === 1) {
130
133
  const visibility = await resolveAppVisibility(appName);
131
134
  info(`registering "${appName}" → v${nextVersion} (${visibility})`);
132
- await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
135
+ await registerApp(
136
+ appName,
137
+ token,
138
+ nextVersion,
139
+ entry,
140
+ visibility,
141
+ gitRemote,
142
+ integrations,
143
+ description,
144
+ masterAppName
145
+ );
133
146
  } else {
134
147
  info(`updating "${appName}" → v${nextVersion}`);
135
148
  await updateApp(appName, token, nextVersion, entry, gitRemote, integrations, description);