From 006bbe6b0c55a66a459ea7bfc9d11083209dc2e4 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 12:43:37 +0000 Subject: [PATCH] Move the action from Deno to Node.js and Vite+ (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Moves `commons/bot-agents` off Deno. The action's TypeScript now runs directly on the job image's Node.js (`node src/.ts`, using Node's built-in type stripping), with no build step and still no runtime dependency. Repository checks move to Vite+ (`vp check`, `vp test`). ## What changed - **Runtime.** `action.yml` runs `prepare.ts`, `run.ts`, and `post.ts` with `node` instead of `deno run -A`. `package.json` (`"type": "module"`, `engines.node >=24.2` for `import.meta.main`) is now copied into the root-owned source copy by `scripts/protect-source.sh`. - **Profiles.** `profiles.yml` → `profiles.json`, with the same content (encrypted values unchanged), parsed with `JSON.parse`. This drops the only runtime dependency (`@std/yaml`). - **Deno APIs → Node.** - `src/command.ts` wraps `child_process.spawn` with the old `Deno.Command` shapes: `output()`/`spawn()`, `clearEnv`, `uid`/`gid`, and Deno's stdio defaults (stdin inherited; stdout/stderr piped for `output()`, inherited for `spawn()`). A signal exit maps to `128 + signo`. Status settles on process exit. - `src/errors.ts` maps `NotFound` to `ENOENT`/`ESRCH`. - fs, path, stream, and readline calls are mapped one for one. `makeTempFile` becomes an exclusive `wx` 0600 create. - **Size refactors.** These are needed for Oxlint's default limits. There are no behavior changes and no overrides for source files. - `run.ts` is split into `run-context.ts`, `agents.ts`, and `agent-{claude,codex,opencode}.ts`. - `post.ts` is split into `post.ts` and `post-artifacts.ts`. - `parseCredentials`, `readOutcome`, and `postRuntime` are broken into helpers. - Loops that awaited sequentially became recursion or a `reduce` chain, so they stay sequential. `/proc` status reads became `Promise.all`. - `src/json.ts` adds type guards in place of `as` casts. - **Tests.** The 58 `Deno.test` cases were converted to Vitest with the codemod, and its TODOs were finished by hand. - Unit tests are `src/*.test.ts`; the repository-level workflow test is `tests/workflow.test.ts`. - Root-only tests are now `test.skipIf(!IS_ROOT)`, so they show as skipped instead of silently passing. - New `tests/entrypoints.test.ts` runs each phase under real Node to prove its whole import graph loads. - **Config.** The only config files are `package.json`, `tsconfig.json`, and `vite.config.ts`, plus `package-lock.json`. - Lint uses Oxlint correctness/suspicious/pedantic/perf at error, with default limits and type-aware checks. - `scripts/project-lint.ts` holds the rule list. It maps Deno's `recommended` rules that fall outside those categories, adds the template's `id-match`, `no-disable-comments`, and `no-non-ts-imports`, and maps Deno's `no-window`/`no-process-global`/`no-node-globals` to `no-restricted-globals` (so `process`/`Buffer` come from `node:` imports). - **CI.** `.gitea/workflows/check.yml` runs `node --version`, `npm ci`, `npx vp check`, and `npx vp test` in the default runner image. - **Formatting.** Oxfmt formatting is a separate first commit, so the second commit shows only the semantic diff. - **AGENTS.md** is updated for the new toolchain. ## Deviations / risks - **Live `@main` switch.** Six consumer repos use `commons/bot-agents@main`, so merging switches all of them at once. Suggested before merging: tag the current `main` (e.g. `deno-final`) as a rollback target, and canary this branch from one consumer (`uses: …/commons/bot-agents@node-migration`) on a throwaway PR. - **Node version.** `post.ts` relies on `import.meta.main`, which needs Node ≥ 24.2. DANNYEC's cached `node:24-bookworm` minor is unknown (`forcePull=false`), and the CI job now prints `node --version` to confirm it. - **Deno stays in the global install.** `deno` remains in the action's global npm install because agents still verify Deno-based consumers (memona, memona-home). Remove it once those repos move. - **`prefer-readonly-parameter-types` is off.** This pedantic rule is turned off, as in the explicit-types template and the app repos, because Node/web types (`Response`, `Stats`, `RequestInit`) can never satisfy it. `scripts/**` has one narrow override (`no-unsafe-enum-comparison`) for AST node `type` comparisons. - **Stricter validation of malformed input.** For valid input, behavior is unchanged. A few validations are stricter where the old code would have used a malformed value: - Gitea `/user` and comment responses are checked. - The runtime manifest and the outcome's profile type are checked against known values. - Some type-check errors are now `TypeError` (same messages). - **@cunarist lint plugins.** "CI fails until @cunarist/eslint-plugin-* are published" does **not** apply here. This repo's Deno setup enabled no plugin, so it depends on none of `@cunarist/eslint-plugin-*`, and CI can pass as is. ## Verification - `vp check`: formatting, lint (0 warnings), and type check all pass. - `vp test` as a normal user: 56 passed, 5 root-only skipped. - `vp test` as root in `node:24-bookworm` (Node v24.22.0, the CI image): all 61 passed, including the UID isolation, process cleanup, and `prepare.ts` cleanup tests. - End-to-end smoke as root in `node:24-bookworm`, with fake `codex`/`claude`/`npx` binaries and a fake Gitea, through `protect-source.sh` → `prepare.ts` → `run.ts` → `post.ts`: - Codex hit "usage limit" and fell through to Claude. - Agents ran as UIDs 20000/20001 without the master key. - The Codex login was persisted to `/root/.codex/auth.json` (0600). - The PR review with an inline comment was posted with the reviewer token. - Runtime credentials were removed and the git author was configured. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://git.capsulizers.com/commons/bot-agents/pulls/20 Reviewed-by: bot Co-authored-by: Danny Kim Co-committed-by: Danny Kim --- .agents/memory/agents-md-and-damem.md | 39 + .agents/memory/app-containers-and-ci.md | 36 + .agents/memory/app-generated-js-as-ts.md | 20 + .agents/memory/app-lint-and-local-rules.md | 41 + .agents/memory/app-repo-specific-decisions.md | 29 + .agents/memory/app-vite-build-output.md | 43 + .agents/memory/bot-agents-lint-opt-outs.md | 20 + .../memory/bot-agents-node-command-shim.md | 25 + .agents/memory/bot-agents-runtime-no-build.md | 31 + .agents/memory/bot-agents-tests-root-only.md | 22 + .../memory/decorators-standard-accessor.md | 44 + .agents/memory/deno-to-node-migration.md | 45 + .agents/memory/git-pr-workflow.md | 52 + .agents/memory/icons-memona-style.md | 43 + .agents/memory/imports-aliases-barrels.md | 48 + .agents/memory/lint-config-shape.md | 47 + .agents/memory/lint-no-disable-comments.md | 30 + .agents/memory/lint-overrides-policy.md | 45 + .agents/memory/lint-parity-never-shrinks.md | 46 + .agents/memory/lit-analyzer-ts6.md | 40 + .agents/memory/naming-id-match.md | 48 + .agents/memory/package-json-shape.md | 51 + .agents/memory/project-lint-plugin.md | 55 + .agents/memory/publishing-ci-only.md | 37 + .agents/memory/tests-layout-vitest.md | 36 + .agents/memory/toolchain-node-vite-plus.md | 53 + .agents/memory/ts-only-files-and-imports.md | 48 + .agents/memory/working-with-the-user.md | 53 + .agents/scratch/.gitignore | 1 + .agents/skills/migrate-deno-repo/SKILL.md | 69 + .agents/skills/review-cycle/SKILL.md | 40 + .gitea/workflows/check.yml | 17 +- .gitignore | 2 + AGENTS.md | 29 +- action.yml | 10 +- package-lock.json | 3083 +++++++++++++++++ package.json | 16 + profiles.json | 19 + profiles.yml | 10 - scripts/project-lint.ts | 232 ++ scripts/protect-source.sh | 3 +- src/agent-claude.ts | 119 + src/agent-codex.ts | 174 + src/agent-opencode.ts | 46 + src/agents.ts | 30 + src/auth.test.ts | 178 + src/auth.ts | 123 +- src/auth_test.ts | 203 -- src/command.ts | 160 + src/credentials.test.ts | 68 + src/credentials.ts | 148 +- src/credentials_test.ts | 92 - src/errors.ts | 8 + src/isolation.test.ts | 112 + src/isolation.ts | 111 +- src/isolation_test.ts | 134 - src/json.ts | 12 + src/opencode.test.ts | 103 + src/opencode.ts | 52 +- src/opencode_test.ts | 120 - src/post-artifacts.ts | 114 + src/post.test.ts | 296 ++ src/post.ts | 355 +- src/post_test.ts | 296 -- src/prepare.ts | 19 +- src/{profiles_test.ts => profiles.test.ts} | 176 +- src/profiles.ts | 146 +- src/run-context.ts | 79 + src/run.ts | 499 +-- src/runtime.test.ts | 105 + src/runtime.ts | 113 +- src/runtime_test.ts | 108 - src/testing.ts | 23 + src/workflow_test.ts | 188 - tests/entrypoints.test.ts | 27 + tests/workflow.test.ts | 175 + tsconfig.json | 22 + vite.config.ts | 25 + 78 files changed, 7186 insertions(+), 2231 deletions(-) create mode 100644 .agents/memory/agents-md-and-damem.md create mode 100644 .agents/memory/app-containers-and-ci.md create mode 100644 .agents/memory/app-generated-js-as-ts.md create mode 100644 .agents/memory/app-lint-and-local-rules.md create mode 100644 .agents/memory/app-repo-specific-decisions.md create mode 100644 .agents/memory/app-vite-build-output.md create mode 100644 .agents/memory/bot-agents-lint-opt-outs.md create mode 100644 .agents/memory/bot-agents-node-command-shim.md create mode 100644 .agents/memory/bot-agents-runtime-no-build.md create mode 100644 .agents/memory/bot-agents-tests-root-only.md create mode 100644 .agents/memory/decorators-standard-accessor.md create mode 100644 .agents/memory/deno-to-node-migration.md create mode 100644 .agents/memory/git-pr-workflow.md create mode 100644 .agents/memory/icons-memona-style.md create mode 100644 .agents/memory/imports-aliases-barrels.md create mode 100644 .agents/memory/lint-config-shape.md create mode 100644 .agents/memory/lint-no-disable-comments.md create mode 100644 .agents/memory/lint-overrides-policy.md create mode 100644 .agents/memory/lint-parity-never-shrinks.md create mode 100644 .agents/memory/lit-analyzer-ts6.md create mode 100644 .agents/memory/naming-id-match.md create mode 100644 .agents/memory/package-json-shape.md create mode 100644 .agents/memory/project-lint-plugin.md create mode 100644 .agents/memory/publishing-ci-only.md create mode 100644 .agents/memory/tests-layout-vitest.md create mode 100644 .agents/memory/toolchain-node-vite-plus.md create mode 100644 .agents/memory/ts-only-files-and-imports.md create mode 100644 .agents/memory/working-with-the-user.md create mode 100644 .agents/scratch/.gitignore create mode 100644 .agents/skills/migrate-deno-repo/SKILL.md create mode 100644 .agents/skills/review-cycle/SKILL.md create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 profiles.json delete mode 100644 profiles.yml create mode 100644 scripts/project-lint.ts create mode 100644 src/agent-claude.ts create mode 100644 src/agent-codex.ts create mode 100644 src/agent-opencode.ts create mode 100644 src/agents.ts create mode 100644 src/auth.test.ts delete mode 100644 src/auth_test.ts create mode 100644 src/command.ts create mode 100644 src/credentials.test.ts delete mode 100644 src/credentials_test.ts create mode 100644 src/errors.ts create mode 100644 src/isolation.test.ts delete mode 100644 src/isolation_test.ts create mode 100644 src/json.ts create mode 100644 src/opencode.test.ts delete mode 100644 src/opencode_test.ts create mode 100644 src/post-artifacts.ts create mode 100644 src/post.test.ts delete mode 100644 src/post_test.ts rename src/{profiles_test.ts => profiles.test.ts} (53%) create mode 100644 src/run-context.ts create mode 100644 src/runtime.test.ts delete mode 100644 src/runtime_test.ts create mode 100644 src/testing.ts delete mode 100644 src/workflow_test.ts create mode 100644 tests/entrypoints.test.ts create mode 100644 tests/workflow.test.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.agents/memory/agents-md-and-damem.md b/.agents/memory/agents-md-and-damem.md new file mode 100644 index 0000000..d2f091c --- /dev/null +++ b/.agents/memory/agents-md-and-damem.md @@ -0,0 +1,39 @@ +--- +description: Every repo has AGENTS.md starting with the damem recall header; durable context lives in .agents/memory (flat, one fact per file) and .agents/skills +--- + +Standing convention (2026-10-12): every repo has an `AGENTS.md` whose first +lines are exactly the damem header (cunarist/damem): + +```markdown +At the start of every new session, run `damem recall` in the terminal and treat +its output as the context for this repository. If the `damem` command is not +found, read [INSTALL.md](https://github.com/cunarist/damem/blob/main/INSTALL.md) +and follow it before reasoning or responding. +``` + +Below it: `# AGENTS.md`, then `## Project` (what the repo is, runtime, +toolchain, where it came from) and `## Rules` (short bullets, each pointing at +a memory where the reason lives). No `CLAUDE.md`, `GEMINI.md`, or other +per-agent files; `AGENTS.md` is the single entry point. + +## `.agents/` layout + +- `memory/*.md`: one fact per file, flat (subdirectories are not listed), + frontmatter with a one-line `description` that states the fact. Link + related notes with double-bracket wiki links using the file name without + `.md`; a link to a missing note fails `damem doctor`. +- `skills//SKILL.md`: frontmatter `name` and `description` (when to use + it), body says how. Helper scripts live next to it and are Python 3.12+. +- `scratch/`: holds a `.gitignore` containing `*`. +- Every memory and `SKILL.md` stays under 8 KB. + +## Keeping it right + +- Update an existing note instead of adding a near-duplicate; delete notes + that turn out wrong. +- Write a note when the user corrects something, states a preference, or + when understanding something took reading many files. +- Run `damem doctor` before finishing and fix what it reports. +- Changing anything outside `.agents/` (source, config, `AGENTS.md`) follows + the normal branch and PR flow ([[git-pr-workflow]]). diff --git a/.agents/memory/app-containers-and-ci.md b/.agents/memory/app-containers-and-ci.md new file mode 100644 index 0000000..58f77cc --- /dev/null +++ b/.agents/memory/app-containers-and-ci.md @@ -0,0 +1,36 @@ +--- +description: Containerfiles build the frontend in a node:24-alpine stage with npm ci and vp build; CI uses setup-vp and runs vp check, vp build, vp test (plus lit-check) +--- + +## Containerfiles + +- The frontend build stage is `docker.io/library/node:24-alpine`, replacing + `denoland/deno:alpine`. Vite+ publishes `linux-x64-musl` binaries, so Alpine + works. (`ghcr.io/voidzero-dev/vite-plus` is a glibc alternative that would + need a pinned digest; not used.) +- The stage copies `package.json` and `package-lock.json` first, runs + `npm ci`, then copies the sources and configs and runs `npx vp build`. +- The runtime stage copies the built `js/` and `css/` directories (not single + files) from the node stage. +- No Deno binary, `deno install`, or `deno task` remains in any stage. + +```dockerfile +FROM docker.io/library/node:24-alpine AS frontend +WORKDIR /src/frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npx vp build +``` + +## CI + +- GitHub repos (temeddix): `.github/workflows`. Capsulizers repos (Gitea): + `.gitea/workflows`. Don't move a repo's CI between hosts. +- Install with `voidzero-dev/setup-vp`, then run `vp check`, `vp build`, and + `vp test`; Lit repos also run `npm run lit-check` + ([[lit-analyzer-ts6]]). +- Repos that ran CI on a self-hosted runner keep it. +- Until `@cunarist/eslint-plugin-*` are on npm, `npm ci` fails in CI. That is + expected and stated in the PR; run `npm install` once they are published + and commit the refreshed lockfile. diff --git a/.agents/memory/app-generated-js-as-ts.md b/.agents/memory/app-generated-js-as-ts.md new file mode 100644 index 0000000..39b0b4a --- /dev/null +++ b/.agents/memory/app-generated-js-as-ts.md @@ -0,0 +1,20 @@ +--- +description: Code that must ship as standalone JS (memona framed_find.js, brain stream-recovery.js) is written as .ts, compiled at build time, and its output gitignored +--- + +Some apps ship plain JavaScript that runs outside the main bundle: memona's +`framed_find.js` and brain's `stream-recovery.js`. Under the TS-only rule +([[ts-only-files-and-imports]]) they are handled like this (2026-10-16): + +- The source is a `.ts` file, linted and type-checked like everything else. +- The `.js` is produced at build time by the existing build (a Vite input or + a build step in `vite.config.ts` or `scripts/`), never committed. +- The output path is gitignored and, if it lands in a linted folder, listed + in `lint.ignorePatterns`; never in `overrides`, never with disable comments. +- Whatever loads the file (Rust `include_str!`, a container `COPY`, an HTML + `