refactor: Start integrating @doist/cli-core#208
Conversation
Wire twist-cli's foundational primitives through @doist/cli-core,
mirroring todoist-cli #313 (errors, config, spinner helpers,
global-args, output) + #314 (full spinner delegation via
createSpinner). Also rename the local config API
getConfig/setConfig -> readConfig/writeConfig so the vocabulary
matches cli-core.
- errors.ts now subclasses cli-core's CliError, preserving the
positional (code, message, hints?, type?) ctor used across the
CLI's hundreds of call sites.
- config.ts delegates getConfigPath/readConfig/readConfigStrict/
writeConfig to cli-core; readConfigStrict's discriminated result
is translated to twist's existing CONFIG_READ_FAILED /
CONFIG_INVALID_JSON / CONFIG_INVALID_SHAPE CliErrors with the
same hint copy.
- auth.ts call sites renamed; auth's private writeConfig helper
(delete-when-empty for cleanup flows) renamed to
persistOrClearConfig to avoid clashing with the new public
writeConfig.
- view.ts switches CONFIG_PATH constant -> getConfigPath() fn.
- global-args.ts uses cli-core's isCI() (also opt-out aware) in
shouldDisableSpinner.
- output.ts delegates base JSON/NDJSON serialization to cli-core
while keeping twist's entity-aware filterEntityFields wrapping.
- spinner.ts is now a thin re-export of createSpinner({
isDisabled: shouldDisableSpinner }); the LoadingSpinner class,
early-spinner singleton, stdout interception, and adoption
semantics all live in cli-core. yocto-spinner drops out of
direct deps.
- vitest.config.ts inlines @doist/cli-core so vi.mock and
vi.doMock reach its compiled imports.
- Test mocks updated; spinner.test.ts trimmed to wiring smoke
tests (deep behaviour covered in cli-core's own suite).
Behaviour note: cli-core's spinner no longer prepends a manual
'✓ ' / '✗ ' on top of yocto-spinner's '✔' / '✖', so
success/fail lines now render as '✔ Done' instead of '✔ ✓ Done'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the rename portion of the previous commit. The local config API stays at getConfig / setConfig / updateConfig — they just delegate to cli-core's readConfig / writeConfig under the hood. Smaller blast radius across the codebase, no call-site churn for future readers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The smoke tests duplicated cli-core's spinner suite, and the only
twist-specific wiring (TW_SPINNER → shouldDisableSpinner →
isDisabled) is already covered by global-args.test.ts. The
createSpinner({ isDisabled }) hookup is type-checked at compile
time. Also drops the resetEarlySpinner re-export, which was only
exposed for the deleted suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@doist/cli-core
doistbot
left a comment
There was a problem hiding this comment.
This PR cleanly integrates @doist/cli-core 0.9.0 into twist-cli, adopting the central utilities for errors, configuration, output formatting, and spinners while keeping existing call sites intact. Centralizing these foundations significantly reduces local boilerplate and aligns the project more closely with our standard tooling ecosystem. A few minor adjustments are needed to fully wire up the updateConfig delegate, streamline paginated JSON/NDJSON formatting to avoid unnecessary memory allocations, and ensure test coverage is preserved for the spinner, CI environment flags, and configuration path resolution.
- config.ts: updateConfig now actually delegates to cli-core's
updateConfig (was claimed in the PR description but the local body
still composed getConfig + setConfig). Uses cli-core's strict-read
semantics so a broken config file throws instead of being silently
overwritten.
- output.ts: route formatPaginatedJson through the local formatJson
(was calling formatJsonCore directly, leaving a duplicate
serialization path). formatPaginatedNdjson similarly uses local
formatNdjson and appends the cursor trailer via string concat
instead of [...results, trailer], avoiding the O(n) array copy on
every paginated response.
- global-args.test.ts: add a regression test that CI='false' does
NOT disable the spinner, exercising the cli-core isCI() opt-out
path that the swap unlocked.
- config.test.ts: add a 'thin config wrappers' block proving
getConfigPath, getConfig, setConfig, and updateConfig forward to
cli-core with the 'twist-cli' app name. A wrong app name would
silently redirect every config read/write — these tests are the
tripwire.
- spinner.test.ts: re-add a single smoke test that mocks
createSpinner and asserts it's invoked with shouldDisableSpinner.
global-args.test.ts only tests shouldDisableSpinner in isolation;
this test is the only thing catching a broken
createSpinner({ isDisabled }) handoff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary First consumer-side integration of [@doist/cli-core](https://www.npmjs.com/package/@doist/cli-core) 0.9.0 into outline-cli. Mirrors twist-cli [#208](Doist/twist-cli#208) (errors / config / spinner / output / markdown helpers) on this codebase. PR 1 of a 3-PR stack: 1. **This PR** — cli-core wiring (errors, config consolidation, spinner, output, markdown) 2. Next — `global-args` + `ViewOptions` + empty-output test helper (mirror of twist-cli #209) 3. Next — delegate `ol update` to `@doist/cli-core/commands` (mirror of twist-cli #214) The migration step from twist-cli #211 (`updateChannel` → `update_channel`) is not needed: outline-cli already stores the channel under `update_channel`. ## What's wired through cli-core now | Module | Was | Now | | --- | --- | --- | | `src/lib/errors.ts` | n/a (plain `Error`s) | new module — local `CliError` subclass over cli-core's `CliError<ErrorCode>`. Keeps the historical positional `(code, message, hints?, type?)` signature. `code` accepts `ErrorCode \| CliErrorCode` so cli-core's canonical codes type-check at call sites. | | `src/lib/config.ts` | n/a — config reads were split across `auth.ts` (api_token / base_url / oauth_client_id) and `update-config.ts` (update_channel), both writing the same file with their own sync `fs` calls | new module — thin wrappers over cli-core's `readConfig` / `writeConfig` / `updateConfig` with `'outline-cli'` app name. Single source of truth for the config schema. | | `src/lib/auth.ts` | sync `fs` reads + writes, racing `update-config.ts` on the same file | async wrappers over `config.ts`. Public API preserved (`getApiToken` / `getBaseUrl` / `getOAuthClientId` / `getTokenSource` / `saveConfig` / `clearConfig`) — now returns promises. | | `src/lib/update-config.ts` | independent sync `fs` reader/writer hitting the same config file | async wrappers over `config.ts`. | | `src/lib/spinner.ts` | local 70-line impl with yocto-spinner, manual `chalk.green('✓ …')` / `chalk.red('✗ …')` doubling, ad-hoc `shouldDisableSpinner` | `const spinner = createSpinner({ isDisabled: shouldDisableSpinner })` + destructured exports. The CLI-specific bit (`OL_SPINNER` opt-out + `--json` / `--ndjson` / `--no-spinner` fan-out, `CI` opt-out aware: `CI='false'` is treated as not-CI) is wired in as `isDisabled`. | | `src/lib/output.ts` | inline `JSON.stringify(_, null, 2)` / one log per ndjson item | delegates serialization to cli-core's `formatJson` / `formatNdjson`. `formatError` and the essential-keys filtering layer stay local. | | `src/lib/markdown.ts` | `marked@15` + `marked-terminal@7` with sync `marked.parse()` | cli-core's `preloadMarkdown` / `renderMarkdown` (async). `renderMarkdown` call site in `document.ts` now awaits. | ## Dependency bumps - Add `@doist/cli-core` 0.9.0 - `marked` 15.0.12 → 18.0.3 (cli-core peer requires `>=18`) - Replace `marked-terminal` 7.3.0 (caps at `marked@<16`) with `marked-terminal-renderer` 2.2.0 — same fork that cli-core itself uses, no marked version cap - Drop `yocto-spinner` from direct deps (still resolved transitively via cli-core) - `vitest` 4.0.18 → 4.1.5 (cli-core peer requires `>=4.1`) ## Behaviour changes worth flagging - `yocto-spinner.success()` and `.error()` already prepend their own `✔` / `✖` glyphs. The old `spinner.ts` added a manual `chalk.green('✓ ')` / `chalk.red('✗ ')` on top, which produced doubled symbols in real terminals (e.g. `✔ ✓ Done`). cli-core 0.3+ drops the manual prefixes — so the success / fail line now renders as a single `✔ Done` instead of `✔ ✓ Done`. - `updateConfig` now uses cli-core's strict-read semantics: a broken config file throws instead of being silently overwritten (the previous lenient `getUpdateChannel` + `setUpdateChannel` composition could clobber data the user might have recovered by hand-fixing the file). - `auth.ts` and `update-config.ts` no longer race on writes — both go through the single `config.ts` wrapper. - `CI='false'` is now honoured as an opt-out (the spinner runs). ## vitest config Adds `server.deps.inline: ['@doist/cli-core']` to a new `vitest.config.ts`. Without it, vitest treats the real-installed cli-core as external and `vi.mock('@doist/cli-core', …)` doesn't reach its compiled imports. ## Tests - `src/__tests__/spinner.test.ts` trimmed to a single smoke test that mocks `@doist/cli-core#createSpinner` and asserts the kit is built with a function `isDisabled`. The previous ~200 lines of behavioural coverage now live in cli-core's own spinner suite. - `src/__tests__/auth.test.ts` updated for the async API; same coverage. - `src/__tests__/output.test.ts` adjusts the ndjson expectation to match cli-core's `formatNdjson` (single `console.log` with `\n`-joined entries rather than one log per item). - `src/__tests__/commands.test.ts` mocks `auth.js` with async functions; the document-get rendering assertion drops the `# ` prefix (marked-terminal-renderer renders headings differently from the old marked-terminal). ## Test plan - [x] `npm run type-check` - [x] `npm run lint:check` — 0 warnings, 0 errors - [x] `npm test` — **118 / 118** pass - [x] `npm run build` - [x] `npm run check:skill-sync` - [x] `npm ls yocto-spinner` confirms cli-core still resolves it transitively - [ ] Manually exercise `ol auth status`, `ol auth login`, `ol update --channel` against a real config - [ ] Run a long-running command (`ol search foo`) in a TTY to confirm the spinner appears and **no doubled `✔ ✓` glyphs** on success - [ ] `CI=1 ol search foo`, `ol search foo --json`, `OL_SPINNER=false ol search foo` to confirm spinner suppression 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
🎉 This PR is included in version 2.36.4 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
First consumer-side integration of @doist/cli-core 0.9.0 into twist-cli. Mirrors todoist-cli #313 (errors / config / spinner helpers / global-args / output) and #314 (full spinner delegation via
createSpinner) as a single combined PR.The local config API stays at
getConfig/setConfig/updateConfig— they're now thin wrappers around cli-core'sreadConfig/writeConfig/updateConfig. Zero call-site churn outsidesrc/lib/config.tsitself.What's wired through cli-core now
src/lib/errors.tsCliErrorclassCliError<ErrorCode>— keeps the historical positional(code, message, hints?, type?)signature so the ~hundreds of existing call sites stay untouched.codeacceptsErrorCode | CliErrorCodeso cli-core's canonical codes (CONFIG_*, etc.) type-check at call sites without redeclaring them locally.src/lib/config.tsnode:fs/promises,node:os,node:pathgetConfigPath(lazy, takes'twist-cli'),getConfig→readConfig,setConfig→writeConfig,updateConfig→updateConfig. Schema (Config,AuthMode,UpdateChannel,UserSettings, theKNOWN_*_KEYSsets,validateConfigForDoctor) stays.readConfigStricttranslates cli-core's discriminatedReadConfigStrictResultinto the same three twistCliErrors with identical hint copy.src/lib/spinner.tsLoadingSpinnerclass, colour paletteconst spinner = createSpinner({ isDisabled: shouldDisableSpinner })+ destructured exports. The CLI-specific bit (TW_SPINNERopt-out +--json/--ndjson/--no-spinnerfan-out) is wired in asisDisabled.yocto-spinnerdrops from direct deps (still resolved transitively via cli-core).src/lib/global-args.tsprocess.env.CIdirect readisCI()(also opt-out aware:CI='false'is treated as not-CI)src/lib/output.tsJSON.stringify(_, null, 2)/items.map(JSON.stringify).join('\n')formatJson/formatNdjson; entity-aware processing viafilterEntityFieldsand the_metacursor sentinel for paginated NDJSON stays. Paginated formatters route through the local helpers (single serialization path).formatErrorJsondeliberately keeps its inline compactJSON.stringify— it emits single-line JSON, not pretty-print, so it doesn't fit cli-core'sformatJsonprimitive.shouldDisableSpinnerlikewise stays local since it bundles theTW_SPINNERenv var with the global-args flag fan-out.Behaviour change worth flagging
yocto-spinner.success()and.error()already prepend their own✔/✖glyphs. The old code added a manual✓/✗on top, which produced doubled symbols in real terminals (e.g.✔ ✓ Done). cli-core 0.3+ drops the manual prefixes — so the success / fail line now renders as a single✔ Doneinstead of✔ ✓ Done. Surfaced upstream during the cli-core review and confirmed in cli-core's own spinner tests.updateConfignow uses cli-core's strict-read semantics: a broken config file throws instead of being silently overwritten (the previous lenientgetConfig+setConfigcomposition could clobber data the user might have recovered by hand-fixing the file).vitest config
Adds
server.deps.inline: ['@doist/cli-core']tovitest.config.ts. Without it, vitest treats the real-installed cli-core as external andvi.mock('@doist/cli-core', …)/vi.doMock('node:fs/promises', …)don't reach its compiled imports, breaking theauth/configsuites.Tests
src/lib/spinner.test.tsis trimmed to a single smoke test that mocks@doist/cli-core#createSpinnerand asserts the kit is built with{ isDisabled: shouldDisableSpinner }. The previous 333 lines of behavioural coverage are now in cli-core's own spinner suite; the wiring assertion is the one thing that has to live here.src/lib/config.test.tsgains two new blocks:readConfigStrict wrapper— covers all five state→CliErrortranslations (missing, present, read-failed, invalid-json, invalid-shape).thin config wrappers— provesgetConfigPath,getConfig,setConfig,updateConfigeach forward to cli-core with the resolved'twist-cli'app-name path. A wrong app name would silently redirect every config read/write — these tests are the tripwire.src/lib/global-args.test.tsadds a regression locking in cli-core'sisCI()opt-out:CI='false'no longer disables the spinner.Test plan
npm run type-checknpm run lint:check— 0 warnings, 0 errorsnpm test— 579 / 579 passnpm run buildnpm ls yocto-spinnerconfirms cli-core still resolves it transitivelytw auth status,tw config view,tw doctoragainst a real config to confirm parity at the wrapper boundarytw inbox) in a TTY to confirm the spinner still appears, the early spinner is adopted, and no doubled✔ ✓glyphs on successCI=1 tw inbox,tw inbox --json,TW_SPINNER=false tw inboxto confirm spinner suppression viaisCI()andshouldDisableSpinner🤖 Generated with Claude Code