Skip to content

Commit da4cb2b

Browse files
committed
build: make web an npm workspace, with one lockfile and both runtimes working
`web/` had a tsconfig and a vite config but no manifest, so its build tooling — vite, panda, preact, @types/react-dom — sat in the root manifest advertising itself as a dependency of a launcher that never loads any of it. It is now `@swisscode/web`, private, owning its own devDeps. Nothing about the published artifact changes: `files` decides that, not the layout. Still 75 files, 122.8 kB packed. ONE LOCKFILE, AND IT HAS TO BE package-lock.json. Not preference: `bun install` does not read it and resolves fresh — I diffed the resulting trees and found 5 transitive packages disagreeing (lightningcss, postcss, picomatch, @babel/code-frame) while every declared dependency matched. Small, but that is how "works on my machine" starts. And the choice is forced anyway: `bun publish` has no OIDC/provenance equivalent, so npm stays in the release path permanently. `bun.lock` is gitignored so the drift cannot be committed. Bun loses nothing by this. It remains a first-class runtime — `bun run test` passes 770/770 against an npm-installed tree, `bunx swisscode` works, and swisscode runs under bun. It is simply not the installer. Two things this shook out, both fixed rather than worked around: - `npm run --workspace web typecheck` BREAKS UNDER BUN. Bun rewrites `npm run` to `bun run` inside scripts and spells the same idea `--filter`, so the script failed with "Script not found web". The fix is to not need workspace machinery for what is one tsc invocation: `tsc --noEmit -p web/tsconfig.json` works identically everywhere. No package-manager-specific flags in `scripts`. - build.js hard-coded `../node_modules/vite/bin/vite.js`. npm and bun do not agree on where workspace deps land, and either can nest on a version conflict, so it now looks in both `web/node_modules` and the root — the "works on my machine" this project runs two runtimes to avoid. Verified end to end under both: npm install, npm ci, bun install, build, full suite, and the packed artifact. Signed-off-by: jellologic <31935831+jellologic@users.noreply.github.com>
1 parent 24aaba2 commit da4cb2b

6 files changed

Lines changed: 85 additions & 19 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@ web/styled-system/
88
# Playwright MCP writes screenshots here during manual verification
99
.playwright-mcp/
1010
profiles.png
11+
12+
# Bun resolves fresh rather than reading package-lock.json, so a committed
13+
# bun.lock would drift from it silently — measured at 5 transitive packages.
14+
# package-lock.json is canonical: `npm ci` gates CI, and npm trusted publishing
15+
# (OIDC + provenance) keeps the npm CLI in the release path permanently.
16+
# Bun stays a first-class RUNTIME — `bun run`, `bunx` — just not the installer.
17+
bun.lock
18+
bun.lockb

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ runtime assertions and is checked by `tsc`. `npm run typecheck` runs it.
5353
| `src/composition/**` | Five composition roots: `launch-root` (hot path), `config-root`, `doctor-root`, `ui-root`, `web-root`. |
5454
| `bin/swisscode.js` | Published entry shim. Plain JS, never compiled, imports exactly `../dist/cli.js`. |
5555
| `test/**` | `.ts`, run from source, never compiled, never packed. |
56+
| `web/**` | An npm WORKSPACE (`@swisscode/web`, private). Owns the browser-only devDeps — vite, panda, preact, `@types/react-dom`. Ships as built assets inside the swisscode package, never as its own release. |
5657

5758
## Hard invariants
5859

@@ -196,6 +197,17 @@ neutral `LaunchIntent` is missing something — propose that instead.
196197

197198
## Useful facts
198199

200+
- **`package-lock.json` is the only lockfile. Install with npm.** `bun install`
201+
does not read it and resolves fresh — measured drift of 5 transitive packages
202+
— and `bun publish` has no OIDC/provenance, so the npm CLI is permanently in
203+
the release path (`publish.yml`). `bun.lock` is gitignored.
204+
- **Bun is a first-class RUNTIME, not the installer.** `bun run <script>`,
205+
`bunx swisscode` and running swisscode under bun all work against an
206+
npm-installed tree, and CI-equivalent `bun run test` passes. Keep it that way:
207+
no package-manager-specific flags in `scripts`. `npm run --workspace web x`
208+
breaks under bun, which rewrites `npm run` to `bun run` inside scripts and
209+
spells the same idea `--filter`. Plain invocations (`tsc -p web/tsconfig.json`)
210+
work everywhere.
199211
- Config: `~/.config/swisscode/config.json` (honours `XDG_CONFIG_HOME`), `0600`
200212
in a `0700` directory, written atomically. v1 configs migrate on read.
201213
- Env vars swisscode reads: `SWISSCODE_CLAUDE_BIN`, `SWISSCODE_KILO_BIN`,

build.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,28 @@
99
// allowed to reach.
1010
import { execFileSync } from 'node:child_process'
1111
import { existsSync, rmSync } from 'node:fs'
12-
import { join } from 'node:path'
12+
import { join, resolve } from 'node:path'
1313
import * as esbuild from 'esbuild'
1414

1515
const TSC = 'node_modules/typescript/bin/tsc'
16-
const PANDA = '../node_modules/@pandacss/dev/bin.js'
17-
const VITE = '../node_modules/vite/bin/vite.js'
16+
17+
/**
18+
* Find a workspace tool, WHEREVER THE INSTALLER PUT IT.
19+
*
20+
* `web` is a workspace now, and npm and bun do not agree on where its
21+
* dependencies land: npm usually hoists them to the root `node_modules`, bun
22+
* may leave them in `web/node_modules`, and either can change its mind when a
23+
* version conflict forces nesting. Hard-coding one path made the build work
24+
* under one package manager and fail under the other, which is exactly the
25+
* "works on my machine" this project runs two runtimes to avoid.
26+
*/
27+
function workspaceTool(relative) {
28+
for (const base of ['web/node_modules', 'node_modules']) {
29+
const candidate = join(base, relative)
30+
if (existsSync(candidate)) return resolve(candidate)
31+
}
32+
return null
33+
}
1834

1935
// Stale output is worse than no output: a deleted module would otherwise linger
2036
// in dist/ and keep resolving.
@@ -63,16 +79,18 @@ await esbuild.build({
6379
// rather than 404ing.
6480
const webRoot = 'web'
6581
let webBuilt = false
66-
if (existsSync(join(webRoot, 'node_modules', '.bin', 'vite')) || existsSync('node_modules/vite')) {
82+
const panda = workspaceTool('@pandacss/dev/bin.js')
83+
const vite = workspaceTool('vite/bin/vite.js')
84+
if (panda && vite) {
6785
// Panda is CODEGEN, and it has to run before Vite: the generated
6886
// styled-system/ is what the app imports, and its PostCSS plugin is what
6987
// fills the @layer declarations. Skipping it produces a build that succeeds
7088
// and a page that renders completely unstyled.
71-
execFileSync(process.execPath, [PANDA, 'codegen', '--config', 'panda.config.ts'], {
89+
execFileSync(process.execPath, [panda, 'codegen', '--config', 'panda.config.ts'], {
7290
cwd: webRoot,
7391
stdio: 'inherit',
7492
})
75-
execFileSync(process.execPath, [VITE, 'build'], { cwd: webRoot, stdio: 'inherit' })
93+
execFileSync(process.execPath, [vite, 'build'], { cwd: webRoot, stdio: 'inherit' })
7694
webBuilt = true
7795
} else {
7896
console.log('skipped dist/web (frontend toolchain not installed)')

package-lock.json

Lines changed: 19 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "swisscode",
33
"version": "0.4.0",
4-
"description": "Drop-in launcher for Claude Code, Kilo & OpenCode. Run multiple Claude Pro/Max accounts, check usage limits and switch accounts without /login or point any coding CLI at Ollama (local, no key), OpenRouter, z.ai/GLM, DeepSeek & Qwen. No proxy, no daemon.",
4+
"description": "Drop-in launcher for Claude Code, Kilo & OpenCode. Run multiple Claude Pro/Max accounts, check usage limits and switch accounts without /login \u2014 or point any coding CLI at Ollama (local, no key), OpenRouter, z.ai/GLM, DeepSeek & Qwen. No proxy, no daemon.",
55
"type": "module",
66
"bin": {
77
"swisscode": "./bin/swisscode.js"
@@ -14,6 +14,9 @@
1414
"dist",
1515
"README.md"
1616
],
17+
"workspaces": [
18+
"web"
19+
],
1720
"scripts": {
1821
"build": "node build.js",
1922
"dev": "node build.js && tsc -p tsconfig.build.json --watch --preserveWatchOutput",
@@ -75,15 +78,10 @@
7578
"react": "^19.2.0"
7679
},
7780
"devDependencies": {
78-
"@pandacss/dev": "^1.11.4",
7981
"@types/node": "^22.20.1",
8082
"@types/react": "^19.2.17",
81-
"@types/react-dom": "^19.2.3",
82-
"@vitejs/plugin-react": "^5.2.0",
8383
"esbuild": "^0.28.1",
8484
"ink-testing-library": "^4.0.0",
85-
"preact": "^10.29.7",
86-
"typescript": "^7.0.2",
87-
"vite": "^8.1.5"
85+
"typescript": "^7.0.2"
8886
}
8987
}

web/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@swisscode/web",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"description": "The browser UI for swisscode. Private: it ships as built assets inside the swisscode package, never as its own release.",
7+
"scripts": {
8+
"typecheck": "tsc --noEmit -p tsconfig.json"
9+
},
10+
"devDependencies": {
11+
"@pandacss/dev": "^1.11.4",
12+
"@vitejs/plugin-react": "^5.2.0",
13+
"vite": "^8.1.5",
14+
"preact": "^10.29.7",
15+
"@types/react-dom": "^19.2.3"
16+
}
17+
}

0 commit comments

Comments
 (0)