@mrpatronz/nexusflow 0.2.19 → 1.3.1

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 (294) hide show
  1. package/README.md +37 -7
  2. package/dist/analyzers/index.d.ts +23 -0
  3. package/dist/analyzers/index.d.ts.map +1 -1
  4. package/dist/analyzers/index.js +62 -0
  5. package/dist/analyzers/index.js.map +1 -1
  6. package/dist/commands/add-repo.d.ts +9 -0
  7. package/dist/commands/add-repo.d.ts.map +1 -1
  8. package/dist/commands/add-repo.js +12 -1
  9. package/dist/commands/add-repo.js.map +1 -1
  10. package/dist/commands/add-repo.test.d.ts +2 -0
  11. package/dist/commands/add-repo.test.d.ts.map +1 -0
  12. package/dist/commands/add-repo.test.js +30 -0
  13. package/dist/commands/add-repo.test.js.map +1 -0
  14. package/dist/commands/commands.test.js +26 -6
  15. package/dist/commands/commands.test.js.map +1 -1
  16. package/dist/commands/commit.d.ts +7 -1
  17. package/dist/commands/commit.d.ts.map +1 -1
  18. package/dist/commands/commit.js +31 -8
  19. package/dist/commands/commit.js.map +1 -1
  20. package/dist/commands/commit.test.d.ts +2 -0
  21. package/dist/commands/commit.test.d.ts.map +1 -0
  22. package/dist/commands/commit.test.js +54 -0
  23. package/dist/commands/commit.test.js.map +1 -0
  24. package/dist/commands/diff.d.ts +9 -2
  25. package/dist/commands/diff.d.ts.map +1 -1
  26. package/dist/commands/diff.js +36 -12
  27. package/dist/commands/diff.js.map +1 -1
  28. package/dist/commands/doctor.d.ts.map +1 -1
  29. package/dist/commands/doctor.js +16 -20
  30. package/dist/commands/doctor.js.map +1 -1
  31. package/dist/commands/handoff.d.ts.map +1 -1
  32. package/dist/commands/handoff.js +11 -15
  33. package/dist/commands/handoff.js.map +1 -1
  34. package/dist/commands/refresh.d.ts +4 -2
  35. package/dist/commands/refresh.d.ts.map +1 -1
  36. package/dist/commands/refresh.js +20 -36
  37. package/dist/commands/refresh.js.map +1 -1
  38. package/dist/commands/schedule.d.ts +35 -0
  39. package/dist/commands/schedule.d.ts.map +1 -0
  40. package/dist/commands/schedule.js +151 -0
  41. package/dist/commands/schedule.js.map +1 -0
  42. package/dist/commands/sync.d.ts.map +1 -1
  43. package/dist/commands/sync.js +28 -7
  44. package/dist/commands/sync.js.map +1 -1
  45. package/dist/commands/tui.js +1 -1
  46. package/dist/commands/tui.js.map +1 -1
  47. package/dist/commands/ui.d.ts +1 -0
  48. package/dist/commands/ui.d.ts.map +1 -1
  49. package/dist/commands/ui.js +5 -2
  50. package/dist/commands/ui.js.map +1 -1
  51. package/dist/core/adapters/local-storage.d.ts +7 -0
  52. package/dist/core/adapters/local-storage.d.ts.map +1 -1
  53. package/dist/core/adapters/local-storage.js +13 -4
  54. package/dist/core/adapters/local-storage.js.map +1 -1
  55. package/dist/core/adapters/storage.test.js +18 -0
  56. package/dist/core/adapters/storage.test.js.map +1 -1
  57. package/dist/core/analysis-cache.d.ts +58 -0
  58. package/dist/core/analysis-cache.d.ts.map +1 -0
  59. package/dist/core/analysis-cache.js +116 -0
  60. package/dist/core/analysis-cache.js.map +1 -0
  61. package/dist/core/analysis-cache.test.d.ts +2 -0
  62. package/dist/core/analysis-cache.test.d.ts.map +1 -0
  63. package/dist/core/analysis-cache.test.js +108 -0
  64. package/dist/core/analysis-cache.test.js.map +1 -0
  65. package/dist/core/refresh.d.ts +41 -0
  66. package/dist/core/refresh.d.ts.map +1 -0
  67. package/dist/core/refresh.js +56 -0
  68. package/dist/core/refresh.js.map +1 -0
  69. package/dist/core/scheduler.d.ts +117 -0
  70. package/dist/core/scheduler.d.ts.map +1 -0
  71. package/dist/core/scheduler.js +361 -0
  72. package/dist/core/scheduler.js.map +1 -0
  73. package/dist/core/scheduler.test.d.ts +2 -0
  74. package/dist/core/scheduler.test.d.ts.map +1 -0
  75. package/dist/core/scheduler.test.js +163 -0
  76. package/dist/core/scheduler.test.js.map +1 -0
  77. package/dist/core/sync.d.ts +6 -0
  78. package/dist/core/sync.d.ts.map +1 -1
  79. package/dist/core/sync.js +17 -12
  80. package/dist/core/sync.js.map +1 -1
  81. package/dist/core/workspace.d.ts +6 -0
  82. package/dist/core/workspace.d.ts.map +1 -1
  83. package/dist/core/workspace.js +32 -15
  84. package/dist/core/workspace.js.map +1 -1
  85. package/dist/core/workspace.test.d.ts +2 -0
  86. package/dist/core/workspace.test.d.ts.map +1 -0
  87. package/dist/core/workspace.test.js +42 -0
  88. package/dist/core/workspace.test.js.map +1 -0
  89. package/dist/generators/index.d.ts +7 -1
  90. package/dist/generators/index.d.ts.map +1 -1
  91. package/dist/generators/index.js +14 -1
  92. package/dist/generators/index.js.map +1 -1
  93. package/dist/generators/map-generator.d.ts.map +1 -1
  94. package/dist/generators/map-generator.js +4 -3
  95. package/dist/generators/map-generator.js.map +1 -1
  96. package/dist/generators/map-generator.test.js +4 -2
  97. package/dist/generators/map-generator.test.js.map +1 -1
  98. package/dist/gui/assets/index-ElroPpSP.js +26 -0
  99. package/dist/gui/index.html +1 -1
  100. package/dist/index.js +108 -4
  101. package/dist/index.js.map +1 -1
  102. package/dist/orchestration/index.d.ts +1 -1
  103. package/dist/orchestration/index.d.ts.map +1 -1
  104. package/dist/orchestration/index.js +1 -1
  105. package/dist/orchestration/index.js.map +1 -1
  106. package/dist/orchestration/runner.d.ts +16 -1
  107. package/dist/orchestration/runner.d.ts.map +1 -1
  108. package/dist/orchestration/runner.js +41 -5
  109. package/dist/orchestration/runner.js.map +1 -1
  110. package/dist/orchestration/runner.test.d.ts +2 -0
  111. package/dist/orchestration/runner.test.d.ts.map +1 -0
  112. package/dist/orchestration/runner.test.js +71 -0
  113. package/dist/orchestration/runner.test.js.map +1 -0
  114. package/dist/server.d.ts +17 -1
  115. package/dist/server.d.ts.map +1 -1
  116. package/dist/server.js +275 -114
  117. package/dist/server.js.map +1 -1
  118. package/dist/server.test.js +158 -1
  119. package/dist/server.test.js.map +1 -1
  120. package/dist/utils/multi-git.d.ts +21 -0
  121. package/dist/utils/multi-git.d.ts.map +1 -1
  122. package/dist/utils/multi-git.js +40 -4
  123. package/dist/utils/multi-git.js.map +1 -1
  124. package/dist/utils/prompts.d.ts.map +1 -1
  125. package/dist/utils/prompts.js +8 -2
  126. package/dist/utils/prompts.js.map +1 -1
  127. package/package.json +13 -1
  128. package/.agents/skills/ccc/SKILL.md +0 -71
  129. package/.agents/skills/ccc/references/management.md +0 -110
  130. package/.agents/skills/ccc/references/settings.md +0 -126
  131. package/.github/dependabot.yml +0 -52
  132. package/.github/workflows/ci.yml +0 -44
  133. package/.github/workflows/dependabot-merge.yml +0 -53
  134. package/.github/workflows/release-desktop.yml +0 -67
  135. package/.github/workflows/release-npm.yml +0 -65
  136. package/.github/workflows/release-vscode.yml +0 -61
  137. package/.vscode/launch.json +0 -17
  138. package/.vscode/tasks.json +0 -17
  139. package/GETTING_STARTED.md +0 -114
  140. package/desktop/build-installer.js +0 -228
  141. package/desktop/build.js +0 -118
  142. package/desktop/installer/nexusflow.iss +0 -35
  143. package/desktop/neutralino.config.json +0 -39
  144. package/desktop/package-lock.json +0 -1060
  145. package/desktop/package.json +0 -14
  146. package/dist/gui/assets/index-D8iJPvnk.js +0 -26
  147. package/extension/media/icon.svg +0 -1
  148. package/extension/package-lock.json +0 -6044
  149. package/extension/package.json +0 -117
  150. package/extension/src/extension.ts +0 -653
  151. package/extension/tsconfig.json +0 -21
  152. package/gui/README.md +0 -73
  153. package/gui/e2e/dashboard.spec.ts +0 -61
  154. package/gui/e2e/wizard.spec.ts +0 -346
  155. package/gui/eslint.config.js +0 -26
  156. package/gui/index.html +0 -17
  157. package/gui/package-lock.json +0 -3137
  158. package/gui/package.json +0 -37
  159. package/gui/playwright.config.ts +0 -41
  160. package/gui/public/app_logo.png +0 -0
  161. package/gui/public/favicon.svg +0 -1
  162. package/gui/public/icons.svg +0 -24
  163. package/gui/src/App.css +0 -1
  164. package/gui/src/App.tsx +0 -3573
  165. package/gui/src/app/AppSidebar.tsx +0 -69
  166. package/gui/src/assets/hero.png +0 -0
  167. package/gui/src/assets/react.svg +0 -1
  168. package/gui/src/assets/vite.svg +0 -1
  169. package/gui/src/components/AddRepoPicker.tsx +0 -81
  170. package/gui/src/components/ui/Button.tsx +0 -40
  171. package/gui/src/components/ui/Card.tsx +0 -6
  172. package/gui/src/components/ui/EmptyState.tsx +0 -22
  173. package/gui/src/components/ui/Input.tsx +0 -21
  174. package/gui/src/components/ui/Menu.tsx +0 -71
  175. package/gui/src/components/ui/Modal.tsx +0 -39
  176. package/gui/src/components/ui/PageHeader.tsx +0 -21
  177. package/gui/src/components/ui/RepoStatusStrip.tsx +0 -33
  178. package/gui/src/components/ui/Skeleton.tsx +0 -5
  179. package/gui/src/components/ui/StatusPill.tsx +0 -36
  180. package/gui/src/components/ui/Tabs.tsx +0 -40
  181. package/gui/src/components/ui/cn.ts +0 -4
  182. package/gui/src/components/ui/index.ts +0 -16
  183. package/gui/src/features/changes/ChangesViewer.tsx +0 -388
  184. package/gui/src/features/knowledge/KnowledgeBase.tsx +0 -84
  185. package/gui/src/features/onboarding/OnboardingWizard.tsx +0 -230
  186. package/gui/src/features/plan/ImplementationPlan.tsx +0 -32
  187. package/gui/src/features/services/ServiceConsole.tsx +0 -215
  188. package/gui/src/features/sessions/SessionHistory.tsx +0 -195
  189. package/gui/src/index.css +0 -167
  190. package/gui/src/lib/status.ts +0 -24
  191. package/gui/src/main.tsx +0 -94
  192. package/gui/src/pages/DashboardPage.tsx +0 -129
  193. package/gui/src/pages/WorkspacesPage.tsx +0 -352
  194. package/gui/src/types.ts +0 -81
  195. package/gui/tsconfig.app.json +0 -25
  196. package/gui/tsconfig.json +0 -7
  197. package/gui/tsconfig.node.json +0 -24
  198. package/gui/vite.config.ts +0 -12
  199. package/resources/workflows/plan-implement-review.md +0 -8
  200. package/resources/workflows/research-verify.md +0 -6
  201. package/resources/workflows/solo-developer.md +0 -3
  202. package/scripts/simulate-workspaces.ts +0 -55
  203. package/skills-lock.json +0 -11
  204. package/src/analyzers/detect-apis.ts +0 -290
  205. package/src/analyzers/detect-deps.ts +0 -315
  206. package/src/analyzers/detect-existing.ts +0 -74
  207. package/src/analyzers/detect-ports.ts +0 -110
  208. package/src/analyzers/index.ts +0 -97
  209. package/src/analyzers/messaging-analyzer.ts +0 -254
  210. package/src/analyzers/readme-summarizer.ts +0 -102
  211. package/src/analyzers/run-analyzer.ts +0 -269
  212. package/src/analyzers/tech-stack.ts +0 -283
  213. package/src/commands/adapter.ts +0 -348
  214. package/src/commands/add-repo.ts +0 -156
  215. package/src/commands/commands.test.ts +0 -156
  216. package/src/commands/commit.ts +0 -131
  217. package/src/commands/config.ts +0 -52
  218. package/src/commands/create.ts +0 -204
  219. package/src/commands/desktop.ts +0 -128
  220. package/src/commands/diff.ts +0 -125
  221. package/src/commands/doctor.ts +0 -370
  222. package/src/commands/handoff.ts +0 -266
  223. package/src/commands/init.ts +0 -134
  224. package/src/commands/list.ts +0 -46
  225. package/src/commands/logs.ts +0 -63
  226. package/src/commands/mcp.ts +0 -94
  227. package/src/commands/open.ts +0 -129
  228. package/src/commands/refresh.ts +0 -137
  229. package/src/commands/remove.ts +0 -98
  230. package/src/commands/start.ts +0 -117
  231. package/src/commands/status.ts +0 -54
  232. package/src/commands/stop.ts +0 -55
  233. package/src/commands/sync.ts +0 -125
  234. package/src/commands/tui.ts +0 -484
  235. package/src/commands/ui.ts +0 -111
  236. package/src/core/adapters/local-storage.ts +0 -66
  237. package/src/core/adapters/obsidian-storage.ts +0 -151
  238. package/src/core/adapters/registry.ts +0 -44
  239. package/src/core/adapters/storage.test.ts +0 -174
  240. package/src/core/adapters/vault-storage.ts +0 -86
  241. package/src/core/config.test.ts +0 -96
  242. package/src/core/config.ts +0 -140
  243. package/src/core/graph.ts +0 -344
  244. package/src/core/plugins/index.ts +0 -17
  245. package/src/core/plugins/loader.ts +0 -36
  246. package/src/core/ports/storage.ts +0 -71
  247. package/src/core/scanner.test.ts +0 -61
  248. package/src/core/scanner.ts +0 -91
  249. package/src/core/storage.ts +0 -37
  250. package/src/core/sync.ts +0 -121
  251. package/src/core/workspace-state.test.ts +0 -108
  252. package/src/core/workspace-state.ts +0 -130
  253. package/src/core/workspace.ts +0 -487
  254. package/src/core/worktree.ts +0 -120
  255. package/src/generators/antigravity.ts +0 -32
  256. package/src/generators/base.ts +0 -243
  257. package/src/generators/claude.ts +0 -34
  258. package/src/generators/codex.ts +0 -48
  259. package/src/generators/copilot.ts +0 -56
  260. package/src/generators/cursor.ts +0 -44
  261. package/src/generators/diff-context.ts +0 -75
  262. package/src/generators/index.ts +0 -322
  263. package/src/generators/map-generator.test.ts +0 -159
  264. package/src/generators/map-generator.ts +0 -539
  265. package/src/generators/plan-generator.ts +0 -483
  266. package/src/generators/skills-generator.ts +0 -259
  267. package/src/index.ts +0 -539
  268. package/src/mcp/server.ts +0 -338
  269. package/src/orchestration/detect.ts +0 -313
  270. package/src/orchestration/index.ts +0 -7
  271. package/src/orchestration/runner.ts +0 -283
  272. package/src/server.test.ts +0 -618
  273. package/src/server.ts +0 -1402
  274. package/src/types.ts +0 -451
  275. package/src/utils/detect-ai.test.ts +0 -44
  276. package/src/utils/detect-ai.ts +0 -84
  277. package/src/utils/detect-editors.test.ts +0 -48
  278. package/src/utils/detect-editors.ts +0 -57
  279. package/src/utils/git.test.ts +0 -74
  280. package/src/utils/git.ts +0 -117
  281. package/src/utils/local-ai.test.ts +0 -130
  282. package/src/utils/local-ai.ts +0 -116
  283. package/src/utils/multi-git.test.ts +0 -158
  284. package/src/utils/multi-git.ts +0 -405
  285. package/src/utils/prompts.ts +0 -209
  286. package/src/utils/session-finder.ts +0 -483
  287. package/src/utils/system-scanner.test.ts +0 -89
  288. package/src/utils/system-scanner.ts +0 -96
  289. package/src/utils/update-check.ts +0 -286
  290. package/src/utils/workflow-advisor.ts +0 -118
  291. package/src/utils/workflows.test.ts +0 -114
  292. package/src/utils/workflows.ts +0 -178
  293. package/tsconfig.json +0 -19
  294. package/vitest.config.ts +0 -14
@@ -0,0 +1,361 @@
1
+ /**
2
+ * @module core/scheduler
3
+ * Recurring workspace jobs — periodic `sync` and `refresh` runs per workspace.
4
+ *
5
+ * Job definitions are persisted at `~/.nexusflow/schedules.json` and executed
6
+ * by any long-running NexusFlow process (the dashboard server started with
7
+ * `nexusflow ui` runs the scheduler automatically). Jobs use the headless
8
+ * cores (`syncWorkspace`, `refreshWorkspace`), which are cache-aware: a
9
+ * scheduled run only re-analyzes repos whose content changed, so unattended
10
+ * schedules stay token-efficient and never churn unchanged context files.
11
+ */
12
+ import * as fs from 'node:fs/promises';
13
+ import * as path from 'node:path';
14
+ import { getConfigDir, ensureConfigDir } from './config.js';
15
+ import { syncWorkspace } from './sync.js';
16
+ import { refreshWorkspace } from './refresh.js';
17
+ /** Name of the schedules file inside ~/.nexusflow. */
18
+ const SCHEDULES_FILE = 'schedules.json';
19
+ const SCHEDULES_LOCK_FILE = `${SCHEDULES_FILE}.lock`;
20
+ const RUN_LOCK_DIR = 'schedule-runs';
21
+ const STORE_LOCK_TIMEOUT_MS = 10_000;
22
+ const STORE_LOCK_STALE_MS = 2 * 60_000;
23
+ const RUN_LOCK_STALE_MS = 24 * 60 * 60_000;
24
+ const LOCK_RETRY_MS = 50;
25
+ let scheduleStoreMutationQueue = Promise.resolve();
26
+ /**
27
+ * Returns the path to the schedules file (~/.nexusflow/schedules.json).
28
+ */
29
+ export function getSchedulesPath() {
30
+ return path.join(getConfigDir(), SCHEDULES_FILE);
31
+ }
32
+ function getSchedulesLockPath() {
33
+ return path.join(getConfigDir(), SCHEDULES_LOCK_FILE);
34
+ }
35
+ function getRunLockPath(jobId) {
36
+ const safeId = jobId.replace(/[^a-zA-Z0-9._-]/g, '_');
37
+ return path.join(getConfigDir(), RUN_LOCK_DIR, `${safeId}.lock`);
38
+ }
39
+ function getErrorCode(error) {
40
+ return typeof error === 'object' && error !== null && 'code' in error
41
+ ? String(error.code)
42
+ : undefined;
43
+ }
44
+ function delay(ms) {
45
+ return new Promise((resolve) => setTimeout(resolve, ms));
46
+ }
47
+ async function clearStaleLock(lockPath, staleMs) {
48
+ try {
49
+ const stat = await fs.stat(lockPath);
50
+ if (Date.now() - stat.mtimeMs <= staleMs)
51
+ return false;
52
+ await fs.unlink(lockPath);
53
+ return true;
54
+ }
55
+ catch (error) {
56
+ return getErrorCode(error) === 'ENOENT';
57
+ }
58
+ }
59
+ async function acquireLock(lockPath, options) {
60
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
61
+ const startedAt = Date.now();
62
+ for (;;) {
63
+ try {
64
+ const handle = await fs.open(lockPath, 'wx');
65
+ try {
66
+ await handle.writeFile(JSON.stringify({
67
+ pid: process.pid,
68
+ createdAt: new Date().toISOString(),
69
+ }) + '\n', 'utf-8');
70
+ }
71
+ catch (error) {
72
+ await handle.close().catch(() => { });
73
+ await fs.unlink(lockPath).catch(() => { });
74
+ throw error;
75
+ }
76
+ let released = false;
77
+ return async () => {
78
+ if (released)
79
+ return;
80
+ released = true;
81
+ await handle.close().catch(() => { });
82
+ await fs.unlink(lockPath).catch(() => { });
83
+ };
84
+ }
85
+ catch (error) {
86
+ if (getErrorCode(error) !== 'EEXIST')
87
+ throw error;
88
+ if (await clearStaleLock(lockPath, options.staleMs))
89
+ continue;
90
+ if (Date.now() - startedAt >= options.timeoutMs) {
91
+ throw new Error(options.timeoutMessage);
92
+ }
93
+ await delay(LOCK_RETRY_MS);
94
+ }
95
+ }
96
+ }
97
+ function enqueueScheduleStoreMutation(operation) {
98
+ const run = scheduleStoreMutationQueue.then(operation, operation);
99
+ scheduleStoreMutationQueue = run.then(() => undefined, () => undefined);
100
+ return run;
101
+ }
102
+ async function mutateSchedules(mutator) {
103
+ return enqueueScheduleStoreMutation(async () => {
104
+ const release = await acquireLock(getSchedulesLockPath(), {
105
+ staleMs: STORE_LOCK_STALE_MS,
106
+ timeoutMs: STORE_LOCK_TIMEOUT_MS,
107
+ timeoutMessage: 'Timed out waiting for schedules.json to become writable.',
108
+ });
109
+ try {
110
+ const store = await loadSchedules();
111
+ const result = await mutator(store);
112
+ if (result.save) {
113
+ await saveSchedules(store);
114
+ }
115
+ return result.value;
116
+ }
117
+ finally {
118
+ await release();
119
+ }
120
+ });
121
+ }
122
+ /**
123
+ * Loads all schedules, returning an empty store when the file does not exist
124
+ * or cannot be parsed.
125
+ */
126
+ export async function loadSchedules() {
127
+ try {
128
+ const raw = await fs.readFile(getSchedulesPath(), 'utf-8');
129
+ const store = JSON.parse(raw);
130
+ if (!Array.isArray(store.jobs)) {
131
+ store.jobs = [];
132
+ }
133
+ return store;
134
+ }
135
+ catch {
136
+ return { version: 1, jobs: [] };
137
+ }
138
+ }
139
+ /**
140
+ * Persists the schedule store to ~/.nexusflow/schedules.json.
141
+ */
142
+ export async function saveSchedules(store) {
143
+ await ensureConfigDir();
144
+ const data = JSON.stringify(store, null, 2) + '\n';
145
+ await fs.writeFile(getSchedulesPath(), data, 'utf-8');
146
+ }
147
+ /**
148
+ * Parses a human interval like '30m', '2h', '1d', or a bare minute count
149
+ * ('45') into minutes. Returns null for unparseable or non-positive input.
150
+ */
151
+ export function parseInterval(text) {
152
+ const match = String(text).trim().match(/^(\d+)\s*([mhd]?)$/i);
153
+ if (!match)
154
+ return null;
155
+ const value = parseInt(match[1], 10);
156
+ if (value <= 0)
157
+ return null;
158
+ switch ((match[2] || 'm').toLowerCase()) {
159
+ case 'h':
160
+ return value * 60;
161
+ case 'd':
162
+ return value * 60 * 24;
163
+ default:
164
+ return value;
165
+ }
166
+ }
167
+ /**
168
+ * Formats an interval in minutes back to a compact human string.
169
+ */
170
+ export function formatInterval(minutes) {
171
+ if (minutes % (60 * 24) === 0)
172
+ return `${minutes / (60 * 24)}d`;
173
+ if (minutes % 60 === 0)
174
+ return `${minutes / 60}h`;
175
+ return `${minutes}m`;
176
+ }
177
+ /**
178
+ * Whether a job is due to run: it is enabled and either never ran or its
179
+ * interval has elapsed since the last run. A job whose interval elapsed while
180
+ * no scheduler was running simply runs on the next tick.
181
+ */
182
+ export function isDue(job, now = new Date()) {
183
+ if (!job.enabled)
184
+ return false;
185
+ if (!job.lastRunAt)
186
+ return true;
187
+ const last = Date.parse(job.lastRunAt);
188
+ if (Number.isNaN(last))
189
+ return true;
190
+ return now.getTime() - last >= job.intervalMinutes * 60_000;
191
+ }
192
+ /**
193
+ * Computes when a job will next be due, for display purposes.
194
+ */
195
+ export function nextDueAt(job) {
196
+ if (!job.enabled)
197
+ return null;
198
+ if (!job.lastRunAt)
199
+ return new Date();
200
+ const last = Date.parse(job.lastRunAt);
201
+ if (Number.isNaN(last))
202
+ return new Date();
203
+ return new Date(last + job.intervalMinutes * 60_000);
204
+ }
205
+ /**
206
+ * Adds a new schedule and persists it.
207
+ *
208
+ * @param input - Workspace, task, and interval for the new job.
209
+ * @returns The created job.
210
+ */
211
+ export async function addSchedule(input) {
212
+ return mutateSchedules((store) => {
213
+ const workspaceName = path.basename(input.workspacePath);
214
+ const job = {
215
+ id: `${input.task}-${workspaceName}-${Date.now().toString(36)}`,
216
+ workspacePath: input.workspacePath,
217
+ task: input.task,
218
+ intervalMinutes: input.intervalMinutes,
219
+ enabled: true,
220
+ createdAt: new Date().toISOString(),
221
+ };
222
+ store.jobs.push(job);
223
+ return { value: job, save: true };
224
+ });
225
+ }
226
+ /**
227
+ * Removes a schedule by id. Returns false when no job matched.
228
+ */
229
+ export async function removeSchedule(id) {
230
+ return mutateSchedules((store) => {
231
+ const before = store.jobs.length;
232
+ store.jobs = store.jobs.filter((j) => j.id !== id);
233
+ const removed = store.jobs.length !== before;
234
+ return { value: removed, save: removed };
235
+ });
236
+ }
237
+ /**
238
+ * Enables or disables a schedule by id. Returns the updated job, or null
239
+ * when no job matched.
240
+ */
241
+ export async function setScheduleEnabled(id, enabled) {
242
+ return mutateSchedules((store) => {
243
+ const job = store.jobs.find((j) => j.id === id);
244
+ if (!job)
245
+ return { value: null, save: false };
246
+ job.enabled = enabled;
247
+ return { value: job, save: true };
248
+ });
249
+ }
250
+ /**
251
+ * Runs a single job immediately and records the outcome in the store.
252
+ * Never throws — failures are captured in the returned result.
253
+ *
254
+ * @param job - The job to run.
255
+ * @returns The classified outcome with a summary message.
256
+ */
257
+ export async function runJob(job) {
258
+ let releaseRunLock = null;
259
+ try {
260
+ releaseRunLock = await acquireLock(getRunLockPath(job.id), {
261
+ staleMs: RUN_LOCK_STALE_MS,
262
+ timeoutMs: 0,
263
+ timeoutMessage: `Schedule ${job.id} is already running.`,
264
+ });
265
+ }
266
+ catch (error) {
267
+ return {
268
+ status: 'error',
269
+ message: error instanceof Error ? error.message : String(error),
270
+ };
271
+ }
272
+ let result;
273
+ try {
274
+ if (job.task === 'sync') {
275
+ const report = await syncWorkspace(job.workspacePath);
276
+ const parts = [`${report.syncedCount} synced`];
277
+ if (report.conflictCount > 0)
278
+ parts.push(`${report.conflictCount} conflict(s)`);
279
+ if (report.errorCount > 0)
280
+ parts.push(`${report.errorCount} error(s)`);
281
+ parts.push(report.contextRefreshed ? 'context refreshed' : 'context unchanged');
282
+ result = {
283
+ status: report.conflictCount > 0 || report.errorCount > 0 ? 'error' : 'success',
284
+ message: parts.join(', '),
285
+ };
286
+ }
287
+ else {
288
+ const report = await refreshWorkspace(job.workspacePath);
289
+ result = {
290
+ status: 'success',
291
+ message: `${report.analyzedRepos.length} repo(s) re-analyzed, ${report.reusedRepos.length} reused from cache`,
292
+ };
293
+ }
294
+ }
295
+ catch (error) {
296
+ result = {
297
+ status: 'error',
298
+ message: error instanceof Error ? error.message : String(error),
299
+ };
300
+ }
301
+ // Record the run. Re-load the store so concurrent CLI edits are not lost.
302
+ try {
303
+ await mutateSchedules((store) => {
304
+ const stored = store.jobs.find((j) => j.id === job.id);
305
+ if (!stored)
306
+ return { value: undefined, save: false };
307
+ stored.lastRunAt = new Date().toISOString();
308
+ stored.lastStatus = result.status;
309
+ stored.lastMessage = result.message;
310
+ return { value: undefined, save: true };
311
+ });
312
+ }
313
+ catch {
314
+ // Best-effort bookkeeping; the job itself already ran.
315
+ }
316
+ finally {
317
+ await releaseRunLock();
318
+ }
319
+ return result;
320
+ }
321
+ /**
322
+ * Starts the scheduler loop inside the current process. Every tick it
323
+ * re-loads the schedules file (so jobs added/removed via CLI are picked up
324
+ * without a restart) and runs all due jobs sequentially. Ticks never overlap.
325
+ *
326
+ * @param options - Tick interval (default 60s) and an optional log sink.
327
+ * @returns A stop function that clears the interval.
328
+ */
329
+ export function startScheduler(options = {}) {
330
+ const tickMs = options.tickMs ?? 60_000;
331
+ const log = options.log ?? (() => { });
332
+ let running = false;
333
+ const tick = async () => {
334
+ if (running)
335
+ return;
336
+ running = true;
337
+ try {
338
+ const store = await loadSchedules();
339
+ for (const job of store.jobs) {
340
+ if (!isDue(job))
341
+ continue;
342
+ log(`Running scheduled ${job.task} for ${path.basename(job.workspacePath)} (${job.id})...`);
343
+ const result = await runJob(job);
344
+ log(`Scheduled ${job.task} ${result.status === 'success' ? 'completed' : 'failed'}: ${result.message}`);
345
+ }
346
+ }
347
+ catch {
348
+ // A broken schedules file must not kill the host process's timer.
349
+ }
350
+ finally {
351
+ running = false;
352
+ }
353
+ };
354
+ const timer = setInterval(tick, tickMs);
355
+ // Don't let the scheduler keep a short-lived CLI process alive.
356
+ timer.unref?.();
357
+ // First tick runs immediately so overdue jobs don't wait a full interval.
358
+ void tick();
359
+ return () => clearInterval(timer);
360
+ }
361
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scheduler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.test.d.ts","sourceRoot":"","sources":["../../src/core/scheduler.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import { addSchedule, formatInterval, isDue, loadSchedules, nextDueAt, parseInterval, removeSchedule, runJob, setScheduleEnabled, } from './scheduler.js';
4
+ import { refreshWorkspace } from './refresh.js';
5
+ import { syncWorkspace } from './sync.js';
6
+ vi.mock('node:fs/promises');
7
+ vi.mock('./refresh.js');
8
+ vi.mock('./sync.js');
9
+ /** Parses the JSON written by the most recent writeFile call. */
10
+ function lastWritten() {
11
+ const calls = vi.mocked(fs.writeFile).mock.calls;
12
+ const data = calls[calls.length - 1][1];
13
+ return JSON.parse(data);
14
+ }
15
+ function mockScheduleStore(initial) {
16
+ let current = JSON.parse(JSON.stringify(initial));
17
+ vi.mocked(fs.readFile).mockImplementation(async () => JSON.stringify(current));
18
+ vi.mocked(fs.writeFile).mockImplementation(async (_path, data) => {
19
+ current = JSON.parse(String(data));
20
+ });
21
+ return { current: () => current };
22
+ }
23
+ function makeJob(overrides = {}) {
24
+ return {
25
+ id: 'sync-ws-abc',
26
+ workspacePath: '/ws',
27
+ task: 'sync',
28
+ intervalMinutes: 60,
29
+ enabled: true,
30
+ createdAt: '2026-01-01T00:00:00.000Z',
31
+ ...overrides,
32
+ };
33
+ }
34
+ describe('scheduler', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
38
+ vi.mocked(fs.mkdir).mockResolvedValue(undefined);
39
+ vi.mocked(fs.unlink).mockResolvedValue(undefined);
40
+ vi.mocked(fs.stat).mockResolvedValue({ mtimeMs: Date.now() });
41
+ vi.mocked(fs.open).mockResolvedValue({
42
+ writeFile: vi.fn().mockResolvedValue(undefined),
43
+ close: vi.fn().mockResolvedValue(undefined),
44
+ });
45
+ vi.mocked(syncWorkspace).mockResolvedValue({
46
+ syncedCount: 1,
47
+ conflictCount: 0,
48
+ errorCount: 0,
49
+ contextRefreshed: false,
50
+ });
51
+ vi.mocked(refreshWorkspace).mockResolvedValue({
52
+ analyzedRepos: ['/repo'],
53
+ reusedRepos: [],
54
+ });
55
+ });
56
+ describe('parseInterval', () => {
57
+ it('parses minutes, hours, days, and bare numbers', () => {
58
+ expect(parseInterval('30m')).toBe(30);
59
+ expect(parseInterval('2h')).toBe(120);
60
+ expect(parseInterval('1d')).toBe(1440);
61
+ expect(parseInterval('45')).toBe(45);
62
+ });
63
+ it('rejects invalid or non-positive input', () => {
64
+ expect(parseInterval('')).toBeNull();
65
+ expect(parseInterval('abc')).toBeNull();
66
+ expect(parseInterval('0m')).toBeNull();
67
+ expect(parseInterval('-5m')).toBeNull();
68
+ expect(parseInterval('2w')).toBeNull();
69
+ });
70
+ });
71
+ describe('formatInterval', () => {
72
+ it('formats back to compact strings', () => {
73
+ expect(formatInterval(30)).toBe('30m');
74
+ expect(formatInterval(120)).toBe('2h');
75
+ expect(formatInterval(1440)).toBe('1d');
76
+ expect(formatInterval(90)).toBe('90m');
77
+ });
78
+ });
79
+ describe('isDue', () => {
80
+ it('is due when it has never run', () => {
81
+ expect(isDue(makeJob())).toBe(true);
82
+ });
83
+ it('is not due when disabled, even if never run', () => {
84
+ expect(isDue(makeJob({ enabled: false }))).toBe(false);
85
+ });
86
+ it('respects the interval since the last run', () => {
87
+ const now = new Date('2026-01-02T12:00:00.000Z');
88
+ const recent = makeJob({ lastRunAt: '2026-01-02T11:30:00.000Z' }); // 30m ago, every 60m
89
+ const stale = makeJob({ lastRunAt: '2026-01-02T10:00:00.000Z' }); // 2h ago, every 60m
90
+ expect(isDue(recent, now)).toBe(false);
91
+ expect(isDue(stale, now)).toBe(true);
92
+ });
93
+ it('treats an unparseable lastRunAt as due', () => {
94
+ expect(isDue(makeJob({ lastRunAt: 'garbage' }))).toBe(true);
95
+ });
96
+ });
97
+ describe('nextDueAt', () => {
98
+ it('is null for disabled jobs', () => {
99
+ expect(nextDueAt(makeJob({ enabled: false }))).toBeNull();
100
+ });
101
+ it('is lastRunAt + interval for jobs that ran', () => {
102
+ const job = makeJob({ lastRunAt: '2026-01-02T10:00:00.000Z' });
103
+ expect(nextDueAt(job).toISOString()).toBe('2026-01-02T11:00:00.000Z');
104
+ });
105
+ });
106
+ describe('store operations', () => {
107
+ it('loadSchedules returns an empty store when the file is absent', async () => {
108
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
109
+ const store = await loadSchedules();
110
+ expect(store.jobs).toEqual([]);
111
+ });
112
+ it('addSchedule persists a new enabled job', async () => {
113
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
114
+ const job = await addSchedule({ workspacePath: '/ws/feature-x', task: 'refresh', intervalMinutes: 120 });
115
+ expect(job.enabled).toBe(true);
116
+ expect(job.task).toBe('refresh');
117
+ expect(job.id).toContain('refresh-feature-x');
118
+ const written = lastWritten();
119
+ expect(written.jobs).toHaveLength(1);
120
+ expect(written.jobs[0].intervalMinutes).toBe(120);
121
+ });
122
+ it('removeSchedule deletes by id and reports misses', async () => {
123
+ mockScheduleStore({ version: 1, jobs: [makeJob()] });
124
+ expect(await removeSchedule('sync-ws-abc')).toBe(true);
125
+ expect(lastWritten().jobs).toHaveLength(0);
126
+ expect(await removeSchedule('nope')).toBe(false);
127
+ });
128
+ it('setScheduleEnabled toggles and persists', async () => {
129
+ mockScheduleStore({ version: 1, jobs: [makeJob()] });
130
+ const job = await setScheduleEnabled('sync-ws-abc', false);
131
+ expect(job.enabled).toBe(false);
132
+ expect(lastWritten().jobs[0].enabled).toBe(false);
133
+ expect(await setScheduleEnabled('nope', true)).toBeNull();
134
+ });
135
+ it('serializes concurrent schedule mutations so updates are not lost', async () => {
136
+ const store = mockScheduleStore({ version: 1, jobs: [] });
137
+ const [first, second] = await Promise.all([
138
+ addSchedule({ workspacePath: '/ws/feature-a', task: 'sync', intervalMinutes: 30 }),
139
+ addSchedule({ workspacePath: '/ws/feature-b', task: 'refresh', intervalMinutes: 60 }),
140
+ ]);
141
+ expect(store.current().jobs.map((job) => job.id)).toEqual([first.id, second.id]);
142
+ });
143
+ });
144
+ describe('runJob', () => {
145
+ it('does not run a job that already has an active run lock', async () => {
146
+ vi.mocked(fs.open).mockRejectedValueOnce(Object.assign(new Error('exists'), { code: 'EEXIST' }));
147
+ const result = await runJob(makeJob());
148
+ expect(result.status).toBe('error');
149
+ expect(result.message).toContain('already running');
150
+ expect(syncWorkspace).not.toHaveBeenCalled();
151
+ expect(refreshWorkspace).not.toHaveBeenCalled();
152
+ });
153
+ it('records the run result through the serialized schedule mutation path', async () => {
154
+ const store = mockScheduleStore({ version: 1, jobs: [makeJob()] });
155
+ const result = await runJob(makeJob());
156
+ expect(result.status).toBe('success');
157
+ expect(syncWorkspace).toHaveBeenCalledWith('/ws');
158
+ expect(store.current().jobs[0].lastStatus).toBe('success');
159
+ expect(store.current().jobs[0].lastMessage).toBe('1 synced, context unchanged');
160
+ });
161
+ });
162
+ });
163
+ //# sourceMappingURL=scheduler.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.test.js","sourceRoot":"","sources":["../../src/core/scheduler.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EACL,WAAW,EACX,cAAc,EACd,KAAK,EACL,aAAa,EACb,SAAS,EACT,aAAa,EACb,cAAc,EACd,MAAM,EACN,kBAAkB,GAGnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;AAC5B,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AACxB,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAErB,iEAAiE;AACjE,SAAS,WAAW;IAClB,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IACjD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAW,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;AAC3C,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAsB;IAC/C,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAkB,CAAC;IACnE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,kBAAkB,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAQ,CAAC,CAAC;IACtF,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC/D,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAkB,CAAC;IACtD,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,OAAO,CAAC,YAAmC,EAAE;IACpD,OAAO;QACL,EAAE,EAAE,aAAa;QACjB,aAAa,EAAE,KAAK;QACpB,IAAI,EAAE,MAAM;QACZ,eAAe,EAAE,EAAE;QACnB,OAAO,EAAE,IAAI;QACb,SAAS,EAAE,0BAA0B;QACrC,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACrD,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,iBAAiB,CAAC,SAAgB,CAAC,CAAC;QACxD,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,SAAgB,CAAC,CAAC;QACzD,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,EAAS,CAAC,CAAC;QACrE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,iBAAiB,CAAC;YACnC,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;YAC/C,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;SACrC,CAAC,CAAC;QACV,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,iBAAiB,CAAC;YACzC,WAAW,EAAE,CAAC;YACd,aAAa,EAAE,CAAC;YAChB,UAAU,EAAE,CAAC;YACb,gBAAgB,EAAE,KAAK;SACjB,CAAC,CAAC;QACV,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,iBAAiB,CAAC;YAC5C,aAAa,EAAE,CAAC,OAAO,CAAC;YACxB,WAAW,EAAE,EAAE;SACT,CAAC,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;YACvD,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACtC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;YAC/C,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YACrC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YACxC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YACxC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;YACzC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;QACrB,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;YACtC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;YACrD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;YAClD,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,0BAA0B,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,SAAS,EAAE,0BAA0B,EAAE,CAAC,CAAC,CAAC,qBAAqB;YACxF,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,SAAS,EAAE,0BAA0B,EAAE,CAAC,CAAC,CAAC,oBAAoB;YAEtF,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;QACzB,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;YACnC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC5D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;YACnD,MAAM,GAAG,GAAG,OAAO,CAAC,EAAE,SAAS,EAAE,0BAA0B,EAAE,CAAC,CAAC;YAC/D,MAAM,CAAC,SAAS,CAAC,GAAG,CAAE,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACzE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;YAC5E,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAE9D,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAC;YAEpC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;YACtD,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAE9D,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC;YAEzG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACjC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;YAC9C,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;YAC9B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;YAC/D,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YAErD,MAAM,CAAC,MAAM,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,CAAC,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;YACvD,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YAErD,MAAM,GAAG,GAAG,MAAM,kBAAkB,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YAE3D,MAAM,CAAC,GAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjC,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnD,MAAM,CAAC,MAAM,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC5D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;YAChF,MAAM,KAAK,GAAG,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YAE1D,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACxC,WAAW,CAAC,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC;gBAClF,WAAW,CAAC,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC;aACtF,CAAC,CAAC;YAEH,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;QACtB,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;YACtE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,qBAAqB,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;YAEjG,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAEvC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;YACpD,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YAC7C,MAAM,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;YACpF,MAAM,KAAK,GAAG,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YAEnE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAEvC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtC,MAAM,CAAC,aAAa,CAAC,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;YAClD,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC5D,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -10,12 +10,16 @@ import type { SyncStatus } from '../types.js';
10
10
  export interface RepoSyncReport {
11
11
  /** Directory name of the repo. */
12
12
  name: string;
13
+ /** Base branch the repo rebases onto (e.g. 'main'). */
14
+ baseBranch: string;
13
15
  /** Classified outcome. */
14
16
  status: SyncStatus;
15
17
  /** Human-readable message. */
16
18
  message: string;
17
19
  /** Conflict stderr — populated only for `status === 'conflict'`. */
18
20
  conflict?: string;
21
+ /** Paths that had merge conflicts — populated only for `status === 'conflict'`. */
22
+ conflictFiles?: string[];
19
23
  }
20
24
  /** Aggregated result of syncing an entire workspace. */
21
25
  export interface SyncReport {
@@ -31,6 +35,8 @@ export interface SyncReport {
31
35
  conflictCount: number;
32
36
  /** Count of repos that failed for infrastructure reasons (fetch/auth, etc.). */
33
37
  errorCount: number;
38
+ /** Whether context files/maps were regenerated (only when content changed). */
39
+ contextRefreshed: boolean;
34
40
  }
35
41
  /**
36
42
  * Syncs all repos in a workspace. Throws if the workspace configuration cannot
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/core/sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AAEhE,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wDAAwD;AACxD,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA8D9E"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/core/sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AAEhE,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wDAAwD;AACxD,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAOD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAsE9E"}
package/dist/core/sync.js CHANGED
@@ -5,11 +5,10 @@
5
5
  * context files. Produces no console output, so it can back the CLI, the HTTP
6
6
  * API, and the MCP tool alike.
7
7
  */
8
- import * as path from 'node:path';
9
- import { loadFeatureConfig } from './workspace.js';
8
+ import { loadFeatureConfig, resolveRepoInfos } from './workspace.js';
10
9
  import { recordRepoSync } from './workspace-state.js';
11
10
  import { getWorkspaceRepos, rebaseRepo } from '../utils/multi-git.js';
12
- import { analyzeAllRepos } from '../analyzers/index.js';
11
+ import { analyzeAllReposCached } from '../analyzers/index.js';
13
12
  import { generateContextFiles } from '../generators/index.js';
14
13
  /** A sync status counts as "synced" when the rebase itself landed. */
15
14
  function isSynced(status) {
@@ -42,9 +41,11 @@ export async function syncWorkspace(workspacePath) {
42
41
  });
43
42
  repoReports.push({
44
43
  name: repo.name,
44
+ baseBranch: defaultBranch,
45
45
  status: result.status,
46
46
  message: result.message,
47
47
  conflict: result.conflict,
48
+ conflictFiles: result.conflictFiles,
48
49
  });
49
50
  if (isSynced(result.status))
50
51
  syncedCount++;
@@ -53,18 +54,21 @@ export async function syncWorkspace(workspacePath) {
53
54
  else
54
55
  errorCount++;
55
56
  }
56
- // Regenerate maps/context when anything synced. A regen failure must never
57
+ // Regenerate maps/context only when a rebase actually changed repo content
58
+ // ('rebased' or 'stash-conflict'). When every repo was already up to date,
59
+ // skipping regeneration keeps context files byte-identical — no wasted
60
+ // analysis and no invalidated AI prompt caches. The cached analyzer then
61
+ // limits the work to the repos that changed. A regen failure must never
57
62
  // fail the sync itself — the rebases already happened.
58
- if (syncedCount > 0) {
63
+ const contentChanged = repoReports.some((r) => r.status === 'rebased' || r.status === 'stash-conflict');
64
+ let contextRefreshed = false;
65
+ if (contentChanged) {
59
66
  try {
60
- const allRepos = feature.repos.map((r) => ({
61
- name: path.basename(r),
62
- path: r,
63
- defaultBranch: 'main',
64
- }));
65
- const analysis = await analyzeAllRepos(allRepos);
67
+ const allRepos = await resolveRepoInfos(feature.repos);
68
+ const { analysis, analyzed } = await analyzeAllReposCached(allRepos, workspacePath);
66
69
  const ctx = { feature, repos: allRepos, analysis };
67
- await generateContextFiles(ctx, feature.assistants, workspacePath);
70
+ await generateContextFiles(ctx, feature.assistants, workspacePath, undefined, undefined, analyzed);
71
+ contextRefreshed = true;
68
72
  }
69
73
  catch {
70
74
  // Best-effort regeneration; ignore failures.
@@ -77,6 +81,7 @@ export async function syncWorkspace(workspacePath) {
77
81
  syncedCount,
78
82
  conflictCount,
79
83
  errorCount,
84
+ contextRefreshed,
80
85
  };
81
86
  }
82
87
  //# sourceMappingURL=sync.js.map