@namzu/cli 29.0.0 → 29.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -0
- package/README.md +20 -0
- package/dist/browser/commands.d.ts +29 -0
- package/dist/browser/commands.d.ts.map +1 -0
- package/dist/browser/commands.js +361 -0
- package/dist/browser/commands.js.map +1 -0
- package/dist/browser/control.d.ts +79 -0
- package/dist/browser/control.d.ts.map +1 -0
- package/dist/browser/control.js +140 -0
- package/dist/browser/control.js.map +1 -0
- package/dist/browser/describe.d.ts +12 -0
- package/dist/browser/describe.d.ts.map +1 -0
- package/dist/browser/describe.js +27 -0
- package/dist/browser/describe.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +1 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/browser.d.ts +7 -0
- package/dist/commands/browser.d.ts.map +1 -0
- package/dist/commands/browser.js +16 -0
- package/dist/commands/browser.js.map +1 -0
- package/dist/commands/drain.d.ts +2 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +6 -3
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/exec-flags.d.ts +2 -1
- package/dist/commands/exec-flags.d.ts.map +1 -1
- package/dist/commands/exec-flags.js +6 -3
- package/dist/commands/exec-flags.js.map +1 -1
- package/dist/commands/exec-json.d.ts.map +1 -1
- package/dist/commands/exec-json.js +2 -1
- package/dist/commands/exec-json.js.map +1 -1
- package/dist/commands/exec.d.ts.map +1 -1
- package/dist/commands/exec.js +2 -1
- package/dist/commands/exec.js.map +1 -1
- package/dist/commands/host-queries.d.ts.map +1 -1
- package/dist/commands/host-queries.js +10 -3
- package/dist/commands/host-queries.js.map +1 -1
- package/dist/commands/schedule.d.ts +9 -0
- package/dist/commands/schedule.d.ts.map +1 -0
- package/dist/commands/schedule.js +126 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/serve.d.ts +3 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +5 -3
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/skills.d.ts +10 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +23 -7
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/upgrade.d.ts +5 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +11 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/config/load.d.ts +39 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +232 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +78 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/user-config.d.ts +23 -0
- package/dist/config/user-config.d.ts.map +1 -0
- package/dist/config/user-config.js +48 -0
- package/dist/config/user-config.js.map +1 -0
- package/dist/context/capabilities.d.ts +1 -1
- package/dist/context/capabilities.d.ts.map +1 -1
- package/dist/context/capabilities.js +1 -0
- package/dist/context/capabilities.js.map +1 -1
- package/dist/doctor/checks/browser.d.ts +15 -0
- package/dist/doctor/checks/browser.d.ts.map +1 -0
- package/dist/doctor/checks/browser.js +63 -0
- package/dist/doctor/checks/browser.js.map +1 -0
- package/dist/doctor/checks/index.d.ts +4 -1
- package/dist/doctor/checks/index.d.ts.map +1 -1
- package/dist/doctor/checks/index.js +8 -1
- package/dist/doctor/checks/index.js.map +1 -1
- package/dist/doctor/checks/scheduler.d.ts +10 -0
- package/dist/doctor/checks/scheduler.d.ts.map +1 -0
- package/dist/doctor/checks/scheduler.js +74 -0
- package/dist/doctor/checks/scheduler.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/notifications/desktop/freedesktop.d.ts +10 -0
- package/dist/integrations/notifications/desktop/freedesktop.d.ts.map +1 -0
- package/dist/integrations/notifications/desktop/freedesktop.js +33 -0
- package/dist/integrations/notifications/desktop/freedesktop.js.map +1 -0
- package/dist/integrations/notifications/desktop/macos.d.ts +6 -0
- package/dist/integrations/notifications/desktop/macos.d.ts.map +1 -0
- package/dist/integrations/notifications/desktop/macos.js +17 -0
- package/dist/integrations/notifications/desktop/macos.js.map +1 -0
- package/dist/integrations/notifications/desktop/sanitize.d.ts +16 -0
- package/dist/integrations/notifications/desktop/sanitize.d.ts.map +1 -0
- package/dist/integrations/notifications/desktop/sanitize.js +49 -0
- package/dist/integrations/notifications/desktop/sanitize.js.map +1 -0
- package/dist/integrations/notifications/desktop/throttle.d.ts +10 -0
- package/dist/integrations/notifications/desktop/throttle.d.ts.map +1 -0
- package/dist/integrations/notifications/desktop/throttle.js +39 -0
- package/dist/integrations/notifications/desktop/throttle.js.map +1 -0
- package/dist/integrations/notifications/desktop/windows-toast.d.ts +23 -0
- package/dist/integrations/notifications/desktop/windows-toast.d.ts.map +1 -0
- package/dist/integrations/notifications/desktop/windows-toast.js +56 -0
- package/dist/integrations/notifications/desktop/windows-toast.js.map +1 -0
- package/dist/integrations/notifications/desktop.d.ts +82 -0
- package/dist/integrations/notifications/desktop.d.ts.map +1 -0
- package/dist/integrations/notifications/desktop.js +198 -0
- package/dist/integrations/notifications/desktop.js.map +1 -0
- package/dist/integrations/resident/session-step.d.ts.map +1 -1
- package/dist/integrations/resident/session-step.js +1 -0
- package/dist/integrations/resident/session-step.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts +6 -0
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +14 -5
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/permissions/browser-sites.d.ts +95 -0
- package/dist/permissions/browser-sites.d.ts.map +1 -0
- package/dist/permissions/browser-sites.js +270 -0
- package/dist/permissions/browser-sites.js.map +1 -0
- package/dist/permissions/checks.d.ts.map +1 -1
- package/dist/permissions/checks.js +7 -1
- package/dist/permissions/checks.js.map +1 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +4 -8
- package/dist/permissions/rules.js.map +1 -1
- package/dist/schedule/build.d.ts +79 -0
- package/dist/schedule/build.d.ts.map +1 -0
- package/dist/schedule/build.js +246 -0
- package/dist/schedule/build.js.map +1 -0
- package/dist/schedule/changes.d.ts +22 -0
- package/dist/schedule/changes.d.ts.map +1 -0
- package/dist/schedule/changes.js +55 -0
- package/dist/schedule/changes.js.map +1 -0
- package/dist/schedule/commands/add.d.ts +24 -0
- package/dist/schedule/commands/add.d.ts.map +1 -0
- package/dist/schedule/commands/add.js +459 -0
- package/dist/schedule/commands/add.js.map +1 -0
- package/dist/schedule/commands/args.d.ts +22 -0
- package/dist/schedule/commands/args.d.ts.map +1 -0
- package/dist/schedule/commands/args.js +80 -0
- package/dist/schedule/commands/args.js.map +1 -0
- package/dist/schedule/commands/confirm-prompt.d.ts +6 -0
- package/dist/schedule/commands/confirm-prompt.d.ts.map +1 -0
- package/dist/schedule/commands/confirm-prompt.js +21 -0
- package/dist/schedule/commands/confirm-prompt.js.map +1 -0
- package/dist/schedule/commands/lifecycle.d.ts +39 -0
- package/dist/schedule/commands/lifecycle.d.ts.map +1 -0
- package/dist/schedule/commands/lifecycle.js +585 -0
- package/dist/schedule/commands/lifecycle.js.map +1 -0
- package/dist/schedule/commands/list.d.ts +33 -0
- package/dist/schedule/commands/list.d.ts.map +1 -0
- package/dist/schedule/commands/list.js +202 -0
- package/dist/schedule/commands/list.js.map +1 -0
- package/dist/schedule/commands/logs.d.ts +7 -0
- package/dist/schedule/commands/logs.d.ts.map +1 -0
- package/dist/schedule/commands/logs.js +106 -0
- package/dist/schedule/commands/logs.js.map +1 -0
- package/dist/schedule/commands/service.d.ts +16 -0
- package/dist/schedule/commands/service.d.ts.map +1 -0
- package/dist/schedule/commands/service.js +458 -0
- package/dist/schedule/commands/service.js.map +1 -0
- package/dist/schedule/daemon/daemon.d.ts +156 -0
- package/dist/schedule/daemon/daemon.d.ts.map +1 -0
- package/dist/schedule/daemon/daemon.js +1248 -0
- package/dist/schedule/daemon/daemon.js.map +1 -0
- package/dist/schedule/daemon/endpoint.d.ts +38 -0
- package/dist/schedule/daemon/endpoint.d.ts.map +1 -0
- package/dist/schedule/daemon/endpoint.js +158 -0
- package/dist/schedule/daemon/endpoint.js.map +1 -0
- package/dist/schedule/daemon/exit.d.ts +8 -0
- package/dist/schedule/daemon/exit.d.ts.map +1 -0
- package/dist/schedule/daemon/exit.js +8 -0
- package/dist/schedule/daemon/exit.js.map +1 -0
- package/dist/schedule/daemon/log.d.ts +12 -0
- package/dist/schedule/daemon/log.d.ts.map +1 -0
- package/dist/schedule/daemon/log.js +53 -0
- package/dist/schedule/daemon/log.js.map +1 -0
- package/dist/schedule/daemon/notify.d.ts +31 -0
- package/dist/schedule/daemon/notify.d.ts.map +1 -0
- package/dist/schedule/daemon/notify.js +89 -0
- package/dist/schedule/daemon/notify.js.map +1 -0
- package/dist/schedule/daemon/retention.d.ts +16 -0
- package/dist/schedule/daemon/retention.d.ts.map +1 -0
- package/dist/schedule/daemon/retention.js +46 -0
- package/dist/schedule/daemon/retention.js.map +1 -0
- package/dist/schedule/daemon/sessions.d.ts +23 -0
- package/dist/schedule/daemon/sessions.d.ts.map +1 -0
- package/dist/schedule/daemon/sessions.js +52 -0
- package/dist/schedule/daemon/sessions.js.map +1 -0
- package/dist/schedule/env.d.ts +28 -0
- package/dist/schedule/env.d.ts.map +1 -0
- package/dist/schedule/env.js +99 -0
- package/dist/schedule/env.js.map +1 -0
- package/dist/schedule/fire/browser-preflight.d.ts +41 -0
- package/dist/schedule/fire/browser-preflight.d.ts.map +1 -0
- package/dist/schedule/fire/browser-preflight.js +73 -0
- package/dist/schedule/fire/browser-preflight.js.map +1 -0
- package/dist/schedule/fire/fire.d.ts +78 -0
- package/dist/schedule/fire/fire.d.ts.map +1 -0
- package/dist/schedule/fire/fire.js +482 -0
- package/dist/schedule/fire/fire.js.map +1 -0
- package/dist/schedule/fire/result.d.ts +13 -0
- package/dist/schedule/fire/result.d.ts.map +1 -0
- package/dist/schedule/fire/result.js +18 -0
- package/dist/schedule/fire/result.js.map +1 -0
- package/dist/schedule/fire/unattended-note.d.ts +22 -0
- package/dist/schedule/fire/unattended-note.d.ts.map +1 -0
- package/dist/schedule/fire/unattended-note.js +46 -0
- package/dist/schedule/fire/unattended-note.js.map +1 -0
- package/dist/schedule/floor.d.ts +116 -0
- package/dist/schedule/floor.d.ts.map +1 -0
- package/dist/schedule/floor.js +1051 -0
- package/dist/schedule/floor.js.map +1 -0
- package/dist/schedule/folder.d.ts +34 -0
- package/dist/schedule/folder.d.ts.map +1 -0
- package/dist/schedule/folder.js +84 -0
- package/dist/schedule/folder.js.map +1 -0
- package/dist/schedule/paths.d.ts +49 -0
- package/dist/schedule/paths.d.ts.map +1 -0
- package/dist/schedule/paths.js +53 -0
- package/dist/schedule/paths.js.map +1 -0
- package/dist/schedule/policy.d.ts +141 -0
- package/dist/schedule/policy.d.ts.map +1 -0
- package/dist/schedule/policy.js +378 -0
- package/dist/schedule/policy.js.map +1 -0
- package/dist/schedule/resume-command.d.ts +36 -0
- package/dist/schedule/resume-command.d.ts.map +1 -0
- package/dist/schedule/resume-command.js +72 -0
- package/dist/schedule/resume-command.js.map +1 -0
- package/dist/schedule/service/detect.d.ts +22 -0
- package/dist/schedule/service/detect.d.ts.map +1 -0
- package/dist/schedule/service/detect.js +29 -0
- package/dist/schedule/service/detect.js.map +1 -0
- package/dist/schedule/service/index.d.ts +48 -0
- package/dist/schedule/service/index.d.ts.map +1 -0
- package/dist/schedule/service/index.js +352 -0
- package/dist/schedule/service/index.js.map +1 -0
- package/dist/schedule/service/launchd.d.ts +24 -0
- package/dist/schedule/service/launchd.d.ts.map +1 -0
- package/dist/schedule/service/launchd.js +84 -0
- package/dist/schedule/service/launchd.js.map +1 -0
- package/dist/schedule/service/manifest.d.ts +44 -0
- package/dist/schedule/service/manifest.d.ts.map +1 -0
- package/dist/schedule/service/manifest.js +16 -0
- package/dist/schedule/service/manifest.js.map +1 -0
- package/dist/schedule/service/names.d.ts +9 -0
- package/dist/schedule/service/names.d.ts.map +1 -0
- package/dist/schedule/service/names.js +22 -0
- package/dist/schedule/service/names.js.map +1 -0
- package/dist/schedule/service/quote.d.ts +24 -0
- package/dist/schedule/service/quote.d.ts.map +1 -0
- package/dist/schedule/service/quote.js +64 -0
- package/dist/schedule/service/quote.js.map +1 -0
- package/dist/schedule/service/runner.d.ts +17 -0
- package/dist/schedule/service/runner.d.ts.map +1 -0
- package/dist/schedule/service/runner.js +31 -0
- package/dist/schedule/service/runner.js.map +1 -0
- package/dist/schedule/service/systemd.d.ts +37 -0
- package/dist/schedule/service/systemd.d.ts.map +1 -0
- package/dist/schedule/service/systemd.js +96 -0
- package/dist/schedule/service/systemd.js.map +1 -0
- package/dist/schedule/service/windows-task.d.ts +51 -0
- package/dist/schedule/service/windows-task.d.ts.map +1 -0
- package/dist/schedule/service/windows-task.js +134 -0
- package/dist/schedule/service/windows-task.js.map +1 -0
- package/dist/schedule/service/wsl.d.ts +57 -0
- package/dist/schedule/service/wsl.d.ts.map +1 -0
- package/dist/schedule/service/wsl.js +111 -0
- package/dist/schedule/service/wsl.js.map +1 -0
- package/dist/schedule/store/atomic.d.ts +43 -0
- package/dist/schedule/store/atomic.d.ts.map +1 -0
- package/dist/schedule/store/atomic.js +135 -0
- package/dist/schedule/store/atomic.js.map +1 -0
- package/dist/schedule/store/claims.d.ts +26 -0
- package/dist/schedule/store/claims.d.ts.map +1 -0
- package/dist/schedule/store/claims.js +57 -0
- package/dist/schedule/store/claims.js.map +1 -0
- package/dist/schedule/store/digest.d.ts +18 -0
- package/dist/schedule/store/digest.d.ts.map +1 -0
- package/dist/schedule/store/digest.js +139 -0
- package/dist/schedule/store/digest.js.map +1 -0
- package/dist/schedule/store/history.d.ts +21 -0
- package/dist/schedule/store/history.d.ts.map +1 -0
- package/dist/schedule/store/history.js +114 -0
- package/dist/schedule/store/history.js.map +1 -0
- package/dist/schedule/store/jobs.d.ts +61 -0
- package/dist/schedule/store/jobs.d.ts.map +1 -0
- package/dist/schedule/store/jobs.js +210 -0
- package/dist/schedule/store/jobs.js.map +1 -0
- package/dist/schedule/store/state.d.ts +19 -0
- package/dist/schedule/store/state.d.ts.map +1 -0
- package/dist/schedule/store/state.js +38 -0
- package/dist/schedule/store/state.js.map +1 -0
- package/dist/schedule/types.d.ts +256 -0
- package/dist/schedule/types.d.ts.map +1 -0
- package/dist/schedule/types.js +9 -0
- package/dist/schedule/types.js.map +1 -0
- package/dist/skills/catalog.d.ts +96 -0
- package/dist/skills/catalog.d.ts.map +1 -0
- package/dist/skills/catalog.js +226 -0
- package/dist/skills/catalog.js.map +1 -0
- package/dist/skills/save.d.ts +148 -0
- package/dist/skills/save.d.ts.map +1 -0
- package/dist/skills/save.js +344 -0
- package/dist/skills/save.js.map +1 -0
- package/dist/skills/store.d.ts +103 -16
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +230 -29
- package/dist/skills/store.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +591 -72
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +8 -1
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +20 -4
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +24 -4
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +113 -15
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/SaveSkillOverlay.d.ts +33 -0
- package/dist/tui/SaveSkillOverlay.d.ts.map +1 -0
- package/dist/tui/SaveSkillOverlay.js +118 -0
- package/dist/tui/SaveSkillOverlay.js.map +1 -0
- package/dist/tui/agent.d.ts +185 -5
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +325 -54
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/browser-notices.d.ts +33 -0
- package/dist/tui/browser-notices.d.ts.map +1 -0
- package/dist/tui/browser-notices.js +102 -0
- package/dist/tui/browser-notices.js.map +1 -0
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +53 -4
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/schedule/host-commands.d.ts +34 -0
- package/dist/tui/schedule/host-commands.d.ts.map +1 -0
- package/dist/tui/schedule/host-commands.js +309 -0
- package/dist/tui/schedule/host-commands.js.map +1 -0
- package/dist/tui/schedule/integration.d.ts +62 -0
- package/dist/tui/schedule/integration.d.ts.map +1 -0
- package/dist/tui/schedule/integration.js +145 -0
- package/dist/tui/schedule/integration.js.map +1 -0
- package/dist/tui/schedule/loop-host.d.ts +42 -0
- package/dist/tui/schedule/loop-host.d.ts.map +1 -0
- package/dist/tui/schedule/loop-host.js +153 -0
- package/dist/tui/schedule/loop-host.js.map +1 -0
- package/dist/tui/schedule/resume.d.ts +122 -0
- package/dist/tui/schedule/resume.d.ts.map +1 -0
- package/dist/tui/schedule/resume.js +324 -0
- package/dist/tui/schedule/resume.js.map +1 -0
- package/dist/tui/schedule/startup.d.ts +11 -0
- package/dist/tui/schedule/startup.d.ts.map +1 -0
- package/dist/tui/schedule/startup.js +121 -0
- package/dist/tui/schedule/startup.js.map +1 -0
- package/dist/tui/schedule/tool-host.d.ts +44 -0
- package/dist/tui/schedule/tool-host.d.ts.map +1 -0
- package/dist/tui/schedule/tool-host.js +294 -0
- package/dist/tui/schedule/tool-host.js.map +1 -0
- package/dist/tui/skills/learning.d.ts +74 -0
- package/dist/tui/skills/learning.d.ts.map +1 -0
- package/dist/tui/skills/learning.js +131 -0
- package/dist/tui/skills/learning.js.map +1 -0
- package/dist/tui/skills/suggest-setting.d.ts +22 -0
- package/dist/tui/skills/suggest-setting.d.ts.map +1 -0
- package/dist/tui/skills/suggest-setting.js +55 -0
- package/dist/tui/skills/suggest-setting.js.map +1 -0
- package/dist/tui/skills/suggestion-ledger.d.ts +35 -0
- package/dist/tui/skills/suggestion-ledger.d.ts.map +1 -0
- package/dist/tui/skills/suggestion-ledger.js +52 -0
- package/dist/tui/skills/suggestion-ledger.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +20 -0
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +76 -2
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/turn-interruption.d.ts +20 -1
- package/dist/tui/turn-interruption.d.ts.map +1 -1
- package/dist/tui/turn-interruption.js +43 -0
- package/dist/tui/turn-interruption.js.map +1 -1
- package/dist/tui/types.d.ts +9 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/skills/README.md +15 -0
- package/skills/browser-automation/SKILL.md +84 -0
- package/skills/schedule-task/SKILL.md +140 -0
- package/skills/skill-creator/SKILL.md +101 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scheduled-run floor: what no scheduled run may do, whatever its rules
|
|
3
|
+
* say.
|
|
4
|
+
*
|
|
5
|
+
* A scheduled run must not reach its own scheduler — stop, disable or remove
|
|
6
|
+
* the service, or run a `namzu schedule` subcommand that changes something —
|
|
7
|
+
* and must not name `NAMZU_HOME`, where its job, its history, the daemon's
|
|
8
|
+
* endpoint token and the credentials beside them live, nor the Windows
|
|
9
|
+
* browser's profile folder (`%LOCALAPPDATA%\namzu`), where namzu keeps the
|
|
10
|
+
* profiles it drives from WSL with the cookies of every site the operator
|
|
11
|
+
* signed in to.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this is code and not patterns
|
|
14
|
+
*
|
|
15
|
+
* The floor used to be regular expressions over the command line's text.
|
|
16
|
+
* A pattern about what a line RUNS has to re-implement the shell's quoting to
|
|
17
|
+
* see through `'sched'ule`, `$'\x73'chedule`, `sche\<newline>dule` and
|
|
18
|
+
* `namzu --add-dir ';' schedule stop`, and each review round found another
|
|
19
|
+
* form it missed, plus a pattern that backtracked quadratically. The SDK
|
|
20
|
+
* reads a command line once, with the lexer the gate itself uses
|
|
21
|
+
* (`lexShellCommandLine`, checked against real bash), and this module decides
|
|
22
|
+
* on what that lexer reports: the words bash will pass to each simple
|
|
23
|
+
* command, after quote removal and `$'…'` decoding, the payload of a nested
|
|
24
|
+
* `bash -c`, every redirection target, and the words of `for` lists and
|
|
25
|
+
* `case` statements. Quoting is the lexer's business; this file never
|
|
26
|
+
* mentions a quote character except in the tripwire below.
|
|
27
|
+
*
|
|
28
|
+
* ## What is decided, and how
|
|
29
|
+
*
|
|
30
|
+
* - **Scheduler commands**, on each simple command's words in order,
|
|
31
|
+
* anywhere in the command, so `sudo`, `env`, `nohup` or `timeout` in front
|
|
32
|
+
* changes nothing: `systemctl` + a stopping verb + a unit naming namzu (or
|
|
33
|
+
* a glob), `launchctl` + a removing verb + a namzu label, `schtasks` with a
|
|
34
|
+
* `/delete`, `/change` or `/end` switch and a namzu task in any order,
|
|
35
|
+
* `pkill`/`killall` with a pattern that could match the daemon,
|
|
36
|
+
* `busctl`/`dbus-send`/`gdbus` naming namzu, and the CLI (`namzu` or a path
|
|
37
|
+
* to it, `@namzu/cli`, a `bin.js`, or a JavaScript runtime) with `schedule`
|
|
38
|
+
* followed by anything but a read-only verb. A word that expands at
|
|
39
|
+
* runtime (`$V`) stands for any word in any of those places; one whose only
|
|
40
|
+
* expansion is locale quoting (`$"stop"`) is read as its text.
|
|
41
|
+
* - **NAMZU_HOME**, on every word, redirection target, `for` list and `case`
|
|
42
|
+
* word: `~` (where bash expands it), `$HOME`, `${HOME}`, `$NAMZU_HOME` and
|
|
43
|
+
* `$PWD` are expanded here from known values, as are variables the line
|
|
44
|
+
* itself assigns; `..` and `.` are resolved; a relative path is resolved
|
|
45
|
+
* against the job's folder and every directory a `cd` before it names.
|
|
46
|
+
* Where a word still holds an unknown expansion or a glob, it is denied
|
|
47
|
+
* when the known part before it could lead into NAMZU_HOME (`~/.nam*`,
|
|
48
|
+
* `~/$X`, `/$X`), or the text after it names NAMZU_HOME's last segment. An
|
|
49
|
+
* assignment standing alone reaches no program and is judged where the
|
|
50
|
+
* variable is used; one that is exported is judged where it stands.
|
|
51
|
+
* - **What the lexer cannot account for** — an opaque line (a command
|
|
52
|
+
* substitution, `eval`-like constructs, a syntax error, a construct it does
|
|
53
|
+
* not model), a command whose name expands, or a program that runs text as
|
|
54
|
+
* code (`sh` reading its input, `sudo bash -c`, `python`, `xargs`, `watch`
|
|
55
|
+
* …) — is denied when the line's text mentions anything the floor
|
|
56
|
+
* protects: a scheduler word, `namzu`, NAMZU_HOME's name or a path into it.
|
|
57
|
+
* That is the tripwire, deliberately textual and cheap. Text such a program
|
|
58
|
+
* may run — an argument, a here-string, a here-document's body — is also
|
|
59
|
+
* read as a command line of its own, so an escape only the inner shell
|
|
60
|
+
* undoes does not hide it.
|
|
61
|
+
|
|
62
|
+
* - **The Windows browser's profiles**, on the same words, spellings and
|
|
63
|
+
* directories: a path whose segments run `AppData/Local/namzu` (any user,
|
|
64
|
+
* any drive, `/mnt/c/Users/<you>/…` or `C:\Users\<you>\…`, either slash,
|
|
65
|
+
* any letter case), with `%LOCALAPPDATA%`, `$env:LOCALAPPDATA` and
|
|
66
|
+
* `$LOCALAPPDATA` spelled out. The user folder above it is not known here,
|
|
67
|
+
* so the three segments are what is matched. Where a word holds an unknown
|
|
68
|
+
* expansion or a glob, it is denied when its segments could still read
|
|
69
|
+
* `AppData/Local/namzu` and one of them is spelled out, or when an unknown
|
|
70
|
+
* expansion is followed by a `namzu` segment.
|
|
71
|
+
|
|
72
|
+
* Every other tool's arguments are strings, not shell: each is searched for
|
|
73
|
+
* NAMZU_HOME and the browser's profiles by path, in any letter case, with
|
|
74
|
+
* `~`, `$HOME`, `$NAMZU_HOME` and `%LOCALAPPDATA%` spelled out, and once more
|
|
75
|
+
* with shell quotes and backslashes dropped, in case the tool hands it to a
|
|
76
|
+
* shell.
|
|
77
|
+
*
|
|
78
|
+
* Everything here is linear in the input.
|
|
79
|
+
*/
|
|
80
|
+
import { builtinCommandArguments, commandArgumentOf, lexShellCommandLine, } from '@namzu/sdk';
|
|
81
|
+
/** Scheduler verbs a run may use: they read, they change nothing. */
|
|
82
|
+
export const READ_ONLY_VERBS = ['list', 'show', 'status', 'history', 'logs'];
|
|
83
|
+
/** The floor's reason for denying a call, or null: what the rule below decides on. */
|
|
84
|
+
export function scheduledRunFloorVerdict(options) {
|
|
85
|
+
const floor = new Floor(options);
|
|
86
|
+
return (call) => floor.verdict(call);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Rules this module made, which decide `deny` or nothing: a rule here never
|
|
90
|
+
* lets a call through, so a tool it covers may still be withheld from a run.
|
|
91
|
+
*/
|
|
92
|
+
const DENY_ONLY = new WeakSet();
|
|
93
|
+
/** Whether `rule` is the floor's, and so can only deny. */
|
|
94
|
+
export function isFloorRule(rule) {
|
|
95
|
+
return DENY_ONLY.has(rule);
|
|
96
|
+
}
|
|
97
|
+
/** The floor as one gate rule. */
|
|
98
|
+
export function scheduledRunFloorRule(options) {
|
|
99
|
+
const floor = new Floor(options);
|
|
100
|
+
const rule = {
|
|
101
|
+
type: 'predicate',
|
|
102
|
+
description: `the scheduled-run floor: a scheduled run may not stop, disable or remove the scheduler, run a \`namzu schedule\` subcommand other than ${READ_ONLY_VERBS.join(', ')}, or name NAMZU_HOME (${options.namzuHome}) or the Windows browser's profile folder (%LOCALAPPDATA%\\namzu) in any argument. It holds for every scheduled run, so rewording the call will not help`,
|
|
103
|
+
decide: (call) => (floor.verdict(call) === null ? null : 'deny'),
|
|
104
|
+
};
|
|
105
|
+
DENY_ONLY.add(rule);
|
|
106
|
+
return rule;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Words
|
|
110
|
+
const SYSTEMCTL_VERBS_WITH_UNIT = new Set([
|
|
111
|
+
'stop',
|
|
112
|
+
'disable',
|
|
113
|
+
'mask',
|
|
114
|
+
'edit',
|
|
115
|
+
'kill',
|
|
116
|
+
'revert',
|
|
117
|
+
'freeze',
|
|
118
|
+
'set-property',
|
|
119
|
+
'clean',
|
|
120
|
+
]);
|
|
121
|
+
/** Verbs that stop units without naming them. */
|
|
122
|
+
const SYSTEMCTL_VERBS_ALONE = new Set(['isolate', 'exit']);
|
|
123
|
+
const LAUNCHCTL_VERBS = new Set(['bootout', 'unload', 'remove', 'disable', 'kill', 'stop']);
|
|
124
|
+
const SCHTASKS_SWITCH = /^[/-](?:delete|change|end)$/;
|
|
125
|
+
const DBUS_TOOLS = new Set(['busctl', 'dbus-send', 'gdbus']);
|
|
126
|
+
/** Runtimes the CLI can be started through without its name in the line. */
|
|
127
|
+
const JS_RUNTIMES = new Set([
|
|
128
|
+
'node',
|
|
129
|
+
'nodejs',
|
|
130
|
+
'npx',
|
|
131
|
+
'pnpx',
|
|
132
|
+
'bunx',
|
|
133
|
+
'bun',
|
|
134
|
+
'deno',
|
|
135
|
+
'tsx',
|
|
136
|
+
'ts-node',
|
|
137
|
+
'npm',
|
|
138
|
+
'pnpm',
|
|
139
|
+
'yarn',
|
|
140
|
+
]);
|
|
141
|
+
const CLI_ENTRY = /^bin(?:\.[cm]?js)?$/;
|
|
142
|
+
/** Shells: a payload the lexer did not follow (stdin, a script, `sudo bash -c`) is text it never read. */
|
|
143
|
+
const SHELLS = new Set([
|
|
144
|
+
'sh',
|
|
145
|
+
'bash',
|
|
146
|
+
'dash',
|
|
147
|
+
'zsh',
|
|
148
|
+
'ksh',
|
|
149
|
+
'ash',
|
|
150
|
+
'mksh',
|
|
151
|
+
'busybox',
|
|
152
|
+
'fish',
|
|
153
|
+
'csh',
|
|
154
|
+
'tcsh',
|
|
155
|
+
'pwsh',
|
|
156
|
+
'powershell',
|
|
157
|
+
'cmd',
|
|
158
|
+
'wsl',
|
|
159
|
+
]);
|
|
160
|
+
/** Programs that run a string, a file or their input as code or as a command. */
|
|
161
|
+
const INTERPRETERS = /^(?:python[0-9.]*|pypy[0-9.]*|node|nodejs|deno|bun|perl[0-9.]*|ruby|php[0-9.]*|lua[0-9.]*|luajit|osascript|awk|gawk|mawk|nawk|sed|gsed|tclsh|wish|expect|eval|source|xargs|parallel|watch|su|runuser|ssh|script|flock|at|batch|crontab)$/;
|
|
162
|
+
/** Options that make `sudo` or `env` run a string rather than an argv. */
|
|
163
|
+
const STRING_OPTIONS = {
|
|
164
|
+
sudo: /^-(?:[a-z]*[si][a-z]*|-shell|-login)$/,
|
|
165
|
+
env: /^-(?:[a-z]*s[a-z]*|-split-string(?:=.*)?)$/i,
|
|
166
|
+
};
|
|
167
|
+
/** Words that, mentioned anywhere in a line the lexer cannot account for, deny it. */
|
|
168
|
+
const SENSITIVE = [
|
|
169
|
+
'namzu',
|
|
170
|
+
'schedul',
|
|
171
|
+
'systemctl',
|
|
172
|
+
'launchctl',
|
|
173
|
+
'schtasks',
|
|
174
|
+
'pkill',
|
|
175
|
+
'killall',
|
|
176
|
+
'busctl',
|
|
177
|
+
'dbus-send',
|
|
178
|
+
'gdbus',
|
|
179
|
+
];
|
|
180
|
+
const NAME_CHAR = /[a-z0-9._-]/;
|
|
181
|
+
function lower(text) {
|
|
182
|
+
return text.toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
/** The last path segment, lower-cased, with `.exe` dropped. */
|
|
185
|
+
function commandName(value) {
|
|
186
|
+
const cut = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
|
187
|
+
const base = lower(cut < 0 ? value : value.slice(cut + 1));
|
|
188
|
+
return base.endsWith('.exe') ? base.slice(0, -4) : base;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* `$"…"` read as its text. The lexer leaves locale quoting as written and
|
|
192
|
+
* flags the word, because a translation catalogue could change it; the text
|
|
193
|
+
* itself is what a C locale passes.
|
|
194
|
+
*/
|
|
195
|
+
function unlocale(value) {
|
|
196
|
+
return value.includes('$"')
|
|
197
|
+
? value.replace(/\$"((?:[^"\\]|\\.)*)"/g, (_, inner) => inner.replace(/\\([$`"\\\n])/g, '$1'))
|
|
198
|
+
: value;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A word whose only expansion is locale quoting, read as its C-locale text,
|
|
202
|
+
* unless the line can choose a translation catalogue (`TEXTDOMAIN`,
|
|
203
|
+
* `TEXTDOMAINDIR`), which could turn `$"status"` into anything.
|
|
204
|
+
*/
|
|
205
|
+
function localeOnly(value, catalogue) {
|
|
206
|
+
return !catalogue && value.includes('$"') && !/[$`*?[{~]/.test(unlocale(value));
|
|
207
|
+
}
|
|
208
|
+
function wordsOf(command, catalogue) {
|
|
209
|
+
return command.words.map((w) => {
|
|
210
|
+
const text = w.expands ? unlocale(w.value) : w.value;
|
|
211
|
+
return { text: lower(text), wild: w.expands && !localeOnly(w.value, catalogue) };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Scheduler commands
|
|
216
|
+
/**
|
|
217
|
+
* Whether one command's words, in order, reach the scheduler. Each role is
|
|
218
|
+
* taken at its first place after the one before; for a question of "is
|
|
219
|
+
* there such a subsequence" that loses nothing, and it is one pass.
|
|
220
|
+
*/
|
|
221
|
+
function reachesScheduler(words, daemonCommandLine) {
|
|
222
|
+
const n = words.length;
|
|
223
|
+
const find = (from, test) => {
|
|
224
|
+
for (let i = from; i < n; i++)
|
|
225
|
+
if (test(words[i]))
|
|
226
|
+
return i;
|
|
227
|
+
return -1;
|
|
228
|
+
};
|
|
229
|
+
const named = (name) => (w) => w.wild ? w.text.includes(name) : commandName(w.text) === name;
|
|
230
|
+
const namesNamzu = (w) => w.wild || w.text.includes('namzu');
|
|
231
|
+
// systemctl <verb> <unit>
|
|
232
|
+
const systemctl = find(0, named('systemctl'));
|
|
233
|
+
if (systemctl >= 0) {
|
|
234
|
+
if (find(systemctl + 1, (w) => !w.wild && SYSTEMCTL_VERBS_ALONE.has(w.text)) >= 0)
|
|
235
|
+
return true;
|
|
236
|
+
const verb = find(systemctl + 1, (w) => w.wild || SYSTEMCTL_VERBS_WITH_UNIT.has(w.text));
|
|
237
|
+
// systemctl matches unit names against a glob.
|
|
238
|
+
if (verb >= 0 && find(verb + 1, (u) => namesNamzu(u) || /[*?[]/.test(u.text)) >= 0)
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
// launchctl <verb> <label or plist>
|
|
242
|
+
const launchctl = find(0, named('launchctl'));
|
|
243
|
+
if (launchctl >= 0) {
|
|
244
|
+
const verb = find(launchctl + 1, (w) => w.wild || LAUNCHCTL_VERBS.has(w.text));
|
|
245
|
+
if (verb >= 0 && find(verb + 1, namesNamzu) >= 0)
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
// schtasks, whose switches come in any order.
|
|
249
|
+
const schtasks = find(0, named('schtasks'));
|
|
250
|
+
if (schtasks >= 0) {
|
|
251
|
+
const rest = words.slice(schtasks + 1);
|
|
252
|
+
if (rest.some((w) => w.wild || SCHTASKS_SWITCH.test(w.text)) && rest.some(namesNamzu))
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
// pkill / killall: a pattern that could match the daemon's process.
|
|
256
|
+
const killer = find(0, (w) => named('pkill')(w) || named('killall')(w));
|
|
257
|
+
if (killer >= 0) {
|
|
258
|
+
const full = words
|
|
259
|
+
.slice(killer + 1)
|
|
260
|
+
.some((w) => /^-[a-z]*f/.test(w.text) || w.text === '--full');
|
|
261
|
+
for (let i = killer + 1; i < n; i++) {
|
|
262
|
+
const w = words[i];
|
|
263
|
+
if (w.wild)
|
|
264
|
+
return true;
|
|
265
|
+
if (w.text.startsWith('-'))
|
|
266
|
+
continue;
|
|
267
|
+
if (/[.*+?^$|()[\]{}\\]/.test(w.text))
|
|
268
|
+
return true;
|
|
269
|
+
if (w.text.includes('namzu') || w.text.includes('schedul'))
|
|
270
|
+
return true;
|
|
271
|
+
if ('node'.includes(w.text) || (full && daemonCommandLine.includes(w.text)))
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// A D-Bus call to systemd naming the unit.
|
|
276
|
+
const dbus = find(0, (w) => DBUS_TOOLS.has(commandName(w.text)));
|
|
277
|
+
if (dbus >= 0 && find(dbus + 1, namesNamzu) >= 0)
|
|
278
|
+
return true;
|
|
279
|
+
// The CLI, by name (`namzu`, a path to it, `npx @namzu/cli`) or through
|
|
280
|
+
// its entry script or a JavaScript runtime: `schedule` and then anything
|
|
281
|
+
// but a read-only verb.
|
|
282
|
+
const strong = find(0, (w) => commandName(w.text) === 'namzu' || w.text.includes('@namzu/cli'));
|
|
283
|
+
const weak = find(0, (w) => CLI_ENTRY.test(commandName(w.text)) || JS_RUNTIMES.has(commandName(w.text)));
|
|
284
|
+
// After the CLI's own name, a word that expands may be `schedule stop`.
|
|
285
|
+
if (strong >= 0 && find(strong + 1, (w) => w.wild) >= 0)
|
|
286
|
+
return true;
|
|
287
|
+
const cli = strong < 0 ? weak : weak < 0 ? strong : Math.min(strong, weak);
|
|
288
|
+
if (cli >= 0) {
|
|
289
|
+
for (let i = cli + 1; i < n; i++) {
|
|
290
|
+
if (words[i].text !== 'schedule' || words[i].wild)
|
|
291
|
+
continue;
|
|
292
|
+
const verb = words[i + 1];
|
|
293
|
+
if (verb === undefined)
|
|
294
|
+
continue;
|
|
295
|
+
if (verb.wild || !READ_ONLY_VERBS.includes(verb.text))
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Whether a command runs text the lexer did not read as commands: a shell
|
|
303
|
+
* the lexer did not follow, an interpreter, `xargs`, `sudo -s`, `env -S`.
|
|
304
|
+
*/
|
|
305
|
+
function runsUnreadText(command) {
|
|
306
|
+
const words = command.words;
|
|
307
|
+
const head = command.assignments;
|
|
308
|
+
const names = words.map((w) => (w.expands ? '' : commandName(w.value)));
|
|
309
|
+
// The lexer reads the payload of a shell at the head of its command run
|
|
310
|
+
// with `-c`. Anywhere else, or without `-c`, it did not.
|
|
311
|
+
const dashAt = words.findIndex((w, i) => i > head && !w.expands && /^-[a-z]*c[a-z]*$/.test(w.value));
|
|
312
|
+
// A payload that expands is not one the lexer could read.
|
|
313
|
+
const dashC = dashAt > 0 && words[dashAt + 1] !== undefined && !words[dashAt + 1]?.expands;
|
|
314
|
+
for (let i = 0; i < words.length; i++) {
|
|
315
|
+
const name = names[i];
|
|
316
|
+
if (name === '')
|
|
317
|
+
continue;
|
|
318
|
+
if (INTERPRETERS.test(name))
|
|
319
|
+
return true;
|
|
320
|
+
if (i === head && name === '.')
|
|
321
|
+
return true;
|
|
322
|
+
if (SHELLS.has(name) && !(i === head && dashC))
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
for (const [program, option] of Object.entries(STRING_OPTIONS))
|
|
326
|
+
if (names.includes(program) && words.some((w) => !w.expands && option.test(w.value)))
|
|
327
|
+
return true;
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Paths
|
|
332
|
+
/**
|
|
333
|
+
* `text` with `\` read as `/`, empty and `.` segments dropped and `..`
|
|
334
|
+
* applied, the way the kernel resolves a path (symbolic links aside). Applied
|
|
335
|
+
* to any text, not only a path: a segment that is not a path's is kept.
|
|
336
|
+
*/
|
|
337
|
+
function normalize(text) {
|
|
338
|
+
const out = [];
|
|
339
|
+
const parts = text.replace(/\\/g, '/').split('/');
|
|
340
|
+
for (let i = 0; i < parts.length; i++) {
|
|
341
|
+
const part = parts[i];
|
|
342
|
+
if (part === '' && i > 0)
|
|
343
|
+
continue;
|
|
344
|
+
if (part === '.')
|
|
345
|
+
continue;
|
|
346
|
+
if (part === '..') {
|
|
347
|
+
const last = out.at(-1);
|
|
348
|
+
if (last === '')
|
|
349
|
+
continue; // at the root
|
|
350
|
+
if (last !== undefined && last !== '..') {
|
|
351
|
+
out.pop();
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
out.push(part);
|
|
356
|
+
}
|
|
357
|
+
if (out.length === 1 && out[0] === '')
|
|
358
|
+
return '/';
|
|
359
|
+
return out.join('/');
|
|
360
|
+
}
|
|
361
|
+
function isAbsolute(path) {
|
|
362
|
+
return path.startsWith('/') || path.startsWith('\\') || /^[a-z]:[\\/]/.test(path);
|
|
363
|
+
}
|
|
364
|
+
function join(dir, rel) {
|
|
365
|
+
return normalize(`${dir}/${rel}`);
|
|
366
|
+
}
|
|
367
|
+
/** Linear-time search for `home` in normalized `text`, as a whole path. */
|
|
368
|
+
function containsPath(text, home) {
|
|
369
|
+
let at = text.indexOf(home);
|
|
370
|
+
while (at >= 0) {
|
|
371
|
+
const before = at === 0 ? '' : text[at - 1];
|
|
372
|
+
const after = text[at + home.length] ?? '';
|
|
373
|
+
if (!NAME_CHAR.test(before) && !NAME_CHAR.test(after))
|
|
374
|
+
return true;
|
|
375
|
+
at = text.indexOf(home, at + 1);
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
/** No directory known: a relative path is checked against none. */
|
|
380
|
+
const NOWHERE = { dirs: [], unknown: true };
|
|
381
|
+
const UNKNOWN = { values: [], unknown: true };
|
|
382
|
+
/** How many ways a word may be spelled out before the floor stops trying and denies. */
|
|
383
|
+
const MAX_ALTERNATIVES = 64;
|
|
384
|
+
class TooMany extends Error {
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// The Windows browser's profiles
|
|
388
|
+
/**
|
|
389
|
+
* The segments that end the path of the Windows browser's profile root,
|
|
390
|
+
* `%LOCALAPPDATA%\namzu`. The user folder above them is not known here (a
|
|
391
|
+
* run may name any user's), so these are what a path is matched on.
|
|
392
|
+
*/
|
|
393
|
+
const PROFILE_SEGMENTS = ['appdata', 'local', 'namzu'];
|
|
394
|
+
const PROFILE_TAIL = PROFILE_SEGMENTS.join('/');
|
|
395
|
+
/** What `%LOCALAPPDATA%` and `$LOCALAPPDATA` stand for in the checks below. */
|
|
396
|
+
const LOCAL_APP_DATA = 'c:/users/%username%/appdata/local';
|
|
397
|
+
/**
|
|
398
|
+
* Lower-cased `text` with `%LOCALAPPDATA%`, `$env:LOCALAPPDATA`,
|
|
399
|
+
* `${env:LOCALAPPDATA}`, `$LOCALAPPDATA` and `${LOCALAPPDATA}` spelled out.
|
|
400
|
+
*/
|
|
401
|
+
function spellLocalAppData(text) {
|
|
402
|
+
return text.includes('localappdata')
|
|
403
|
+
? text.replace(/%localappdata%|\$\{?(?:env:)?localappdata(?![a-z0-9_])\}?/g, LOCAL_APP_DATA)
|
|
404
|
+
: text;
|
|
405
|
+
}
|
|
406
|
+
/** Whether normalized, lower-cased text names the profile root or a path in it. */
|
|
407
|
+
function namesProfile(text) {
|
|
408
|
+
return containsPath(text, PROFILE_TAIL);
|
|
409
|
+
}
|
|
410
|
+
/** Characters that stand for text the floor does not know: a glob or an expansion. */
|
|
411
|
+
const WILD = /[*?[$`{]/;
|
|
412
|
+
const WILD_RUN = /\$\{[^}]*\}?|\$[a-z0-9_]*|`[^`]*`?|\[[^\]]*\]?|[*?{}]/;
|
|
413
|
+
/** Whether one path segment with wildcards in it could be `name`: its literal pieces, in order. */
|
|
414
|
+
function mayBe(segment, name) {
|
|
415
|
+
if (!WILD.test(segment))
|
|
416
|
+
return segment === name;
|
|
417
|
+
const pieces = segment.split(WILD_RUN);
|
|
418
|
+
const first = pieces[0];
|
|
419
|
+
const last = pieces.at(-1);
|
|
420
|
+
if (!name.startsWith(first) || !name.endsWith(last))
|
|
421
|
+
return false;
|
|
422
|
+
let at = first.length;
|
|
423
|
+
for (const piece of pieces.slice(1, -1)) {
|
|
424
|
+
const found = name.indexOf(piece, at);
|
|
425
|
+
if (found < 0)
|
|
426
|
+
return false;
|
|
427
|
+
at = found + piece.length;
|
|
428
|
+
}
|
|
429
|
+
return at <= name.length - last.length;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Whether a normalized path with wildcards in it could run through
|
|
433
|
+
* `appdata/local/namzu`. Three segments of nothing but wildcards are not
|
|
434
|
+
* enough: `ls` of three globbed levels in a project is not a way into a Windows profile.
|
|
435
|
+
*/
|
|
436
|
+
function mayNameProfile(text) {
|
|
437
|
+
const segments = text.split('/');
|
|
438
|
+
for (let i = 0; i + PROFILE_SEGMENTS.length <= segments.length; i++) {
|
|
439
|
+
const run = segments.slice(i, i + PROFILE_SEGMENTS.length);
|
|
440
|
+
if (!run.every((segment, k) => mayBe(segment, PROFILE_SEGMENTS[k])))
|
|
441
|
+
continue;
|
|
442
|
+
if (run.some((segment) => segment.split(WILD_RUN).some((piece) => piece !== '')))
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Whether a path stops short inside the profile root's last name
|
|
449
|
+
* (`…/AppData/Local/nam`), to be finished by an expansion elsewhere.
|
|
450
|
+
*/
|
|
451
|
+
function endsShortOfProfile(path) {
|
|
452
|
+
const segments = path.split('/');
|
|
453
|
+
const n = segments.length;
|
|
454
|
+
const last = segments[n - 1];
|
|
455
|
+
return (n >= 3 &&
|
|
456
|
+
segments[n - 3] === 'appdata' &&
|
|
457
|
+
segments[n - 2] === 'local' &&
|
|
458
|
+
last !== '' &&
|
|
459
|
+
last !== 'namzu' &&
|
|
460
|
+
'namzu'.startsWith(last));
|
|
461
|
+
}
|
|
462
|
+
class Floor {
|
|
463
|
+
home;
|
|
464
|
+
homeCore;
|
|
465
|
+
userHome;
|
|
466
|
+
folders;
|
|
467
|
+
daemonCommandLine;
|
|
468
|
+
builtinCommands;
|
|
469
|
+
constructor(options) {
|
|
470
|
+
this.home = normalize(lower(options.namzuHome));
|
|
471
|
+
this.userHome = normalize(lower(options.userHome));
|
|
472
|
+
this.folders = (options.folders ?? []).map((f) => normalize(lower(f)));
|
|
473
|
+
const last = this.home.split('/').at(-1) ?? '';
|
|
474
|
+
this.homeCore = last.replace(/^\.+/, '');
|
|
475
|
+
this.builtinCommands = builtinCommandArguments();
|
|
476
|
+
this.daemonCommandLine = lower(options.daemonCommandLine ?? `${process.execPath} ${process.argv[1] ?? ''} schedule daemon`);
|
|
477
|
+
}
|
|
478
|
+
/** Why the floor denies the call, or null when it does not. */
|
|
479
|
+
verdict(call) {
|
|
480
|
+
const argument = call.toolDef
|
|
481
|
+
? commandArgumentOf(call.toolDef)
|
|
482
|
+
: this.builtinCommands.get(call.toolName);
|
|
483
|
+
const input = call.toolInput;
|
|
484
|
+
let line;
|
|
485
|
+
if (argument !== undefined && typeof input === 'object' && input !== null) {
|
|
486
|
+
const value = input[argument];
|
|
487
|
+
if (typeof value === 'string')
|
|
488
|
+
line = value;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const named = this.stringsName(input, line);
|
|
492
|
+
if (named !== null)
|
|
493
|
+
return `names ${named}`;
|
|
494
|
+
if (line !== undefined)
|
|
495
|
+
return this.lineVerdict(line, call.commandDialect);
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
if (error instanceof TooMany)
|
|
500
|
+
return 'too many spellings';
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// ---- any tool -----------------------------------------------------------
|
|
505
|
+
/** Every string in the input but the command line, searched as text. */
|
|
506
|
+
stringsName(input, line) {
|
|
507
|
+
const stack = [input];
|
|
508
|
+
let skipped = false;
|
|
509
|
+
while (stack.length > 0) {
|
|
510
|
+
const value = stack.pop();
|
|
511
|
+
if (typeof value === 'string') {
|
|
512
|
+
if (!skipped && value === line) {
|
|
513
|
+
skipped = true;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (this.textNamesHome(value))
|
|
517
|
+
return 'NAMZU_HOME';
|
|
518
|
+
if (textNamesProfile(value))
|
|
519
|
+
return 'the browser profiles';
|
|
520
|
+
}
|
|
521
|
+
else if (Array.isArray(value)) {
|
|
522
|
+
stack.push(...value);
|
|
523
|
+
}
|
|
524
|
+
else if (value !== null && typeof value === 'object') {
|
|
525
|
+
for (const [key, each] of Object.entries(value))
|
|
526
|
+
stack.push(key, each);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* NAMZU_HOME in free text: as a path, through `~`, `$HOME`, `${HOME}` or
|
|
533
|
+
* `$NAMZU_HOME`, in any letter case; and again with quotes and
|
|
534
|
+
* backslashes dropped, as a shell would drop them.
|
|
535
|
+
*/
|
|
536
|
+
textNamesHome(text) {
|
|
537
|
+
const plain = lower(text);
|
|
538
|
+
const unquoted = plain
|
|
539
|
+
.replace(/\\\n/g, '')
|
|
540
|
+
.replace(/\$(?=["'])/g, '')
|
|
541
|
+
.replace(/["'\\]/g, '');
|
|
542
|
+
for (const variant of [plain, unquoted]) {
|
|
543
|
+
if (/\$\{?[!#]?namzu_home(?![a-z0-9_])/.test(variant))
|
|
544
|
+
return true;
|
|
545
|
+
const spelled = variant
|
|
546
|
+
.replace(/\$\{home\}|\$home(?![a-z0-9_])/g, this.userHome)
|
|
547
|
+
.replace(/(^|[\s"'=:(<>|;&,`])~[a-z0-9._-]*(?=[/\\]|$|[\s"'])/g, `$1${this.userHome}`);
|
|
548
|
+
if (containsPath(normalize(spelled), this.home))
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
// ---- a command line -----------------------------------------------------
|
|
554
|
+
lineVerdict(line, dialect, depth = 0) {
|
|
555
|
+
// A line for a shell that may be bash or a POSIX shell is read both
|
|
556
|
+
// ways, and denied if either reading denies it.
|
|
557
|
+
const readings = dialect === 'bash'
|
|
558
|
+
? [lexShellCommandLine(line, { dialect: 'bash' })]
|
|
559
|
+
: [
|
|
560
|
+
lexShellCommandLine(line, { dialect: 'sh' }),
|
|
561
|
+
lexShellCommandLine(line, { dialect: 'bash' }),
|
|
562
|
+
];
|
|
563
|
+
const catalogue = /textdomain/i.test(line);
|
|
564
|
+
const loops = /\b(?:while|until|for|select)\b/.test(line);
|
|
565
|
+
let unaccounted = null;
|
|
566
|
+
for (const reading of readings) {
|
|
567
|
+
const reason = this.readingVerdict(reading, catalogue, loops);
|
|
568
|
+
if (reason !== null)
|
|
569
|
+
return reason;
|
|
570
|
+
if (reading.opaque)
|
|
571
|
+
unaccounted ??= 'opaque line mentions a protected name';
|
|
572
|
+
else if (reading.commands.some(unreadCommand))
|
|
573
|
+
unaccounted ??= 'unread text mentions a protected name';
|
|
574
|
+
}
|
|
575
|
+
if (unaccounted === null)
|
|
576
|
+
return null;
|
|
577
|
+
if (this.mentionsProtected(line, readings))
|
|
578
|
+
return unaccounted;
|
|
579
|
+
// Text a program may run as a command line — an argument (`echo '…' |
|
|
580
|
+
// sh`, `sudo bash -c '…'`), a here-string, a here-document's body —
|
|
581
|
+
// is read as one, decoded, so what the tripwire looks for cannot hide
|
|
582
|
+
// behind escapes that only the inner shell undoes.
|
|
583
|
+
if (depth < MAX_TEXT_DEPTH)
|
|
584
|
+
for (const text of fedTexts(readings)) {
|
|
585
|
+
if (this.lineVerdict(text, 'bash', depth + 1) !== null)
|
|
586
|
+
return unaccounted;
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
/** The tripwire: the line's text names something the floor protects. */
|
|
591
|
+
mentionsProtected(line, readings) {
|
|
592
|
+
const squashed = lower(line)
|
|
593
|
+
.replace(/\\\n/g, '')
|
|
594
|
+
.replace(/["'\\$`]/g, '');
|
|
595
|
+
const decoded = readings
|
|
596
|
+
.flatMap((r) => r.commands.flatMap((c) => c.words.map((w) => lower(w.value))))
|
|
597
|
+
.join(' ');
|
|
598
|
+
const tokens = [...SENSITIVE, 'namzu_home'];
|
|
599
|
+
if (this.homeCore.length >= 3)
|
|
600
|
+
tokens.push(this.homeCore);
|
|
601
|
+
for (const text of [squashed, decoded])
|
|
602
|
+
if (tokens.some((token) => text.includes(token)))
|
|
603
|
+
return true;
|
|
604
|
+
return this.textNamesHome(line);
|
|
605
|
+
}
|
|
606
|
+
readingVerdict(reading, catalogue, loops) {
|
|
607
|
+
for (const command of reading.commands)
|
|
608
|
+
if (reachesScheduler(wordsOf(command, catalogue), this.daemonCommandLine))
|
|
609
|
+
return 'scheduler command';
|
|
610
|
+
const variables = this.assignments(reading);
|
|
611
|
+
const expanding = (w) => w.expands && !localeOnly(w.value, catalogue);
|
|
612
|
+
const expands = reading.commands.some((c) => c.words.some(expanding)) ||
|
|
613
|
+
reading.redirections.some((r) => expanding(r.target)) ||
|
|
614
|
+
reading.compoundWords.some(expanding);
|
|
615
|
+
// An assignment on its own reaches no program, and every use of the
|
|
616
|
+
// variable is spelled out with its value. One that is exported, or
|
|
617
|
+
// could be, is checked where it stands.
|
|
618
|
+
const exported = reading.commands.some((c) => c.words.some((w) => !w.expands && EXPORTING.has(commandName(w.value))));
|
|
619
|
+
const cwd = new Directories(this.folders);
|
|
620
|
+
// In a loop, a `cd` late in the body applies to the next pass's first
|
|
621
|
+
// command: every command gets every directory the line can reach.
|
|
622
|
+
if (loops)
|
|
623
|
+
for (const command of reading.commands)
|
|
624
|
+
cwd.after(command, (target) => this.spell(target, variables, cwd.current()), this.userHome);
|
|
625
|
+
const seen = new Set();
|
|
626
|
+
for (const command of reading.commands) {
|
|
627
|
+
const here = cwd.current();
|
|
628
|
+
const check = (word) => this.wordNames(word, variables, here, expands);
|
|
629
|
+
const standalone = command.words.length === command.assignments && !exported;
|
|
630
|
+
for (const [i, word] of command.words.entries()) {
|
|
631
|
+
if (standalone && i < command.assignments)
|
|
632
|
+
continue;
|
|
633
|
+
const named = check(word);
|
|
634
|
+
if (named !== null)
|
|
635
|
+
return `word names ${named}`;
|
|
636
|
+
// `X=~/.namzu` as an argument, or exported: the value on its own.
|
|
637
|
+
const assigned = assignedValue(word);
|
|
638
|
+
const value = assigned ? check(assigned.word) : null;
|
|
639
|
+
if (value !== null)
|
|
640
|
+
return `assigned value names ${value}`;
|
|
641
|
+
}
|
|
642
|
+
for (const redirection of command.redirections) {
|
|
643
|
+
seen.add(redirection);
|
|
644
|
+
const named = redirectsTo(redirection) ? check(redirection.target) : null;
|
|
645
|
+
if (named !== null)
|
|
646
|
+
return `redirection names ${named}`;
|
|
647
|
+
}
|
|
648
|
+
cwd.after(command, (target) => this.spell(target, variables, cwd.current()), this.userHome);
|
|
649
|
+
}
|
|
650
|
+
// A compound command's redirections and words belong to no one
|
|
651
|
+
// command: every directory the line can be in applies.
|
|
652
|
+
const anywhere = cwd.current();
|
|
653
|
+
const check = (word) => this.wordNames(word, variables, anywhere, expands);
|
|
654
|
+
for (const redirection of reading.redirections) {
|
|
655
|
+
if (seen.has(redirection) || !redirectsTo(redirection))
|
|
656
|
+
continue;
|
|
657
|
+
const named = check(redirection.target);
|
|
658
|
+
if (named !== null)
|
|
659
|
+
return `redirection names ${named}`;
|
|
660
|
+
}
|
|
661
|
+
for (const word of reading.compoundWords) {
|
|
662
|
+
const named = check(word);
|
|
663
|
+
if (named !== null)
|
|
664
|
+
return `loop or case word names ${named}`;
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
/** Literal values the line assigns to each variable (`X=…`, `export X=…`). */
|
|
669
|
+
assignments(reading) {
|
|
670
|
+
const found = new Map();
|
|
671
|
+
const unknown = new Set();
|
|
672
|
+
const record = (word) => {
|
|
673
|
+
const value = assignedValue(word);
|
|
674
|
+
if (value === null)
|
|
675
|
+
return;
|
|
676
|
+
const { name, append } = value;
|
|
677
|
+
const spellings = append ? [] : this.spell(value.word, new Map(), NOWHERE);
|
|
678
|
+
if (spellings.length === 0 || spellings.some((s) => s.stop !== 'end')) {
|
|
679
|
+
unknown.add(name);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const values = found.get(name) ?? new Set();
|
|
683
|
+
for (const s of spellings)
|
|
684
|
+
values.add(s.known);
|
|
685
|
+
found.set(name, values);
|
|
686
|
+
};
|
|
687
|
+
for (const command of reading.commands) {
|
|
688
|
+
command.words.slice(0, command.assignments).forEach(record);
|
|
689
|
+
const head = command.words[command.assignments];
|
|
690
|
+
if (head &&
|
|
691
|
+
!head.expands &&
|
|
692
|
+
['export', 'declare', 'typeset', 'local', 'readonly'].includes(head.value))
|
|
693
|
+
command.words.slice(command.assignments + 1).forEach(record);
|
|
694
|
+
}
|
|
695
|
+
const out = new Map();
|
|
696
|
+
for (const [name, values] of found) {
|
|
697
|
+
if (values.size > 8)
|
|
698
|
+
throw new TooMany();
|
|
699
|
+
// The environment's value is possible too: a use may come before the assignment.
|
|
700
|
+
out.set(name, { values: [...values], unknown: true });
|
|
701
|
+
}
|
|
702
|
+
for (const name of unknown)
|
|
703
|
+
if (!out.has(name))
|
|
704
|
+
out.set(name, UNKNOWN);
|
|
705
|
+
return out;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* A word's value with `~`, `$HOME`, `${HOME}`, `$NAMZU_HOME`, `$PWD` and
|
|
709
|
+
* the line's own literal assignments spelled out, one spelling per
|
|
710
|
+
* combination of the values each may hold.
|
|
711
|
+
*/
|
|
712
|
+
spell(word, variables, cwd) {
|
|
713
|
+
if (!word.expands)
|
|
714
|
+
return [{ known: lower(word.value), stop: 'end', rest: '' }];
|
|
715
|
+
const value = unlocale(word.value.replace(/\\\n/g, ''));
|
|
716
|
+
// Bash expands a tilde only when nothing between it and the first
|
|
717
|
+
// unquoted `/` is quoted: `~/x` does, `"~"/x`, `~"/x"` and `~\/x` do not.
|
|
718
|
+
const raw = word.text.replace(/\\\n/g, '');
|
|
719
|
+
const tildeFirst = /^~[A-Za-z0-9._-]*(?:\/|$)/.test(raw);
|
|
720
|
+
const tildeAssigned = /^[A-Za-z_][A-Za-z0-9_]*\+?=/.test(raw) && /[=:]~[A-Za-z0-9._-]*(?:[/:]|$)/.test(raw);
|
|
721
|
+
let open = [''];
|
|
722
|
+
const out = [];
|
|
723
|
+
const stopAll = (stop, from) => {
|
|
724
|
+
const rest = lower(value.slice(from));
|
|
725
|
+
for (const known of open)
|
|
726
|
+
out.push({ known: lower(known), stop, rest });
|
|
727
|
+
};
|
|
728
|
+
let i = 0;
|
|
729
|
+
while (i < value.length && open.length > 0) {
|
|
730
|
+
const c = value[i];
|
|
731
|
+
if (c === '~' &&
|
|
732
|
+
((i === 0 && tildeFirst) ||
|
|
733
|
+
((value[i - 1] === '=' || value[i - 1] === ':') && tildeAssigned))) {
|
|
734
|
+
let j = i + 1;
|
|
735
|
+
while (j < value.length && NAME_CHAR.test(lower(value[j])))
|
|
736
|
+
j++;
|
|
737
|
+
if (j === value.length || value[j] === '/') {
|
|
738
|
+
open = open.map((known) => known + this.userHome);
|
|
739
|
+
i = j;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (c === '$') {
|
|
744
|
+
const simple = /^\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})/.exec(value.slice(i, i + 256));
|
|
745
|
+
if (simple) {
|
|
746
|
+
const name = (simple[1] ?? simple[2]);
|
|
747
|
+
const alternatives = name === 'HOME'
|
|
748
|
+
? { values: [this.userHome], unknown: false }
|
|
749
|
+
: name === 'NAMZU_HOME'
|
|
750
|
+
? { values: [this.home], unknown: false }
|
|
751
|
+
: name === 'PWD'
|
|
752
|
+
? { values: cwd.dirs, unknown: cwd.unknown }
|
|
753
|
+
: name === 'LOCALAPPDATA'
|
|
754
|
+
? localAppData(variables.get(name))
|
|
755
|
+
: (variables.get(name) ?? UNKNOWN);
|
|
756
|
+
i += simple[0].length;
|
|
757
|
+
// An unknown value ends every spelling here; a known one
|
|
758
|
+
// continues each, once per value.
|
|
759
|
+
if (alternatives.unknown)
|
|
760
|
+
stopAll('unknown', i);
|
|
761
|
+
const next = [];
|
|
762
|
+
for (const known of open)
|
|
763
|
+
for (const v of alternatives.values)
|
|
764
|
+
next.push(known + v);
|
|
765
|
+
if (next.length + out.length > MAX_ALTERNATIVES)
|
|
766
|
+
throw new TooMany();
|
|
767
|
+
open = next;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (i + 1 < value.length) {
|
|
771
|
+
stopAll('unknown', i + 1);
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (c === '`' || c === '{') {
|
|
776
|
+
stopAll('unknown', i + 1);
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
if (c === '*' || c === '?' || c === '[') {
|
|
780
|
+
stopAll('glob', i);
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
783
|
+
open = open.map((known) => known + c);
|
|
784
|
+
i++;
|
|
785
|
+
}
|
|
786
|
+
stopAll('end', value.length);
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
inside(path) {
|
|
790
|
+
return path === this.home || path.startsWith(this.home === '/' ? '/' : `${this.home}/`);
|
|
791
|
+
}
|
|
792
|
+
/** The home segment right below `dir`, when `dir` is an ancestor of NAMZU_HOME. */
|
|
793
|
+
belowAncestor(dir) {
|
|
794
|
+
const prefix = dir === '/' ? '/' : `${dir}/`;
|
|
795
|
+
if (!this.home.startsWith(prefix))
|
|
796
|
+
return null;
|
|
797
|
+
return this.home.slice(prefix.length).split('/')[0] ?? '';
|
|
798
|
+
}
|
|
799
|
+
/** What a word names that the floor protects, or null. */
|
|
800
|
+
wordNames(word, variables, cwd, lineExpands) {
|
|
801
|
+
if (this.wordNamesHome(word, variables, cwd, lineExpands))
|
|
802
|
+
return 'NAMZU_HOME';
|
|
803
|
+
if (this.wordNamesProfile(word, variables, cwd, lineExpands))
|
|
804
|
+
return 'the browser profiles';
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* The Windows browser's profiles in a word, on the same spellings and
|
|
809
|
+
* directories as NAMZU_HOME. The root is known only by its last three
|
|
810
|
+
* segments, so a word with an unknown part is denied when its segments
|
|
811
|
+
* could still read `appdata/local/namzu` and one of them is spelled out,
|
|
812
|
+
* or when an unknown expansion is followed by a `namzu` segment.
|
|
813
|
+
*/
|
|
814
|
+
wordNamesProfile(word, variables, cwd, lineExpands) {
|
|
815
|
+
for (const spelling of this.spell(word, variables, cwd)) {
|
|
816
|
+
const known = spellLocalAppData(spelling.known);
|
|
817
|
+
const rest = spellLocalAppData(spelling.rest);
|
|
818
|
+
// Anywhere in the word, and as a relative path from each directory.
|
|
819
|
+
const bases = isAbsolute(known) ? [''] : ['', ...cwd.dirs];
|
|
820
|
+
// A word with none of the root's names, no `..` and nothing unknown
|
|
821
|
+
// in it cannot lead into the root from a directory outside it:
|
|
822
|
+
// only a directory already inside it matters then, and joining the
|
|
823
|
+
// word to every directory a line of `cd`s names is the cost.
|
|
824
|
+
const inert = !/namzu|local|appdata|\.\.|[*?[$`{]/.test(known + rest);
|
|
825
|
+
for (const base of bases) {
|
|
826
|
+
if (inert && base !== '') {
|
|
827
|
+
if (namesProfile(base))
|
|
828
|
+
return true;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const at = (text) => (base === '' ? normalize(text) : join(base, text));
|
|
832
|
+
if (spelling.stop === 'end') {
|
|
833
|
+
const path = at(known);
|
|
834
|
+
if (namesProfile(path))
|
|
835
|
+
return true;
|
|
836
|
+
if (lineExpands && endsShortOfProfile(path))
|
|
837
|
+
return true;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
// The unknown part stands for any text: `*` does, as a segment's wildcard.
|
|
841
|
+
if (mayNameProfile(at(`${known}*${rest}`)))
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
// An expansion may hold slashes, and so the whole path above `namzu`.
|
|
845
|
+
if (spelling.stop === 'unknown' && /(?:^|\/)namzu(?:\/|$)/.test(normalize(rest)))
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
wordNamesHome(word, variables, cwd, lineExpands) {
|
|
851
|
+
if (word.expands && /\$\{?[!#]?namzu_home(?![a-z0-9_])/i.test(word.value))
|
|
852
|
+
return true;
|
|
853
|
+
for (const spelling of this.spell(word, variables, cwd)) {
|
|
854
|
+
const { known } = spelling;
|
|
855
|
+
// Anywhere in the word: `--config=/…/.namzu/x`, `-f/…`.
|
|
856
|
+
if (containsPath(normalize(known), this.home))
|
|
857
|
+
return true;
|
|
858
|
+
const bases = isAbsolute(known) ? [''] : cwd.dirs;
|
|
859
|
+
for (const base of bases) {
|
|
860
|
+
const path = base === '' ? normalize(known) : join(base, known);
|
|
861
|
+
if (spelling.stop === 'end') {
|
|
862
|
+
if (this.inside(path))
|
|
863
|
+
return true;
|
|
864
|
+
// A piece of the path, to be finished by an expansion elsewhere
|
|
865
|
+
// in the line: `for p in ~/.nam; do ls ${p}zu; done`.
|
|
866
|
+
if (lineExpands &&
|
|
867
|
+
path !== '/' &&
|
|
868
|
+
this.home.startsWith(path) &&
|
|
869
|
+
this.home[path.length] !== '/')
|
|
870
|
+
return true;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
// Stopped at an unknown: does the known part lead into NAMZU_HOME?
|
|
874
|
+
const slash = known.lastIndexOf('/');
|
|
875
|
+
const partial = known.slice(slash + 1);
|
|
876
|
+
const dir = slash < 0
|
|
877
|
+
? base === ''
|
|
878
|
+
? null
|
|
879
|
+
: base
|
|
880
|
+
: base === ''
|
|
881
|
+
? normalize(known.slice(0, slash + 1) || '/')
|
|
882
|
+
: join(base, known.slice(0, slash + 1));
|
|
883
|
+
if (dir === null)
|
|
884
|
+
continue;
|
|
885
|
+
if (this.inside(dir))
|
|
886
|
+
return true;
|
|
887
|
+
const next = this.belowAncestor(dir);
|
|
888
|
+
if (next === null)
|
|
889
|
+
continue;
|
|
890
|
+
if (partial === '') {
|
|
891
|
+
// A glob does not match a leading dot unless it spells one.
|
|
892
|
+
if (spelling.stop === 'glob' && next.startsWith('.'))
|
|
893
|
+
continue;
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
if (next.startsWith(partial))
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
if (spelling.stop !== 'end' &&
|
|
900
|
+
this.homeCore.length >= 3 &&
|
|
901
|
+
spelling.rest.includes(this.homeCore))
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** `NAME=value`, `NAME+=value` or `--opt=value`: the value as a word of its own. */
|
|
908
|
+
function assignedValue(word) {
|
|
909
|
+
// An assignment's name and `=` are never quoted, so both spellings start
|
|
910
|
+
// with them.
|
|
911
|
+
const text = word.text.replace(/\\\n/g, '');
|
|
912
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)(\+?)=/.exec(text);
|
|
913
|
+
if (!match || !word.value.startsWith(match[0]))
|
|
914
|
+
return null;
|
|
915
|
+
return {
|
|
916
|
+
name: match[1],
|
|
917
|
+
append: match[2] === '+',
|
|
918
|
+
word: {
|
|
919
|
+
text: text.slice(match[0].length),
|
|
920
|
+
value: word.value.slice(match[0].length),
|
|
921
|
+
expands: word.expands,
|
|
922
|
+
quoted: word.quoted,
|
|
923
|
+
},
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** `$LOCALAPPDATA`'s values: the profile root's parent, and whatever the line assigns it. */
|
|
927
|
+
function localAppData(assigned) {
|
|
928
|
+
return {
|
|
929
|
+
values: [LOCAL_APP_DATA, ...(assigned?.values ?? [])],
|
|
930
|
+
unknown: assigned?.unknown ?? false,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* The Windows browser's profiles in free text: a path through
|
|
935
|
+
* `AppData/Local/namzu` or `%LOCALAPPDATA%\namzu`, in any letter case, either
|
|
936
|
+
* slash; and again with quotes and backslashes dropped, as a shell would.
|
|
937
|
+
*/
|
|
938
|
+
export function textNamesProfile(text) {
|
|
939
|
+
const plain = lower(text);
|
|
940
|
+
const unquoted = plain
|
|
941
|
+
.replace(/\\\n/g, '')
|
|
942
|
+
.replace(/\$(?=["'])/g, '')
|
|
943
|
+
.replace(/["']/g, '');
|
|
944
|
+
for (const variant of [plain, unquoted])
|
|
945
|
+
if (namesProfile(normalize(spellLocalAppData(variant))))
|
|
946
|
+
return true;
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
/** Commands that export a variable, or print the environment. */
|
|
950
|
+
const EXPORTING = new Set([
|
|
951
|
+
'export',
|
|
952
|
+
'declare',
|
|
953
|
+
'typeset',
|
|
954
|
+
'local',
|
|
955
|
+
'readonly',
|
|
956
|
+
'set',
|
|
957
|
+
'env',
|
|
958
|
+
'printenv',
|
|
959
|
+
]);
|
|
960
|
+
/** Whether a redirection names a file: not a here-document's delimiter. */
|
|
961
|
+
function redirectsTo(redirection) {
|
|
962
|
+
return redirection.operator !== '<<' && redirection.operator !== '<<-';
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Where a relative path may start, command by command: the job's folder and
|
|
966
|
+
* every directory a `cd` or `pushd` earlier in the line names, from any of
|
|
967
|
+
* those (a `cd` in a subshell or a pipeline is counted too: broader, never
|
|
968
|
+
* narrower). Unknown once a `cd` goes somewhere the line does not spell out.
|
|
969
|
+
*/
|
|
970
|
+
class Directories {
|
|
971
|
+
dirs;
|
|
972
|
+
unknown = false;
|
|
973
|
+
constructor(folders) {
|
|
974
|
+
this.dirs = new Set(folders);
|
|
975
|
+
}
|
|
976
|
+
/** Every directory known to be possible, and whether others are. */
|
|
977
|
+
current() {
|
|
978
|
+
return { dirs: [...this.dirs], unknown: this.unknown };
|
|
979
|
+
}
|
|
980
|
+
after(command, spell, userHome) {
|
|
981
|
+
const words = command.words.slice(command.assignments);
|
|
982
|
+
let at = 0;
|
|
983
|
+
while (at < words.length &&
|
|
984
|
+
!words[at]?.expands &&
|
|
985
|
+
['builtin', 'command'].includes(words[at]?.value ?? ''))
|
|
986
|
+
at++;
|
|
987
|
+
const head = words[at];
|
|
988
|
+
if (!head || head.expands)
|
|
989
|
+
return;
|
|
990
|
+
if (head.value === 'popd') {
|
|
991
|
+
this.unknown = true;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (head.value !== 'cd' && head.value !== 'pushd')
|
|
995
|
+
return;
|
|
996
|
+
let target = words.slice(at + 1).find((w) => w.expands || !/^-[LPe@]+$/.test(w.value));
|
|
997
|
+
if (target && !target.expands && target.value === '--')
|
|
998
|
+
target = words[words.indexOf(target) + 1];
|
|
999
|
+
if (!target) {
|
|
1000
|
+
this.dirs.add(userHome);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (!target.expands && (target.value === '-' || /^[+-]\d+$/.test(target.value))) {
|
|
1004
|
+
this.unknown = true;
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const from = [...this.dirs];
|
|
1008
|
+
for (const spelling of spell(target)) {
|
|
1009
|
+
if (spelling.stop !== 'end') {
|
|
1010
|
+
this.unknown = true;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
for (const dir of isAbsolute(spelling.known) ? ['/'] : from) {
|
|
1014
|
+
if (this.dirs.size >= 16) {
|
|
1015
|
+
this.unknown = true;
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
this.dirs.add(join(dir, spelling.known));
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/** How many levels of text-inside-text the tripwire reads. */
|
|
1024
|
+
const MAX_TEXT_DEPTH = 3;
|
|
1025
|
+
/** Every string in the line a program could run as a command line. */
|
|
1026
|
+
function fedTexts(readings) {
|
|
1027
|
+
const out = new Set();
|
|
1028
|
+
for (const reading of readings) {
|
|
1029
|
+
for (const command of reading.commands)
|
|
1030
|
+
for (const word of command.words) {
|
|
1031
|
+
const text = word.expands ? unlocale(word.value) : word.value;
|
|
1032
|
+
if (/\s|;|&|\|/.test(text))
|
|
1033
|
+
out.add(text);
|
|
1034
|
+
}
|
|
1035
|
+
for (const redirection of reading.redirections) {
|
|
1036
|
+
if (redirection.body !== undefined)
|
|
1037
|
+
out.add(redirection.body);
|
|
1038
|
+
if (redirection.operator === '<<<')
|
|
1039
|
+
out.add(redirection.target.value);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return [...out];
|
|
1043
|
+
}
|
|
1044
|
+
/** A command the lexer listed whose name or payload it could not read. */
|
|
1045
|
+
function unreadCommand(command) {
|
|
1046
|
+
const head = command.words[command.assignments];
|
|
1047
|
+
if (head?.expands)
|
|
1048
|
+
return true;
|
|
1049
|
+
return runsUnreadText(command);
|
|
1050
|
+
}
|
|
1051
|
+
//# sourceMappingURL=floor.js.map
|