mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
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:
parent
66f0aebebb
commit
3a1de9d4cf
17 changed files with 12 additions and 170 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
12
bun.lock
12
bun.lock
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.15.5",
|
||||
"version": "0.15.6",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@openrouter/spawn-shared",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"dependencies": {
|
||||
"valibot": "1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue