terminal-pilot 0.0.30 → 0.0.31
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/cli.js +1375 -435
- package/dist/cli.js.map +4 -4
- package/dist/commands/close-session.js +485 -6
- package/dist/commands/close-session.js.map +4 -4
- package/dist/commands/create-session.js +485 -6
- package/dist/commands/create-session.js.map +4 -4
- package/dist/commands/fill.js +485 -6
- package/dist/commands/fill.js.map +4 -4
- package/dist/commands/get-session.js +485 -6
- package/dist/commands/get-session.js.map +4 -4
- package/dist/commands/index.js +626 -44
- package/dist/commands/index.js.map +4 -4
- package/dist/commands/install.js +622 -40
- package/dist/commands/install.js.map +4 -4
- package/dist/commands/installer.js +68 -17
- package/dist/commands/installer.js.map +4 -4
- package/dist/commands/list-sessions.js +485 -6
- package/dist/commands/list-sessions.js.map +4 -4
- package/dist/commands/press-key.js +485 -6
- package/dist/commands/press-key.js.map +4 -4
- package/dist/commands/read-history.js +485 -6
- package/dist/commands/read-history.js.map +4 -4
- package/dist/commands/read-screen.js +485 -6
- package/dist/commands/read-screen.js.map +4 -4
- package/dist/commands/resize.js +485 -6
- package/dist/commands/resize.js.map +4 -4
- package/dist/commands/screenshot.js +485 -6
- package/dist/commands/screenshot.js.map +4 -4
- package/dist/commands/send-signal.js +485 -6
- package/dist/commands/send-signal.js.map +4 -4
- package/dist/commands/type.js +485 -6
- package/dist/commands/type.js.map +4 -4
- package/dist/commands/uninstall.js +550 -20
- package/dist/commands/uninstall.js.map +4 -4
- package/dist/commands/wait-for-exit.js +485 -6
- package/dist/commands/wait-for-exit.js.map +4 -4
- package/dist/commands/wait-for.js +485 -6
- package/dist/commands/wait-for.js.map +4 -4
- package/dist/testing/cli-repl.js +1375 -435
- package/dist/testing/cli-repl.js.map +4 -4
- package/dist/testing/qa-cli.js +1385 -445
- package/dist/testing/qa-cli.js.map +4 -4
- package/node_modules/@poe-code/agent-defs/README.md +35 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +21 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +18 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +25 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/cursor.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +15 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +16 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +16 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +9 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.js +9 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +16 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +20 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
- package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.js +45 -0
- package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
- package/node_modules/@poe-code/agent-defs/dist/specifier.js +40 -0
- package/node_modules/@poe-code/agent-defs/dist/types.d.ts +28 -0
- package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
- package/node_modules/@poe-code/agent-defs/package.json +20 -0
- package/node_modules/@poe-code/agent-skill-config/dist/apply.js +2 -1
- package/node_modules/@poe-code/agent-skill-config/dist/bridge-active-skills.d.ts +1 -0
- package/node_modules/@poe-code/agent-skill-config/dist/bridge-active-skills.js +37 -18
- package/node_modules/@poe-code/agent-skill-config/dist/resolve-skill-reference.js +8 -4
- package/node_modules/@poe-code/agent-skill-config/package.json +1 -5
- package/node_modules/@poe-code/config-mutations/README.md +55 -0
- package/node_modules/@poe-code/config-mutations/dist/error-codes.d.ts +1 -0
- package/node_modules/@poe-code/config-mutations/dist/error-codes.js +6 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +6 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +829 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +64 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +39 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +32 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +171 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +75 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +76 -0
- package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
- package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +43 -0
- package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
- package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +62 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +55 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
- package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +6 -0
- package/node_modules/@poe-code/config-mutations/dist/template/render.js +8 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +194 -0
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +168 -0
- package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
- package/node_modules/@poe-code/config-mutations/package.json +29 -0
- package/node_modules/@poe-code/frontmatter/README.md +35 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +25 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +100 -0
- package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.js +209 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +48 -0
- package/node_modules/@poe-code/frontmatter/package.json +25 -0
- package/node_modules/toolcraft-design/README.md +160 -0
- package/node_modules/toolcraft-design/dist/acp/components.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/acp/components.js +137 -0
- package/node_modules/toolcraft-design/dist/acp/index.d.ts +3 -0
- package/node_modules/toolcraft-design/dist/acp/index.js +2 -0
- package/node_modules/toolcraft-design/dist/acp/writer.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/acp/writer.js +21 -0
- package/node_modules/toolcraft-design/dist/components/browser.d.ts +15 -0
- package/node_modules/toolcraft-design/dist/components/browser.js +31 -0
- package/node_modules/toolcraft-design/dist/components/catalog.d.ts +26 -0
- package/node_modules/toolcraft-design/dist/components/catalog.js +95 -0
- package/node_modules/toolcraft-design/dist/components/color.d.ts +31 -0
- package/node_modules/toolcraft-design/dist/components/color.js +102 -0
- package/node_modules/toolcraft-design/dist/components/command-errors.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/components/command-errors.js +23 -0
- package/node_modules/toolcraft-design/dist/components/detail-card.d.ts +22 -0
- package/node_modules/toolcraft-design/dist/components/detail-card.js +45 -0
- package/node_modules/toolcraft-design/dist/components/help-formatter-plain.d.ts +5 -0
- package/node_modules/toolcraft-design/dist/components/help-formatter-plain.js +132 -0
- package/node_modules/toolcraft-design/dist/components/help-formatter.d.ts +33 -0
- package/node_modules/toolcraft-design/dist/components/help-formatter.js +213 -0
- package/node_modules/toolcraft-design/dist/components/index.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/components/index.js +10 -0
- package/node_modules/toolcraft-design/dist/components/logger.d.ts +11 -0
- package/node_modules/toolcraft-design/dist/components/logger.js +60 -0
- package/node_modules/toolcraft-design/dist/components/symbols.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/components/symbols.js +71 -0
- package/node_modules/toolcraft-design/dist/components/table.d.ts +15 -0
- package/node_modules/toolcraft-design/dist/components/table.js +302 -0
- package/node_modules/toolcraft-design/dist/components/template.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/components/template.js +437 -0
- package/node_modules/toolcraft-design/dist/components/text.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/components/text.js +145 -0
- package/node_modules/toolcraft-design/dist/dashboard/ansi.d.ts +18 -0
- package/node_modules/toolcraft-design/dist/dashboard/ansi.js +343 -0
- package/node_modules/toolcraft-design/dist/dashboard/buffer.d.ts +25 -0
- package/node_modules/toolcraft-design/dist/dashboard/buffer.js +219 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/border.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/border.js +123 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/footer.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/footer.js +56 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.js +268 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.d.ts +7 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.js +107 -0
- package/node_modules/toolcraft-design/dist/dashboard/dashboard.d.ts +20 -0
- package/node_modules/toolcraft-design/dist/dashboard/dashboard.js +167 -0
- package/node_modules/toolcraft-design/dist/dashboard/demo.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/dashboard/demo.js +145 -0
- package/node_modules/toolcraft-design/dist/dashboard/index.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/dashboard/index.js +4 -0
- package/node_modules/toolcraft-design/dist/dashboard/keymap.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/dashboard/keymap.js +233 -0
- package/node_modules/toolcraft-design/dist/dashboard/layout.d.ts +25 -0
- package/node_modules/toolcraft-design/dist/dashboard/layout.js +79 -0
- package/node_modules/toolcraft-design/dist/dashboard/should-use-dashboard.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/dashboard/should-use-dashboard.js +7 -0
- package/node_modules/toolcraft-design/dist/dashboard/snapshot.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/dashboard/snapshot.js +68 -0
- package/node_modules/toolcraft-design/dist/dashboard/store.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/dashboard/store.js +51 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +71 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal.d.ts +37 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal.js +264 -0
- package/node_modules/toolcraft-design/dist/dashboard/types.d.ts +38 -0
- package/node_modules/toolcraft-design/dist/dashboard/types.js +1 -0
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/explorer/actions.js +39 -0
- package/node_modules/toolcraft-design/dist/explorer/demo.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/explorer/demo.js +297 -0
- package/node_modules/toolcraft-design/dist/explorer/events.d.ts +67 -0
- package/node_modules/toolcraft-design/dist/explorer/events.js +1 -0
- package/node_modules/toolcraft-design/dist/explorer/filter.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/explorer/filter.js +95 -0
- package/node_modules/toolcraft-design/dist/explorer/index.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/explorer/index.js +8 -0
- package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +7 -0
- package/node_modules/toolcraft-design/dist/explorer/jobs.js +59 -0
- package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +21 -0
- package/node_modules/toolcraft-design/dist/explorer/keymap.js +376 -0
- package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +20 -0
- package/node_modules/toolcraft-design/dist/explorer/layout.js +73 -0
- package/node_modules/toolcraft-design/dist/explorer/reducer.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +782 -0
- package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/render/detail.js +97 -0
- package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/render/footer.js +73 -0
- package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/render/header.js +49 -0
- package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/explorer/render/index.js +53 -0
- package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +116 -0
- package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +3 -0
- package/node_modules/toolcraft-design/dist/explorer/render/modal.js +87 -0
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +156 -0
- package/node_modules/toolcraft-design/dist/explorer/render/text.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/explorer/render/text.js +81 -0
- package/node_modules/toolcraft-design/dist/explorer/runtime.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +340 -0
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +50 -0
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +101 -0
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +137 -0
- package/node_modules/toolcraft-design/dist/explorer/state.js +86 -0
- package/node_modules/toolcraft-design/dist/explorer/theme.d.ts +27 -0
- package/node_modules/toolcraft-design/dist/explorer/theme.js +29 -0
- package/node_modules/toolcraft-design/dist/index.d.ts +50 -0
- package/node_modules/toolcraft-design/dist/index.js +43 -0
- package/node_modules/toolcraft-design/dist/internal/color-support.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/internal/color-support.js +12 -0
- package/node_modules/toolcraft-design/dist/internal/output-format.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/internal/output-format.js +22 -0
- package/node_modules/toolcraft-design/dist/internal/strip-ansi.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/internal/strip-ansi.js +50 -0
- package/node_modules/toolcraft-design/dist/internal/theme-detect.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/internal/theme-detect.js +71 -0
- package/node_modules/toolcraft-design/dist/internal/theme-state.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/internal/theme-state.js +35 -0
- package/node_modules/toolcraft-design/dist/prompts/index.d.ts +91 -0
- package/node_modules/toolcraft-design/dist/prompts/index.js +137 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +9 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/intro.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/intro.js +16 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/log.d.ts +18 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/log.js +104 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/note.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/note.js +39 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/outro.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/outro.js +16 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.js +74 -0
- package/node_modules/toolcraft-design/dist/prompts/theme.d.ts +11 -0
- package/node_modules/toolcraft-design/dist/prompts/theme.js +15 -0
- package/node_modules/toolcraft-design/dist/static/index.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/static/index.js +2 -0
- package/node_modules/toolcraft-design/dist/static/menu.d.ts +11 -0
- package/node_modules/toolcraft-design/dist/static/menu.js +42 -0
- package/node_modules/toolcraft-design/dist/static/spinner.d.ts +14 -0
- package/node_modules/toolcraft-design/dist/static/spinner.js +52 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/ast.d.ts +92 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/ast.js +1 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/demo-content.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/demo-content.js +139 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/index.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/index.js +8 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/block.d.ts +7 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/block.js +1495 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +36 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/inline.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/inline.js +1190 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser.d.ts +5 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser.js +42 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/renderer.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/renderer.js +615 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/testing/theme-render-fixture.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/testing/theme-render-fixture.js +27 -0
- package/node_modules/toolcraft-design/dist/tokens/brand.d.ts +5 -0
- package/node_modules/toolcraft-design/dist/tokens/brand.js +5 -0
- package/node_modules/toolcraft-design/dist/tokens/colors.d.ts +37 -0
- package/node_modules/toolcraft-design/dist/tokens/colors.js +85 -0
- package/node_modules/toolcraft-design/dist/tokens/index.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/tokens/index.js +5 -0
- package/node_modules/toolcraft-design/dist/tokens/spacing.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/tokens/spacing.js +6 -0
- package/node_modules/toolcraft-design/dist/tokens/typography.d.ts +7 -0
- package/node_modules/toolcraft-design/dist/tokens/typography.js +8 -0
- package/node_modules/toolcraft-design/dist/tokens/widths.d.ts +5 -0
- package/node_modules/toolcraft-design/dist/tokens/widths.js +5 -0
- package/node_modules/toolcraft-design/package.json +44 -0
- package/package.json +15 -5
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderTemplate } from "toolcraft-design";
|
|
4
|
+
import { getConfigFormat, detectFormat } from "../formats/index.js";
|
|
5
|
+
import { cloneConfigObject, setConfigEntry } from "../formats/object.js";
|
|
6
|
+
import { resolvePath } from "./path-utils.js";
|
|
7
|
+
import { isNotFound, readFileIfExists, pathExists, createTimestamp } from "../fs-utils.js";
|
|
8
|
+
import { hasOwnErrorCode } from "../error-codes.js";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Helper Functions
|
|
11
|
+
// ============================================================================
|
|
12
|
+
function resolveValue(resolver, options) {
|
|
13
|
+
if (typeof resolver === "function") {
|
|
14
|
+
return resolver(options);
|
|
15
|
+
}
|
|
16
|
+
return resolver;
|
|
17
|
+
}
|
|
18
|
+
function createInvalidDocumentBackupPath(targetPath) {
|
|
19
|
+
const ext = targetPath.includes(".") ? targetPath.split(".").pop() : "bak";
|
|
20
|
+
return `${targetPath}.invalid-${createTimestamp()}.${ext}`;
|
|
21
|
+
}
|
|
22
|
+
async function backupInvalidDocument(context, targetPath, content) {
|
|
23
|
+
const baseBackupPath = createInvalidDocumentBackupPath(targetPath);
|
|
24
|
+
let attempt = 0;
|
|
25
|
+
while (true) {
|
|
26
|
+
const backupPath = attempt === 0 ? baseBackupPath : `${baseBackupPath}-${attempt}`;
|
|
27
|
+
await assertRegularWriteTarget(context, backupPath);
|
|
28
|
+
try {
|
|
29
|
+
await context.fs.writeFile(backupPath, content, { encoding: "utf8", flag: "wx" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (!isAlreadyExists(error)) {
|
|
34
|
+
await context.fs.unlink(backupPath).catch(() => undefined);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
attempt += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isAlreadyExists(error) {
|
|
42
|
+
return hasOwnErrorCode(error, "EEXIST");
|
|
43
|
+
}
|
|
44
|
+
async function assertRegularWriteTarget(context, targetPath) {
|
|
45
|
+
// Symlinks inside the managed home directory are untrusted: an attacker could
|
|
46
|
+
// plant one to redirect a credential/config write outside it. Symlinks at or
|
|
47
|
+
// above home are legitimate system links (e.g. /tmp -> /private/tmp on macOS,
|
|
48
|
+
// /var -> /private/var) and must not block writes, so bound the walk at home.
|
|
49
|
+
const boundary = path.dirname(path.resolve(context.homeDir));
|
|
50
|
+
let currentPath = path.resolve(targetPath);
|
|
51
|
+
while (currentPath !== boundary) {
|
|
52
|
+
try {
|
|
53
|
+
if ((await context.fs.lstat(currentPath)).isSymbolicLink()) {
|
|
54
|
+
throw new Error(`Refusing mutation write through symbolic link: ${currentPath}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (!isNotFound(error)) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const parentPath = path.dirname(currentPath);
|
|
63
|
+
if (parentPath === currentPath) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
currentPath = parentPath;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function writeAtomically(context, targetPath, content) {
|
|
70
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
71
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
72
|
+
const tempPath = `${targetPath}.mutation-tmp-${process.pid}-${randomUUID()}`;
|
|
73
|
+
let tempCreated = false;
|
|
74
|
+
try {
|
|
75
|
+
await assertRegularWriteTarget(context, tempPath);
|
|
76
|
+
await context.fs.writeFile(tempPath, content, { encoding: "utf8", flag: "wx" });
|
|
77
|
+
tempCreated = true;
|
|
78
|
+
await context.fs.rename(tempPath, targetPath);
|
|
79
|
+
tempCreated = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const alreadyExists = isAlreadyExists(error);
|
|
84
|
+
if (tempCreated || !alreadyExists) {
|
|
85
|
+
try {
|
|
86
|
+
await context.fs.unlink(tempPath);
|
|
87
|
+
}
|
|
88
|
+
catch (cleanupError) {
|
|
89
|
+
if (!isNotFound(cleanupError)) {
|
|
90
|
+
void cleanupError;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (alreadyExists) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Unable to create temporary mutation file for ${targetPath}.`);
|
|
101
|
+
}
|
|
102
|
+
function describeMutation(kind, targetPath) {
|
|
103
|
+
const displayPath = targetPath ?? "target";
|
|
104
|
+
switch (kind) {
|
|
105
|
+
case "ensureDirectory":
|
|
106
|
+
return `Create ${displayPath}`;
|
|
107
|
+
case "removeDirectory":
|
|
108
|
+
return `Remove directory ${displayPath}`;
|
|
109
|
+
case "backup":
|
|
110
|
+
return `Backup ${displayPath}`;
|
|
111
|
+
case "restoreBackup":
|
|
112
|
+
return `Restore ${displayPath}`;
|
|
113
|
+
case "templateWrite":
|
|
114
|
+
return `Write ${displayPath}`;
|
|
115
|
+
case "chmod":
|
|
116
|
+
return `Set permissions on ${displayPath}`;
|
|
117
|
+
case "removeFile":
|
|
118
|
+
return `Remove ${displayPath}`;
|
|
119
|
+
case "configMerge":
|
|
120
|
+
case "configPrune":
|
|
121
|
+
case "configTransform":
|
|
122
|
+
case "templateMergeToml":
|
|
123
|
+
case "templateMergeJson":
|
|
124
|
+
return `Update ${displayPath}`;
|
|
125
|
+
default:
|
|
126
|
+
return "Operation";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function mutationTargetPath(mutation, options) {
|
|
130
|
+
switch (mutation.kind) {
|
|
131
|
+
case "ensureDirectory":
|
|
132
|
+
case "removeDirectory":
|
|
133
|
+
return resolveValue(mutation.path, options);
|
|
134
|
+
case "removeFile":
|
|
135
|
+
case "chmod":
|
|
136
|
+
case "backup":
|
|
137
|
+
case "restoreBackup":
|
|
138
|
+
case "configMerge":
|
|
139
|
+
case "configPrune":
|
|
140
|
+
case "configTransform":
|
|
141
|
+
case "templateWrite":
|
|
142
|
+
case "templateMergeToml":
|
|
143
|
+
case "templateMergeJson":
|
|
144
|
+
return resolveValue(mutation.target, options);
|
|
145
|
+
default:
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function resolveMutationDetails(mutation, context, options) {
|
|
150
|
+
try {
|
|
151
|
+
const rawTarget = mutationTargetPath(mutation, options);
|
|
152
|
+
if (rawTarget === undefined) {
|
|
153
|
+
return {
|
|
154
|
+
kind: mutation.kind,
|
|
155
|
+
label: mutation.label ?? mutation.kind
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const targetPath = resolvePath(rawTarget, context.homeDir, context.pathMapper);
|
|
160
|
+
return {
|
|
161
|
+
kind: mutation.kind,
|
|
162
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
163
|
+
targetPath
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return {
|
|
168
|
+
kind: mutation.kind,
|
|
169
|
+
label: mutation.label ?? describeMutation(mutation.kind, rawTarget),
|
|
170
|
+
targetPath: undefined
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return {
|
|
176
|
+
kind: mutation.kind,
|
|
177
|
+
label: mutation.label ?? mutation.kind
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function pruneKeysByPrefix(table, prefix) {
|
|
182
|
+
const result = {};
|
|
183
|
+
for (const [key, value] of Object.entries(table)) {
|
|
184
|
+
if (!key.startsWith(prefix)) {
|
|
185
|
+
setConfigEntry(result, key, value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
function isConfigObject(value) {
|
|
191
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
function mergeWithPruneByPrefix(base, patch, pruneByPrefix) {
|
|
194
|
+
const result = cloneConfigObject(base);
|
|
195
|
+
const prefixMap = pruneByPrefix ?? {};
|
|
196
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
197
|
+
if (value === undefined) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const current = result[key];
|
|
201
|
+
const prefix = prefixMap[key];
|
|
202
|
+
if (isConfigObject(current) && isConfigObject(value)) {
|
|
203
|
+
if (prefix) {
|
|
204
|
+
const pruned = pruneKeysByPrefix(current, prefix);
|
|
205
|
+
setConfigEntry(result, key, mergePrunedConfigObject(pruned, value));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
setConfigEntry(result, key, mergeWithPruneByPrefix(current, value, prefixMap));
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
setConfigEntry(result, key, value);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
function mergePrunedConfigObject(base, patch) {
|
|
217
|
+
const result = cloneConfigObject(base);
|
|
218
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
219
|
+
if (value === undefined) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
setConfigEntry(result, key, value);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
function serializeConfigUpdate(format, rawContent, current, next) {
|
|
227
|
+
if (rawContent !== null && format.serializeUpdate) {
|
|
228
|
+
return format.serializeUpdate(rawContent, current, next);
|
|
229
|
+
}
|
|
230
|
+
return format.serialize(next);
|
|
231
|
+
}
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Apply Mutation
|
|
234
|
+
// ============================================================================
|
|
235
|
+
export async function applyMutation(mutation, context, options) {
|
|
236
|
+
switch (mutation.kind) {
|
|
237
|
+
case "ensureDirectory":
|
|
238
|
+
return applyEnsureDirectory(mutation, context, options);
|
|
239
|
+
case "removeDirectory":
|
|
240
|
+
return applyRemoveDirectory(mutation, context, options);
|
|
241
|
+
case "removeFile":
|
|
242
|
+
return applyRemoveFile(mutation, context, options);
|
|
243
|
+
case "chmod":
|
|
244
|
+
return applyChmod(mutation, context, options);
|
|
245
|
+
case "backup":
|
|
246
|
+
return applyBackup(mutation, context, options);
|
|
247
|
+
case "restoreBackup":
|
|
248
|
+
return applyRestoreBackup(mutation, context, options);
|
|
249
|
+
case "configMerge":
|
|
250
|
+
return applyConfigMerge(mutation, context, options);
|
|
251
|
+
case "configPrune":
|
|
252
|
+
return applyConfigPrune(mutation, context, options);
|
|
253
|
+
case "configTransform":
|
|
254
|
+
return applyConfigTransform(mutation, context, options);
|
|
255
|
+
case "templateWrite":
|
|
256
|
+
return applyTemplateWrite(mutation, context, options);
|
|
257
|
+
case "templateMergeToml":
|
|
258
|
+
return applyTemplateMerge(mutation, context, options, "toml");
|
|
259
|
+
case "templateMergeJson":
|
|
260
|
+
return applyTemplateMerge(mutation, context, options, "json");
|
|
261
|
+
default: {
|
|
262
|
+
const never = mutation;
|
|
263
|
+
throw new Error(`Unknown mutation kind: ${never.kind}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// File Mutation Handlers
|
|
269
|
+
// ============================================================================
|
|
270
|
+
async function applyEnsureDirectory(mutation, context, options) {
|
|
271
|
+
const rawPath = resolveValue(mutation.path, options);
|
|
272
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
273
|
+
const details = {
|
|
274
|
+
kind: mutation.kind,
|
|
275
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
276
|
+
targetPath
|
|
277
|
+
};
|
|
278
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
279
|
+
const existed = await pathExists(context.fs, targetPath);
|
|
280
|
+
if (!context.dryRun) {
|
|
281
|
+
await context.fs.mkdir(targetPath, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
outcome: {
|
|
285
|
+
changed: !existed,
|
|
286
|
+
effect: "mkdir",
|
|
287
|
+
detail: existed ? "noop" : "create"
|
|
288
|
+
},
|
|
289
|
+
details
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async function applyRemoveDirectory(mutation, context, options) {
|
|
293
|
+
const rawPath = resolveValue(mutation.path, options);
|
|
294
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
295
|
+
const details = {
|
|
296
|
+
kind: mutation.kind,
|
|
297
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
298
|
+
targetPath
|
|
299
|
+
};
|
|
300
|
+
const existed = await pathExists(context.fs, targetPath);
|
|
301
|
+
if (!existed) {
|
|
302
|
+
return {
|
|
303
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
304
|
+
details
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (typeof context.fs.rm !== "function") {
|
|
308
|
+
return {
|
|
309
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
310
|
+
details
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (mutation.force) {
|
|
314
|
+
if (!context.dryRun) {
|
|
315
|
+
await context.fs.rm(targetPath, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
319
|
+
details
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const entries = await context.fs.readdir(targetPath);
|
|
323
|
+
if (entries.length > 0) {
|
|
324
|
+
return {
|
|
325
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
326
|
+
details
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (!context.dryRun) {
|
|
330
|
+
await context.fs.rm(targetPath, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
334
|
+
details
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
async function applyRemoveFile(mutation, context, options) {
|
|
338
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
339
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
340
|
+
const details = {
|
|
341
|
+
kind: mutation.kind,
|
|
342
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
343
|
+
targetPath
|
|
344
|
+
};
|
|
345
|
+
try {
|
|
346
|
+
const content = await context.fs.readFile(targetPath, "utf8");
|
|
347
|
+
const trimmed = content.trim();
|
|
348
|
+
// Check whenContentMatches guard
|
|
349
|
+
if (mutation.whenContentMatches) {
|
|
350
|
+
mutation.whenContentMatches.lastIndex = 0;
|
|
351
|
+
}
|
|
352
|
+
if (mutation.whenContentMatches && !mutation.whenContentMatches.test(trimmed)) {
|
|
353
|
+
return {
|
|
354
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
355
|
+
details
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// Check whenEmpty guard
|
|
359
|
+
if (mutation.whenEmpty && trimmed.length > 0) {
|
|
360
|
+
return {
|
|
361
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
362
|
+
details
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!context.dryRun) {
|
|
366
|
+
await context.fs.unlink(targetPath);
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
370
|
+
details
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (isNotFound(error)) {
|
|
375
|
+
return {
|
|
376
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
377
|
+
details
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function applyChmod(mutation, context, options) {
|
|
384
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
385
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
386
|
+
const details = {
|
|
387
|
+
kind: mutation.kind,
|
|
388
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
389
|
+
targetPath
|
|
390
|
+
};
|
|
391
|
+
if (typeof context.fs.chmod !== "function") {
|
|
392
|
+
return {
|
|
393
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
394
|
+
details
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
399
|
+
const stat = await context.fs.stat(targetPath);
|
|
400
|
+
const currentMode = typeof stat.mode === "number" ? stat.mode & 0o777 : null;
|
|
401
|
+
if (currentMode === mutation.mode) {
|
|
402
|
+
return {
|
|
403
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
404
|
+
details
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (!context.dryRun) {
|
|
408
|
+
await context.fs.chmod(targetPath, mutation.mode);
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
outcome: { changed: true, effect: "chmod", detail: "update" },
|
|
412
|
+
details
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
if (isNotFound(error)) {
|
|
417
|
+
return {
|
|
418
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
419
|
+
details
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function applyBackup(mutation, context, options) {
|
|
426
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
427
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
428
|
+
const details = {
|
|
429
|
+
kind: mutation.kind,
|
|
430
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
431
|
+
targetPath
|
|
432
|
+
};
|
|
433
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
434
|
+
if (mutation.once && (await findLatestGeneratedBackup(context.fs, targetPath)) !== null) {
|
|
435
|
+
return {
|
|
436
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
437
|
+
details
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const content = await readFileIfExists(context.fs, targetPath);
|
|
441
|
+
if (content === null && !mutation.once) {
|
|
442
|
+
return {
|
|
443
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
444
|
+
details
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (!context.dryRun) {
|
|
448
|
+
const baseBackupPath = `${targetPath}.backup-${createTimestamp()}${content === null ? ".missing" : ""}`;
|
|
449
|
+
let attempt = 0;
|
|
450
|
+
while (true) {
|
|
451
|
+
const backupPath = attempt === 0 ? baseBackupPath : `${baseBackupPath}-${attempt}`;
|
|
452
|
+
try {
|
|
453
|
+
await assertRegularWriteTarget(context, backupPath);
|
|
454
|
+
await context.fs.writeFile(backupPath, content ?? "", { encoding: "utf8", flag: "wx" });
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
if (!isAlreadyExists(error)) {
|
|
459
|
+
await context.fs.unlink(backupPath).catch(() => undefined);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
attempt += 1;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
outcome: { changed: true, effect: "copy", detail: "backup" },
|
|
468
|
+
details
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async function applyRestoreBackup(mutation, context, options) {
|
|
472
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
473
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
474
|
+
const details = {
|
|
475
|
+
kind: mutation.kind,
|
|
476
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
477
|
+
targetPath
|
|
478
|
+
};
|
|
479
|
+
const backup = await findLatestGeneratedBackup(context.fs, targetPath);
|
|
480
|
+
if (backup === null) {
|
|
481
|
+
return { outcome: { changed: false, effect: "none", detail: "noop" }, details };
|
|
482
|
+
}
|
|
483
|
+
if (!context.dryRun) {
|
|
484
|
+
await assertRegularWriteTarget(context, backup.path);
|
|
485
|
+
if (backup.originallyMissing) {
|
|
486
|
+
try {
|
|
487
|
+
await context.fs.unlink(targetPath);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
if (!isNotFound(error)) {
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const content = await context.fs.readFile(backup.path, "utf8");
|
|
497
|
+
await writeAtomically(context, targetPath, content);
|
|
498
|
+
}
|
|
499
|
+
await context.fs.unlink(backup.path);
|
|
500
|
+
}
|
|
501
|
+
return { outcome: { changed: true, effect: "copy", detail: "restore" }, details };
|
|
502
|
+
}
|
|
503
|
+
async function findLatestGeneratedBackup(fs, targetPath) {
|
|
504
|
+
const separatorIndex = targetPath.lastIndexOf("/");
|
|
505
|
+
const directoryPath = separatorIndex <= 0 ? "/" : targetPath.slice(0, separatorIndex);
|
|
506
|
+
const targetName = targetPath.slice(separatorIndex + 1);
|
|
507
|
+
let entries;
|
|
508
|
+
try {
|
|
509
|
+
entries = await fs.readdir(directoryPath);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (isNotFound(error)) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
const backupName = entries
|
|
518
|
+
.filter((entry) => isGeneratedBackupName(entry, targetName))
|
|
519
|
+
.sort()
|
|
520
|
+
.at(-1);
|
|
521
|
+
return backupName === undefined
|
|
522
|
+
? null
|
|
523
|
+
: {
|
|
524
|
+
path: `${directoryPath}/${backupName}`,
|
|
525
|
+
originallyMissing: backupName.endsWith(".missing")
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function isGeneratedBackupName(entry, targetName) {
|
|
529
|
+
const prefix = `${targetName}.backup-`;
|
|
530
|
+
if (!entry.startsWith(prefix)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
const suffix = entry.slice(prefix.length);
|
|
534
|
+
const timestamp = suffix.slice(0, 24);
|
|
535
|
+
if (timestamp.length !== 24 || timestamp[4] !== "-" || timestamp[7] !== "-" || timestamp[10] !== "T" || timestamp[13] !== "-" || timestamp[16] !== "-" || timestamp[19] !== "-" || timestamp[23] !== "Z") {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
const parsedTimestamp = `${timestamp.slice(0, 13)}:${timestamp.slice(14, 16)}:${timestamp.slice(17, 19)}.${timestamp.slice(20)}`;
|
|
539
|
+
if (Number.isNaN(Date.parse(parsedTimestamp))) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const collisionSuffix = suffix.slice(24);
|
|
543
|
+
if (collisionSuffix.length === 0) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (collisionSuffix === ".missing") {
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return collisionSuffix[0] === "-" && collisionSuffix.slice(1).length > 0 && [...collisionSuffix.slice(1)].every((character) => character >= "0" && character <= "9");
|
|
550
|
+
}
|
|
551
|
+
// ============================================================================
|
|
552
|
+
// Config Mutation Handlers
|
|
553
|
+
// ============================================================================
|
|
554
|
+
async function applyConfigMerge(mutation, context, options) {
|
|
555
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
556
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
557
|
+
const details = {
|
|
558
|
+
kind: mutation.kind,
|
|
559
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
560
|
+
targetPath
|
|
561
|
+
};
|
|
562
|
+
const formatName = mutation.format ?? detectFormat(rawPath);
|
|
563
|
+
if (!formatName) {
|
|
564
|
+
throw new Error(`Cannot detect config format for "${rawPath}". Provide explicit format option.`);
|
|
565
|
+
}
|
|
566
|
+
const format = getConfigFormat(formatName);
|
|
567
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
568
|
+
let preserveContent = rawContent;
|
|
569
|
+
let current;
|
|
570
|
+
try {
|
|
571
|
+
current = rawContent === null ? {} : format.parse(rawContent);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// Invalid file - backup and start fresh
|
|
575
|
+
if (rawContent !== null && !context.dryRun) {
|
|
576
|
+
await backupInvalidDocument(context, targetPath, rawContent);
|
|
577
|
+
}
|
|
578
|
+
current = {};
|
|
579
|
+
preserveContent = null;
|
|
580
|
+
}
|
|
581
|
+
const value = resolveValue(mutation.value, options);
|
|
582
|
+
if (!isConfigObject(value)) {
|
|
583
|
+
throw new Error(`configMerge value must be an object for "${rawPath}".`);
|
|
584
|
+
}
|
|
585
|
+
// Keep prefix pruning on the same proto-safe object-write path as normal merges.
|
|
586
|
+
let merged;
|
|
587
|
+
if (mutation.pruneByPrefix) {
|
|
588
|
+
merged = mergeWithPruneByPrefix(current, value, mutation.pruneByPrefix);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
merged = format.merge(current, value);
|
|
592
|
+
}
|
|
593
|
+
const serialized = serializeConfigUpdate(format, preserveContent, current, merged);
|
|
594
|
+
const changed = serialized !== rawContent;
|
|
595
|
+
if (changed && !context.dryRun) {
|
|
596
|
+
await writeAtomically(context, targetPath, serialized);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
outcome: {
|
|
600
|
+
changed,
|
|
601
|
+
effect: changed ? "write" : "none",
|
|
602
|
+
detail: changed ? (rawContent === null ? "create" : "update") : "noop"
|
|
603
|
+
},
|
|
604
|
+
details
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
async function applyConfigPrune(mutation, context, options) {
|
|
608
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
609
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
610
|
+
const details = {
|
|
611
|
+
kind: mutation.kind,
|
|
612
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
613
|
+
targetPath
|
|
614
|
+
};
|
|
615
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
616
|
+
if (rawContent === null) {
|
|
617
|
+
return {
|
|
618
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
619
|
+
details
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const formatName = mutation.format ?? detectFormat(rawPath);
|
|
623
|
+
if (!formatName) {
|
|
624
|
+
throw new Error(`Cannot detect config format for "${rawPath}". Provide explicit format option.`);
|
|
625
|
+
}
|
|
626
|
+
const format = getConfigFormat(formatName);
|
|
627
|
+
let current;
|
|
628
|
+
try {
|
|
629
|
+
current = format.parse(rawContent);
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// Invalid file - can't prune, leave as-is
|
|
633
|
+
return {
|
|
634
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
635
|
+
details
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// Check onlyIf guard
|
|
639
|
+
if (mutation.onlyIf && !mutation.onlyIf(current, options)) {
|
|
640
|
+
return {
|
|
641
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
642
|
+
details
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const shape = resolveValue(mutation.shape, options);
|
|
646
|
+
const { changed, result } = format.prune(current, shape);
|
|
647
|
+
if (!changed) {
|
|
648
|
+
return {
|
|
649
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
650
|
+
details
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
// Delete file if empty
|
|
654
|
+
if (Object.keys(result).length === 0) {
|
|
655
|
+
if (!context.dryRun) {
|
|
656
|
+
await context.fs.unlink(targetPath);
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
660
|
+
details
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const serialized = serializeConfigUpdate(format, rawContent, current, result);
|
|
664
|
+
if (!context.dryRun) {
|
|
665
|
+
await writeAtomically(context, targetPath, serialized);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
outcome: { changed: true, effect: "write", detail: "update" },
|
|
669
|
+
details
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async function applyConfigTransform(mutation, context, options) {
|
|
673
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
674
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
675
|
+
const details = {
|
|
676
|
+
kind: mutation.kind,
|
|
677
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
678
|
+
targetPath
|
|
679
|
+
};
|
|
680
|
+
const formatName = mutation.format ?? detectFormat(rawPath);
|
|
681
|
+
if (!formatName) {
|
|
682
|
+
throw new Error(`Cannot detect config format for "${rawPath}". Provide explicit format option.`);
|
|
683
|
+
}
|
|
684
|
+
const format = getConfigFormat(formatName);
|
|
685
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
686
|
+
let preserveContent = rawContent;
|
|
687
|
+
let current;
|
|
688
|
+
try {
|
|
689
|
+
current = rawContent === null ? {} : format.parse(rawContent);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
if (rawContent !== null && !context.dryRun) {
|
|
693
|
+
await backupInvalidDocument(context, targetPath, rawContent);
|
|
694
|
+
}
|
|
695
|
+
current = {};
|
|
696
|
+
preserveContent = null;
|
|
697
|
+
}
|
|
698
|
+
const { content: transformed, changed } = mutation.transform(current, options);
|
|
699
|
+
if (!changed) {
|
|
700
|
+
return {
|
|
701
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
702
|
+
details
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
// Delete file if null
|
|
706
|
+
if (transformed === null) {
|
|
707
|
+
if (rawContent === null) {
|
|
708
|
+
return {
|
|
709
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
710
|
+
details
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (!context.dryRun) {
|
|
714
|
+
await context.fs.unlink(targetPath);
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
718
|
+
details
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const serialized = serializeConfigUpdate(format, preserveContent, current, transformed);
|
|
722
|
+
const serializedChanged = serialized !== rawContent;
|
|
723
|
+
if (!serializedChanged) {
|
|
724
|
+
return {
|
|
725
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
726
|
+
details
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (!context.dryRun) {
|
|
730
|
+
await writeAtomically(context, targetPath, serialized);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
outcome: {
|
|
734
|
+
changed: true,
|
|
735
|
+
effect: "write",
|
|
736
|
+
detail: rawContent === null ? "create" : "update"
|
|
737
|
+
},
|
|
738
|
+
details
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
// ============================================================================
|
|
742
|
+
// Template Mutation Handlers
|
|
743
|
+
// ============================================================================
|
|
744
|
+
async function applyTemplateWrite(mutation, context, options) {
|
|
745
|
+
if (!context.templates) {
|
|
746
|
+
throw new Error("Template mutations require a templates loader. " +
|
|
747
|
+
"Provide templates function to runMutations context.");
|
|
748
|
+
}
|
|
749
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
750
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
751
|
+
const details = {
|
|
752
|
+
kind: mutation.kind,
|
|
753
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
754
|
+
targetPath
|
|
755
|
+
};
|
|
756
|
+
const template = await context.templates(mutation.templateId);
|
|
757
|
+
const templateContext = mutation.context
|
|
758
|
+
? resolveValue(mutation.context, options)
|
|
759
|
+
: {};
|
|
760
|
+
const rendered = renderTemplate(template, templateContext);
|
|
761
|
+
const current = await readFileIfExists(context.fs, targetPath);
|
|
762
|
+
const changed = current !== rendered;
|
|
763
|
+
if (changed && !context.dryRun) {
|
|
764
|
+
await writeAtomically(context, targetPath, rendered);
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
outcome: {
|
|
768
|
+
changed,
|
|
769
|
+
effect: changed ? "write" : "none",
|
|
770
|
+
detail: changed ? (current === null ? "create" : "update") : "noop"
|
|
771
|
+
},
|
|
772
|
+
details
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
async function applyTemplateMerge(mutation, context, options, formatName) {
|
|
776
|
+
if (!context.templates) {
|
|
777
|
+
throw new Error("Template mutations require a templates loader. " +
|
|
778
|
+
"Provide templates function to runMutations context.");
|
|
779
|
+
}
|
|
780
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
781
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
782
|
+
const details = {
|
|
783
|
+
kind: mutation.kind,
|
|
784
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
785
|
+
targetPath
|
|
786
|
+
};
|
|
787
|
+
const format = getConfigFormat(formatName);
|
|
788
|
+
// Load and render template
|
|
789
|
+
const template = await context.templates(mutation.templateId);
|
|
790
|
+
const templateContext = mutation.context
|
|
791
|
+
? resolveValue(mutation.context, options)
|
|
792
|
+
: {};
|
|
793
|
+
const rendered = renderTemplate(template, templateContext);
|
|
794
|
+
// Parse rendered template
|
|
795
|
+
let templateDoc;
|
|
796
|
+
try {
|
|
797
|
+
templateDoc = format.parse(rendered);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
throw new Error(`Failed to parse rendered template "${mutation.templateId}" as ${formatName.toUpperCase()}: ${error}`, { cause: error });
|
|
801
|
+
}
|
|
802
|
+
// Read and parse existing file
|
|
803
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
804
|
+
let current;
|
|
805
|
+
try {
|
|
806
|
+
current = rawContent === null ? {} : format.parse(rawContent);
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
if (rawContent !== null && !context.dryRun) {
|
|
810
|
+
await backupInvalidDocument(context, targetPath, rawContent);
|
|
811
|
+
}
|
|
812
|
+
current = {};
|
|
813
|
+
}
|
|
814
|
+
// Merge
|
|
815
|
+
const merged = format.merge(current, templateDoc);
|
|
816
|
+
const serialized = format.serialize(merged);
|
|
817
|
+
const changed = serialized !== rawContent;
|
|
818
|
+
if (changed && !context.dryRun) {
|
|
819
|
+
await writeAtomically(context, targetPath, serialized);
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
outcome: {
|
|
823
|
+
changed,
|
|
824
|
+
effect: changed ? "write" : "none",
|
|
825
|
+
detail: changed ? (rawContent === null ? "create" : "update") : "noop"
|
|
826
|
+
},
|
|
827
|
+
details
|
|
828
|
+
};
|
|
829
|
+
}
|