refactor: remove packages/shared, deduplicate with CLI shared (#2257)

* refactor: remove packages/shared, deduplicate with packages/cli/src/shared

packages/shared duplicated packages/cli/src/shared (parse.ts, result.ts,
type-guards.ts) with the CLI never importing from the shared package.
The only consumer was .claude/skills/setup-spa, which now imports directly
from packages/cli/src/shared via relative paths.

- Delete packages/shared entirely
- Update setup-spa imports to use relative paths to CLI shared
- Remove @openrouter/spawn-shared workspace dependency from setup-spa
- Update CLAUDE.md and type-safety.md references

Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove packages/shared from lint workflow, fix import sorting

The Biome Lint CI step referenced packages/shared/src/ which no longer
exists after this PR removes the package. Also fix import ordering in
setup-spa files to satisfy Biome's organizeImports rule.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address Devin review — update stale packages/shared references

- Update type-safety.md line 67: packages/shared/src/parse.ts → packages/cli/src/shared/parse.ts
- Update install.ps1 sparse-checkout: remove packages/shared reference

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-06 18:58:42 -08:00 committed by GitHub
parent 66f0aebebb
commit 3a1de9d4cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 12 additions and 170 deletions

View file

@ -64,7 +64,7 @@ If multiple modules validate the same shape, extract the schema to a shared file
Shared schema locations:
- `.claude/scripts/schemas.ts` — hook stdin payload schemas
- `packages/shared/src/parse.ts` — `parseJsonWith(text, schema)` and `parseJsonObj(text)`
- `packages/cli/src/shared/parse.ts` — `parseJsonWith(text, schema)` and `parseJsonObj(text)`
### For test mocks — use proper Response objects instead of `as any`:
```typescript
@ -83,5 +83,5 @@ global.fetch = mock(() => Promise.resolve(new Response("Error", { status: 500 })
```
### Shared utilities
- `packages/shared/src/parse.ts` — `parseJsonWith(text, schema)` and `parseJsonObj(text)`
- `packages/shared/src/type-guards.ts` — `isString`, `isNumber`, `hasStatus`, `hasMessage`
- `packages/cli/src/shared/parse.ts` — `parseJsonWith(text, schema)` and `parseJsonObj(text)`
- `packages/cli/src/shared/type-guards.ts` — `isString`, `isNumber`, `hasStatus`, `hasMessage`

View file

@ -1,13 +1,14 @@
// SPA helpers — pure functions for parsing Claude Code stream events,
// Slack formatting, state management, and file download/cleanup.
import type { Result } from "@openrouter/spawn-shared";
import type { Result } from "../../../packages/cli/src/shared/result";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { Err, isString, Ok, toRecord } from "@openrouter/spawn-shared";
import { slackifyMarkdown } from "slackify-markdown";
import * as v from "valibot";
import { Err, Ok } from "../../../packages/cli/src/shared/result";
import { isString, toRecord } from "../../../packages/cli/src/shared/type-guards";
// #region State

View file

@ -4,9 +4,9 @@
import type { ContextBlock, KnownBlock, SectionBlock } from "@slack/bolt";
import type { State, ToolCall } from "./helpers";
import { isString, toRecord } from "@openrouter/spawn-shared";
import { App } from "@slack/bolt";
import * as v from "valibot";
import { isString, toRecord } from "../../../packages/cli/src/shared/type-guards";
import {
addMapping,
downloadSlackFile,

View file

@ -6,7 +6,6 @@
"start": "bun run main.ts"
},
"dependencies": {
"@openrouter/spawn-shared": "workspace:*",
"@slack/bolt": "4.6.0",
"slackify-markdown": "^5.0.0",
"valibot": "1.2.0"

View file

@ -1,8 +1,8 @@
import type { ToolCall } from "./helpers";
import { afterEach, describe, expect, it, mock } from "bun:test";
import { toRecord } from "@openrouter/spawn-shared";
import streamEvents from "../../../fixtures/claude-code/stream-events.json";
import { toRecord } from "../../../packages/cli/src/shared/type-guards";
import {
downloadSlackFile,
extractToolHint,

View file

@ -50,7 +50,7 @@ jobs:
run: bun install
- name: Run Biome check (all packages)
run: bunx @biomejs/biome check packages/cli/src/ packages/shared/src/ .claude/scripts/ .claude/skills/setup-spa/
run: bunx @biomejs/biome check packages/cli/src/ .claude/scripts/ .claude/skills/setup-spa/
macos-compat:
name: macOS Compatibility

View file

@ -20,10 +20,6 @@ spawn/
src/commands/ # Per-command modules (interactive, list, run, etc.)
src/commands.ts # Compatibility shim → re-exports from commands/
package.json # npm package (@openrouter/spawn)
shared/
src/parse.ts # parseJsonWith(text, schema) and parseJsonObj(text)
src/type-guards.ts # isString, isNumber, hasStatus, hasMessage
package.json # npm package (@openrouter/spawn-shared)
sh/
cli/
install.sh # One-liner installer (bun → npm → auto-install bun)

View file

@ -13,7 +13,6 @@
".claude/skills/setup-spa": {
"name": "spawn-slack-bot",
"dependencies": {
"@openrouter/spawn-shared": "workspace:*",
"@slack/bolt": "4.6.0",
"slackify-markdown": "^5.0.0",
"valibot": "1.2.0",
@ -21,7 +20,7 @@
},
"packages/cli": {
"name": "@openrouter/spawn",
"version": "0.12.14",
"version": "0.15.3",
"bin": {
"spawn": "cli.js",
},
@ -35,13 +34,6 @@
"@types/bun": "1.3.8",
},
},
"packages/shared": {
"name": "@openrouter/spawn-shared",
"version": "0.1.1",
"dependencies": {
"valibot": "1.2.0",
},
},
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.4.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.3", "@biomejs/cli-darwin-x64": "2.4.3", "@biomejs/cli-linux-arm64": "2.4.3", "@biomejs/cli-linux-arm64-musl": "2.4.3", "@biomejs/cli-linux-x64": "2.4.3", "@biomejs/cli-linux-x64-musl": "2.4.3", "@biomejs/cli-win32-arm64": "2.4.3", "@biomejs/cli-win32-x64": "2.4.3" }, "bin": { "biome": "bin/biome" } }, "sha512-cBrjf6PNF6yfL8+kcNl85AjiK2YHNsbU0EvDOwiZjBPbMbQ5QcgVGFpjD0O52p8nec5O8NYw7PKw3xUR7fPAkQ=="],
@ -68,8 +60,6 @@
"@openrouter/spawn": ["@openrouter/spawn@workspace:packages/cli"],
"@openrouter/spawn-shared": ["@openrouter/spawn-shared@workspace:packages/shared"],
"@slack/bolt": ["@slack/bolt@4.6.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^3.0.4", "@slack/socket-mode": "^2.0.5", "@slack/types": "^2.18.0", "@slack/web-api": "^7.12.0", "axios": "^1.12.0", "express": "^5.0.0", "path-to-regexp": "^8.1.0", "raw-body": "^3", "tsscmp": "^1.0.6" }, "peerDependencies": { "@types/express": "^5.0.0" } }, "sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ=="],
"@slack/logger": ["@slack/logger@4.0.0", "", { "dependencies": { "@types/node": ">=18.0.0" } }, "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA=="],

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.15.5",
"version": "0.15.6",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -1,15 +0,0 @@
{
"root": false,
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
"extends": ["../../biome.json"],
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
},
"files": {
"ignoreUnknown": false,
"includes": ["src/**/*.ts"]
}
}

View file

@ -1,9 +0,0 @@
{
"name": "@openrouter/spawn-shared",
"version": "0.1.1",
"type": "module",
"main": "src/index.ts",
"dependencies": {
"valibot": "1.2.0"
}
}

View file

@ -1,3 +0,0 @@
export { parseJsonObj, parseJsonWith } from "./parse";
export { Err, Ok, type Result } from "./result";
export { hasMessage, hasStatus, isNumber, isString, toObjectArray, toRecord } from "./type-guards";

View file

@ -1,35 +0,0 @@
// shared/parse.ts — Schema-validated JSON parsing (replaces unsafe `as` casts)
import * as v from "valibot";
/**
* Parse a JSON string and validate it against a valibot schema.
* Returns the validated value, or null if parsing/validation fails.
*/
export function parseJsonWith<T extends v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>>(
text: string,
schema: T,
): v.InferOutput<T> | null {
try {
return v.parse(schema, JSON.parse(text));
} catch {
return null;
}
}
/**
* Parse a JSON string and return it as a Record<string, unknown> or null.
* Rejects non-object results (arrays, primitives).
* Use for API responses that are always a JSON object.
*/
export function parseJsonObj(text: string): Record<string, unknown> | null {
try {
const val = JSON.parse(text);
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
return val;
}
return null;
} catch {
return null;
}
}

View file

@ -1,24 +0,0 @@
// shared/result.ts — Lightweight Result monad for retry-aware error handling.
//
// Returning Err() signals a retryable failure; throwing signals a non-retryable one.
// Used with withRetry() so callers decide at the point of failure whether an error
// is retryable (return Err) or fatal (throw), instead of relying on brittle
// error-message pattern matching after the fact.
export type Result<T> =
| {
ok: true;
data: T;
}
| {
ok: false;
error: Error;
};
export const Ok = <T>(data: T): Result<T> => ({
ok: true,
data,
});
export const Err = <T>(error: Error): Result<T> => ({
ok: false,
error,
});

View file

@ -1,44 +0,0 @@
// shared/type-guards.ts — Runtime type guards (replaces unsafe `as` casts on non-API values)
export function isString(val: unknown): val is string {
return typeof val === "string";
}
export function isNumber(val: unknown): val is number {
return typeof val === "number";
}
export function hasStatus(err: unknown): err is {
status: number;
} {
return err !== null && typeof err === "object" && "status" in err && typeof err.status === "number";
}
export function hasMessage(err: unknown): err is {
message: string;
} {
return err !== null && typeof err === "object" && "message" in err && typeof err.message === "string";
}
/**
* Safely narrow an unknown value to a Record<string, unknown> or return null.
*/
export function toRecord(val: unknown): Record<string, unknown> | null {
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
return val satisfies Record<string, unknown>;
}
return null;
}
/**
* Safely narrow an unknown value to an array of Record<string, unknown>.
* Filters out non-object items.
*/
export function toObjectArray(val: unknown): Record<string, unknown>[] {
if (!Array.isArray(val)) {
return [];
}
return val.filter(
(item): item is Record<string, unknown> => item !== null && typeof item === "object" && !Array.isArray(item),
);
}

View file

@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src"]
}

View file

@ -93,7 +93,7 @@ function Install-SpawnCli {
git clone --depth 1 --filter=blob:none --sparse `
"https://github.com/$SPAWN_REPO.git" $repoDir 2>$null
Push-Location $repoDir
git sparse-checkout set packages/cli packages/shared 2>$null
git sparse-checkout set packages/cli 2>$null
Pop-Location
Move-Item (Join-Path $repoDir "packages" "cli") $cliDir
Remove-Item $repoDir -Recurse -Force -ErrorAction SilentlyContinue