@myapihq/cli 2.27.0 → 2.27.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.
@@ -9,6 +9,10 @@ import { requireOrg } from '../helpers.js';
9
9
  // OIDC endpoints (discovery/jwks/authorize/token/userinfo) are consumed by
10
10
  // apps + the JS SDK, not the CLI, so only the management surface is here.
11
11
  export const SCHEMA = {
12
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
13
+ // called `--org` a typo on a command that had just honoured it — telling
14
+ // people the one flag that keeps writes in the right tenant is unrecognized.
15
+ org: 'string',
12
16
  name: 'string',
13
17
  type: 'string', // spa | web
14
18
  redirect: 'list', // comma-separated OR repeated; occurrences accumulate
@@ -20,6 +20,10 @@ export const EXPOSES = [
20
20
  'DELETE /container/orgs/{org_id}/containers/{id}/domain',
21
21
  ];
22
22
  export const SCHEMA = {
23
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
24
+ // called `--org` a typo on a command that had just honoured it — telling
25
+ // people the one flag that keeps writes in the right tenant is unrecognized.
26
+ org: 'string',
23
27
  name: 'string',
24
28
  type: 'string',
25
29
  cron: 'string',
@@ -32,7 +32,11 @@ export const EXPOSES = [
32
32
  // Lifetime delivery counts, to replace the 30-day 'quiet webhook' advice.
33
33
  'GET /webhook/orgs/{org_id}/deliveries',
34
34
  ];
35
- export const SCHEMA = {};
35
+ export const SCHEMA = {
36
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
37
+ // called `--org` a typo on a command that had just honoured it.
38
+ org: 'string',
39
+ };
36
40
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
37
41
  const C = useColor ? {
38
42
  ok: '\x1b[32m', warn: '\x1b[33m', err: '\x1b[31m',
@@ -22,6 +22,10 @@ export const EXPOSES = [
22
22
  'DELETE /feedback/orgs/{org_id}/widgets/{id}',
23
23
  ];
24
24
  export const SCHEMA = {
25
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
26
+ // called `--org` a typo on a command that had just honoured it — telling
27
+ // people the one flag that keeps writes in the right tenant is unrecognized.
28
+ org: 'string',
25
29
  // Declared because `list` reads flags.limit/offset, --help documents both,
26
30
  // and a truncated page prints "raise --limit" — while the parser refused
27
31
  // them as unknown. Reported by ImmoPilot on 2026-08-19.
@@ -15,6 +15,10 @@ export const EXPOSES = [
15
15
  'PATCH /function/orgs/{org_id}/functions/{id}/scopes',
16
16
  ];
17
17
  export const SCHEMA = {
18
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
19
+ // called `--org` a typo on a command that had just honoured it — telling
20
+ // people the one flag that keeps writes in the right tenant is unrecognized.
21
+ org: 'string',
18
22
  // `create` takes a positional name; --name stays accepted for
19
23
  // back-compat (RELEASING.md's stated convention) and must be declared
20
24
  // here so the command-aware flag check in index.ts doesn't flag it.
@@ -24,6 +24,10 @@ export const EXPOSES = [
24
24
  'GET /hq/orgs/{org_id}',
25
25
  ];
26
26
  export const SCHEMA = {
27
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
28
+ // called `--org` a typo on a command that had just honoured it — telling
29
+ // people the one flag that keeps writes in the right tenant is unrecognized.
30
+ org: 'string',
27
31
  funnel: 'string',
28
32
  slug: 'string',
29
33
  env: 'string',
@@ -24,6 +24,10 @@ export const EXPOSES = [
24
24
  'POST /git/orgs/{org_id}/repos/{repo}/repack',
25
25
  ];
26
26
  export const SCHEMA = {
27
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
28
+ // called `--org` a typo on a command that had just honoured it — telling
29
+ // people the one flag that keeps writes in the right tenant is unrecognized.
30
+ org: 'string',
27
31
  name: 'string',
28
32
  'default-branch': 'string',
29
33
  ref: 'string',
@@ -13,6 +13,10 @@ export const EXPOSES = [
13
13
  'POST /payments/webhook/{org_id}',
14
14
  ];
15
15
  export const SCHEMA = {
16
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
17
+ // called `--org` a typo on a command that had just honoured it — telling
18
+ // people the one flag that keeps writes in the right tenant is unrecognized.
19
+ org: 'string',
16
20
  'stripe-key': 'string',
17
21
  amount: 'string',
18
22
  description: 'string',
@@ -13,6 +13,10 @@ export const EXPOSES = [
13
13
  'GET /queue/orgs/{org_id}/jobs/{id}',
14
14
  ];
15
15
  export const SCHEMA = {
16
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
17
+ // called `--org` a typo on a command that had just honoured it — telling
18
+ // people the one flag that keeps writes in the right tenant is unrecognized.
19
+ org: 'string',
16
20
  name: 'string',
17
21
  'consumer-url': 'string',
18
22
  'max-attempts': 'number',
@@ -22,6 +22,10 @@ function taskOrigin(flags) {
22
22
  return typeof v === 'string' ? v : undefined;
23
23
  }
24
24
  export const SCHEMA = {
25
+ // Declared because requireOrg reads it. Undeclared, the foreign-flag check
26
+ // called `--org` a typo on a command that had just honoured it — telling
27
+ // people the one flag that keeps writes in the right tenant is unrecognized.
28
+ org: 'string',
25
29
  body: 'string',
26
30
  importance: 'string',
27
31
  due: 'string',
package/dist/errors.js CHANGED
@@ -125,6 +125,28 @@ export function friendlyError(err) {
125
125
  out += '\n Current numbers: myapi account sending';
126
126
  return withOrgContext(out, err);
127
127
  }
128
+ // A failure on the platform's side, with no message of its own. The handler
129
+ // that raises it calls envelope.Error(w, r, 500, "internal_error") — a bare
130
+ // string, so there is no `message` to fall through to and the caller was left
131
+ // looking at the word `internal_error` alone: no cause, no next step, and
132
+ // nothing to quote at anybody.
133
+ //
134
+ // The one genuinely useful thing about a 500 is its request id, which the
135
+ // platform returns on every response and the SDK used to discard.
136
+ //
137
+ // Deliberately claims nothing about what did or did not happen. A 500 can
138
+ // land before or after the write, and inventing "nothing was changed" here
139
+ // would be the kind of reassuring guess this codebase keeps having to undo.
140
+ if (err.status >= 500 || err.code === 'internal_error') {
141
+ const lines = ['The platform failed on this request — this is our fault, not a bad command.'];
142
+ if (err.detail && err.detail !== err.code)
143
+ lines.push(` ${err.detail}`);
144
+ lines.push(err.requestId
145
+ ? ` Quote this when reporting it: ${err.requestId}`
146
+ : ' No request id came back, so quote the exact command and the time instead.');
147
+ lines.push(' Whether the change went through is not knowable from here — check before retrying.');
148
+ return withOrgContext(lines.join('\n'), err);
149
+ }
128
150
  const base = ERROR_MESSAGES[err.code] || err.code;
129
151
  // Generic fallback: append the backend's `message` when it adds info beyond
130
152
  // the friendly mapping. Future per-service detail fields can be added here.
@@ -68,3 +68,42 @@ describe('paused sending points at the CLI, not an HTTP route', () => {
68
68
  expect(msg).toContain('myapi account resume-sending');
69
69
  });
70
70
  });
71
+ describe('a 500 must be reportable', () => {
72
+ // `envelope.Error(w, r, 500, "internal_error")` sends a bare string: no
73
+ // message to fall through to. The caller used to see the word
74
+ // `internal_error` alone — no cause, no next step, nothing to quote. Hit it
75
+ // for real on 2026-08-23 running `funnel push` with an empty --funnel.
76
+ //
77
+ // The one useful thing about a 500 is the request id, which the platform
78
+ // returns on every response and the SDK discarded.
79
+ function fail(code, status, requestId, detail) {
80
+ const e = new MyApiError(code, status, detail);
81
+ if (requestId)
82
+ e.requestId = requestId;
83
+ return e;
84
+ }
85
+ it('quotes the request id so it can be reported and traced', () => {
86
+ const out = friendlyError(fail('internal_error', 500, 'req_1adc0b171e187b45'));
87
+ expect(out).toContain('req_1adc0b171e187b45');
88
+ expect(out).toMatch(/our fault/i);
89
+ });
90
+ it('says what to quote when no request id came back', () => {
91
+ const out = friendlyError(fail('internal_error', 500));
92
+ expect(out).toMatch(/exact command/i);
93
+ });
94
+ it('never claims the write did not happen', () => {
95
+ // A 500 can land either side of the write. Saying "nothing was changed"
96
+ // would be a reassuring guess, and this codebase keeps having to undo those.
97
+ const out = friendlyError(fail('internal_error', 500, 'req_x'));
98
+ expect(out).not.toMatch(/nothing was (changed|written|charged)/i);
99
+ expect(out).toMatch(/not knowable|check before retrying/i);
100
+ });
101
+ it('applies to any 5xx, not only the internal_error code', () => {
102
+ expect(friendlyError(fail('BAD_GATEWAY', 502, 'req_y'))).toContain('req_y');
103
+ });
104
+ it('leaves 4xx alone — those have their own guidance', () => {
105
+ const out = friendlyError(fail('INVALID_SLUG', 422, 'req_z', 'slug must look like /about'));
106
+ expect(out).toContain('slug must look like /about');
107
+ expect(out).not.toMatch(/our fault/i);
108
+ });
109
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ // A command that reads `--org` must declare it.
2
+ //
3
+ // Ten command modules called requireOrg() — which reads flags.org — without
4
+ // listing `org` in their SCHEMA. The flag still worked, because requireOrg
5
+ // reads the raw flags. But the foreign-flag check did not know about it, so
6
+ // every one of them answered a correct command with:
7
+ //
8
+ // › Note: unknown flag(s) not recognized by this command: --org
9
+ // (took value "c2c4810d-…") — check for typos.
10
+ //
11
+ // and then used the value anyway. `funnel list --org <other>` returned the
12
+ // other org's funnels while calling the flag a typo.
13
+ //
14
+ // This is not cosmetic. `--org` is the flag that keeps a write in the right
15
+ // tenant, and telling people it is unrecognized is an invitation to drop it and
16
+ // rely on the machine-wide default — which is precisely the habit behind the
17
+ // cross-project collisions customers report. An agent reading stderr is even
18
+ // likelier to "fix" the typo by removing the flag.
19
+ //
20
+ // Found by running a real command and reading what it printed, not from a test.
21
+ import { describe, it, expect } from 'vitest';
22
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
23
+ import * as path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ const COMMANDS_DIR = fileURLToPath(new URL('./commands/', import.meta.url));
26
+ function moduleFiles() {
27
+ const out = [];
28
+ for (const entry of readdirSync(COMMANDS_DIR).sort()) {
29
+ const p = path.join(COMMANDS_DIR, entry);
30
+ if (statSync(p).isDirectory()) {
31
+ const idx = path.join(p, 'index.ts');
32
+ try {
33
+ statSync(idx);
34
+ out.push(`${entry}/index.ts`);
35
+ }
36
+ catch { /* no dispatcher */ }
37
+ }
38
+ else if (entry.endsWith('.ts') && !entry.includes('.test.')) {
39
+ out.push(entry);
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ describe('every command that reads --org declares it', () => {
45
+ const files = moduleFiles();
46
+ it('found command modules (or this proves nothing)', () => {
47
+ expect(files.length).toBeGreaterThan(10);
48
+ });
49
+ it('no module uses requireOrg without declaring org in its SCHEMA', () => {
50
+ const undeclared = [];
51
+ for (const file of files) {
52
+ const src = readFileSync(path.join(COMMANDS_DIR, file), 'utf8');
53
+ const readsOrg = /\brequireOrg\s*\(|\brequireExplicitOrg\s*\(/.test(src);
54
+ if (!readsOrg)
55
+ continue;
56
+ // A module with no SCHEMA of its own inherits nothing — it still needs
57
+ // the declaration, so absence counts as undeclared rather than exempt.
58
+ const declares = /^\s*org:\s*'string',/m.test(src);
59
+ if (!declares)
60
+ undeclared.push(file);
61
+ }
62
+ expect(undeclared, `${undeclared.join(', ')} call requireOrg but do not declare ` +
63
+ "`org: 'string'` in SCHEMA. The flag will still work and the CLI will call it a " +
64
+ 'typo — on the one flag that keeps a write in the right tenant.').toEqual([]);
65
+ });
66
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.27.0",
4
+ "version": "2.27.2",
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.27.0"
49
+ "@myapihq/sdk": "^2.27.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",