@myapihq/cli 2.25.0 → 2.25.1

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.
@@ -786,6 +786,7 @@ Subcommands:
786
786
  domain <id> <domain> Bind a custom domain (--remove to unbind)
787
787
  env <id> Change environment variables (merge; rolls a new revision)
788
788
  get <id> Inspect a container
789
+ health-check <id> Set or clear the startup probe (--clear); rolls a new revision
789
790
  list List containers in your org
790
791
  logs <id> Show recent runtime logs (--tail <n>, --scope all) — see build-logs for build failures
791
792
  promote <id> <rev> Move all traffic to a revision (seconds, no rebuild)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ // A verb you cannot find is a verb you do not have.
2
+ //
3
+ // `container health-check` shipped dispatched, documented in SUBCOMMAND_USAGE,
4
+ // covered by tests and named in the skill — and absent from the list that
5
+ // `myapi container --help` prints. The only way to discover it was to already
6
+ // know it existed. It was found by running the published binary after release,
7
+ // which is not a mechanism.
8
+ //
9
+ // The existing help linter checks that listings are ALPHABETICAL. Nothing
10
+ // checked that they are COMPLETE, so a new verb could be sorted correctly into
11
+ // a list it was not in.
12
+ //
13
+ // This drives each command's own help output — the exact string the user sees,
14
+ // not a listing block matched by a regex, because two earlier attempts at this
15
+ // measured the wrong thing: one required a `Subcommands:` header that `task`
16
+ // does not use, and one accepted an entry in SUBCOMMAND_USAGE, which is what
17
+ // health-check already had while remaining invisible.
18
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
19
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ const COMMANDS_DIR = fileURLToPath(new URL('.', import.meta.url));
23
+ // Verbs deliberately absent from the printed list, each with a reason.
24
+ // A hidden verb is a decision; an accidentally hidden one is the bug above.
25
+ const HIDDEN_OK = {
26
+ 'database.ts': {
27
+ keys: 'undocumented alias of `entries`, kept so existing scripts keep working; ' +
28
+ 'the code says it goes no earlier than the next minor',
29
+ },
30
+ 'org.ts': {
31
+ import: 'deprecated alias of `sync-brand`; still dispatches and prints a ' +
32
+ 'deprecation notice, deliberately absent from the listing',
33
+ },
34
+ };
35
+ function moduleFiles() {
36
+ const out = [];
37
+ for (const entry of readdirSync(COMMANDS_DIR).sort()) {
38
+ const p = path.join(COMMANDS_DIR, entry);
39
+ if (statSync(p).isDirectory()) {
40
+ const idx = path.join(p, 'index.ts');
41
+ try {
42
+ statSync(idx);
43
+ out.push(`${entry}/index.ts`);
44
+ }
45
+ catch { /* no dispatcher */ }
46
+ }
47
+ else if (entry.endsWith('.ts') && !entry.includes('.test.')) {
48
+ out.push(entry);
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ // Only the TOP-LEVEL dispatcher's cases. A nested switch — `account registrant
54
+ // set`, `domain records create`, `llm cache list` — belongs to a sub-verb that
55
+ // the parent line documents inline, and counting those made the check report
56
+ // three commands that are fine.
57
+ function dispatchedVerbs(src) {
58
+ const start = src.indexOf('export async function run(');
59
+ if (start === -1)
60
+ return [];
61
+ let i = src.indexOf('{', start);
62
+ if (i === -1)
63
+ return [];
64
+ let depth = 0, end = src.length;
65
+ for (let j = i; j < src.length; j++) {
66
+ if (src[j] === '{')
67
+ depth++;
68
+ else if (src[j] === '}') {
69
+ depth--;
70
+ if (depth === 0) {
71
+ end = j;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ const body = src.slice(i, end);
77
+ return [...new Set([...body.matchAll(/case\s+'([a-z][a-z0-9-]*)'\s*:/g)].map(m => m[1]))];
78
+ }
79
+ let printed;
80
+ beforeEach(async () => {
81
+ printed = [];
82
+ const output = await import('../output.js');
83
+ const capture = ((m) => { printed.push(String(m ?? '')); });
84
+ vi.spyOn(output, 'info').mockImplementation(capture);
85
+ vi.spyOn(output, 'success').mockImplementation(capture);
86
+ vi.spyOn(output, 'printJson').mockImplementation(capture);
87
+ vi.spyOn(output, 'error').mockImplementation(((m) => {
88
+ printed.push(String(m));
89
+ throw new Error('__EXIT__');
90
+ }));
91
+ });
92
+ afterEach(() => vi.restoreAllMocks());
93
+ async function helpTextOf(file) {
94
+ const mod = await import(`./${file}`);
95
+ if (typeof mod.run !== 'function')
96
+ return '';
97
+ try {
98
+ await mod.run(undefined, [], {});
99
+ }
100
+ catch (e) {
101
+ if (e?.message !== '__EXIT__')
102
+ throw e;
103
+ }
104
+ return printed.join('\n');
105
+ }
106
+ describe('every dispatched verb appears in its command help', () => {
107
+ const files = moduleFiles();
108
+ it('no waiver claims to hide a verb that is listed anyway', async () => {
109
+ const stale = [];
110
+ for (const [file, waived] of Object.entries(HIDDEN_OK)) {
111
+ const help = await helpTextOf(file);
112
+ for (const verb of Object.keys(waived)) {
113
+ if (new RegExp(`^\\s+\`?${verb}\\b`, 'm').test(help))
114
+ stale.push(`${file}: ${verb}`);
115
+ }
116
+ printed = [];
117
+ }
118
+ expect(stale, 'these are in the help now, so the waiver claims a gap that is ' +
119
+ 'closed — delete the entry.').toEqual([]);
120
+ });
121
+ it('found command modules to check (or this test proves nothing)', () => {
122
+ expect(files.length).toBeGreaterThan(10);
123
+ });
124
+ for (const file of files) {
125
+ const src = readFileSync(path.join(COMMANDS_DIR, file), 'utf8');
126
+ const verbs = dispatchedVerbs(src);
127
+ if (!verbs.length)
128
+ continue;
129
+ it(`${file} lists all ${verbs.length} of its verbs`, async () => {
130
+ const help = await helpTextOf(file);
131
+ // A module whose bare `run()` does not print help has nothing to check
132
+ // here; its verbs are reachable from the parent listing.
133
+ if (!help.trim())
134
+ return;
135
+ const waived = HIDDEN_OK[file] ?? {};
136
+ const missing = verbs
137
+ .filter(v => !(v in waived))
138
+ // Listed means: named at the start of an indented line, the shape
139
+ // every command uses for its entries.
140
+ .filter(v => !new RegExp(`^\\s+\`?${v}\\b`, 'm').test(help));
141
+ expect(missing, `${file}: ${missing.join(', ')} dispatch but are not in the ` +
142
+ 'help this command prints. A verb nobody can find is a verb nobody has — ' +
143
+ 'add it to the listing, or to HIDDEN_OK with a reason.').toEqual([]);
144
+ });
145
+ }
146
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.25.0",
4
+ "version": "2.25.1",
5
5
  "description": "MyAPI command-line interface",
6
6
  "repository": {
7
7
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
47
47
  },
48
48
  "dependencies": {
49
- "@myapihq/sdk": "^2.25.0"
49
+ "@myapihq/sdk": "^2.25.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",