@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.
@@ -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.35",
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",