You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a4015ab
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): a flag given without its value (os dev -d) opens with the INVOCATION ERROR line (#29004)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XSSJD6C4GLhhpJrw1FTNZZn)_Fixes#28931
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`os dev -d`、`os serve --port`、`os migrate meta --from`
这类「旗标没给值」的调用,现在和未知旗标一样,stderr 第一行先打出 `objectstack: INVOCATION ERROR —
Flag --database expects a value. The command never ran: nothing was
started and nothing is
listening.`。同类的另外三种解析失败也一并补上:带选项列表的旗标没给值(`--log-level`)、单值旗标给了两次(`-p 1
-p 2`)、旗标自带解析拒收的值(`--eval-min abc`)。
- 为什么改:原来这几种只有 oclif 一行 `Error: Flag --database expects a value`,退出码
2。后台跑时看起来像服务起来又挂了,读的人会去排查一个从没启动过的应用。
- 风险与回滚:只改 stderr 首行;接受/拒收的输入、退出码 2、oclif
自己的后续行都不变。命令解析成功后在运行中失败的错误(`this.error()`)不加横幅,有正反两个测试钉住。回滚即还原本 PR。
- 先看:`packages/cli/src/utils/invocation.ts` 的
`isInvocationError`(新增按抛出位置识别:错误自身栈里有 oclif
解析器模块的帧);`src/utils/invocation.test.ts` 新增的用例。
- 两分钟验证:在任一应用目录跑 `node packages/cli/bin/run.js dev -d`,预期退出码 2,stderr
第一行是 `objectstack: INVOCATION ERROR — Flag --database expects a value.
...`,下面仍是 oclif 原来那一行。
- 需要你做的:无,席位落地。
## What changes
- **`isInvocationError()` gains a second reading: the throw site.**
oclif's parser throws a plain `CLIError` (no `parse` / `showHelp` own
fields) for a flag given without its value, a single-valued flag given
twice and a bare option-list flag, and passes a flag's own `parse`
failure (`Flags.integer` on `abc`) through. Those errors carry nothing a
runtime failure does not: a command that parsed and then called
`this.error()` throws the same class, with the same own fields
(`stack,message,code,oclif,skipOclifErrorHandling,suggestions`) and the
same `oclif.exit` of 2 (measured, and pinned in the unit file). So the
order's hypothesised readings (class / own fields / `oclif.exit`) cannot
separate them. What differs is where the error was thrown: the predicate
reads the error's own `stack` for a frame inside
`@oclif/core/lib/parser/` (either path separator). A command body never
runs with that module on the stack, because `this.parse()` has returned
before it does. No English message text is matched.
- **The `CLIParseError` reading is unchanged** and still covers an
unknown flag, a refused option value, a missing required flag, a missing
required arg and an unexpected arg (all five measured printing the
banner on `main` already).
- **Degrades towards silence.** A stack it cannot read (stack-trace
limit 0, a moved parser module) answers false, which is the behaviour
before this PR, never a banner on a runtime failure. A moved module is
caught by the unit file, which throws every case through the real
`Parser.parse`.
- **Unchanged:** which inputs are accepted or refused, the exit code
(2), and oclif's own lines after the banner. No new flag.
## Tests
- **Unit, `packages/cli/src/utils/invocation.test.ts`:** recognised
through the real parser: missing value (`-d`), bare option-list flag,
flag given twice, `Flags.integer` refusal; missing required flag,
missing required arg and unexpected arg kept recognised. Not claimed: a
real oclif `Command` that parsed and then called `this.error()`
(asserted to share class, own fields and `oclif.exit: 2` with the
parse-time error), and a bare `Errors.error()`. `invocationFailureLine`
names `Flag --database expects a value` for `dev -d` and returns
`undefined` for the runtime command.
- **Spawn, `packages/cli/test/invocation-loudness.e2e.test.ts` (nightly
e2e tier, already spawning `dev --no-ui`):** one more spawn, `dev -d`:
exit 2, oclif's own line kept, the INVOCATION ERROR line first, stdout
empty.
<details><summary>Measured on main (base 135054e), built bin/run.js,
from examples/app-todo</summary>
| invocation | exit | banner on main | banner on head |
|---|---|---|---|
| `dev --no-compile -p PORT -d` | 2 | no (`Error: Flag --database
expects a value`) | yes |
| `serve --port` | 2 | no | yes |
| `migrate meta --from` | 2 | no | yes |
| `dev --no-compile --log-level` | 2 | no (`expects one of these
values`) | yes |
| `dev --no-compile -p A -p B` | 2 | no (`can only be specified once`) |
yes |
| `lint --eval-min abc` | 2 | no (`Parsing --eval-min` / `Expected an
integer`) | yes |
| `plugin keygen` (missing required flag) | 2 | yes | yes |
| `plugin uninstall` (missing required arg) | 2 | yes | yes |
| `plugin uninstall a b` (unexpected arg) | 2 | yes | yes |
| `serve --artifact x` (nonexistent flag, control) | 2 | yes | yes |
| `dev --no-compile -p abc` (runtime refusal) | 1 | no | no |
| `plugin uninstall no-such-plugin` (runtime) | 1 | no | no |
Error shapes (real `Parser.parse` and a real `Command`): every
parse-time plain error and the runtime `this.error()` are `CLIError`,
own fields
`stack,message,code,oclif,skipOclifErrorHandling,suggestions`, `oclif
{"exit":2}`, `code undefined`. Parse-time stacks carry
`@oclif/core/lib/parser/parse.js` frames; the runtime stack carries
`lib/errors/error.js` and `lib/command.js` only.
</details>
<details><summary>Red on base, green on head, ablations</summary>
- Red (base source, new tests): unit `5 failed | 21 passed (26)`, the
four parse-time recognitions and the `dev -d` line; the negatives pass
on base. Spawn `1 failed | 13 passed (14)`, "puts the INVOCATION ERROR
line FIRST on stderr".
- Green on head `45c1b2f0`: unit `26 passed (26)`; spawn `14 passed
(14)`.
- Ablation 1 (`scripts/ablation-replace.mjs`: the throw-site reading
forced false): unit `5 failed | 21 passed`, spawn `1 failed | 13
passed`, the same cases as red-first. Restored: blob `78901ca1` == HEAD,
`git diff HEAD` empty.
- Ablation 2, the negative pin (every `Error` claimed, the naive
class/exit reading): unit `5 failed | 21 passed`, exactly the five
runtime-not-claimed cases. Restored: blob == HEAD, `git diff HEAD`
empty.
- Both test files import `src/` (the spawn runs `bin/run-dev.js` under
tsx), so no `dist/` leg; the head probe table above ran the rebuilt
`bin/run.js`.
</details>
## Acceptance notes
- The banner's reason is the first line of oclif's message, so for a
flag-parse refusal it reads `INVOCATION ERROR — Parsing --eval-min.` and
for a missing required flag (unchanged from `main`) `INVOCATION ERROR —
The following error occurred:.`; the actual reason is on oclif's lines
right below. Left as is: `reasonOf()` is shared by every class and the
order keeps this change to recognition.
- One more cold spawn in `invocation-loudness.e2e.test.ts` (five instead
of four); the file is in the nightly e2e tier, not the PR queue.
## Verification
All at head `45c1b2f0`.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 61 derived families. `--ran` reconciliation
exit 0: **60 run, exit 0; 1 NOT MEASURED** (`check:dual-build-cjs-loads`
exit 3, PREREQUISITE NOT MET: needs every package's `dist/`; CI measures
it). `check:i18n-coverage` first exited 3 (examples' plugin `dist/`
absent), exit 0 after building the closure it names.
`check-plugin-teardown-shape --self-test` ran after `git fetch
--depth=1` of its pinned fixture commit.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: `368
passed (368)` files, `5205 passed (5205)` tests (lock VERDICT
command-exit 0). The integration tier is CI's; the one spawn file
touched is nightly-tier and was run alone (`14 passed`).
- `pnpm --filter @objectstack/cli typecheck` (incl.
`check:test-typecheck`): exit 0.
- eslint `--no-inline-config` over `packages/cli`: exit 0.
- CI check-runs: not waited on at hand-back; the seat reads them.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5pvjo67wc1GwKuRXFr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli): a flag given without its value (`os dev -d`) now opens with the INVOCATION ERROR line
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`os dev -d`, `os serve --port` and `os migrate meta --from` used to exit 2 with only oclif's `Error: Flag --NAME expects a value` line. Backgrounded, that reads like a server that booted and died. The CLI now writes its one-line `objectstack: INVOCATION ERROR — … The command never ran: nothing was started and nothing is listening.` first, as it already did for an unknown flag or a refused value.
11
+
12
+
The same line now also leads three other parse failures that oclif raises the same way: a flag with an option list given without its value (`os dev --log-level`), a single-valued flag given twice (`os dev -p 1 -p 2`), and a value a flag's own parser refused (`os lint --eval-min abc`).
13
+
14
+
Nothing else changes: the same inputs are accepted and refused, the exit code is still 2, and oclif's own lines follow unchanged. A command that parsed and then failed while running gets no banner.
15
+
16
+
The banner's reason now carries oclif's whole message on one line instead of only its first line, so `os plugin keygen` reads `The following error occurred: Missing required flag out` and `os lint --eval-min abc` reads `Parsing --eval-min; Expected an integer but received: abc` where both used to name only the header.
0 commit comments