dsh-worktree-switch 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -2
- package/cordis.patch.yml +19 -0
- package/icon.svg +8 -0
- package/lib/client.js +1068 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +717 -0
- package/lib/index.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +133 -4
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/host/git.ts","../src/host/naming.ts","../src/host/handlers.ts","../src/host/routes.ts","../src/index.ts"],"sourcesContent":["/**\n * Unconfined Git calls behind the plugin's user-initiated verbs.\n *\n * Creation goes through `ctx.worktrees` (the harness service), which runs its\n * own Git under the calling session's sandbox policy. The list, status, and\n * discard verbs here belong to a person clicking in the UI rather than to a\n * model turn: they inspect and delete the copies the user can see, from a path\n * the same UI just listed. They therefore spawn Git directly, the same way the\n * installed `dsh-worktree-space` plugin does, instead of borrowing a session's\n * policy for an operation no session asked for.\n * @module dsh-worktree-switch/host/git\n */\n\nimport { realpath } from 'node:fs/promises'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-subprocess'\n\n/** Whole-command deadline for one Git call. */\nexport const GIT_TIMEOUT_MS = 20_000\n\n/** Per-stream capture cap; worktree listings and status lines are small. */\nconst MAX_OUTPUT_BYTES = 1_048_576\n\n/**\n * Clear inherited repository selectors and config so the command reads the\n * checkout named by `cwd`, never one inherited from the Host's environment.\n * One entry per key: `undefined` removes the variable for the child.\n */\nconst GIT_ENV: NodeJS.ProcessEnv = Object.fromEntries([\n 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY',\n 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT',\n 'GIT_CEILING_DIRECTORIES', 'GIT_PREFIX', 'GIT_SHALLOW_FILE', 'GIT_GRAFT_FILE',\n 'GIT_REPLACE_REF_BASE', 'GIT_ATTR_SOURCE',\n].map(key => [key, undefined]))\n\n/** One bounded Git invocation's captured result. */\nexport interface GitResult {\n /** Captured stdout. */\n readonly stdout: string\n /** Captured stderr. */\n readonly stderr: string\n /** Process exit status. */\n readonly exitCode: number\n}\n\n/** A Git invocation that exited with a status the caller did not allow. */\nexport class GitFailure extends Error {\n /**\n * @param args - arguments after the executable, for diagnostics.\n * @param result - the captured outcome.\n */\n constructor(readonly args: readonly string[], readonly result: GitResult) {\n const detail = result.stderr.trim() === '' ? result.stdout.trim() : result.stderr.trim()\n super(`git ${args.join(' ')} failed (exit ${result.exitCode})${detail === '' ? '' : `: ${detail}`}`)\n this.name = 'GitFailure'\n }\n}\n\n/** Options for {@link runGit}. */\nexport interface GitOptions {\n /** Caller cancellation, combined with the command deadline. */\n readonly signal?: AbortSignal | undefined\n /** Exit statuses that are not failures; defaults to `[0]`. */\n readonly allowedExitCodes?: readonly number[] | undefined\n /** Override for {@link GIT_TIMEOUT_MS}. */\n readonly timeoutMs?: number | undefined\n}\n\n/**\n * Run one Git command without a shell and await its termination.\n * @param ctx - Host context carrying the subprocess provider.\n * @param cwd - absolute directory the command runs in.\n * @param args - arguments after the executable.\n * @param options - cancellation, accepted exit statuses, and deadline.\n * @returns captured stdout, stderr, and exit status.\n * @throws {GitFailure} when the exit status is not allowed or the command was killed.\n */\nexport async function runGit(\n ctx: Context,\n cwd: string,\n args: readonly string[],\n options: GitOptions = {},\n): Promise<GitResult> {\n const deadline = AbortSignal.timeout(options.timeoutMs ?? GIT_TIMEOUT_MS)\n const signal = options.signal === undefined ? deadline : AbortSignal.any([options.signal, deadline])\n const allowed = options.allowedExitCodes ?? [0]\n const program = await ctx.subprocess.resolveExecutable('git', undefined, signal)\n const argv = [program, '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', ...args]\n const child = ctx.subprocess.spawn({\n argv,\n cwd,\n env: {\n ...GIT_ENV,\n GIT_TERMINAL_PROMPT: '0',\n GIT_LFS_SKIP_SMUDGE: '1',\n GIT_NO_LAZY_FETCH: '1',\n },\n signal,\n graceMs: 1_500,\n stdio: {\n stdin: 'ignore',\n stdout: { maxBytes: MAX_OUTPUT_BYTES },\n stderr: { maxBytes: MAX_OUTPUT_BYTES },\n },\n })\n try {\n const outcome = await child.done\n const stdout = child.collected.stdout?.readFrom(0).text ?? ''\n const stderr = child.collected.stderr?.readFrom(0).text ?? ''\n const result: GitResult = { stdout, stderr, exitCode: outcome.exitCode ?? -1 }\n if (outcome.exitCode === null || !allowed.includes(outcome.exitCode)) throw new GitFailure(args, result)\n return result\n } finally {\n child.terminate()\n await child.waitForExit()\n }\n}\n\n/**\n * Resolve the repository root that contains `directory`.\n * @param ctx - Host context carrying the subprocess provider.\n * @param directory - absolute directory inside (or at) the checkout.\n * @param signal - caller cancellation.\n * @returns the canonical top-level directory Git reports.\n * @throws {GitFailure} when the directory is not inside a Git working tree.\n */\nexport async function repositoryRoot(ctx: Context, directory: string, signal?: AbortSignal): Promise<string> {\n const result = await runGit(ctx, directory, ['rev-parse', '--show-toplevel'], {\n signal,\n allowedExitCodes: [0, 128],\n })\n if (result.exitCode !== 0) throw new GitFailure(['rev-parse', '--show-toplevel'], result)\n return result.stdout.replace(/\\r?\\n$/, '')\n}\n\n/**\n * Resolve symlinks in a path that exists, falling back to the path as given.\n * @param path - absolute path.\n * @returns the canonical spelling when the path exists.\n */\nexport async function canonicalPath(path: string): Promise<string> {\n try {\n return await realpath(path)\n } catch {\n return path\n }\n}\n\n/** One checkout reported by `git worktree list --porcelain`. */\nexport interface WorktreeRecord {\n /** Absolute checkout directory. */\n readonly path: string\n /** Branch the checkout has checked out, when it has one. */\n readonly branch?: string | undefined\n}\n\n/**\n * Parse `git worktree list --porcelain` output into its records.\n * @param stdout - the command's stdout.\n * @returns one record per reported checkout, in Git's order.\n */\nexport function parseWorktreeList(stdout: string): WorktreeRecord[] {\n const records: WorktreeRecord[] = []\n let path: string | undefined\n let branch: string | undefined\n const flush = (): void => {\n if (path === undefined) return\n records.push(branch === undefined ? { path } : { path, branch })\n path = undefined\n branch = undefined\n }\n for (const line of stdout.split('\\n')) {\n if (line === '') {\n flush()\n continue\n }\n if (line.startsWith('worktree ')) path = line.slice('worktree '.length)\n else if (line.startsWith('branch refs/heads/')) branch = line.slice('branch refs/heads/'.length)\n }\n flush()\n return records\n}\n\n/**\n * List the worktree checkouts of one repository.\n * @param ctx - Host context carrying the subprocess provider.\n * @param root - repository root the command runs in.\n * @param signal - caller cancellation.\n * @returns every checkout Git reports for the repository.\n */\nexport async function listWorktreeRecords(\n ctx: Context,\n root: string,\n signal?: AbortSignal,\n): Promise<WorktreeRecord[]> {\n const result = await runGit(ctx, root, ['worktree', 'list', '--porcelain'], { signal })\n return parseWorktreeList(result.stdout)\n}\n\n/**\n * Count the changed and untracked files of one checkout.\n * @param ctx - Host context carrying the subprocess provider.\n * @param directory - checkout directory to inspect.\n * @param signal - caller cancellation.\n * @returns the number of `git status --porcelain` entries.\n */\nexport async function changedFileCount(\n ctx: Context,\n directory: string,\n signal?: AbortSignal,\n): Promise<number> {\n const result = await runGit(ctx, directory, ['status', '--porcelain', '--untracked-files=normal'], {\n signal,\n })\n return result.stdout.split('\\n').filter(line => line.trim() !== '').length\n}\n","/**\n * Names for the copy a session gets: lowercase words and a short suffix, so a\n * branch, a directory, a `git branch` row and this plugin's UI all read the\n * same. No prefix: the pool path already says where it lives.\n * @module dsh-worktree-switch/host/naming\n */\n\n/** First word of a generated name. */\nconst ADJECTIVES = [\n 'amber', 'brisk', 'calm', 'clever', 'crisp', 'eager', 'faint', 'fresh',\n 'gentle', 'happy', 'hollow', 'kind', 'lively', 'mellow', 'nimble', 'polite',\n 'quiet', 'rapid', 'ready', 'silent', 'soft', 'solid', 'steady', 'sunny',\n 'swift', 'tidy', 'vivid', 'warm', 'wise', 'young',\n] as const\n\n/** Second word of a generated name. */\nconst NOUNS = [\n 'anchor', 'atlas', 'badger', 'birch', 'brook', 'cedar', 'comet', 'coral',\n 'delta', 'ember', 'fern', 'field', 'finch', 'glade', 'harbor', 'heron',\n 'island', 'lark', 'ledge', 'maple', 'meadow', 'otter', 'pebble', 'pine',\n 'quartz', 'river', 'shore', 'stone', 'trail', 'willow',\n] as const\n\n/**\n * Generate one candidate worktree name.\n * @param random - uniform source in `[0, 1)`; injectable for deterministic tests.\n * @returns a lowercase, hyphen-joined branch-safe name such as `quiet-harbor-3fa9`.\n */\nexport function worktreeName(random: () => number = Math.random): string {\n const pick = (values: readonly string[]): string => {\n const index = Math.min(values.length - 1, Math.floor(random() * values.length))\n return values[index] ?? values[0] ?? 'copy'\n }\n const suffix = Math.min(0xffff, Math.floor(random() * 0x10000)).toString(16).padStart(4, '0')\n return `${pick(ADJECTIVES)}-${pick(NOUNS)}-${suffix}`\n}\n\n/** A pool entry name is exactly one path segment, as `git worktree` names it. */\nconst NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/\n\n/**\n * Validate a worktree name received from the browser before it reaches Git.\n * @param value - candidate name.\n * @returns true for one plain path segment that Git also accepts as a branch.\n */\nexport function isWorktreeName(value: string): boolean {\n return NAME_PATTERN.test(value) && !value.endsWith('.lock')\n}\n","/**\n * The plugin's host Remote face: the four verbs the browser half can call,\n * expressed against a live session and the checkouts on disk.\n *\n * This module is the seam the host tests drive directly. Everything below it —\n * Git, the worktree service, the sandbox policy it applies, and session\n * working-directory publication — belongs to the harness and is exercised by\n * its own tests.\n * @module dsh-worktree-switch/host/handlers\n */\n\nimport { lstat, stat } from 'node:fs/promises'\nimport { basename, isAbsolute, relative, resolve } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { Agent } from '@deepseek-ai/dsh-agent'\nimport type { SessionId } from '@deepseek-ai/dsh-session'\nimport type {} from '@deepseek-ai/dsh-api-session-controller'\nimport type {} from '@deepseek-ai/dsh-experimental-worktree'\nimport type {} from '@deepseek-ai/dsh-working-directory'\nimport type {\n DiscardValue,\n ListValue,\n PrepareValue,\n StatusValue,\n WorktreeItem,\n WorktreeRef,\n} from '../types.ts'\nimport {\n canonicalPath,\n changedFileCount,\n GitFailure,\n listWorktreeRecords,\n repositoryRoot,\n runGit,\n} from './git.ts'\nimport { isWorktreeName, worktreeName } from './naming.ts'\n\n/** Attempts at a fresh name before a creation failure becomes an error. */\nconst CREATE_ATTEMPTS = 3\n\nexport type {\n DiscardValue,\n ListValue,\n PrepareValue,\n StatusValue,\n WorktreeItem,\n WorktreeRef,\n} from '../types.ts'\n\n/** A failed verb, carrying the stable code the browser half switches on. */\nexport class WorktreeToggleError extends Error {\n /**\n * @param code - stable failure code.\n * @param message - human-readable explanation.\n * @param details - extra machine-readable facts.\n */\n constructor(\n readonly code: string,\n message: string,\n readonly details: Readonly<Record<string, unknown>> = {},\n ) {\n super(message)\n this.name = 'WorktreeToggleError'\n }\n}\n\n/** Row config the verbs read. */\nexport interface WorktreeToggleOptions {\n /** Default for a project nothing has remembered yet. */\n readonly defaultEnabled: boolean\n}\n\n/** The host face the route layer exposes over the Connection fetch bridge. */\nexport interface WorktreeToggle {\n /**\n * Report whether the session's project can be isolated, and in what.\n * @param request - the calling session.\n * @param signal - caller cancellation.\n * @returns the status payload; `supported: false` is a normal answer.\n */\n status(request: { readonly sessionId: string }, signal?: AbortSignal): Promise<StatusValue>\n /**\n * Put the session into a fresh copy, creating it when necessary.\n * @param request - the calling session.\n * @param signal - caller cancellation.\n * @returns the created or already-present copy.\n * @throws {WorktreeToggleError} when the project cannot host a copy or Git refused.\n */\n prepare(request: { readonly sessionId: string }, signal?: AbortSignal): Promise<PrepareValue>\n /**\n * List the copies under one project's pool.\n * @param request - any directory inside the project.\n * @param signal - caller cancellation.\n * @returns the pool contents, or `supported: false` for a non-repository path.\n */\n list(request: { readonly path: string }, signal?: AbortSignal): Promise<ListValue>\n /**\n * Delete one copy and its branch.\n * @param request - a directory inside the project, and the copy's name.\n * @param signal - caller cancellation.\n * @returns the removed copy's identity.\n * @throws {WorktreeToggleError} when the copy is missing, dirty, or Git refused.\n */\n discard(\n request: { readonly path: string; readonly name: string },\n signal?: AbortSignal,\n ): Promise<DiscardValue>\n}\n\n/** Normalize a browser-supplied path before any filesystem work. */\nfunction absolutePath(value: string): string {\n const trimmed = value.trim()\n if (trimmed === '') throw new WorktreeToggleError('bad-request', 'a project path is required')\n return resolve(trimmed)\n}\n\n/**\n * The copy a session already runs in.\n *\n * A linked worktree is recognised from the checkout itself — its `.git` is a\n * file pointing back at the source repository, not a directory — rather than\n * from any configured pool path. The pool belongs to the harness worktree\n * service, so this plugin never needs a second copy of that setting: moving the\n * pool in the service's row config keeps every verb working.\n * @param current - the session's committed working directory.\n * @param repositoryRoot - canonical root of the project's repository.\n * @returns the copy's identity, or null when the directory is not one.\n */\nasync function worktreeAt(current: string, repositoryRoot: string): Promise<WorktreeRef | null> {\n const path = await canonicalPath(current)\n if (path === repositoryRoot) return null\n const inside = relative(repositoryRoot, path)\n if (inside === '' || inside.startsWith('..') || isAbsolute(inside)) return null\n const name = basename(path)\n if (!isWorktreeName(name)) return null\n try {\n if (!(await lstat(`${path}/.git`)).isFile()) return null\n } catch {\n return null\n }\n return { name, path, branch: name }\n}\n\n/**\n * Build the host face.\n * @param ctx - Host context with `agents`, `worktrees`, `workingDirectory`, and `subprocess`.\n * @param options - resolved row config.\n * @returns the four verbs.\n */\nexport function createWorktreeToggle(ctx: Context, options: WorktreeToggleOptions): WorktreeToggle {\n /** One in-flight preparation per session, so a double mount cannot create twice. */\n const pending = new Map<string, Promise<PrepareValue>>()\n\n const sessionOf = async (sessionId: string, signal?: AbortSignal): Promise<Agent> => {\n signal?.throwIfAborted()\n if (sessionId === '') throw new WorktreeToggleError('bad-request', 'a session id is required')\n const controller = ctx.get('sessionController')\n if (controller !== undefined) {\n const resolved = await controller.resolveAgent(sessionId as SessionId)\n if ('error' in resolved) {\n throw new WorktreeToggleError(resolved.error.code, resolved.error.message, {\n sessionId,\n })\n }\n return resolved.agent\n }\n const agent = ctx.agents.get(sessionId as SessionId)\n if (agent === undefined) {\n throw new WorktreeToggleError('session-not-found', `no live agent for session ${sessionId}`, {\n sessionId,\n })\n }\n return agent\n }\n\n /**\n * Resolve the canonical root of the repository that contains a directory.\n * Git reports the spelling of the path it was given; the worktree service\n * publishes canonical paths, so both halves must agree on one spelling.\n * @param directory - absolute directory inside the project.\n * @param signal - caller cancellation.\n * @returns the canonical repository root.\n */\n const repositoryOf = async (directory: string, signal?: AbortSignal): Promise<string> =>\n canonicalPath(await repositoryRoot(ctx, directory, signal))\n\n const status = async (\n request: { readonly sessionId: string },\n signal?: AbortSignal,\n ): Promise<StatusValue> => {\n const agent = await sessionOf(request.sessionId, signal)\n // The immutable creation directory is the project anchor; the committed\n // working directory may already be a copy this session entered.\n const startDirectory = agent.session.header.cwd ?? ctx.workingDirectory.get(agent.session)\n let root: string\n try {\n root = await repositoryOf(startDirectory, signal)\n } catch (error) {\n if (error instanceof GitFailure) {\n return { supported: false, reason: 'not-a-git-repository', defaultEnabled: options.defaultEnabled }\n }\n throw error\n }\n const current = ctx.workingDirectory.get(agent.session)\n signal?.throwIfAborted()\n const sourceDirty = await changedFileCount(ctx, root, signal)\n return {\n supported: true,\n repositoryRoot: root,\n sessionWorktree: await worktreeAt(current, root),\n sourceDirty,\n defaultEnabled: options.defaultEnabled,\n }\n }\n\n const prepareOnce = async (\n request: { readonly sessionId: string },\n signal?: AbortSignal,\n ): Promise<PrepareValue> => {\n const agent = await sessionOf(request.sessionId, signal)\n const current = await status(request, signal)\n if (!current.supported) {\n throw new WorktreeToggleError(\n 'not-a-git-repository',\n 'the session project is not inside a Git working tree',\n )\n }\n if (current.sessionWorktree !== null) {\n return {\n state: 'existing',\n worktree: current.sessionWorktree,\n repositoryRoot: current.repositoryRoot,\n sourceDirty: current.sourceDirty,\n }\n }\n // Status anchors on the immutable creation directory, while the worktree\n // service creates relative to the session's CURRENT directory. Refuse to\n // create when the two name different repositories: a copy in the wrong\n // project would look like success and be worse than no copy.\n const working = ctx.workingDirectory.get(agent.session)\n const workingProject = await repositoryOf(working, signal)\n if (workingProject !== current.repositoryRoot) {\n throw new WorktreeToggleError(\n 'session-moved',\n `this session now works in ${workingProject}, not in its project ${current.repositoryRoot}`,\n { repositoryRoot: current.repositoryRoot, workingProject },\n )\n }\n let lastFailure: unknown\n for (let attempt = 0; attempt < CREATE_ATTEMPTS; attempt += 1) {\n signal?.throwIfAborted()\n const name = worktreeName()\n try {\n const created = await ctx.worktrees.create(agent, { name, from: 'HEAD' }, signal)\n return {\n state: 'created',\n worktree: { name: created.branch, path: created.path, branch: created.branch },\n repositoryRoot: created.repositoryRoot,\n sourceDirty: current.sourceDirty,\n }\n } catch (error) {\n lastFailure = error\n const message = error instanceof Error ? error.message : String(error)\n // A name collision is the one failure a fresh name can fix. Everything\n // else (no commits yet, branch or path exists, Git missing) is reported\n // as-is: the caller must not be told \"created\" after a silent retry.\n if (!/already exists|worktree branch already exists|worktree path already exists/i.test(message)) {\n throw new WorktreeToggleError('worktree-create-failed', message, { name })\n }\n }\n }\n throw new WorktreeToggleError(\n 'worktree-create-failed',\n `could not allocate a free worktree name after ${CREATE_ATTEMPTS} attempts`,\n { cause: lastFailure instanceof Error ? lastFailure.message : String(lastFailure) },\n )\n }\n\n const prepare = async (\n request: { readonly sessionId: string },\n signal?: AbortSignal,\n ): Promise<PrepareValue> => {\n const running = pending.get(request.sessionId)\n if (running !== undefined) return running\n const operation = prepareOnce(request, signal)\n pending.set(request.sessionId, operation)\n try {\n return await operation\n } finally {\n if (pending.get(request.sessionId) === operation) pending.delete(request.sessionId)\n }\n }\n\n const list = async (\n request: { readonly path: string },\n signal?: AbortSignal,\n ): Promise<ListValue> => {\n const directory = absolutePath(request.path)\n let root: string\n try {\n root = await repositoryOf(directory, signal)\n } catch (error) {\n if (error instanceof GitFailure) return { supported: false, reason: 'not-a-git-repository' }\n throw error\n }\n const records = await listWorktreeRecords(ctx, root, signal)\n const items: WorktreeItem[] = []\n for (const record of records) {\n signal?.throwIfAborted()\n const path = await canonicalPath(record.path)\n // The repository's own checkout is the source, never a copy.\n if (path === root) continue\n const name = basename(path)\n if (!isWorktreeName(name)) continue\n const branch = record.branch ?? name\n let createdAt = 0\n let prunable = true\n try {\n const info = await stat(path)\n createdAt = Math.trunc(info.birthtimeMs > 0 ? info.birthtimeMs : info.mtimeMs)\n prunable = false\n } catch {\n // The checkout directory is gone; Git still lists it until pruned.\n }\n const changedFiles = prunable ? 0 : await changedFileCount(ctx, path, signal)\n items.push({ name, path, branch, createdAt, changedFiles, prunable })\n }\n items.sort((left, right) => right.createdAt - left.createdAt || left.name.localeCompare(right.name))\n return { supported: true, repositoryRoot: root, items }\n }\n\n const discard = async (\n request: { readonly path: string; readonly name: string },\n signal?: AbortSignal,\n ): Promise<DiscardValue> => {\n const directory = absolutePath(request.path)\n if (!isWorktreeName(request.name)) {\n throw new WorktreeToggleError('worktree-name-invalid', `not a worktree name: ${request.name}`)\n }\n const root = await repositoryOf(directory, signal)\n const listed = await list({ path: directory }, signal)\n if (!listed.supported) {\n throw new WorktreeToggleError('not-a-git-repository', 'the project is not inside a Git working tree')\n }\n const item = listed.items.find(entry => entry.name === request.name)\n if (item === undefined) {\n throw new WorktreeToggleError(\n 'worktree-not-found',\n `no worktree named ${request.name} in ${root}`,\n { name: request.name, repositoryRoot: root },\n )\n }\n if (item.changedFiles > 0) {\n throw new WorktreeToggleError(\n 'worktree-dirty',\n `${item.name} has ${item.changedFiles} uncommitted change(s); commit or discard them first`,\n { name: item.name, changedFiles: item.changedFiles },\n )\n }\n if (item.prunable) {\n await runGit(ctx, root, ['worktree', 'prune'], { signal })\n } else {\n const removed = await runGit(ctx, root, ['worktree', 'remove', item.path], {\n signal,\n allowedExitCodes: [0, 128],\n })\n if (removed.exitCode !== 0) {\n throw new WorktreeToggleError(\n 'worktree-remove-failed',\n new GitFailure(['worktree', 'remove', item.path], removed).message,\n { name: item.name, path: item.path },\n )\n }\n }\n const deleted = await runGit(ctx, root, ['branch', '-D', '--', item.branch], {\n signal,\n allowedExitCodes: [0, 1],\n })\n // Status 1 with \"not found\" is the branch already being gone (removed by\n // hand between the read and the click); that is not a failed discard.\n const branchGone = deleted.exitCode === 1 && /not found/i.test(deleted.stderr)\n if (deleted.exitCode !== 0 && !branchGone) {\n throw new WorktreeToggleError(\n 'worktree-remove-failed',\n new GitFailure(['branch', '-D', item.branch], deleted).message,\n { name: item.name, branch: item.branch },\n )\n }\n return { removed: true, name: item.name, branch: item.branch, path: item.path }\n }\n\n return { status, prepare, list, discard }\n}\n","/**\n * The Connection fetch bridge: four exact POST routes on the shared `/api`\n * channel, one per verb, speaking the `client-request` / `server-response`\n * envelope the browser's `connection.rpc.call` already implements.\n *\n * Exact routes are the only shape an out-of-tree plugin can claim: `/api` is\n * the one logical channel the API Gateway owns, so `rpc.handle` refuses it and\n * a second `rpc.intercept` throws. Every route below answers owner peers only,\n * which is what the local Web GUI is.\n * @module dsh-worktree-switch/host/routes\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { ConnectionRpcResult } from '@deepseek-ai/dsh-client-connection'\nimport type {} from '@deepseek-ai/dsh-client-connection'\nimport type { DiscardValue, ListValue, PrepareValue, StatusValue, WorktreeToggle } from './handlers.ts'\nimport { WorktreeToggleError } from './handlers.ts'\n\n/** Absolute prefix every route below lives under. */\nexport const API_PREFIX = '/api/dsh-worktree-switch'\n\n/** Plugin id; the wire method prefix and the package name are the same string. */\nexport const PLUGIN_ID = 'dsh-worktree-switch'\n\n/** One route per verb. */\nexport const ENDPOINTS = ['status', 'prepare', 'list', 'discard'] as const\n\n/** A verb this route table serves. */\nexport type Endpoint = (typeof ENDPOINTS)[number]\n\n/** A decoded client request. */\ninterface ClientRequest {\n readonly rpcId: string\n readonly method: string\n readonly payload: Record<string, unknown>\n}\n\n/** A payload object with the named string field, or the failure to send back. */\nfunction requiredString(\n payload: Record<string, unknown>,\n field: string,\n): { readonly value: string } | { readonly failure: WorktreeToggleError } {\n const value = payload[field]\n if (typeof value !== 'string' || value.trim() === '') {\n return {\n failure: new WorktreeToggleError('bad-request', `payload.${field} must be a non-empty string`),\n }\n }\n return { value }\n}\n\n/**\n * Decode the browser's `client-request` envelope.\n * @param body - parsed JSON body.\n * @returns the request, or null when the envelope is malformed.\n */\nexport function readClientRequest(body: unknown): ClientRequest | null {\n if (body === null || typeof body !== 'object' || Array.isArray(body)) return null\n const record = body as Record<string, unknown>\n if (record.type !== 'client-request') return null\n const { rpcId, method } = record\n if (typeof rpcId !== 'string' || rpcId === '') return null\n if (typeof method !== 'string' || method === '') return null\n const payload = record.payload\n return {\n rpcId,\n method,\n payload: payload !== null && typeof payload === 'object' && !Array.isArray(payload)\n ? payload as Record<string, unknown>\n : {},\n }\n}\n\n/** A successful result. */\nfunction ok<T>(value: T): ConnectionRpcResult<T> {\n return { ok: true, value }\n}\n\n/** A failed result carrying the stable code. */\nfunction fail(code: string, message: string, details: Record<string, unknown> = {}): ConnectionRpcResult<never> {\n return { ok: false, error: { code, message, details } }\n}\n\n/**\n * Dispatch one decoded verb.\n * @param toggle - the host face.\n * @param endpoint - the verb.\n * @param payload - its decoded payload.\n * @param signal - request cancellation.\n * @returns the wire result.\n */\nexport async function dispatch(\n toggle: WorktreeToggle,\n endpoint: Endpoint,\n payload: Record<string, unknown>,\n signal: AbortSignal,\n): Promise<ConnectionRpcResult<StatusValue | PrepareValue | ListValue | DiscardValue>> {\n try {\n if (endpoint === 'status') {\n const sessionId = requiredString(payload, 'sessionId')\n if ('failure' in sessionId) throw sessionId.failure\n return ok(await toggle.status({ sessionId: sessionId.value }, signal))\n }\n if (endpoint === 'prepare') {\n const sessionId = requiredString(payload, 'sessionId')\n if ('failure' in sessionId) throw sessionId.failure\n return ok(await toggle.prepare({ sessionId: sessionId.value }, signal))\n }\n if (endpoint === 'list') {\n const path = requiredString(payload, 'path')\n if ('failure' in path) throw path.failure\n return ok(await toggle.list({ path: path.value }, signal))\n }\n const path = requiredString(payload, 'path')\n if ('failure' in path) throw path.failure\n const name = requiredString(payload, 'name')\n if ('failure' in name) throw name.failure\n return ok(await toggle.discard({ path: path.value, name: name.value }, signal))\n } catch (error) {\n if (error instanceof WorktreeToggleError) return fail(error.code, error.message, { ...error.details })\n return fail('internal', error instanceof Error ? error.message : String(error))\n }\n}\n\n/**\n * Register one exact route per verb on the shared `/api` channel.\n * @param ctx - Host context carrying `connection`.\n * @param toggle - the host face the routes call.\n */\nexport function registerWorktreeRoutes(ctx: Context, toggle: WorktreeToggle): void {\n for (const endpoint of ENDPOINTS) {\n ctx.effect(() => ctx.connection.fetch.register({\n path: `${API_PREFIX}/${endpoint}`,\n methods: ['POST'],\n requestBody: 'buffered',\n async fetch(request) {\n const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n return new Response('content type must be application/json', { status: 415 })\n }\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return new Response('body is not JSON', { status: 400 })\n }\n const message = readClientRequest(body)\n if (message === null) return new Response('invalid client-request message', { status: 400 })\n if (message.method !== `${PLUGIN_ID}/${endpoint}`) {\n return Response.json({\n type: 'server-response',\n rpcId: message.rpcId,\n result: fail('bad-request', 'RPC method does not match endpoint'),\n })\n }\n const result = await dispatch(toggle, endpoint, message.payload, request.signal)\n return Response.json({ type: 'server-response', rpcId: message.rpcId, result })\n },\n }), `${PLUGIN_ID} ${endpoint} route`)\n }\n}\n","/**\n * Host half of the Worktree switch.\n *\n * It mounts no service itself: the bundle patch inserts the harness worktree\n * service (`@deepseek-ai/dsh-experimental-worktree`, which provides\n * `ctx.worktrees`) and this plugin's own row. This plugin exposes four verbs\n * over the Connection fetch bridge and drives that service for creation; the\n * browser half renders the switch and the panel.\n * @module dsh-worktree-switch\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport type {} from '@deepseek-ai/dsh-experimental-worktree'\nimport type {} from '@deepseek-ai/dsh-subprocess'\nimport type {} from '@deepseek-ai/dsh-working-directory'\nimport { createWorktreeToggle } from './host/handlers.ts'\nimport { registerWorktreeRoutes } from './host/routes.ts'\n\n/** Plugin id: package name, bundle row id, browser module id. */\nexport const name = 'dsh-worktree-switch'\n\n/**\n * Services this plugin cannot work without.\n *\n * `worktrees` is provided by the row this bundle's patch inserts next to its\n * own, so Cordis holds this plugin until that row has mounted.\n */\nexport const inject = ['agents', 'connection', 'subprocess', 'worktrees', 'workingDirectory']\n\n/** Row config accepted from `cordis.patch.yml`. */\nexport interface Config {\n /** Default for a project the user has never switched either way. */\n defaultEnabled: boolean\n}\n\n/** Schemastery schema for {@link Config}. */\nexport const Config: Schema<Config> = Schema.object({\n defaultEnabled: Schema.boolean()\n .default(true)\n .description('Start a new session in a worktree copy until the user turns the switch off for that project.'),\n})\n\n/**\n * Mount the host half.\n * @param ctx - Host context.\n * @param config - validated row config.\n */\nexport function apply(ctx: Context, config: Config): void {\n const toggle = createWorktreeToggle(ctx, { defaultEnabled: config.defaultEnabled })\n registerWorktreeRoutes(ctx, toggle)\n}\n"],"mappings":"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"}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,135 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-worktree-switch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "A New Session switch that puts the conversation into a throwaway Git worktree under the project's worktree pool (`<project>/.dsh/worktrees` by default), plus a panel to inspect and discard the copies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"plugin",
|
|
10
|
+
"git",
|
|
11
|
+
"worktree"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"icon": "icon.svg",
|
|
16
|
+
"main": "lib/index.js",
|
|
17
|
+
"types": "lib/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./lib/index.d.ts",
|
|
21
|
+
"default": "./lib/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./client": "./lib/client.js",
|
|
24
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
25
|
+
"./locale/*.json": "./locale/*.json",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"cordis.patch.yml",
|
|
31
|
+
"locale",
|
|
32
|
+
"icon.svg",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
37
|
+
"dsh": ">=0.2.1-rc.5 <0.2.2-0"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"manifestVersion": 1,
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
},
|
|
44
|
+
"client": {
|
|
45
|
+
"platform": "web",
|
|
46
|
+
"inject": [
|
|
47
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
48
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
49
|
+
"@deepseek-ai/dsh-client-connection",
|
|
50
|
+
"@deepseek-ai/dsh-client-locale",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
62
|
+
"@deepseek-ai/dsh-api-session-controller": ">=0.2.1-rc.5 <0.2.2-0",
|
|
63
|
+
"@deepseek-ai/dsh-api-workspace-controller": ">=0.2.1-rc.5 <0.2.2-0",
|
|
64
|
+
"@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.5 <0.2.2-0",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.5 <0.2.2-0",
|
|
66
|
+
"@deepseek-ai/dsh-client-store": ">=0.2.1-rc.5 <0.2.2-0",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.2.1-rc.5 <0.2.2-0",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-layout": ">=0.2.1-rc.5 <0.2.2-0",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.2.1-rc.5 <0.2.2-0",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.5 <0.2.2-0",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-session": ">=0.2.1-rc.5 <0.2.2-0",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.2.1-rc.5 <0.2.2-0",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.5 <0.2.2-0",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-workspace": ">=0.2.1-rc.5 <0.2.2-0",
|
|
75
|
+
"@deepseek-ai/dsh-experimental-worktree": ">=0.2.1-rc.5 <0.2.2-0",
|
|
76
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
80
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.5",
|
|
81
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "0.2.1-rc.5",
|
|
82
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.5",
|
|
83
|
+
"@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.5",
|
|
84
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
|
|
85
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
|
|
86
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.5",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.5",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.5",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.5",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-workspace": "0.2.1-rc.5",
|
|
95
|
+
"@deepseek-ai/dsh-experimental-worktree": "0.2.1-rc.5",
|
|
96
|
+
"@deepseek-ai/dsh-fs": "0.2.1-rc.5",
|
|
97
|
+
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.5",
|
|
98
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.5",
|
|
99
|
+
"@deepseek-ai/dsh-sandbox-local": "0.2.1-rc.5",
|
|
100
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.5",
|
|
101
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.5",
|
|
102
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.5",
|
|
103
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-rc.5",
|
|
104
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.5",
|
|
105
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.1-rc.5",
|
|
106
|
+
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.5",
|
|
107
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
108
|
+
"@testing-library/react": "^16.3.3",
|
|
109
|
+
"@types/node": "^26.6.6",
|
|
110
|
+
"@types/react": "~18.3.1",
|
|
111
|
+
"@types/react-dom": "~18.3.1",
|
|
112
|
+
"immer": "^10.2.0",
|
|
113
|
+
"jsdom": "^26.1.0",
|
|
114
|
+
"react": "18.3.1",
|
|
115
|
+
"react-dom": "18.3.1",
|
|
116
|
+
"tsdown": "^0.23.4",
|
|
117
|
+
"typescript": "^7.0.2",
|
|
118
|
+
"vitest": "^5.0.5",
|
|
119
|
+
"zustand": "~4.5.7"
|
|
120
|
+
},
|
|
121
|
+
"scripts": {
|
|
122
|
+
"build": "tsdown",
|
|
123
|
+
"watch": "tsdown --watch",
|
|
124
|
+
"typecheck": "tsc --noEmit",
|
|
125
|
+
"test": "vitest run",
|
|
126
|
+
"prepare": "tsdown"
|
|
127
|
+
},
|
|
128
|
+
"author": "mxn404",
|
|
129
|
+
"homepage": "https://github.com/mxn404/deepseek-plugins/tree/main/packages/dsh-worktree-switch",
|
|
130
|
+
"repository": {
|
|
131
|
+
"type": "git",
|
|
132
|
+
"url": "git+https://github.com/mxn404/deepseek-plugins.git",
|
|
133
|
+
"directory": "packages/dsh-worktree-switch"
|
|
134
|
+
}
|
|
135
|
+
}
|