@mondaydotcomorg/z2h-cli 0.28.0 → 0.29.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.
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF5F"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6F5F"}
@@ -58,11 +58,19 @@ async function createCommand(appName, opts = {}) {
58
58
  if (!(await isDirEmpty(appDir))) {
59
59
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
60
60
  }
61
- // Check the broker — fail fast if the app is already registered.
61
+ // Check the broker — fail fast if the app already has a real release.
62
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past:
63
+ // either it's this same name being re-created after a local wipe (we'd only
64
+ // see it at all if we still have access to it), or it genuinely doesn't
65
+ // exist yet and we claim it below.
62
66
  const existing = await util_broker_app.fetchApp(util_appName.mfAppName(appName));
63
- if (existing) {
67
+ if (existing?.deployed) {
64
68
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
65
69
  }
70
+ const gitRemote = util_git_remoteOptions.resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
71
+ if (!existing) {
72
+ await util_broker_app.claimApp(util_appName.mfAppName(appName), gitRemote);
73
+ }
66
74
  // Compute substitutions.
67
75
  const vars = {
68
76
  __APP_NAME__: appName,
@@ -87,7 +95,6 @@ async function createCommand(appName, opts = {}) {
87
95
  await fs.copy(opts.htmlFile, path__default.default.join(appDir, 'src', 'page.html'));
88
96
  }
89
97
  util_logger.info(`initializing git repo for ${appName}...`);
90
- const gitRemote = util_git_remoteOptions.resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
91
98
  await util_git_repo.initRepo(appDir, gitRemote);
92
99
  util_logger.info(`installing dependencies for ${appName}...`);
93
100
  await util_npm_install.runYarnInstall({ cwd: workspace });
@@ -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,CA6E3F"}
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"}
@@ -136,8 +136,9 @@ async function generateCommand(appName, opts) {
136
136
  if (!(await isDirEmpty(appDir))) {
137
137
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
138
138
  }
139
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past — only a real release blocks generate.
139
140
  const existing = await util_broker_app.fetchApp(util_appName.mfAppName(appName));
140
- if (existing) {
141
+ if (existing?.deployed) {
141
142
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
142
143
  }
143
144
  util_logger.info(`copying template "${template.id}" → ${appDir}`);
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF5F"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6F5F"}
@@ -8,7 +8,7 @@ import { runYarnInstall } from '../util/npm/install.mjs';
8
8
  import { initRepo } from '../util/git/repo.mjs';
9
9
  import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
10
10
  export { REMOTE_TYPES } from '../util/git/remote-options.mjs';
11
- import { fetchApp } from '../util/broker/app.mjs';
11
+ import { fetchApp, claimApp } from '../util/broker/app.mjs';
12
12
  import { validateAppName, mfAppName } from '../util/app-name.mjs';
13
13
  import { prepareShadow } from '../shadow/prepare.mjs';
14
14
 
@@ -53,11 +53,19 @@ async function createCommand(appName, opts = {}) {
53
53
  if (!(await isDirEmpty(appDir))) {
54
54
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
55
55
  }
56
- // Check the broker — fail fast if the app is already registered.
56
+ // Check the broker — fail fast if the app already has a real release.
57
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past:
58
+ // either it's this same name being re-created after a local wipe (we'd only
59
+ // see it at all if we still have access to it), or it genuinely doesn't
60
+ // exist yet and we claim it below.
57
61
  const existing = await fetchApp(mfAppName(appName));
58
- if (existing) {
62
+ if (existing?.deployed) {
59
63
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
60
64
  }
65
+ const gitRemote = resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
66
+ if (!existing) {
67
+ await claimApp(mfAppName(appName), gitRemote);
68
+ }
61
69
  // Compute substitutions.
62
70
  const vars = {
63
71
  __APP_NAME__: appName,
@@ -82,7 +90,6 @@ async function createCommand(appName, opts = {}) {
82
90
  await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
83
91
  }
84
92
  info(`initializing git repo for ${appName}...`);
85
- const gitRemote = resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
86
93
  await initRepo(appDir, gitRemote);
87
94
  info(`installing dependencies for ${appName}...`);
88
95
  await runYarnInstall({ cwd: workspace });
@@ -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,CA6E3F"}
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"}
@@ -128,8 +128,9 @@ async function generateCommand(appName, opts) {
128
128
  if (!(await isDirEmpty(appDir))) {
129
129
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
130
130
  }
131
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past — only a real release blocks generate.
131
132
  const existing = await fetchApp(mfAppName(appName));
132
- if (existing) {
133
+ if (existing?.deployed) {
133
134
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
134
135
  }
135
136
  info(`copying template "${template.id}" → ${appDir}`);
@@ -2,16 +2,26 @@ import type { ManifestEntry } from '../manifest';
2
2
  export interface AppInfo {
3
3
  appName: string;
4
4
  version: number;
5
- entry: Record<string, unknown>;
5
+ /** Null for a claimed-but-unpublished app — reserved via `create`, never deployed yet. */
6
+ entry: Record<string, unknown> | null;
7
+ /** True once a real release has landed (`entry` is set) — false for a claimed-but-unpublished stub. */
8
+ deployed: boolean;
6
9
  deployedBy: string;
7
10
  gitRemote: string;
8
11
  createdBy: string;
9
12
  }
10
13
  /**
11
14
  * Fetches a single app's current state from the broker.
12
- * Returns null if the app has never been deployed (404).
15
+ * Returns null if the app has never even been claimed (404).
13
16
  */
14
17
  export declare function fetchApp(appName: string): Promise<AppInfo | null>;
18
+ /**
19
+ * Reserves an app name (+ owner grant) before any deploy — called once from
20
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
21
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
22
+ * claimed the exact same name in the gap between the two calls.
23
+ */
24
+ export declare function claimApp(appName: string, gitRemote: string): Promise<void>;
15
25
  export interface RegisterOrUpdateOptions {
16
26
  appName: string;
17
27
  token: string;
@@ -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,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,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;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;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"}
@@ -6,7 +6,7 @@ import { tagDeployError, DeployError } from '../errors.mjs';
6
6
 
7
7
  /**
8
8
  * Fetches a single app's current state from the broker.
9
- * Returns null if the app has never been deployed (404).
9
+ * Returns null if the app has never even been claimed (404).
10
10
  */
11
11
  async function fetchApp(appName) {
12
12
  const { token } = await readZthAuth();
@@ -29,6 +29,24 @@ async function fetchApp(appName) {
29
29
  }
30
30
  return res.json();
31
31
  }
32
+ /**
33
+ * Reserves an app name (+ owner grant) before any deploy — called once from
34
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
35
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
36
+ * claimed the exact same name in the gap between the two calls.
37
+ */
38
+ async function claimApp(appName, gitRemote) {
39
+ const { token } = await readZthAuth();
40
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/claim`, {
41
+ method: 'POST',
42
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
43
+ body: JSON.stringify({ appName, gitRemote }),
44
+ });
45
+ if (!res.ok) {
46
+ const text = await res.text();
47
+ throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
48
+ }
49
+ }
32
50
  async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
33
51
  const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
34
52
  method: 'POST',
@@ -78,4 +96,4 @@ async function registerOrUpdateApp(opts) {
78
96
  }
79
97
  }
80
98
 
81
- export { fetchApp, registerOrUpdateApp };
99
+ export { claimApp, fetchApp, registerOrUpdateApp };
@@ -2,16 +2,26 @@ import type { ManifestEntry } from '../manifest';
2
2
  export interface AppInfo {
3
3
  appName: string;
4
4
  version: number;
5
- entry: Record<string, unknown>;
5
+ /** Null for a claimed-but-unpublished app — reserved via `create`, never deployed yet. */
6
+ entry: Record<string, unknown> | null;
7
+ /** True once a real release has landed (`entry` is set) — false for a claimed-but-unpublished stub. */
8
+ deployed: boolean;
6
9
  deployedBy: string;
7
10
  gitRemote: string;
8
11
  createdBy: string;
9
12
  }
10
13
  /**
11
14
  * Fetches a single app's current state from the broker.
12
- * Returns null if the app has never been deployed (404).
15
+ * Returns null if the app has never even been claimed (404).
13
16
  */
14
17
  export declare function fetchApp(appName: string): Promise<AppInfo | null>;
18
+ /**
19
+ * Reserves an app name (+ owner grant) before any deploy — called once from
20
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
21
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
22
+ * claimed the exact same name in the gap between the two calls.
23
+ */
24
+ export declare function claimApp(appName: string, gitRemote: string): Promise<void>;
15
25
  export interface RegisterOrUpdateOptions {
16
26
  appName: string;
17
27
  token: string;
@@ -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,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,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;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;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"}
@@ -8,7 +8,7 @@ const util_errors = require('../errors.js');
8
8
 
9
9
  /**
10
10
  * Fetches a single app's current state from the broker.
11
- * Returns null if the app has never been deployed (404).
11
+ * Returns null if the app has never even been claimed (404).
12
12
  */
13
13
  async function fetchApp(appName) {
14
14
  const { token } = await util_auth_authStore.readZthAuth();
@@ -31,6 +31,24 @@ async function fetchApp(appName) {
31
31
  }
32
32
  return res.json();
33
33
  }
34
+ /**
35
+ * Reserves an app name (+ owner grant) before any deploy — called once from
36
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
37
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
38
+ * claimed the exact same name in the gap between the two calls.
39
+ */
40
+ async function claimApp(appName, gitRemote) {
41
+ const { token } = await util_auth_authStore.readZthAuth();
42
+ const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/claim`, {
43
+ method: 'POST',
44
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
45
+ body: JSON.stringify({ appName, gitRemote }),
46
+ });
47
+ if (!res.ok) {
48
+ const text = await res.text();
49
+ throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
50
+ }
51
+ }
34
52
  async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
35
53
  const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
36
54
  method: 'POST',
@@ -80,5 +98,6 @@ async function registerOrUpdateApp(opts) {
80
98
  }
81
99
  }
82
100
 
101
+ exports.claimApp = claimApp;
83
102
  exports.fetchApp = fetchApp;
84
103
  exports.registerOrUpdateApp = registerOrUpdateApp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -52,7 +52,7 @@
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-s3": "^3.706.0",
54
54
  "@mondaydotcomorg/z2h-dashboard-templates": "^0.3.4",
55
- "@mondaydotcomorg/z2h-shared-utils": "^0.6.0",
55
+ "@mondaydotcomorg/z2h-shared-utils": "^0.6.1",
56
56
  "acorn": "^8.14.0",
57
57
  "acorn-walk": "^8.3.4",
58
58
  "chokidar": "^3.6.0",
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
  import { runYarnInstall } from '../../util/npm/install';
7
7
  import { initRepo, defaultS3Remote } from '../../util/git/repo';
8
8
  import { prepareShadow } from '../../shadow/prepare';
9
+ import { fetchApp, claimApp } from '../../util/broker/app';
9
10
  import { createCommand } from '../create';
10
11
 
11
12
  // Mock the install helper so `yarn install` (with its broker preflight) doesn't
@@ -28,6 +29,7 @@ vi.mock('../../shadow/prepare', () => ({
28
29
  // Mock broker app lookup — no token needed in tests; default to null (app does not exist).
29
30
  vi.mock('../../util/broker/app', () => ({
30
31
  fetchApp: vi.fn().mockResolvedValue(null),
32
+ claimApp: vi.fn().mockResolvedValue(undefined),
31
33
  }));
32
34
 
33
35
  async function makeWorkspace(tmp: string): Promise<void> {
@@ -50,6 +52,8 @@ describe('create', () => {
50
52
  vi.mocked(initRepo).mockClear();
51
53
  vi.mocked(defaultS3Remote).mockClear();
52
54
  vi.mocked(prepareShadow).mockClear();
55
+ vi.mocked(fetchApp).mockReset().mockResolvedValue(null);
56
+ vi.mocked(claimApp).mockReset().mockResolvedValue(undefined);
53
57
  });
54
58
 
55
59
  afterEach(async () => {
@@ -162,4 +166,46 @@ describe('create', () => {
162
166
  await createCommand('gh-app', { remote: 'github', repoUrl: 'https://github.com/acme/gh-app' });
163
167
  expect(vi.mocked(initRepo)).toHaveBeenCalledWith(path.join(tmp, 'gh-app'), 'https://github.com/acme/gh-app');
164
168
  });
169
+
170
+ it('claims the app name with the resolved gitRemote when the name is genuinely free', async () => {
171
+ await makeWorkspace(tmp);
172
+ await createCommand('my-app', { remote: 's3' });
173
+ expect(vi.mocked(claimApp)).toHaveBeenCalledWith(
174
+ 'mf-my-app',
175
+ 's3://prod-use1-bigbrain-zth-mf-assets/mf-my-app/git'
176
+ );
177
+ });
178
+
179
+ it('throws when the app already has a real release', async () => {
180
+ await makeWorkspace(tmp);
181
+ vi.mocked(fetchApp).mockResolvedValue({
182
+ appName: 'mf-demo-app',
183
+ version: 1,
184
+ entry: { version: 1, url: 'https://cdn/demo-app/1/' },
185
+ deployed: true,
186
+ deployedBy: 'x@x.com',
187
+ gitRemote: 's3://bucket/demo-app/git',
188
+ createdBy: 'x@x.com',
189
+ });
190
+ await expect(createCommand('demo-app', { remote: 's3' })).rejects.toThrow(/already exists/);
191
+ expect(claimApp).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it('proceeds without re-claiming when the app was already claimed but never published', async () => {
195
+ await makeWorkspace(tmp);
196
+ vi.mocked(fetchApp).mockResolvedValue({
197
+ appName: 'mf-demo-app',
198
+ version: 0,
199
+ entry: null,
200
+ deployed: false,
201
+ deployedBy: 'x@x.com',
202
+ gitRemote: 's3://bucket/demo-app/git-claim',
203
+ createdBy: 'x@x.com',
204
+ });
205
+
206
+ await createCommand('demo-app', { remote: 's3' });
207
+
208
+ expect(claimApp).not.toHaveBeenCalled();
209
+ expect(await pathExists(path.join(tmp, 'demo-app', 'package.json'))).toBe(true);
210
+ });
165
211
  });
@@ -78,6 +78,7 @@ describe('edit', () => {
78
78
  version: 3,
79
79
  gitRemote: S3_REMOTE,
80
80
  entry: {},
81
+ deployed: true,
81
82
  deployedBy: 'u@x.com',
82
83
  createdBy: 'u@x.com',
83
84
  });
@@ -100,6 +101,7 @@ describe('edit', () => {
100
101
  version: 3,
101
102
  gitRemote: S3_REMOTE,
102
103
  entry: {},
104
+ deployed: true,
103
105
  deployedBy: 'u@x.com',
104
106
  createdBy: 'u@x.com',
105
107
  });
@@ -117,6 +119,7 @@ describe('edit', () => {
117
119
  version: 3,
118
120
  gitRemote: S3_REMOTE,
119
121
  entry: {},
122
+ deployed: true,
120
123
  deployedBy: 'u@x.com',
121
124
  createdBy: 'u@x.com',
122
125
  });
@@ -134,6 +137,7 @@ describe('edit', () => {
134
137
  version: 3,
135
138
  gitRemote: GITHUB_REMOTE,
136
139
  entry: {},
140
+ deployed: true,
137
141
  deployedBy: 'u@x.com',
138
142
  createdBy: 'u@x.com',
139
143
  });
@@ -151,6 +155,7 @@ describe('edit', () => {
151
155
  version: 3,
152
156
  gitRemote: S3_REMOTE,
153
157
  entry: {},
158
+ deployed: true,
154
159
  deployedBy: 'u@x.com',
155
160
  createdBy: 'u@x.com',
156
161
  });
@@ -172,6 +177,7 @@ describe('edit', () => {
172
177
  version: 3,
173
178
  gitRemote: S3_REMOTE,
174
179
  entry: {},
180
+ deployed: true,
175
181
  deployedBy: 'u@x.com',
176
182
  createdBy: 'u@x.com',
177
183
  });
@@ -7,7 +7,7 @@ import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from
7
7
  import { runYarnInstall } from '../util/npm/install';
8
8
  import { initRepo } from '../util/git/repo';
9
9
  import { REMOTE_TYPES, type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
10
- import { fetchApp } from '../util/broker/app';
10
+ import { fetchApp, claimApp } from '../util/broker/app';
11
11
  import { mfAppName, validateAppName } from '../util/app-name';
12
12
  import { prepareShadow } from '../shadow/prepare';
13
13
 
@@ -69,12 +69,22 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
69
69
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
70
70
  }
71
71
 
72
- // Check the broker — fail fast if the app is already registered.
72
+ // Check the broker — fail fast if the app already has a real release.
73
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past:
74
+ // either it's this same name being re-created after a local wipe (we'd only
75
+ // see it at all if we still have access to it), or it genuinely doesn't
76
+ // exist yet and we claim it below.
73
77
  const existing = await fetchApp(mfAppName(appName));
74
- if (existing) {
78
+ if (existing?.deployed) {
75
79
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
76
80
  }
77
81
 
82
+ const gitRemote = resolveRemoteUrl(opts.remote as RemoteType, appName, opts.repoUrl);
83
+
84
+ if (!existing) {
85
+ await claimApp(mfAppName(appName), gitRemote);
86
+ }
87
+
78
88
  // Compute substitutions.
79
89
  const vars = {
80
90
  __APP_NAME__: appName,
@@ -105,7 +115,6 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
105
115
  }
106
116
 
107
117
  info(`initializing git repo for ${appName}...`);
108
- const gitRemote = resolveRemoteUrl(opts.remote as RemoteType, appName, opts.repoUrl);
109
118
  await initRepo(appDir, gitRemote);
110
119
 
111
120
  info(`installing dependencies for ${appName}...`);
@@ -157,8 +157,9 @@ export async function generateCommand(appName: string, opts: GenerateOptions): P
157
157
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
158
158
  }
159
159
 
160
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past — only a real release blocks generate.
160
161
  const existing = await fetchApp(mfAppName(appName));
161
- if (existing) {
162
+ if (existing?.deployed) {
162
163
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
163
164
  }
164
165
 
@@ -1,9 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { registerOrUpdateApp } from '../app';
2
+ import { registerOrUpdateApp, claimApp } from '../app';
3
3
  import * as promptVisibility from '../../prompt-visibility';
4
4
  import { DeployError } from '../../errors';
5
+ import * as authStore from '../../auth/auth-store';
5
6
  import type { ManifestEntry } from '../../manifest';
6
7
 
8
+ vi.spyOn(authStore, 'readZthAuth').mockResolvedValue({ token: 'jwt-stub' });
9
+
7
10
  vi.mock('../../prompt-visibility', () => ({
8
11
  resolveAppVisibility: vi.fn(),
9
12
  }));
@@ -105,3 +108,28 @@ describe('registerOrUpdateApp', () => {
105
108
  expect((err as DeployError).reason).toBe('register_error');
106
109
  });
107
110
  });
111
+
112
+ describe('claimApp', () => {
113
+ afterEach(() => {
114
+ vi.unstubAllGlobals();
115
+ });
116
+
117
+ it('posts the app name and gitRemote to the claim endpoint', async () => {
118
+ const fetchMock = mockFetchOnce(204);
119
+
120
+ await claimApp('mf-my-app', 's3://bucket/mf-my-app/git');
121
+
122
+ const [calledUrl, init] = fetchMock.mock.calls[0] as [string, RequestInit];
123
+ expect(calledUrl).toContain('/z2h-cli/apps/claim');
124
+ expect(JSON.parse(init.body as string)).toEqual({
125
+ appName: 'mf-my-app',
126
+ gitRemote: 's3://bucket/mf-my-app/git',
127
+ });
128
+ });
129
+
130
+ it('propagates a claim failure', async () => {
131
+ mockFetchOnce(409);
132
+
133
+ await expect(claimApp('mf-my-app', 's3://bucket/mf-my-app/git')).rejects.toThrow(/Claiming app name failed/);
134
+ });
135
+ });
@@ -8,7 +8,10 @@ import type { ManifestEntry } from '../manifest';
8
8
  export interface AppInfo {
9
9
  appName: string;
10
10
  version: number;
11
- entry: Record<string, unknown>;
11
+ /** Null for a claimed-but-unpublished app — reserved via `create`, never deployed yet. */
12
+ entry: Record<string, unknown> | null;
13
+ /** True once a real release has landed (`entry` is set) — false for a claimed-but-unpublished stub. */
14
+ deployed: boolean;
12
15
  deployedBy: string;
13
16
  gitRemote: string;
14
17
  createdBy: string;
@@ -16,7 +19,7 @@ export interface AppInfo {
16
19
 
17
20
  /**
18
21
  * Fetches a single app's current state from the broker.
19
- * Returns null if the app has never been deployed (404).
22
+ * Returns null if the app has never even been claimed (404).
20
23
  */
21
24
  export async function fetchApp(appName: string): Promise<AppInfo | null> {
22
25
  const { token } = await readZthAuth();
@@ -42,6 +45,25 @@ export async function fetchApp(appName: string): Promise<AppInfo | null> {
42
45
  return res.json() as Promise<AppInfo>;
43
46
  }
44
47
 
48
+ /**
49
+ * Reserves an app name (+ owner grant) before any deploy — called once from
50
+ * `create`. Idempotent from the caller's perspective: `create` only calls this
51
+ * when {@link fetchApp} found nothing at all, so a 409 here means someone else
52
+ * claimed the exact same name in the gap between the two calls.
53
+ */
54
+ export async function claimApp(appName: string, gitRemote: string): Promise<void> {
55
+ const { token } = await readZthAuth();
56
+ const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/claim`, {
57
+ method: 'POST',
58
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
59
+ body: JSON.stringify({ appName, gitRemote }),
60
+ });
61
+ if (!res.ok) {
62
+ const text = await res.text();
63
+ throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
64
+ }
65
+ }
66
+
45
67
  async function registerApp(
46
68
  appName: string,
47
69
  token: string,