@mondaydotcomorg/z2h-cli 0.13.2 → 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/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/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 +13 -0
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/commands/__tests__/doctor.test.ts +411 -0
- package/src/commands/doctor.ts +393 -0
- package/src/index.ts +13 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
import { pathExists, readJson } from 'fs-extra';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { setOutputMode, output } from '../util/logger.mjs';
|
|
10
|
+
import { getWorkspaceHome, isZ2hWorkspace } from '../util/workspace.mjs';
|
|
11
|
+
import { tryReadAuthTokenSync, Z2H_AUTH_FILE } from '../util/auth/auth-store.mjs';
|
|
12
|
+
import { brokerNpmRegistryUrl, routingHeaders } from '../constants.mjs';
|
|
13
|
+
|
|
14
|
+
const HOME = os.homedir();
|
|
15
|
+
const Z2H_REPO_ROOT = path.join(HOME, 'Development', 'bigbrain-z2h');
|
|
16
|
+
const BOOTSTRAP_CMD = `bash ${path.join(Z2H_REPO_ROOT, 'z2h/setup/bootstrap.sh')}`;
|
|
17
|
+
const UPDATE_CMD = `bash ${path.join(Z2H_REPO_ROOT, 'z2h/setup/update.sh')}`;
|
|
18
|
+
const CLAUDE_PLUGINS_JSON = path.join(HOME, '.claude', 'plugins', 'installed_plugins.json');
|
|
19
|
+
const SSL_CERT_PATH = path.join(HOME, 'Library', 'Application Support', 'devcert', 'domains', 'webpack.llama.fan', 'certificate.crt');
|
|
20
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
21
|
+
async function checkTool(label, opts) {
|
|
22
|
+
const { cmd, args, failStatus = 'fail', getVersion, fix } = opts;
|
|
23
|
+
let stdout;
|
|
24
|
+
try {
|
|
25
|
+
({ stdout } = await execa(cmd, args));
|
|
26
|
+
stdout = stdout.trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { label, status: failStatus, detail: 'not found', fix };
|
|
30
|
+
}
|
|
31
|
+
let version;
|
|
32
|
+
try {
|
|
33
|
+
version = getVersion ? await getVersion(stdout) : stdout;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* best-effort */
|
|
37
|
+
}
|
|
38
|
+
return { label, status: 'ok', detail: version };
|
|
39
|
+
}
|
|
40
|
+
// ── checks ───────────────────────────────────────────────────────────────────
|
|
41
|
+
function checkNvm() {
|
|
42
|
+
const nvmScript = path.join(HOME, '.nvm', 'nvm.sh');
|
|
43
|
+
return checkTool('nvm', {
|
|
44
|
+
cmd: 'bash',
|
|
45
|
+
args: ['-c', `source "${nvmScript}" && nvm --version`],
|
|
46
|
+
failStatus: 'warn',
|
|
47
|
+
fix: BOOTSTRAP_CMD,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function checkNode() {
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execa('node', ['--version']);
|
|
53
|
+
const current = stdout.trim();
|
|
54
|
+
let requiredNodeVersion;
|
|
55
|
+
try {
|
|
56
|
+
const nvmrc = (await readFile(path.join(Z2H_REPO_ROOT, '.nvmrc'), 'utf8')).trim();
|
|
57
|
+
requiredNodeVersion = semver.coerce(nvmrc)?.version;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// repo not cloned — skip version comparison
|
|
61
|
+
}
|
|
62
|
+
if (!requiredNodeVersion) {
|
|
63
|
+
return { label: 'node', status: 'ok', detail: current };
|
|
64
|
+
}
|
|
65
|
+
const matches = semver.satisfies(current, `~${requiredNodeVersion}`);
|
|
66
|
+
return {
|
|
67
|
+
label: 'node',
|
|
68
|
+
status: matches ? 'ok' : 'fail',
|
|
69
|
+
detail: matches ? current : `${current} (expected ${requiredNodeVersion})`,
|
|
70
|
+
fix: matches ? undefined : BOOTSTRAP_CMD,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { label: 'node', status: 'fail', detail: 'not found', fix: BOOTSTRAP_CMD };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function checkYarn() {
|
|
78
|
+
return checkTool('yarn', { cmd: 'yarn', args: ['--version'], fix: BOOTSTRAP_CMD });
|
|
79
|
+
}
|
|
80
|
+
function checkAws() {
|
|
81
|
+
return checkTool('aws', { cmd: 'aws', args: ['--version'], getVersion: s => s.split(/\s/)[0], fix: BOOTSTRAP_CMD });
|
|
82
|
+
}
|
|
83
|
+
function checkGitRemoteS3() {
|
|
84
|
+
return checkTool('git-remote-s3', {
|
|
85
|
+
cmd: 'which',
|
|
86
|
+
args: ['git-remote-s3'],
|
|
87
|
+
getVersion: async () => {
|
|
88
|
+
const { stdout: pipxOut } = await execa('pipx', ['runpip', 'git-remote-s3', 'show', 'git-remote-s3']);
|
|
89
|
+
return pipxOut.match(/^Version:\s+(.+)/m)?.[1]?.trim();
|
|
90
|
+
},
|
|
91
|
+
fix: BOOTSTRAP_CMD,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function checkZ2hCli() {
|
|
95
|
+
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
96
|
+
let latestVersion;
|
|
97
|
+
try {
|
|
98
|
+
const token = tryReadAuthTokenSync();
|
|
99
|
+
if (token) {
|
|
100
|
+
const res = await fetch(`${brokerNpmRegistryUrl()}/@mondaydotcomorg%2Fz2h-cli`, {
|
|
101
|
+
headers: { Authorization: `Bearer ${token}`, ...routingHeaders() },
|
|
102
|
+
signal: AbortSignal.timeout(5000),
|
|
103
|
+
});
|
|
104
|
+
if (res.ok) {
|
|
105
|
+
const data = (await res.json());
|
|
106
|
+
latestVersion = data['dist-tags']?.latest;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// version check is best-effort — no network / token not ready
|
|
112
|
+
}
|
|
113
|
+
if (latestVersion && latestVersion !== version) {
|
|
114
|
+
return { label: 'z2h-cli', status: 'warn', detail: `${version} (latest: ${latestVersion})`, fix: UPDATE_CMD };
|
|
115
|
+
}
|
|
116
|
+
return { label: 'z2h-cli', status: 'ok', detail: latestVersion ? `${version} (up to date)` : version };
|
|
117
|
+
}
|
|
118
|
+
async function checkAuthToken() {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = (await readJson(Z2H_AUTH_FILE));
|
|
121
|
+
if (!parsed.token) {
|
|
122
|
+
return {
|
|
123
|
+
label: 'auth token',
|
|
124
|
+
status: 'fail',
|
|
125
|
+
detail: `${Z2H_AUTH_FILE} missing token field`,
|
|
126
|
+
fix: 'z2h-cli generate-z2h-token',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { label: 'auth token', status: 'ok', detail: Z2H_AUTH_FILE };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
let detail;
|
|
133
|
+
if (err.code === 'ENOENT') {
|
|
134
|
+
detail = `${Z2H_AUTH_FILE} not found`;
|
|
135
|
+
}
|
|
136
|
+
else if (err instanceof SyntaxError) {
|
|
137
|
+
detail = `${Z2H_AUTH_FILE} could not be parsed`;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
detail = `${Z2H_AUTH_FILE} is unreadable`;
|
|
141
|
+
}
|
|
142
|
+
return { label: 'auth token', status: 'fail', detail, fix: 'z2h-cli generate-z2h-token' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function checkNpmTokenInProfile() {
|
|
146
|
+
const profiles = [
|
|
147
|
+
path.join(HOME, 'dotfiles', '.bash_profile'),
|
|
148
|
+
path.join(HOME, '.zshrc'),
|
|
149
|
+
path.join(HOME, '.bash_profile'),
|
|
150
|
+
];
|
|
151
|
+
for (const profile of profiles) {
|
|
152
|
+
if (!(await pathExists(profile))) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const content = await readFile(profile, 'utf8');
|
|
157
|
+
if (content.includes('export Z2H_NPM_TOKEN')) {
|
|
158
|
+
return { label: 'npm token in profile', status: 'ok', detail: path.basename(profile) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// skip unreadable profile
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
label: 'npm token in profile',
|
|
167
|
+
status: 'fail',
|
|
168
|
+
detail: 'export Z2H_NPM_TOKEN not found in any shell profile',
|
|
169
|
+
fix: BOOTSTRAP_CMD,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function checkWorkspace() {
|
|
173
|
+
const home = getWorkspaceHome();
|
|
174
|
+
const ok = await isZ2hWorkspace(home);
|
|
175
|
+
return {
|
|
176
|
+
label: 'workspace',
|
|
177
|
+
status: ok ? 'ok' : 'fail',
|
|
178
|
+
detail: ok ? home : `${home} not initialized`,
|
|
179
|
+
fix: ok ? undefined : 'z2h-cli create-workspace',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function checkReactVersion() {
|
|
183
|
+
const REACT_VERSION_LABEL = 'react version';
|
|
184
|
+
const REQUIRED_REACT_VERSION = '18.x';
|
|
185
|
+
const reactVersionFail = detail => ({
|
|
186
|
+
label: REACT_VERSION_LABEL,
|
|
187
|
+
status: 'fail',
|
|
188
|
+
detail,
|
|
189
|
+
fix: BOOTSTRAP_CMD,
|
|
190
|
+
});
|
|
191
|
+
const home = getWorkspaceHome();
|
|
192
|
+
let pkg;
|
|
193
|
+
try {
|
|
194
|
+
pkg = (await readJson(path.join(home, 'package.json')));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return reactVersionFail('could not read workspace package.json');
|
|
198
|
+
}
|
|
199
|
+
const reactVersion = pkg.dependencies?.react;
|
|
200
|
+
if (!reactVersion) {
|
|
201
|
+
return reactVersionFail('react not in workspace dependencies');
|
|
202
|
+
}
|
|
203
|
+
const coerced = semver.coerce(reactVersion)?.version;
|
|
204
|
+
if (!coerced || !semver.satisfies(coerced, REQUIRED_REACT_VERSION)) {
|
|
205
|
+
return reactVersionFail(`invalid react version: ${reactVersion} (expected ${REQUIRED_REACT_VERSION})`);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
label: REACT_VERSION_LABEL,
|
|
209
|
+
status: 'ok',
|
|
210
|
+
detail: reactVersion,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function checkSslCert() {
|
|
214
|
+
const ok = await pathExists(SSL_CERT_PATH);
|
|
215
|
+
return {
|
|
216
|
+
label: 'SSL cert',
|
|
217
|
+
status: ok ? 'ok' : 'warn',
|
|
218
|
+
detail: ok ? 'webpack.llama.fan' : 'not found — auto-installs on first z2h-cli dev',
|
|
219
|
+
fix: ok ? undefined : 'z2h-cli dev',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function checkPlugin(pluginKey, label, isMandatory, pluginsJson) {
|
|
223
|
+
const missingStatus = isMandatory ? 'fail' : 'warn';
|
|
224
|
+
if (!pluginsJson) {
|
|
225
|
+
return { label, status: missingStatus, detail: `${CLAUDE_PLUGINS_JSON} not found`, fix: BOOTSTRAP_CMD };
|
|
226
|
+
}
|
|
227
|
+
const entries = pluginsJson.plugins?.[pluginKey];
|
|
228
|
+
if (!entries?.length) {
|
|
229
|
+
return { label, status: missingStatus, detail: `plugin ${pluginKey} not registered`, fix: BOOTSTRAP_CMD };
|
|
230
|
+
}
|
|
231
|
+
const installPath = entries[0].installPath;
|
|
232
|
+
if (!installPath) {
|
|
233
|
+
return { label, status: missingStatus, detail: 'installPath missing', fix: BOOTSTRAP_CMD };
|
|
234
|
+
}
|
|
235
|
+
const exists = await pathExists(installPath);
|
|
236
|
+
return {
|
|
237
|
+
label,
|
|
238
|
+
status: exists ? 'ok' : missingStatus,
|
|
239
|
+
detail: exists ? installPath : `${installPath} not found`,
|
|
240
|
+
fix: exists ? undefined : BOOTSTRAP_CMD,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// ── rendering ─────────────────────────────────────────────────────────────────
|
|
244
|
+
const STATUS_ICON = {
|
|
245
|
+
ok: pc.green('✅'),
|
|
246
|
+
warn: pc.yellow('⚠️ '),
|
|
247
|
+
fail: pc.red('❌'),
|
|
248
|
+
};
|
|
249
|
+
function renderCheck(c) {
|
|
250
|
+
const icon = STATUS_ICON[c.status];
|
|
251
|
+
const label = c.label.padEnd(22);
|
|
252
|
+
const detail = c.detail ? pc.dim(c.detail) : '';
|
|
253
|
+
process.stdout.write(` ${icon} ${label}${detail}\n`);
|
|
254
|
+
}
|
|
255
|
+
// ── command ───────────────────────────────────────────────────────────────────
|
|
256
|
+
async function doctorCommand(opts = {}) {
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
setOutputMode('json');
|
|
259
|
+
}
|
|
260
|
+
let pluginsJson = null;
|
|
261
|
+
try {
|
|
262
|
+
if (await pathExists(CLAUDE_PLUGINS_JSON)) {
|
|
263
|
+
pluginsJson = (await readJson(CLAUDE_PLUGINS_JSON));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// treat as missing
|
|
268
|
+
}
|
|
269
|
+
const systemChecks = await Promise.all([checkNvm(), checkNode(), checkYarn(), checkAws(), checkGitRemoteS3()]);
|
|
270
|
+
const cliChecks = await Promise.all([checkZ2hCli(), checkAuthToken(), checkNpmTokenInProfile()]);
|
|
271
|
+
const workspaceChecks = await Promise.all([checkWorkspace(), checkReactVersion(), checkSslCert()]);
|
|
272
|
+
const pluginChecks = await Promise.all([
|
|
273
|
+
checkPlugin('z2h@bigbrain-z2h', 'z2h plugin', true, pluginsJson),
|
|
274
|
+
checkPlugin('cf-external-claude-plugin@client-foundations-ai-tools', 'cf-external plugin', false, pluginsJson),
|
|
275
|
+
checkPlugin('pr-guardrails@agentic-builders-hub', 'pr-guardrails plugin', false, pluginsJson),
|
|
276
|
+
]);
|
|
277
|
+
const sections = [
|
|
278
|
+
{ title: 'System tools', checks: systemChecks },
|
|
279
|
+
{ title: 'CLI & auth', checks: cliChecks },
|
|
280
|
+
{ title: 'Workspace', checks: workspaceChecks },
|
|
281
|
+
{ title: 'Plugins', checks: pluginChecks },
|
|
282
|
+
];
|
|
283
|
+
const allChecks = sections.flatMap(s => s.checks);
|
|
284
|
+
const issues = allChecks.filter(c => c.status !== 'ok');
|
|
285
|
+
if (opts.json) {
|
|
286
|
+
output('', {
|
|
287
|
+
ok: issues.filter(c => c.status === 'fail').length === 0,
|
|
288
|
+
checks: allChecks,
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
for (const section of sections) {
|
|
293
|
+
process.stdout.write(`\n${pc.bold(section.title)}\n`);
|
|
294
|
+
for (const check of section.checks) {
|
|
295
|
+
renderCheck(check);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
process.stdout.write('\n');
|
|
299
|
+
if (issues.length === 0) {
|
|
300
|
+
process.stdout.write(pc.bold(pc.green('No issues detected 🚀\n')));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
const fails = issues.filter(c => c.status === 'fail').length;
|
|
304
|
+
const warns = issues.filter(c => c.status === 'warn').length;
|
|
305
|
+
const parts = [
|
|
306
|
+
fails && `${fails} failure${fails > 1 ? 's' : ''}`,
|
|
307
|
+
warns && `${warns} warning${warns > 1 ? 's' : ''}`,
|
|
308
|
+
].filter(Boolean);
|
|
309
|
+
process.stdout.write(pc.yellow(`${parts.join(', ')} found\n`));
|
|
310
|
+
// Deduplicated fixes in the order they were first encountered
|
|
311
|
+
const fixes = [...new Set(issues.map(c => c.fix).filter(Boolean))];
|
|
312
|
+
if (fixes.length > 0) {
|
|
313
|
+
process.stdout.write('\n');
|
|
314
|
+
process.stdout.write(pc.bold(pc.red('In order to fix these issues, run the following commands:')));
|
|
315
|
+
process.stdout.write('\n');
|
|
316
|
+
for (const fix of fixes) {
|
|
317
|
+
process.stdout.write(` ${pc.cyan(fix)}\n`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write('\n');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export { doctorCommand };
|
package/dist/esm/index.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { generateZ2HTokenCommand } from './commands/generate-z2h-token.mjs';
|
|
|
16
16
|
import { migrateCommand } from './commands/migrate.mjs';
|
|
17
17
|
import { tagCommand } from './commands/tag.mjs';
|
|
18
18
|
import { editCommand } from './commands/edit.mjs';
|
|
19
|
+
import { doctorCommand } from './commands/doctor.mjs';
|
|
19
20
|
import { fail, errorLog, getOutputMode } from './util/logger.mjs';
|
|
20
21
|
import { trackDuration } from './util/tracker.mjs';
|
|
21
22
|
import { getCwdAppName } from './util/app-name.mjs';
|
|
@@ -257,6 +258,18 @@ async function main() {
|
|
|
257
258
|
await fail(`migrate failed: ${err.message}`, { command: 'migrate' }, 1);
|
|
258
259
|
}
|
|
259
260
|
});
|
|
261
|
+
program
|
|
262
|
+
.command('doctor')
|
|
263
|
+
.description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
|
|
264
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
265
|
+
.action(async (opts) => {
|
|
266
|
+
try {
|
|
267
|
+
await doctorCommand(opts);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
await fail(`doctor failed: ${err.message}`, { command: 'doctor' }, 1);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
260
273
|
await program.parseAsync(process.argv);
|
|
261
274
|
}
|
|
262
275
|
void main()
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const commands_generateZ2hToken = require('./commands/generate-z2h-token.js');
|
|
|
16
16
|
const commands_migrate = require('./commands/migrate.js');
|
|
17
17
|
const commands_tag = require('./commands/tag.js');
|
|
18
18
|
const commands_edit = require('./commands/edit.js');
|
|
19
|
+
const commands_doctor = require('./commands/doctor.js');
|
|
19
20
|
const util_logger = require('./util/logger.js');
|
|
20
21
|
const util_tracker = require('./util/tracker.js');
|
|
21
22
|
const util_appName = require('./util/app-name.js');
|
|
@@ -263,6 +264,18 @@ async function main() {
|
|
|
263
264
|
await util_logger.fail(`migrate failed: ${err.message}`, { command: 'migrate' }, 1);
|
|
264
265
|
}
|
|
265
266
|
});
|
|
267
|
+
program
|
|
268
|
+
.command('doctor')
|
|
269
|
+
.description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
|
|
270
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
271
|
+
.action(async (opts) => {
|
|
272
|
+
try {
|
|
273
|
+
await commands_doctor.doctorCommand(opts);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
await util_logger.fail(`doctor failed: ${err.message}`, { command: 'doctor' }, 1);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
266
279
|
await program.parseAsync(process.argv);
|
|
267
280
|
}
|
|
268
281
|
void main()
|