devworkspace 0.1.0 → 0.2.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +16 -2
  3. package/dist/agents/claude/adapter.d.ts +5 -0
  4. package/dist/agents/claude/adapter.js +10 -0
  5. package/dist/agents/claude/adapter.js.map +1 -1
  6. package/dist/agents/claude/content.js +3 -3
  7. package/dist/agents/claude/content.js.map +1 -1
  8. package/dist/cli/args.d.ts +22 -0
  9. package/dist/cli/args.js +7 -0
  10. package/dist/cli/args.js.map +1 -1
  11. package/dist/cli/help.js +54 -7
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli/render.d.ts +9 -0
  14. package/dist/cli/render.js +105 -13
  15. package/dist/cli/render.js.map +1 -1
  16. package/dist/cli/run.d.ts +6 -0
  17. package/dist/cli/run.js +75 -3
  18. package/dist/cli/run.js.map +1 -1
  19. package/dist/core/workspace.js +2 -1
  20. package/dist/core/workspace.js.map +1 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/integrations/adapters/agent-skills.d.ts +8 -0
  25. package/dist/integrations/adapters/agent-skills.js +25 -0
  26. package/dist/integrations/adapters/agent-skills.js.map +1 -0
  27. package/dist/integrations/adapters/claudePlugin.d.ts +41 -0
  28. package/dist/integrations/adapters/claudePlugin.js +129 -0
  29. package/dist/integrations/adapters/claudePlugin.js.map +1 -0
  30. package/dist/integrations/adapters/graphify.d.ts +2 -0
  31. package/dist/integrations/adapters/graphify.js +239 -0
  32. package/dist/integrations/adapters/graphify.js.map +1 -0
  33. package/dist/integrations/adapters/omniroute.d.ts +6 -0
  34. package/dist/integrations/adapters/omniroute.js +32 -0
  35. package/dist/integrations/adapters/omniroute.js.map +1 -0
  36. package/dist/integrations/adapters/pluginSkill.d.ts +28 -0
  37. package/dist/integrations/adapters/pluginSkill.js +195 -0
  38. package/dist/integrations/adapters/pluginSkill.js.map +1 -0
  39. package/dist/integrations/adapters/ponytail.d.ts +8 -0
  40. package/dist/integrations/adapters/ponytail.js +33 -0
  41. package/dist/integrations/adapters/ponytail.js.map +1 -0
  42. package/dist/integrations/exec.d.ts +26 -0
  43. package/dist/integrations/exec.js +167 -0
  44. package/dist/integrations/exec.js.map +1 -0
  45. package/dist/integrations/index.d.ts +5 -6
  46. package/dist/integrations/index.js +3 -29
  47. package/dist/integrations/index.js.map +1 -1
  48. package/dist/integrations/manager.d.ts +62 -0
  49. package/dist/integrations/manager.js +248 -0
  50. package/dist/integrations/manager.js.map +1 -0
  51. package/dist/integrations/notices.d.ts +13 -0
  52. package/dist/integrations/notices.js +68 -0
  53. package/dist/integrations/notices.js.map +1 -0
  54. package/dist/integrations/protect.d.ts +20 -0
  55. package/dist/integrations/protect.js +131 -0
  56. package/dist/integrations/protect.js.map +1 -0
  57. package/dist/integrations/state.d.ts +10 -0
  58. package/dist/integrations/state.js +81 -0
  59. package/dist/integrations/state.js.map +1 -0
  60. package/dist/integrations/types.d.ts +133 -16
  61. package/dist/integrations/types.js +13 -1
  62. package/dist/integrations/types.js.map +1 -1
  63. package/dist/prompts/onboarding.d.ts +3 -3
  64. package/dist/prompts/onboarding.js +5 -4
  65. package/dist/prompts/onboarding.js.map +1 -1
  66. package/dist/services/analysis.d.ts +2 -2
  67. package/dist/services/analysis.js +4 -5
  68. package/dist/services/analysis.js.map +1 -1
  69. package/dist/services/common.d.ts +8 -5
  70. package/dist/services/common.js +2 -5
  71. package/dist/services/common.js.map +1 -1
  72. package/dist/services/doctor.d.ts +10 -0
  73. package/dist/services/doctor.js +9 -15
  74. package/dist/services/doctor.js.map +1 -1
  75. package/dist/services/init.d.ts +9 -2
  76. package/dist/services/init.js +10 -7
  77. package/dist/services/init.js.map +1 -1
  78. package/dist/services/integrations.d.ts +26 -0
  79. package/dist/services/integrations.js +35 -0
  80. package/dist/services/integrations.js.map +1 -0
  81. package/dist/services/reset.d.ts +23 -0
  82. package/dist/services/reset.js +96 -0
  83. package/dist/services/reset.js.map +1 -0
  84. package/dist/services/status.d.ts +3 -2
  85. package/dist/services/status.js +3 -3
  86. package/dist/services/status.js.map +1 -1
  87. package/dist/utils/lock.d.ts +2 -0
  88. package/dist/utils/lock.js +2 -2
  89. package/dist/utils/lock.js.map +1 -1
  90. package/dist/utils/proc.d.ts +8 -1
  91. package/dist/utils/proc.js +31 -8
  92. package/dist/utils/proc.js.map +1 -1
  93. package/docs/architecture.md +1 -1
  94. package/docs/claude.md +4 -0
  95. package/docs/getting-started.md +13 -2
  96. package/docs/integrations.md +98 -33
  97. package/docs/security.md +5 -1
  98. package/docs/testing.md +2 -0
  99. package/docs/troubleshooting.md +9 -1
  100. package/package.json +1 -1
  101. package/dist/integrations/agent-skills.d.ts +0 -10
  102. package/dist/integrations/agent-skills.js +0 -45
  103. package/dist/integrations/agent-skills.js.map +0 -1
  104. package/dist/integrations/graphify.d.ts +0 -7
  105. package/dist/integrations/graphify.js +0 -54
  106. package/dist/integrations/graphify.js.map +0 -1
  107. package/dist/integrations/omniroute.d.ts +0 -7
  108. package/dist/integrations/omniroute.js +0 -24
  109. package/dist/integrations/omniroute.js.map +0 -1
  110. package/dist/integrations/ponytail.d.ts +0 -10
  111. package/dist/integrations/ponytail.js +0 -58
  112. package/dist/integrations/ponytail.js.map +0 -1
  113. package/dist/integrations/probe.d.ts +0 -6
  114. package/dist/integrations/probe.js +0 -41
  115. package/dist/integrations/probe.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { removeManagedSection } from '../agents/claude/adapter.js';
4
+ import { SKILL_MARKER, SKILL_PATH } from '../agents/claude/content.js';
5
+ import { DevWorkspaceError } from '../errors/index.js';
6
+ import { findWorkspaceRoot, WORKSPACE_DIR, wsPaths } from '../core/workspace.js';
7
+ import { atomicWriteFile, pathExists } from '../utils/fs.js';
8
+ import { isInside } from '../utils/paths.js';
9
+ import { withLock } from '../utils/lock.js';
10
+ async function countFiles(dir) {
11
+ let n = 0;
12
+ for (const e of await fs.readdir(dir, { withFileTypes: true })) {
13
+ if (e.isDirectory())
14
+ n += await countFiles(path.join(dir, e.name));
15
+ else
16
+ n++;
17
+ }
18
+ return n;
19
+ }
20
+ /**
21
+ * Remove .devworkspace/ so the user can start again with `init`. Only DevWorkspace's own folder is ever deleted;
22
+ * application files are never touched, and with includeAgentFiles only the managed block / managed skill are removed.
23
+ */
24
+ export async function resetWorkspace(o) {
25
+ let root = await findWorkspaceRoot(o.cwd);
26
+ if (!root && (await pathExists(path.join(o.cwd, WORKSPACE_DIR))))
27
+ root = path.resolve(o.cwd); // half-initialised workspace
28
+ if (!root) {
29
+ throw new DevWorkspaceError('NOT_FOUND', 'No .devworkspace folder found in this directory or any parent.', { hint: 'Nothing to reset.' });
30
+ }
31
+ const ws = wsPaths(root);
32
+ const st = await fs.lstat(ws.dir);
33
+ if (st.isSymbolicLink() || !st.isDirectory()) {
34
+ throw new DevWorkspaceError('UNSAFE_PATH', '.devworkspace is a symlink or not a directory; refusing to delete it.', {
35
+ hint: 'Remove it by hand if you are sure.',
36
+ });
37
+ }
38
+ const real = await fs.realpath(ws.dir);
39
+ if (!isInside(await fs.realpath(root), real) || path.basename(real) !== WORKSPACE_DIR) {
40
+ throw new DevWorkspaceError('UNSAFE_PATH', 'Resolved workspace path is outside the project; refusing to delete it.');
41
+ }
42
+ const fileCount = await countFiles(ws.dir);
43
+ const actions = [
44
+ { path: `${WORKSPACE_DIR}/`, action: 'delete-folder', note: `${fileCount} file(s): knowledge, decisions, tasks, sessions, analysis and local state` },
45
+ ];
46
+ const pending = [];
47
+ if (o.includeAgentFiles) {
48
+ for (const rel of ['CLAUDE.md', '.claude/CLAUDE.md']) {
49
+ const full = path.join(root, ...rel.split('/'));
50
+ const text = await fs.readFile(full, 'utf8').catch(() => null);
51
+ if (text === null)
52
+ continue;
53
+ const removed = removeManagedSection(text);
54
+ if (!removed) {
55
+ if (text.includes('<!-- devworkspace:start') || text.includes('<!-- devworkspace:end')) {
56
+ actions.push({ path: rel, action: 'skip', note: 'managed markers are unbalanced; edit by hand' });
57
+ }
58
+ continue;
59
+ }
60
+ if (removed.empty) {
61
+ actions.push({ path: rel, action: 'delete-file', note: 'contained only the DevWorkspace block' });
62
+ pending.push(() => fs.rm(full, { force: true }));
63
+ }
64
+ else {
65
+ actions.push({ path: rel, action: 'remove-managed-section', note: 'your own content is kept' });
66
+ pending.push(() => atomicWriteFile(full, removed.content));
67
+ }
68
+ }
69
+ const skillFile = path.join(root, ...SKILL_PATH.split('/'));
70
+ const skill = await fs.readFile(skillFile, 'utf8').catch(() => null);
71
+ if (skill !== null) {
72
+ if (skill.includes(SKILL_MARKER)) {
73
+ const dir = path.dirname(skillFile);
74
+ const others = (await fs.readdir(dir)).filter((n) => n !== 'SKILL.md');
75
+ actions.push({ path: SKILL_PATH, action: 'delete-file', note: 'DevWorkspace-managed skill' });
76
+ pending.push(async () => {
77
+ await fs.rm(skillFile, { force: true });
78
+ if (others.length === 0)
79
+ await fs.rmdir(dir).catch(() => undefined);
80
+ });
81
+ }
82
+ else {
83
+ actions.push({ path: SKILL_PATH, action: 'skip', note: 'not managed by DevWorkspace (no marker); left alone' });
84
+ }
85
+ }
86
+ }
87
+ if (!o.confirm || o.dryRun)
88
+ return { root, executed: false, actions, fileCount };
89
+ await withLock(ws.dir, async () => {
90
+ for (const w of pending)
91
+ await w();
92
+ });
93
+ await fs.rm(ws.dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
94
+ return { root, executed: true, actions, fileCount };
95
+ }
96
+ //# sourceMappingURL=reset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset.js","sourceRoot":"","sources":["../../src/services/reset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAuB5C,KAAK,UAAU,UAAU,CAAC,GAAW;IACnC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC,CAAC,WAAW,EAAE;YAAE,CAAC,IAAI,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;;YAC9D,CAAC,EAAE,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,CAAe;IAClD,IAAI,IAAI,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,6BAA6B;IAC3H,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,iBAAiB,CAAC,WAAW,EAAE,gEAAgE,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAC5I,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,iBAAiB,CAAC,aAAa,EAAE,uEAAuE,EAAE;YAClH,IAAI,EAAE,oCAAoC;SAC3C,CAAC,CAAC;IACL,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,aAAa,EAAE,CAAC;QACtF,MAAM,IAAI,iBAAiB,CAAC,aAAa,EAAE,wEAAwE,CAAC,CAAC;IACvH,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAkB;QAC7B,EAAE,IAAI,EAAE,GAAG,aAAa,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,GAAG,SAAS,2EAA2E,EAAE;KACtJ,CAAC;IACF,MAAM,OAAO,GAA4B,EAAE,CAAC;IAE5C,IAAI,CAAC,CAAC,iBAAiB,EAAE,CAAC;QACxB,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YAChD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC/D,IAAI,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC5B,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,IAAI,IAAI,CAAC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC;oBACvF,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,8CAA8C,EAAE,CAAC,CAAC;gBACpG,CAAC;gBACD,SAAS;YACX,CAAC;YACD,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;gBAClB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,uCAAuC,EAAE,CAAC,CAAC;gBAClG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,wBAAwB,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAC,CAAC;gBAChG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACrE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;gBACjC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACpC,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC;gBACvE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,4BAA4B,EAAE,CAAC,CAAC;gBAC9F,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;oBACtB,MAAM,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;oBACxC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;wBAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBACtE,CAAC,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,qDAAqD,EAAE,CAAC,CAAC;YAClH,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAEjF,MAAM,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE;QAChC,KAAK,MAAM,CAAC,IAAI,OAAO;YAAE,MAAM,CAAC,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IACH,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;IACtF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { removeManagedSection } from '../agents/claude/adapter.js';\nimport { SKILL_MARKER, SKILL_PATH } from '../agents/claude/content.js';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { findWorkspaceRoot, WORKSPACE_DIR, wsPaths } from '../core/workspace.js';\nimport { atomicWriteFile, pathExists } from '../utils/fs.js';\nimport { isInside } from '../utils/paths.js';\nimport { withLock } from '../utils/lock.js';\nimport type { ServiceOptions } from './common.js';\n\nexport interface ResetOptions extends ServiceOptions {\n /** Actually delete. Without it, reset only reports what it would remove. */\n confirm?: boolean;\n /** Also remove the DevWorkspace block from CLAUDE.md and the managed devworkspace skill. */\n includeAgentFiles?: boolean;\n}\n\nexport interface ResetAction {\n path: string;\n action: 'delete-folder' | 'remove-managed-section' | 'delete-file' | 'skip';\n note: string;\n}\n\nexport interface ResetResult {\n root: string;\n executed: boolean;\n actions: ResetAction[];\n fileCount: number;\n}\n\nasync function countFiles(dir: string): Promise<number> {\n let n = 0;\n for (const e of await fs.readdir(dir, { withFileTypes: true })) {\n if (e.isDirectory()) n += await countFiles(path.join(dir, e.name));\n else n++;\n }\n return n;\n}\n\n/**\n * Remove .devworkspace/ so the user can start again with `init`. Only DevWorkspace's own folder is ever deleted;\n * application files are never touched, and with includeAgentFiles only the managed block / managed skill are removed.\n */\nexport async function resetWorkspace(o: ResetOptions): Promise<ResetResult> {\n let root = await findWorkspaceRoot(o.cwd);\n if (!root && (await pathExists(path.join(o.cwd, WORKSPACE_DIR)))) root = path.resolve(o.cwd); // half-initialised workspace\n if (!root) {\n throw new DevWorkspaceError('NOT_FOUND', 'No .devworkspace folder found in this directory or any parent.', { hint: 'Nothing to reset.' });\n }\n const ws = wsPaths(root);\n const st = await fs.lstat(ws.dir);\n if (st.isSymbolicLink() || !st.isDirectory()) {\n throw new DevWorkspaceError('UNSAFE_PATH', '.devworkspace is a symlink or not a directory; refusing to delete it.', {\n hint: 'Remove it by hand if you are sure.',\n });\n }\n const real = await fs.realpath(ws.dir);\n if (!isInside(await fs.realpath(root), real) || path.basename(real) !== WORKSPACE_DIR) {\n throw new DevWorkspaceError('UNSAFE_PATH', 'Resolved workspace path is outside the project; refusing to delete it.');\n }\n\n const fileCount = await countFiles(ws.dir);\n const actions: ResetAction[] = [\n { path: `${WORKSPACE_DIR}/`, action: 'delete-folder', note: `${fileCount} file(s): knowledge, decisions, tasks, sessions, analysis and local state` },\n ];\n const pending: (() => Promise<void>)[] = [];\n\n if (o.includeAgentFiles) {\n for (const rel of ['CLAUDE.md', '.claude/CLAUDE.md']) {\n const full = path.join(root, ...rel.split('/'));\n const text = await fs.readFile(full, 'utf8').catch(() => null);\n if (text === null) continue;\n const removed = removeManagedSection(text);\n if (!removed) {\n if (text.includes('<!-- devworkspace:start') || text.includes('<!-- devworkspace:end')) {\n actions.push({ path: rel, action: 'skip', note: 'managed markers are unbalanced; edit by hand' });\n }\n continue;\n }\n if (removed.empty) {\n actions.push({ path: rel, action: 'delete-file', note: 'contained only the DevWorkspace block' });\n pending.push(() => fs.rm(full, { force: true }));\n } else {\n actions.push({ path: rel, action: 'remove-managed-section', note: 'your own content is kept' });\n pending.push(() => atomicWriteFile(full, removed.content));\n }\n }\n const skillFile = path.join(root, ...SKILL_PATH.split('/'));\n const skill = await fs.readFile(skillFile, 'utf8').catch(() => null);\n if (skill !== null) {\n if (skill.includes(SKILL_MARKER)) {\n const dir = path.dirname(skillFile);\n const others = (await fs.readdir(dir)).filter((n) => n !== 'SKILL.md');\n actions.push({ path: SKILL_PATH, action: 'delete-file', note: 'DevWorkspace-managed skill' });\n pending.push(async () => {\n await fs.rm(skillFile, { force: true });\n if (others.length === 0) await fs.rmdir(dir).catch(() => undefined);\n });\n } else {\n actions.push({ path: SKILL_PATH, action: 'skip', note: 'not managed by DevWorkspace (no marker); left alone' });\n }\n }\n }\n\n if (!o.confirm || o.dryRun) return { root, executed: false, actions, fileCount };\n\n await withLock(ws.dir, async () => {\n for (const w of pending) await w();\n });\n await fs.rm(ws.dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });\n return { root, executed: true, actions, fileCount };\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { type Ws } from '../core/workspace.js';
2
- import { type IntegrationStatus } from '../integrations/index.js';
2
+ import { type IntegrationReport } from '../integrations/index.js';
3
3
  import { type ServiceOptions } from './common.js';
4
4
  export interface TaskBrief {
5
5
  id: string;
@@ -36,7 +36,8 @@ export interface StatusResult {
36
36
  knowledgeWritten: boolean;
37
37
  ready: boolean;
38
38
  };
39
- integrations: IntegrationStatus[];
39
+ /** Shallow observation (files and PATH only; never VERIFIED). Run doctor or `integrations status` to verify. */
40
+ integrations: IntegrationReport[];
40
41
  activeTasks: TaskBrief[];
41
42
  verification: {
42
43
  id: string;
@@ -4,14 +4,14 @@ import { SCHEMA_VERSION, validateAnalysisState, validateCommands } from '../conf
4
4
  import { findWorkspaceRoot, loadProject, wsPaths } from '../core/workspace.js';
5
5
  import { readVersioned } from '../core/versioned.js';
6
6
  import { getGitState } from '../git/git.js';
7
- import { detectIntegrations } from '../integrations/index.js';
7
+ import { inspectAll } from '../integrations/index.js';
8
8
  import { loadFacts } from '../knowledge/facts.js';
9
9
  import { listSessions } from '../sessions/session.js';
10
10
  import { listTasks, nextRequiredStep } from '../tasks/task.js';
11
11
  import { pathExists } from '../utils/fs.js';
12
12
  import { SECTION_END } from '../agents/claude/content.js';
13
13
  import { SKILL_PATH } from '../agents/claude/content.js';
14
- import { integrationCtx } from './common.js';
14
+ import { managerEnv } from './common.js';
15
15
  const brief = (t) => ({ id: t.id, title: t.title, type: t.type, status: t.status, updatedAt: t.updatedAt, next: nextRequiredStep(t) });
16
16
  export async function instructionsInstalled(ws) {
17
17
  for (const rel of ['CLAUDE.md', '.claude/CLAUDE.md']) {
@@ -102,7 +102,7 @@ export async function getStatus(o) {
102
102
  const sessions = await listSessions(ws, 1);
103
103
  if (sessions[0])
104
104
  base.lastSession = { id: sessions[0].id, goal: sessions[0].goal, next: sessions[0].next };
105
- base.integrations = await detectIntegrations(integrationCtx(o, root));
105
+ base.integrations = await inspectAll(ws, managerEnv(o, root), { deep: false });
106
106
  base.onboarding.instructionsInstalled = await instructionsInstalled(ws);
107
107
  base.onboarding.knowledgeWritten = await knowledgeWritten(ws);
108
108
  base.onboarding.ready = base.onboarding.instructionsInstalled && base.onboarding.knowledgeWritten && base.knowledge.state !== 'Empty';
@@ -1 +1 @@
1
- {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/services/status.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { SCHEMA_VERSION, validateAnalysisState, validateCommands, type CommandEntry } from '../config/schemas.js';\nimport { findWorkspaceRoot, loadProject, wsPaths, type Ws } from '../core/workspace.js';\nimport { readVersioned } from '../core/versioned.js';\nimport { getGitState } from '../git/git.js';\nimport { detectIntegrations, type IntegrationStatus } from '../integrations/index.js';\nimport { loadFacts } from '../knowledge/facts.js';\nimport { listSessions } from '../sessions/session.js';\nimport { listTasks, nextRequiredStep, type Task } from '../tasks/task.js';\nimport { pathExists } from '../utils/fs.js';\nimport { SECTION_END } from '../agents/claude/content.js';\nimport { SKILL_PATH } from '../agents/claude/content.js';\nimport { integrationCtx, type ServiceOptions } from './common.js';\n\nexport interface TaskBrief {\n id: string;\n title: string;\n type: string;\n status: string;\n updatedAt: string;\n next: string;\n}\n\nexport interface StatusResult {\n initialized: boolean;\n root: string | null;\n project: { name: string; id: string } | null;\n workspace: 'Healthy' | 'Needs attention' | 'Not initialized' | 'Unreadable';\n workspaceProblem?: string;\n git: { available: boolean; isRepo: boolean; branch: string | null; workingTree: 'Clean' | 'Modified' | 'Unknown'; changes: number };\n knowledge: { state: 'Current' | 'Stale' | 'Empty' | 'Unknown'; facts: number; stale: number; analyzedAt: string | null };\n onboarding: { instructionsInstalled: boolean; knowledgeWritten: boolean; ready: boolean };\n integrations: IntegrationStatus[];\n activeTasks: TaskBrief[];\n verification: { id: string; available: boolean; command: string | null; required: string }[];\n lastSession: { id: string; goal: string; next: string } | null;\n corruptTasks: number;\n}\n\nconst brief = (t: Task): TaskBrief => ({ id: t.id, title: t.title, type: t.type, status: t.status, updatedAt: t.updatedAt, next: nextRequiredStep(t) });\n\nexport async function instructionsInstalled(ws: Ws): Promise<boolean> {\n for (const rel of ['CLAUDE.md', '.claude/CLAUDE.md']) {\n try {\n const text = await fs.readFile(path.join(ws.root, ...rel.split('/')), 'utf8');\n if (text.includes('<!-- devworkspace:start') && text.includes(SECTION_END) && (await pathExists(path.join(ws.root, ...SKILL_PATH.split('/'))))) return true;\n } catch {\n /* try next */\n }\n }\n return false;\n}\n\nexport async function knowledgeWritten(ws: Ws): Promise<boolean> {\n try {\n return (await fs.stat(path.join(ws.contextDir, 'architecture.md'))).size >= 200;\n } catch {\n return false;\n }\n}\n\n/** Cheap overview: reads small workspace files and Git status only. Never hashes or scans source files. */\nexport async function getStatus(o: ServiceOptions): Promise<StatusResult> {\n const root = await findWorkspaceRoot(o.cwd);\n const git = await getGitState(root ?? o.cwd);\n const gitBrief: StatusResult['git'] = {\n available: git.available,\n isRepo: git.isRepo,\n branch: git.branch,\n workingTree: git.isRepo ? (git.dirty ? 'Modified' : 'Clean') : 'Unknown',\n changes: git.entries.length,\n };\n const base: StatusResult = {\n initialized: false,\n root,\n project: null,\n workspace: 'Not initialized',\n git: gitBrief,\n knowledge: { state: 'Unknown', facts: 0, stale: 0, analyzedAt: null },\n onboarding: { instructionsInstalled: false, knowledgeWritten: false, ready: false },\n integrations: [],\n activeTasks: [],\n verification: [],\n lastSession: null,\n corruptTasks: 0,\n };\n if (!root) return base;\n\n const ws = wsPaths(root);\n base.initialized = true;\n try {\n const project = await loadProject(ws);\n base.project = { name: project.name, id: project.projectId };\n base.workspace = 'Healthy';\n } catch (e) {\n return { ...base, workspace: 'Unreadable', workspaceProblem: (e as Error).message };\n }\n\n try {\n const facts = await loadFacts(ws);\n const stale = facts.facts.filter((f) => f.status === 'STALE').length;\n let analyzedAt: string | null = null;\n if (await pathExists(ws.analysisState)) {\n analyzedAt = (await readVersioned(ws.analysisState, 'analysis', SCHEMA_VERSION, validateAnalysisState)).data.analyzedAt;\n }\n base.knowledge = { facts: facts.facts.length, stale, analyzedAt, state: facts.facts.length === 0 ? 'Empty' : stale > 0 ? 'Stale' : 'Current' };\n } catch (e) {\n base.workspace = 'Needs attention';\n base.workspaceProblem = (e as Error).message;\n }\n\n if (await pathExists(ws.commands)) {\n try {\n const commands = (await readVersioned(ws.commands, 'commands', SCHEMA_VERSION, validateCommands)).data;\n base.verification = commands.checks.map((c: CommandEntry) => ({ id: c.id, available: c.available, command: c.command, required: c.required }));\n } catch (e) {\n base.workspace = 'Needs attention';\n base.workspaceProblem ??= (e as Error).message;\n }\n }\n\n const { tasks, corrupt } = await listTasks(ws);\n base.activeTasks = tasks.map(brief);\n base.corruptTasks = corrupt.length;\n if (corrupt.length > 0) base.workspace = 'Needs attention';\n const sessions = await listSessions(ws, 1);\n if (sessions[0]) base.lastSession = { id: sessions[0].id, goal: sessions[0].goal, next: sessions[0].next };\n\n base.integrations = await detectIntegrations(integrationCtx(o, root));\n base.onboarding.instructionsInstalled = await instructionsInstalled(ws);\n base.onboarding.knowledgeWritten = await knowledgeWritten(ws);\n base.onboarding.ready = base.onboarding.instructionsInstalled && base.onboarding.knowledgeWritten && base.knowledge.state !== 'Empty';\n return base;\n}\n"]}
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/services/status.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { SCHEMA_VERSION, validateAnalysisState, validateCommands, type CommandEntry } from '../config/schemas.js';\nimport { findWorkspaceRoot, loadProject, wsPaths, type Ws } from '../core/workspace.js';\nimport { readVersioned } from '../core/versioned.js';\nimport { getGitState } from '../git/git.js';\nimport { inspectAll, type IntegrationReport } from '../integrations/index.js';\nimport { loadFacts } from '../knowledge/facts.js';\nimport { listSessions } from '../sessions/session.js';\nimport { listTasks, nextRequiredStep, type Task } from '../tasks/task.js';\nimport { pathExists } from '../utils/fs.js';\nimport { SECTION_END } from '../agents/claude/content.js';\nimport { SKILL_PATH } from '../agents/claude/content.js';\nimport { managerEnv, type ServiceOptions } from './common.js';\n\nexport interface TaskBrief {\n id: string;\n title: string;\n type: string;\n status: string;\n updatedAt: string;\n next: string;\n}\n\nexport interface StatusResult {\n initialized: boolean;\n root: string | null;\n project: { name: string; id: string } | null;\n workspace: 'Healthy' | 'Needs attention' | 'Not initialized' | 'Unreadable';\n workspaceProblem?: string;\n git: { available: boolean; isRepo: boolean; branch: string | null; workingTree: 'Clean' | 'Modified' | 'Unknown'; changes: number };\n knowledge: { state: 'Current' | 'Stale' | 'Empty' | 'Unknown'; facts: number; stale: number; analyzedAt: string | null };\n onboarding: { instructionsInstalled: boolean; knowledgeWritten: boolean; ready: boolean };\n /** Shallow observation (files and PATH only; never VERIFIED). Run doctor or `integrations status` to verify. */\n integrations: IntegrationReport[];\n activeTasks: TaskBrief[];\n verification: { id: string; available: boolean; command: string | null; required: string }[];\n lastSession: { id: string; goal: string; next: string } | null;\n corruptTasks: number;\n}\n\nconst brief = (t: Task): TaskBrief => ({ id: t.id, title: t.title, type: t.type, status: t.status, updatedAt: t.updatedAt, next: nextRequiredStep(t) });\n\nexport async function instructionsInstalled(ws: Ws): Promise<boolean> {\n for (const rel of ['CLAUDE.md', '.claude/CLAUDE.md']) {\n try {\n const text = await fs.readFile(path.join(ws.root, ...rel.split('/')), 'utf8');\n if (text.includes('<!-- devworkspace:start') && text.includes(SECTION_END) && (await pathExists(path.join(ws.root, ...SKILL_PATH.split('/'))))) return true;\n } catch {\n /* try next */\n }\n }\n return false;\n}\n\nexport async function knowledgeWritten(ws: Ws): Promise<boolean> {\n try {\n return (await fs.stat(path.join(ws.contextDir, 'architecture.md'))).size >= 200;\n } catch {\n return false;\n }\n}\n\n/** Cheap overview: reads small workspace files and Git status only. Never hashes or scans source files. */\nexport async function getStatus(o: ServiceOptions): Promise<StatusResult> {\n const root = await findWorkspaceRoot(o.cwd);\n const git = await getGitState(root ?? o.cwd);\n const gitBrief: StatusResult['git'] = {\n available: git.available,\n isRepo: git.isRepo,\n branch: git.branch,\n workingTree: git.isRepo ? (git.dirty ? 'Modified' : 'Clean') : 'Unknown',\n changes: git.entries.length,\n };\n const base: StatusResult = {\n initialized: false,\n root,\n project: null,\n workspace: 'Not initialized',\n git: gitBrief,\n knowledge: { state: 'Unknown', facts: 0, stale: 0, analyzedAt: null },\n onboarding: { instructionsInstalled: false, knowledgeWritten: false, ready: false },\n integrations: [],\n activeTasks: [],\n verification: [],\n lastSession: null,\n corruptTasks: 0,\n };\n if (!root) return base;\n\n const ws = wsPaths(root);\n base.initialized = true;\n try {\n const project = await loadProject(ws);\n base.project = { name: project.name, id: project.projectId };\n base.workspace = 'Healthy';\n } catch (e) {\n return { ...base, workspace: 'Unreadable', workspaceProblem: (e as Error).message };\n }\n\n try {\n const facts = await loadFacts(ws);\n const stale = facts.facts.filter((f) => f.status === 'STALE').length;\n let analyzedAt: string | null = null;\n if (await pathExists(ws.analysisState)) {\n analyzedAt = (await readVersioned(ws.analysisState, 'analysis', SCHEMA_VERSION, validateAnalysisState)).data.analyzedAt;\n }\n base.knowledge = { facts: facts.facts.length, stale, analyzedAt, state: facts.facts.length === 0 ? 'Empty' : stale > 0 ? 'Stale' : 'Current' };\n } catch (e) {\n base.workspace = 'Needs attention';\n base.workspaceProblem = (e as Error).message;\n }\n\n if (await pathExists(ws.commands)) {\n try {\n const commands = (await readVersioned(ws.commands, 'commands', SCHEMA_VERSION, validateCommands)).data;\n base.verification = commands.checks.map((c: CommandEntry) => ({ id: c.id, available: c.available, command: c.command, required: c.required }));\n } catch (e) {\n base.workspace = 'Needs attention';\n base.workspaceProblem ??= (e as Error).message;\n }\n }\n\n const { tasks, corrupt } = await listTasks(ws);\n base.activeTasks = tasks.map(brief);\n base.corruptTasks = corrupt.length;\n if (corrupt.length > 0) base.workspace = 'Needs attention';\n const sessions = await listSessions(ws, 1);\n if (sessions[0]) base.lastSession = { id: sessions[0].id, goal: sessions[0].goal, next: sessions[0].next };\n\n base.integrations = await inspectAll(ws, managerEnv(o, root), { deep: false });\n base.onboarding.instructionsInstalled = await instructionsInstalled(ws);\n base.onboarding.knowledgeWritten = await knowledgeWritten(ws);\n base.onboarding.ready = base.onboarding.instructionsInstalled && base.onboarding.knowledgeWritten && base.knowledge.state !== 'Empty';\n return base;\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  export interface LockOptions {
2
2
  timeoutMs?: number;
3
3
  staleMs?: number;
4
+ /** Lock file name under .devworkspace/local/. Long-running operations use their own lock. */
5
+ name?: string;
4
6
  }
5
7
  /** Exclusive advisory lock stored under .devworkspace/local/. Stale locks (dead pid or too old) are broken. */
6
8
  export declare function withLock<T>(workspaceDir: string, fn: () => Promise<T>, opts?: LockOptions): Promise<T>;
@@ -17,7 +17,7 @@ export async function withLock(workspaceDir, fn, opts = {}) {
17
17
  const timeoutMs = opts.timeoutMs ?? 5000;
18
18
  const staleMs = opts.staleMs ?? 60_000;
19
19
  const dir = path.join(workspaceDir, 'local');
20
- const lockPath = path.join(dir, 'workspace.lock');
20
+ const lockPath = path.join(dir, opts.name ?? 'workspace.lock');
21
21
  await ensureDir(dir);
22
22
  const started = Date.now();
23
23
  for (;;) {
@@ -52,7 +52,7 @@ export async function withLock(workspaceDir, fn, opts = {}) {
52
52
  }
53
53
  if (Date.now() - started > timeoutMs) {
54
54
  throw new DevWorkspaceError('LOCKED', 'Another DevWorkspace process is modifying this workspace.', {
55
- hint: `Wait for it to finish, or delete ${path.join('.devworkspace', 'local', 'workspace.lock')} if no process is running.`,
55
+ hint: `Wait for it to finish, or delete ${path.join('.devworkspace', 'local', opts.name ?? 'workspace.lock')} if no process is running.`,
56
56
  });
57
57
  }
58
58
  await new Promise((r) => setTimeout(r, 50));
@@ -1 +1 @@
1
- {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/utils/lock.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs, unlinkSync } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { ensureDir } from './fs.js';\nimport { onCleanup } from './cleanup.js';\n\nexport interface LockOptions {\n timeoutMs?: number;\n staleMs?: number;\n}\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** Exclusive advisory lock stored under .devworkspace/local/. Stale locks (dead pid or too old) are broken. */\nexport async function withLock<T>(workspaceDir: string, fn: () => Promise<T>, opts: LockOptions = {}): Promise<T> {\n const timeoutMs = opts.timeoutMs ?? 5000;\n const staleMs = opts.staleMs ?? 60_000;\n const dir = path.join(workspaceDir, 'local');\n const lockPath = path.join(dir, 'workspace.lock');\n await ensureDir(dir);\n const started = Date.now();\n for (;;) {\n try {\n const fh = await fs.open(lockPath, 'wx');\n await fh.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));\n await fh.close();\n break;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new DevWorkspaceError('IO', 'Could not acquire the workspace lock.', { cause: e });\n }\n let stale: boolean;\n try {\n const info = JSON.parse(await fs.readFile(lockPath, 'utf8')) as { pid?: number; at?: number };\n const age = Date.now() - (info.at ?? 0);\n stale = age > staleMs || (typeof info.pid === 'number' && !pidAlive(info.pid));\n } catch {\n // Unreadable lock: another process may be mid-write, so only break it once it is old.\n try {\n stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > 2000;\n } catch {\n stale = true;\n }\n }\n if (stale) {\n await fs.rm(lockPath, { force: true });\n continue;\n }\n if (Date.now() - started > timeoutMs) {\n throw new DevWorkspaceError('LOCKED', 'Another DevWorkspace process is modifying this workspace.', {\n hint: `Wait for it to finish, or delete ${path.join('.devworkspace', 'local', 'workspace.lock')} if no process is running.`,\n });\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n }\n const dispose = onCleanup(() => {\n try {\n unlinkSync(lockPath);\n } catch {\n /* already gone */\n }\n });\n try {\n return await fn();\n } finally {\n dispose();\n await fs.rm(lockPath, { force: true }).catch(() => undefined);\n }\n}\n"]}
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/utils/lock.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs, unlinkSync } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { ensureDir } from './fs.js';\nimport { onCleanup } from './cleanup.js';\n\nexport interface LockOptions {\n timeoutMs?: number;\n staleMs?: number;\n /** Lock file name under .devworkspace/local/. Long-running operations use their own lock. */\n name?: string;\n}\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** Exclusive advisory lock stored under .devworkspace/local/. Stale locks (dead pid or too old) are broken. */\nexport async function withLock<T>(workspaceDir: string, fn: () => Promise<T>, opts: LockOptions = {}): Promise<T> {\n const timeoutMs = opts.timeoutMs ?? 5000;\n const staleMs = opts.staleMs ?? 60_000;\n const dir = path.join(workspaceDir, 'local');\n const lockPath = path.join(dir, opts.name ?? 'workspace.lock');\n await ensureDir(dir);\n const started = Date.now();\n for (;;) {\n try {\n const fh = await fs.open(lockPath, 'wx');\n await fh.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));\n await fh.close();\n break;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new DevWorkspaceError('IO', 'Could not acquire the workspace lock.', { cause: e });\n }\n let stale: boolean;\n try {\n const info = JSON.parse(await fs.readFile(lockPath, 'utf8')) as { pid?: number; at?: number };\n const age = Date.now() - (info.at ?? 0);\n stale = age > staleMs || (typeof info.pid === 'number' && !pidAlive(info.pid));\n } catch {\n // Unreadable lock: another process may be mid-write, so only break it once it is old.\n try {\n stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > 2000;\n } catch {\n stale = true;\n }\n }\n if (stale) {\n await fs.rm(lockPath, { force: true });\n continue;\n }\n if (Date.now() - started > timeoutMs) {\n throw new DevWorkspaceError('LOCKED', 'Another DevWorkspace process is modifying this workspace.', {\n hint: `Wait for it to finish, or delete ${path.join('.devworkspace', 'local', opts.name ?? 'workspace.lock')} if no process is running.`,\n });\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n }\n const dispose = onCleanup(() => {\n try {\n unlinkSync(lockPath);\n } catch {\n /* already gone */\n }\n });\n try {\n return await fn();\n } finally {\n dispose();\n await fs.rm(lockPath, { force: true }).catch(() => undefined);\n }\n}\n"]}
@@ -4,12 +4,19 @@ export interface RunResult {
4
4
  stdout: string;
5
5
  stderr: string;
6
6
  timedOut: boolean;
7
+ aborted?: boolean;
7
8
  error?: string;
8
9
  }
9
10
  export interface RunOptions {
10
11
  cwd?: string;
11
12
  timeoutMs?: number;
12
13
  maxBytes?: number;
14
+ /** Extra environment variables merged over process.env (undefined values unset the variable). */
15
+ env?: Record<string, string | undefined>;
16
+ /** Abort (kill) the process when the signal fires. */
17
+ signal?: AbortSignal;
18
+ /** Windows only: pass the argument string through unmodified (used for cmd.exe /c with pre-validated arguments). */
19
+ windowsVerbatimArguments?: boolean;
13
20
  }
14
21
  /**
15
22
  * Run an executable with an argument array. Never uses a shell, so project- or
@@ -18,5 +25,5 @@ export interface RunOptions {
18
25
  export declare function run(command: string, args: string[], opts?: RunOptions): Promise<RunResult>;
19
26
  /** Locate an executable on PATH (honouring PATHEXT on Windows) without executing anything. */
20
27
  export declare function which(name: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
21
- /** .cmd/.bat shims need a shell on Windows; we refuse to execute them. */
28
+ /** .cmd/.bat shims need a shell on Windows. They are only run through the validated path in integrations/exec.ts. */
22
29
  export declare function isShellShim(file: string): boolean;
@@ -12,39 +12,62 @@ export function run(command, args, opts = {}) {
12
12
  let stdout = '';
13
13
  let stderr = '';
14
14
  let timedOut = false;
15
+ let aborted = false;
15
16
  let settled = false;
16
17
  // eslint-disable-next-line prefer-const -- assigned after spawn; finish() may run first if spawn throws
17
18
  let timer;
19
+ const onAbort = () => {
20
+ aborted = true;
21
+ child?.kill();
22
+ };
18
23
  const finish = (r) => {
19
24
  if (settled)
20
25
  return;
21
26
  settled = true;
22
27
  if (timer)
23
28
  clearTimeout(timer);
24
- resolve(r);
29
+ opts.signal?.removeEventListener('abort', onAbort);
30
+ resolve(aborted ? { ...r, ok: false, aborted: true } : r);
25
31
  };
26
32
  let child;
33
+ if (opts.signal?.aborted) {
34
+ finish({ ok: false, code: null, stdout, stderr, timedOut: false, aborted: true, error: 'aborted' });
35
+ return;
36
+ }
27
37
  try {
28
- child = spawn(command, args, { cwd: opts.cwd, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
38
+ const env = opts.env ? { ...process.env, ...opts.env } : process.env;
39
+ for (const [k, v] of Object.entries(env))
40
+ if (v === undefined)
41
+ delete env[k];
42
+ child = spawn(command, args, {
43
+ cwd: opts.cwd,
44
+ env,
45
+ shell: false,
46
+ windowsHide: true,
47
+ windowsVerbatimArguments: opts.windowsVerbatimArguments,
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ });
29
50
  }
30
51
  catch (e) {
31
52
  finish({ ok: false, code: null, stdout, stderr, timedOut: false, error: e.message });
32
53
  return;
33
54
  }
55
+ const c = child;
34
56
  timer = setTimeout(() => {
35
57
  timedOut = true;
36
- child.kill();
58
+ c.kill();
37
59
  }, timeoutMs);
38
- child.stdout.on('data', (d) => {
60
+ opts.signal?.addEventListener('abort', onAbort, { once: true });
61
+ c.stdout?.on('data', (d) => {
39
62
  if (stdout.length < maxBytes)
40
63
  stdout += d.toString('utf8');
41
64
  });
42
- child.stderr.on('data', (d) => {
65
+ c.stderr?.on('data', (d) => {
43
66
  if (stderr.length < maxBytes)
44
67
  stderr += d.toString('utf8');
45
68
  });
46
- child.on('error', (e) => finish({ ok: false, code: null, stdout, stderr, timedOut, error: e.message }));
47
- child.on('close', (code) => finish({ ok: code === 0 && !timedOut, code, stdout, stderr, timedOut }));
69
+ c.on('error', (e) => finish({ ok: false, code: null, stdout, stderr, timedOut, error: e.message }));
70
+ c.on('close', (code) => finish({ ok: code === 0 && !timedOut && !aborted, code, stdout, stderr, timedOut }));
48
71
  });
49
72
  }
50
73
  /** Locate an executable on PATH (honouring PATHEXT on Windows) without executing anything. */
@@ -67,7 +90,7 @@ export async function which(name, env = process.env) {
67
90
  }
68
91
  return null;
69
92
  }
70
- /** .cmd/.bat shims need a shell on Windows; we refuse to execute them. */
93
+ /** .cmd/.bat shims need a shell on Windows. They are only run through the validated path in integrations/exec.ts. */
71
94
  export function isShellShim(file) {
72
95
  return /\.(cmd|bat)$/i.test(file);
73
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"proc.js","sourceRoot":"","sources":["../../src/utils/proc.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nexport interface RunResult {\n ok: boolean;\n code: number | null;\n stdout: string;\n stderr: string;\n timedOut: boolean;\n error?: string;\n}\n\nexport interface RunOptions {\n cwd?: string;\n timeoutMs?: number;\n maxBytes?: number;\n}\n\n/**\n * Run an executable with an argument array. Never uses a shell, so project- or\n * user-supplied values cannot be interpreted as shell syntax.\n */\nexport function run(command: string, args: string[], opts: RunOptions = {}): Promise<RunResult> {\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const maxBytes = opts.maxBytes ?? 4 * 1024 * 1024;\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let timedOut = false;\n let settled = false;\n // eslint-disable-next-line prefer-const -- assigned after spawn; finish() may run first if spawn throws\n let timer: NodeJS.Timeout | undefined;\n const finish = (r: RunResult): void => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(r);\n };\n let child;\n try {\n child = spawn(command, args, { cwd: opts.cwd, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });\n } catch (e) {\n finish({ ok: false, code: null, stdout, stderr, timedOut: false, error: (e as Error).message });\n return;\n }\n timer = setTimeout(() => {\n timedOut = true;\n child.kill();\n }, timeoutMs);\n child.stdout.on('data', (d: Buffer) => {\n if (stdout.length < maxBytes) stdout += d.toString('utf8');\n });\n child.stderr.on('data', (d: Buffer) => {\n if (stderr.length < maxBytes) stderr += d.toString('utf8');\n });\n child.on('error', (e) => finish({ ok: false, code: null, stdout, stderr, timedOut, error: e.message }));\n child.on('close', (code) => finish({ ok: code === 0 && !timedOut, code, stdout, stderr, timedOut }));\n });\n}\n\n/** Locate an executable on PATH (honouring PATHEXT on Windows) without executing anything. */\nexport async function which(name: string, env: NodeJS.ProcessEnv = process.env): Promise<string | null> {\n const isWin = process.platform === 'win32';\n const pathVar = env.PATH ?? env.Path ?? '';\n const exts = isWin ? (env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean) : [''];\n for (const dir of pathVar.split(path.delimiter).filter(Boolean)) {\n for (const ext of isWin ? ['', ...exts] : exts) {\n const candidate = path.join(dir, name + ext);\n try {\n const st = await fs.stat(candidate);\n if (st.isFile()) return candidate;\n } catch {\n /* keep looking */\n }\n }\n }\n return null;\n}\n\n/** .cmd/.bat shims need a shell on Windows; we refuse to execute them. */\nexport function isShellShim(file: string): boolean {\n return /\\.(cmd|bat)$/i.test(file);\n}\n"]}
1
+ {"version":3,"file":"proc.js","sourceRoot":"","sources":["../../src/utils/proc.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nexport interface RunResult {\n ok: boolean;\n code: number | null;\n stdout: string;\n stderr: string;\n timedOut: boolean;\n aborted?: boolean;\n error?: string;\n}\n\nexport interface RunOptions {\n cwd?: string;\n timeoutMs?: number;\n maxBytes?: number;\n /** Extra environment variables merged over process.env (undefined values unset the variable). */\n env?: Record<string, string | undefined>;\n /** Abort (kill) the process when the signal fires. */\n signal?: AbortSignal;\n /** Windows only: pass the argument string through unmodified (used for cmd.exe /c with pre-validated arguments). */\n windowsVerbatimArguments?: boolean;\n}\n\n/**\n * Run an executable with an argument array. Never uses a shell, so project- or\n * user-supplied values cannot be interpreted as shell syntax.\n */\nexport function run(command: string, args: string[], opts: RunOptions = {}): Promise<RunResult> {\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const maxBytes = opts.maxBytes ?? 4 * 1024 * 1024;\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let timedOut = false;\n let aborted = false;\n let settled = false;\n // eslint-disable-next-line prefer-const -- assigned after spawn; finish() may run first if spawn throws\n let timer: NodeJS.Timeout | undefined;\n const onAbort = (): void => {\n aborted = true;\n child?.kill();\n };\n const finish = (r: RunResult): void => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n opts.signal?.removeEventListener('abort', onAbort);\n resolve(aborted ? { ...r, ok: false, aborted: true } : r);\n };\n let child: ReturnType<typeof spawn> | undefined;\n if (opts.signal?.aborted) {\n finish({ ok: false, code: null, stdout, stderr, timedOut: false, aborted: true, error: 'aborted' });\n return;\n }\n try {\n const env = opts.env ? { ...process.env, ...opts.env } : process.env;\n for (const [k, v] of Object.entries(env)) if (v === undefined) delete env[k];\n child = spawn(command, args, {\n cwd: opts.cwd,\n env,\n shell: false,\n windowsHide: true,\n windowsVerbatimArguments: opts.windowsVerbatimArguments,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n } catch (e) {\n finish({ ok: false, code: null, stdout, stderr, timedOut: false, error: (e as Error).message });\n return;\n }\n const c = child;\n timer = setTimeout(() => {\n timedOut = true;\n c.kill();\n }, timeoutMs);\n opts.signal?.addEventListener('abort', onAbort, { once: true });\n c.stdout?.on('data', (d: Buffer) => {\n if (stdout.length < maxBytes) stdout += d.toString('utf8');\n });\n c.stderr?.on('data', (d: Buffer) => {\n if (stderr.length < maxBytes) stderr += d.toString('utf8');\n });\n c.on('error', (e) => finish({ ok: false, code: null, stdout, stderr, timedOut, error: e.message }));\n c.on('close', (code) => finish({ ok: code === 0 && !timedOut && !aborted, code, stdout, stderr, timedOut }));\n });\n}\n\n/** Locate an executable on PATH (honouring PATHEXT on Windows) without executing anything. */\nexport async function which(name: string, env: NodeJS.ProcessEnv = process.env): Promise<string | null> {\n const isWin = process.platform === 'win32';\n const pathVar = env.PATH ?? env.Path ?? '';\n const exts = isWin ? (env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean) : [''];\n for (const dir of pathVar.split(path.delimiter).filter(Boolean)) {\n for (const ext of isWin ? ['', ...exts] : exts) {\n const candidate = path.join(dir, name + ext);\n try {\n const st = await fs.stat(candidate);\n if (st.isFile()) return candidate;\n } catch {\n /* keep looking */\n }\n }\n }\n return null;\n}\n\n/** .cmd/.bat shims need a shell on Windows. They are only run through the validated path in integrations/exec.ts. */\nexport function isShellShim(file: string): boolean {\n return /\\.(cmd|bat)$/i.test(file);\n}\n"]}
@@ -26,7 +26,7 @@ The CLI contains no business logic. `src/index.ts` exports the services so a fut
26
26
  | `decisions/`, `sessions/` | Append-only decision records; short session summaries |
27
27
  | `security/` | Sensitive-file rules, secret detection/redaction, safe reading |
28
28
  | `git/` | Porcelain parsing and state (no mutating Git commands anywhere) |
29
- | `integrations/` | `Integration` interface + Graphify, Agent Skills, Ponytail, OmniRoute detectors |
29
+ | `integrations/` | Integration manager: one `IntegrationAdapter` per tool (observe, plan, verify), consent levels, file protection, state, runner abstraction (Windows shims, npx) |
30
30
  | `agents/` | `AgentAdapter` interface + the Claude adapter (managed `CLAUDE.md` block, skill) |
31
31
  | `prompts/` | Onboarding/refresh prompt builders and the behavioural requirement checker |
32
32
  | `portability/` | Export bundle format and hostile-input validation |
package/docs/claude.md CHANGED
@@ -41,6 +41,10 @@ DevWorkspace records what it manages in `project.json` → `managedFiles` (`owne
41
41
  - Never claim completion while required checks fail; report `STATUS: BLOCKED` plainly.
42
42
  - After meaningful work, record only durable knowledge.
43
43
 
44
+ ## Integrations
45
+
46
+ Onboarding tells Claude to run `devworkspace setup --integrations` (after asking you once) rather than installing tools by hand, so installs are previewed, consent-gated, protected and verified the same way whether you or Claude run them. See [integrations.md](integrations.md).
47
+
44
48
  ## Keeping Claude-specific code isolated
45
49
 
46
50
  Everything Claude-specific lives in `src/agents/claude/`. Core code talks to the `AgentAdapter` interface (`detectExisting`, `plan`), so a Cursor/Codex/Gemini adapter is a new directory implementing the same interface, not a change to core.
@@ -41,7 +41,7 @@ Open `.devworkspace/analysis/onboarding-prompt.md` and paste it into Claude Code
41
41
  3. run `devworkspace agent install` (adds a small managed block to `CLAUDE.md` and installs the `devworkspace` skill, preserving anything you wrote),
42
42
  4. write concise knowledge into `.devworkspace/context/`,
43
43
  5. check the testing contract and record a baseline,
44
- 6. configure/verify Graphify, Agent Skills and Ponytail,
44
+ 6. run the integration manager (`devworkspace setup --integrations`) after asking you once; it installs and verifies Graphify, Agent Skills and Ponytail and reports precise statuses,
45
45
  7. run `devworkspace doctor` and report unknowns and warnings.
46
46
 
47
47
  It is told not to modify application source during onboarding.
@@ -52,12 +52,23 @@ It is told not to modify application source during onboarding.
52
52
  npx devworkspace doctor
53
53
  ```
54
54
 
55
- `READY` means the workspace is healthy, instructions are installed and architecture knowledge exists. Warnings (for example *Graphify: not detected*) tell you exactly what to do next and do not fail the command.
55
+ `Core workspace: READY` means the workspace is healthy, instructions are installed and architecture knowledge exists. Integrations are listed separately with their observed status; anything not `VERIFIED` comes with the exact next step and does not fail the command. Run `npx devworkspace setup --integrations --dry-run` to see what setup would do, and `--yes` to do it.
56
56
 
57
57
  ## 5. Work normally
58
58
 
59
59
  Ask Claude for what you want. See [routing](routing.md) for how requests are handled and [claude.md](claude.md) for what was installed. After meaningful work Claude records durable knowledge; you can inspect it any time with `devworkspace context`, and run `devworkspace refresh` after large changes (or after pulling) to catch stale knowledge.
60
60
 
61
+ ## Start over
62
+
63
+ ```bash
64
+ npx devworkspace reset # shows what would be deleted; deletes nothing
65
+ npx devworkspace reset --yes # deletes .devworkspace/ (knowledge, tasks, decisions, local state)
66
+ npx devworkspace reset --yes --include-agent-files # also removes the DevWorkspace block from CLAUDE.md and the managed skill
67
+ npx devworkspace init # begin again with a new project id
68
+ ```
69
+
70
+ Only DevWorkspace's own files are removed: your source code is never touched, text you wrote in `CLAUDE.md` is kept, and a skill you wrote yourself is left alone. Committed knowledge can still be recovered from Git history. Run `devworkspace export` first if you want a copy.
71
+
61
72
  ## Install options
62
73
 
63
74
  ```bash