@goodandready/dsh-kanban 0.2.35 → 0.2.37
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/lib/board-tool.js +8 -0
- package/lib/client.js +3884 -3567
- package/lib/config.js +4 -0
- package/lib/dependencies.js +175 -0
- package/lib/launcher.js +4 -0
- package/lib/plan-generator.js +226 -0
- package/lib/pr-generator.js +270 -0
- package/lib/routes/board.js +4 -0
- package/lib/routes/task-actions.js +160 -1
- package/lib/routes/task.js +27 -1
- package/lib/routes.js +4 -0
- package/lib/server/task-handler.js +51 -0
- package/lib/store.js +25 -2
- package/lib/time-machine.js +214 -0
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Integration service for @goodandready/dsh-time-machine snapshot and rollback capabilities.
|
|
2
|
+
// Provides automatic inception snapshots on task start and one-click rollback to clean state.
|
|
3
|
+
// Degrades gracefully when @goodandready/dsh-time-machine plugin is not installed in DSH profile.
|
|
4
|
+
|
|
5
|
+
import { resolveTaskWorktreePath } from './worktree.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check whether timeMachineEngine is available in the Cordis context.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} ctx Cordis context or harness
|
|
11
|
+
* @returns {boolean} true if time machine engine is available
|
|
12
|
+
*/
|
|
13
|
+
export function isTimeMachineAvailable(ctx) {
|
|
14
|
+
if (!ctx || typeof ctx !== 'object') return false
|
|
15
|
+
const engine = ctx.timeMachineEngine || (typeof ctx.get === 'function' ? (ctx.get('timeMachineEngine') || ctx.get('timeMachine')) : null)
|
|
16
|
+
return !!(engine && (typeof engine.createSnapshot === 'function' || typeof engine.rollbackSnapshot === 'function' || typeof engine.listSnapshots === 'function'))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the timeMachineEngine instance from context.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} ctx Cordis context
|
|
23
|
+
* @returns {object|null}
|
|
24
|
+
*/
|
|
25
|
+
export function getTimeMachineEngine(ctx) {
|
|
26
|
+
if (!ctx || typeof ctx !== 'object') return null
|
|
27
|
+
const engine = ctx.timeMachineEngine || (typeof ctx.get === 'function' ? (ctx.get('timeMachineEngine') || ctx.get('timeMachine')) : null)
|
|
28
|
+
if (engine && (typeof engine.createSnapshot === 'function' || typeof engine.rollbackSnapshot === 'function' || typeof engine.listSnapshots === 'function')) {
|
|
29
|
+
return engine
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the directory path to snapshot for a task.
|
|
36
|
+
*
|
|
37
|
+
* @param {{task: object, config?: object, env?: object, cwdOf?: Function}} options
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function resolveSnapshotDir({ task, config, env, cwdOf }) {
|
|
41
|
+
if (task?.worktree && typeof task.worktree === 'string' && task.worktree.trim() !== '') {
|
|
42
|
+
return task.worktree.trim()
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const resolved = resolveTaskWorktreePath({ task, config, env, cwdOf: cwdOf || (() => process.cwd()) })
|
|
46
|
+
if (resolved) return resolved
|
|
47
|
+
} catch {
|
|
48
|
+
/* fallback to cwd */
|
|
49
|
+
}
|
|
50
|
+
return typeof cwdOf === 'function' ? cwdOf() : process.cwd()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Automatically create inception snapshot when task moves to in-progress.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ctx: object, task: object, config?: object, env?: object, cwdOf?: Function, store?: object, logger?: object}} options
|
|
57
|
+
* @returns {Promise<{available: boolean, created: boolean, snapshotId?: string, alreadyExists?: boolean, snapshot?: object, error?: string}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function createInceptionSnapshot({ ctx, task, config, env, cwdOf, store, logger }) {
|
|
60
|
+
const engine = getTimeMachineEngine(ctx)
|
|
61
|
+
if (!engine) {
|
|
62
|
+
return { available: false, created: false }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (task?.inceptionSnapshotId && typeof task.inceptionSnapshotId === 'string' && task.inceptionSnapshotId.trim() !== '') {
|
|
66
|
+
return {
|
|
67
|
+
available: true,
|
|
68
|
+
created: false,
|
|
69
|
+
alreadyExists: true,
|
|
70
|
+
snapshotId: task.inceptionSnapshotId.trim(),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const targetDir = resolveSnapshotDir({ task, config, env, cwdOf })
|
|
75
|
+
const label = `Inception: ${task?.id || 'task'} (${task?.title || 'untitled'})`
|
|
76
|
+
const sessionId = `task-${task?.id || 'default'}`
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const snapshot = await engine.createSnapshot(label, {
|
|
80
|
+
sessionId,
|
|
81
|
+
cwd: targetDir,
|
|
82
|
+
skipIfNoChanges: false,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (snapshot?.id && store && typeof store.updateTask === 'function' && task?.id) {
|
|
86
|
+
store.updateTask(task.id, { inceptionSnapshotId: snapshot.id })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
available: true,
|
|
91
|
+
created: true,
|
|
92
|
+
snapshotId: snapshot?.id,
|
|
93
|
+
snapshot,
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger?.warn?.('[dsh-kanban] Failed to create inception snapshot:', err?.message || err)
|
|
97
|
+
return {
|
|
98
|
+
available: true,
|
|
99
|
+
created: false,
|
|
100
|
+
error: err?.message || String(err),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Manually create a checkpoint snapshot for a task.
|
|
107
|
+
*
|
|
108
|
+
* @param {{ctx: object, task: object, label?: string, config?: object, env?: object, cwdOf?: Function, logger?: object}} options
|
|
109
|
+
* @returns {Promise<{available: boolean, created: boolean, snapshot?: object, error?: string}>}
|
|
110
|
+
*/
|
|
111
|
+
export async function createCheckpointSnapshot({ ctx, task, label, config, env, cwdOf, logger }) {
|
|
112
|
+
const engine = getTimeMachineEngine(ctx)
|
|
113
|
+
if (!engine) {
|
|
114
|
+
return { available: false, created: false }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const targetDir = resolveSnapshotDir({ task, config, env, cwdOf })
|
|
118
|
+
const snapshotLabel = `Checkpoint: ${label || 'manual'} (${task?.id || 'task'})`
|
|
119
|
+
const sessionId = `task-${task?.id || 'default'}`
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const snapshot = await engine.createSnapshot(snapshotLabel, {
|
|
123
|
+
sessionId,
|
|
124
|
+
cwd: targetDir,
|
|
125
|
+
skipIfNoChanges: false,
|
|
126
|
+
})
|
|
127
|
+
return {
|
|
128
|
+
available: true,
|
|
129
|
+
created: true,
|
|
130
|
+
snapshot,
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
logger?.warn?.('[dsh-kanban] Failed to create checkpoint snapshot:', err?.message || err)
|
|
134
|
+
return {
|
|
135
|
+
available: true,
|
|
136
|
+
created: false,
|
|
137
|
+
error: err?.message || String(err),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Roll back task files to a snapshot (default: inception snapshot).
|
|
144
|
+
*
|
|
145
|
+
* @param {{ctx: object, task: object, snapshotId?: string, config?: object, env?: object, cwdOf?: Function, logger?: object}} options
|
|
146
|
+
* @returns {Promise<{available: boolean, rolledBack: boolean, snapshotId?: string, result?: object, error?: string, message?: string}>}
|
|
147
|
+
*/
|
|
148
|
+
export async function rollbackToSnapshot({ ctx, task, snapshotId, config, env, cwdOf, logger }) {
|
|
149
|
+
const engine = getTimeMachineEngine(ctx)
|
|
150
|
+
if (!engine) {
|
|
151
|
+
return {
|
|
152
|
+
available: false,
|
|
153
|
+
rolledBack: false,
|
|
154
|
+
error: 'time-machine-unavailable',
|
|
155
|
+
message: 'Plugin @goodandready/dsh-time-machine is not installed or active in this profile.',
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const targetId = snapshotId || task?.inceptionSnapshotId
|
|
160
|
+
if (!targetId || typeof targetId !== 'string' || targetId.trim() === '') {
|
|
161
|
+
return {
|
|
162
|
+
available: true,
|
|
163
|
+
rolledBack: false,
|
|
164
|
+
error: 'no-snapshot-id',
|
|
165
|
+
message: 'No snapshot ID specified and task has no inception snapshot.',
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const targetDir = resolveSnapshotDir({ task, config, env, cwdOf })
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await engine.rollbackSnapshot(String(targetId).trim(), {
|
|
173
|
+
confirm: true,
|
|
174
|
+
cwd: targetDir,
|
|
175
|
+
})
|
|
176
|
+
return {
|
|
177
|
+
available: true,
|
|
178
|
+
rolledBack: true,
|
|
179
|
+
snapshotId: targetId,
|
|
180
|
+
result,
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
logger?.error?.('[dsh-kanban] Failed to rollback snapshot:', err?.message || err)
|
|
184
|
+
return {
|
|
185
|
+
available: true,
|
|
186
|
+
rolledBack: false,
|
|
187
|
+
error: 'rollback-failed',
|
|
188
|
+
message: err?.message || String(err),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* List snapshots for a task.
|
|
195
|
+
*
|
|
196
|
+
* @param {{ctx: object, task: object}} options
|
|
197
|
+
* @returns {Promise<Array<object>>}
|
|
198
|
+
*/
|
|
199
|
+
export async function listTaskSnapshots({ ctx, task }) {
|
|
200
|
+
const engine = getTimeMachineEngine(ctx)
|
|
201
|
+
if (!engine || !task?.id) return []
|
|
202
|
+
|
|
203
|
+
const sessionId = `task-${task.id}`
|
|
204
|
+
try {
|
|
205
|
+
if (typeof engine.listSnapshots === 'function') {
|
|
206
|
+
const list = engine.listSnapshots(sessionId)
|
|
207
|
+
return Array.isArray(list) ? list : []
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
/* ignore error */
|
|
211
|
+
}
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
4
4
|
"description": "Visual Kanban board for DeepSeek Harness: Gitea and GitHub-backed tasks, workflow columns, and a dedicated agent session per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|