@mondaydotcomorg/z2h-cli 0.13.1 → 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/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/index.mjs +16 -0
- package/dist/esm/util/git/repo.d.ts.map +1 -1
- package/dist/esm/util/git/repo.mjs +10 -0
- package/dist/index.js +16 -0
- package/dist/util/git/repo.d.ts.map +1 -1
- package/dist/util/git/repo.js +10 -0
- package/package.json +1 -1
- package/src/commands/delete.ts +63 -0
- package/src/index.ts +16 -0
- package/src/util/git/__tests__/repo.test.ts +23 -1
- package/src/util/git/repo.ts +12 -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;
|
|
@@ -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 };
|
package/dist/esm/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ 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';
|
|
@@ -228,6 +229,21 @@ async function main() {
|
|
|
228
229
|
await fail(`transfer-owner failed: ${err.message}`, { command: 'transfer-owner' }, 1);
|
|
229
230
|
}
|
|
230
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
|
+
});
|
|
231
247
|
program
|
|
232
248
|
.command('migrate')
|
|
233
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
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const commands_dev = require('./commands/dev.js');
|
|
|
8
8
|
const commands_build = require('./commands/build.js');
|
|
9
9
|
const commands_clean = require('./commands/clean.js');
|
|
10
10
|
const commands_deploy = require('./commands/deploy.js');
|
|
11
|
+
const commands_delete = require('./commands/delete.js');
|
|
11
12
|
const commands_grant = require('./commands/grant.js');
|
|
12
13
|
const commands_createWorkspace = require('./commands/create-workspace.js');
|
|
13
14
|
const commands_create = require('./commands/create.js');
|
|
@@ -234,6 +235,21 @@ async function main() {
|
|
|
234
235
|
await util_logger.fail(`transfer-owner failed: ${err.message}`, { command: 'transfer-owner' }, 1);
|
|
235
236
|
}
|
|
236
237
|
});
|
|
238
|
+
program
|
|
239
|
+
.command('delete')
|
|
240
|
+
.description('Permanently delete the current app and take it offline (owners/admins only)')
|
|
241
|
+
.option('--yes', 'skip the confirmation prompt (required for --json / non-interactive)')
|
|
242
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
243
|
+
.action(async (opts) => {
|
|
244
|
+
try {
|
|
245
|
+
await util_tracker.trackDuration('z2h_app_delete_success', () => commands_delete.deleteCommand(opts), {
|
|
246
|
+
app_name: util_appName.getCwdAppName(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
await util_logger.fail(`delete failed: ${err.message}`, { command: 'delete' }, 1);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
237
253
|
program
|
|
238
254
|
.command('migrate')
|
|
239
255
|
.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"}
|
package/dist/util/git/repo.js
CHANGED
|
@@ -76,6 +76,16 @@ async function safeBackupIfDirty(dir) {
|
|
|
76
76
|
}
|
|
77
77
|
async function pullAndResolve(dir, env) {
|
|
78
78
|
const gitEnv = env ? { ...process.env, ...env } : undefined;
|
|
79
|
+
// If the remote has no master branch there's nothing to pull — treat it as a
|
|
80
|
+
// first deploy and let the subsequent push create it. This covers a brand-new
|
|
81
|
+
// remote as well as one whose objects were removed (e.g. the app was deleted,
|
|
82
|
+
// wiping its S3 git prefix), which would otherwise surface as a bogus conflict.
|
|
83
|
+
const hasRemoteMaster = await execGit(['ls-remote', '--heads', 'origin', 'master'], dir, gitEnv)
|
|
84
|
+
.then(r => Boolean(r.stdout.trim()))
|
|
85
|
+
.catch(() => false);
|
|
86
|
+
if (!hasRemoteMaster) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
79
89
|
try {
|
|
80
90
|
await execGit(['pull', '--ff-only'], dir, gitEnv);
|
|
81
91
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { output, info, setOutputMode } from '../util/logger';
|
|
3
|
+
import { getCwdAppName, cleanAppName, mfAppName } from '../util/app-name';
|
|
4
|
+
import { brokerBaseUrl, routingHeaders } from '../constants';
|
|
5
|
+
import { readZthAuth } from '../util/auth/auth-store';
|
|
6
|
+
|
|
7
|
+
export interface DeleteOptions {
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Type-the-app-name confirmation. The caller must retype the de-scoped app
|
|
13
|
+
// name (what they see in the URL) to guard against accidental deletes.
|
|
14
|
+
async function confirmByName(appSlug: string): Promise<boolean> {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
16
|
+
const answer = await new Promise<string>(resolve => {
|
|
17
|
+
rl.question(
|
|
18
|
+
`This permanently deletes "${appSlug}" and takes it offline. This cannot be undone.\n` +
|
|
19
|
+
`Type the app name to confirm: `,
|
|
20
|
+
resolve
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
rl.close();
|
|
24
|
+
return answer.trim() === appSlug;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
28
|
+
export async function deleteCommand(opts: DeleteOptions = {}): Promise<void> {
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
setOutputMode('json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cwdName = getCwdAppName();
|
|
34
|
+
if (!cwdName) {
|
|
35
|
+
throw new Error('Run this from inside a consumer app directory (the one with your package.json).');
|
|
36
|
+
}
|
|
37
|
+
const appSlug = cleanAppName(cwdName); // user-facing name (matches the bigbrain.me URL)
|
|
38
|
+
const appName = mfAppName(cwdName); // canonical mf- bundle name the backend keys on
|
|
39
|
+
|
|
40
|
+
// Confirmation. --json is always non-interactive, so it requires --yes.
|
|
41
|
+
if (!opts.yes) {
|
|
42
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
43
|
+
throw new Error('Refusing to delete without confirmation — pass --yes to delete non-interactively.');
|
|
44
|
+
}
|
|
45
|
+
if (!(await confirmByName(appSlug))) {
|
|
46
|
+
throw new Error(`Name did not match "${appSlug}" — aborted, nothing was deleted.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { token } = await readZthAuth();
|
|
51
|
+
|
|
52
|
+
info(`deleting "${appName}"`);
|
|
53
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-apps/${encodeURIComponent(appName)}`, {
|
|
54
|
+
method: 'DELETE',
|
|
55
|
+
headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
throw new Error(`Delete failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
output(`[z2h-cli] "${appName}" deleted ✅ — it is now offline.`, { ok: true, appName, deleted: true });
|
|
63
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { devCommand } from './commands/dev';
|
|
|
8
8
|
import { buildCommand } from './commands/build';
|
|
9
9
|
import { cleanCommand } from './commands/clean';
|
|
10
10
|
import { deployCommand } from './commands/deploy';
|
|
11
|
+
import { deleteCommand } from './commands/delete';
|
|
11
12
|
import { grantCommand, revokeCommand, transferOwnerCommand } from './commands/grant';
|
|
12
13
|
import { createWorkspaceCommand } from './commands/create-workspace';
|
|
13
14
|
import { createCommand, CreateOptions } from './commands/create';
|
|
@@ -254,6 +255,21 @@ async function main(): Promise<void> {
|
|
|
254
255
|
}
|
|
255
256
|
});
|
|
256
257
|
|
|
258
|
+
program
|
|
259
|
+
.command('delete')
|
|
260
|
+
.description('Permanently delete the current app and take it offline (owners/admins only)')
|
|
261
|
+
.option('--yes', 'skip the confirmation prompt (required for --json / non-interactive)')
|
|
262
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
263
|
+
.action(async (opts: { yes?: boolean; json?: boolean }) => {
|
|
264
|
+
try {
|
|
265
|
+
await trackDuration('z2h_app_delete_success', () => deleteCommand(opts), {
|
|
266
|
+
app_name: getCwdAppName(),
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
await fail(`delete failed: ${(err as Error).message}`, { command: 'delete' }, 1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
257
273
|
program
|
|
258
274
|
.command('migrate')
|
|
259
275
|
.description('Run pending workspace migrations')
|
|
@@ -179,13 +179,35 @@ describe('safeBackupIfDirty', () => {
|
|
|
179
179
|
|
|
180
180
|
describe('pullAndResolve', () => {
|
|
181
181
|
it('resolves when pull fast-forwards cleanly', async () => {
|
|
182
|
-
mockExeca
|
|
182
|
+
mockExeca
|
|
183
|
+
.mockResolvedValueOnce(execaOk('deadbeef\trefs/heads/master')) // ls-remote --heads origin master
|
|
184
|
+
.mockResolvedValueOnce(execaOk()); // git pull --ff-only
|
|
183
185
|
|
|
184
186
|
await expect(pullAndResolve(DIR)).resolves.toBeUndefined();
|
|
185
187
|
});
|
|
186
188
|
|
|
189
|
+
it('skips the pull (first-deploy) when the remote has no master ref', async () => {
|
|
190
|
+
mockExeca.mockResolvedValueOnce(execaOk('')); // ls-remote --heads origin master → empty (gone/new remote)
|
|
191
|
+
|
|
192
|
+
await expect(pullAndResolve(DIR)).resolves.toBeUndefined();
|
|
193
|
+
|
|
194
|
+
const calls = mockExeca.mock.calls.map(c => c.slice(0, 2));
|
|
195
|
+
expect(calls).toContainEqual(['git', ['ls-remote', '--heads', 'origin', 'master']]);
|
|
196
|
+
expect(calls).not.toContainEqual(['git', ['pull', '--ff-only']]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('treats an unreachable ls-remote as no remote and skips the pull', async () => {
|
|
200
|
+
mockExeca.mockRejectedValueOnce(new Error('fatal: could not read from remote')); // ls-remote fails
|
|
201
|
+
|
|
202
|
+
await expect(pullAndResolve(DIR)).resolves.toBeUndefined();
|
|
203
|
+
|
|
204
|
+
const calls = mockExeca.mock.calls.map(c => c.slice(0, 2));
|
|
205
|
+
expect(calls).not.toContainEqual(['git', ['pull', '--ff-only']]);
|
|
206
|
+
});
|
|
207
|
+
|
|
187
208
|
it('throws GitConflictError with structured JSON on non-fast-forward', async () => {
|
|
188
209
|
mockExeca
|
|
210
|
+
.mockResolvedValueOnce(execaOk('deadbeef\trefs/heads/master')) // ls-remote --heads origin master
|
|
189
211
|
.mockRejectedValueOnce(new Error('fatal: Not possible to fast-forward')) // git pull
|
|
190
212
|
.mockResolvedValueOnce(execaOk('')) // safeBackupIfDirty: git status --porcelain (clean)
|
|
191
213
|
.mockResolvedValueOnce(execaOk()) // gatherConflictInfo: git fetch origin
|
package/src/util/git/repo.ts
CHANGED
|
@@ -86,6 +86,18 @@ export async function safeBackupIfDirty(dir: string): Promise<void> {
|
|
|
86
86
|
|
|
87
87
|
export async function pullAndResolve(dir: string, env?: Record<string, string>): Promise<void> {
|
|
88
88
|
const gitEnv = env ? { ...process.env, ...env } : undefined;
|
|
89
|
+
|
|
90
|
+
// If the remote has no master branch there's nothing to pull — treat it as a
|
|
91
|
+
// first deploy and let the subsequent push create it. This covers a brand-new
|
|
92
|
+
// remote as well as one whose objects were removed (e.g. the app was deleted,
|
|
93
|
+
// wiping its S3 git prefix), which would otherwise surface as a bogus conflict.
|
|
94
|
+
const hasRemoteMaster = await execGit(['ls-remote', '--heads', 'origin', 'master'], dir, gitEnv)
|
|
95
|
+
.then(r => Boolean(r.stdout.trim()))
|
|
96
|
+
.catch(() => false);
|
|
97
|
+
if (!hasRemoteMaster) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
try {
|
|
90
102
|
await execGit(['pull', '--ff-only'], dir, gitEnv);
|
|
91
103
|
} catch (err) {
|