@myapihq/cli 1.3.1 → 1.3.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.
@@ -0,0 +1,6 @@
1
+ import type { FlagSchema } from '../flags.js';
2
+ import { type Flags } from '../helpers.js';
3
+ import type { Exposes } from '../exposes.js';
4
+ export declare const EXPOSES: Exposes;
5
+ export declare const SCHEMA: FlagSchema;
6
+ export declare function run(_subcommand: string | undefined, _args: string[], flags: Flags): Promise<void>;
@@ -0,0 +1,154 @@
1
+ // `myapi doctor` — thin client over the backend's GET /hq/orgs/{org_id}/doctor.
2
+ //
3
+ // The backend computes the heavy reference-integrity / orphan / activity
4
+ // checks across slots and returns a structured report. The CLI's job is:
5
+ // 1. fetch the report,
6
+ // 2. augment with *customer-perspective* probes the backend structurally
7
+ // can't run (today: DNS resolution from the user's network),
8
+ // 3. render with section grouping, color, and exit codes for CI.
9
+ import { promises as dns } from 'node:dns';
10
+ import { hq as sdkHq } from '@myapihq/sdk';
11
+ import { requireConfig } from '../config.js';
12
+ import { info, error, printJson } from '../output.js';
13
+ import { requireOrg } from '../helpers.js';
14
+ export const EXPOSES = [
15
+ 'GET /hq/orgs/{org_id}/doctor',
16
+ ];
17
+ export const SCHEMA = {};
18
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
19
+ const C = useColor ? {
20
+ ok: '\x1b[32m', warn: '\x1b[33m', err: '\x1b[31m',
21
+ dim: '\x1b[2m', bold: '\x1b[1m', reset: '\x1b[0m',
22
+ } : { ok: '', warn: '', err: '', dim: '', bold: '', reset: '' };
23
+ const MARK = {
24
+ ok: `${C.ok}✓${C.reset}`,
25
+ warn: `${C.warn}⚠${C.reset}`,
26
+ crit: `${C.err}✗${C.reset}`,
27
+ };
28
+ function rule(width = 60) {
29
+ return `${C.dim}${'─'.repeat(width)}${C.reset}`;
30
+ }
31
+ function fmtIssue(i) {
32
+ const head = ` ${MARK[i.severity] ?? '·'} ${i.message}`;
33
+ return i.hint ? `${head}\n ${C.dim}→ ${i.hint}${C.reset}` : head;
34
+ }
35
+ // Local DNS-resolution probe for every distinct domain the report names.
36
+ // The backend can verify domain provisioning state from its own egress;
37
+ // this checks whether the operator's network reaches them today — a
38
+ // different epistemic signal worth surfacing on top of the backend's view.
39
+ async function dnsProbeSection(report) {
40
+ const names = new Set();
41
+ for (const s of report.sections) {
42
+ for (const i of s.issues) {
43
+ if (i.entity?.slot === 'domain' && i.entity.name)
44
+ names.add(i.entity.name);
45
+ }
46
+ }
47
+ if (names.size === 0)
48
+ return null;
49
+ const issues = [];
50
+ await Promise.all([...names].map(async (name) => {
51
+ try {
52
+ const ips = await dns.resolve4(name);
53
+ issues.push({
54
+ id: `dns_local_ok/${name}`,
55
+ severity: 'ok',
56
+ scope: `local/${name}`,
57
+ entity: { slot: 'domain', id: '', name },
58
+ category: 'network',
59
+ message: `${name} resolves from your network${ips.length ? ` (${ips[0]}${ips.length > 1 ? ` +${ips.length - 1}` : ''})` : ''}`,
60
+ });
61
+ }
62
+ catch (e) {
63
+ issues.push({
64
+ id: `dns_local_fail/${name}`,
65
+ severity: 'warn',
66
+ scope: `local/${name}`,
67
+ entity: { slot: 'domain', id: '', name },
68
+ category: 'network',
69
+ message: `${name} did not resolve from your network`,
70
+ hint: e?.code || e?.message || String(e),
71
+ });
72
+ }
73
+ }));
74
+ const warns = issues.filter(i => i.severity === 'warn').length;
75
+ return {
76
+ name: 'local network',
77
+ summary: warns ? `${warns} resolution failure${warns === 1 ? '' : 's'}` : `${issues.length} domain${issues.length === 1 ? '' : 's'} resolved`,
78
+ issues,
79
+ };
80
+ }
81
+ export async function run(_subcommand, _args, flags) {
82
+ if (flags.help) {
83
+ info(HELP);
84
+ return;
85
+ }
86
+ const config = requireConfig();
87
+ const orgId = requireOrg(flags, config, 'myapi doctor [--json] [--verbose]');
88
+ const apiKey = config.api_key;
89
+ const verbose = !!flags.verbose;
90
+ const wantJson = !!flags.json;
91
+ let report;
92
+ try {
93
+ report = await sdkHq.getDoctor(apiKey, orgId);
94
+ }
95
+ catch (e) {
96
+ error(`doctor endpoint failed: ${e?.message ?? String(e)}`);
97
+ }
98
+ const localSection = await dnsProbeSection(report);
99
+ if (localSection)
100
+ report.sections.push(localSection);
101
+ // Re-tally totals after local augmentation.
102
+ const totals = { ok: 0, warn: 0, crit: 0 };
103
+ for (const s of report.sections)
104
+ for (const i of s.issues)
105
+ totals[i.severity]++;
106
+ if (wantJson) {
107
+ printJson({ ...report, totals });
108
+ return;
109
+ }
110
+ info(`${C.bold}Org doctor${C.reset} ${C.dim}· org ${report.org_id}${C.reset} ${C.dim}· ${report.generated_at}${C.reset}`);
111
+ for (const s of report.sections) {
112
+ info('');
113
+ info(`${C.bold}# ${s.name}${C.reset} ${C.dim}· ${s.summary}${C.reset}`);
114
+ for (const i of s.issues) {
115
+ if (!verbose && i.severity === 'ok')
116
+ continue;
117
+ info(fmtIssue(i));
118
+ }
119
+ }
120
+ info('');
121
+ info(rule(60));
122
+ if (totals.crit) {
123
+ info(`${MARK.crit} ${totals.crit} critical, ${MARK.warn} ${totals.warn} warning${totals.warn === 1 ? '' : 's'}`);
124
+ process.exitCode = 1;
125
+ }
126
+ else if (totals.warn) {
127
+ info(`${MARK.warn} ${totals.warn} warning${totals.warn === 1 ? '' : 's'}, no critical issues`);
128
+ }
129
+ else {
130
+ info(`${MARK.ok} ${totals.ok} check${totals.ok === 1 ? '' : 's'} passed`);
131
+ }
132
+ if (!verbose)
133
+ info(`${C.dim}(--verbose to show passing checks · --json for machine-readable)${C.reset}`);
134
+ }
135
+ const HELP = `Usage: myapi doctor [--verbose] [--json] [--org <id>]
136
+
137
+ Org-wide consistency check. Fetches the structured report from the backend
138
+ (GET /hq/orgs/{org_id}/doctor) and augments it with customer-perspective
139
+ probes (DNS resolution from this machine's network).
140
+
141
+ Sections returned by the backend today:
142
+ funnels, webhooks, workflows, domains, containers, emails, payments
143
+
144
+ Local additions:
145
+ network — DNS resolution from your egress for each domain mentioned.
146
+
147
+ Exit codes:
148
+ 0 no critical issues (warnings allowed)
149
+ 1 one or more critical issues
150
+
151
+ Options:
152
+ --verbose Show passing checks too.
153
+ --json Machine-readable output.
154
+ --org <id> Override the default org.`;
@@ -28,7 +28,7 @@ const BLOCK_END = `# end ${PROGRAM} completion`;
28
28
  export const COMMANDS = [
29
29
  'audience', 'auth', 'billing', 'company', 'completion', 'config', 'container',
30
30
  'crm', 'database', 'domain', 'email', 'fn', 'funnel', 'git', 'help', 'image',
31
- 'install-skills', 'keys', 'llm', 'org', 'payments', 'people', 'pixel', 'queue',
31
+ 'doctor', 'install-skills', 'keys', 'llm', 'org', 'payments', 'people', 'pixel', 'queue',
32
32
  'setup', 'status', 'storage', 'task', 'update', 'url', 'webhook', 'whoami',
33
33
  'workflow',
34
34
  ];
@@ -43,6 +43,7 @@ const COMMAND_MODULES = [
43
43
  './commands/git.js',
44
44
  './commands/queue.js',
45
45
  './commands/task.js',
46
+ './commands/doctor.js',
46
47
  ];
47
48
  const ENDPOINT_PATTERN = /^(GET|POST|PATCH|PUT|DELETE) \/[A-Za-z0-9_\-./{}]*$/;
48
49
  describe('every CLI command exports a typed EXPOSES array (S-101)', () => {
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ import * as containerCmd from './commands/container.js';
35
35
  import * as gitCmd from './commands/git.js';
36
36
  import * as queueCmd from './commands/queue.js';
37
37
  import * as taskCmd from './commands/task.js';
38
+ import * as doctorCmd from './commands/doctor.js';
38
39
  // Each command file declares the value flags it understands. We union them
39
40
  // into a single schema for the upfront parse, so adding a new value flag in
40
41
  // one command means editing one file (its SCHEMA), not a global allowlist.
@@ -66,6 +67,7 @@ const COMBINED_SCHEMA = {
66
67
  ...gitCmd.SCHEMA,
67
68
  ...queueCmd.SCHEMA,
68
69
  ...taskCmd.SCHEMA,
70
+ ...doctorCmd.SCHEMA,
69
71
  // Top-level flags
70
72
  version: 'boolean',
71
73
  V: 'boolean',
@@ -234,6 +236,9 @@ async function main() {
234
236
  case 'task':
235
237
  await taskCmd.run(subcommand, restArgs, flags);
236
238
  break;
239
+ case 'doctor':
240
+ await doctorCmd.run(subcommand, restArgs, flags);
241
+ break;
237
242
  // Convenience aliases
238
243
  case 'setup':
239
244
  await setupCmd.setup(flags);
@@ -360,6 +365,7 @@ const HELP_TARGETS = {
360
365
  git: f => gitCmd.run(undefined, [], f),
361
366
  queue: f => queueCmd.run(undefined, [], f),
362
367
  task: f => taskCmd.run(undefined, [], f),
368
+ doctor: f => doctorCmd.run(undefined, [], f),
363
369
  org: f => orgCmd.run(undefined, [], f),
364
370
  billing: f => billingCmd.run(undefined, [], f),
365
371
  keys: f => keysCmd.run(undefined, [], f),
@@ -406,6 +412,7 @@ Commands:
406
412
  git Hosted git repositories — repos, commits, branches, history
407
413
  queue Durable job queue — enqueue work, retried against an HTTP consumer
408
414
  task Agent-task queue — file, claim, and resolve units of work
415
+ doctor Org-wide consistency check — funnels, webhooks, domains, containers
409
416
  payments Take payments with Stripe Checkout (connect, charge, refund)
410
417
  webhook Manage inbound webhook endpoints and inspect deliveries
411
418
  email Manage mailboxes, send/read email, templates, and campaigns
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "1.3.1",
4
+ "version": "1.3.2",
5
5
  "description": "MyAPI command-line interface",
6
6
  "type": "module",
7
7
  "files": [
@@ -29,7 +29,7 @@
29
29
  "lint:changelog": "node ../../scripts/lint-changelog.js"
30
30
  },
31
31
  "dependencies": {
32
- "@myapihq/sdk": "^1.3.1"
32
+ "@myapihq/sdk": "^1.3.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^25.6.0",