@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.3

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.
Files changed (76) hide show
  1. package/.agent/workflows/dev-workflow-bugfix.md +250 -250
  2. package/.agent/workflows/dev-workflow-feature.md +201 -201
  3. package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
  4. package/.agent/workflows/dev-workflow-refactor.md +238 -238
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
  7. package/.github/pull_request_template.md +22 -22
  8. package/.github/workflows/npm-publish.yml +50 -50
  9. package/.husky/commit-msg +0 -0
  10. package/CHANGELOG.md +748 -741
  11. package/CODE_OF_CONDUCT.md +79 -79
  12. package/CONTRIBUTING.md +60 -60
  13. package/LICENSE +21 -21
  14. package/README.md +632 -632
  15. package/SECURITY.md +20 -20
  16. package/db/index.js +204 -204
  17. package/docs/NETLIFY_DEPLOYMENT.md +125 -125
  18. package/docs/PLESK_DEPLOYMENT.md +170 -170
  19. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
  20. package/docs/antigravity/EXAMPLES.md +535 -535
  21. package/docs/antigravity/GETTING-STARTED.md +292 -292
  22. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  23. package/docs/antigravity/WALKTHROUGH.md +25 -25
  24. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  25. package/docs/product/PRD.md +67 -67
  26. package/docs/release-notes/RELEASE_NOTES.md +226 -226
  27. package/docs/web-dashboard.md +175 -175
  28. package/exec.js +49 -49
  29. package/index.js +279 -279
  30. package/install.js +73 -73
  31. package/package.json +8 -8
  32. package/release-wrapper.js +192 -192
  33. package/scripts/build-docs.js +63 -63
  34. package/scripts/hello-world.js +1 -1
  35. package/scripts/release.cjs +252 -252
  36. package/scripts/release.js +252 -0
  37. package/scripts/update-release-notes.cjs +178 -178
  38. package/scripts/update-release-notes.js +182 -0
  39. package/server.json +23 -23
  40. package/tailwind.config.js +8 -8
  41. package/tests/build.test.js +83 -83
  42. package/tests/exec.test.js +35 -35
  43. package/tests/handlers.test.js +1058 -1058
  44. package/tests/hello-world.test.js +13 -13
  45. package/tests/mcpRegistry.test.js +65 -65
  46. package/tests/patch-release.test.js +7 -0
  47. package/tests/release-wrapper.test.js +107 -107
  48. package/tests/workflowState.test.js +505 -505
  49. package/tools/definitions.js +332 -332
  50. package/tools/handlers.js +1019 -1019
  51. package/vite.config.js +43 -43
  52. package/vite.web.config.js +23 -23
  53. package/web/README.md +36 -36
  54. package/web/app/docs/page.tsx +202 -202
  55. package/web/app/globals.css +90 -90
  56. package/web/app/layout.tsx +36 -36
  57. package/web/app/page.tsx +15 -15
  58. package/web/components/ApiReference.tsx +20 -20
  59. package/web/components/Features.tsx +61 -61
  60. package/web/components/Footer.tsx +9 -9
  61. package/web/components/Hero.tsx +96 -96
  62. package/web/components/History.tsx +143 -143
  63. package/web/components/Navbar.tsx +64 -64
  64. package/web/components/Terminal.tsx +114 -114
  65. package/web/eslint.config.mjs +18 -18
  66. package/web/lib/db.js +131 -131
  67. package/web/lib/docs.js +73 -73
  68. package/web/netlify.toml +3 -3
  69. package/web/next.config.ts +12 -12
  70. package/web/package-lock.json +6560 -6560
  71. package/web/package.json +28 -28
  72. package/web/postcss.config.mjs +9 -9
  73. package/web/server.js +31 -31
  74. package/web/tailwind.config.ts +19 -19
  75. package/web/tsconfig.json +42 -42
  76. package/workflow-state.js +612 -612
package/workflow-state.js CHANGED
@@ -1,612 +1,612 @@
1
- import fs from "fs/promises";
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
3
- import path from "path";
4
- import { fileURLToPath } from "url";
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- const STATE_DIR = ".state";
9
- const STATE_FILENAME = "workflow-state.json";
10
- const LEGACY_STATE_FILENAME = ".workflow-state.json";
11
- const PROJECT_SUMMARY_FILENAME = "project-summary.json";
12
- const USER_ID_FILENAME = "user-id";
13
-
14
- let cachedUserId = null;
15
- let cachedUserStateDir = null;
16
-
17
- function generateUserId() {
18
- const timestamp = Date.now().toString(16);
19
- const randomSuffix = Math.random().toString(16).slice(2, 10);
20
- return `user-${timestamp}-${randomSuffix}`;
21
- }
22
-
23
- function resolveUserId(stateDir) {
24
- const envUserId = (process.env.DEV_WORKFLOW_USER_ID || "").trim();
25
- if (envUserId) {
26
- cachedUserId = envUserId;
27
- cachedUserStateDir = stateDir;
28
- return envUserId;
29
- }
30
-
31
- if (cachedUserId && cachedUserStateDir === stateDir) {
32
- return cachedUserId;
33
- }
34
-
35
- const userIdFile = path.join(stateDir, USER_ID_FILENAME);
36
-
37
- try {
38
- if (existsSync(userIdFile)) {
39
- const stored = readFileSync(userIdFile, "utf-8").trim();
40
- if (stored) {
41
- cachedUserId = stored;
42
- cachedUserStateDir = stateDir;
43
- return stored;
44
- }
45
- }
46
- } catch {
47
- // Ignore read errors; we'll generate a new ID below.
48
- }
49
-
50
- const usersDir = path.join(stateDir, "users");
51
-
52
- try {
53
- const entries = readdirSync(usersDir, { withFileTypes: true });
54
- const candidateDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
55
- if (candidateDirs.length === 1) {
56
- const legacyId = candidateDirs[0];
57
- try {
58
- mkdirSync(stateDir, { recursive: true });
59
- writeFileSync(userIdFile, legacyId, "utf-8");
60
- } catch {
61
- // Persisting the legacy ID is best-effort.
62
- }
63
- cachedUserId = legacyId;
64
- cachedUserStateDir = stateDir;
65
- return legacyId;
66
- }
67
- } catch (error) {
68
- if (error?.code !== "ENOENT") {
69
- // Unexpected error accessing users directory – fall through to generate new ID.
70
- }
71
- }
72
-
73
- const legacyDefaultPath = path.join(usersDir, "default", STATE_FILENAME);
74
- if (existsSync(legacyDefaultPath)) {
75
- try {
76
- mkdirSync(stateDir, { recursive: true });
77
- writeFileSync(userIdFile, "default", "utf-8");
78
- } catch {
79
- // Best-effort persistence; continue with in-memory fallback.
80
- }
81
- cachedUserId = "default";
82
- cachedUserStateDir = stateDir;
83
- return "default";
84
- }
85
-
86
- const generatedId = generateUserId();
87
- try {
88
- mkdirSync(stateDir, { recursive: true });
89
- writeFileSync(userIdFile, generatedId, "utf-8");
90
- } catch {
91
- // Ignore persistence errors; continue with in-memory ID.
92
- }
93
-
94
- cachedUserId = generatedId;
95
- cachedUserStateDir = stateDir;
96
- return generatedId;
97
- }
98
-
99
- function hasProjectMarkers(candidate) {
100
- if (!candidate) {
101
- return false;
102
- }
103
-
104
- const resolved = path.resolve(candidate);
105
- return (
106
- existsSync(path.join(resolved, "package.json")) || existsSync(path.join(resolved, ".git"))
107
- );
108
- }
109
-
110
- function normalizeAndStoreCandidate(list, candidate) {
111
- if (!candidate) {
112
- return;
113
- }
114
-
115
- const resolved = path.resolve(candidate);
116
- if (!list.includes(resolved)) {
117
- list.push(resolved);
118
- }
119
- }
120
-
121
- function addAncestorCandidates(list, start) {
122
- if (!start) {
123
- return;
124
- }
125
-
126
- let current = path.resolve(start);
127
- while (!isRootPath(current)) {
128
- const parent = path.dirname(current);
129
- if (parent === current) {
130
- break;
131
- }
132
- if (!isRootPath(parent)) {
133
- normalizeAndStoreCandidate(list, parent);
134
- }
135
- current = parent;
136
- }
137
- }
138
-
139
- function selectProjectRoot(candidates, moduleRoot) {
140
- const cwdResolved = path.resolve(process.cwd());
141
- const prioritized = [];
142
-
143
- for (const candidate of candidates) {
144
- if (!candidate) {
145
- continue;
146
- }
147
-
148
- const resolved = path.resolve(candidate);
149
- if (prioritized.some((entry) => entry.resolved === resolved)) {
150
- continue;
151
- }
152
-
153
- prioritized.push({
154
- resolved,
155
- hasMarkers: hasProjectMarkers(resolved),
156
- isRoot: isRootPath(resolved),
157
- isCwdAncestor: cwdResolved === resolved || cwdResolved.startsWith(`${resolved}${path.sep}`),
158
- });
159
- }
160
-
161
- const ancestorWithMarkers = prioritized.find((entry) => entry.isCwdAncestor && entry.hasMarkers);
162
- if (ancestorWithMarkers) {
163
- return ancestorWithMarkers.resolved;
164
- }
165
-
166
- const ancestorWithoutMarkers = prioritized
167
- .filter((entry) => entry.isCwdAncestor && !entry.hasMarkers && !entry.isRoot)
168
- .sort((a, b) => b.resolved.length - a.resolved.length);
169
- if (ancestorWithoutMarkers.length > 0) {
170
- return ancestorWithoutMarkers[0].resolved;
171
- }
172
-
173
- if (moduleRoot) {
174
- const moduleResolved = path.resolve(moduleRoot);
175
- const moduleEntry = prioritized.find((entry) => entry.resolved === moduleResolved && !entry.isRoot);
176
- if (moduleEntry) {
177
- return moduleEntry.resolved;
178
- }
179
- }
180
-
181
- const withMarkers = prioritized.find((entry) => entry.hasMarkers);
182
- if (withMarkers) {
183
- return withMarkers.resolved;
184
- }
185
-
186
- const nonRoot = prioritized.find((entry) => !entry.isRoot);
187
- if (nonRoot) {
188
- return nonRoot.resolved;
189
- }
190
-
191
- return prioritized.length > 0 ? prioritized[0].resolved : null;
192
- }
193
-
194
- function getUserScopedDir(baseDir) {
195
- const userId = resolveUserId(baseDir);
196
- return path.join(baseDir, "users", userId);
197
- }
198
-
199
- function getCurrentUserId() {
200
- const envUserId = (process.env.DEV_WORKFLOW_USER_ID || "").trim();
201
- if (envUserId) {
202
- return envUserId;
203
- }
204
-
205
- if (cachedUserId) {
206
- return cachedUserId;
207
- }
208
-
209
- if (cachedUserStateDir) {
210
- return resolveUserId(cachedUserStateDir);
211
- }
212
-
213
- return "default";
214
- }
215
-
216
- async function migrateLegacyStateFile(targetPath) {
217
- const stateDir = path.dirname(targetPath);
218
- if (path.basename(stateDir) !== STATE_DIR) {
219
- return;
220
- }
221
-
222
- const projectRoot = path.dirname(stateDir);
223
- const legacyPath = path.join(projectRoot, LEGACY_STATE_FILENAME);
224
-
225
- if (legacyPath === targetPath) {
226
- return;
227
- }
228
-
229
- try {
230
- await fs.access(legacyPath);
231
- } catch {
232
- return;
233
- }
234
-
235
- await fs.mkdir(stateDir, { recursive: true });
236
-
237
- try {
238
- await fs.access(targetPath);
239
- await fs.unlink(legacyPath);
240
- return;
241
- } catch {
242
- // New state file doesn't exist yet; fall through to migrate.
243
- }
244
-
245
- await fs.rename(legacyPath, targetPath);
246
- }
247
-
248
- function isRootPath(candidate) {
249
- if (!candidate) {
250
- return true;
251
- }
252
-
253
- const resolved = path.resolve(candidate);
254
- return path.parse(resolved).root === resolved;
255
- }
256
-
257
- function findProjectRoot(startDir) {
258
- if (!startDir) {
259
- return null;
260
- }
261
-
262
- let current = path.resolve(startDir);
263
- const { root } = path.parse(current);
264
-
265
- while (true) {
266
- const hasPackageJson = existsSync(path.join(current, "package.json"));
267
- const hasGitDir = existsSync(path.join(current, ".git"));
268
- if (hasPackageJson || hasGitDir) {
269
- return current;
270
- }
271
-
272
- if (current === root) {
273
- return null;
274
- }
275
-
276
- const parent = path.dirname(current);
277
- if (parent === current) {
278
- return null;
279
- }
280
-
281
- current = parent;
282
- }
283
- }
284
-
285
- function getCompatibilityPaths(primaryPath) {
286
- const resolvedPrimary = path.resolve(primaryPath);
287
- const candidates = new Set();
288
- const moduleDir = __dirname;
289
-
290
- const directCandidate = path.resolve(moduleDir, STATE_DIR, STATE_FILENAME);
291
- if (directCandidate !== resolvedPrimary) {
292
- candidates.add(directCandidate);
293
- }
294
-
295
- const possibleDistDirs = [
296
- path.join(moduleDir, "dist"),
297
- path.join(path.dirname(moduleDir), "dist"),
298
- ];
299
-
300
- for (const distDir of possibleDistDirs) {
301
- const candidate = path.resolve(distDir, STATE_DIR, STATE_FILENAME);
302
- if (candidate !== resolvedPrimary) {
303
- candidates.add(candidate);
304
- }
305
- }
306
-
307
- return Array.from(candidates);
308
- }
309
-
310
- async function pathExists(filePath) {
311
- try {
312
- await fs.access(filePath);
313
- return true;
314
- } catch {
315
- return false;
316
- }
317
- }
318
-
319
- async function ensureCompatibilityLinks(primaryPath) {
320
- const resolvedPrimary = path.resolve(primaryPath);
321
-
322
- if (!(await pathExists(resolvedPrimary))) {
323
- return;
324
- }
325
-
326
- const compatibilityPaths = getCompatibilityPaths(resolvedPrimary);
327
-
328
- for (const compatPath of compatibilityPaths) {
329
- try {
330
- await fs.mkdir(path.dirname(compatPath), { recursive: true });
331
- } catch {
332
- // Ignore directory creation errors; we'll surface them on actual writes.
333
- }
334
-
335
- try {
336
- const compatStat = await fs.lstat(compatPath);
337
- if (compatStat.isSymbolicLink()) {
338
- const target = await fs.readlink(compatPath);
339
- if (path.resolve(path.dirname(compatPath), target) === resolvedPrimary) {
340
- continue;
341
- }
342
- await fs.unlink(compatPath);
343
- } else if (compatStat.isFile()) {
344
- const [primaryStat, existingStat] = await Promise.all([
345
- fs.stat(resolvedPrimary),
346
- fs.stat(compatPath),
347
- ]);
348
- if (primaryStat.dev === existingStat.dev && primaryStat.ino === existingStat.ino) {
349
- continue;
350
- }
351
- await fs.unlink(compatPath);
352
- } else {
353
- await fs.unlink(compatPath);
354
- }
355
- } catch (error) {
356
- if (error.code !== "ENOENT") {
357
- // Unexpected error accessing legacy path; skip mirroring to avoid breaking primary file.
358
- continue;
359
- }
360
- }
361
-
362
- try {
363
- await fs.link(resolvedPrimary, compatPath);
364
- continue;
365
- } catch (error) {
366
- if (!["EEXIST", "EXDEV", "EPERM"].includes(error.code)) {
367
- continue;
368
- }
369
- if (error.code === "EEXIST") {
370
- continue;
371
- }
372
- }
373
-
374
- try {
375
- await fs.symlink(resolvedPrimary, compatPath);
376
- continue;
377
- } catch (error) {
378
- if (error.code === "EEXIST") {
379
- continue;
380
- }
381
- if (!["EPERM", "ENOTSUP", "EEXIST"].includes(error.code)) {
382
- // If symlink fails for another reason, skip to avoid interfering with primary file.
383
- continue;
384
- }
385
- }
386
-
387
- try {
388
- await fs.copyFile(resolvedPrimary, compatPath);
389
- } catch {
390
- // Final fallback failed; ignore to avoid disrupting primary save.
391
- }
392
- }
393
- }
394
-
395
- export function resolveStateFile() {
396
- const customPath = process.env.DEV_WORKFLOW_STATE_FILE;
397
- if (customPath) {
398
- return path.resolve(customPath);
399
- }
400
-
401
- const moduleRoot = path.resolve(__dirname, "..");
402
- const cwdResolved = path.resolve(process.cwd());
403
- const initCwd = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : null;
404
-
405
- // 1. Prefer the project rooted at the current working directory
406
- let projectRoot = findProjectRoot(cwdResolved);
407
-
408
- // 2. Fall back to the project rooted at INIT_CWD if different
409
- if (!projectRoot && initCwd) {
410
- projectRoot = findProjectRoot(initCwd);
411
- }
412
-
413
- // 3. Fall back to the project containing this module
414
- if (!projectRoot) {
415
- projectRoot = findProjectRoot(moduleRoot);
416
- }
417
-
418
- // 4. Final fallback: use a non-root working directory or the module root
419
- if (!projectRoot) {
420
- projectRoot = !isRootPath(cwdResolved) ? cwdResolved : moduleRoot;
421
- }
422
-
423
- const stateDir = path.join(projectRoot, STATE_DIR);
424
- const userDir = getUserScopedDir(stateDir);
425
- return path.join(userDir, STATE_FILENAME);
426
- }
427
-
428
- export class WorkflowState {
429
- constructor(stateFilePath = resolveStateFile()) {
430
- this.stateFile = stateFilePath;
431
- this.state = {
432
- currentPhase: "idle",
433
- taskDescription: "",
434
- bugFixed: false,
435
- testsCreated: false,
436
- testsPassed: false,
437
- testsSkipped: false,
438
- testsSkippedReason: "",
439
- documentationCreated: false,
440
- readyToCommit: false,
441
- readyCheckCompleted: false,
442
- released: false,
443
- releaseCommand: "",
444
- releaseNotes: "",
445
- commitAndPushCompleted: false,
446
- lastCommitMessage: "",
447
- lastPushBranch: "",
448
- history: [],
449
- taskType: "",
450
- };
451
- }
452
-
453
- async load() {
454
- await migrateLegacyStateFile(this.stateFile);
455
- try {
456
- const data = await fs.readFile(this.stateFile, "utf-8");
457
- const parsed = JSON.parse(data);
458
- this.state = {
459
- ...this.state,
460
- ...parsed,
461
- };
462
- // Ensure compatibility mirrors exist whenever the primary file is present
463
- await ensureCompatibilityLinks(this.stateFile);
464
- if (typeof this.state.testsCreated !== "boolean") {
465
- this.state.testsCreated = false;
466
- }
467
- if (typeof this.state.testsSkipped !== "boolean") {
468
- this.state.testsSkipped = false;
469
- }
470
- if (typeof this.state.testsSkippedReason !== "string") {
471
- this.state.testsSkippedReason = "";
472
- }
473
- if (typeof this.state.readyCheckCompleted !== "boolean") {
474
- this.state.readyCheckCompleted = false;
475
- }
476
- if (typeof this.state.released !== "boolean") {
477
- this.state.released = false;
478
- }
479
- if (typeof this.state.commitAndPushCompleted !== "boolean") {
480
- this.state.commitAndPushCompleted = false;
481
- }
482
- if (typeof this.state.releaseCommand !== "string") {
483
- this.state.releaseCommand = "";
484
- }
485
- if (typeof this.state.releaseNotes !== "string") {
486
- this.state.releaseNotes = "";
487
- }
488
- if (typeof this.state.lastCommitMessage !== "string") {
489
- this.state.lastCommitMessage = "";
490
- }
491
- if (typeof this.state.lastPushBranch !== "string") {
492
- this.state.lastPushBranch = "";
493
- }
494
- } catch (error) {
495
- if (error?.code !== "ENOENT") {
496
- throw error;
497
- }
498
- // Missing file is fine; bootstrap fresh artifacts for downstream consumers.
499
- await this.ensurePrimaryFile();
500
- }
501
- }
502
-
503
- async ensurePrimaryFile() {
504
- let created = false;
505
- try {
506
- await fs.access(this.stateFile);
507
- } catch {
508
- await fs.mkdir(path.dirname(this.stateFile), { recursive: true });
509
- await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
510
- created = true;
511
- }
512
-
513
- // Always ensure a fresh project summary so dependent tooling can read it immediately.
514
- await this.updateProjectSummary();
515
- await ensureCompatibilityLinks(this.stateFile);
516
-
517
- return created;
518
- }
519
-
520
- async save() {
521
- await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
522
- await ensureCompatibilityLinks(this.stateFile);
523
- await this.updateProjectSummary();
524
- await this.syncToDatabase();
525
- }
526
-
527
- async updateProjectSummary() {
528
- const summaryPath = path.join(path.dirname(this.stateFile), PROJECT_SUMMARY_FILENAME);
529
- const summary = this.generateProjectSummaryData();
530
- await fs.mkdir(path.dirname(summaryPath), { recursive: true });
531
- await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2));
532
- }
533
-
534
- async syncToDatabase() {
535
- try {
536
- const { insertHistoryEntry, updateSummaryForUser } = await import("./db/index.js");
537
- const userId = getCurrentUserId();
538
- for (const entry of this.state.history || []) {
539
- insertHistoryEntry(userId, entry);
540
- }
541
- updateSummaryForUser(userId);
542
- } catch {
543
- // SQLite not available; skip sync
544
- }
545
- }
546
-
547
- generateProjectSummaryData() {
548
- const history = this.state.history || [];
549
- if (!history || history.length === 0) {
550
- return {
551
- totalTasks: 0,
552
- taskTypes: {},
553
- lastActive: null,
554
- recentTasks: [],
555
- updatedAt: new Date().toISOString(),
556
- };
557
- }
558
-
559
- const taskTypes = {};
560
- const recentTasks = history.slice(-20);
561
-
562
- for (const entry of recentTasks) {
563
- const type = entry.taskType || "other";
564
- taskTypes[type] = (taskTypes[type] || 0) + 1;
565
- }
566
-
567
- const totalTasks = history.length;
568
- const lastTask = history[history.length - 1];
569
- const lastActive = lastTask ? lastTask.timestamp : null;
570
-
571
- return {
572
- totalTasks,
573
- taskTypes,
574
- lastActive,
575
- recentTasks: history.slice(-5).reverse().map((e) => ({
576
- description: e.taskDescription,
577
- type: e.taskType,
578
- timestamp: e.timestamp,
579
- })),
580
- updatedAt: new Date().toISOString(),
581
- };
582
- }
583
-
584
- reset() {
585
- this.state = {
586
- currentPhase: "idle",
587
- taskDescription: "",
588
- bugFixed: false,
589
- testsCreated: false,
590
- testsPassed: false,
591
- testsSkipped: false,
592
- testsSkippedReason: "",
593
- documentationCreated: false,
594
- readyToCommit: false,
595
- readyCheckCompleted: false,
596
- released: false,
597
- releaseCommand: "",
598
- releaseNotes: "",
599
- commitAndPushCompleted: false,
600
- lastCommitMessage: "",
601
- lastPushBranch: "",
602
- history: this.state.history,
603
- };
604
- }
605
-
606
- addToHistory(entry) {
607
- this.state.history.push({
608
- ...entry,
609
- timestamp: new Date().toISOString(),
610
- });
611
- }
612
- }
1
+ import fs from "fs/promises";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const STATE_DIR = ".state";
9
+ const STATE_FILENAME = "workflow-state.json";
10
+ const LEGACY_STATE_FILENAME = ".workflow-state.json";
11
+ const PROJECT_SUMMARY_FILENAME = "project-summary.json";
12
+ const USER_ID_FILENAME = "user-id";
13
+
14
+ let cachedUserId = null;
15
+ let cachedUserStateDir = null;
16
+
17
+ function generateUserId() {
18
+ const timestamp = Date.now().toString(16);
19
+ const randomSuffix = Math.random().toString(16).slice(2, 10);
20
+ return `user-${timestamp}-${randomSuffix}`;
21
+ }
22
+
23
+ function resolveUserId(stateDir) {
24
+ const envUserId = (process.env.DEV_WORKFLOW_USER_ID || "").trim();
25
+ if (envUserId) {
26
+ cachedUserId = envUserId;
27
+ cachedUserStateDir = stateDir;
28
+ return envUserId;
29
+ }
30
+
31
+ if (cachedUserId && cachedUserStateDir === stateDir) {
32
+ return cachedUserId;
33
+ }
34
+
35
+ const userIdFile = path.join(stateDir, USER_ID_FILENAME);
36
+
37
+ try {
38
+ if (existsSync(userIdFile)) {
39
+ const stored = readFileSync(userIdFile, "utf-8").trim();
40
+ if (stored) {
41
+ cachedUserId = stored;
42
+ cachedUserStateDir = stateDir;
43
+ return stored;
44
+ }
45
+ }
46
+ } catch {
47
+ // Ignore read errors; we'll generate a new ID below.
48
+ }
49
+
50
+ const usersDir = path.join(stateDir, "users");
51
+
52
+ try {
53
+ const entries = readdirSync(usersDir, { withFileTypes: true });
54
+ const candidateDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
55
+ if (candidateDirs.length === 1) {
56
+ const legacyId = candidateDirs[0];
57
+ try {
58
+ mkdirSync(stateDir, { recursive: true });
59
+ writeFileSync(userIdFile, legacyId, "utf-8");
60
+ } catch {
61
+ // Persisting the legacy ID is best-effort.
62
+ }
63
+ cachedUserId = legacyId;
64
+ cachedUserStateDir = stateDir;
65
+ return legacyId;
66
+ }
67
+ } catch (error) {
68
+ if (error?.code !== "ENOENT") {
69
+ // Unexpected error accessing users directory – fall through to generate new ID.
70
+ }
71
+ }
72
+
73
+ const legacyDefaultPath = path.join(usersDir, "default", STATE_FILENAME);
74
+ if (existsSync(legacyDefaultPath)) {
75
+ try {
76
+ mkdirSync(stateDir, { recursive: true });
77
+ writeFileSync(userIdFile, "default", "utf-8");
78
+ } catch {
79
+ // Best-effort persistence; continue with in-memory fallback.
80
+ }
81
+ cachedUserId = "default";
82
+ cachedUserStateDir = stateDir;
83
+ return "default";
84
+ }
85
+
86
+ const generatedId = generateUserId();
87
+ try {
88
+ mkdirSync(stateDir, { recursive: true });
89
+ writeFileSync(userIdFile, generatedId, "utf-8");
90
+ } catch {
91
+ // Ignore persistence errors; continue with in-memory ID.
92
+ }
93
+
94
+ cachedUserId = generatedId;
95
+ cachedUserStateDir = stateDir;
96
+ return generatedId;
97
+ }
98
+
99
+ function hasProjectMarkers(candidate) {
100
+ if (!candidate) {
101
+ return false;
102
+ }
103
+
104
+ const resolved = path.resolve(candidate);
105
+ return (
106
+ existsSync(path.join(resolved, "package.json")) || existsSync(path.join(resolved, ".git"))
107
+ );
108
+ }
109
+
110
+ function normalizeAndStoreCandidate(list, candidate) {
111
+ if (!candidate) {
112
+ return;
113
+ }
114
+
115
+ const resolved = path.resolve(candidate);
116
+ if (!list.includes(resolved)) {
117
+ list.push(resolved);
118
+ }
119
+ }
120
+
121
+ function addAncestorCandidates(list, start) {
122
+ if (!start) {
123
+ return;
124
+ }
125
+
126
+ let current = path.resolve(start);
127
+ while (!isRootPath(current)) {
128
+ const parent = path.dirname(current);
129
+ if (parent === current) {
130
+ break;
131
+ }
132
+ if (!isRootPath(parent)) {
133
+ normalizeAndStoreCandidate(list, parent);
134
+ }
135
+ current = parent;
136
+ }
137
+ }
138
+
139
+ function selectProjectRoot(candidates, moduleRoot) {
140
+ const cwdResolved = path.resolve(process.cwd());
141
+ const prioritized = [];
142
+
143
+ for (const candidate of candidates) {
144
+ if (!candidate) {
145
+ continue;
146
+ }
147
+
148
+ const resolved = path.resolve(candidate);
149
+ if (prioritized.some((entry) => entry.resolved === resolved)) {
150
+ continue;
151
+ }
152
+
153
+ prioritized.push({
154
+ resolved,
155
+ hasMarkers: hasProjectMarkers(resolved),
156
+ isRoot: isRootPath(resolved),
157
+ isCwdAncestor: cwdResolved === resolved || cwdResolved.startsWith(`${resolved}${path.sep}`),
158
+ });
159
+ }
160
+
161
+ const ancestorWithMarkers = prioritized.find((entry) => entry.isCwdAncestor && entry.hasMarkers);
162
+ if (ancestorWithMarkers) {
163
+ return ancestorWithMarkers.resolved;
164
+ }
165
+
166
+ const ancestorWithoutMarkers = prioritized
167
+ .filter((entry) => entry.isCwdAncestor && !entry.hasMarkers && !entry.isRoot)
168
+ .sort((a, b) => b.resolved.length - a.resolved.length);
169
+ if (ancestorWithoutMarkers.length > 0) {
170
+ return ancestorWithoutMarkers[0].resolved;
171
+ }
172
+
173
+ if (moduleRoot) {
174
+ const moduleResolved = path.resolve(moduleRoot);
175
+ const moduleEntry = prioritized.find((entry) => entry.resolved === moduleResolved && !entry.isRoot);
176
+ if (moduleEntry) {
177
+ return moduleEntry.resolved;
178
+ }
179
+ }
180
+
181
+ const withMarkers = prioritized.find((entry) => entry.hasMarkers);
182
+ if (withMarkers) {
183
+ return withMarkers.resolved;
184
+ }
185
+
186
+ const nonRoot = prioritized.find((entry) => !entry.isRoot);
187
+ if (nonRoot) {
188
+ return nonRoot.resolved;
189
+ }
190
+
191
+ return prioritized.length > 0 ? prioritized[0].resolved : null;
192
+ }
193
+
194
+ function getUserScopedDir(baseDir) {
195
+ const userId = resolveUserId(baseDir);
196
+ return path.join(baseDir, "users", userId);
197
+ }
198
+
199
+ function getCurrentUserId() {
200
+ const envUserId = (process.env.DEV_WORKFLOW_USER_ID || "").trim();
201
+ if (envUserId) {
202
+ return envUserId;
203
+ }
204
+
205
+ if (cachedUserId) {
206
+ return cachedUserId;
207
+ }
208
+
209
+ if (cachedUserStateDir) {
210
+ return resolveUserId(cachedUserStateDir);
211
+ }
212
+
213
+ return "default";
214
+ }
215
+
216
+ async function migrateLegacyStateFile(targetPath) {
217
+ const stateDir = path.dirname(targetPath);
218
+ if (path.basename(stateDir) !== STATE_DIR) {
219
+ return;
220
+ }
221
+
222
+ const projectRoot = path.dirname(stateDir);
223
+ const legacyPath = path.join(projectRoot, LEGACY_STATE_FILENAME);
224
+
225
+ if (legacyPath === targetPath) {
226
+ return;
227
+ }
228
+
229
+ try {
230
+ await fs.access(legacyPath);
231
+ } catch {
232
+ return;
233
+ }
234
+
235
+ await fs.mkdir(stateDir, { recursive: true });
236
+
237
+ try {
238
+ await fs.access(targetPath);
239
+ await fs.unlink(legacyPath);
240
+ return;
241
+ } catch {
242
+ // New state file doesn't exist yet; fall through to migrate.
243
+ }
244
+
245
+ await fs.rename(legacyPath, targetPath);
246
+ }
247
+
248
+ function isRootPath(candidate) {
249
+ if (!candidate) {
250
+ return true;
251
+ }
252
+
253
+ const resolved = path.resolve(candidate);
254
+ return path.parse(resolved).root === resolved;
255
+ }
256
+
257
+ function findProjectRoot(startDir) {
258
+ if (!startDir) {
259
+ return null;
260
+ }
261
+
262
+ let current = path.resolve(startDir);
263
+ const { root } = path.parse(current);
264
+
265
+ while (true) {
266
+ const hasPackageJson = existsSync(path.join(current, "package.json"));
267
+ const hasGitDir = existsSync(path.join(current, ".git"));
268
+ if (hasPackageJson || hasGitDir) {
269
+ return current;
270
+ }
271
+
272
+ if (current === root) {
273
+ return null;
274
+ }
275
+
276
+ const parent = path.dirname(current);
277
+ if (parent === current) {
278
+ return null;
279
+ }
280
+
281
+ current = parent;
282
+ }
283
+ }
284
+
285
+ function getCompatibilityPaths(primaryPath) {
286
+ const resolvedPrimary = path.resolve(primaryPath);
287
+ const candidates = new Set();
288
+ const moduleDir = __dirname;
289
+
290
+ const directCandidate = path.resolve(moduleDir, STATE_DIR, STATE_FILENAME);
291
+ if (directCandidate !== resolvedPrimary) {
292
+ candidates.add(directCandidate);
293
+ }
294
+
295
+ const possibleDistDirs = [
296
+ path.join(moduleDir, "dist"),
297
+ path.join(path.dirname(moduleDir), "dist"),
298
+ ];
299
+
300
+ for (const distDir of possibleDistDirs) {
301
+ const candidate = path.resolve(distDir, STATE_DIR, STATE_FILENAME);
302
+ if (candidate !== resolvedPrimary) {
303
+ candidates.add(candidate);
304
+ }
305
+ }
306
+
307
+ return Array.from(candidates);
308
+ }
309
+
310
+ async function pathExists(filePath) {
311
+ try {
312
+ await fs.access(filePath);
313
+ return true;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
318
+
319
+ async function ensureCompatibilityLinks(primaryPath) {
320
+ const resolvedPrimary = path.resolve(primaryPath);
321
+
322
+ if (!(await pathExists(resolvedPrimary))) {
323
+ return;
324
+ }
325
+
326
+ const compatibilityPaths = getCompatibilityPaths(resolvedPrimary);
327
+
328
+ for (const compatPath of compatibilityPaths) {
329
+ try {
330
+ await fs.mkdir(path.dirname(compatPath), { recursive: true });
331
+ } catch {
332
+ // Ignore directory creation errors; we'll surface them on actual writes.
333
+ }
334
+
335
+ try {
336
+ const compatStat = await fs.lstat(compatPath);
337
+ if (compatStat.isSymbolicLink()) {
338
+ const target = await fs.readlink(compatPath);
339
+ if (path.resolve(path.dirname(compatPath), target) === resolvedPrimary) {
340
+ continue;
341
+ }
342
+ await fs.unlink(compatPath);
343
+ } else if (compatStat.isFile()) {
344
+ const [primaryStat, existingStat] = await Promise.all([
345
+ fs.stat(resolvedPrimary),
346
+ fs.stat(compatPath),
347
+ ]);
348
+ if (primaryStat.dev === existingStat.dev && primaryStat.ino === existingStat.ino) {
349
+ continue;
350
+ }
351
+ await fs.unlink(compatPath);
352
+ } else {
353
+ await fs.unlink(compatPath);
354
+ }
355
+ } catch (error) {
356
+ if (error.code !== "ENOENT") {
357
+ // Unexpected error accessing legacy path; skip mirroring to avoid breaking primary file.
358
+ continue;
359
+ }
360
+ }
361
+
362
+ try {
363
+ await fs.link(resolvedPrimary, compatPath);
364
+ continue;
365
+ } catch (error) {
366
+ if (!["EEXIST", "EXDEV", "EPERM"].includes(error.code)) {
367
+ continue;
368
+ }
369
+ if (error.code === "EEXIST") {
370
+ continue;
371
+ }
372
+ }
373
+
374
+ try {
375
+ await fs.symlink(resolvedPrimary, compatPath);
376
+ continue;
377
+ } catch (error) {
378
+ if (error.code === "EEXIST") {
379
+ continue;
380
+ }
381
+ if (!["EPERM", "ENOTSUP", "EEXIST"].includes(error.code)) {
382
+ // If symlink fails for another reason, skip to avoid interfering with primary file.
383
+ continue;
384
+ }
385
+ }
386
+
387
+ try {
388
+ await fs.copyFile(resolvedPrimary, compatPath);
389
+ } catch {
390
+ // Final fallback failed; ignore to avoid disrupting primary save.
391
+ }
392
+ }
393
+ }
394
+
395
+ export function resolveStateFile() {
396
+ const customPath = process.env.DEV_WORKFLOW_STATE_FILE;
397
+ if (customPath) {
398
+ return path.resolve(customPath);
399
+ }
400
+
401
+ const moduleRoot = path.resolve(__dirname, "..");
402
+ const cwdResolved = path.resolve(process.cwd());
403
+ const initCwd = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : null;
404
+
405
+ // 1. Prefer the project rooted at the current working directory
406
+ let projectRoot = findProjectRoot(cwdResolved);
407
+
408
+ // 2. Fall back to the project rooted at INIT_CWD if different
409
+ if (!projectRoot && initCwd) {
410
+ projectRoot = findProjectRoot(initCwd);
411
+ }
412
+
413
+ // 3. Fall back to the project containing this module
414
+ if (!projectRoot) {
415
+ projectRoot = findProjectRoot(moduleRoot);
416
+ }
417
+
418
+ // 4. Final fallback: use a non-root working directory or the module root
419
+ if (!projectRoot) {
420
+ projectRoot = !isRootPath(cwdResolved) ? cwdResolved : moduleRoot;
421
+ }
422
+
423
+ const stateDir = path.join(projectRoot, STATE_DIR);
424
+ const userDir = getUserScopedDir(stateDir);
425
+ return path.join(userDir, STATE_FILENAME);
426
+ }
427
+
428
+ export class WorkflowState {
429
+ constructor(stateFilePath = resolveStateFile()) {
430
+ this.stateFile = stateFilePath;
431
+ this.state = {
432
+ currentPhase: "idle",
433
+ taskDescription: "",
434
+ bugFixed: false,
435
+ testsCreated: false,
436
+ testsPassed: false,
437
+ testsSkipped: false,
438
+ testsSkippedReason: "",
439
+ documentationCreated: false,
440
+ readyToCommit: false,
441
+ readyCheckCompleted: false,
442
+ released: false,
443
+ releaseCommand: "",
444
+ releaseNotes: "",
445
+ commitAndPushCompleted: false,
446
+ lastCommitMessage: "",
447
+ lastPushBranch: "",
448
+ history: [],
449
+ taskType: "",
450
+ };
451
+ }
452
+
453
+ async load() {
454
+ await migrateLegacyStateFile(this.stateFile);
455
+ try {
456
+ const data = await fs.readFile(this.stateFile, "utf-8");
457
+ const parsed = JSON.parse(data);
458
+ this.state = {
459
+ ...this.state,
460
+ ...parsed,
461
+ };
462
+ // Ensure compatibility mirrors exist whenever the primary file is present
463
+ await ensureCompatibilityLinks(this.stateFile);
464
+ if (typeof this.state.testsCreated !== "boolean") {
465
+ this.state.testsCreated = false;
466
+ }
467
+ if (typeof this.state.testsSkipped !== "boolean") {
468
+ this.state.testsSkipped = false;
469
+ }
470
+ if (typeof this.state.testsSkippedReason !== "string") {
471
+ this.state.testsSkippedReason = "";
472
+ }
473
+ if (typeof this.state.readyCheckCompleted !== "boolean") {
474
+ this.state.readyCheckCompleted = false;
475
+ }
476
+ if (typeof this.state.released !== "boolean") {
477
+ this.state.released = false;
478
+ }
479
+ if (typeof this.state.commitAndPushCompleted !== "boolean") {
480
+ this.state.commitAndPushCompleted = false;
481
+ }
482
+ if (typeof this.state.releaseCommand !== "string") {
483
+ this.state.releaseCommand = "";
484
+ }
485
+ if (typeof this.state.releaseNotes !== "string") {
486
+ this.state.releaseNotes = "";
487
+ }
488
+ if (typeof this.state.lastCommitMessage !== "string") {
489
+ this.state.lastCommitMessage = "";
490
+ }
491
+ if (typeof this.state.lastPushBranch !== "string") {
492
+ this.state.lastPushBranch = "";
493
+ }
494
+ } catch (error) {
495
+ if (error?.code !== "ENOENT") {
496
+ throw error;
497
+ }
498
+ // Missing file is fine; bootstrap fresh artifacts for downstream consumers.
499
+ await this.ensurePrimaryFile();
500
+ }
501
+ }
502
+
503
+ async ensurePrimaryFile() {
504
+ let created = false;
505
+ try {
506
+ await fs.access(this.stateFile);
507
+ } catch {
508
+ await fs.mkdir(path.dirname(this.stateFile), { recursive: true });
509
+ await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
510
+ created = true;
511
+ }
512
+
513
+ // Always ensure a fresh project summary so dependent tooling can read it immediately.
514
+ await this.updateProjectSummary();
515
+ await ensureCompatibilityLinks(this.stateFile);
516
+
517
+ return created;
518
+ }
519
+
520
+ async save() {
521
+ await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
522
+ await ensureCompatibilityLinks(this.stateFile);
523
+ await this.updateProjectSummary();
524
+ await this.syncToDatabase();
525
+ }
526
+
527
+ async updateProjectSummary() {
528
+ const summaryPath = path.join(path.dirname(this.stateFile), PROJECT_SUMMARY_FILENAME);
529
+ const summary = this.generateProjectSummaryData();
530
+ await fs.mkdir(path.dirname(summaryPath), { recursive: true });
531
+ await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2));
532
+ }
533
+
534
+ async syncToDatabase() {
535
+ try {
536
+ const { insertHistoryEntry, updateSummaryForUser } = await import("./db/index.js");
537
+ const userId = getCurrentUserId();
538
+ for (const entry of this.state.history || []) {
539
+ insertHistoryEntry(userId, entry);
540
+ }
541
+ updateSummaryForUser(userId);
542
+ } catch {
543
+ // SQLite not available; skip sync
544
+ }
545
+ }
546
+
547
+ generateProjectSummaryData() {
548
+ const history = this.state.history || [];
549
+ if (!history || history.length === 0) {
550
+ return {
551
+ totalTasks: 0,
552
+ taskTypes: {},
553
+ lastActive: null,
554
+ recentTasks: [],
555
+ updatedAt: new Date().toISOString(),
556
+ };
557
+ }
558
+
559
+ const taskTypes = {};
560
+ const recentTasks = history.slice(-20);
561
+
562
+ for (const entry of recentTasks) {
563
+ const type = entry.taskType || "other";
564
+ taskTypes[type] = (taskTypes[type] || 0) + 1;
565
+ }
566
+
567
+ const totalTasks = history.length;
568
+ const lastTask = history[history.length - 1];
569
+ const lastActive = lastTask ? lastTask.timestamp : null;
570
+
571
+ return {
572
+ totalTasks,
573
+ taskTypes,
574
+ lastActive,
575
+ recentTasks: history.slice(-5).reverse().map((e) => ({
576
+ description: e.taskDescription,
577
+ type: e.taskType,
578
+ timestamp: e.timestamp,
579
+ })),
580
+ updatedAt: new Date().toISOString(),
581
+ };
582
+ }
583
+
584
+ reset() {
585
+ this.state = {
586
+ currentPhase: "idle",
587
+ taskDescription: "",
588
+ bugFixed: false,
589
+ testsCreated: false,
590
+ testsPassed: false,
591
+ testsSkipped: false,
592
+ testsSkippedReason: "",
593
+ documentationCreated: false,
594
+ readyToCommit: false,
595
+ readyCheckCompleted: false,
596
+ released: false,
597
+ releaseCommand: "",
598
+ releaseNotes: "",
599
+ commitAndPushCompleted: false,
600
+ lastCommitMessage: "",
601
+ lastPushBranch: "",
602
+ history: this.state.history,
603
+ };
604
+ }
605
+
606
+ addToHistory(entry) {
607
+ this.state.history.push({
608
+ ...entry,
609
+ timestamp: new Date().toISOString(),
610
+ });
611
+ }
612
+ }