dsh-taskboard 0.4.5 → 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 +24 -1
- package/lib/client.js +434 -193
- 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 +210 -112
- 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 +128 -97
- 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 +48 -5
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +9 -8
- package/src/client/api.ts +26 -8
- package/src/client/board/ImportModal.tsx +1 -1
- package/src/client/board/SettingsModal.tsx +84 -0
- package/src/client/board/TaskBoard.tsx +47 -40
- package/src/client/board/TaskCard.tsx +3 -5
- package/src/client/board/TaskDetail.tsx +30 -21
- package/src/client/board/TaskFormModal.tsx +39 -31
- package/src/client/board/format.ts +26 -0
- package/src/client/board/labels.ts +44 -0
- package/src/client/controller.ts +86 -34
- package/src/client/index.ts +7 -5
- package/src/client/sidebar-entry.ts +5 -1
- package/src/client/styles.ts +4 -0
- package/src/host/execution.ts +90 -16
- package/src/host/git.ts +39 -10
- package/src/host/routes.ts +263 -128
- 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 +187 -126
- package/src/index.ts +10 -1
- package/src/shared/api.ts +11 -2
- package/src/shared/protocol.ts +83 -6
- package/src/shared/version.ts +1 -1
- package/src/client/board/NewTaskModal.tsx +0 -8
package/src/shared/protocol.ts
CHANGED
|
@@ -107,11 +107,20 @@ export const URGENCY_COLOR: Readonly<Record<Urgency, string>> = {
|
|
|
107
107
|
* - `worktree`: each execution runs in a fresh `git worktree` on a dedicated
|
|
108
108
|
* task branch (`task/<标题>+<taskId>`) under `<workspace>/.dsh-worktrees/`.
|
|
109
109
|
* - `none`: run in the workspace directory as before, zero git interaction.
|
|
110
|
-
* Omitted =
|
|
111
|
-
*
|
|
110
|
+
* Omitted = {@link DEFAULT_ISOLATION}; since 0.5.0 creation materializes the
|
|
111
|
+
* board default onto the record (看板设置 → 执行隔离), and non-git projects
|
|
112
|
+
* still auto-degrade at run time (the execution record carries an
|
|
113
|
+
* `isolationNote` explaining why).
|
|
112
114
|
*/
|
|
113
115
|
export type IsolationMode = 'worktree' | 'none'
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Factory-default isolation (0.5.0): 原目录执行. Applies when neither the
|
|
119
|
+
* task record nor the board setting (`BoardSettings.defaultIsolation`)
|
|
120
|
+
* says otherwise. Before 0.5.0 the implicit default was 'worktree'.
|
|
121
|
+
*/
|
|
122
|
+
export const DEFAULT_ISOLATION: IsolationMode = 'none'
|
|
123
|
+
|
|
115
124
|
/** Validate an isolation value. */
|
|
116
125
|
export function asIsolation(raw: string): IsolationMode {
|
|
117
126
|
if (raw !== 'worktree' && raw !== 'none') {
|
|
@@ -120,9 +129,39 @@ export function asIsolation(raw: string): IsolationMode {
|
|
|
120
129
|
return raw
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
/** Resolve a task's effective isolation (omitted → the
|
|
132
|
+
/** Resolve a task's effective isolation (omitted → the factory default). */
|
|
124
133
|
export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): IsolationMode {
|
|
125
|
-
return task.isolation === undefined ?
|
|
134
|
+
return task.isolation === undefined ? DEFAULT_ISOLATION : task.isolation
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Board-level settings persisted with the ledger (0.5.0). Only fields the
|
|
139
|
+
* user explicitly set are present; absent fields follow factory defaults.
|
|
140
|
+
*/
|
|
141
|
+
export type BoardSettings = {
|
|
142
|
+
/** Default code isolation applied when a NEW task is created without an explicit choice. */
|
|
143
|
+
defaultIsolation?: IsolationMode
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */
|
|
147
|
+
export function asBoardSettings(raw: unknown): BoardSettings {
|
|
148
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
149
|
+
throw new Error('board settings must be an object')
|
|
150
|
+
}
|
|
151
|
+
const e = raw as Record<string, unknown>
|
|
152
|
+
const out: BoardSettings = {}
|
|
153
|
+
if (e.defaultIsolation !== undefined) {
|
|
154
|
+
if (typeof e.defaultIsolation !== 'string') {
|
|
155
|
+
throw new Error("defaultIsolation must be 'worktree' or 'none'")
|
|
156
|
+
}
|
|
157
|
+
out.defaultIsolation = asIsolation(e.defaultIsolation)
|
|
158
|
+
}
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The effective default isolation for NEW tasks (board setting → factory default). */
|
|
163
|
+
export function defaultIsolationOf(settings?: BoardSettings): IsolationMode {
|
|
164
|
+
return settings?.defaultIsolation ?? DEFAULT_ISOLATION
|
|
126
165
|
}
|
|
127
166
|
|
|
128
167
|
/** How a task may run. */
|
|
@@ -247,6 +286,7 @@ export function nextCronTime(match: CronMatch, from: number): number | null {
|
|
|
247
286
|
export type Actor =
|
|
248
287
|
| { kind: 'user' }
|
|
249
288
|
| { kind: 'agent'; sessionId: string }
|
|
289
|
+
| { kind: 'system' }
|
|
250
290
|
|
|
251
291
|
/** A progress/report comment on a task. */
|
|
252
292
|
export type CommentRecord = {
|
|
@@ -419,6 +459,8 @@ export type TaskLedger = {
|
|
|
419
459
|
/** Global monotonic revision; every mutation bumps it. */
|
|
420
460
|
revision: number
|
|
421
461
|
tasks: TaskRecord[]
|
|
462
|
+
/** Board-level settings (0.5.0); absent on ledgers never touched by 设置. */
|
|
463
|
+
settings?: BoardSettings
|
|
422
464
|
}
|
|
423
465
|
|
|
424
466
|
/** Current ledger format version. */
|
|
@@ -438,6 +480,16 @@ function suffix(): string {
|
|
|
438
480
|
return Math.random().toString(36).slice(2, 8)
|
|
439
481
|
}
|
|
440
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Legal task id charset (R4): `t-<base36>-<base36>` from {@link newTaskId},
|
|
485
|
+
* and the ONLY shape accepted from the outside (import) or used to build
|
|
486
|
+
* filesystem paths (worktree dirs). Ids ride into `join(ws, '.dsh-worktrees',
|
|
487
|
+
* id)` — a lax charset here is an arbitrary-directory delete primitive.
|
|
488
|
+
*/
|
|
489
|
+
export function isValidTaskId(id: string): boolean {
|
|
490
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/.test(id)
|
|
491
|
+
}
|
|
492
|
+
|
|
441
493
|
/** Mint a task id. */
|
|
442
494
|
export function newTaskId(): string {
|
|
443
495
|
return `t-${Date.now().toString(36)}-${suffix()}`
|
|
@@ -740,6 +792,8 @@ export type ImportPlan = {
|
|
|
740
792
|
overwrite: TaskRecord[]
|
|
741
793
|
/** Invalid entries with a human-readable reason (never imported). */
|
|
742
794
|
invalid: Array<{ id?: string; reason: string }>
|
|
795
|
+
/** The file's board settings (0.5.0); replace-mode swaps them, merge keeps the live ones. */
|
|
796
|
+
settings?: BoardSettings
|
|
743
797
|
}
|
|
744
798
|
|
|
745
799
|
/** One unknown-value read helper: string fields with defaults. */
|
|
@@ -768,7 +822,10 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
768
822
|
const e = raw as Record<string, unknown>
|
|
769
823
|
const id = typeof e.id === 'string' ? e.id.trim() : ''
|
|
770
824
|
const fail = (reason: string): { ok: false; reason: string } => ({ ok: false, reason })
|
|
771
|
-
|
|
825
|
+
// R4①: length alone let traversal-shaped ids (`../../x`, `..\..\x`) into
|
|
826
|
+
// the ledger; the charset gate is the primary defense for every downstream
|
|
827
|
+
// filesystem use of a task id.
|
|
828
|
+
if (!isValidTaskId(id)) return fail('missing/invalid id (must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$)')
|
|
772
829
|
try {
|
|
773
830
|
const execution = normalizeExecution(
|
|
774
831
|
typeof e.execution === 'object' && e.execution !== null ? e.execution as { mode?: string; cron?: string } : {},
|
|
@@ -866,6 +923,26 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
866
923
|
}
|
|
867
924
|
}
|
|
868
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Minimal structural check for ONE ledger record at load time (S11): unlike
|
|
928
|
+
* {@link validateImportedTask} this REBUILDS NOTHING (cron state, ids and
|
|
929
|
+
* timestamps must survive a load untouched) — it only rejects entries whose
|
|
930
|
+
* shape would break downstream consumers, including the R4 id charset.
|
|
931
|
+
* @param raw - the untyped record.
|
|
932
|
+
*/
|
|
933
|
+
export function isPlausibleTaskRecord(raw: unknown): boolean {
|
|
934
|
+
if (typeof raw !== 'object' || raw === null) return false
|
|
935
|
+
const t = raw as Record<string, unknown>
|
|
936
|
+
return typeof t.id === 'string' && isValidTaskId(t.id)
|
|
937
|
+
&& typeof t.title === 'string' && t.title.length > 0
|
|
938
|
+
&& typeof t.workspaceId === 'string' && t.workspaceId.length > 0
|
|
939
|
+
&& ALL_STATUSES.includes(t.status as TaskStatus)
|
|
940
|
+
&& typeof t.version === 'number' && Number.isFinite(t.version) && t.version >= 1
|
|
941
|
+
&& Array.isArray(t.comments) && Array.isArray(t.executions)
|
|
942
|
+
&& typeof t.execution === 'object' && t.execution !== null
|
|
943
|
+
&& (t.execution as { mode?: unknown }).mode !== undefined
|
|
944
|
+
}
|
|
945
|
+
|
|
869
946
|
/**
|
|
870
947
|
* Validate a whole imported ledger and classify its tasks against the live
|
|
871
948
|
* one (pure). Duplicate ids INSIDE the file are invalid (first wins, later
|
|
@@ -882,7 +959,7 @@ export function validateLedgerImport(raw: unknown, knownIds: ReadonlySet<string>
|
|
|
882
959
|
throw new Error(`不支持的 schemaVersion ${String(e.schemaVersion)}(当前支持 ${LEDGER_SCHEMA_VERSION})`)
|
|
883
960
|
}
|
|
884
961
|
if (!Array.isArray(e.tasks)) throw new Error('导入文件的 tasks 不是数组')
|
|
885
|
-
const plan: ImportPlan = { create: [], overwrite: [], invalid: [] }
|
|
962
|
+
const plan: ImportPlan = { create: [], overwrite: [], invalid: [], ...(e.settings !== undefined ? { settings: asBoardSettings(e.settings) } : {}) }
|
|
886
963
|
const seen = new Set<string>()
|
|
887
964
|
for (const entry of e.tasks) {
|
|
888
965
|
const id = typeof (entry as { id?: unknown })?.id === 'string' ? (entry as { id: string }).id : undefined
|
package/src/shared/version.ts
CHANGED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compatibility shim: the composer moved into TaskFormModal (create + edit
|
|
3
|
-
* in one dialog). Kept so existing imports keep working.
|
|
4
|
-
*
|
|
5
|
-
* @module dsh-taskboard/client/board/NewTaskModal
|
|
6
|
-
*/
|
|
7
|
-
export { TaskFormModal as NewTaskModal } from './TaskFormModal.tsx'
|
|
8
|
-
export type { CatalogModel } from './TaskFormModal.tsx'
|