@mondaydotcomorg/z2h-cli 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/__tests__/doctor.test.d.ts +2 -0
- package/dist/commands/__tests__/doctor.test.d.ts.map +1 -0
- 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/doctor.d.ts +5 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +334 -0
- package/dist/esm/commands/__tests__/doctor.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/doctor.test.d.ts.map +1 -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/doctor.d.ts +5 -0
- package/dist/esm/commands/doctor.d.ts.map +1 -0
- package/dist/esm/commands/doctor.mjs +324 -0
- package/dist/esm/index.mjs +29 -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 +29 -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/__tests__/doctor.test.ts +411 -0
- package/src/commands/delete.ts +63 -0
- package/src/commands/doctor.ts +393 -0
- package/src/index.ts +29 -0
- package/src/util/git/__tests__/repo.test.ts +23 -1
- package/src/util/git/repo.ts +12 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { pathExists, readJson } from 'fs-extra';
|
|
6
|
+
import { tryReadAuthTokenSync } from '../../util/auth/auth-store';
|
|
7
|
+
import { doctorCommand } from '../doctor';
|
|
8
|
+
|
|
9
|
+
vi.mock('execa', () => ({ execa: vi.fn() }));
|
|
10
|
+
|
|
11
|
+
vi.mock('fs-extra', async importOriginal => {
|
|
12
|
+
const actual = await importOriginal<typeof import('fs-extra')>();
|
|
13
|
+
return { ...actual, pathExists: vi.fn(), readJson: vi.fn() };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('node:fs/promises', async importOriginal => {
|
|
17
|
+
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
|
18
|
+
return { ...actual, readFile: vi.fn() };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Return null so checkZ2hCli skips the broker fetch entirely
|
|
22
|
+
vi.mock('../../util/auth/auth-store', async importOriginal => {
|
|
23
|
+
const actual = await importOriginal<typeof import('../../util/auth/auth-store')>();
|
|
24
|
+
return { ...actual, tryReadAuthTokenSync: vi.fn().mockReturnValue(null) };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const mockExeca = vi.mocked(execa);
|
|
28
|
+
const mockPathExists = vi.mocked(pathExists);
|
|
29
|
+
const mockReadJson = vi.mocked(readJson);
|
|
30
|
+
const mockReadFile = vi.mocked(readFile);
|
|
31
|
+
const mockTryReadAuthTokenSync = vi.mocked(tryReadAuthTokenSync);
|
|
32
|
+
|
|
33
|
+
// Capture stdout written during doctorCommand
|
|
34
|
+
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
|
35
|
+
let out = '';
|
|
36
|
+
const spy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
|
|
37
|
+
out += String(chunk);
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
await fn();
|
|
42
|
+
} finally {
|
|
43
|
+
spy.mockRestore();
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('doctorCommand', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
|
|
52
|
+
mockPathExists.mockResolvedValue(true as never);
|
|
53
|
+
|
|
54
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
55
|
+
const filePath = String(p);
|
|
56
|
+
if (filePath.endsWith('auth.json')) {
|
|
57
|
+
return { token: 'fake.jwt.token' };
|
|
58
|
+
}
|
|
59
|
+
if (filePath.endsWith('installed_plugins.json')) {
|
|
60
|
+
return {
|
|
61
|
+
plugins: {
|
|
62
|
+
'z2h@bigbrain-z2h': [{ installPath: '/dev/z2h' }],
|
|
63
|
+
'cf-external-claude-plugin@client-foundations-ai-tools': [{ installPath: '/dev/cf' }],
|
|
64
|
+
'pr-guardrails@agentic-builders-hub': [{ installPath: '/dev/pr' }],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (filePath.endsWith('package.json')) {
|
|
69
|
+
return { z2hWorkspace: true, dependencies: { react: '^18.3.1' } };
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// readFile: return profile content with token for shell profiles, throw for everything else
|
|
75
|
+
mockReadFile.mockImplementation(((p: unknown) => {
|
|
76
|
+
const filePath = String(p);
|
|
77
|
+
if (filePath.endsWith('.zshrc') || filePath.endsWith('.bash_profile')) {
|
|
78
|
+
return Promise.resolve('export Z2H_NPM_TOKEN="fake-token"\n');
|
|
79
|
+
}
|
|
80
|
+
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
|
81
|
+
}) as never);
|
|
82
|
+
|
|
83
|
+
mockExeca.mockImplementation(((cmd: unknown, args?: unknown) => {
|
|
84
|
+
const command = String(cmd);
|
|
85
|
+
const argv = (args as string[]) ?? [];
|
|
86
|
+
if (command === 'node' && argv[0] === '--version') {
|
|
87
|
+
return Promise.resolve({ stdout: 'v24.13.1' });
|
|
88
|
+
}
|
|
89
|
+
if (command === 'yarn' && argv[0] === '--version') {
|
|
90
|
+
return Promise.resolve({ stdout: '4.5.1' });
|
|
91
|
+
}
|
|
92
|
+
if (command === 'aws' && argv[0] === '--version') {
|
|
93
|
+
return Promise.resolve({ stdout: 'aws-cli/2.22.0 Python/3.13' });
|
|
94
|
+
}
|
|
95
|
+
if (command === 'bash') {
|
|
96
|
+
return Promise.resolve({ stdout: '0.39.7' });
|
|
97
|
+
} // nvm --version
|
|
98
|
+
if (command === 'pipx') {
|
|
99
|
+
return Promise.resolve({ stdout: 'Name: git-remote-s3\nVersion: 0.2.3\n' });
|
|
100
|
+
}
|
|
101
|
+
if (command === 'which') {
|
|
102
|
+
return Promise.resolve({ stdout: '/usr/local/bin/' + String(argv[0]) });
|
|
103
|
+
}
|
|
104
|
+
return Promise.reject(new Error(`unexpected: ${command}`));
|
|
105
|
+
}) as never);
|
|
106
|
+
|
|
107
|
+
vi.stubEnv('Z2H_WORKSPACE_HOME', '/fake/zth-projects');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
vi.unstubAllEnvs();
|
|
112
|
+
vi.unstubAllGlobals();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── happy path ────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
it('prints no-issues when everything is healthy', async () => {
|
|
118
|
+
const out = await captureStdout(() => doctorCommand());
|
|
119
|
+
expect(out).toContain('No issues detected');
|
|
120
|
+
expect(out).not.toContain('❌');
|
|
121
|
+
expect(out).not.toContain('⚠️');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── System tools ──────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
it('shows warn for nvm when nvm is not available', async () => {
|
|
127
|
+
mockExeca.mockImplementation(((cmd: unknown) => {
|
|
128
|
+
if (String(cmd) === 'bash') {
|
|
129
|
+
return Promise.reject(new Error('not found'));
|
|
130
|
+
}
|
|
131
|
+
return Promise.resolve({ stdout: '' });
|
|
132
|
+
}) as never);
|
|
133
|
+
const out = await captureStdout(() => doctorCommand());
|
|
134
|
+
expect(out).toContain('nvm');
|
|
135
|
+
expect(out).toContain('⚠️');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('shows fail for node when command not found', async () => {
|
|
139
|
+
mockExeca.mockImplementation(((cmd: unknown) => {
|
|
140
|
+
if (String(cmd) === 'node') {
|
|
141
|
+
return Promise.reject(new Error('not found'));
|
|
142
|
+
}
|
|
143
|
+
return Promise.resolve({ stdout: '' });
|
|
144
|
+
}) as never);
|
|
145
|
+
const out = await captureStdout(() => doctorCommand());
|
|
146
|
+
expect(out).toContain('node');
|
|
147
|
+
expect(out).toContain('❌');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('shows fail for node when version mismatches .nvmrc', async () => {
|
|
151
|
+
mockExeca.mockImplementation(((cmd: unknown, args?: unknown) => {
|
|
152
|
+
const argv = (args as string[]) ?? [];
|
|
153
|
+
if (String(cmd) === 'node' && argv[0] === '--version') {
|
|
154
|
+
return Promise.resolve({ stdout: 'v22.0.0' });
|
|
155
|
+
}
|
|
156
|
+
return Promise.resolve({ stdout: '' });
|
|
157
|
+
}) as never);
|
|
158
|
+
// make readFile return a valid .nvmrc so version comparison runs
|
|
159
|
+
mockReadFile.mockImplementation(((p: unknown) => {
|
|
160
|
+
if (String(p).includes('.nvmrc')) {
|
|
161
|
+
return Promise.resolve('v24.13');
|
|
162
|
+
}
|
|
163
|
+
return Promise.resolve('export Z2H_NPM_TOKEN="x"\n');
|
|
164
|
+
}) as never);
|
|
165
|
+
const out = await captureStdout(() => doctorCommand());
|
|
166
|
+
expect(out).toContain('expected');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('shows fail for yarn when command not found', async () => {
|
|
170
|
+
mockExeca.mockImplementation(((cmd: unknown) => {
|
|
171
|
+
if (String(cmd) === 'yarn') {
|
|
172
|
+
return Promise.reject(new Error('not found'));
|
|
173
|
+
}
|
|
174
|
+
return Promise.resolve({ stdout: '' });
|
|
175
|
+
}) as never);
|
|
176
|
+
const out = await captureStdout(() => doctorCommand());
|
|
177
|
+
expect(out).toContain('yarn');
|
|
178
|
+
expect(out).toContain('❌');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('shows fail for aws when command not found', async () => {
|
|
182
|
+
mockExeca.mockImplementation(((cmd: unknown) => {
|
|
183
|
+
if (String(cmd) === 'aws') {
|
|
184
|
+
return Promise.reject(new Error('not found'));
|
|
185
|
+
}
|
|
186
|
+
return Promise.resolve({ stdout: '' });
|
|
187
|
+
}) as never);
|
|
188
|
+
const out = await captureStdout(() => doctorCommand());
|
|
189
|
+
expect(out).toContain('aws');
|
|
190
|
+
expect(out).toContain('❌');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('shows fail for git-remote-s3 when not on PATH', async () => {
|
|
194
|
+
mockExeca.mockImplementation(((cmd: unknown, args?: unknown) => {
|
|
195
|
+
const argv = (args as string[]) ?? [];
|
|
196
|
+
if (String(cmd) === 'which' && argv[0] === 'git-remote-s3') {
|
|
197
|
+
return Promise.reject(new Error('not found'));
|
|
198
|
+
}
|
|
199
|
+
return Promise.resolve({ stdout: '' });
|
|
200
|
+
}) as never);
|
|
201
|
+
const out = await captureStdout(() => doctorCommand());
|
|
202
|
+
expect(out).toContain('git-remote-s3');
|
|
203
|
+
expect(out).toContain('❌');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── CLI & auth ────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
it('shows warn for z2h-cli when a newer version is available', async () => {
|
|
209
|
+
mockTryReadAuthTokenSync.mockReturnValueOnce('fake.jwt.token');
|
|
210
|
+
vi.stubGlobal(
|
|
211
|
+
'fetch',
|
|
212
|
+
vi.fn().mockResolvedValue({
|
|
213
|
+
ok: true,
|
|
214
|
+
json: async () => ({ 'dist-tags': { latest: '999.0.0' } }),
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
const out = await captureStdout(() => doctorCommand());
|
|
218
|
+
expect(out).toContain('z2h-cli');
|
|
219
|
+
expect(out).toContain('⚠️');
|
|
220
|
+
expect(out).toContain('latest: 999.0.0');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('shows fail for auth token when auth.json is missing', async () => {
|
|
224
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
225
|
+
if (String(p).endsWith('auth.json')) {
|
|
226
|
+
const err = new Error('ENOENT: no such file or directory') as NodeJS.ErrnoException;
|
|
227
|
+
err.code = 'ENOENT';
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
return {};
|
|
231
|
+
});
|
|
232
|
+
const out = await captureStdout(() => doctorCommand());
|
|
233
|
+
expect(out).toContain('auth token');
|
|
234
|
+
expect(out).toContain('❌');
|
|
235
|
+
expect(out).toContain('not found');
|
|
236
|
+
expect(out).toContain('generate-z2h-token');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('shows fail for auth token when auth.json contains invalid JSON', async () => {
|
|
240
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
241
|
+
if (String(p).endsWith('auth.json')) {
|
|
242
|
+
throw new SyntaxError('Unexpected token in JSON');
|
|
243
|
+
}
|
|
244
|
+
return {};
|
|
245
|
+
});
|
|
246
|
+
const out = await captureStdout(() => doctorCommand());
|
|
247
|
+
expect(out).toContain('auth token');
|
|
248
|
+
expect(out).toContain('❌');
|
|
249
|
+
expect(out).toContain('could not be parsed');
|
|
250
|
+
expect(out).toContain('generate-z2h-token');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('shows fail for auth token when token field is missing', async () => {
|
|
254
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
255
|
+
if (String(p).endsWith('auth.json')) {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
return {};
|
|
259
|
+
});
|
|
260
|
+
const out = await captureStdout(() => doctorCommand());
|
|
261
|
+
expect(out).toContain('auth token');
|
|
262
|
+
expect(out).toContain('❌');
|
|
263
|
+
expect(out).toContain('missing token field');
|
|
264
|
+
expect(out).toContain('generate-z2h-token');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('shows fail for npm token when not found in any profile', async () => {
|
|
268
|
+
mockReadFile.mockResolvedValue('# no z2h token here\n' as never);
|
|
269
|
+
const out = await captureStdout(() => doctorCommand());
|
|
270
|
+
expect(out).toContain('npm token in profile');
|
|
271
|
+
expect(out).toContain('❌');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Workspace ─────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
it('shows fail for workspace when not initialized', async () => {
|
|
277
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
278
|
+
if (String(p).endsWith('package.json')) {
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
if (String(p).endsWith('auth.json')) {
|
|
282
|
+
return { token: 'fake.jwt.token' };
|
|
283
|
+
}
|
|
284
|
+
if (String(p).endsWith('installed_plugins.json')) {
|
|
285
|
+
return {
|
|
286
|
+
plugins: {
|
|
287
|
+
'z2h@bigbrain-z2h': [{ installPath: '/dev/z2h' }],
|
|
288
|
+
'cf-external-claude-plugin@client-foundations-ai-tools': [{ installPath: '/dev/cf' }],
|
|
289
|
+
'pr-guardrails@agentic-builders-hub': [{ installPath: '/dev/pr' }],
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {};
|
|
294
|
+
});
|
|
295
|
+
const out = await captureStdout(() => doctorCommand());
|
|
296
|
+
expect(out).toContain('workspace');
|
|
297
|
+
expect(out).toContain('❌');
|
|
298
|
+
expect(out).toContain('create-workspace');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('shows fail for react version when workspace has react 19', async () => {
|
|
302
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
303
|
+
if (String(p).endsWith('package.json')) {
|
|
304
|
+
return { z2hWorkspace: true, dependencies: { react: '^19.0.0' } };
|
|
305
|
+
}
|
|
306
|
+
if (String(p).endsWith('auth.json')) {
|
|
307
|
+
return { token: 'fake.jwt.token' };
|
|
308
|
+
}
|
|
309
|
+
if (String(p).endsWith('installed_plugins.json')) {
|
|
310
|
+
return {
|
|
311
|
+
plugins: {
|
|
312
|
+
'z2h@bigbrain-z2h': [{ installPath: '/dev/z2h' }],
|
|
313
|
+
'cf-external-claude-plugin@client-foundations-ai-tools': [{ installPath: '/dev/cf' }],
|
|
314
|
+
'pr-guardrails@agentic-builders-hub': [{ installPath: '/dev/pr' }],
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {};
|
|
319
|
+
});
|
|
320
|
+
const out = await captureStdout(() => doctorCommand());
|
|
321
|
+
expect(out).toContain('react version');
|
|
322
|
+
expect(out).toContain('❌');
|
|
323
|
+
expect(out).toContain('expected 18.x');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('shows warn for react version when react is not in dependencies', async () => {
|
|
327
|
+
mockReadJson.mockImplementation(async (p: unknown) => {
|
|
328
|
+
if (String(p).endsWith('package.json')) {
|
|
329
|
+
return { z2hWorkspace: true, dependencies: {} };
|
|
330
|
+
}
|
|
331
|
+
if (String(p).endsWith('auth.json')) {
|
|
332
|
+
return { token: 'fake.jwt.token' };
|
|
333
|
+
}
|
|
334
|
+
if (String(p).endsWith('installed_plugins.json')) {
|
|
335
|
+
return {
|
|
336
|
+
plugins: {
|
|
337
|
+
'z2h@bigbrain-z2h': [{ installPath: '/dev/z2h' }],
|
|
338
|
+
'cf-external-claude-plugin@client-foundations-ai-tools': [{ installPath: '/dev/cf' }],
|
|
339
|
+
'pr-guardrails@agentic-builders-hub': [{ installPath: '/dev/pr' }],
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {};
|
|
344
|
+
});
|
|
345
|
+
const out = await captureStdout(() => doctorCommand());
|
|
346
|
+
expect(out).toContain('react version');
|
|
347
|
+
expect(out).toContain('❌');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('shows warn for SSL cert when not found', async () => {
|
|
351
|
+
mockPathExists.mockImplementation(async (p: unknown) => {
|
|
352
|
+
if (String(p).includes('devcert')) {
|
|
353
|
+
return false as never;
|
|
354
|
+
}
|
|
355
|
+
return true as never;
|
|
356
|
+
});
|
|
357
|
+
const out = await captureStdout(() => doctorCommand());
|
|
358
|
+
expect(out).toContain('SSL cert');
|
|
359
|
+
expect(out).toContain('⚠️');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ── Plugins ───────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
it('shows fail for z2h plugin (mandatory) when installPath dir is missing', async () => {
|
|
365
|
+
mockPathExists.mockImplementation(async (p: unknown) => {
|
|
366
|
+
if (String(p) === '/dev/z2h') {
|
|
367
|
+
return false as never;
|
|
368
|
+
}
|
|
369
|
+
return true as never;
|
|
370
|
+
});
|
|
371
|
+
const out = await captureStdout(() => doctorCommand());
|
|
372
|
+
expect(out).toContain('z2h plugin');
|
|
373
|
+
expect(out).toContain('❌');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('shows warn for cf-external plugin (optional) when installPath dir is missing', async () => {
|
|
377
|
+
mockPathExists.mockImplementation(async (p: unknown) => {
|
|
378
|
+
if (String(p) === '/dev/cf') {
|
|
379
|
+
return false as never;
|
|
380
|
+
}
|
|
381
|
+
return true as never;
|
|
382
|
+
});
|
|
383
|
+
const out = await captureStdout(() => doctorCommand());
|
|
384
|
+
expect(out).toContain('cf-external plugin');
|
|
385
|
+
expect(out).toContain('⚠️');
|
|
386
|
+
expect(out).not.toContain('❌');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('shows warn for pr-guardrails plugin (optional) when installPath dir is missing', async () => {
|
|
390
|
+
mockPathExists.mockImplementation(async (p: unknown) => {
|
|
391
|
+
if (String(p) === '/dev/pr') {
|
|
392
|
+
return false as never;
|
|
393
|
+
}
|
|
394
|
+
return true as never;
|
|
395
|
+
});
|
|
396
|
+
const out = await captureStdout(() => doctorCommand());
|
|
397
|
+
expect(out).toContain('pr-guardrails plugin');
|
|
398
|
+
expect(out).toContain('⚠️');
|
|
399
|
+
expect(out).not.toContain('❌');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// ── JSON output ───────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
it('emits structured JSON when --json flag is set', async () => {
|
|
405
|
+
const jsonLine = await captureStdout(() => doctorCommand({ json: true }));
|
|
406
|
+
const parsed = JSON.parse(jsonLine.trim()) as { ok: boolean; checks: unknown[] };
|
|
407
|
+
expect(parsed.ok).toBe(true);
|
|
408
|
+
expect(Array.isArray(parsed.checks)).toBe(true);
|
|
409
|
+
expect(parsed.checks.length).toBeGreaterThan(0);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -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
|
+
}
|