@nemus-cli/nemus 0.2.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 (320) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/assets/README.md +53 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/banner.svg +66 -0
  7. package/assets/favicon-32.png +0 -0
  8. package/assets/favicon-64.png +0 -0
  9. package/assets/icon-256.png +0 -0
  10. package/assets/icon.svg +23 -0
  11. package/assets/logo-256.png +0 -0
  12. package/assets/logo-512.png +0 -0
  13. package/assets/logo-wordmark.svg +35 -0
  14. package/assets/logo.svg +32 -0
  15. package/bin/workspace.js +130 -0
  16. package/dist/cli/ai-prompt.js +421 -0
  17. package/dist/cli/dashboard/AgentList.js +61 -0
  18. package/dist/cli/dashboard/DashboardSidebar.js +280 -0
  19. package/dist/cli/dashboard/HelpFooter.js +73 -0
  20. package/dist/cli/dashboard/StatusBadge.js +50 -0
  21. package/dist/cli/dashboard/index.js +11 -0
  22. package/dist/cli/dashboard/types.js +2 -0
  23. package/dist/cli/tui.js +48 -0
  24. package/dist/commands/ai-prompt.js +22 -0
  25. package/dist/commands/analyze-deps.js +138 -0
  26. package/dist/commands/archive.js +113 -0
  27. package/dist/commands/branch/create.js +119 -0
  28. package/dist/commands/branch/index.js +104 -0
  29. package/dist/commands/branch/merge.js +90 -0
  30. package/dist/commands/branch/rebase.js +84 -0
  31. package/dist/commands/branch/switch.js +165 -0
  32. package/dist/commands/cache/index.js +80 -0
  33. package/dist/commands/cache/manager.js +198 -0
  34. package/dist/commands/cleanup.js +153 -0
  35. package/dist/commands/configure-claude.js +55 -0
  36. package/dist/commands/configure.js +231 -0
  37. package/dist/commands/create.js +204 -0
  38. package/dist/commands/dashboard/hook-handler.js +162 -0
  39. package/dist/commands/dashboard/index.js +258 -0
  40. package/dist/commands/dashboard/launcher.js +40 -0
  41. package/dist/commands/dashboard/session-picker.js +106 -0
  42. package/dist/commands/dashboard/sidebar.js +39 -0
  43. package/dist/commands/dashboard/workspace-picker.js +95 -0
  44. package/dist/commands/delete.js +194 -0
  45. package/dist/commands/deprecated-aliases.js +44 -0
  46. package/dist/commands/diff.js +129 -0
  47. package/dist/commands/doctor.js +141 -0
  48. package/dist/commands/generate-docs.js +77 -0
  49. package/dist/commands/ghq-status.js +72 -0
  50. package/dist/commands/go.js +154 -0
  51. package/dist/commands/history.js +124 -0
  52. package/dist/commands/list.js +191 -0
  53. package/dist/commands/mcp/index.js +69 -0
  54. package/dist/commands/migrate.js +296 -0
  55. package/dist/commands/remove-repo.js +217 -0
  56. package/dist/commands/report-bug.js +112 -0
  57. package/dist/commands/run.js +147 -0
  58. package/dist/commands/save-context.js +171 -0
  59. package/dist/commands/sessions.js +129 -0
  60. package/dist/commands/status.js +138 -0
  61. package/dist/commands/suite/create.js +212 -0
  62. package/dist/commands/suite/delete.js +64 -0
  63. package/dist/commands/suite/export.js +126 -0
  64. package/dist/commands/suite/import.js +128 -0
  65. package/dist/commands/suite/index.js +94 -0
  66. package/dist/commands/suite/list.js +49 -0
  67. package/dist/commands/suite/use.js +227 -0
  68. package/dist/commands/sync.js +117 -0
  69. package/dist/commands/update.js +203 -0
  70. package/dist/mcp/install.js +550 -0
  71. package/dist/mcp/server.js +398 -0
  72. package/dist/mcp/tools.js +922 -0
  73. package/dist/pi-extensions/permission-sync.ts.template +41 -0
  74. package/dist/pi-extensions/permissions-gate.ts.template +69 -0
  75. package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
  76. package/dist/pi-extensions/review-reminder.ts.template +58 -0
  77. package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
  78. package/dist/program.js +136 -0
  79. package/dist/scripts/workspace-table.py +394 -0
  80. package/dist/types/dashboard.js +11 -0
  81. package/dist/types/index.js +2 -0
  82. package/dist/utils/agent-config.js +263 -0
  83. package/dist/utils/agent-state.js +178 -0
  84. package/dist/utils/banner.js +24 -0
  85. package/dist/utils/branch-operations.js +232 -0
  86. package/dist/utils/bug-report.js +355 -0
  87. package/dist/utils/cache.js +168 -0
  88. package/dist/utils/claude-integration.js +617 -0
  89. package/dist/utils/claude-sessions.js +260 -0
  90. package/dist/utils/cleanup-operations.js +120 -0
  91. package/dist/utils/colors.js +26 -0
  92. package/dist/utils/command-helpers.js +44 -0
  93. package/dist/utils/config.js +150 -0
  94. package/dist/utils/context-file.js +33 -0
  95. package/dist/utils/dashboard-hooks.js +172 -0
  96. package/dist/utils/dependency-analyzer.js +234 -0
  97. package/dist/utils/diff-operations.js +80 -0
  98. package/dist/utils/doc-generator.js +184 -0
  99. package/dist/utils/ghq-integration.js +332 -0
  100. package/dist/utils/git-operations.js +237 -0
  101. package/dist/utils/git-status.js +187 -0
  102. package/dist/utils/github.js +68 -0
  103. package/dist/utils/health-checks.js +345 -0
  104. package/dist/utils/history.js +150 -0
  105. package/dist/utils/hooks.js +100 -0
  106. package/dist/utils/logger.js +40 -0
  107. package/dist/utils/permission-sync.js +681 -0
  108. package/dist/utils/pi-extensions.js +224 -0
  109. package/dist/utils/progress.js +92 -0
  110. package/dist/utils/prompts.js +279 -0
  111. package/dist/utils/repo-resolver.js +260 -0
  112. package/dist/utils/retry.js +65 -0
  113. package/dist/utils/run-operations.js +30 -0
  114. package/dist/utils/suite.js +159 -0
  115. package/dist/utils/sync-operations.js +107 -0
  116. package/dist/utils/tmux-dashboard.js +184 -0
  117. package/dist/utils/validation.js +153 -0
  118. package/dist/utils/version-check.js +121 -0
  119. package/dist/utils/workspace-meta.js +185 -0
  120. package/install-shell-integration.sh +532 -0
  121. package/install.sh +401 -0
  122. package/package.json +90 -0
  123. package/remove-shell-block.awk +44 -0
  124. package/scripts/postinstall.js +85 -0
  125. package/skills/analyze-deps.md +23 -0
  126. package/skills/archive-workspace.md +26 -0
  127. package/skills/branch-create.md +18 -0
  128. package/skills/create-workspace.md +39 -0
  129. package/skills/delete-workspace.md +37 -0
  130. package/skills/list-org-repos.md +26 -0
  131. package/skills/list-suites.md +24 -0
  132. package/skills/list-workspaces.md +27 -0
  133. package/skills/refresh-workspace-docs.md +89 -0
  134. package/skills/remove-repo.md +25 -0
  135. package/skills/run-command.md +23 -0
  136. package/skills/save-context.md +68 -0
  137. package/skills/search-repos.md +31 -0
  138. package/skills/snapshot-list.md +18 -0
  139. package/skills/snapshot-save.md +12 -0
  140. package/skills/switch-branch.md +18 -0
  141. package/skills/update-cache.md +21 -0
  142. package/skills/update-workspace.md +66 -0
  143. package/skills/workspace-cleanup.md +18 -0
  144. package/skills/workspace-diff.md +23 -0
  145. package/skills/workspace-doctor.md +23 -0
  146. package/skills/workspace-info.md +45 -0
  147. package/skills/workspace-manager/SKILL.md +201 -0
  148. package/skills/workspace-manager/references/ai-prompt.md +44 -0
  149. package/skills/workspace-manager/references/analyze-deps.md +45 -0
  150. package/skills/workspace-manager/references/archive-workspace.md +38 -0
  151. package/skills/workspace-manager/references/branch-create.md +31 -0
  152. package/skills/workspace-manager/references/branch-merge.md +22 -0
  153. package/skills/workspace-manager/references/branch-rebase.md +22 -0
  154. package/skills/workspace-manager/references/cache-clear.md +18 -0
  155. package/skills/workspace-manager/references/cache-info.md +22 -0
  156. package/skills/workspace-manager/references/configure-claude.md +31 -0
  157. package/skills/workspace-manager/references/configure.md +35 -0
  158. package/skills/workspace-manager/references/create-workspace.md +45 -0
  159. package/skills/workspace-manager/references/dashboard.md +61 -0
  160. package/skills/workspace-manager/references/delete-workspace.md +33 -0
  161. package/skills/workspace-manager/references/generate-docs.md +36 -0
  162. package/skills/workspace-manager/references/ghq-status.md +28 -0
  163. package/skills/workspace-manager/references/go.md +33 -0
  164. package/skills/workspace-manager/references/history.md +41 -0
  165. package/skills/workspace-manager/references/list-org-repos.md +26 -0
  166. package/skills/workspace-manager/references/list-suites.md +21 -0
  167. package/skills/workspace-manager/references/list-workspaces.md +27 -0
  168. package/skills/workspace-manager/references/mcp.md +49 -0
  169. package/skills/workspace-manager/references/remove-repo.md +26 -0
  170. package/skills/workspace-manager/references/run-command.md +44 -0
  171. package/skills/workspace-manager/references/search-repos.md +24 -0
  172. package/skills/workspace-manager/references/sessions.md +27 -0
  173. package/skills/workspace-manager/references/suite-create.md +22 -0
  174. package/skills/workspace-manager/references/suite-delete.md +21 -0
  175. package/skills/workspace-manager/references/suite-export.md +19 -0
  176. package/skills/workspace-manager/references/suite-import.md +19 -0
  177. package/skills/workspace-manager/references/suite-use.md +27 -0
  178. package/skills/workspace-manager/references/switch-branch.md +25 -0
  179. package/skills/workspace-manager/references/update-cache.md +22 -0
  180. package/skills/workspace-manager/references/update-workspace.md +50 -0
  181. package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
  182. package/skills/workspace-manager/references/workspace-diff.md +27 -0
  183. package/skills/workspace-manager/references/workspace-doctor.md +27 -0
  184. package/skills/workspace-manager/references/workspace-info.md +33 -0
  185. package/skills/workspace-manager/references/workspace-status.md +43 -0
  186. package/skills/workspace-manager/references/workspace-sync.md +26 -0
  187. package/skills/workspace-status.md +26 -0
  188. package/skills/workspace-sync.md +23 -0
  189. package/src/cli/ai-prompt.test.ts +667 -0
  190. package/src/cli/ai-prompt.ts +421 -0
  191. package/src/cli/dashboard/AgentList.tsx +44 -0
  192. package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
  193. package/src/cli/dashboard/HelpFooter.tsx +22 -0
  194. package/src/cli/dashboard/StatusBadge.tsx +23 -0
  195. package/src/cli/dashboard/index.ts +5 -0
  196. package/src/cli/dashboard/types.ts +16 -0
  197. package/src/cli/tui.tsx +51 -0
  198. package/src/cli/workspace-cli.test.ts +30 -0
  199. package/src/commands/ai-prompt.ts +21 -0
  200. package/src/commands/analyze-deps.ts +119 -0
  201. package/src/commands/archive.ts +114 -0
  202. package/src/commands/branch/create.ts +94 -0
  203. package/src/commands/branch/index.ts +74 -0
  204. package/src/commands/branch/merge.ts +61 -0
  205. package/src/commands/branch/rebase.ts +54 -0
  206. package/src/commands/branch/switch.ts +150 -0
  207. package/src/commands/cache/index.ts +51 -0
  208. package/src/commands/cache/manager.ts +168 -0
  209. package/src/commands/cleanup.ts +132 -0
  210. package/src/commands/configure-claude.ts +55 -0
  211. package/src/commands/configure.ts +204 -0
  212. package/src/commands/create.ts +196 -0
  213. package/src/commands/dashboard/hook-handler.ts +138 -0
  214. package/src/commands/dashboard/index.ts +248 -0
  215. package/src/commands/dashboard/launcher.ts +42 -0
  216. package/src/commands/dashboard/session-picker.ts +73 -0
  217. package/src/commands/dashboard/sidebar.js +39 -0
  218. package/src/commands/dashboard/workspace-picker.ts +62 -0
  219. package/src/commands/delete.test.ts +250 -0
  220. package/src/commands/delete.ts +171 -0
  221. package/src/commands/deprecated-aliases.ts +55 -0
  222. package/src/commands/diff.ts +106 -0
  223. package/src/commands/doctor.ts +110 -0
  224. package/src/commands/generate-docs.ts +45 -0
  225. package/src/commands/ghq-status.ts +70 -0
  226. package/src/commands/go.ts +125 -0
  227. package/src/commands/history.ts +136 -0
  228. package/src/commands/list.test.ts +219 -0
  229. package/src/commands/list.ts +176 -0
  230. package/src/commands/mcp/index.ts +39 -0
  231. package/src/commands/migrate.ts +277 -0
  232. package/src/commands/remove-repo.ts +194 -0
  233. package/src/commands/report-bug.ts +88 -0
  234. package/src/commands/run.ts +125 -0
  235. package/src/commands/save-context.test.ts +113 -0
  236. package/src/commands/save-context.ts +141 -0
  237. package/src/commands/sessions.ts +99 -0
  238. package/src/commands/status.ts +111 -0
  239. package/src/commands/suite/create.ts +240 -0
  240. package/src/commands/suite/delete.ts +65 -0
  241. package/src/commands/suite/export.ts +97 -0
  242. package/src/commands/suite/import.ts +100 -0
  243. package/src/commands/suite/index.ts +66 -0
  244. package/src/commands/suite/list.ts +54 -0
  245. package/src/commands/suite/use.ts +232 -0
  246. package/src/commands/sync.ts +97 -0
  247. package/src/commands/update.ts +197 -0
  248. package/src/mcp/install.ts +529 -0
  249. package/src/mcp/server.ts +579 -0
  250. package/src/mcp/tools.test.ts +545 -0
  251. package/src/mcp/tools.ts +1019 -0
  252. package/src/mcp/update-workspace.test.ts +220 -0
  253. package/src/pi-extensions/permission-sync.ts.template +41 -0
  254. package/src/pi-extensions/permissions-gate.ts.template +69 -0
  255. package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
  256. package/src/pi-extensions/review-reminder.ts.template +58 -0
  257. package/src/pi-extensions/workspace-input-status.ts.template +919 -0
  258. package/src/program.ts +112 -0
  259. package/src/scripts/workspace-table.py +394 -0
  260. package/src/types/dashboard.ts +29 -0
  261. package/src/types/index.ts +109 -0
  262. package/src/types/ink.d.ts +171 -0
  263. package/src/utils/agent-config.test.ts +303 -0
  264. package/src/utils/agent-config.ts +264 -0
  265. package/src/utils/agent-state.test.ts +166 -0
  266. package/src/utils/agent-state.ts +135 -0
  267. package/src/utils/banner.ts +24 -0
  268. package/src/utils/branch-operations.ts +237 -0
  269. package/src/utils/bug-report.test.ts +174 -0
  270. package/src/utils/bug-report.ts +355 -0
  271. package/src/utils/cache.ts +150 -0
  272. package/src/utils/claude-integration.test.ts +381 -0
  273. package/src/utils/claude-integration.ts +649 -0
  274. package/src/utils/claude-sessions.test.ts +262 -0
  275. package/src/utils/claude-sessions.ts +251 -0
  276. package/src/utils/cleanup-operations.ts +94 -0
  277. package/src/utils/colors.ts +25 -0
  278. package/src/utils/command-helpers.ts +46 -0
  279. package/src/utils/config.test.ts +203 -0
  280. package/src/utils/config.ts +133 -0
  281. package/src/utils/context-file.ts +36 -0
  282. package/src/utils/dashboard-hooks.test.ts +168 -0
  283. package/src/utils/dashboard-hooks.ts +163 -0
  284. package/src/utils/dependency-analyzer.ts +243 -0
  285. package/src/utils/diff-operations.ts +59 -0
  286. package/src/utils/doc-generator.ts +187 -0
  287. package/src/utils/ghq-integration.test.ts +195 -0
  288. package/src/utils/ghq-integration.ts +317 -0
  289. package/src/utils/git-operations.test.ts +265 -0
  290. package/src/utils/git-operations.ts +233 -0
  291. package/src/utils/git-status.ts +171 -0
  292. package/src/utils/github.ts +74 -0
  293. package/src/utils/health-checks.ts +359 -0
  294. package/src/utils/history.ts +140 -0
  295. package/src/utils/hooks.ts +92 -0
  296. package/src/utils/logger.ts +37 -0
  297. package/src/utils/permission-sync.test.ts +593 -0
  298. package/src/utils/permission-sync.ts +720 -0
  299. package/src/utils/pi-extensions.test.ts +182 -0
  300. package/src/utils/pi-extensions.ts +226 -0
  301. package/src/utils/progress.ts +73 -0
  302. package/src/utils/prompts.test.ts +169 -0
  303. package/src/utils/prompts.ts +296 -0
  304. package/src/utils/repo-resolver.test.ts +313 -0
  305. package/src/utils/repo-resolver.ts +294 -0
  306. package/src/utils/retry.test.ts +124 -0
  307. package/src/utils/retry.ts +85 -0
  308. package/src/utils/run-operations.ts +38 -0
  309. package/src/utils/suite.test.ts +256 -0
  310. package/src/utils/suite.ts +144 -0
  311. package/src/utils/sync-operations.ts +87 -0
  312. package/src/utils/tmux-dashboard.ts +193 -0
  313. package/src/utils/validation.test.ts +139 -0
  314. package/src/utils/validation.ts +128 -0
  315. package/src/utils/version-check.test.ts +108 -0
  316. package/src/utils/version-check.ts +96 -0
  317. package/src/utils/workspace-meta.ts +173 -0
  318. package/sync-permissions.sh +137 -0
  319. package/tsconfig.json +20 -0
  320. package/uninstall.sh +162 -0
@@ -0,0 +1,277 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs/promises';
4
+ import { execSync } from 'child_process';
5
+ import { WORKSPACES_DIR, getUserConfig } from '../utils/config';
6
+ import { listWorkspaces, loadMetadata, saveMetadata } from '../utils/workspace-meta';
7
+ import { generateClaudeContext, buildPerRepoContextSection } from '../utils/claude-integration';
8
+ import { logInfo, logSuccess, logError, logWarning } from '../utils/logger';
9
+ import { colorize } from '../utils/colors';
10
+ import { WorkspaceMetadata, RepositoryMetadata } from '../types';
11
+ import { getContextFileNames } from '../utils/agent-config';
12
+
13
+ export function registerMigrateCommand(parent: Command) {
14
+ parent
15
+ .command('migrate')
16
+ .description('Migrate all workspaces to latest format (regenerate context files, update metadata)')
17
+ .option('--dry-run', 'Show what would be changed without making changes')
18
+ .action(async (opts) => {
19
+ await handleMigrate(opts);
20
+ });
21
+ }
22
+
23
+ async function handleMigrate(opts: { dryRun?: boolean }) {
24
+ const dryRun = opts.dryRun ?? false;
25
+
26
+ if (dryRun) {
27
+ logInfo('Dry run mode - no changes will be made\n');
28
+ }
29
+
30
+ const workspaces = await listWorkspaces(false);
31
+ if (workspaces.length === 0) {
32
+ logInfo('No workspaces found. Nothing to migrate.');
33
+ return;
34
+ }
35
+
36
+ logInfo(`Found ${colorize(String(workspaces.length), 'cyan')} workspaces to migrate\n`);
37
+
38
+ let migrated = 0;
39
+ let skipped = 0;
40
+ let errors = 0;
41
+
42
+ for (const ws of workspaces) {
43
+ const wsPath = ws.path;
44
+ const metadata = await loadMetadata(wsPath);
45
+
46
+ if (!metadata) {
47
+ // No metadata file - reconstruct it by scanning git subdirectories
48
+ logWarning(`${ws.name}: no metadata found - reconstructing from git repos`);
49
+ const reconstructed = await reconstructMetadata(ws.name, wsPath);
50
+ if (!reconstructed) {
51
+ logError(` ${ws.name}: could not reconstruct metadata (no git repos found), skipping`);
52
+ skipped++;
53
+ continue;
54
+ }
55
+ if (!dryRun) {
56
+ await saveMetadata(wsPath, reconstructed);
57
+ logSuccess(` ${colorize(ws.name, 'cyan')}: metadata created (${reconstructed.repositories.length} repos detected)`);
58
+ } else {
59
+ logInfo(` ${colorize(ws.name, 'cyan')}: would create metadata (${reconstructed.repositories.length} repos detected)`);
60
+ }
61
+ // Regenerate context files using the reconstructed metadata.
62
+ const meta = reconstructed;
63
+ if (!dryRun) {
64
+ const repos = meta.repositories
65
+ .filter(r => r.status === 'success')
66
+ .map(r => ({ name: r.name, description: '', url: r.cloneUrl, owner: { login: r.owner }, sshUrl: r.cloneUrl }));
67
+ try {
68
+ await generateClaudeContext(wsPath, ws.name, repos as any, meta);
69
+ } catch { /* context gen is best-effort */ }
70
+ }
71
+ migrated++;
72
+ continue;
73
+ }
74
+
75
+ try {
76
+ const dirNames = metadata.repositories
77
+ .filter(r => r.status === 'success')
78
+ .map(r => r.directoryName);
79
+
80
+ const contextFiles = getContextFileNames();
81
+
82
+ // Read all existing context files first — before any writes.
83
+ // If any file is missing we must fall back to full regeneration;
84
+ // we determine this BEFORE touching anything so we never mix
85
+ // surgical patches with a subsequent full overwrite.
86
+ const existingContents = new Map<string, string>();
87
+ let needsFullRegen = false;
88
+ for (const fileName of contextFiles) {
89
+ try {
90
+ existingContents.set(fileName, await fs.readFile(path.join(wsPath, fileName), 'utf-8'));
91
+ } catch {
92
+ needsFullRegen = true;
93
+ break;
94
+ }
95
+ }
96
+
97
+ // Build the per-repo section (needs disk access; do once for both paths).
98
+ const perRepoSection = await buildPerRepoContextSection(wsPath, dirNames);
99
+ const repoCount = perRepoSection ? (perRepoSection.match(/^###/gm) || []).length : 0;
100
+
101
+ if (dryRun) {
102
+ if (needsFullRegen) {
103
+ logInfo(` ${colorize(ws.name, 'cyan')}: would regenerate context files from scratch (context file missing)`);
104
+ } else if (repoCount > 0) {
105
+ logInfo(` ${colorize(ws.name, 'cyan')}: would embed per-repo context from ${repoCount} location(s) into ${contextFiles.join(', ')}`);
106
+ } else {
107
+ logInfo(` ${colorize(ws.name, 'cyan')}: would update ${contextFiles.join(', ')} (no per-repo context files found — any existing section removed)`);
108
+ }
109
+ migrated++;
110
+ continue;
111
+ }
112
+
113
+ // Ensure lastModified is set
114
+ if (!metadata.lastModified) {
115
+ metadata.lastModified = new Date().toISOString();
116
+ await saveMetadata(wsPath, metadata);
117
+ }
118
+
119
+ if (needsFullRegen) {
120
+ // One or more context files are missing — regenerate all from scratch.
121
+ const repos = metadata.repositories
122
+ .filter(r => r.status === 'success')
123
+ .map(r => ({
124
+ name: r.name, description: '', url: r.cloneUrl,
125
+ owner: { login: r.owner }, sshUrl: r.cloneUrl,
126
+ }));
127
+ await generateClaudeContext(wsPath, ws.name, repos as any, metadata);
128
+ } else {
129
+ // All context files exist — patch only the per-repo section in each.
130
+ for (const [fileName, existing] of existingContents) {
131
+ const updated = upsertPerRepoSection(existing, perRepoSection);
132
+ if (updated !== existing) {
133
+ await fs.writeFile(path.join(wsPath, fileName), updated, 'utf-8');
134
+ }
135
+ }
136
+ }
137
+
138
+ const detail = needsFullRegen
139
+ ? ' (regenerated from scratch)'
140
+ : repoCount > 0
141
+ ? ` (embedded per-repo context from ${repoCount} location(s))`
142
+ : ' (no per-repo context files found)';
143
+ logSuccess(` ${colorize(ws.name, 'cyan')}: migrated${detail}`);
144
+ migrated++;
145
+ } catch (err) {
146
+ const msg = err instanceof Error ? err.message : String(err);
147
+ logError(` ${ws.name}: ${msg}`);
148
+ errors++;
149
+ }
150
+ }
151
+
152
+ console.log('');
153
+ logInfo(dryRun ? 'Dry run complete:' : 'Migration complete:');
154
+ console.log(` ${colorize(String(migrated), 'green')} ${dryRun ? 'would migrate' : 'migrated'}`);
155
+ if (skipped > 0) console.log(` ${colorize(String(skipped), 'yellow')} skipped`);
156
+ if (errors > 0) console.log(` ${colorize(String(errors), 'red')} errors`);
157
+ }
158
+
159
+ /**
160
+ * Reconstruct a WorkspaceMetadata object for a workspace that has no metadata
161
+ * file by scanning subdirectories for git repositories and reading their
162
+ * remote origin URLs.
163
+ *
164
+ * Returns null if no git repos are found in the workspace directory.
165
+ */
166
+ async function reconstructMetadata(
167
+ workspaceName: string,
168
+ workspacePath: string,
169
+ ): Promise<WorkspaceMetadata | null> {
170
+ let entries: string[];
171
+ try {
172
+ entries = await fs.readdir(workspacePath);
173
+ } catch {
174
+ return null;
175
+ }
176
+
177
+ const repositories: RepositoryMetadata[] = [];
178
+
179
+ for (const entry of entries) {
180
+ const repoPath = path.join(workspacePath, entry);
181
+ try {
182
+ const stat = await fs.stat(repoPath);
183
+ if (!stat.isDirectory()) continue;
184
+
185
+ // Check it's a git repo
186
+ await fs.access(path.join(repoPath, '.git'));
187
+
188
+ // Get the remote origin URL
189
+ let cloneUrl = '';
190
+ let owner = getUserConfig().githubOrg || 'your-org';
191
+ try {
192
+ cloneUrl = execSync('git remote get-url origin', {
193
+ cwd: repoPath, stdio: 'pipe', encoding: 'utf-8',
194
+ }).trim();
195
+ // Extract owner from SSH URL: git@github.com:owner/repo.git
196
+ const sshMatch = cloneUrl.match(/github\.com[:/]([^/]+)\//);
197
+ if (sshMatch) owner = sshMatch[1];
198
+ } catch { /* no remote - leave blank */ }
199
+
200
+ // Derive the canonical repo name from the remote URL, falling back
201
+ // to the directory name.
202
+ let repoName = entry;
203
+ try {
204
+ const urlMatch = cloneUrl.match(/\/([^/]+?)\.git$/) ||
205
+ cloneUrl.match(/\/([^/]+)$/);
206
+ if (urlMatch) repoName = urlMatch[1];
207
+ } catch { /* use directory name */ }
208
+
209
+ repositories.push({
210
+ name: repoName,
211
+ directoryName: entry,
212
+ owner,
213
+ clonedAt: new Date().toISOString(),
214
+ cloneUrl,
215
+ status: 'success',
216
+ });
217
+ } catch { /* not accessible or not a git repo - skip */ }
218
+ }
219
+
220
+ if (repositories.length === 0) return null;
221
+
222
+ return {
223
+ workspaceName,
224
+ createdAt: new Date().toISOString(),
225
+ lastModified: new Date().toISOString(),
226
+ repositories,
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Insert or replace the '## Per-Repository Context' section in an existing
232
+ * workspace context file, leaving everything else (user notes, etc.) intact.
233
+ *
234
+ * Insertion order (preferred first — we want the section as early as
235
+ * possible so the LLM weights it heavily):
236
+ * 1. Before '## Common Workflows' — the new canonical position
237
+ * 2. Before '## Workspace Management' (older layouts)
238
+ * 3. Before '## AI Agent Rules' (older layouts)
239
+ * 4. Before '## Tips for Working' (older layouts)
240
+ * 5. Before '## Notes' (legacy fallback)
241
+ * 6. Before '## Saved Context' (legacy fallback)
242
+ * 7. Appended at the end (last resort)
243
+ *
244
+ * If `newSection` is null the existing section (if any) is removed so that
245
+ * a workspace that previously had per-repo context but no longer does gets
246
+ * cleaned up.
247
+ */
248
+ function upsertPerRepoSection(content: string, newSection: string | null): string {
249
+ // Match the section *including* its leading blank line(s) so removal
250
+ // leaves a clean join with no leftover blank lines. No 'm' flag so '$'
251
+ // only matches the true end-of-string.
252
+ const sectionRegex = /\n{1,2}## Per-Repository Context\b[\s\S]*?(?=\n## |\n#{1,2} |$)/;
253
+ // Only trimEnd() the very end of the file — never touch the body.
254
+ const base = content.replace(sectionRegex, '').trimEnd();
255
+
256
+ if (!newSection) return base + '\n'; // section removed, nothing to add back
257
+
258
+ const block = `\n\n${newSection.trim()}\n`;
259
+
260
+ // Try anchors in order of preference — earliest first so per-repo rules
261
+ // are read by the LLM before generic boilerplate.
262
+ const anchors = [
263
+ '\n## Common Workflows',
264
+ '\n## Workspace Management',
265
+ '\n## AI Agent Rules',
266
+ '\n## Tips for Working',
267
+ '\n## Notes',
268
+ '\n## Saved Context',
269
+ ];
270
+ for (const anchor of anchors) {
271
+ if (base.includes(anchor)) {
272
+ return base.replace(anchor, `${block}${anchor}`);
273
+ }
274
+ }
275
+ // Append (last resort)
276
+ return base + block;
277
+ }
@@ -0,0 +1,194 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import { WORKSPACES_DIR } from '../utils/config';
5
+ import { loadMetadata, saveMetadata } from '../utils/workspace-meta';
6
+ import { generateClaudeContext } from '../utils/claude-integration';
7
+ import { logInfo, logSuccess, logError, logStep } from '../utils/logger';
8
+ import { colorize } from '../utils/colors';
9
+ import inquirer from 'inquirer';
10
+ import autocompletePrompt from 'inquirer-autocomplete-prompt';
11
+ import * as fuzzy from 'fuzzy';
12
+ import { getGlobalOpts, resolveWorkspace, parseList } from '../utils/command-helpers';
13
+
14
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
15
+
16
+ export function registerRemoveRepoCommand(parent: Command) {
17
+ parent
18
+ .command('remove-repo')
19
+ .alias('rr')
20
+ .description('Remove a repo from a workspace')
21
+ .option('-w, --workspace <name>', 'Workspace name')
22
+ .option('-r, --repos <repos>', 'Comma-separated repository directory names')
23
+ .action(async (opts, cmd) => {
24
+ const globalOpts = getGlobalOpts(cmd);
25
+ await handleRemoveRepo({ ...opts, ...globalOpts });
26
+ });
27
+ }
28
+
29
+ async function handleRemoveRepo(opts: {
30
+ workspace?: string;
31
+ repos?: string;
32
+ yes: boolean;
33
+ forceRefresh: boolean;
34
+ }) {
35
+ console.log('\n' + '='.repeat(60));
36
+ console.log(colorize('Remove Repository Instances', 'bright'));
37
+ console.log('='.repeat(60) + '\n');
38
+
39
+ try {
40
+ logStep(1, 4, 'Select workspace...');
41
+ const workspaceName = await resolveWorkspace(opts.workspace);
42
+ const workspacePath = path.join(WORKSPACES_DIR, workspaceName);
43
+ logInfo(`Selected workspace: ${colorize(workspaceName, 'cyan')}`);
44
+
45
+ logStep(2, 4, 'Loading repository instances...');
46
+ const metadata = await loadMetadata(workspacePath);
47
+ const successRepos = metadata?.repositories.filter(r => r.status === 'success') ?? [];
48
+
49
+ const entries = await fs.readdir(workspacePath, { withFileTypes: true });
50
+ const diskDirs = entries
51
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
52
+ .map(e => e.name);
53
+
54
+ const metaDirNames = new Set(successRepos.map(r => r.directoryName));
55
+ const repoEntries: Array<{ displayName: string; dirName: string }> = [];
56
+
57
+ for (const repo of successRepos) {
58
+ repoEntries.push({
59
+ displayName: repo.directoryName !== repo.name
60
+ ? `${repo.directoryName} (${repo.name})`
61
+ : repo.directoryName,
62
+ dirName: repo.directoryName,
63
+ });
64
+ }
65
+
66
+ for (const dir of diskDirs) {
67
+ if (!metaDirNames.has(dir)) {
68
+ repoEntries.push({ displayName: `${dir} (untracked)`, dirName: dir });
69
+ }
70
+ }
71
+
72
+ if (repoEntries.length === 0) {
73
+ logInfo('No repositories found in this workspace.');
74
+ if (opts.yes) {
75
+ await fs.rm(workspacePath, { recursive: true, force: true });
76
+ logSuccess(`Workspace "${workspaceName}" deleted.`);
77
+ } else {
78
+ const { deleteWorkspace } = await inquirer.prompt([{
79
+ type: 'confirm', name: 'deleteWorkspace',
80
+ message: `Delete the entire workspace folder "${workspaceName}"?`, default: false,
81
+ }]);
82
+ if (deleteWorkspace) {
83
+ await fs.rm(workspacePath, { recursive: true, force: true });
84
+ logSuccess(`Workspace "${workspaceName}" deleted.`);
85
+ } else {
86
+ logInfo('No action taken.');
87
+ }
88
+ }
89
+ process.exit(0);
90
+ }
91
+
92
+ logStep(3, 4, 'Select instances to remove...');
93
+
94
+ let selectedDirs: string[];
95
+
96
+ if (opts.repos) {
97
+ selectedDirs = parseList(opts.repos);
98
+ const unknownDirs = selectedDirs.filter(d => !repoEntries.some(e => e.dirName === d));
99
+ if (unknownDirs.length > 0) {
100
+ logError(`Directories not found in workspace: ${unknownDirs.join(', ')}`);
101
+ process.exit(1);
102
+ }
103
+ } else {
104
+ console.log('\n' + colorize('Repository Selection', 'bright'));
105
+ console.log('Type to search repositories. Use arrow keys to navigate, Enter to select.');
106
+ console.log('Type "done" when finished selecting.\n');
107
+
108
+ selectedDirs = [];
109
+
110
+ while (true) {
111
+ const available = repoEntries.filter(e => !selectedDirs.includes(e.dirName));
112
+ if (available.length === 0) {
113
+ console.log(colorize('All repositories selected.', 'yellow'));
114
+ break;
115
+ }
116
+
117
+ try {
118
+ const { repoDir } = await inquirer.prompt([{
119
+ type: 'autocomplete', name: 'repoDir',
120
+ message: `Search and select repository (${colorize(String(selectedDirs.length), 'cyan')} selected):`,
121
+ source: async (_answersSoFar: any, input: string | undefined) => {
122
+ const searchInput = input || '';
123
+ const doneOption = { name: colorize('done - Finish selection', 'green'), value: 'done' };
124
+ if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
125
+ return [doneOption, ...available.map(e => ({ name: e.displayName, value: e.dirName }))];
126
+ }
127
+ const results = fuzzy.filter(searchInput, available, { extract: (e) => e.displayName });
128
+ return [doneOption, ...results.map(result => ({ name: result.original.displayName, value: result.original.dirName }))];
129
+ },
130
+ pageSize: 16,
131
+ } as any]);
132
+
133
+ if (repoDir === 'done') {
134
+ if (selectedDirs.length === 0) { console.log(colorize('\nYou must select at least one repository\n', 'yellow')); continue; }
135
+ break;
136
+ }
137
+
138
+ selectedDirs.push(repoDir);
139
+ const entry = repoEntries.find(e => e.dirName === repoDir);
140
+ console.log(colorize(`Added: ${entry?.displayName || repoDir}`, 'green'));
141
+ } catch { console.log('\n'); process.exit(0); }
142
+ }
143
+ }
144
+
145
+ console.log(`\n${colorize(`Selected ${selectedDirs.length} repository instance${selectedDirs.length === 1 ? '' : 's'}:`, 'cyan')}`);
146
+ selectedDirs.forEach(dir => console.log(` - ${dir}`));
147
+ console.log('');
148
+
149
+ if (!opts.yes) {
150
+ const { confirm } = await inquirer.prompt([{
151
+ type: 'confirm', name: 'confirm',
152
+ message: `Remove ${selectedDirs.length} instance(s)? This will delete the directories.`, default: false,
153
+ }]);
154
+ if (!confirm) { logInfo('Removal cancelled'); process.exit(0); }
155
+ }
156
+
157
+ logStep(4, 4, 'Removing instances...');
158
+
159
+ for (const dirName of selectedDirs) {
160
+ const dirPath = path.join(workspacePath, dirName);
161
+ try {
162
+ await fs.rm(dirPath, { recursive: true, force: true });
163
+ logSuccess(`Removed: ${dirName}`);
164
+ } catch (error) {
165
+ logError(`Failed to remove ${dirName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
166
+ }
167
+ }
168
+
169
+ const removedSet = new Set(selectedDirs);
170
+ if (metadata) {
171
+ metadata.repositories = metadata.repositories.filter(r => !removedSet.has(r.directoryName));
172
+ await saveMetadata(workspacePath, metadata);
173
+
174
+ const remainingRepos = metadata.repositories
175
+ .filter(r => r.status === 'success')
176
+ .map(r => ({
177
+ name: r.name, url: '', sshUrl: r.cloneUrl,
178
+ owner: { login: r.owner }, description: '', isPrivate: true,
179
+ }));
180
+
181
+ if (remainingRepos.length > 0) {
182
+ await generateClaudeContext(workspacePath, workspaceName, remainingRepos, metadata);
183
+ }
184
+
185
+ console.log(`\nWorkspace now has ${colorize(String(metadata.repositories.length), 'cyan')} repositories\n`);
186
+ }
187
+ logSuccess('Repository instances removed successfully!');
188
+
189
+ } catch (error) {
190
+ logError('Failed to remove repository instances');
191
+ if (error instanceof Error) { logError(error.message); }
192
+ process.exit(1);
193
+ }
194
+ }
@@ -0,0 +1,88 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { logInfo, logSuccess, logError, logWarning } from '../utils/logger';
5
+ import { colorize } from '../utils/colors';
6
+ import {
7
+ reportBug,
8
+ readLastError,
9
+ type CapturedError,
10
+ BUG_REPORT_REPO,
11
+ } from '../utils/bug-report';
12
+
13
+ function getVersion(): string {
14
+ try {
15
+ const pkg = JSON.parse(
16
+ fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'),
17
+ );
18
+ return pkg.version ?? 'unknown';
19
+ } catch {
20
+ return 'unknown';
21
+ }
22
+ }
23
+
24
+ export function registerReportBugCommand(parent: Command) {
25
+ parent
26
+ .command('report-bug')
27
+ .description('File the last error (or a custom message) as a GitHub issue')
28
+ .option('-m, --message <message>', 'Report a custom message instead of the last captured error')
29
+ .option('-f, --force', 'Report even if the error looks environmental (network/auth)')
30
+ .action(async (opts: { message?: string; force?: boolean }) => {
31
+ await handleReportBug(opts);
32
+ });
33
+ }
34
+
35
+ async function handleReportBug(opts: { message?: string; force?: boolean }): Promise<void> {
36
+ let captured: CapturedError | null;
37
+
38
+ if (opts.message) {
39
+ captured = {
40
+ command: 'w report-bug (manual)',
41
+ message: opts.message,
42
+ timestamp: new Date().toISOString(),
43
+ version: getVersion(),
44
+ };
45
+ } else {
46
+ captured = readLastError();
47
+ if (!captured) {
48
+ logError('No recent error to report.');
49
+ logInfo('Run a command that fails first, or use: w report-bug -m "describe the bug"');
50
+ process.exit(1);
51
+ return;
52
+ }
53
+ // Show which error we're about to report (with its age). Some failures
54
+ // — notably `nemus -- <prompt>` — exit without updating last-error.json, so
55
+ // the most recent capture could be an older crash. Surfacing the
56
+ // timestamp lets the user catch a stale report before filing.
57
+ const ageMs = Date.now() - new Date(captured.timestamp).getTime();
58
+ const ageMin = Math.floor(ageMs / 60000);
59
+ const when = Number.isFinite(ageMin)
60
+ ? (ageMin < 1 ? 'just now' : `${ageMin} min ago`)
61
+ : 'unknown time';
62
+ logInfo(`Last captured error (${when}): ${colorize(captured.command, 'cyan')}`);
63
+ logInfo(` ${captured.message.split('\n')[0].slice(0, 100)}`);
64
+ if (ageMin >= 10) {
65
+ logWarning(`This error is ${ageMin} min old — make sure it's the one you mean to report.`);
66
+ logInfo('Tip: pass -m "..." to report a specific message instead.');
67
+ }
68
+ }
69
+
70
+ logInfo(`Reporting to ${colorize(BUG_REPORT_REPO, 'cyan')}...`);
71
+ const result = reportBug(captured, getVersion(), { force: opts.force });
72
+
73
+ switch (result.status) {
74
+ case 'created':
75
+ logSuccess(`Bug issue created: ${colorize(result.url!, 'cyan')}`);
76
+ break;
77
+ case 'duplicate':
78
+ logInfo(`This error was already reported: ${colorize(result.url!, 'cyan')}`);
79
+ logInfo('Subscribe there for updates — no duplicate filed.');
80
+ break;
81
+ case 'skipped':
82
+ logWarning(result.reason || 'Skipped.');
83
+ break;
84
+ case 'failed':
85
+ logError(result.reason || 'Failed to file the bug report.');
86
+ process.exit(1);
87
+ }
88
+ }
@@ -0,0 +1,125 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import { WORKSPACES_DIR } from '../utils/config';
4
+ import { loadMetadata } from '../utils/workspace-meta';
5
+ import { logError } from '../utils/logger';
6
+ import { colorize } from '../utils/colors';
7
+ import { resolveWorkspace } from '../utils/command-helpers';
8
+ import { runInRepo, RunResult } from '../utils/run-operations';
9
+
10
+ const CONCURRENCY_LIMIT = 3;
11
+
12
+ export function registerRunCommand(parent: Command) {
13
+ parent
14
+ .command('run')
15
+ .alias('r')
16
+ .description('Run command across all repos')
17
+ .argument('[workspace]', 'Workspace name')
18
+ .argument('[command...]', 'Command to run (use quotes for flags: "git status -s")')
19
+ .allowUnknownOption()
20
+ .allowExcessArguments()
21
+ .action(async (workspace, commandParts) => {
22
+ // Commander parses positional args correctly even with allowUnknownOption.
23
+ // workspace = first positional, commandParts = rest as array.
24
+ // Unknown options (like -s in "git status -s") end up in commandParts too.
25
+ if (!workspace && (!commandParts || commandParts.length === 0)) {
26
+ // No args at all
27
+ await handleRun(undefined, []);
28
+ } else if (!commandParts || commandParts.length === 0) {
29
+ // Single arg: treat as command, prompt for workspace
30
+ await handleRun(undefined, [workspace]);
31
+ } else {
32
+ // First arg is workspace, rest is command
33
+ await handleRun(workspace, commandParts);
34
+ }
35
+ });
36
+ }
37
+
38
+ async function handleRun(workspaceArg?: string, commandParts?: string[]) {
39
+ try {
40
+ let workspaceName: string;
41
+ let command: string;
42
+
43
+ if (!commandParts || commandParts.length === 0) {
44
+ if (!workspaceArg) {
45
+ logError('Usage: w run [workspace-name] "<command>"');
46
+ process.exit(1);
47
+ }
48
+ // Only one arg: treat as command, prompt for workspace
49
+ command = workspaceArg;
50
+ workspaceName = await resolveWorkspace();
51
+ } else if (!workspaceArg) {
52
+ // commandParts provided but no workspace — treat first part as command
53
+ command = commandParts.join(' ');
54
+ workspaceName = await resolveWorkspace();
55
+ } else {
56
+ // Both workspace and command provided
57
+ workspaceName = workspaceArg;
58
+ command = commandParts.join(' ');
59
+ }
60
+
61
+ const workspacePath = path.join(WORKSPACES_DIR, workspaceName);
62
+ const metadata = await loadMetadata(workspacePath);
63
+
64
+ if (!metadata) {
65
+ logError(`Workspace metadata not found for: ${workspaceName}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ const repos = metadata.repositories.filter(r => r.status === 'success');
70
+
71
+ console.log('\n' + colorize('Run Command Across Workspace', 'bright'));
72
+ console.log(`Workspace: ${colorize(workspaceName, 'cyan')}`);
73
+ console.log(`Command: ${colorize(command, 'yellow')}`);
74
+ console.log(`Repos: ${repos.length}\n`);
75
+
76
+ const results: RunResult[] = [];
77
+
78
+ for (let i = 0; i < repos.length; i += CONCURRENCY_LIMIT) {
79
+ const chunk = repos.slice(i, i + CONCURRENCY_LIMIT);
80
+ const chunkResults = await Promise.all(
81
+ chunk.map(repo => {
82
+ const repoPath = path.join(workspacePath, repo.directoryName);
83
+ const displayName = repo.directoryName !== repo.name
84
+ ? `${repo.directoryName} (${repo.name})`
85
+ : repo.name;
86
+ return runInRepo(repoPath, displayName, command);
87
+ })
88
+ );
89
+ results.push(...chunkResults);
90
+ }
91
+
92
+ console.log(colorize('═'.repeat(60), 'gray'));
93
+ for (const result of results) {
94
+ const icon = result.status === 'success'
95
+ ? colorize('✓', 'green')
96
+ : colorize('✗', 'red');
97
+
98
+ console.log(`\n${icon} ${colorize(result.repo, 'cyan')}`);
99
+
100
+ if (result.stdout) {
101
+ console.log(result.stdout);
102
+ }
103
+ if (result.stderr) {
104
+ console.log(colorize(result.stderr, 'yellow'));
105
+ }
106
+ }
107
+
108
+ const successful = results.filter(r => r.status === 'success').length;
109
+ const failed = results.filter(r => r.status === 'failed').length;
110
+
111
+ console.log('\n' + colorize('═'.repeat(60), 'gray'));
112
+ console.log(`${colorize('✓', 'green')} ${successful} successful, ${colorize('✗', 'red')} ${failed} failed`);
113
+ console.log('');
114
+
115
+ if (failed > 0) {
116
+ process.exit(1);
117
+ }
118
+ } catch (error) {
119
+ logError('Failed to run command');
120
+ if (error instanceof Error) {
121
+ logError(error.message);
122
+ }
123
+ process.exit(1);
124
+ }
125
+ }