@mondaydotcomorg/z2h-cli 0.12.1 → 0.13.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.
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +26 -0
- package/dist/commands/tag.d.ts +14 -0
- package/dist/commands/tag.d.ts.map +1 -0
- package/dist/commands/tag.js +114 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +27 -1
- package/dist/esm/commands/tag.d.ts +14 -0
- package/dist/esm/commands/tag.d.ts.map +1 -0
- package/dist/esm/commands/tag.mjs +107 -0
- package/dist/esm/index.mjs +20 -0
- package/dist/esm/util/tags.d.ts +24 -0
- package/dist/esm/util/tags.d.ts.map +1 -0
- package/dist/esm/util/tags.mjs +63 -0
- package/dist/index.js +20 -0
- package/dist/util/tags.d.ts +24 -0
- package/dist/util/tags.d.ts.map +1 -0
- package/dist/util/tags.js +69 -0
- package/package.json +1 -1
- package/src/commands/deploy.ts +31 -1
- package/src/commands/tag.ts +134 -0
- package/src/index.ts +21 -0
- package/src/templates/app/html-index.tsx.tmpl +22 -10
- package/src/util/tags.ts +94 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuJ3E"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -6,6 +6,7 @@ const node_readline = require('node:readline');
|
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
7
|
const util_logger = require('../util/logger.js');
|
|
8
8
|
const shadow_paths = require('../shadow/paths.js');
|
|
9
|
+
const util_tags = require('../util/tags.js');
|
|
9
10
|
const util_s3_upload = require('../util/s3/upload.js');
|
|
10
11
|
const constants = require('../constants.js');
|
|
11
12
|
const util_auth_brokerCredentialProvider = require('../util/auth/broker-credential-provider.js');
|
|
@@ -16,6 +17,7 @@ const util_git_repo = require('../util/git/repo.js');
|
|
|
16
17
|
const util_git_gitEnv = require('../util/git/git-env.js');
|
|
17
18
|
const util_git_remoteOptions = require('../util/git/remote-options.js');
|
|
18
19
|
const util_s3_client = require('../util/s3/client.js');
|
|
20
|
+
const commands_tag = require('./tag.js');
|
|
19
21
|
const commands_build = require('./build.js');
|
|
20
22
|
|
|
21
23
|
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
@@ -138,6 +140,9 @@ async function deployCommand(opts = {}) {
|
|
|
138
140
|
finally {
|
|
139
141
|
await releaseLockBestEffort(appName, token);
|
|
140
142
|
}
|
|
143
|
+
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
|
144
|
+
// tag failure never fails the deploy (handled inside).
|
|
145
|
+
await handleDeployTags(appName);
|
|
141
146
|
// The host MF parses the app name from the URL and prepends `mf-` itself
|
|
142
147
|
// before looking it up in the manifest. So the user-facing URL must use
|
|
143
148
|
// the consumer's de-scoped package name (no `mf-` prefix, no `@scope/`).
|
|
@@ -164,6 +169,27 @@ async function registerApp(appName, token, version, entry, visibility, gitRemote
|
|
|
164
169
|
throw new Error(`App registration failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
165
170
|
}
|
|
166
171
|
}
|
|
172
|
+
const TAG_NUDGE = "run 'z2h-cli tag' to categorize this app (app type & team)";
|
|
173
|
+
async function handleDeployTags(appSlug) {
|
|
174
|
+
try {
|
|
175
|
+
const interactive = util_logger.getOutputMode() === 'human' && Boolean(process.stdin.isTTY);
|
|
176
|
+
if (!interactive) {
|
|
177
|
+
util_logger.info(TAG_NUDGE);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const current = await util_tags.fetchAppTags(appSlug);
|
|
181
|
+
if (current.length > 0) {
|
|
182
|
+
return; // already tagged — don't prompt on every redeploy
|
|
183
|
+
}
|
|
184
|
+
const catalog = await util_tags.fetchTagCatalog();
|
|
185
|
+
const selected = await commands_tag.promptForTags(appSlug, catalog, current);
|
|
186
|
+
const tags = await util_tags.setAppTags(appSlug, selected);
|
|
187
|
+
util_logger.info(tags.length > 0 ? `tags set: ${tags.map(t => t.name).join(', ')}` : TAG_NUDGE);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
util_logger.warn(`could not update tags: ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
167
193
|
// Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
|
|
168
194
|
// so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
|
|
169
195
|
async function acquireDeployLock(appName, token) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AppTag, type CatalogEntry } from '../util/tags';
|
|
2
|
+
export interface TagOptions {
|
|
3
|
+
/** Repeatable `--tag` — sets the full tag set non-interactively. */
|
|
4
|
+
tag?: string[];
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Closed-list prompt: one numbered pick per dimension (app type, team). Enter
|
|
9
|
+
* keeps the current value for that dimension (or skips it if unset). Returns
|
|
10
|
+
* the full replacement set of tag names.
|
|
11
|
+
*/
|
|
12
|
+
export declare function promptForTags(appSlug: string, catalog: CatalogEntry[], current: AppTag[]): Promise<string[]>;
|
|
13
|
+
export declare function tagCommand(appArg: string | undefined, opts: TagOptions): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=tag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0ClH;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5F"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const readline = require('node:readline/promises');
|
|
4
|
+
const node_process = require('node:process');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const shadow_paths = require('../shadow/paths.js');
|
|
7
|
+
const util_appName = require('../util/app-name.js');
|
|
8
|
+
const util_logger = require('../util/logger.js');
|
|
9
|
+
const util_tags = require('../util/tags.js');
|
|
10
|
+
|
|
11
|
+
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
12
|
+
|
|
13
|
+
const readline__default = /*#__PURE__*/_interopDefault(readline);
|
|
14
|
+
|
|
15
|
+
/** Render an app's tags as `name (Type), ...` — or `(none)`. */
|
|
16
|
+
function formatTags(tags) {
|
|
17
|
+
return tags.map(t => `${t.name} (${util_tags.TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the target app name: an explicit arg wins, else the consumer
|
|
21
|
+
* package.json `name` in the cwd. Normalized to the `mf-` bundle name the
|
|
22
|
+
* broker stores (via `mfAppName`), so the server finds the app.
|
|
23
|
+
*/
|
|
24
|
+
async function resolveAppSlug(appArg) {
|
|
25
|
+
if (appArg) {
|
|
26
|
+
return util_appName.mfAppName(appArg);
|
|
27
|
+
}
|
|
28
|
+
const paths = shadow_paths.resolveConsumerPaths(process.cwd());
|
|
29
|
+
let pkg;
|
|
30
|
+
try {
|
|
31
|
+
pkg = (await fs.readJson(paths.consumerPkg));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error('Could not read a consumer package.json in the current directory. ' +
|
|
35
|
+
'Either cd into your app folder or pass an app name: z2h-cli tag <app-name>.');
|
|
36
|
+
}
|
|
37
|
+
if (!pkg.name) {
|
|
38
|
+
throw new Error('Consumer package.json must have a "name" field (or pass an app name argument).');
|
|
39
|
+
}
|
|
40
|
+
return util_appName.mfAppName(pkg.name);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Closed-list prompt: one numbered pick per dimension (app type, team). Enter
|
|
44
|
+
* keeps the current value for that dimension (or skips it if unset). Returns
|
|
45
|
+
* the full replacement set of tag names.
|
|
46
|
+
*/
|
|
47
|
+
async function promptForTags(appSlug, catalog, current) {
|
|
48
|
+
const rl = readline__default.default.createInterface({ input: node_process.stdin, output: node_process.stdout });
|
|
49
|
+
const selected = [];
|
|
50
|
+
try {
|
|
51
|
+
util_logger.info(`Tagging "${appSlug}".`);
|
|
52
|
+
for (const type of util_tags.TAG_TYPES) {
|
|
53
|
+
const options = catalog.filter(t => t.type === type);
|
|
54
|
+
if (options.length === 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const currentName = current.find(c => c.type === type)?.name;
|
|
58
|
+
const label = util_tags.TAG_TYPE_LABELS[type];
|
|
59
|
+
util_logger.info(`\n${label}${currentName ? ` (current: ${currentName})` : ''}:`);
|
|
60
|
+
options.forEach((o, i) => util_logger.info(` ${i + 1}) ${o.name}`));
|
|
61
|
+
const raw = await rl.question(`Pick a ${label} (number or name; Enter to ${currentName ? 'keep current' : 'skip'}):\n> `);
|
|
62
|
+
const trimmed = raw.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
if (currentName) {
|
|
65
|
+
selected.push(currentName);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Accept either the 1-based number or the tag name (case-insensitive).
|
|
70
|
+
const idx = Number(trimmed) - 1;
|
|
71
|
+
const byNumber = Number.isInteger(idx) && idx >= 0 && idx < options.length ? options[idx] : undefined;
|
|
72
|
+
const chosen = byNumber ?? options.find(o => o.name.toLowerCase() === trimmed.toLowerCase());
|
|
73
|
+
if (chosen) {
|
|
74
|
+
selected.push(chosen.name);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
util_logger.info(`Invalid choice "${trimmed}" — ${currentName ? 'keeping current' : 'skipping'} ${label}.`);
|
|
78
|
+
if (currentName) {
|
|
79
|
+
selected.push(currentName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return selected;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
rl.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function tagCommand(appArg, opts) {
|
|
90
|
+
if (opts.json) {
|
|
91
|
+
util_logger.setOutputMode('json');
|
|
92
|
+
}
|
|
93
|
+
const appSlug = await resolveAppSlug(appArg);
|
|
94
|
+
// Non-interactive write: `--tag` flags set the full set directly.
|
|
95
|
+
if (opts.tag && opts.tag.length > 0) {
|
|
96
|
+
const tags = await util_tags.setAppTags(appSlug, opts.tag);
|
|
97
|
+
util_logger.output(`[z2h-cli] tags for "${appSlug}": ${formatTags(tags)}`, { ok: true, appName: appSlug, tags });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const [catalog, current] = await Promise.all([util_tags.fetchTagCatalog(), util_tags.fetchAppTags(appSlug)]);
|
|
101
|
+
// Read-only listing: agents (`--json`) and any non-TTY caller get the catalog
|
|
102
|
+
// + current tags without a prompt — then they set tags via `--tag`. This also
|
|
103
|
+
// avoids hanging on `readline` when no human is attached to stdin.
|
|
104
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
105
|
+
util_logger.output(`[z2h-cli] "${appSlug}"\n catalog: ${catalog.map(t => `${t.name} (${util_tags.TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)'}\n current: ${formatTags(current)}`, { ok: true, appName: appSlug, catalog, current });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const selected = await promptForTags(appSlug, catalog, current);
|
|
109
|
+
const tags = await util_tags.setAppTags(appSlug, selected);
|
|
110
|
+
util_logger.output(`[z2h-cli] tags for "${appSlug}": ${formatTags(tags)}`, { ok: true, appName: appSlug, tags });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
exports.promptForTags = promptForTags;
|
|
114
|
+
exports.tagCommand = tagCommand;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuJ3E"}
|
|
@@ -2,8 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { readJson, pathExists } from 'fs-extra';
|
|
5
|
-
import { setOutputMode, info, output, warn } from '../util/logger.mjs';
|
|
5
|
+
import { setOutputMode, info, output, getOutputMode, warn } from '../util/logger.mjs';
|
|
6
6
|
import { resolveConsumerPaths } from '../shadow/paths.mjs';
|
|
7
|
+
import { fetchAppTags, fetchTagCatalog, setAppTags } from '../util/tags.mjs';
|
|
7
8
|
import { uploadDir } from '../util/s3/upload.mjs';
|
|
8
9
|
import { BUCKET_WRITE_NAME, brokerBaseUrl, routingHeaders, BUCKET_READ_DNS_URL } from '../constants.mjs';
|
|
9
10
|
import { fetchDeployVersion, brokerCredentialProvider } from '../util/auth/broker-credential-provider.mjs';
|
|
@@ -14,6 +15,7 @@ import { getRemoteUrl, initRepo, guardCleanTree, pullAndResolve, pushToRemote }
|
|
|
14
15
|
import { resolveGitEnv } from '../util/git/git-env.mjs';
|
|
15
16
|
import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
|
|
16
17
|
import { setupS3Client } from '../util/s3/client.mjs';
|
|
18
|
+
import { promptForTags } from './tag.mjs';
|
|
17
19
|
import { buildCommand } from './build.mjs';
|
|
18
20
|
|
|
19
21
|
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
@@ -131,6 +133,9 @@ async function deployCommand(opts = {}) {
|
|
|
131
133
|
finally {
|
|
132
134
|
await releaseLockBestEffort(appName, token);
|
|
133
135
|
}
|
|
136
|
+
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
|
137
|
+
// tag failure never fails the deploy (handled inside).
|
|
138
|
+
await handleDeployTags(appName);
|
|
134
139
|
// The host MF parses the app name from the URL and prepends `mf-` itself
|
|
135
140
|
// before looking it up in the manifest. So the user-facing URL must use
|
|
136
141
|
// the consumer's de-scoped package name (no `mf-` prefix, no `@scope/`).
|
|
@@ -157,6 +162,27 @@ async function registerApp(appName, token, version, entry, visibility, gitRemote
|
|
|
157
162
|
throw new Error(`App registration failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
158
163
|
}
|
|
159
164
|
}
|
|
165
|
+
const TAG_NUDGE = "run 'z2h-cli tag' to categorize this app (app type & team)";
|
|
166
|
+
async function handleDeployTags(appSlug) {
|
|
167
|
+
try {
|
|
168
|
+
const interactive = getOutputMode() === 'human' && Boolean(process.stdin.isTTY);
|
|
169
|
+
if (!interactive) {
|
|
170
|
+
info(TAG_NUDGE);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const current = await fetchAppTags(appSlug);
|
|
174
|
+
if (current.length > 0) {
|
|
175
|
+
return; // already tagged — don't prompt on every redeploy
|
|
176
|
+
}
|
|
177
|
+
const catalog = await fetchTagCatalog();
|
|
178
|
+
const selected = await promptForTags(appSlug, catalog, current);
|
|
179
|
+
const tags = await setAppTags(appSlug, selected);
|
|
180
|
+
info(tags.length > 0 ? `tags set: ${tags.map(t => t.name).join(', ')}` : TAG_NUDGE);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
warn(`could not update tags: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
160
186
|
// Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
|
|
161
187
|
// so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
|
|
162
188
|
async function acquireDeployLock(appName, token) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AppTag, type CatalogEntry } from '../util/tags';
|
|
2
|
+
export interface TagOptions {
|
|
3
|
+
/** Repeatable `--tag` — sets the full tag set non-interactively. */
|
|
4
|
+
tag?: string[];
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Closed-list prompt: one numbered pick per dimension (app type, team). Enter
|
|
9
|
+
* keeps the current value for that dimension (or skips it if unset). Returns
|
|
10
|
+
* the full replacement set of tag names.
|
|
11
|
+
*/
|
|
12
|
+
export declare function promptForTags(appSlug: string, catalog: CatalogEntry[], current: AppTag[]): Promise<string[]>;
|
|
13
|
+
export declare function tagCommand(appArg: string | undefined, opts: TagOptions): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=tag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0ClH;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5F"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { readJson } from 'fs-extra';
|
|
4
|
+
import { resolveConsumerPaths } from '../shadow/paths.mjs';
|
|
5
|
+
import { mfAppName } from '../util/app-name.mjs';
|
|
6
|
+
import { info, setOutputMode, output } from '../util/logger.mjs';
|
|
7
|
+
import { TAG_TYPES, TAG_TYPE_LABELS, setAppTags, fetchTagCatalog, fetchAppTags } from '../util/tags.mjs';
|
|
8
|
+
|
|
9
|
+
/** Render an app's tags as `name (Type), ...` — or `(none)`. */
|
|
10
|
+
function formatTags(tags) {
|
|
11
|
+
return tags.map(t => `${t.name} (${TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the target app name: an explicit arg wins, else the consumer
|
|
15
|
+
* package.json `name` in the cwd. Normalized to the `mf-` bundle name the
|
|
16
|
+
* broker stores (via `mfAppName`), so the server finds the app.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveAppSlug(appArg) {
|
|
19
|
+
if (appArg) {
|
|
20
|
+
return mfAppName(appArg);
|
|
21
|
+
}
|
|
22
|
+
const paths = resolveConsumerPaths(process.cwd());
|
|
23
|
+
let pkg;
|
|
24
|
+
try {
|
|
25
|
+
pkg = (await readJson(paths.consumerPkg));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error('Could not read a consumer package.json in the current directory. ' +
|
|
29
|
+
'Either cd into your app folder or pass an app name: z2h-cli tag <app-name>.');
|
|
30
|
+
}
|
|
31
|
+
if (!pkg.name) {
|
|
32
|
+
throw new Error('Consumer package.json must have a "name" field (or pass an app name argument).');
|
|
33
|
+
}
|
|
34
|
+
return mfAppName(pkg.name);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Closed-list prompt: one numbered pick per dimension (app type, team). Enter
|
|
38
|
+
* keeps the current value for that dimension (or skips it if unset). Returns
|
|
39
|
+
* the full replacement set of tag names.
|
|
40
|
+
*/
|
|
41
|
+
async function promptForTags(appSlug, catalog, current) {
|
|
42
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
43
|
+
const selected = [];
|
|
44
|
+
try {
|
|
45
|
+
info(`Tagging "${appSlug}".`);
|
|
46
|
+
for (const type of TAG_TYPES) {
|
|
47
|
+
const options = catalog.filter(t => t.type === type);
|
|
48
|
+
if (options.length === 0) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const currentName = current.find(c => c.type === type)?.name;
|
|
52
|
+
const label = TAG_TYPE_LABELS[type];
|
|
53
|
+
info(`\n${label}${currentName ? ` (current: ${currentName})` : ''}:`);
|
|
54
|
+
options.forEach((o, i) => info(` ${i + 1}) ${o.name}`));
|
|
55
|
+
const raw = await rl.question(`Pick a ${label} (number or name; Enter to ${currentName ? 'keep current' : 'skip'}):\n> `);
|
|
56
|
+
const trimmed = raw.trim();
|
|
57
|
+
if (!trimmed) {
|
|
58
|
+
if (currentName) {
|
|
59
|
+
selected.push(currentName);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Accept either the 1-based number or the tag name (case-insensitive).
|
|
64
|
+
const idx = Number(trimmed) - 1;
|
|
65
|
+
const byNumber = Number.isInteger(idx) && idx >= 0 && idx < options.length ? options[idx] : undefined;
|
|
66
|
+
const chosen = byNumber ?? options.find(o => o.name.toLowerCase() === trimmed.toLowerCase());
|
|
67
|
+
if (chosen) {
|
|
68
|
+
selected.push(chosen.name);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
info(`Invalid choice "${trimmed}" — ${currentName ? 'keeping current' : 'skipping'} ${label}.`);
|
|
72
|
+
if (currentName) {
|
|
73
|
+
selected.push(currentName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
rl.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function tagCommand(appArg, opts) {
|
|
84
|
+
if (opts.json) {
|
|
85
|
+
setOutputMode('json');
|
|
86
|
+
}
|
|
87
|
+
const appSlug = await resolveAppSlug(appArg);
|
|
88
|
+
// Non-interactive write: `--tag` flags set the full set directly.
|
|
89
|
+
if (opts.tag && opts.tag.length > 0) {
|
|
90
|
+
const tags = await setAppTags(appSlug, opts.tag);
|
|
91
|
+
output(`[z2h-cli] tags for "${appSlug}": ${formatTags(tags)}`, { ok: true, appName: appSlug, tags });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const [catalog, current] = await Promise.all([fetchTagCatalog(), fetchAppTags(appSlug)]);
|
|
95
|
+
// Read-only listing: agents (`--json`) and any non-TTY caller get the catalog
|
|
96
|
+
// + current tags without a prompt — then they set tags via `--tag`. This also
|
|
97
|
+
// avoids hanging on `readline` when no human is attached to stdin.
|
|
98
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
99
|
+
output(`[z2h-cli] "${appSlug}"\n catalog: ${catalog.map(t => `${t.name} (${TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)'}\n current: ${formatTags(current)}`, { ok: true, appName: appSlug, catalog, current });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const selected = await promptForTags(appSlug, catalog, current);
|
|
103
|
+
const tags = await setAppTags(appSlug, selected);
|
|
104
|
+
output(`[z2h-cli] tags for "${appSlug}": ${formatTags(tags)}`, { ok: true, appName: appSlug, tags });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { promptForTags, tagCommand };
|
package/dist/esm/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { createWorkspaceCommand } from './commands/create-workspace.mjs';
|
|
|
13
13
|
import { createCommand } from './commands/create.mjs';
|
|
14
14
|
import { generateZ2HTokenCommand } from './commands/generate-z2h-token.mjs';
|
|
15
15
|
import { migrateCommand } from './commands/migrate.mjs';
|
|
16
|
+
import { tagCommand } from './commands/tag.mjs';
|
|
16
17
|
import { editCommand } from './commands/edit.mjs';
|
|
17
18
|
import { fail, errorLog, getOutputMode } from './util/logger.mjs';
|
|
18
19
|
import { trackDuration } from './util/tracker.mjs';
|
|
@@ -37,6 +38,10 @@ function stripConflictingEnvVars() {
|
|
|
37
38
|
delete process.env[name];
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
/** commander collector for a repeatable `--tag` option. */
|
|
42
|
+
function collectTag(value, previous) {
|
|
43
|
+
return previous.concat(value);
|
|
44
|
+
}
|
|
40
45
|
async function main() {
|
|
41
46
|
stripConflictingEnvVars();
|
|
42
47
|
const program = new Command();
|
|
@@ -109,6 +114,21 @@ async function main() {
|
|
|
109
114
|
await fail(`deploy failed: ${err.message}`, { command: 'deploy' }, 2);
|
|
110
115
|
}
|
|
111
116
|
});
|
|
117
|
+
program
|
|
118
|
+
.command('tag [app-name]')
|
|
119
|
+
.description("Set an app's category tags — app type & team (interactive unless --tag or --json is given)")
|
|
120
|
+
.option('--tag <tag>', 'set a tag (repeatable); full-replaces the tag set non-interactively', collectTag, [])
|
|
121
|
+
.option('--json', "read-only: print the popular catalog + the app's current tags as JSON (no prompt)")
|
|
122
|
+
.action(async (appName, opts) => {
|
|
123
|
+
try {
|
|
124
|
+
await trackDuration('z2h_app_tag_success', () => tagCommand(appName, opts), {
|
|
125
|
+
app_name: appName ?? getCwdAppName(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
await fail(`tag failed: ${err.message}`, { command: 'tag' }, 1);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
112
132
|
program
|
|
113
133
|
.command('create-workspace')
|
|
114
134
|
.description(`Bootstrap the ${getWorkspaceHome()}/ yarn workspace (one-time setup)`)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The dimension a tag categorizes an app by. */
|
|
2
|
+
export type TagType = 'app_type' | 'team';
|
|
3
|
+
/** The tag dimensions, in prompt order. */
|
|
4
|
+
export declare const TAG_TYPES: TagType[];
|
|
5
|
+
/** Human labels for each dimension (CLI prompts / output). */
|
|
6
|
+
export declare const TAG_TYPE_LABELS: Record<TagType, string>;
|
|
7
|
+
/** A catalog entry: one selectable tag with its dimension + global usage count. */
|
|
8
|
+
export interface CatalogEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
type: TagType;
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
/** A tag assigned to an app — name + dimension. */
|
|
14
|
+
export interface AppTag {
|
|
15
|
+
name: string;
|
|
16
|
+
type: TagType;
|
|
17
|
+
}
|
|
18
|
+
/** The full closed tag catalog (app types + teams), with usage counts. */
|
|
19
|
+
export declare function fetchTagCatalog(): Promise<CatalogEntry[]>;
|
|
20
|
+
/** Tags currently assigned to an app (bare slug). */
|
|
21
|
+
export declare function fetchAppTags(appSlug: string): Promise<AppTag[]>;
|
|
22
|
+
/** Full-replace an app's tag set; returns the resulting tags. */
|
|
23
|
+
export declare function setAppTags(appSlug: string, tags: string[]): Promise<AppTag[]>;
|
|
24
|
+
//# sourceMappingURL=tags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../../src/util/tags.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAE1C,2CAA2C;AAC3C,eAAO,MAAM,SAAS,EAAE,OAAO,EAAyB,CAAC;AAEzD,8DAA8D;AAC9D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAGnD,CAAC;AAEF,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf;AAgDD,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAI/D;AAED,qDAAqD;AACrD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIrE;AAED,iEAAiE;AACjE,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAInF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { brokerBaseUrl, routingHeaders } from '../constants.mjs';
|
|
2
|
+
import { readZthAuth } from './auth/auth-store.mjs';
|
|
3
|
+
|
|
4
|
+
/** The tag dimensions, in prompt order. */
|
|
5
|
+
const TAG_TYPES = ['app_type', 'team'];
|
|
6
|
+
/** Human labels for each dimension (CLI prompts / output). */
|
|
7
|
+
const TAG_TYPE_LABELS = {
|
|
8
|
+
app_type: 'App type',
|
|
9
|
+
team: 'Team',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Authenticated request to the bigbrain-zth tag endpoints. Mirrors the Bearer
|
|
13
|
+
* pattern used by the STS broker / lock-release calls, with network-error
|
|
14
|
+
* wrapping, auth-failure guidance, and response-body inclusion on failure.
|
|
15
|
+
*/
|
|
16
|
+
async function tagFetch(method, path, body) {
|
|
17
|
+
const { token } = await readZthAuth();
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await fetch(`${brokerBaseUrl()}${path}`, {
|
|
21
|
+
method,
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${token}`,
|
|
24
|
+
Accept: 'application/json',
|
|
25
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
26
|
+
...routingHeaders(),
|
|
27
|
+
},
|
|
28
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
throw new Error(`Z2H tag request failed: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
if (res.status === 401 || res.status === 403) {
|
|
36
|
+
throw new Error(`Z2H tag request rejected by the broker (${res.status}). Refresh your token by running 'z2h-cli generate-z2h-token'.`);
|
|
37
|
+
}
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
throw new Error(`Z2H tag request returned ${res.status}: ${text.slice(0, 1024)}`);
|
|
41
|
+
}
|
|
42
|
+
return res;
|
|
43
|
+
}
|
|
44
|
+
/** The full closed tag catalog (app types + teams), with usage counts. */
|
|
45
|
+
async function fetchTagCatalog() {
|
|
46
|
+
const res = await tagFetch('GET', '/z2h-cli/tags');
|
|
47
|
+
const data = (await res.json());
|
|
48
|
+
return data.tags ?? [];
|
|
49
|
+
}
|
|
50
|
+
/** Tags currently assigned to an app (bare slug). */
|
|
51
|
+
async function fetchAppTags(appSlug) {
|
|
52
|
+
const res = await tagFetch('GET', `/z2h-cli/tags/${encodeURIComponent(appSlug)}`);
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
return data.tags ?? [];
|
|
55
|
+
}
|
|
56
|
+
/** Full-replace an app's tag set; returns the resulting tags. */
|
|
57
|
+
async function setAppTags(appSlug, tags) {
|
|
58
|
+
const res = await tagFetch('PUT', `/z2h-cli/tags/${encodeURIComponent(appSlug)}`, { tags });
|
|
59
|
+
const data = (await res.json());
|
|
60
|
+
return data.tags ?? [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { TAG_TYPES, TAG_TYPE_LABELS, fetchAppTags, fetchTagCatalog, setAppTags };
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const commands_createWorkspace = require('./commands/create-workspace.js');
|
|
|
13
13
|
const commands_create = require('./commands/create.js');
|
|
14
14
|
const commands_generateZ2hToken = require('./commands/generate-z2h-token.js');
|
|
15
15
|
const commands_migrate = require('./commands/migrate.js');
|
|
16
|
+
const commands_tag = require('./commands/tag.js');
|
|
16
17
|
const commands_edit = require('./commands/edit.js');
|
|
17
18
|
const util_logger = require('./util/logger.js');
|
|
18
19
|
const util_tracker = require('./util/tracker.js');
|
|
@@ -43,6 +44,10 @@ function stripConflictingEnvVars() {
|
|
|
43
44
|
delete process.env[name];
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
/** commander collector for a repeatable `--tag` option. */
|
|
48
|
+
function collectTag(value, previous) {
|
|
49
|
+
return previous.concat(value);
|
|
50
|
+
}
|
|
46
51
|
async function main() {
|
|
47
52
|
stripConflictingEnvVars();
|
|
48
53
|
const program = new commander.Command();
|
|
@@ -115,6 +120,21 @@ async function main() {
|
|
|
115
120
|
await util_logger.fail(`deploy failed: ${err.message}`, { command: 'deploy' }, 2);
|
|
116
121
|
}
|
|
117
122
|
});
|
|
123
|
+
program
|
|
124
|
+
.command('tag [app-name]')
|
|
125
|
+
.description("Set an app's category tags — app type & team (interactive unless --tag or --json is given)")
|
|
126
|
+
.option('--tag <tag>', 'set a tag (repeatable); full-replaces the tag set non-interactively', collectTag, [])
|
|
127
|
+
.option('--json', "read-only: print the popular catalog + the app's current tags as JSON (no prompt)")
|
|
128
|
+
.action(async (appName, opts) => {
|
|
129
|
+
try {
|
|
130
|
+
await util_tracker.trackDuration('z2h_app_tag_success', () => commands_tag.tagCommand(appName, opts), {
|
|
131
|
+
app_name: appName ?? util_appName.getCwdAppName(),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
await util_logger.fail(`tag failed: ${err.message}`, { command: 'tag' }, 1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
118
138
|
program
|
|
119
139
|
.command('create-workspace')
|
|
120
140
|
.description(`Bootstrap the ${util_workspace.getWorkspaceHome()}/ yarn workspace (one-time setup)`)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The dimension a tag categorizes an app by. */
|
|
2
|
+
export type TagType = 'app_type' | 'team';
|
|
3
|
+
/** The tag dimensions, in prompt order. */
|
|
4
|
+
export declare const TAG_TYPES: TagType[];
|
|
5
|
+
/** Human labels for each dimension (CLI prompts / output). */
|
|
6
|
+
export declare const TAG_TYPE_LABELS: Record<TagType, string>;
|
|
7
|
+
/** A catalog entry: one selectable tag with its dimension + global usage count. */
|
|
8
|
+
export interface CatalogEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
type: TagType;
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
/** A tag assigned to an app — name + dimension. */
|
|
14
|
+
export interface AppTag {
|
|
15
|
+
name: string;
|
|
16
|
+
type: TagType;
|
|
17
|
+
}
|
|
18
|
+
/** The full closed tag catalog (app types + teams), with usage counts. */
|
|
19
|
+
export declare function fetchTagCatalog(): Promise<CatalogEntry[]>;
|
|
20
|
+
/** Tags currently assigned to an app (bare slug). */
|
|
21
|
+
export declare function fetchAppTags(appSlug: string): Promise<AppTag[]>;
|
|
22
|
+
/** Full-replace an app's tag set; returns the resulting tags. */
|
|
23
|
+
export declare function setAppTags(appSlug: string, tags: string[]): Promise<AppTag[]>;
|
|
24
|
+
//# sourceMappingURL=tags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../src/util/tags.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAE1C,2CAA2C;AAC3C,eAAO,MAAM,SAAS,EAAE,OAAO,EAAyB,CAAC;AAEzD,8DAA8D;AAC9D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAGnD,CAAC;AAEF,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf;AAgDD,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAI/D;AAED,qDAAqD;AACrD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIrE;AAED,iEAAiE;AACjE,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAInF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const constants = require('../constants.js');
|
|
4
|
+
const util_auth_authStore = require('./auth/auth-store.js');
|
|
5
|
+
|
|
6
|
+
/** The tag dimensions, in prompt order. */
|
|
7
|
+
const TAG_TYPES = ['app_type', 'team'];
|
|
8
|
+
/** Human labels for each dimension (CLI prompts / output). */
|
|
9
|
+
const TAG_TYPE_LABELS = {
|
|
10
|
+
app_type: 'App type',
|
|
11
|
+
team: 'Team',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Authenticated request to the bigbrain-zth tag endpoints. Mirrors the Bearer
|
|
15
|
+
* pattern used by the STS broker / lock-release calls, with network-error
|
|
16
|
+
* wrapping, auth-failure guidance, and response-body inclusion on failure.
|
|
17
|
+
*/
|
|
18
|
+
async function tagFetch(method, path, body) {
|
|
19
|
+
const { token } = await util_auth_authStore.readZthAuth();
|
|
20
|
+
let res;
|
|
21
|
+
try {
|
|
22
|
+
res = await fetch(`${constants.brokerBaseUrl()}${path}`, {
|
|
23
|
+
method,
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
Accept: 'application/json',
|
|
27
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
28
|
+
...constants.routingHeaders(),
|
|
29
|
+
},
|
|
30
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
throw new Error(`Z2H tag request failed: ${message}`);
|
|
36
|
+
}
|
|
37
|
+
if (res.status === 401 || res.status === 403) {
|
|
38
|
+
throw new Error(`Z2H tag request rejected by the broker (${res.status}). Refresh your token by running 'z2h-cli generate-z2h-token'.`);
|
|
39
|
+
}
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
throw new Error(`Z2H tag request returned ${res.status}: ${text.slice(0, 1024)}`);
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
}
|
|
46
|
+
/** The full closed tag catalog (app types + teams), with usage counts. */
|
|
47
|
+
async function fetchTagCatalog() {
|
|
48
|
+
const res = await tagFetch('GET', '/z2h-cli/tags');
|
|
49
|
+
const data = (await res.json());
|
|
50
|
+
return data.tags ?? [];
|
|
51
|
+
}
|
|
52
|
+
/** Tags currently assigned to an app (bare slug). */
|
|
53
|
+
async function fetchAppTags(appSlug) {
|
|
54
|
+
const res = await tagFetch('GET', `/z2h-cli/tags/${encodeURIComponent(appSlug)}`);
|
|
55
|
+
const data = (await res.json());
|
|
56
|
+
return data.tags ?? [];
|
|
57
|
+
}
|
|
58
|
+
/** Full-replace an app's tag set; returns the resulting tags. */
|
|
59
|
+
async function setAppTags(appSlug, tags) {
|
|
60
|
+
const res = await tagFetch('PUT', `/z2h-cli/tags/${encodeURIComponent(appSlug)}`, { tags });
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
return data.tags ?? [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
exports.TAG_TYPES = TAG_TYPES;
|
|
66
|
+
exports.TAG_TYPE_LABELS = TAG_TYPE_LABELS;
|
|
67
|
+
exports.fetchAppTags = fetchAppTags;
|
|
68
|
+
exports.fetchTagCatalog = fetchTagCatalog;
|
|
69
|
+
exports.setAppTags = setAppTags;
|
package/package.json
CHANGED
package/src/commands/deploy.ts
CHANGED
|
@@ -2,8 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { pathExists, readJson } from 'fs-extra';
|
|
5
|
-
import { info, output, setOutputMode,
|
|
5
|
+
import { info, warn, output, setOutputMode, getOutputMode } from '../util/logger';
|
|
6
6
|
import { resolveConsumerPaths } from '../shadow/paths';
|
|
7
|
+
import { fetchAppTags, fetchTagCatalog, setAppTags } from '../util/tags';
|
|
7
8
|
import { type AssetManifestFile, type ManifestEntry } from '../util/manifest';
|
|
8
9
|
import { uploadDir } from '../util/s3/upload';
|
|
9
10
|
import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL, brokerBaseUrl, routingHeaders } from '../constants';
|
|
@@ -15,6 +16,7 @@ import { getRemoteUrl, guardCleanTree, pullAndResolve, pushToRemote, initRepo }
|
|
|
15
16
|
import { resolveGitEnv } from '../util/git/git-env';
|
|
16
17
|
import { type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
|
|
17
18
|
import { setupS3Client } from '../util/s3/client';
|
|
19
|
+
import { promptForTags } from './tag';
|
|
18
20
|
import { buildCommand } from './build';
|
|
19
21
|
|
|
20
22
|
export interface DeployOptions {
|
|
@@ -158,6 +160,10 @@ export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
|
|
|
158
160
|
await releaseLockBestEffort(appName, token);
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
|
164
|
+
// tag failure never fails the deploy (handled inside).
|
|
165
|
+
await handleDeployTags(appName);
|
|
166
|
+
|
|
161
167
|
// The host MF parses the app name from the URL and prepends `mf-` itself
|
|
162
168
|
// before looking it up in the manifest. So the user-facing URL must use
|
|
163
169
|
// the consumer's de-scoped package name (no `mf-` prefix, no `@scope/`).
|
|
@@ -193,6 +199,30 @@ async function registerApp(
|
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
const TAG_NUDGE = "run 'z2h-cli tag' to categorize this app (app type & team)";
|
|
203
|
+
|
|
204
|
+
async function handleDeployTags(appSlug: string): Promise<void> {
|
|
205
|
+
try {
|
|
206
|
+
const interactive = getOutputMode() === 'human' && Boolean(process.stdin.isTTY);
|
|
207
|
+
if (!interactive) {
|
|
208
|
+
info(TAG_NUDGE);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const current = await fetchAppTags(appSlug);
|
|
213
|
+
if (current.length > 0) {
|
|
214
|
+
return; // already tagged — don't prompt on every redeploy
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const catalog = await fetchTagCatalog();
|
|
218
|
+
const selected = await promptForTags(appSlug, catalog, current);
|
|
219
|
+
const tags = await setAppTags(appSlug, selected);
|
|
220
|
+
info(tags.length > 0 ? `tags set: ${tags.map(t => t.name).join(', ')}` : TAG_NUDGE);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
warn(`could not update tags: ${(err as Error).message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
196
226
|
// Acquire the per-app deploy lock. Throws a friendly error on 409 (someone else is deploying)
|
|
197
227
|
// so the deploy stops and the caller can retry; the backend TTL backstops a crashed holder.
|
|
198
228
|
async function acquireDeployLock(appName: string, token: string): Promise<void> {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { readJson } from 'fs-extra';
|
|
4
|
+
import { resolveConsumerPaths } from '../shadow/paths';
|
|
5
|
+
import { mfAppName } from '../util/app-name';
|
|
6
|
+
import { info, output, setOutputMode } from '../util/logger';
|
|
7
|
+
import {
|
|
8
|
+
fetchAppTags,
|
|
9
|
+
fetchTagCatalog,
|
|
10
|
+
setAppTags,
|
|
11
|
+
TAG_TYPES,
|
|
12
|
+
TAG_TYPE_LABELS,
|
|
13
|
+
type AppTag,
|
|
14
|
+
type CatalogEntry,
|
|
15
|
+
} from '../util/tags';
|
|
16
|
+
|
|
17
|
+
export interface TagOptions {
|
|
18
|
+
/** Repeatable `--tag` — sets the full tag set non-interactively. */
|
|
19
|
+
tag?: string[];
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Render an app's tags as `name (Type), ...` — or `(none)`. */
|
|
24
|
+
function formatTags(tags: AppTag[]): string {
|
|
25
|
+
return tags.map(t => `${t.name} (${TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the target app name: an explicit arg wins, else the consumer
|
|
30
|
+
* package.json `name` in the cwd. Normalized to the `mf-` bundle name the
|
|
31
|
+
* broker stores (via `mfAppName`), so the server finds the app.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveAppSlug(appArg?: string): Promise<string> {
|
|
34
|
+
if (appArg) {
|
|
35
|
+
return mfAppName(appArg);
|
|
36
|
+
}
|
|
37
|
+
const paths = resolveConsumerPaths(process.cwd());
|
|
38
|
+
let pkg: { name?: string };
|
|
39
|
+
try {
|
|
40
|
+
pkg = (await readJson(paths.consumerPkg)) as { name?: string };
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Could not read a consumer package.json in the current directory. ' +
|
|
44
|
+
'Either cd into your app folder or pass an app name: z2h-cli tag <app-name>.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (!pkg.name) {
|
|
48
|
+
throw new Error('Consumer package.json must have a "name" field (or pass an app name argument).');
|
|
49
|
+
}
|
|
50
|
+
return mfAppName(pkg.name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Closed-list prompt: one numbered pick per dimension (app type, team). Enter
|
|
55
|
+
* keeps the current value for that dimension (or skips it if unset). Returns
|
|
56
|
+
* the full replacement set of tag names.
|
|
57
|
+
*/
|
|
58
|
+
export async function promptForTags(appSlug: string, catalog: CatalogEntry[], current: AppTag[]): Promise<string[]> {
|
|
59
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
60
|
+
const selected: string[] = [];
|
|
61
|
+
try {
|
|
62
|
+
info(`Tagging "${appSlug}".`);
|
|
63
|
+
for (const type of TAG_TYPES) {
|
|
64
|
+
const options = catalog.filter(t => t.type === type);
|
|
65
|
+
if (options.length === 0) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const currentName = current.find(c => c.type === type)?.name;
|
|
69
|
+
const label = TAG_TYPE_LABELS[type];
|
|
70
|
+
info(`\n${label}${currentName ? ` (current: ${currentName})` : ''}:`);
|
|
71
|
+
options.forEach((o, i) => info(` ${i + 1}) ${o.name}`));
|
|
72
|
+
|
|
73
|
+
const raw = await rl.question(
|
|
74
|
+
`Pick a ${label} (number or name; Enter to ${currentName ? 'keep current' : 'skip'}):\n> `
|
|
75
|
+
);
|
|
76
|
+
const trimmed = raw.trim();
|
|
77
|
+
if (!trimmed) {
|
|
78
|
+
if (currentName) {
|
|
79
|
+
selected.push(currentName);
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Accept either the 1-based number or the tag name (case-insensitive).
|
|
84
|
+
const idx = Number(trimmed) - 1;
|
|
85
|
+
const byNumber = Number.isInteger(idx) && idx >= 0 && idx < options.length ? options[idx] : undefined;
|
|
86
|
+
const chosen = byNumber ?? options.find(o => o.name.toLowerCase() === trimmed.toLowerCase());
|
|
87
|
+
if (chosen) {
|
|
88
|
+
selected.push(chosen.name);
|
|
89
|
+
} else {
|
|
90
|
+
info(`Invalid choice "${trimmed}" — ${currentName ? 'keeping current' : 'skipping'} ${label}.`);
|
|
91
|
+
if (currentName) {
|
|
92
|
+
selected.push(currentName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return selected;
|
|
97
|
+
} finally {
|
|
98
|
+
rl.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function tagCommand(appArg: string | undefined, opts: TagOptions): Promise<void> {
|
|
103
|
+
if (opts.json) {
|
|
104
|
+
setOutputMode('json');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const appSlug = await resolveAppSlug(appArg);
|
|
108
|
+
|
|
109
|
+
// Non-interactive write: `--tag` flags set the full set directly.
|
|
110
|
+
if (opts.tag && opts.tag.length > 0) {
|
|
111
|
+
const tags = await setAppTags(appSlug, opts.tag);
|
|
112
|
+
output(`[z2h-cli] tags for "${appSlug}": ${formatTags(tags)}`, { ok: true, appName: appSlug, tags });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [catalog, current] = await Promise.all([fetchTagCatalog(), fetchAppTags(appSlug)]);
|
|
117
|
+
|
|
118
|
+
// Read-only listing: agents (`--json`) and any non-TTY caller get the catalog
|
|
119
|
+
// + current tags without a prompt — then they set tags via `--tag`. This also
|
|
120
|
+
// avoids hanging on `readline` when no human is attached to stdin.
|
|
121
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
122
|
+
output(
|
|
123
|
+
`[z2h-cli] "${appSlug}"\n catalog: ${
|
|
124
|
+
catalog.map(t => `${t.name} (${TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)'
|
|
125
|
+
}\n current: ${formatTags(current)}`,
|
|
126
|
+
{ ok: true, appName: appSlug, catalog, current }
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const selected = await promptForTags(appSlug, catalog, current);
|
|
132
|
+
const tags = await setAppTags(appSlug, selected);
|
|
133
|
+
output(`[z2h-cli] tags for "${appSlug}": ${formatTags(tags)}`, { ok: true, appName: appSlug, tags });
|
|
134
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { createWorkspaceCommand } from './commands/create-workspace';
|
|
|
13
13
|
import { createCommand, CreateOptions } from './commands/create';
|
|
14
14
|
import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
|
|
15
15
|
import { migrateCommand } from './commands/migrate';
|
|
16
|
+
import { tagCommand } from './commands/tag';
|
|
16
17
|
import { editCommand } from './commands/edit';
|
|
17
18
|
import { errorLog, fail, getOutputMode } from './util/logger';
|
|
18
19
|
import { trackDuration } from './util/tracker';
|
|
@@ -41,6 +42,11 @@ function stripConflictingEnvVars(): void {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** commander collector for a repeatable `--tag` option. */
|
|
46
|
+
function collectTag(value: string, previous: string[]): string[] {
|
|
47
|
+
return previous.concat(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
async function main(): Promise<void> {
|
|
45
51
|
stripConflictingEnvVars();
|
|
46
52
|
|
|
@@ -127,6 +133,21 @@ async function main(): Promise<void> {
|
|
|
127
133
|
}
|
|
128
134
|
});
|
|
129
135
|
|
|
136
|
+
program
|
|
137
|
+
.command('tag [app-name]')
|
|
138
|
+
.description("Set an app's category tags — app type & team (interactive unless --tag or --json is given)")
|
|
139
|
+
.option('--tag <tag>', 'set a tag (repeatable); full-replaces the tag set non-interactively', collectTag, [])
|
|
140
|
+
.option('--json', "read-only: print the popular catalog + the app's current tags as JSON (no prompt)")
|
|
141
|
+
.action(async (appName: string | undefined, opts: { tag?: string[]; json?: boolean }) => {
|
|
142
|
+
try {
|
|
143
|
+
await trackDuration('z2h_app_tag_success', () => tagCommand(appName, opts), {
|
|
144
|
+
app_name: appName ?? getCwdAppName(),
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
await fail(`tag failed: ${(err as Error).message}`, { command: 'tag' }, 1);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
130
151
|
program
|
|
131
152
|
.command('create-workspace')
|
|
132
153
|
.description(`Bootstrap the ${getWorkspaceHome()}/ yarn workspace (one-time setup)`)
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// /src/page.html (and copies it to /assets/ at build time). Same-origin
|
|
4
|
-
// (the URL lives under whatever host is rendering the app), so the page's
|
|
5
|
-
// fetch() calls carry bigbrain.me auth cookies normally.
|
|
6
|
-
//
|
|
7
|
-
// The iframe gives the page its own document — scripts execute natively,
|
|
8
|
-
// CSS/JS is fully isolated from the host React app, no script reinjection
|
|
9
|
-
// logic needed.
|
|
10
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { getBigBrainAPI } from '@mondaydotcomorg/trident-runtime';
|
|
11
3
|
import pageUrl from './page.html?url';
|
|
12
4
|
|
|
5
|
+
const BACKEND_SUBDOMAIN = 'bigbrain-zth';
|
|
6
|
+
|
|
7
|
+
function getBackendBaseUrl(): string | null {
|
|
8
|
+
const base = getBigBrainAPI()?.contextService?.bigbrainBaseUrl;
|
|
9
|
+
return base ? base.replace('//', `//${BACKEND_SUBDOMAIN}.`) : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
13
12
|
export default function __APP_COMPONENT__() {
|
|
13
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handleMessage = (event: MessageEvent) => {
|
|
16
|
+
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) return;
|
|
17
|
+
if (event.data?.type !== 'z2h:getConfig') return;
|
|
18
|
+
const baseUrl = getBackendBaseUrl();
|
|
19
|
+
if (!baseUrl) return;
|
|
20
|
+
(event.source as Window).postMessage({ type: 'z2h:config', baseUrl }, event.origin);
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener('message', handleMessage);
|
|
23
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
24
|
+
}, []);
|
|
14
25
|
return (
|
|
15
26
|
<iframe
|
|
27
|
+
ref={iframeRef}
|
|
16
28
|
src={pageUrl}
|
|
17
29
|
title="App"
|
|
18
30
|
style={{ width: '100vw', height: '100vh', border: 'none', display: 'block' }}
|
package/src/util/tags.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { brokerBaseUrl, routingHeaders } from '../constants';
|
|
2
|
+
import { readZthAuth } from './auth/auth-store';
|
|
3
|
+
|
|
4
|
+
/** The dimension a tag categorizes an app by. */
|
|
5
|
+
export type TagType = 'app_type' | 'team';
|
|
6
|
+
|
|
7
|
+
/** The tag dimensions, in prompt order. */
|
|
8
|
+
export const TAG_TYPES: TagType[] = ['app_type', 'team'];
|
|
9
|
+
|
|
10
|
+
/** Human labels for each dimension (CLI prompts / output). */
|
|
11
|
+
export const TAG_TYPE_LABELS: Record<TagType, string> = {
|
|
12
|
+
app_type: 'App type',
|
|
13
|
+
team: 'Team',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** A catalog entry: one selectable tag with its dimension + global usage count. */
|
|
17
|
+
export interface CatalogEntry {
|
|
18
|
+
name: string;
|
|
19
|
+
type: TagType;
|
|
20
|
+
count: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A tag assigned to an app — name + dimension. */
|
|
24
|
+
export interface AppTag {
|
|
25
|
+
name: string;
|
|
26
|
+
type: TagType;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CatalogResponse {
|
|
30
|
+
tags?: CatalogEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface AppTagsResponse {
|
|
34
|
+
tags?: AppTag[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Authenticated request to the bigbrain-zth tag endpoints. Mirrors the Bearer
|
|
39
|
+
* pattern used by the STS broker / lock-release calls, with network-error
|
|
40
|
+
* wrapping, auth-failure guidance, and response-body inclusion on failure.
|
|
41
|
+
*/
|
|
42
|
+
async function tagFetch(method: 'GET' | 'PUT', path: string, body?: unknown): Promise<Response> {
|
|
43
|
+
const { token } = await readZthAuth();
|
|
44
|
+
|
|
45
|
+
let res: Response;
|
|
46
|
+
try {
|
|
47
|
+
res = await fetch(`${brokerBaseUrl()}${path}`, {
|
|
48
|
+
method,
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${token}`,
|
|
51
|
+
Accept: 'application/json',
|
|
52
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
53
|
+
...routingHeaders(),
|
|
54
|
+
},
|
|
55
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
throw new Error(`Z2H tag request failed: ${message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (res.status === 401 || res.status === 403) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Z2H tag request rejected by the broker (${res.status}). Refresh your token by running 'z2h-cli generate-z2h-token'.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
throw new Error(`Z2H tag request returned ${res.status}: ${text.slice(0, 1024)}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return res;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The full closed tag catalog (app types + teams), with usage counts. */
|
|
76
|
+
export async function fetchTagCatalog(): Promise<CatalogEntry[]> {
|
|
77
|
+
const res = await tagFetch('GET', '/z2h-cli/tags');
|
|
78
|
+
const data = (await res.json()) as CatalogResponse;
|
|
79
|
+
return data.tags ?? [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Tags currently assigned to an app (bare slug). */
|
|
83
|
+
export async function fetchAppTags(appSlug: string): Promise<AppTag[]> {
|
|
84
|
+
const res = await tagFetch('GET', `/z2h-cli/tags/${encodeURIComponent(appSlug)}`);
|
|
85
|
+
const data = (await res.json()) as AppTagsResponse;
|
|
86
|
+
return data.tags ?? [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Full-replace an app's tag set; returns the resulting tags. */
|
|
90
|
+
export async function setAppTags(appSlug: string, tags: string[]): Promise<AppTag[]> {
|
|
91
|
+
const res = await tagFetch('PUT', `/z2h-cli/tags/${encodeURIComponent(appSlug)}`, { tags });
|
|
92
|
+
const data = (await res.json()) as AppTagsResponse;
|
|
93
|
+
return data.tags ?? [];
|
|
94
|
+
}
|