@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.41
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +108 -1
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1899 -38
- package/dist/core/repl/slash-commands.js +406 -21
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3073 -321
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +390 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +156 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +69 -2
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +303 -13
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +72 -14
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L30 (2026-05-27) — TUI color theme presets.
|
|
3
|
+
*
|
|
4
|
+
* Sibling to `core/output-style/presets.ts` but addressing a different
|
|
5
|
+
* layer of the operator surface:
|
|
6
|
+
*
|
|
7
|
+
* - `output-style` steers the engine's PROSE register (terse /
|
|
8
|
+
* explanatory / russian-formal / casual). It compiles into a rule
|
|
9
|
+
* block appended to the system prompt; the model adjusts voice.
|
|
10
|
+
*
|
|
11
|
+
* - `theme` steers the LOCAL TUI's color palette. It never reaches
|
|
12
|
+
* the engine; only the Ink components in `tui/*.tsx` read it. The
|
|
13
|
+
* operator can run any output-style under any theme — the two
|
|
14
|
+
* surfaces are orthogonal.
|
|
15
|
+
*
|
|
16
|
+
* Design contract:
|
|
17
|
+
*
|
|
18
|
+
* - The catalogue is a closed set of 4 entries — `default`, `dark`,
|
|
19
|
+
* `light`, `colorblind`. The slug union is intentionally tight so
|
|
20
|
+
* the operator can hold the full surface in working memory and so
|
|
21
|
+
* Ink consumers can switch on every slug without a fall-through.
|
|
22
|
+
*
|
|
23
|
+
* - Each preset defines 7 semantic color tokens (foreground, muted,
|
|
24
|
+
* accent, success, warning, error, background). Ink components
|
|
25
|
+
* reference these tokens via `useTheme()` instead of inlining
|
|
26
|
+
* literal hex codes. The brand accent `#3da9fc` is preserved in
|
|
27
|
+
* `default` so the existing header / splash chrome reads
|
|
28
|
+
* identically when no override is active.
|
|
29
|
+
*
|
|
30
|
+
* - `colorblind` is tuned for deuteranopia (red-green color
|
|
31
|
+
* blindness, ~5% of male population). Status colors are mapped to
|
|
32
|
+
* a blue-yellow axis (`success` cyan, `warning` yellow, `error`
|
|
33
|
+
* bright-magenta) so the OK/WARN/ERROR triplet remains
|
|
34
|
+
* distinguishable without red/green discrimination. The accent is
|
|
35
|
+
* also shifted to bright-yellow `#ffd166` so it does not collide
|
|
36
|
+
* with the success cue.
|
|
37
|
+
*
|
|
38
|
+
* - `light` uses darker foreground + lighter accent values so the
|
|
39
|
+
* palette reads on a white terminal background. Most operators
|
|
40
|
+
* run dark terminals, so `dark` is closer to the default;
|
|
41
|
+
* `light` exists specifically for screen-share / projector demos
|
|
42
|
+
* where the room is bright.
|
|
43
|
+
*
|
|
44
|
+
* - All color values are 6-digit hex strings prefixed with `#`. Ink's
|
|
45
|
+
* `Text color` prop accepts hex strings directly; we deliberately
|
|
46
|
+
* do NOT use named colors like `green` / `red` so the palette is
|
|
47
|
+
* fully under operator control. The Ink-named fallback for OK/
|
|
48
|
+
* WARN/ERROR exists separately in `doctor-table.tsx` legacy code
|
|
49
|
+
* and gets superseded once the theme context is wired.
|
|
50
|
+
*
|
|
51
|
+
* Test surface: `test/commands/theme-presets.spec.ts` exercises
|
|
52
|
+
* catalogue invariants (4 entries, unique slugs, every preset has all
|
|
53
|
+
* 7 tokens, hex format, colorblind palette avoids red-green for
|
|
54
|
+
* status), the `isThemeSlug` predicate, and the `compileSampleRow`
|
|
55
|
+
* helper used by the preview table.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* The closed list of theme slugs in catalogue order. Mirror used by
|
|
59
|
+
* the CLI surface (`/theme` table, `pugi theme --list`) so the
|
|
60
|
+
* operator sees themes in a stable order regardless of iteration
|
|
61
|
+
* order of the keyed catalogue.
|
|
62
|
+
*/
|
|
63
|
+
export const THEME_SLUGS = Object.freeze([
|
|
64
|
+
'default',
|
|
65
|
+
'dark',
|
|
66
|
+
'light',
|
|
67
|
+
'colorblind',
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Default slug used when no workspace-/user-level preference is set.
|
|
71
|
+
* Exported so `state.ts` and the CLI handler share one constant.
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_THEME = 'default';
|
|
74
|
+
/**
|
|
75
|
+
* Catalogue keyed by slug. Frozen so callers cannot mutate the
|
|
76
|
+
* shared rows; the CLI handler returns slugs, not preset references,
|
|
77
|
+
* to keep the boundary clean.
|
|
78
|
+
*
|
|
79
|
+
* Color choices:
|
|
80
|
+
*
|
|
81
|
+
* - `default` accent `#3da9fc` is the existing Pugi blue baked into
|
|
82
|
+
* `repl.tsx` header + `/help` palette. Preserved verbatim so the
|
|
83
|
+
* default theme reads identically to pre-L30 chrome.
|
|
84
|
+
*
|
|
85
|
+
* - `dark` saturates the brand palette for deep-black terminals
|
|
86
|
+
* (true-black iTerm, kitty, alacritty). Accent cyan `#22d3ee`
|
|
87
|
+
* pops against `#0a0a0a`; foreground is a near-white `#f5f5f5`
|
|
88
|
+
* that does not glare.
|
|
89
|
+
*
|
|
90
|
+
* - `light` inverts the contrast: foreground is `#1a1a1a` (near-
|
|
91
|
+
* black), background `#fafafa` (off-white), accent `#1e40af`
|
|
92
|
+
* (deep blue, readable on white). Status colors are darkened
|
|
93
|
+
* equivalents — `#15803d` green, `#a16207` amber, `#b91c1c`
|
|
94
|
+
* deep-red — so they retain saturation on bright backgrounds.
|
|
95
|
+
*
|
|
96
|
+
* - `colorblind` shifts the status axis from red-green to
|
|
97
|
+
* blue-yellow. `#0ea5e9` cyan replaces green, `#facc15` yellow
|
|
98
|
+
* stays warning, `#d946ef` magenta replaces red. The
|
|
99
|
+
* deuteranopia community can distinguish blue from yellow from
|
|
100
|
+
* magenta even when red/green collapse. Accent moves to
|
|
101
|
+
* `#ffd166` bright-yellow so it does not collide with success
|
|
102
|
+
* cyan; we trade brand consistency for accessibility here, which
|
|
103
|
+
* is the explicit purpose of the preset.
|
|
104
|
+
*/
|
|
105
|
+
export const THEMES = Object.freeze({
|
|
106
|
+
default: Object.freeze({
|
|
107
|
+
slug: 'default',
|
|
108
|
+
title: 'Default',
|
|
109
|
+
gloss: 'Current Pugi palette — blue accent on dark terminal.',
|
|
110
|
+
colors: Object.freeze({
|
|
111
|
+
foreground: '#e5e7eb',
|
|
112
|
+
background: '#0f172a',
|
|
113
|
+
muted: '#94a3b8',
|
|
114
|
+
accent: '#3da9fc',
|
|
115
|
+
success: '#22c55e',
|
|
116
|
+
warning: '#eab308',
|
|
117
|
+
error: '#ef4444',
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
dark: Object.freeze({
|
|
121
|
+
slug: 'dark',
|
|
122
|
+
title: 'Dark',
|
|
123
|
+
gloss: 'Saturated palette for deep-black terminals (iTerm, alacritty, kitty).',
|
|
124
|
+
colors: Object.freeze({
|
|
125
|
+
foreground: '#f5f5f5',
|
|
126
|
+
background: '#0a0a0a',
|
|
127
|
+
muted: '#737373',
|
|
128
|
+
accent: '#22d3ee',
|
|
129
|
+
success: '#4ade80',
|
|
130
|
+
warning: '#fbbf24',
|
|
131
|
+
error: '#f87171',
|
|
132
|
+
}),
|
|
133
|
+
}),
|
|
134
|
+
light: Object.freeze({
|
|
135
|
+
slug: 'light',
|
|
136
|
+
title: 'Light',
|
|
137
|
+
gloss: 'Inverted palette for projector demos + white-background terminals.',
|
|
138
|
+
colors: Object.freeze({
|
|
139
|
+
foreground: '#1a1a1a',
|
|
140
|
+
background: '#fafafa',
|
|
141
|
+
muted: '#525252',
|
|
142
|
+
accent: '#1e40af',
|
|
143
|
+
success: '#15803d',
|
|
144
|
+
warning: '#a16207',
|
|
145
|
+
error: '#b91c1c',
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
colorblind: Object.freeze({
|
|
149
|
+
slug: 'colorblind',
|
|
150
|
+
title: 'Colorblind',
|
|
151
|
+
gloss: 'High-contrast deuteranopia-safe — status on blue-yellow-magenta axis.',
|
|
152
|
+
colors: Object.freeze({
|
|
153
|
+
foreground: '#f5f5f5',
|
|
154
|
+
background: '#0f172a',
|
|
155
|
+
muted: '#9ca3af',
|
|
156
|
+
accent: '#ffd166',
|
|
157
|
+
success: '#0ea5e9',
|
|
158
|
+
warning: '#facc15',
|
|
159
|
+
error: '#d946ef',
|
|
160
|
+
}),
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
/**
|
|
164
|
+
* Type-narrowing predicate. Used by the slash-command parser + state
|
|
165
|
+
* loader so an unknown string from operator input or a stale config
|
|
166
|
+
* file degrades to the default theme instead of crashing.
|
|
167
|
+
*/
|
|
168
|
+
export function isThemeSlug(value) {
|
|
169
|
+
return (typeof value === 'string' && THEME_SLUGS.includes(value));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolve the colors for a slug. Pure lookup; never throws. Callers
|
|
173
|
+
* pass the slug from `resolveTheme()` so unknown values cannot reach
|
|
174
|
+
* this helper, but defensive isThemeSlug + DEFAULT_THEME fallback is
|
|
175
|
+
* still applied so future refactors cannot regress to a runtime
|
|
176
|
+
* crash on a stale config.
|
|
177
|
+
*/
|
|
178
|
+
export function getThemeColors(slug) {
|
|
179
|
+
const preset = THEMES[slug] ?? THEMES[DEFAULT_THEME];
|
|
180
|
+
return preset.colors;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Render the theme catalogue as a plain-text table for the `/theme`
|
|
184
|
+
* + `pugi theme` surfaces. Marks the active slug with `*` so the
|
|
185
|
+
* operator can see at a glance which theme is in effect.
|
|
186
|
+
*
|
|
187
|
+
* Pure renderer (no fs, no env). Identical text is emitted from both
|
|
188
|
+
* the slash dispatcher and the top-level CLI command so operators
|
|
189
|
+
* trained on one surface read the same table on the other.
|
|
190
|
+
*
|
|
191
|
+
* The plain-text variant skips the color-sample preview row — Ink's
|
|
192
|
+
* `<ThemeTable>` in `tui/theme-table.tsx` renders the sample row
|
|
193
|
+
* inline using `Text color={preset.colors.accent}` so the operator
|
|
194
|
+
* can preview each palette before switching.
|
|
195
|
+
*/
|
|
196
|
+
export function renderThemeTable(active) {
|
|
197
|
+
const slugWidth = Math.max('NAME'.length, ...THEME_SLUGS.map((slug) => slug.length));
|
|
198
|
+
const header = `${'NAME'.padEnd(slugWidth)} GLOSS`;
|
|
199
|
+
const rows = THEME_SLUGS.map((slug) => {
|
|
200
|
+
const preset = THEMES[slug];
|
|
201
|
+
const marker = slug === active ? '*' : ' ';
|
|
202
|
+
return `${marker} ${slug.padEnd(slugWidth)} ${preset.gloss}`;
|
|
203
|
+
});
|
|
204
|
+
return [header, ...rows].join('\n');
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build the per-row sample text used by `<ThemeTable>`. Pure helper
|
|
208
|
+
* so the spec can assert the canonical sample copy ("Aa 123 OK WARN
|
|
209
|
+
* ERROR") without mounting Ink. The TUI component colors each
|
|
210
|
+
* fragment with the matching token (`foreground`, `accent`, `success`,
|
|
211
|
+
* `warning`, `error`) — this helper only returns the source strings
|
|
212
|
+
* the renderer splices in.
|
|
213
|
+
*/
|
|
214
|
+
export function compileSampleRow(slug) {
|
|
215
|
+
// The strings are slug-independent today; the helper exists so
|
|
216
|
+
// future presets can ship per-theme sample copy (e.g. localised
|
|
217
|
+
// OK/ERROR labels for a future `russian-tui` preset) without
|
|
218
|
+
// touching the consumer Ink component.
|
|
219
|
+
void slug;
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
foreground: 'Aa',
|
|
222
|
+
accent: '123',
|
|
223
|
+
success: 'OK',
|
|
224
|
+
warning: 'WARN',
|
|
225
|
+
error: 'ERROR',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=presets.js.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L30 (2026-05-27) — Theme state persistence.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of `core/output-style/state.ts` — same two-tier ladder
|
|
5
|
+
* (workspace > user > default), same `~/.pugi/config.json` envelope,
|
|
6
|
+
* same malformed-config tolerance. The two modules write to disjoint
|
|
7
|
+
* keys (`outputStyle` vs `theme`) of the same JSON file so neighbour
|
|
8
|
+
* settings (`permissionMode`, `privacy`, `model`, `outputStyle`)
|
|
9
|
+
* survive a theme flip.
|
|
10
|
+
*
|
|
11
|
+
* Two-tier storage:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Workspace** — `<workspaceRoot>/.pugi/config.json`. Set by
|
|
14
|
+
* `/theme <name>` or `pugi theme <name>` without `--persist`.
|
|
15
|
+
* Overrides the user default for the current workspace only.
|
|
16
|
+
*
|
|
17
|
+
* 2. **User default** — `~/.pugi/config.json` (PUGI_HOME-aware).
|
|
18
|
+
* Set by `pugi theme <name> --persist` or `/theme <name>
|
|
19
|
+
* --persist`. Applies to every workspace that has no
|
|
20
|
+
* workspace-level override.
|
|
21
|
+
*
|
|
22
|
+
* Precedence (highest → lowest):
|
|
23
|
+
*
|
|
24
|
+
* workspace value > user value > DEFAULT_THEME ('default')
|
|
25
|
+
*
|
|
26
|
+
* The reader tolerates:
|
|
27
|
+
* - missing file (returns the default slug),
|
|
28
|
+
* - empty file (returns the default slug),
|
|
29
|
+
* - malformed JSON (returns the default slug — DO NOT crash REPL
|
|
30
|
+
* boot because of a hand-edited config),
|
|
31
|
+
* - unknown slug (returns the default slug + emits no error; the
|
|
32
|
+
* operator can `/theme` to see the table and re-set).
|
|
33
|
+
*
|
|
34
|
+
* The writer is a read-modify-write to preserve neighbouring keys
|
|
35
|
+
* (`outputStyle`, `permissionMode`, etc.) — overwriting the whole
|
|
36
|
+
* file would clobber the other tier's settings AND any sibling
|
|
37
|
+
* module's slot in the same envelope.
|
|
38
|
+
*
|
|
39
|
+
* Test surface: `test/commands/theme-state.spec.ts` exercises
|
|
40
|
+
* precedence, malformed-config tolerance, persistence across reads,
|
|
41
|
+
* the `--persist` (user-default) path, reset semantics, and the
|
|
42
|
+
* coexistence contract with `outputStyle` in the same file.
|
|
43
|
+
*/
|
|
44
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
45
|
+
import { homedir } from 'node:os';
|
|
46
|
+
import { dirname, resolve } from 'node:path';
|
|
47
|
+
import { DEFAULT_THEME, isThemeSlug } from './presets.js';
|
|
48
|
+
/**
|
|
49
|
+
* Env override for `~/.pugi` so the spec can sandbox both tiers
|
|
50
|
+
* without touching the developer's real config. Re-exported under a
|
|
51
|
+
* theme-specific alias so consumers in this module do not need to
|
|
52
|
+
* import the output-style constant; the underlying env key is shared
|
|
53
|
+
* (`PUGI_HOME`) because the two modules write to the same config
|
|
54
|
+
* envelope.
|
|
55
|
+
*/
|
|
56
|
+
export const PUGI_HOME_ENV = 'PUGI_HOME';
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the active theme for the workspace, applying the
|
|
59
|
+
* precedence ladder (workspace > user > default).
|
|
60
|
+
*
|
|
61
|
+
* Pure read. Never writes, never throws — every IO failure degrades
|
|
62
|
+
* to the default slug. The function returns the source label too so
|
|
63
|
+
* the CLI surface can show the operator where the value came from.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveTheme(io) {
|
|
66
|
+
const workspaceSlug = readSlugFromFile(workspaceConfigPath(io.workspaceRoot));
|
|
67
|
+
if (workspaceSlug)
|
|
68
|
+
return { slug: workspaceSlug, source: 'workspace' };
|
|
69
|
+
const userSlug = readSlugFromFile(userConfigPath(io.env ?? process.env));
|
|
70
|
+
if (userSlug)
|
|
71
|
+
return { slug: userSlug, source: 'user' };
|
|
72
|
+
return { slug: DEFAULT_THEME, source: 'default' };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Write `slug` to the workspace tier. Creates `<workspaceRoot>/.pugi/`
|
|
76
|
+
* if missing. Preserves neighbouring config keys via read-modify-write.
|
|
77
|
+
*/
|
|
78
|
+
export function setWorkspaceTheme(slug, io) {
|
|
79
|
+
writeSlugToFile(workspaceConfigPath(io.workspaceRoot), slug);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Write `slug` to the user tier (`~/.pugi/config.json`).
|
|
83
|
+
*
|
|
84
|
+
* Mirrors the workspace writer's read-modify-write so the user's
|
|
85
|
+
* `outputStyle` / `permissionMode` / `privacy` / `model` keys survive
|
|
86
|
+
* a theme flip.
|
|
87
|
+
*/
|
|
88
|
+
export function setUserTheme(slug, io) {
|
|
89
|
+
writeSlugToFile(userConfigPath(io.env ?? process.env), slug);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clear the workspace tier's `theme` key. The user tier (and
|
|
93
|
+
* therefore the eventual resolved theme) is left untouched.
|
|
94
|
+
*
|
|
95
|
+
* Used by `/theme --reset` so the operator can revert a workspace
|
|
96
|
+
* override without nuking the rest of their workspace config (or the
|
|
97
|
+
* user default).
|
|
98
|
+
*/
|
|
99
|
+
export function clearWorkspaceTheme(io) {
|
|
100
|
+
clearSlugInFile(workspaceConfigPath(io.workspaceRoot));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Clear the user tier's `theme` key. Lower-blast-radius reset for
|
|
104
|
+
* operators who want every workspace to fall back to `default` unless
|
|
105
|
+
* an explicit workspace value is set.
|
|
106
|
+
*/
|
|
107
|
+
export function clearUserTheme(io) {
|
|
108
|
+
clearSlugInFile(userConfigPath(io.env ?? process.env));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Workspace config path. Exported for the spec; production callers
|
|
112
|
+
* should use the `setWorkspace…` / `resolveTheme` helpers.
|
|
113
|
+
*/
|
|
114
|
+
export function workspaceConfigPath(workspaceRoot) {
|
|
115
|
+
return resolve(workspaceRoot, '.pugi', 'config.json');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* User config path resolved against `PUGI_HOME` (or `~/.pugi`).
|
|
119
|
+
* Exported for the spec.
|
|
120
|
+
*/
|
|
121
|
+
export function userConfigPath(env = process.env) {
|
|
122
|
+
const home = env[PUGI_HOME_ENV] ?? resolve(homedir(), '.pugi');
|
|
123
|
+
return resolve(home, 'config.json');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Read + parse a config file. Returns an empty object on any IO or
|
|
127
|
+
* parse error. Caller-provided JSON must be a plain object; arrays /
|
|
128
|
+
* scalars / null are treated as "no config" so a hand-edited file
|
|
129
|
+
* never crashes the REPL.
|
|
130
|
+
*/
|
|
131
|
+
function readConfigFile(path) {
|
|
132
|
+
if (!existsSync(path))
|
|
133
|
+
return {};
|
|
134
|
+
let raw;
|
|
135
|
+
try {
|
|
136
|
+
raw = readFileSync(path, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
if (raw.trim().length === 0)
|
|
142
|
+
return {};
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(raw);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
151
|
+
return {};
|
|
152
|
+
return parsed;
|
|
153
|
+
}
|
|
154
|
+
function writeConfigFile(path, config) {
|
|
155
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
156
|
+
// 0o600 mirrors `core/output-style/state.ts` + `runtime/commands/config.ts` —
|
|
157
|
+
// the config file may hold preferredEndpoint URLs etc. that should
|
|
158
|
+
// not be world-readable.
|
|
159
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
160
|
+
encoding: 'utf8',
|
|
161
|
+
mode: 0o600,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function readSlugFromFile(path) {
|
|
165
|
+
const config = readConfigFile(path);
|
|
166
|
+
const candidate = config.theme;
|
|
167
|
+
return isThemeSlug(candidate) ? candidate : null;
|
|
168
|
+
}
|
|
169
|
+
function writeSlugToFile(path, slug) {
|
|
170
|
+
const config = readConfigFile(path);
|
|
171
|
+
config.theme = slug;
|
|
172
|
+
writeConfigFile(path, config);
|
|
173
|
+
}
|
|
174
|
+
function clearSlugInFile(path) {
|
|
175
|
+
const config = readConfigFile(path);
|
|
176
|
+
if (!('theme' in config))
|
|
177
|
+
return;
|
|
178
|
+
delete config.theme;
|
|
179
|
+
writeConfigFile(path, config);
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-in-progress invariant constants and types — Leak L16.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a separate module so the tool-bridge dispatcher and the
|
|
5
|
+
* spec layer can import the sentinel string + types without pulling
|
|
6
|
+
* in `node:fs` via `state.ts`. The sentinel prefix is stable so the
|
|
7
|
+
* engine adapter / model prompt can pattern-match on it verbatim.
|
|
8
|
+
*/
|
|
9
|
+
export const TODO_INVARIANT_VIOLATED = 'TODO_INVARIANT_VIOLATED';
|
|
10
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-scoped todo board persistence — Leak L16 (TodoWrite invariant).
|
|
3
|
+
*
|
|
4
|
+
* `todo_write` is the BATCH-replace counterpart to the granular `task_*`
|
|
5
|
+
* journal in `tools/tasks.ts`. Where `task_*` appends one journal line
|
|
6
|
+
* per mutation (session-scoped, JSONL, append-only), `todo_write` snaps
|
|
7
|
+
* the entire board to disk in one atomic write (workspace-scoped, JSON,
|
|
8
|
+
* snapshot-only). They are complementary surfaces for the same problem
|
|
9
|
+
* — `task_*` is fine-grained, `todo_write` mirrors Claude Code's
|
|
10
|
+
* TodoWrite verbatim so a model trained on that grammar speaks Pugi's
|
|
11
|
+
* variant 1:1.
|
|
12
|
+
*
|
|
13
|
+
* Single-in-progress invariant
|
|
14
|
+
* ----------------------------
|
|
15
|
+
* Claude Code TodoWrite enforces at most ONE todo `status: 'in_progress'`
|
|
16
|
+
* at a time. The invariant prevents the model from fragmenting attention
|
|
17
|
+
* across multiple parallel claims of "I am working on X right now" — a
|
|
18
|
+
* pattern that empirically produces stalled work and operator confusion
|
|
19
|
+
* about which thread is alive. We enforce the same invariant at the tool
|
|
20
|
+
* boundary: a dispatch with >1 `in_progress` rejects with
|
|
21
|
+
* `TODO_INVARIANT_VIOLATED: ...` and the board on disk is left unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Persistence shape
|
|
24
|
+
* -----------------
|
|
25
|
+
* Path: `<workspaceRoot>/.pugi/todos.json` (one board per workspace).
|
|
26
|
+
* Atomic: write to `<path>.pugi-tmp-<ts>` then `renameSync` — the rename
|
|
27
|
+
* is the commit point. A crash mid-write leaves the previous snapshot
|
|
28
|
+
* intact (or, on first write, leaves the dangling tmp file which the
|
|
29
|
+
* loader ignores). Mode 0o600 — the board can contain operator task
|
|
30
|
+
* descriptions and should not be world-readable through an inherited
|
|
31
|
+
* umask.
|
|
32
|
+
*
|
|
33
|
+
* Why workspace-scoped, not session-scoped
|
|
34
|
+
* ----------------------------------------
|
|
35
|
+
* The Claude Code TodoWrite contract is "the board persists across the
|
|
36
|
+
* session". Pugi sessions are short-lived (one REPL run); the operator's
|
|
37
|
+
* mental model of the board is "this is the workspace's plan", not "this
|
|
38
|
+
* is THIS session's plan". Workspace scope matches that mental model and
|
|
39
|
+
* lets `/todos` (when wired) read the latest board even after a CLI
|
|
40
|
+
* restart. The `task_*` ledger remains session-scoped for the agent's
|
|
41
|
+
* dispatch trace.
|
|
42
|
+
*/
|
|
43
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
44
|
+
import { dirname, join } from 'node:path';
|
|
45
|
+
import { TODO_INVARIANT_VIOLATED } from './invariant.js';
|
|
46
|
+
/** Path the board lives at. Workspace-scoped, NOT session-scoped. */
|
|
47
|
+
export function todoBoardPath(ctx) {
|
|
48
|
+
return join(ctx.workspaceRoot, '.pugi', 'todos.json');
|
|
49
|
+
}
|
|
50
|
+
function ensureDir(path) {
|
|
51
|
+
const dir = dirname(path);
|
|
52
|
+
if (!existsSync(dir)) {
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
try {
|
|
55
|
+
chmodSync(dir, 0o700);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort; Windows NTFS no-op. The 0o600 mode on the JSON
|
|
59
|
+
// file itself remains the primary guard.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function nowIso(ctx) {
|
|
64
|
+
return (ctx.now ? ctx.now() : new Date()).toISOString();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load the current board. Returns an empty board when the file is
|
|
68
|
+
* absent or malformed — the rationale is that a corrupted state file
|
|
69
|
+
* should NOT brick the tool surface; the model can re-emit the full
|
|
70
|
+
* board on the next call. Malformed loads are silent (no throw) so a
|
|
71
|
+
* fresh workspace and a corrupted workspace look identical to the
|
|
72
|
+
* caller.
|
|
73
|
+
*
|
|
74
|
+
* The schema check is deliberately defensive: any missing field, wrong
|
|
75
|
+
* type, or unexpected status string drops the load to an empty board.
|
|
76
|
+
* The model will see `todos: []` and is free to redeclare the plan.
|
|
77
|
+
*/
|
|
78
|
+
export function loadTodoBoard(ctx) {
|
|
79
|
+
const path = todoBoardPath(ctx);
|
|
80
|
+
if (!existsSync(path)) {
|
|
81
|
+
return { version: 1, updatedAt: nowIso(ctx), todos: [] };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const raw = readFileSync(path, 'utf8');
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
if (!isPlainObject(parsed))
|
|
87
|
+
return emptyBoard(ctx);
|
|
88
|
+
if (parsed.version !== 1)
|
|
89
|
+
return emptyBoard(ctx);
|
|
90
|
+
if (typeof parsed.updatedAt !== 'string')
|
|
91
|
+
return emptyBoard(ctx);
|
|
92
|
+
if (!Array.isArray(parsed.todos))
|
|
93
|
+
return emptyBoard(ctx);
|
|
94
|
+
const todos = [];
|
|
95
|
+
for (const entry of parsed.todos) {
|
|
96
|
+
if (!isPlainObject(entry))
|
|
97
|
+
return emptyBoard(ctx);
|
|
98
|
+
if (typeof entry.id !== 'string' || entry.id.length === 0)
|
|
99
|
+
return emptyBoard(ctx);
|
|
100
|
+
if (typeof entry.content !== 'string' || entry.content.length === 0) {
|
|
101
|
+
return emptyBoard(ctx);
|
|
102
|
+
}
|
|
103
|
+
if (entry.status !== 'pending' &&
|
|
104
|
+
entry.status !== 'in_progress' &&
|
|
105
|
+
entry.status !== 'completed') {
|
|
106
|
+
return emptyBoard(ctx);
|
|
107
|
+
}
|
|
108
|
+
const item = {
|
|
109
|
+
id: entry.id,
|
|
110
|
+
content: entry.content,
|
|
111
|
+
status: entry.status,
|
|
112
|
+
...(typeof entry.activeForm === 'string' && entry.activeForm.length > 0
|
|
113
|
+
? { activeForm: entry.activeForm }
|
|
114
|
+
: {}),
|
|
115
|
+
};
|
|
116
|
+
todos.push(item);
|
|
117
|
+
}
|
|
118
|
+
return { version: 1, updatedAt: parsed.updatedAt, todos };
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return emptyBoard(ctx);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function emptyBoard(ctx) {
|
|
125
|
+
return { version: 1, updatedAt: nowIso(ctx), todos: [] };
|
|
126
|
+
}
|
|
127
|
+
function isPlainObject(value) {
|
|
128
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Persist a new board atomically. Enforces the single-in-progress
|
|
132
|
+
* invariant BEFORE touching disk so a violating dispatch never reaches
|
|
133
|
+
* the filesystem. On invariant violation, throws an Error whose message
|
|
134
|
+
* starts with `TODO_INVARIANT_VIOLATED:` (the sentinel prefix the tool
|
|
135
|
+
* dispatcher pattern-matches on).
|
|
136
|
+
*
|
|
137
|
+
* The atomic write uses tmp+rename — `writeFileSync` to a sibling tmp
|
|
138
|
+
* path, then `renameSync` onto the real file. POSIX guarantees rename is
|
|
139
|
+
* atomic within the same directory; on a crash, the operator sees the
|
|
140
|
+
* previous board (or no board at all on first write), never a torn
|
|
141
|
+
* write. Mode 0o600 is set on the tmp file so the rename inherits the
|
|
142
|
+
* restrictive mode.
|
|
143
|
+
*/
|
|
144
|
+
export function saveTodoBoard(ctx, todos) {
|
|
145
|
+
// Invariant check FIRST — never write a violating board.
|
|
146
|
+
const inProgress = todos.filter((t) => t.status === 'in_progress').length;
|
|
147
|
+
if (inProgress > 1) {
|
|
148
|
+
throw new Error(`${TODO_INVARIANT_VIOLATED}: ${inProgress} items in_progress simultaneously. ` +
|
|
149
|
+
`Mark all-but-one as 'pending' or 'completed'.`);
|
|
150
|
+
}
|
|
151
|
+
// Duplicate-id check — every id must be unique within the board so
|
|
152
|
+
// a `todo_get(id)` would have a single answer. The model emits ids;
|
|
153
|
+
// we refuse to persist a board that would silently shadow one.
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
for (const todo of todos) {
|
|
156
|
+
if (seen.has(todo.id)) {
|
|
157
|
+
throw new Error(`TODO_DUPLICATE_ID: id "${todo.id}" appears more than once in the batch. ` +
|
|
158
|
+
`Use a stable, unique id per todo item.`);
|
|
159
|
+
}
|
|
160
|
+
seen.add(todo.id);
|
|
161
|
+
}
|
|
162
|
+
const board = {
|
|
163
|
+
version: 1,
|
|
164
|
+
updatedAt: nowIso(ctx),
|
|
165
|
+
todos: todos.map((t) => ({ ...t })),
|
|
166
|
+
};
|
|
167
|
+
const path = todoBoardPath(ctx);
|
|
168
|
+
ensureDir(path);
|
|
169
|
+
const tmp = `${path}.pugi-tmp-${Date.now()}`;
|
|
170
|
+
writeFileSync(tmp, `${JSON.stringify(board, null, 2)}\n`, {
|
|
171
|
+
encoding: 'utf8',
|
|
172
|
+
mode: 0o600,
|
|
173
|
+
});
|
|
174
|
+
renameSync(tmp, path);
|
|
175
|
+
return board;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=state.js.map
|