kimaki 0.26.0 → 0.28.0
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.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bump-holocron-consumers
|
|
3
|
+
repo: remorses/holocron
|
|
4
|
+
description: >
|
|
5
|
+
Bump @holocron.so/vite and @holocron.so/cli in Tommy's other GitHub repos.
|
|
6
|
+
Use when asked to update holocron versions across remorses consumer sites,
|
|
7
|
+
find repos that depend on holocron.so/vite or holocron.so/cli, or sync
|
|
8
|
+
docs-site packages after a holocron release.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Bump Holocron in consumer repos
|
|
12
|
+
|
|
13
|
+
After a holocron npm release, bump **@holocron.so/vite** and **@holocron.so/cli** in Tommy's other repos. Do this on **local clones**. Do not clone missing repos unless asked. Do not commit unless asked.
|
|
14
|
+
|
|
15
|
+
Load the **pnpm** skill before running any `pnpm update`.
|
|
16
|
+
|
|
17
|
+
## 1. Read the latest versions
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm view @holocron.so/vite version
|
|
21
|
+
npm view @holocron.so/cli version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Confirm they match `vite/package.json` and `cli/package.json` in this repo. Never guess.
|
|
25
|
+
|
|
26
|
+
## 2. Search GitHub
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
gh search code '@holocron.so/vite' --owner remorses --filename package.json --limit 100 --json repository,path,textMatches
|
|
30
|
+
|
|
31
|
+
gh search code '@holocron.so/cli' --owner remorses --filename package.json --limit 50 --json repository,path,textMatches
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Read the **full** JSON. Never pipe to `head` or `tail`.
|
|
35
|
+
|
|
36
|
+
Treat a hit as a real dependency only when it is in `dependencies` or `devDependencies`. Skip `packageExtensions`, the holocron package's own `"name"` field, and `workspace:^`.
|
|
37
|
+
|
|
38
|
+
## 3. Skip these
|
|
39
|
+
|
|
40
|
+
- **remorses/holocron** itself (`workspace:^`)
|
|
41
|
+
- Specifiers already set to **`latest`** (template, throwaway test sites)
|
|
42
|
+
- Empty directories and non-git folders
|
|
43
|
+
- Repos not on this machine, unless the user asks to clone them
|
|
44
|
+
|
|
45
|
+
Typical `latest` skips: `holocron-template`, `context-dev-docs`, `palmier-pro-docs`, `example-holocron-again`, `holocron-testing-again`, `holocron-example-againagain`, `holocron-test-antoher`, `holocron-docs-new-again`, `holocron-test-2`.
|
|
46
|
+
|
|
47
|
+
## 4. Map GitHub repo to a local folder
|
|
48
|
+
|
|
49
|
+
Search these roots, in order:
|
|
50
|
+
|
|
51
|
+
1. `/Users/morse/Documents/GitHub/<name>`
|
|
52
|
+
2. `/Users/morse/Documents/<name>`
|
|
53
|
+
3. The alias table below
|
|
54
|
+
4. `git remote -v` on nearby folders when the folder name differs from the GitHub name
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
for d in /Users/morse/Documents/GitHub/*/ /Users/morse/Documents/*/; do
|
|
58
|
+
url=$(git -C "$d" remote get-url origin 2>/dev/null) || continue
|
|
59
|
+
case "$url" in
|
|
60
|
+
*remorses/kimaki*) echo "kimaki -> $d" ;;
|
|
61
|
+
*remorses/gpuix*) echo "gpuix -> $d" ;;
|
|
62
|
+
*remorses/opensession*) echo "opensession -> $d" ;;
|
|
63
|
+
*remorses/traforo*) echo "traforo -> $d" ;;
|
|
64
|
+
*remorses/spiceflow*) echo "spiceflow -> $d" ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Known aliases** (verify `origin` if a path is missing):
|
|
70
|
+
|
|
71
|
+
| GitHub | Local path | Notes |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| remorses/kimaki | `/Users/morse/Documents/GitHub/kimakivoice` | `/GitHub/kimaki` is an empty dir. Dep lives in `website/` |
|
|
74
|
+
| remorses/gpuix | `/Users/morse/Documents/GitHub/gpuixlocal` | `website/` is **not** in bun workspaces. Update from that folder |
|
|
75
|
+
| remorses/opensession | `/Users/morse/Documents/GitHub/opensessions` | vite + cli |
|
|
76
|
+
| remorses/traforo | `/Users/morse/Documents/GitHub/kimakivoice/traforo` | git submodule. kimaki workspace also includes `./traforo/website` |
|
|
77
|
+
| remorses/spiceflow | `/Users/morse/Documents/spiceflow` | Not under GitHub/. Local HEAD can lag origin and omit `website/` |
|
|
78
|
+
| remorses/playwriter | `/Users/morse/Documents/GitHub/playwriter` | `website/` |
|
|
79
|
+
| remorses/termcast | `/Users/morse/Documents/GitHub/termcast` | `website/` |
|
|
80
|
+
| remorses/egaki | `/Users/morse/Documents/GitHub/egaki` | `website/` |
|
|
81
|
+
| remorses/sigillo | `/Users/morse/Documents/GitHub/sigillo` | Dep is in **`app/`**, not website/ |
|
|
82
|
+
| remorses/subrouter | `/Users/morse/Documents/GitHub/subrouter` | Prefer this clone over `kimakivoice/subrouter` |
|
|
83
|
+
| remorses/strada | `/Users/morse/Documents/GitHub/strada` | `website/` |
|
|
84
|
+
| remorses/notaku | `/Users/morse/Documents/GitHub/notaku` | `website/` |
|
|
85
|
+
|
|
86
|
+
Repos often **not** on disk: `akarso-private`, `dmcafreemusic`. Skip them. Report them.
|
|
87
|
+
|
|
88
|
+
If `website/` is missing locally, do not `git pull` to create it unless asked. Report that the clone is behind.
|
|
89
|
+
|
|
90
|
+
## 5. Update
|
|
91
|
+
|
|
92
|
+
Pick the package manager from the lockfile at the **repo root** (`pnpm-lock.yaml` vs `bun.lock`).
|
|
93
|
+
|
|
94
|
+
**pnpm workspace** (run at repo root):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pnpm update -r --latest @holocron.so/vite
|
|
98
|
+
pnpm update -r --latest @holocron.so/cli
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**bun**, from the folder whose `package.json` lists the dep (often `website/`):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bun update --latest @holocron.so/vite
|
|
105
|
+
bun update --latest @holocron.so/cli
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Never hand-edit `package.json` to bump a version. Never use `pnpm add` to update. Never add `pnpm.overrides`.
|
|
109
|
+
|
|
110
|
+
If the dep folder is **not** a workspace member (gpuix `website/`), run the update **inside that folder**, not at the repo root.
|
|
111
|
+
|
|
112
|
+
Keep the existing specifier style. Exact `0.33.0` stays exact. Caret `^0.30.0` stays caret.
|
|
113
|
+
|
|
114
|
+
## 6. Same-day publish / minimum release age
|
|
115
|
+
|
|
116
|
+
Global pnpm `minimumReleaseAge` is **1440** minutes. `@holocron.so/*` is already on the exclude list. If install still fails with:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
ERR_PNPM_NO_MATCHING_VERSION No matching version found for @holocron.so/cli@^X.Y.Z
|
|
120
|
+
The latest release of @holocron.so/cli is "X.Y.Z".
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
the project is pinned to an older pnpm via `packageManager` (notaku uses `pnpm@10.16.1`) and the exclude glob is ignored. Retry with:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm_config_minimum_release_age=0 pnpm --filter website update @holocron.so/vite@<latest>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Do not write `minimum-release-age=0` into the consumer's `.npmrc`.
|
|
130
|
+
|
|
131
|
+
## 7. Verify, then stop
|
|
132
|
+
|
|
133
|
+
Read each touched `package.json` and confirm the specifier is the latest. Then stop. Do not commit. Do not push. Do not deploy.
|
|
134
|
+
|
|
135
|
+
If the user asks to commit, commit **only** the holocron package.json + lockfile hunks in each consumer repo. Use that repo's own git. Never commit unrelated dirty files.
|
|
136
|
+
|
|
137
|
+
When a new GitHub-to-folder alias is discovered, add it to the table in this skill.
|
package/skills/critique/SKILL.md
CHANGED
|
@@ -74,17 +74,17 @@ critique hunks list
|
|
|
74
74
|
critique hunks list --filter "src/**/*.ts"
|
|
75
75
|
|
|
76
76
|
# Stage specific hunks by ID
|
|
77
|
-
critique hunks add 'src/main.ts
|
|
78
|
-
critique hunks add 'src/main.ts
|
|
77
|
+
critique hunks add 'src/main.ts:@a1b2c3d4e5f6'
|
|
78
|
+
critique hunks add 'src/main.ts:@a1b2c3d4e5f6' 'src/utils.ts:@f6e5d4c3b2a1'
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Hunk ID format: `file:@-oldStart
|
|
81
|
+
Hunk ID format: `file:@<hash>` or `file:@<hash>.1`. The hash is from added and removed lines, so unrelated edits above or below the hunk do not change the ID. Duplicate payloads in the same file get `.1`, `.2`. Old `file:@-oldStart,...` IDs are no longer accepted. `hunks list` still prints the `@@` header under each ID.
|
|
82
82
|
|
|
83
83
|
**Typical workflow:**
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
critique hunks list # see all unstaged hunks
|
|
87
|
-
critique hunks add 'file
|
|
87
|
+
critique hunks add 'file:@a1b2c3d4e5f6' # stage only your hunks
|
|
88
88
|
git commit -m "your changes" # commit separately
|
|
89
89
|
```
|
|
90
90
|
|
package/skills/errore/SKILL.md
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: errore
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
description: |
|
|
4
|
+
Go-style error handling for TypeScript: return errors as a plain `Error | T` union
|
|
5
|
+
instead of throwing. `instanceof Error` narrows the type — no Result wrapper. The gist:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
class NotFoundError extends errore.createTaggedError({
|
|
9
|
+
name: 'NotFoundError',
|
|
10
|
+
message: 'User $id not found',
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
async function getUser(id: string) {
|
|
14
|
+
const user = await db.findUser(id).catch((e) => new DbError({ cause: e }))
|
|
15
|
+
if (user instanceof Error) return user // early return, like Go's err != nil
|
|
16
|
+
if (!user) return new NotFoundError({ id })
|
|
17
|
+
return user // caller narrows the same way
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
ALWAYS read this skill when a repo uses the errore convention (errore.org).
|
|
22
|
+
ALWAYS use errore for new TypeScript projects.
|
|
15
23
|
version: 0.11.0
|
|
16
24
|
---
|
|
17
25
|
|
|
@@ -27,72 +35,42 @@ console.log(user.name) // TypeScript knows: User
|
|
|
27
35
|
|
|
28
36
|
## Rules
|
|
29
37
|
|
|
38
|
+
Each rule is one line; the details live once in the pattern sections below.
|
|
39
|
+
|
|
30
40
|
1. Always `import * as errore from 'errore'` — namespace import, never destructure
|
|
31
41
|
2. Never throw for expected failures — return errors as values
|
|
32
42
|
3. Never return `unknown | Error` — the union collapses to `unknown`, breaks narrowing. Common trap: `res.json()` returns `unknown`, so `return await res.json()` makes the return type `MyError | unknown` → `unknown`. Fix: cast with `as` → `return (await res.json()) as User`
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
35
|
-
6.
|
|
36
|
-
7. Use `
|
|
37
|
-
8. Use
|
|
38
|
-
9.
|
|
39
|
-
10. Always
|
|
40
|
-
11. Always
|
|
41
|
-
12.
|
|
42
|
-
13.
|
|
43
|
-
14.
|
|
44
|
-
15. Always
|
|
45
|
-
16. Use `errore.isAbortError` to detect abort errors — never check `error.name === 'AbortError'` manually, because tagged abort errors have their tag as `.name`
|
|
46
|
-
17. Custom abort errors MUST extend `errore.AbortError` — so `isAbortError` detects them in the cause chain even when wrapped by `.catch()`
|
|
47
|
-
18. Keep abort checks flat — check `isAbortError(result)` first as its own early return, then `result instanceof Error` as a separate early return. Never nest `isAbortError` inside `instanceof Error`:
|
|
43
|
+
4. Convert exceptions to values only at boundaries — `.catch()` for async, `errore.try` for sync (see [Boundaries](#boundaries-catch-for-async-erroretry-for-sync)). Never `try-catch` for control flow
|
|
44
|
+
5. Always wrap boundary catches in a tagged domain error with `cause` — `.catch((e) => new MyError({ cause: e }))`. Never `.catch((e) => e as Error)`, never omit `cause` (it preserves the chain for debugging and abort detection)
|
|
45
|
+
6. Use `createTaggedError` for domain errors (see [Defining Errors](#defining-errors)); use `cause` to wrap errors
|
|
46
|
+
7. Use `| null` for optional values, not `| undefined` — three-way narrowing: `instanceof Error`, `=== null`, then value
|
|
47
|
+
8. Use `const` + expressions, never `let` + try-catch (see [Expressions over Statements](#expressions-over-statements))
|
|
48
|
+
9. Handle errors inside `if` branches with early exits, keep the happy path at root indentation (see [Flat Control Flow](#flat-control-flow))
|
|
49
|
+
10. Always include `Error` handler in `matchError` — required fallback for plain Error instances
|
|
50
|
+
11. Always prefer `errore.try` over `errore.tryFn` — same function, `errore.try` is the canonical name
|
|
51
|
+
12. Abort: custom abort errors MUST extend `errore.AbortError`, detect with `errore.isAbortError` (never `error.name === 'AbortError'`), and check `isAbortError(result)` as its own early return before `instanceof Error` (see [Abort & Cancellation](#abort--cancellation))
|
|
52
|
+
13. Don't reassign after error early returns — TypeScript narrows the original variable; a `const narrowed = result` alias is redundant
|
|
53
|
+
14. Write `instanceof Error` early returns on one line — `if (result instanceof Error) return result`. Use a `{` block only when the branch has more than one statement
|
|
54
|
+
15. Always log errors that are not propagated — when an error branch doesn't `return` or `throw` (loops with `continue`, fallback branches, fire-and-forget calls), add a `console.warn`/`console.error` so the failure leaves a trace. Propagated errors don't need logging — the caller handles them:
|
|
48
55
|
|
|
49
56
|
```ts
|
|
50
|
-
const
|
|
51
|
-
(e) => new
|
|
57
|
+
const emailResult = await sendEmail(user.email).catch(
|
|
58
|
+
(e) => new EmailError({ email: user.email, cause: e }),
|
|
52
59
|
)
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
19. Don't reassign after error early returns — TypeScript narrows the original variable automatically after `instanceof Error` checks return. A `const narrowed = result` alias is redundant:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
const result = await fetch(url).catch((e) => new FetchError({ cause: e }))
|
|
61
|
-
if (result instanceof Error) return `Failed: ${result.message}`
|
|
62
|
-
await result.json() // TS knows result is Response here
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
20. Always write `instanceof Error` early returns on one line — no `{` block, no extra lines. `if (x instanceof Error) return x` keeps the happy path readable and reduces visual noise. Only use a block when the branch has more than one statement:
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
// good — one line, no block
|
|
69
|
-
if (result instanceof Error) return result
|
|
70
|
-
if (user instanceof Error) return user
|
|
71
|
-
|
|
72
|
-
// bad — block for a single return adds noise
|
|
73
|
-
if (result instanceof Error) {
|
|
74
|
-
return result
|
|
60
|
+
if (emailResult instanceof Error) {
|
|
61
|
+
console.warn('Failed to send email:', emailResult.message)
|
|
75
62
|
}
|
|
76
63
|
```
|
|
77
64
|
|
|
78
|
-
|
|
65
|
+
16. Always hint a **solution** in user-facing error messages — include a login URL, CLI command, or missing flag so agents and users can self-correct instead of guessing. Applies to HTTP responses and CLI output; internal errors (DB failures, parse errors) can stay technical:
|
|
79
66
|
|
|
80
67
|
```ts
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// GOOD: log before continuing — error is visible in logs
|
|
88
|
-
const result = await syncToCloud(data)
|
|
89
|
-
if (result instanceof Error) {
|
|
90
|
-
console.warn('Cloud sync failed:', result.message)
|
|
91
|
-
}
|
|
68
|
+
class UnauthorizedError extends errore.createTaggedError({
|
|
69
|
+
name: 'UnauthorizedError',
|
|
70
|
+
message: 'Not logged in. Sign in at $loginUrl or run: $loginCommand',
|
|
71
|
+
}) {}
|
|
92
72
|
```
|
|
93
73
|
|
|
94
|
-
> Propagated errors (`return error`) don't need logging — the caller handles them. But errors you choose to ignore must leave a trace. This applies to loops with `continue`, fallback branches, and any path where the error is intentionally dropped.
|
|
95
|
-
|
|
96
74
|
## TypeScript Rules
|
|
97
75
|
|
|
98
76
|
- **Object args over positional** — `({id, retries})` not `(id, retries)` for functions with 2+ params
|
|
@@ -103,24 +81,7 @@ console.log(user.name) // TypeScript knows: User
|
|
|
103
81
|
- **No uninitialized `let`** — use IIFE with returns instead of `let x; if (...) { x = ... }`
|
|
104
82
|
- **Type empty arrays** — `const items: string[] = []` not `const items = []`
|
|
105
83
|
- **Module imports for node builtins** — `import fs from 'node:fs'` then `fs.readFileSync(...)`, not named imports
|
|
106
|
-
- **Let TypeScript infer return types** —
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
// let inference do its job
|
|
110
|
-
function getUser(id: string) {
|
|
111
|
-
const user = await db.find(id)
|
|
112
|
-
if (!user) return new NotFoundError({ id })
|
|
113
|
-
return user
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// explicit annotation when it adds clarity on a complex public API
|
|
117
|
-
function processRequest(
|
|
118
|
-
req: Request,
|
|
119
|
-
): Promise<ValidationError | AuthError | DbError | null | Response> {
|
|
120
|
-
// ...
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
84
|
+
- **Let TypeScript infer return types** — inferred types are always correct. Annotate only when it genuinely improves readability (complex unions, public API boundaries) or when inference produces a wider type than intended
|
|
124
85
|
- **`.filter(isTruthy)` not `.filter(Boolean)`** — `Boolean` doesn't narrow types, so `(T | null)[]` stays `(T | null)[]` after filtering. Use a type guard:
|
|
125
86
|
|
|
126
87
|
```ts
|
|
@@ -130,53 +91,11 @@ console.log(user.name) // TypeScript knows: User
|
|
|
130
91
|
const items = results.filter(isTruthy)
|
|
131
92
|
```
|
|
132
93
|
|
|
133
|
-
- **`controller.abort()` must use typed errors** — `abort(reason)` throws `reason` as-is. MUST pass a tagged error extending `errore.AbortError`, NEVER `new Error()` or a string — otherwise `isAbortError` can't detect it in the cause chain:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
class TimeoutError extends errore.createTaggedError({
|
|
137
|
-
name: 'TimeoutError',
|
|
138
|
-
message: 'Request timed out for $operation',
|
|
139
|
-
extends: errore.AbortError,
|
|
140
|
-
}) {}
|
|
141
|
-
controller.abort(new TimeoutError({ operation: 'fetch' }))
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
- **Never silently suppress errors** — empty `catch {}` and unlogged error branches hide failures. With errore you rarely need catch at all, but at any boundary where an error is not propagated, always log it (see rule 20):
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
const emailResult = await sendEmail(user.email).catch(
|
|
148
|
-
(e) => new EmailError({ email: user.email, cause: e }),
|
|
149
|
-
)
|
|
150
|
-
if (emailResult instanceof Error) {
|
|
151
|
-
console.warn('Failed to send email:', emailResult.message)
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
94
|
## Flat Control Flow
|
|
156
95
|
|
|
157
96
|
Keep block nesting minimal. Every level of indentation is cognitive load. The ideal function reads top to bottom at root level — checks and early returns, no `else`, no nested `if`, no `try-catch`.
|
|
158
97
|
|
|
159
|
-
**Core pattern** — call → check error → exit if error → continue at root. This is the single most important structural rule.
|
|
160
|
-
|
|
161
|
-
**Go:**
|
|
162
|
-
|
|
163
|
-
```go
|
|
164
|
-
user, err := getUser(id)
|
|
165
|
-
if err != nil {
|
|
166
|
-
return fmt.Errorf("get user: %w", err)
|
|
167
|
-
}
|
|
168
|
-
// user is valid here, at root level
|
|
169
|
-
|
|
170
|
-
posts, err := getPosts(user.ID)
|
|
171
|
-
if err != nil {
|
|
172
|
-
return fmt.Errorf("get posts: %w", err)
|
|
173
|
-
}
|
|
174
|
-
// posts is valid here, at root level
|
|
175
|
-
|
|
176
|
-
return render(user, posts)
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**errore (identical structure):**
|
|
98
|
+
**Core pattern** — call → check error → exit if error → continue at root. This is the single most important structural rule. Identical structure to Go's `if err != nil { return err }` blocks — speed bumps the reader skips over while scanning the left edge:
|
|
180
99
|
|
|
181
100
|
```ts
|
|
182
101
|
const user = await getUser(id)
|
|
@@ -188,8 +107,6 @@ if (posts instanceof Error) return posts
|
|
|
188
107
|
return render(user, posts)
|
|
189
108
|
```
|
|
190
109
|
|
|
191
|
-
The reader scans the left edge of the function to follow the happy path — just like reading a Go function where `if err != nil` blocks are speed bumps you skip over.
|
|
192
|
-
|
|
193
110
|
**No `else`** — early return eliminates it: `if (x) return 'A'; return 'B'`
|
|
194
111
|
|
|
195
112
|
**No `else if` chains** — sequence of early-return `if` blocks:
|
|
@@ -203,7 +120,7 @@ function getStatus(code: number): string {
|
|
|
203
120
|
}
|
|
204
121
|
```
|
|
205
122
|
|
|
206
|
-
**Flatten nested `if`** — invert conditions and return early. `if (A) { if (B) { ... } }` becomes `if (!A) return; if (!B) return; ...`.
|
|
123
|
+
**Flatten nested `if`** — invert conditions and return early. `if (A) { if (B) { ... } }` becomes `if (!A) return; if (!B) return; ...`. Take the outermost `if` condition, negate it, return the failure case, then continue at root level. Repeat for each nested `if`. The happy path falls through to the end.
|
|
207
124
|
|
|
208
125
|
**Avoid `try-catch` for control flow** — `try-catch` is the worst offender for nesting. It forces a two-branch structure (`try` + `catch`) and hides which line threw. Convert exceptions to values at boundaries:
|
|
209
126
|
|
|
@@ -228,26 +145,6 @@ async function loadConfig(): Promise<Config> {
|
|
|
228
145
|
|
|
229
146
|
**Errors in branches, happy path at root** — always handle errors inside `if` blocks, never success logic. Error handling goes in branches with early exits. Putting success logic inside `if` blocks inverts the flow and buries the happy path. **If you see `!(x instanceof Error)` in a condition, you've inverted the pattern — flip it.**
|
|
230
147
|
|
|
231
|
-
**Keep the happy path at minimum indentation** — the reader scans down the left edge to follow the main logic:
|
|
232
|
-
|
|
233
|
-
```ts
|
|
234
|
-
async function handleRequest(req: Request): Promise<AppError | Response> {
|
|
235
|
-
const body = await parseBody(req)
|
|
236
|
-
if (body instanceof Error) return body
|
|
237
|
-
|
|
238
|
-
const user = await authenticate(req.headers)
|
|
239
|
-
if (user instanceof Error) return user
|
|
240
|
-
|
|
241
|
-
const permission = checkPermission(user, body.resource)
|
|
242
|
-
if (permission instanceof Error) return permission
|
|
243
|
-
|
|
244
|
-
const result = await execute(body.action, body.resource)
|
|
245
|
-
if (result instanceof Error) return result
|
|
246
|
-
|
|
247
|
-
return new Response(JSON.stringify(result), { status: 200 })
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
148
|
Same in loops — error in `if` + `continue`, happy path flat:
|
|
252
149
|
|
|
253
150
|
```ts
|
|
@@ -330,47 +227,11 @@ async function getUser(id: string) {
|
|
|
330
227
|
|
|
331
228
|
> Return the error, don't throw it. The return type tells callers exactly what can go wrong.
|
|
332
229
|
|
|
333
|
-
###
|
|
334
|
-
|
|
335
|
-
```ts
|
|
336
|
-
const user = await getUser(id)
|
|
337
|
-
if (user instanceof Error) return user
|
|
338
|
-
|
|
339
|
-
const posts = await getPosts(user.id)
|
|
340
|
-
if (posts instanceof Error) return posts
|
|
341
|
-
|
|
342
|
-
return posts
|
|
343
|
-
```
|
|
230
|
+
### Boundaries (.catch for async, errore.try for sync)
|
|
344
231
|
|
|
345
|
-
|
|
232
|
+
`.catch()` and `errore.try` should only appear at the **lowest level** of your call stack — right at the boundary with code you don't control (third-party libraries, `JSON.parse`, `fetch`, file I/O, etc.). Your own functions return errors as values, so they never need `.catch()` or `try`.
|
|
346
233
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
```ts
|
|
350
|
-
async function fetchJson<T>(url: string): Promise<NetworkError | T> {
|
|
351
|
-
const response = await fetch(url).catch(
|
|
352
|
-
(e) => new NetworkError({ url, reason: 'Fetch failed', cause: e }),
|
|
353
|
-
)
|
|
354
|
-
if (response instanceof Error) return response
|
|
355
|
-
|
|
356
|
-
if (!response.ok) {
|
|
357
|
-
return new NetworkError({ url, reason: `HTTP ${response.status}` })
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const data = await (response.json() as Promise<T>).catch(
|
|
361
|
-
(e) => new NetworkError({ url, reason: 'Invalid JSON', cause: e }),
|
|
362
|
-
)
|
|
363
|
-
return data
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
> `.catch()` on a promise converts rejections to typed errors. TypeScript infers the union (`Response | NetworkError`) automatically. Use `errore.try` for sync boundaries (`JSON.parse`, etc.).
|
|
368
|
-
|
|
369
|
-
### Boundary Rule (.catch for async, errore.try for sync)
|
|
370
|
-
|
|
371
|
-
`.catch()` and `errore.try` should only appear at the **lowest level** of your call stack — right at the boundary with code you don't control (third-party libraries, `JSON.parse`, `fetch`, file I/O, etc.). Your own functions should never throw, so they never need `.catch()` or `try`.
|
|
372
|
-
|
|
373
|
-
For **async** boundaries: use `.catch((e) => new MyError({ cause: e }))` directly on the promise. TypeScript infers the union automatically. For **sync** boundaries: use `errore.try(() => ..., (e) => ...)`. The `.catch()` callback receives `any` (Promise rejections are untyped), but wrapping in a typed error gives the union a concrete type — no `as` assertions needed.
|
|
234
|
+
For **async** boundaries: `.catch((e) => new MyError({ cause: e }))` directly on the promise. For **sync** boundaries: `errore.try(() => ..., (e) => ...)`. The `.catch()` callback receives `any` (Promise rejections are untyped), but wrapping in a typed error gives the union a concrete type — no `as` assertions needed. TypeScript infers the union automatically:
|
|
374
235
|
|
|
375
236
|
```ts
|
|
376
237
|
async function getUser(id: string) {
|
|
@@ -379,6 +240,8 @@ async function getUser(id: string) {
|
|
|
379
240
|
)
|
|
380
241
|
if (res instanceof Error) return res
|
|
381
242
|
|
|
243
|
+
if (!res.ok) return new NetworkError({ url: `/users/${id}`, reason: `HTTP ${res.status}` })
|
|
244
|
+
|
|
382
245
|
const data = await (res.json() as Promise<UserPayload>).catch(
|
|
383
246
|
(e) => new NetworkError({ url: `/users/${id}`, cause: e }),
|
|
384
247
|
)
|
|
@@ -389,7 +252,7 @@ async function getUser(id: string) {
|
|
|
389
252
|
}
|
|
390
253
|
```
|
|
391
254
|
|
|
392
|
-
> Think of `.catch()` and `errore.try` as the **adapter** between the throwing world (external code) and the errore world (errors as values). Once you've converted exceptions to values at the boundary, everything above is plain `instanceof` checks.
|
|
255
|
+
> Think of `.catch()` and `errore.try` as the **adapter** between the throwing world (external code) and the errore world (errors as values). Once you've converted exceptions to values at the boundary, everything above is plain `instanceof` checks.
|
|
393
256
|
|
|
394
257
|
### Optional Values (| null)
|
|
395
258
|
|
|
@@ -447,7 +310,7 @@ return res.status(response.status).json(response.body)
|
|
|
447
310
|
|
|
448
311
|
### Resource Cleanup (defer) — Replacing try/finally with `using`
|
|
449
312
|
|
|
450
|
-
`try/finally` has a structural problem: **every resource adds a nesting level**. Two resources = two levels of indentation
|
|
313
|
+
`try/finally` has a structural problem: **every resource adds a nesting level**. Two resources = two levels of indentation, and cleanup is split across `finally` blocks far from where the resource was acquired. `await using` + `DisposableStack` keeps the function flat — one `cleanup.defer()` per resource, same indentation whether you have one resource or ten. Cleanup runs automatically in reverse (LIFO) order on every exit path — normal return, early error return, or exception.
|
|
451
314
|
|
|
452
315
|
**tsconfig requirement:** add `"ESNext.Disposable"` to `lib`:
|
|
453
316
|
|
|
@@ -459,29 +322,6 @@ return res.status(response.status).json(response.body)
|
|
|
459
322
|
}
|
|
460
323
|
```
|
|
461
324
|
|
|
462
|
-
**Before — nested try/finally:**
|
|
463
|
-
|
|
464
|
-
```ts
|
|
465
|
-
async function importData(url: string, dbUrl: string) {
|
|
466
|
-
const db = await connectDb(dbUrl)
|
|
467
|
-
try {
|
|
468
|
-
const tmpFile = await createTempFile()
|
|
469
|
-
try {
|
|
470
|
-
const data = await (await fetch(url)).text()
|
|
471
|
-
await tmpFile.write(data)
|
|
472
|
-
await db.import(tmpFile.path)
|
|
473
|
-
return { rows: await db.count() }
|
|
474
|
-
} finally {
|
|
475
|
-
await tmpFile.delete()
|
|
476
|
-
}
|
|
477
|
-
} finally {
|
|
478
|
-
await db.close()
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
**After — flat with `await using`:**
|
|
484
|
-
|
|
485
325
|
```ts
|
|
486
326
|
async function importData(url: string, dbUrl: string): Promise<ImportError | { rows: number }> {
|
|
487
327
|
await using cleanup = new errore.AsyncDisposableStack()
|
|
@@ -503,7 +343,7 @@ async function importData(url: string, dbUrl: string): Promise<ImportError | { r
|
|
|
503
343
|
}
|
|
504
344
|
```
|
|
505
345
|
|
|
506
|
-
>
|
|
346
|
+
> Adding a resource is one line (`cleanup.defer()`), not another nesting level. The errore polyfill handles the runtime; the tsconfig `lib` entry handles the types.
|
|
507
347
|
|
|
508
348
|
### Fallback Values
|
|
509
349
|
|
|
@@ -570,7 +410,7 @@ async function legacyHandler(id: string) {
|
|
|
570
410
|
|
|
571
411
|
### Converting `{ data, error }` Returns
|
|
572
412
|
|
|
573
|
-
Some SDKs (Supabase, Stripe, etc.) return `{ data, error }` instead of throwing. Destructure inline, check `error` first
|
|
413
|
+
Some SDKs (Supabase, Stripe, etc.) return `{ data, error }` instead of throwing. Destructure inline, check `error` first with a truthy check (not `instanceof` — most SDKs return plain objects), wrap in a tagged error, then continue with `data`:
|
|
574
414
|
|
|
575
415
|
```ts
|
|
576
416
|
const { data, error } = await supabase.from('users').select('*').eq('id', id)
|
|
@@ -579,7 +419,7 @@ if (data === null) return new NotFoundError({ id })
|
|
|
579
419
|
// data is narrowed here
|
|
580
420
|
```
|
|
581
421
|
|
|
582
|
-
>
|
|
422
|
+
> Wrapping in a domain error is better than returning the SDK's error directly — gives you `_tag`, typed properties, and the `cause` chain.
|
|
583
423
|
|
|
584
424
|
### Partition: Splitting Successes and Failures
|
|
585
425
|
|
|
@@ -595,9 +435,11 @@ errors.forEach((e) => console.warn('Failed:', e.message))
|
|
|
595
435
|
|
|
596
436
|
### Abort & Cancellation
|
|
597
437
|
|
|
598
|
-
`controller.abort(reason)` throws `reason` as-is — whatever you pass is what `.catch()` receives. This means you MUST pass a typed error extending `errore.AbortError`, never a plain `Error` or string.
|
|
438
|
+
`controller.abort(reason)` throws `reason` as-is — whatever you pass is what `.catch()` receives. This means you MUST pass a typed error extending `errore.AbortError`, never a plain `Error` or string — otherwise `isAbortError` can't detect it.
|
|
439
|
+
|
|
440
|
+
Always use `errore.isAbortError(error)` to detect abort errors — never check `error.name === 'AbortError'` manually, because tagged abort errors have their tag as `.name`. `isAbortError` walks the entire `.cause` chain, so it works even when the abort error is wrapped by `.catch()`.
|
|
599
441
|
|
|
600
|
-
|
|
442
|
+
Keep abort checks flat: `isAbortError(result)` first as its own early return, then `result instanceof Error` as a separate early return. Never nest one inside the other.
|
|
601
443
|
|
|
602
444
|
```ts
|
|
603
445
|
import * as errore from 'errore'
|