Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- run: npm run catalog:check
- run: npm run typecheck
- run: npm run lint
- run: npm run test:extractor
- run: npm test
- run: npm run build

cli:
Expand Down
8 changes: 4 additions & 4 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ Inter loaded from `rsms.me/inter` (self-hosted); `ss03` stylistic set enabled si
### Shape

```
--radius-card 12px List rows, modals, stat cards
--radius-control 8px Buttons, inputs, list-row icons
--radius-tile 6px Icon tiles, list-row check
--radius-pill 9999px Primary CTAs (1Password pattern)
--radius-card 6px List rows, modals, stat cards, doctor cards
--radius-control 6px Buttons, inputs, sidebar items
--radius-tile 4px Avatars, icon tiles, chips
--radius-pill 9999px Badges + primary pill CTAs only
```

Pill primary buttons + sharp cards is a deliberate tension — primary actions look "elevated" by their roundedness; everything else stays grounded.
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"prebuild": "npm run gen:catalog",
"prebuild:renderer": "npm run gen:catalog",
"catalog:check": "node scripts/check-catalog-stale.mjs",
"test": "npm run build:main && npm run test:unit",
"test:unit": "node scripts/test-extract-base-url.mjs && node scripts/test-doctor.mjs && node scripts/test-versions.mjs && node scripts/test-secrets.mjs && node scripts/test-homebrew.mjs",
"test:extractor": "node scripts/test-extract-base-url.mjs",
"package": "npm run build && electron-builder"
},
Expand All @@ -47,5 +49,6 @@
},
"allowScripts": {
"electron@35.7.5": true
}
},
"packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631"
}
197 changes: 197 additions & 0 deletions scripts/test-doctor.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env node
/**
* Unit tests for src/shared/doctor.ts (via dist build).
*/
import { createRequire } from 'node:module'
import { existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const doctorJs = join(root, 'dist/shared/doctor.js')

if (!existsSync(doctorJs)) {
console.error('dist/shared/doctor.js missing — run npm run build:main first')
process.exit(1)
}

const require = createRequire(import.meta.url)
const { analyzeLibrary, catalogBinaryName } = require(doctorJs)

let failed = 0
function assert(cond, msg) {
if (cond) console.log(` ✓ ${msg}`)
else {
failed++
console.error(` ✗ ${msg}`)
}
}

function nodeEntry() {
return {
id: 'node',
catalogId: 'node',
kind: 'runtime',
name: 'Node.js',
status: 'installed',
version: '26.0.0',
path: '/opt/homebrew/bin/node',
source: 'Homebrew',
packageManager: 'homebrew',
homebrew: 'formula',
primary: true,
installs: [
{
path: '/opt/homebrew/bin/node',
realPath: '/opt/homebrew/Cellar/node/26.0.0/bin/node',
version: '26.0.0',
source: 'Homebrew',
packageManager: 'homebrew',
homebrew: 'formula',
primary: true,
},
],
}
}

// --- catalogBinaryName ---
assert(catalogBinaryName('claude-code') === 'claude', 'claude-code → claude')
assert(catalogBinaryName('opencode') === 'opencode', 'opencode → opencode')
assert(catalogBinaryName('python') === 'python3', 'python → python3')

// --- single install + node present: healthy ---
{
const report = analyzeLibrary([
{
id: 'claude-code',
catalogId: 'claude-code',
kind: 'harness',
name: 'Claude Code',
status: 'installed',
version: '2.1.226',
path: '/opt/homebrew/bin/claude',
source: 'Homebrew Cask',
packageManager: 'homebrew',
homebrew: 'cask',
primary: true,
installs: [
{
path: '/opt/homebrew/bin/claude',
realPath: '/opt/homebrew/Caskroom/claude-code/2.1.226/claude',
version: '2.1.226',
source: 'Homebrew Cask',
packageManager: 'homebrew',
homebrew: 'cask',
primary: true,
},
],
},
nodeEntry(),
])
assert(report.summary.error === 0, 'single install: no errors')
assert(report.summary.warn === 0, 'single install: no warns')
assert(report.findings.some((f) => f.severity === 'ok'), 'single install: healthy finding')
}

// --- multi-install harness version skew → warn + fix actions ---
{
const report = analyzeLibrary([
nodeEntry(),
{
id: 'claude-code#1',
catalogId: 'claude-code',
kind: 'harness',
name: 'Claude Code',
status: 'installed',
version: '2.1.211',
path: '/opt/homebrew/bin/claude',
source: 'Homebrew Cask',
packageManager: 'homebrew',
homebrew: 'cask',
primary: true,
installs: [
{
path: '/opt/homebrew/bin/claude',
realPath: '/a',
version: '2.1.211',
source: 'Homebrew Cask',
packageManager: 'homebrew',
homebrew: 'cask',
primary: true,
},
{
path: '/usr/local/bin/claude',
realPath: '/b',
version: '2.0.76',
source: 'npm',
packageManager: 'npm',
homebrew: null,
primary: false,
},
],
},
])
assert(report.summary.error === 0, 'version skew: not an error')
assert(report.summary.warn >= 1, 'version skew: at least one warn')
const shadow = report.findings.find((f) => f.id === 'shadow:claude-code')
assert(!!shadow, 'version skew: shadow finding exists')
const actions = (shadow?.resolutions ?? []).filter((r) => r.action)
assert(actions.some((r) => r.action?.type === 'upgrade'), 'has upgrade action')
assert(actions.some((r) => r.action?.type === 'reconfigure'), 'has reconfigure action')
assert(actions.some((r) => r.action?.type === 'uninstall'), 'has uninstall action for npm dup')
}

// --- node multi-install is info, not warn ---
{
const report = analyzeLibrary([
{
id: 'node#1',
catalogId: 'node',
kind: 'runtime',
name: 'Node.js',
status: 'installed',
version: '26.7.0',
path: '/opt/homebrew/bin/node',
source: 'Homebrew',
packageManager: 'homebrew',
homebrew: 'formula',
primary: true,
installs: [
{
path: '/opt/homebrew/bin/node',
realPath: '/a',
version: '26.7.0',
source: 'Homebrew',
packageManager: 'homebrew',
homebrew: 'formula',
primary: true,
},
{
path: '/Users/x/.asdf/shims/node',
realPath: '/b',
version: '22.9.0',
source: 'asdf',
packageManager: 'asdf',
homebrew: null,
primary: false,
},
],
},
])
const shadow = report.findings.find((f) => f.id === 'shadow:node')
assert(shadow?.severity === 'info', 'node multi-version is info')
assert(report.summary.error === 0, 'node multi: no errors')
assert(report.summary.warn === 0, 'node multi: no warns (node present)')
}

// --- missing node → warn ---
{
const report = analyzeLibrary([])
assert(report.findings.some((f) => f.id === 'missing:node' && f.severity === 'warn'), 'missing node warns')
}

if (failed > 0) {
console.error(`\n${failed} doctor assertion(s) failed`)
process.exit(1)
}
console.log('\nall doctor cases passed')
84 changes: 84 additions & 0 deletions scripts/test-homebrew.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Unit tests for Homebrew channel detection (via dist build).
*/
import { createRequire } from 'node:module'
import { existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const libraryJs = join(root, 'dist/main/library.js')

if (!existsSync(libraryJs)) {
console.error('dist/main/library.js missing — run npm run build:main first')
process.exit(1)
}

const require = createRequire(import.meta.url)
const { detectHomebrew, sourceLabel, detectPackageManager } = require(libraryJs)

let failed = 0
function assert(cond, msg) {
if (cond) console.log(` ✓ ${msg}`)
else {
failed++
console.error(` ✗ ${msg}`)
}
}

const prefix = '/opt/homebrew'

assert(
detectHomebrew('/opt/homebrew/bin/claude', '/opt/homebrew/Caskroom/claude-code/2.1.211/claude', prefix) === 'cask',
'cask detection',
)
assert(
detectHomebrew('/opt/homebrew/bin/node', '/opt/homebrew/Cellar/node/26.7.0/bin/node', prefix) === 'formula',
'formula detection',
)
assert(
detectHomebrew(
'/opt/homebrew/bin/opencode',
'/opt/homebrew/lib/node_modules/opencode-ai/bin/opencode.exe',
prefix,
) === 'node',
'homebrew node_modules → node channel',
)
assert(
detectHomebrew('/Users/x/.asdf/shims/claude', '/Users/x/.asdf/shims/claude', prefix) === null,
'asdf is not homebrew',
)
assert(
detectHomebrew('/Users/x/.bun/bin/bun', '/Users/x/.bun/bin/bun', prefix) === null,
'bun is not homebrew',
)

assert(
sourceLabel('/opt/homebrew/bin/claude', '/opt/homebrew/Caskroom/claude-code/x/claude', 'cask') === 'Homebrew Cask',
'source label cask',
)
assert(
sourceLabel('/opt/homebrew/bin/opencode', '/opt/homebrew/lib/node_modules/opencode-ai/bin/x', 'node') ===
'npm · Homebrew Node',
'source label npm under brew node',
)

assert(
detectPackageManager('/opt/homebrew/bin/claude', '/opt/homebrew/Caskroom/x', 'cask') === 'homebrew',
'pm homebrew for cask',
)
assert(
detectPackageManager('/opt/homebrew/bin/opencode', '/opt/homebrew/lib/node_modules/x', 'node') === 'npm',
'pm npm for brew-node globals',
)
assert(
detectPackageManager('/Users/x/.bun/bin/bun', '/Users/x/.bun/bin/bun', null) === 'bun',
'pm bun',
)

if (failed > 0) {
console.error(`\n${failed} homebrew assertion(s) failed`)
process.exit(1)
}
console.log('\nall homebrew detection cases passed')
41 changes: 41 additions & 0 deletions scripts/test-secrets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node
/**
* Unit tests for vault secret id helpers.
*/
import { createRequire } from 'node:module'
import { existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const secretsJs = join(root, 'dist/shared/secrets.js')

if (!existsSync(secretsJs)) {
console.error('dist/shared/secrets.js missing — run npm run build:main first')
process.exit(1)
}

const require = createRequire(import.meta.url)
const { secretIdForProvider, providerIdFromSecretId } = require(secretsJs)

let failed = 0
function assert(cond, msg) {
if (cond) console.log(` ✓ ${msg}`)
else {
failed++
console.error(` ✗ ${msg}`)
}
}

assert(secretIdForProvider('anthropic') === 'provider:anthropic:api_key', 'secret id format')
assert(secretIdForProvider('openai') === 'provider:openai:api_key', 'openai secret id')
assert(providerIdFromSecretId('provider:anthropic:api_key') === 'anthropic', 'parse anthropic')
assert(providerIdFromSecretId('provider:openai:api_key') === 'openai', 'parse openai')
assert(providerIdFromSecretId('random') === null, 'reject non-canonical')
assert(providerIdFromSecretId(secretIdForProvider('groq')) === 'groq', 'round-trip')

if (failed > 0) {
console.error(`\n${failed} secrets assertion(s) failed`)
process.exit(1)
}
console.log('\nall secrets cases passed')
Loading
Loading