Move the action from Deno to Node.js and Vite+ (#20)

## Summary

Moves `commons/bot-agents` off Deno. The action's TypeScript now runs directly on the job image's Node.js (`node src/<phase>.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: #20
Reviewed-by: bot <noreply@capsulizers.com>
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #20.
This commit is contained in:
Danny Kim
2026-10-16 12:43:37 +00:00
committed by temeddix
parent f001a71c61
commit 006bbe6b0c
78 changed files with 7186 additions and 2231 deletions
+45
View File
@@ -0,0 +1,45 @@
---
description: Every repo moved from Deno to Node + Vite+ in Oct 2026 because Cloudflare bought Deno; nothing goes back to Deno
---
On 2026-10-12 the user moved every project from Deno to Node.js + Vite+
(`vp`). Cloudflare bought Deno on 2026-10-09; the runtime gets roughly 12
months of maintenance and Deno Deploy shuts down around April 2027. None of the
repos used Deploy or KV, so the move is about the toolchain, not hosting.
Do not reintroduce `deno.json`, `deno.lock`, `Deno.*` APIs, `jsr:` or `npm:`
specifiers, `mod.ts` entries, or `deno task` anywhere, docs and scripts
included.
## Order the migration followed
1. The custom Deno lint plugins were ported to npm first, as
`@cunarist/eslint-plugin-explicit-types`, `-import-check`, and `-lit`.
`cunarist/eslint-plugin-explicit-types` is the template every other repo
copies (config shape, `scripts/project-lint.ts`, AGENTS.md, notes).
2. Then `commons/bot-agents` (six repos use it as an action), then the apps:
home, eximium, stockvibe, brain, memona, memona-home.
3. deno-practice, interview-practice, web-landing, and vemium were left out
(archivable). Old `cunarist/deno-*` repos are archived one at a time, and
only after their `eslint-plugin-*` replacement is published to npm.
## What "migrated" means
- Same behavior. A migration PR changes tooling, not features.
- Lint coverage is at least what Deno enforced: [[lint-parity-never-shrinks]].
- Only three config files and only `.ts`: [[toolchain-node-vite-plus]],
[[ts-only-files-and-imports]].
- Deno.test suites became Vitest: [[tests-layout-vitest]].
The custom plugins' Deno names map like this:
| Deno | npm |
| ----------------------------------------------- | ---------------------------------------- |
| `jsr:@cunarist/deno-lint-plugin-explicit-types` | `@cunarist/eslint-plugin-explicit-types` |
| `jsr:@cunarist/deno-import-check` (`./lint`) | `@cunarist/eslint-plugin-import-check` |
| `jsr:@cunarist/deno-lint-plugin-lit` | `@cunarist/eslint-plugin-lit` |
| `scripts/deno-lint-plugin.ts` (per app) | `scripts/project-lint.ts` (per app) |
Until those packages are on npm, a repo that depends on them fails `npm ci` in
CI; that is expected and stated in the PR, never worked around by committing
tarballs or `file:` dependencies.