@stilero/bankan 1.0.0

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,55 @@
1
+ import { tmpdir, homedir } from 'node:os';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const ROOT_DIR = join(__dirname, '..', '..');
7
+ const APP_NAME = 'bankan';
8
+
9
+ function isPackagedRuntime() {
10
+ return process.env.BANKAN_RUNTIME_MODE === 'packaged';
11
+ }
12
+
13
+ function getDefaultAppDataDir() {
14
+ if (process.platform === 'darwin') {
15
+ return join(homedir(), 'Library', 'Application Support', APP_NAME);
16
+ }
17
+
18
+ if (process.platform === 'win32') {
19
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), APP_NAME);
20
+ }
21
+
22
+ return join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), APP_NAME);
23
+ }
24
+
25
+ export function getRuntimePaths() {
26
+ const packaged = isPackagedRuntime();
27
+ const dataDir = packaged
28
+ ? resolve(process.env.BANKAN_HOME || getDefaultAppDataDir())
29
+ : join(ROOT_DIR, '.data');
30
+ const tempRoot = packaged
31
+ ? join(tmpdir(), APP_NAME)
32
+ : dataDir;
33
+
34
+ return {
35
+ appName: APP_NAME,
36
+ packaged,
37
+ rootDir: ROOT_DIR,
38
+ dataDir,
39
+ envFile: packaged ? join(dataDir, '.env.local') : join(ROOT_DIR, '.env.local'),
40
+ settingsFile: join(dataDir, 'config.json'),
41
+ tasksFile: join(dataDir, 'tasks.json'),
42
+ plansDir: join(dataDir, 'plans'),
43
+ workspacesDir: join(dataDir, 'workspaces'),
44
+ bridgesDir: join(tempRoot, 'terminal-bridges'),
45
+ clientDistDir: join(ROOT_DIR, 'client', 'dist'),
46
+ };
47
+ }
48
+
49
+ export function getAppDataDir() {
50
+ return getRuntimePaths().dataDir;
51
+ }
52
+
53
+ export function getEnvFilePath() {
54
+ return getRuntimePaths().envFile;
55
+ }
@@ -0,0 +1,287 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import bus from './events.js';
5
+ import { getRuntimeStatePaths } from './config.js';
6
+
7
+ const runtimePaths = getRuntimeStatePaths();
8
+ const DATA_DIR = runtimePaths.dataDir;
9
+ const TASKS_FILE = runtimePaths.tasksFile;
10
+ const PLANS_DIR = runtimePaths.plansDir;
11
+
12
+ function statusToStage(status) {
13
+ if (['workspace_setup', 'planning', 'awaiting_approval'].includes(status)) return 'planning';
14
+ if (['queued', 'implementing'].includes(status)) return 'implementation';
15
+ if (status === 'review') return 'review';
16
+ if (status === 'done') return 'done';
17
+ if (['backlog', 'aborted'].includes(status)) return 'backlog';
18
+ return null;
19
+ }
20
+
21
+ function isLikelyRemoteRepoRef(value) {
22
+ if (typeof value !== 'string') return false;
23
+ const trimmed = value.trim();
24
+ return /^https?:\/\//i.test(trimmed) || /^git@[^:]+:.+/i.test(trimmed) || /^ssh:\/\//i.test(trimmed);
25
+ }
26
+
27
+ function isLegacyPlannerPathBlocker(task) {
28
+ if (task.status !== 'blocked' || task.workspacePath) return false;
29
+ if (typeof task.blockedReason !== 'string' || !task.blockedReason.trim()) return false;
30
+
31
+ const reason = task.blockedReason.trim();
32
+
33
+ if (reason.startsWith('Invalid repository path:')) {
34
+ return isLikelyRemoteRepoRef(task.repoPath);
35
+ }
36
+
37
+ if (reason.startsWith('Invalid planner working directory:')) {
38
+ const blockedPath = reason.slice('Invalid planner working directory:'.length).trim();
39
+ return blockedPath === task.repoPath && isLikelyRemoteRepoRef(task.repoPath);
40
+ }
41
+
42
+ return false;
43
+ }
44
+
45
+ class TaskStore {
46
+ constructor() {
47
+ this.tasks = [];
48
+ this._ensureDirs();
49
+ this._load();
50
+ }
51
+
52
+ _ensureDirs() {
53
+ mkdirSync(DATA_DIR, { recursive: true });
54
+ mkdirSync(PLANS_DIR, { recursive: true });
55
+ }
56
+
57
+ _load() {
58
+ try {
59
+ if (existsSync(TASKS_FILE)) {
60
+ this.tasks = JSON.parse(readFileSync(TASKS_FILE, 'utf-8'));
61
+ this.tasks = this.tasks.map(task => {
62
+ const normalized = {
63
+ reviewCycleCount: 0,
64
+ lastActiveStage: statusToStage(task.status) || 'backlog',
65
+ previousStatus: null,
66
+ totalTokens: 0,
67
+ startedAt: null,
68
+ completedAt: null,
69
+ ...task,
70
+ };
71
+
72
+ if (normalized.status === 'awaiting_human_review') {
73
+ normalized.status = 'done';
74
+ }
75
+ if (normalized.status === 'done') {
76
+ normalized.assignedTo = null;
77
+ normalized.workspacePath = null;
78
+ }
79
+ if (typeof normalized.reviewCycleCount !== 'number' || normalized.reviewCycleCount < 0) {
80
+ normalized.reviewCycleCount = 0;
81
+ }
82
+ if (typeof normalized.totalTokens !== 'number' || normalized.totalTokens < 0) {
83
+ normalized.totalTokens = 0;
84
+ }
85
+ if (normalized.startedAt !== null && typeof normalized.startedAt !== 'string') {
86
+ normalized.startedAt = null;
87
+ }
88
+ if (normalized.completedAt !== null && typeof normalized.completedAt !== 'string') {
89
+ normalized.completedAt = null;
90
+ }
91
+ if (!normalized.lastActiveStage) {
92
+ normalized.lastActiveStage = statusToStage(normalized.status) || 'backlog';
93
+ }
94
+ if (normalized.previousStatus === undefined) {
95
+ normalized.previousStatus = null;
96
+ }
97
+
98
+ return normalized;
99
+ });
100
+ }
101
+ } catch {
102
+ this.tasks = [];
103
+ }
104
+ }
105
+
106
+ _save() {
107
+ writeFileSync(TASKS_FILE, JSON.stringify(this.tasks, null, 2));
108
+ }
109
+
110
+ addTask({ title, priority = 'medium', description = '', repoPath = '' }) {
111
+ const task = {
112
+ id: 'T-' + uuidv4().slice(0, 6).toUpperCase(),
113
+ title,
114
+ priority,
115
+ description,
116
+ repoPath,
117
+ status: 'backlog',
118
+ branch: null,
119
+ plan: null,
120
+ review: null,
121
+ prUrl: null,
122
+ prNumber: null,
123
+ assignedTo: null,
124
+ reviewFeedback: null,
125
+ planFeedback: null,
126
+ blockedReason: null,
127
+ workspacePath: null,
128
+ reviewCycleCount: 0,
129
+ lastActiveStage: 'backlog',
130
+ previousStatus: null,
131
+ totalTokens: 0,
132
+ startedAt: null,
133
+ completedAt: null,
134
+ progress: 0,
135
+ createdAt: new Date().toISOString(),
136
+ updatedAt: new Date().toISOString(),
137
+ log: [{ ts: new Date().toISOString(), message: 'Task created' }],
138
+ };
139
+ this.tasks.push(task);
140
+ this._save();
141
+ bus.emit('task:added', task);
142
+ bus.emit('tasks:changed', this.tasks);
143
+ return task;
144
+ }
145
+
146
+ getTask(id) {
147
+ return this.tasks.find(t => t.id === id) || null;
148
+ }
149
+
150
+ getAllTasks() {
151
+ return this.tasks;
152
+ }
153
+
154
+ updateTask(id, updates) {
155
+ const task = this.getTask(id);
156
+ if (!task) return null;
157
+ const nextStatus = updates.status;
158
+ if (nextStatus) {
159
+ const nextStage = statusToStage(nextStatus);
160
+ if (nextStage) {
161
+ updates.lastActiveStage = nextStage;
162
+ }
163
+ }
164
+ Object.assign(task, updates, { updatedAt: new Date().toISOString() });
165
+ if (nextStatus) {
166
+ task.log.push({ ts: new Date().toISOString(), message: `Status changed to ${updates.status}` });
167
+ }
168
+ this._save();
169
+ bus.emit('task:updated', task);
170
+ bus.emit('tasks:changed', this.tasks);
171
+ return task;
172
+ }
173
+
174
+ savePlan(taskId, planText) {
175
+ writeFileSync(join(PLANS_DIR, `${taskId}.md`), planText);
176
+ }
177
+
178
+ removePlan(taskId) {
179
+ rmSync(join(PLANS_DIR, `${taskId}.md`), { force: true });
180
+ }
181
+
182
+ appendLog(id, message) {
183
+ const task = this.getTask(id);
184
+ if (!task) return null;
185
+ task.log.push({ ts: new Date().toISOString(), message });
186
+ task.updatedAt = new Date().toISOString();
187
+ this._save();
188
+ bus.emit('task:updated', task);
189
+ bus.emit('tasks:changed', this.tasks);
190
+ return task;
191
+ }
192
+
193
+ updateTaskTokens(id, totalTokens) {
194
+ const task = this.getTask(id);
195
+ if (!task) return null;
196
+ if (typeof totalTokens !== 'number' || totalTokens < task.totalTokens) return task;
197
+ task.totalTokens = totalTokens;
198
+ task.updatedAt = new Date().toISOString();
199
+ this._save();
200
+ bus.emit('task:updated', task);
201
+ bus.emit('tasks:changed', this.tasks);
202
+ return task;
203
+ }
204
+
205
+ deleteTask(id) {
206
+ const index = this.tasks.findIndex(t => t.id === id);
207
+ if (index === -1) return null;
208
+ const [task] = this.tasks.splice(index, 1);
209
+ this._save();
210
+ bus.emit('tasks:changed', this.tasks);
211
+ return task;
212
+ }
213
+
214
+ restartRecovery() {
215
+ const recoveryMap = {
216
+ planning: 'backlog',
217
+ workspace_setup: 'backlog',
218
+ queued: 'queued',
219
+ implementing: 'queued',
220
+ review: 'review',
221
+ };
222
+ let changed = false;
223
+ for (const task of this.tasks) {
224
+ if (!task.lastActiveStage) {
225
+ task.lastActiveStage = statusToStage(task.status) || 'backlog';
226
+ changed = true;
227
+ }
228
+ if (typeof task.reviewCycleCount !== 'number' || task.reviewCycleCount < 0) {
229
+ task.reviewCycleCount = 0;
230
+ changed = true;
231
+ }
232
+ if (typeof task.totalTokens !== 'number' || task.totalTokens < 0) {
233
+ task.totalTokens = 0;
234
+ changed = true;
235
+ }
236
+ if (task.status === 'awaiting_human_review') {
237
+ task.status = 'done';
238
+ task.assignedTo = null;
239
+ task.workspacePath = null;
240
+ task.lastActiveStage = 'done';
241
+ task.updatedAt = new Date().toISOString();
242
+ task.log.push({ ts: new Date().toISOString(), message: 'Restart recovery: normalized awaiting_human_review to done' });
243
+ changed = true;
244
+ continue;
245
+ }
246
+ // Leave paused tasks as paused but clear assignedTo
247
+ if (task.status === 'paused') {
248
+ if (task.assignedTo) {
249
+ task.assignedTo = null;
250
+ task.updatedAt = new Date().toISOString();
251
+ changed = true;
252
+ }
253
+ continue;
254
+ }
255
+ const resetTo = recoveryMap[task.status];
256
+ if (resetTo) {
257
+ task.status = resetTo;
258
+ task.assignedTo = null;
259
+ task.lastActiveStage = statusToStage(resetTo) || task.lastActiveStage;
260
+ task.updatedAt = new Date().toISOString();
261
+ task.log.push({ ts: new Date().toISOString(), message: `Restart recovery: reset to ${resetTo}` });
262
+ changed = true;
263
+ }
264
+
265
+ if (isLegacyPlannerPathBlocker(task)) {
266
+ task.status = 'backlog';
267
+ task.assignedTo = null;
268
+ task.blockedReason = null;
269
+ task.lastActiveStage = 'backlog';
270
+ task.previousStatus = null;
271
+ task.updatedAt = new Date().toISOString();
272
+ task.log.push({
273
+ ts: new Date().toISOString(),
274
+ message: 'Restart recovery: reset legacy planner path blocker to backlog',
275
+ });
276
+ changed = true;
277
+ }
278
+ }
279
+ if (changed) {
280
+ this._save();
281
+ bus.emit('tasks:changed', this.tasks);
282
+ }
283
+ }
284
+ }
285
+
286
+ const store = new TaskStore();
287
+ export default store;