dsh-taskboard 0.5.0 → 0.5.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 +15 -0
- package/lib/client.js +219 -161
- package/lib/host/execution.js +80 -33
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +49 -5
- package/lib/host/git.js.map +1 -1
- package/lib/host/routes.js +180 -109
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +50 -28
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/sdk.js +7 -2
- package/lib/host/sdk.js.map +1 -1
- package/lib/host/store.js +41 -8
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +10 -3
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +124 -93
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +23 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +3 -2
- package/src/client/api.ts +19 -9
- package/src/client/board/ImportModal.tsx +1 -1
- package/src/client/board/TaskBoard.tsx +7 -38
- package/src/client/board/TaskCard.tsx +3 -5
- package/src/client/board/TaskDetail.tsx +30 -21
- package/src/client/board/TaskFormModal.tsx +30 -23
- package/src/client/board/format.ts +26 -0
- package/src/client/board/labels.ts +44 -0
- package/src/client/controller.ts +60 -13
- package/src/client/index.ts +7 -5
- package/src/client/sidebar-entry.ts +5 -1
- package/src/host/execution.ts +90 -16
- package/src/host/git.ts +39 -10
- package/src/host/routes.ts +227 -126
- package/src/host/scheduler.ts +62 -36
- package/src/host/sdk.ts +12 -1
- package/src/host/store.ts +53 -7
- package/src/host/templates.ts +12 -3
- package/src/host/tools.ts +180 -123
- package/src/index.ts +10 -1
- package/src/shared/api.ts +1 -1
- package/src/shared/protocol.ts +35 -1
- package/src/shared/version.ts +1 -1
- package/src/client/board/NewTaskModal.tsx +0 -8
package/src/host/git.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @module dsh-taskboard/host/git
|
|
24
24
|
*/
|
|
25
|
-
import
|
|
25
|
+
import { resolve } from 'node:path'
|
|
26
|
+
import { isValidTaskId, type CommitInfo } from '../shared/protocol.ts'
|
|
26
27
|
|
|
27
28
|
/** Timeout for quick read-only queries (rev-parse / status / log / diff). */
|
|
28
29
|
const QUICK_TIMEOUT_MS = 2_000
|
|
@@ -123,8 +124,12 @@ export interface GitFace {
|
|
|
123
124
|
merge(root: string, branch: string): Promise<void>
|
|
124
125
|
/** Whether `branch` is already an ancestor of HEAD (a merge would be a no-op). */
|
|
125
126
|
isAncestor(root: string, branch: string): Promise<boolean>
|
|
126
|
-
/**
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Remove a worktree. Resolves 'removed' on success, 'unregistered' when git
|
|
129
|
+
* no longer knows the path (an orphaned directory). THROWS when it still
|
|
130
|
+
* has uncommitted changes, or on any other git failure (readable reason).
|
|
131
|
+
*/
|
|
132
|
+
removeWorktree(root: string, worktreePath: string): Promise<'removed' | 'unregistered'>
|
|
128
133
|
/** Delete a branch; THROWS (e.g. still checked out in a worktree). */
|
|
129
134
|
deleteBranch(root: string, branch: string): Promise<void>
|
|
130
135
|
/**
|
|
@@ -162,8 +167,16 @@ export function sanitizeBranchName(title: string, taskId: string): string {
|
|
|
162
167
|
return head.length === 0 ? `task/${taskId}` : `task/${head}+${taskId}`
|
|
163
168
|
}
|
|
164
169
|
|
|
165
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* The canonical worktree path of a task inside its workspace (forward
|
|
172
|
+
* slashes). R4②: the id is validated HERE so every present and future call
|
|
173
|
+
* site is covered — a traversal-shaped id must never ride into a filesystem
|
|
174
|
+
* path (the cleanup/purge flows `rm -rf` what this returns).
|
|
175
|
+
*/
|
|
166
176
|
export function worktreePathOf(workspacePath: string, taskId: string): string {
|
|
177
|
+
if (!isValidTaskId(taskId)) {
|
|
178
|
+
throw new Error(`Error: invalid_input: illegal task id ${JSON.stringify(taskId.slice(0, 40))}`)
|
|
179
|
+
}
|
|
167
180
|
const root = workspacePath.replace(/[\\/]+$/, '').replaceAll('\\', '/')
|
|
168
181
|
return `${root}/${WORKTREE_DIR}/${taskId}`
|
|
169
182
|
}
|
|
@@ -219,10 +232,15 @@ export function createGitFace(exec: ExecFn = realExec): GitFace {
|
|
|
219
232
|
// worktree's own HEAD so evidence covers only the new run.
|
|
220
233
|
if (mode === 'reuse') {
|
|
221
234
|
const wtHead = await quick(['rev-parse', 'HEAD'], path)
|
|
222
|
-
|
|
235
|
+
// S14: a readable HEAD is not enough — the worktree must be on OUR
|
|
236
|
+
// branch, otherwise a user-created repo at the path would be silently
|
|
237
|
+
// taken over. Foreign or detached → fall through to fresh preparation.
|
|
238
|
+
const wtBranch = wtHead.ok ? await quick(['rev-parse', '--abbrev-ref', 'HEAD'], path) : undefined
|
|
239
|
+
if (wtHead.ok && wtHead.stdout.trim().length > 0
|
|
240
|
+
&& wtBranch !== undefined && wtBranch.ok && wtBranch.stdout.trim() === branch) {
|
|
223
241
|
return { path, branch, baseCommit: wtHead.stdout.trim(), reused: true }
|
|
224
242
|
}
|
|
225
|
-
// No live worktree → fall through to a fresh preparation.
|
|
243
|
+
// No live worktree on our branch → fall through to a fresh preparation.
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
// Baseline: the main worktree's current HEAD (also validates the repo).
|
|
@@ -302,7 +320,8 @@ export function createGitFace(exec: ExecFn = realExec): GitFace {
|
|
|
302
320
|
return path !== WORKTREE_DIR && !path.startsWith(`${WORKTREE_DIR}/`)
|
|
303
321
|
})
|
|
304
322
|
if (dirtyLines.length > 0) {
|
|
305
|
-
|
|
323
|
+
// Machine-readable tag: callers classify without parsing zh-CN text.
|
|
324
|
+
throw Object.assign(new Error(`主工作区有 ${dirtyLines.length} 处未提交修改,请先提交或暂存后再合并`), { code: 'dirty-tree' })
|
|
306
325
|
}
|
|
307
326
|
}
|
|
308
327
|
const merged = await heavy(['merge', '--no-ff', '--no-edit', branch], root)
|
|
@@ -320,14 +339,24 @@ export function createGitFace(exec: ExecFn = realExec): GitFace {
|
|
|
320
339
|
return r.ok
|
|
321
340
|
},
|
|
322
341
|
|
|
323
|
-
removeWorktree: (root, worktreePath) => withRootLock(root, async () => {
|
|
342
|
+
removeWorktree: (root, worktreePath) => withRootLock(root, async (): Promise<'removed' | 'unregistered'> => {
|
|
324
343
|
const status = await quick(['status', '--porcelain'], worktreePath)
|
|
325
344
|
if (status.ok && status.stdout.trim().length > 0) {
|
|
326
345
|
const lines = status.stdout.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
|
327
|
-
|
|
346
|
+
// Machine-readable tag: purge flows classify without parsing zh-CN text.
|
|
347
|
+
throw Object.assign(new Error(`worktree 有 ${lines.length} 处未提交修改,拒绝删除:\n${lines.slice(0, 10).join('\n')}`), { code: 'dirty-worktree' })
|
|
328
348
|
}
|
|
329
349
|
const removed = await heavy(['worktree', 'remove', worktreePath], root)
|
|
330
|
-
if (
|
|
350
|
+
if (removed.ok) return 'removed'
|
|
351
|
+
// S3: classify the failure WITHOUT parsing git's (localizable) stderr —
|
|
352
|
+
// a path absent from `worktree list` is an unregistered leftover, not
|
|
353
|
+
// an error the caller should relay verbatim.
|
|
354
|
+
const list = await quick(['worktree', 'list', '--porcelain'], root)
|
|
355
|
+
const registered = list.ok && list.stdout.split('\n')
|
|
356
|
+
.some(l => l.startsWith('worktree ')
|
|
357
|
+
&& resolve(l.slice('worktree '.length).trim()).toLowerCase() === resolve(worktreePath).toLowerCase())
|
|
358
|
+
if (!registered) return 'unregistered'
|
|
359
|
+
throw new Error(`删除 worktree 失败:${(removed.stderr.trim() || removed.stdout.trim()).slice(0, 300)}`)
|
|
331
360
|
}),
|
|
332
361
|
|
|
333
362
|
deleteBranch: (root, branch) => withRootLock(root, async () => {
|