@mondaydotcomorg/z2h-cli 0.28.0 → 0.29.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.
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +10 -3
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +2 -1
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +11 -4
- package/dist/esm/commands/generate.d.ts.map +1 -1
- package/dist/esm/commands/generate.mjs +2 -1
- package/dist/esm/util/broker/app.d.ts +12 -2
- package/dist/esm/util/broker/app.d.ts.map +1 -1
- package/dist/esm/util/broker/app.mjs +20 -2
- package/dist/util/broker/app.d.ts +12 -2
- package/dist/util/broker/app.d.ts.map +1 -1
- package/dist/util/broker/app.js +20 -1
- package/package.json +1 -1
- package/src/commands/__tests__/create.test.ts +46 -0
- package/src/commands/__tests__/edit.test.ts +6 -0
- package/src/commands/create.ts +13 -4
- package/src/commands/generate.ts +2 -1
- package/src/util/broker/__tests__/app.test.ts +29 -1
- package/src/util/broker/app.ts +24 -2
|
@@ -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,
|
|
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"}
|
package/dist/commands/create.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
|
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"}
|
package/dist/util/broker/app.js
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
});
|
package/src/commands/create.ts
CHANGED
|
@@ -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
|
|
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}...`);
|
package/src/commands/generate.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/util/broker/app.ts
CHANGED
|
@@ -8,7 +8,10 @@ import type { ManifestEntry } from '../manifest';
|
|
|
8
8
|
export interface AppInfo {
|
|
9
9
|
appName: string;
|
|
10
10
|
version: number;
|
|
11
|
-
|
|
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
|
|
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,
|