@mondaydotcomorg/z2h-cli 0.13.0 → 0.13.2
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/delete.d.ts +6 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +53 -0
- 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/delete.d.ts +6 -0
- package/dist/esm/commands/delete.d.ts.map +1 -0
- package/dist/esm/commands/delete.mjs +51 -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 +36 -0
- package/dist/esm/util/git/repo.d.ts.map +1 -1
- package/dist/esm/util/git/repo.mjs +10 -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 +36 -0
- package/dist/util/git/repo.d.ts.map +1 -1
- package/dist/util/git/repo.js +10 -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/delete.ts +63 -0
- package/src/commands/deploy.ts +31 -1
- package/src/commands/tag.ts +134 -0
- package/src/index.ts +37 -0
- package/src/util/git/__tests__/repo.test.ts +23 -1
- package/src/util/git/repo.ts +12 -0
- package/src/util/tags.ts +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../src/commands/delete.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAkBD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC3E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const node_readline = require('node:readline');
|
|
4
|
+
const util_logger = require('../util/logger.js');
|
|
5
|
+
const util_appName = require('../util/app-name.js');
|
|
6
|
+
const constants = require('../constants.js');
|
|
7
|
+
const util_auth_authStore = require('../util/auth/auth-store.js');
|
|
8
|
+
|
|
9
|
+
// Type-the-app-name confirmation. The caller must retype the de-scoped app
|
|
10
|
+
// name (what they see in the URL) to guard against accidental deletes.
|
|
11
|
+
async function confirmByName(appSlug) {
|
|
12
|
+
const rl = node_readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
13
|
+
const answer = await new Promise(resolve => {
|
|
14
|
+
rl.question(`This permanently deletes "${appSlug}" and takes it offline. This cannot be undone.\n` +
|
|
15
|
+
`Type the app name to confirm: `, resolve);
|
|
16
|
+
});
|
|
17
|
+
rl.close();
|
|
18
|
+
return answer.trim() === appSlug;
|
|
19
|
+
}
|
|
20
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
21
|
+
async function deleteCommand(opts = {}) {
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
util_logger.setOutputMode('json');
|
|
24
|
+
}
|
|
25
|
+
const cwdName = util_appName.getCwdAppName();
|
|
26
|
+
if (!cwdName) {
|
|
27
|
+
throw new Error('Run this from inside a consumer app directory (the one with your package.json).');
|
|
28
|
+
}
|
|
29
|
+
const appSlug = util_appName.cleanAppName(cwdName); // user-facing name (matches the bigbrain.me URL)
|
|
30
|
+
const appName = util_appName.mfAppName(cwdName); // canonical mf- bundle name the backend keys on
|
|
31
|
+
// Confirmation. --json is always non-interactive, so it requires --yes.
|
|
32
|
+
if (!opts.yes) {
|
|
33
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
34
|
+
throw new Error('Refusing to delete without confirmation — pass --yes to delete non-interactively.');
|
|
35
|
+
}
|
|
36
|
+
if (!(await confirmByName(appSlug))) {
|
|
37
|
+
throw new Error(`Name did not match "${appSlug}" — aborted, nothing was deleted.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const { token } = await util_auth_authStore.readZthAuth();
|
|
41
|
+
util_logger.info(`deleting "${appName}"`);
|
|
42
|
+
const res = await fetch(`${constants.brokerBaseUrl()}/z2h-apps/${encodeURIComponent(appName)}`, {
|
|
43
|
+
method: 'DELETE',
|
|
44
|
+
headers: { Authorization: `Bearer ${token}`, ...constants.routingHeaders() },
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const text = await res.text();
|
|
48
|
+
throw new Error(`Delete failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
49
|
+
}
|
|
50
|
+
util_logger.output(`[z2h-cli] "${appName}" deleted ✅ — it is now offline.`, { ok: true, appName, deleted: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exports.deleteCommand = deleteCommand;
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/commands/delete.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAkBD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC3E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { setOutputMode, info, output } from '../util/logger.mjs';
|
|
3
|
+
import { getCwdAppName, cleanAppName, mfAppName } from '../util/app-name.mjs';
|
|
4
|
+
import { brokerBaseUrl, routingHeaders } from '../constants.mjs';
|
|
5
|
+
import { readZthAuth } from '../util/auth/auth-store.mjs';
|
|
6
|
+
|
|
7
|
+
// Type-the-app-name confirmation. The caller must retype the de-scoped app
|
|
8
|
+
// name (what they see in the URL) to guard against accidental deletes.
|
|
9
|
+
async function confirmByName(appSlug) {
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
11
|
+
const answer = await new Promise(resolve => {
|
|
12
|
+
rl.question(`This permanently deletes "${appSlug}" and takes it offline. This cannot be undone.\n` +
|
|
13
|
+
`Type the app name to confirm: `, resolve);
|
|
14
|
+
});
|
|
15
|
+
rl.close();
|
|
16
|
+
return answer.trim() === appSlug;
|
|
17
|
+
}
|
|
18
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
19
|
+
async function deleteCommand(opts = {}) {
|
|
20
|
+
if (opts.json) {
|
|
21
|
+
setOutputMode('json');
|
|
22
|
+
}
|
|
23
|
+
const cwdName = getCwdAppName();
|
|
24
|
+
if (!cwdName) {
|
|
25
|
+
throw new Error('Run this from inside a consumer app directory (the one with your package.json).');
|
|
26
|
+
}
|
|
27
|
+
const appSlug = cleanAppName(cwdName); // user-facing name (matches the bigbrain.me URL)
|
|
28
|
+
const appName = mfAppName(cwdName); // canonical mf- bundle name the backend keys on
|
|
29
|
+
// Confirmation. --json is always non-interactive, so it requires --yes.
|
|
30
|
+
if (!opts.yes) {
|
|
31
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
32
|
+
throw new Error('Refusing to delete without confirmation — pass --yes to delete non-interactively.');
|
|
33
|
+
}
|
|
34
|
+
if (!(await confirmByName(appSlug))) {
|
|
35
|
+
throw new Error(`Name did not match "${appSlug}" — aborted, nothing was deleted.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const { token } = await readZthAuth();
|
|
39
|
+
info(`deleting "${appName}"`);
|
|
40
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${encodeURIComponent(appName)}`, {
|
|
41
|
+
method: 'DELETE',
|
|
42
|
+
headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
throw new Error(`Delete failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
47
|
+
}
|
|
48
|
+
output(`[z2h-cli] "${appName}" deleted ✅ — it is now offline.`, { ok: true, appName, deleted: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { deleteCommand };
|
|
@@ -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
|
@@ -8,11 +8,13 @@ import { devCommand } from './commands/dev.mjs';
|
|
|
8
8
|
import { buildCommand } from './commands/build.mjs';
|
|
9
9
|
import { cleanCommand } from './commands/clean.mjs';
|
|
10
10
|
import { deployCommand } from './commands/deploy.mjs';
|
|
11
|
+
import { deleteCommand } from './commands/delete.mjs';
|
|
11
12
|
import { grantCommand, revokeCommand, transferOwnerCommand } from './commands/grant.mjs';
|
|
12
13
|
import { createWorkspaceCommand } from './commands/create-workspace.mjs';
|
|
13
14
|
import { createCommand } from './commands/create.mjs';
|
|
14
15
|
import { generateZ2HTokenCommand } from './commands/generate-z2h-token.mjs';
|
|
15
16
|
import { migrateCommand } from './commands/migrate.mjs';
|
|
17
|
+
import { tagCommand } from './commands/tag.mjs';
|
|
16
18
|
import { editCommand } from './commands/edit.mjs';
|
|
17
19
|
import { fail, errorLog, getOutputMode } from './util/logger.mjs';
|
|
18
20
|
import { trackDuration } from './util/tracker.mjs';
|
|
@@ -37,6 +39,10 @@ function stripConflictingEnvVars() {
|
|
|
37
39
|
delete process.env[name];
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
/** commander collector for a repeatable `--tag` option. */
|
|
43
|
+
function collectTag(value, previous) {
|
|
44
|
+
return previous.concat(value);
|
|
45
|
+
}
|
|
40
46
|
async function main() {
|
|
41
47
|
stripConflictingEnvVars();
|
|
42
48
|
const program = new Command();
|
|
@@ -109,6 +115,21 @@ async function main() {
|
|
|
109
115
|
await fail(`deploy failed: ${err.message}`, { command: 'deploy' }, 2);
|
|
110
116
|
}
|
|
111
117
|
});
|
|
118
|
+
program
|
|
119
|
+
.command('tag [app-name]')
|
|
120
|
+
.description("Set an app's category tags — app type & team (interactive unless --tag or --json is given)")
|
|
121
|
+
.option('--tag <tag>', 'set a tag (repeatable); full-replaces the tag set non-interactively', collectTag, [])
|
|
122
|
+
.option('--json', "read-only: print the popular catalog + the app's current tags as JSON (no prompt)")
|
|
123
|
+
.action(async (appName, opts) => {
|
|
124
|
+
try {
|
|
125
|
+
await trackDuration('z2h_app_tag_success', () => tagCommand(appName, opts), {
|
|
126
|
+
app_name: appName ?? getCwdAppName(),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
await fail(`tag failed: ${err.message}`, { command: 'tag' }, 1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
112
133
|
program
|
|
113
134
|
.command('create-workspace')
|
|
114
135
|
.description(`Bootstrap the ${getWorkspaceHome()}/ yarn workspace (one-time setup)`)
|
|
@@ -208,6 +229,21 @@ async function main() {
|
|
|
208
229
|
await fail(`transfer-owner failed: ${err.message}`, { command: 'transfer-owner' }, 1);
|
|
209
230
|
}
|
|
210
231
|
});
|
|
232
|
+
program
|
|
233
|
+
.command('delete')
|
|
234
|
+
.description('Permanently delete the current app and take it offline (owners/admins only)')
|
|
235
|
+
.option('--yes', 'skip the confirmation prompt (required for --json / non-interactive)')
|
|
236
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
237
|
+
.action(async (opts) => {
|
|
238
|
+
try {
|
|
239
|
+
await trackDuration('z2h_app_delete_success', () => deleteCommand(opts), {
|
|
240
|
+
app_name: getCwdAppName(),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
await fail(`delete failed: ${err.message}`, { command: 'delete' }, 1);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
211
247
|
program
|
|
212
248
|
.command('migrate')
|
|
213
249
|
.description('Run pending workspace migrations')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../../src/util/git/repo.ts"],"names":[],"mappings":"AAkBA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../../src/util/git/repo.ts"],"names":[],"mappings":"AAkBA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D"}
|
|
@@ -69,6 +69,16 @@ async function safeBackupIfDirty(dir) {
|
|
|
69
69
|
}
|
|
70
70
|
async function pullAndResolve(dir, env) {
|
|
71
71
|
const gitEnv = env ? { ...process.env, ...env } : undefined;
|
|
72
|
+
// If the remote has no master branch there's nothing to pull — treat it as a
|
|
73
|
+
// first deploy and let the subsequent push create it. This covers a brand-new
|
|
74
|
+
// remote as well as one whose objects were removed (e.g. the app was deleted,
|
|
75
|
+
// wiping its S3 git prefix), which would otherwise surface as a bogus conflict.
|
|
76
|
+
const hasRemoteMaster = await execGit(['ls-remote', '--heads', 'origin', 'master'], dir, gitEnv)
|
|
77
|
+
.then(r => Boolean(r.stdout.trim()))
|
|
78
|
+
.catch(() => false);
|
|
79
|
+
if (!hasRemoteMaster) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
72
82
|
try {
|
|
73
83
|
await execGit(['pull', '--ff-only'], dir, gitEnv);
|
|
74
84
|
}
|
|
@@ -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"}
|