@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,74 @@
1
+ import { exec, execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { GitHubRepo } from '../types';
4
+ import { logError, logInfo } from './logger';
5
+ import { getCachedRepos, setCachedRepos } from './cache';
6
+ import { getUserConfig } from './config';
7
+
8
+ const execAsync = promisify(exec);
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ export const verifyGhAuth = async (): Promise<boolean> => {
12
+ try {
13
+ const { stdout } = await execAsync('gh auth status');
14
+ return stdout.includes('Logged in') || stdout.includes('✓');
15
+ } catch (error) {
16
+ return false;
17
+ }
18
+ };
19
+
20
+ export const fetchOrgRepos = async (options: { forceRefresh?: boolean } = {}): Promise<GitHubRepo[]> => {
21
+ const { forceRefresh = false } = options;
22
+
23
+ try {
24
+ const { githubOrg } = getUserConfig();
25
+
26
+ // Try to get cached repos first
27
+ if (!forceRefresh) {
28
+ const cachedRepos = await getCachedRepos({ org: githubOrg });
29
+ if (cachedRepos) {
30
+ return cachedRepos.sort((a, b) => a.name.localeCompare(b.name));
31
+ }
32
+ }
33
+ logInfo(
34
+ githubOrg
35
+ ? `Fetching ${githubOrg} repositories from GitHub...`
36
+ : 'Fetching your GitHub repositories... (set an org with `nemus configure` to list an organization)'
37
+ );
38
+
39
+ // With no configured org, `gh repo list` (no positional) lists the
40
+ // authenticated user's own repos. With an org, it lists that org's repos.
41
+ const args = [
42
+ 'repo', 'list',
43
+ ...(githubOrg ? [githubOrg] : []),
44
+ '--limit', '1000',
45
+ '--json', 'name,url,sshUrl,owner,description,isPrivate',
46
+ ];
47
+ const { stdout } = await execFileAsync('gh', args);
48
+
49
+ const repos: GitHubRepo[] = JSON.parse(stdout);
50
+
51
+ logInfo(`Found ${repos.length} repositories`);
52
+
53
+ // Cache the results (24 hour TTL)
54
+ await setCachedRepos(repos, 86400000, githubOrg);
55
+
56
+ return repos.sort((a, b) => a.name.localeCompare(b.name));
57
+ } catch (error) {
58
+ logError('Failed to fetch repositories from GitHub');
59
+ if (error instanceof Error) {
60
+ logError(error.message);
61
+ }
62
+ throw new Error('Unable to fetch repositories. Please ensure gh CLI is authenticated.');
63
+ }
64
+ };
65
+
66
+ export const displayAuthInstructions = (): void => {
67
+ console.log('\n' + '='.repeat(60));
68
+ console.log('GitHub CLI Authentication Required');
69
+ console.log('='.repeat(60));
70
+ console.log('\nPlease authenticate with GitHub CLI by running:');
71
+ console.log('\n gh auth login\n');
72
+ console.log('Then try running this command again.');
73
+ console.log('='.repeat(60) + '\n');
74
+ };
@@ -0,0 +1,359 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { HealthCheckResult, WorkspaceMetadata } from '../types';
6
+ import { isGitRepository, hasUncommittedChanges } from './git-status';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ export const checkMissingRepositories = async (
11
+ workspacePath: string,
12
+ metadata: WorkspaceMetadata
13
+ ): Promise<HealthCheckResult> => {
14
+ const missingRepos: string[] = [];
15
+
16
+ for (const repo of metadata.repositories) {
17
+ const repoPath = path.join(workspacePath, repo.directoryName);
18
+ try {
19
+ await fs.access(repoPath);
20
+ } catch {
21
+ missingRepos.push(repo.directoryName);
22
+ }
23
+ }
24
+
25
+ if (missingRepos.length === 0) {
26
+ return {
27
+ category: 'Missing Repositories',
28
+ status: 'healthy',
29
+ message: 'All repositories are present',
30
+ };
31
+ }
32
+
33
+ return {
34
+ category: 'Missing Repositories',
35
+ status: 'error',
36
+ message: `${missingRepos.length} repository(ies) missing`,
37
+ details: missingRepos.join(', '),
38
+ actionable: `Run 'workspace sync ${metadata.workspaceName}' to clone missing repositories`,
39
+ };
40
+ };
41
+
42
+ export const checkGitHealth = async (
43
+ workspacePath: string,
44
+ repoNames: string[]
45
+ ): Promise<HealthCheckResult> => {
46
+ const issues: string[] = [];
47
+
48
+ for (const repoName of repoNames) {
49
+ const repoPath = path.join(workspacePath, repoName);
50
+
51
+ try {
52
+ await fs.access(repoPath);
53
+ } catch {
54
+ continue; // Skip missing repos (handled by checkMissingRepositories)
55
+ }
56
+
57
+ const isRepo = await isGitRepository(repoPath);
58
+ if (!isRepo) {
59
+ issues.push(`${repoName}: not a git repository`);
60
+ continue;
61
+ }
62
+
63
+ // Check for corrupted repo
64
+ try {
65
+ await execAsync('git fsck --no-progress', {
66
+ cwd: repoPath,
67
+ timeout: 30000,
68
+ });
69
+ } catch (error) {
70
+ issues.push(`${repoName}: repository may be corrupted`);
71
+ }
72
+ }
73
+
74
+ if (issues.length === 0) {
75
+ return {
76
+ category: 'Git Health',
77
+ status: 'healthy',
78
+ message: 'All repositories are healthy',
79
+ };
80
+ }
81
+
82
+ return {
83
+ category: 'Git Health',
84
+ status: 'error',
85
+ message: `${issues.length} repository(ies) have issues`,
86
+ details: issues.join('\n'),
87
+ actionable: 'Check repository integrity and consider re-cloning problematic repos',
88
+ };
89
+ };
90
+
91
+ export const checkUncommittedChanges = async (
92
+ workspacePath: string,
93
+ repoNames: string[]
94
+ ): Promise<HealthCheckResult> => {
95
+ const dirtyRepos: string[] = [];
96
+
97
+ for (const repoName of repoNames) {
98
+ const repoPath = path.join(workspacePath, repoName);
99
+
100
+ try {
101
+ await fs.access(repoPath);
102
+ } catch {
103
+ continue;
104
+ }
105
+
106
+ const hasChanges = await hasUncommittedChanges(repoPath);
107
+ if (hasChanges) {
108
+ dirtyRepos.push(repoName);
109
+ }
110
+ }
111
+
112
+ if (dirtyRepos.length === 0) {
113
+ return {
114
+ category: 'Uncommitted Changes',
115
+ status: 'healthy',
116
+ message: 'No uncommitted changes',
117
+ };
118
+ }
119
+
120
+ return {
121
+ category: 'Uncommitted Changes',
122
+ status: 'warning',
123
+ message: `${dirtyRepos.length} repository(ies) have uncommitted changes`,
124
+ details: dirtyRepos.join(', '),
125
+ actionable: `Review and commit or stash changes before switching branches`,
126
+ };
127
+ };
128
+
129
+ export const checkAuthentication = async (): Promise<HealthCheckResult> => {
130
+ const issues: string[] = [];
131
+
132
+ // Check GitHub CLI
133
+ try {
134
+ await execAsync('gh auth status', { timeout: 5000 });
135
+ } catch {
136
+ issues.push('GitHub CLI (gh) not authenticated');
137
+ }
138
+
139
+ // Check SSH keys
140
+ try {
141
+ await execAsync('ssh -T git@github.com', { timeout: 5000 });
142
+ } catch (error) {
143
+ const errorMessage = error instanceof Error ? error.message : '';
144
+ if (!errorMessage.includes('successfully authenticated')) {
145
+ issues.push('SSH key not configured for GitHub');
146
+ }
147
+ }
148
+
149
+ if (issues.length === 0) {
150
+ return {
151
+ category: 'Authentication',
152
+ status: 'healthy',
153
+ message: 'Authentication configured correctly',
154
+ };
155
+ }
156
+
157
+ return {
158
+ category: 'Authentication',
159
+ status: 'warning',
160
+ message: 'Authentication issues detected',
161
+ details: issues.join('\n'),
162
+ actionable: 'Run "gh auth login" or configure SSH keys',
163
+ };
164
+ };
165
+
166
+ export const checkDependencies = async (
167
+ workspacePath: string,
168
+ repoNames: string[]
169
+ ): Promise<HealthCheckResult> => {
170
+ const missingDeps: string[] = [];
171
+
172
+ for (const repoName of repoNames) {
173
+ const repoPath = path.join(workspacePath, repoName);
174
+
175
+ try {
176
+ await fs.access(repoPath);
177
+ } catch {
178
+ continue;
179
+ }
180
+
181
+ // Check for package.json
182
+ const packageJsonPath = path.join(repoPath, 'package.json');
183
+ try {
184
+ await fs.access(packageJsonPath);
185
+
186
+ // Check if node_modules exists
187
+ const nodeModulesPath = path.join(repoPath, 'node_modules');
188
+ try {
189
+ await fs.access(nodeModulesPath);
190
+ } catch {
191
+ missingDeps.push(`${repoName}: missing node_modules`);
192
+ }
193
+ } catch {
194
+ // No package.json, skip
195
+ }
196
+ }
197
+
198
+ if (missingDeps.length === 0) {
199
+ return {
200
+ category: 'Dependencies',
201
+ status: 'healthy',
202
+ message: 'All dependencies installed',
203
+ };
204
+ }
205
+
206
+ return {
207
+ category: 'Dependencies',
208
+ status: 'warning',
209
+ message: `${missingDeps.length} repository(ies) missing dependencies`,
210
+ details: missingDeps.join('\n'),
211
+ actionable: 'Run "npm install" or "yarn install" in affected repositories',
212
+ };
213
+ };
214
+
215
+ export const checkDiskSpace = async (workspacePath: string): Promise<HealthCheckResult> => {
216
+ try {
217
+ // Get disk usage for workspace
218
+ const { stdout } = await execAsync(`du -sh "${workspacePath}"`, { timeout: 30000 });
219
+ const sizeStr = stdout.trim().split('\t')[0];
220
+
221
+ // Parse size (rough check for > 10GB)
222
+ const sizeValue = parseFloat(sizeStr);
223
+ const unit = sizeStr.replace(/[0-9.]/g, '').trim();
224
+
225
+ const isLarge = (unit === 'G' && sizeValue > 10) || unit === 'T';
226
+
227
+ if (isLarge) {
228
+ return {
229
+ category: 'Disk Space',
230
+ status: 'warning',
231
+ message: `Workspace is large (${sizeStr})`,
232
+ actionable: 'Consider running "workspace cleanup" to free up space',
233
+ };
234
+ }
235
+
236
+ return {
237
+ category: 'Disk Space',
238
+ status: 'healthy',
239
+ message: `Workspace size: ${sizeStr}`,
240
+ };
241
+ } catch (error) {
242
+ return {
243
+ category: 'Disk Space',
244
+ status: 'warning',
245
+ message: 'Could not calculate disk usage',
246
+ };
247
+ }
248
+ };
249
+
250
+ export const checkMetadataIntegrity = async (
251
+ workspacePath: string,
252
+ metadata: WorkspaceMetadata
253
+ ): Promise<HealthCheckResult> => {
254
+ const issues: string[] = [];
255
+
256
+ // Check required fields
257
+ if (!metadata.workspaceName) {
258
+ issues.push('Missing workspace name');
259
+ }
260
+
261
+ if (!metadata.createdAt) {
262
+ issues.push('Missing creation timestamp');
263
+ }
264
+
265
+ if (!metadata.repositories || metadata.repositories.length === 0) {
266
+ issues.push('No repositories in metadata');
267
+ }
268
+
269
+ // Check for duplicate directory names
270
+ const dirNames = metadata.repositories.map(r => r.directoryName);
271
+ const uniqueDirNames = new Set(dirNames);
272
+ if (dirNames.length !== uniqueDirNames.size) {
273
+ issues.push('Duplicate directory names in metadata');
274
+ }
275
+
276
+ // Check for extra directories not in metadata
277
+ try {
278
+ const entries = await fs.readdir(workspacePath, { withFileTypes: true });
279
+ const directories = entries.filter(e => e.isDirectory()).map(e => e.name);
280
+ const metadataDirNames = new Set(dirNames);
281
+ const extraDirs = directories.filter(
282
+ dir => !metadataDirNames.has(dir) && !dir.startsWith('.')
283
+ );
284
+
285
+ if (extraDirs.length > 0) {
286
+ issues.push(`${extraDirs.length} directories not tracked in metadata: ${extraDirs.join(', ')}`);
287
+ }
288
+ } catch (error) {
289
+ // Ignore read errors
290
+ }
291
+
292
+ if (issues.length === 0) {
293
+ return {
294
+ category: 'Metadata Integrity',
295
+ status: 'healthy',
296
+ message: 'Metadata is valid',
297
+ };
298
+ }
299
+
300
+ return {
301
+ category: 'Metadata Integrity',
302
+ status: 'warning',
303
+ message: 'Metadata issues detected',
304
+ details: issues.join('\n'),
305
+ actionable: 'Review workspace metadata file',
306
+ };
307
+ };
308
+
309
+ export const checkGhqInstalled = async (): Promise<HealthCheckResult> => {
310
+ try {
311
+ await execAsync('which ghq', { timeout: 5000 });
312
+ return {
313
+ category: 'ghq',
314
+ status: 'healthy',
315
+ message: 'ghq is installed (fast cached clones enabled)',
316
+ };
317
+ } catch {
318
+ return {
319
+ category: 'ghq',
320
+ status: 'warning',
321
+ message: 'ghq is not installed',
322
+ details: 'Without ghq, repos are cloned directly with git — slower, no shared cache, and large repos can hit clone timeouts.',
323
+ actionable: 'Install ghq: brew install ghq (macOS) — https://github.com/x-motemen/ghq#installation',
324
+ };
325
+ }
326
+ };
327
+
328
+ export const runAllHealthChecks = async (
329
+ workspacePath: string,
330
+ metadata: WorkspaceMetadata
331
+ ): Promise<HealthCheckResult[]> => {
332
+ const repoNames = metadata.repositories.map(r => r.directoryName);
333
+
334
+ const checks = await Promise.all([
335
+ checkMissingRepositories(workspacePath, metadata),
336
+ checkGitHealth(workspacePath, repoNames),
337
+ checkUncommittedChanges(workspacePath, repoNames),
338
+ checkAuthentication(),
339
+ checkGhqInstalled(),
340
+ checkDependencies(workspacePath, repoNames),
341
+ checkDiskSpace(workspacePath),
342
+ checkMetadataIntegrity(workspacePath, metadata),
343
+ ]);
344
+
345
+ return checks;
346
+ };
347
+
348
+ export const calculateHealthScore = (results: HealthCheckResult[]): number => {
349
+ const weights = {
350
+ healthy: 100,
351
+ warning: 60,
352
+ error: 0,
353
+ };
354
+
355
+ const totalWeight = results.reduce((sum, result) => sum + weights[result.status], 0);
356
+ const maxWeight = results.length * 100;
357
+
358
+ return Math.round((totalWeight / maxWeight) * 100);
359
+ };
@@ -0,0 +1,140 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ const CACHE_DIR = path.join(os.homedir(), '.workspace-manager-cache');
6
+ const HISTORY_FILE = path.join(CACHE_DIR, 'history.jsonl');
7
+
8
+ export interface OperationRecord {
9
+ timestamp: string;
10
+ command: string;
11
+ workspace?: string;
12
+ duration: number;
13
+ success: boolean;
14
+ error?: string;
15
+ }
16
+
17
+ export const ensureCacheDir = async (): Promise<void> => {
18
+ try {
19
+ await fs.mkdir(CACHE_DIR, { recursive: true });
20
+ } catch {
21
+ // Ignore if already exists
22
+ }
23
+ };
24
+
25
+ export const logOperation = async (record: OperationRecord): Promise<void> => {
26
+ await ensureCacheDir();
27
+
28
+ const line = JSON.stringify(record) + '\n';
29
+
30
+ try {
31
+ await fs.appendFile(HISTORY_FILE, line, 'utf-8');
32
+ } catch (error) {
33
+ // Silently fail - don't block operations if logging fails
34
+ }
35
+ };
36
+
37
+ export const readHistory = async (limit?: number): Promise<OperationRecord[]> => {
38
+ try {
39
+ const content = await fs.readFile(HISTORY_FILE, 'utf-8');
40
+ const lines = content.trim().split('\n');
41
+ const records = lines
42
+ .filter(line => line.trim())
43
+ .map(line => {
44
+ try {
45
+ return JSON.parse(line) as OperationRecord;
46
+ } catch {
47
+ return null;
48
+ }
49
+ })
50
+ .filter((record): record is OperationRecord => record !== null);
51
+
52
+ // Return most recent first
53
+ const reversed = records.reverse();
54
+
55
+ return limit ? reversed.slice(0, limit) : reversed;
56
+ } catch {
57
+ return [];
58
+ }
59
+ };
60
+
61
+ export const getOperationStats = async (): Promise<{
62
+ totalOperations: number;
63
+ successRate: number;
64
+ avgDuration: number;
65
+ commandCounts: Record<string, number>;
66
+ }> => {
67
+ const history = await readHistory();
68
+
69
+ if (history.length === 0) {
70
+ return {
71
+ totalOperations: 0,
72
+ successRate: 0,
73
+ avgDuration: 0,
74
+ commandCounts: {},
75
+ };
76
+ }
77
+
78
+ const successful = history.filter(r => r.success).length;
79
+ const totalDuration = history.reduce((sum, r) => sum + r.duration, 0);
80
+ const commandCounts: Record<string, number> = {};
81
+
82
+ for (const record of history) {
83
+ commandCounts[record.command] = (commandCounts[record.command] || 0) + 1;
84
+ }
85
+
86
+ return {
87
+ totalOperations: history.length,
88
+ successRate: (successful / history.length) * 100,
89
+ avgDuration: totalDuration / history.length,
90
+ commandCounts,
91
+ };
92
+ };
93
+
94
+ export const filterHistory = async (
95
+ command?: string,
96
+ workspace?: string,
97
+ successOnly?: boolean
98
+ ): Promise<OperationRecord[]> => {
99
+ const history = await readHistory();
100
+
101
+ return history.filter(record => {
102
+ if (command && record.command !== command) return false;
103
+ if (workspace && record.workspace !== workspace) return false;
104
+ if (successOnly && !record.success) return false;
105
+ return true;
106
+ });
107
+ };
108
+
109
+ export const clearHistory = async (): Promise<void> => {
110
+ try {
111
+ await fs.unlink(HISTORY_FILE);
112
+ } catch {
113
+ // Ignore if file doesn't exist
114
+ }
115
+ };
116
+
117
+ export class OperationTimer {
118
+ private startTime: number;
119
+ private command: string;
120
+ private workspace?: string;
121
+
122
+ constructor(command: string, workspace?: string) {
123
+ this.command = command;
124
+ this.workspace = workspace;
125
+ this.startTime = Date.now();
126
+ }
127
+
128
+ async end(success: boolean, error?: string): Promise<void> {
129
+ const duration = Date.now() - this.startTime;
130
+
131
+ await logOperation({
132
+ timestamp: new Date().toISOString(),
133
+ command: this.command,
134
+ workspace: this.workspace,
135
+ duration,
136
+ success,
137
+ error,
138
+ });
139
+ }
140
+ }
@@ -0,0 +1,92 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as path from 'path';
4
+ import { RepositoryMetadata, PostCloneHook } from '../types';
5
+ import { logInfo, logSuccess, logError, logWarning } from './logger';
6
+ import { colorize } from './colors';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ const HOOK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
11
+
12
+ export interface HookResult {
13
+ repo: string;
14
+ command: string;
15
+ status: 'success' | 'failed';
16
+ error?: string;
17
+ }
18
+
19
+ export async function runPostCloneHooks(
20
+ workspacePath: string,
21
+ repositories: RepositoryMetadata[],
22
+ hooks?: PostCloneHook[]
23
+ ): Promise<HookResult[]> {
24
+ if (!hooks || hooks.length === 0) {
25
+ return [];
26
+ }
27
+
28
+ const results: HookResult[] = [];
29
+
30
+ logInfo('Running post-clone hooks...');
31
+ console.log('');
32
+
33
+ for (const hook of hooks) {
34
+ if (hook.description) {
35
+ logInfo(`Hook: ${colorize(hook.description, 'cyan')}`);
36
+ }
37
+
38
+ // Determine target repos
39
+ const targetRepos = hook.repoName
40
+ ? repositories.filter(r => r.name === hook.repoName || r.directoryName === hook.repoName)
41
+ : repositories;
42
+
43
+ if (targetRepos.length === 0) {
44
+ if (hook.repoName) {
45
+ logWarning(`No matching repo found for hook target: ${hook.repoName}`);
46
+ }
47
+ continue;
48
+ }
49
+
50
+ for (const repo of targetRepos) {
51
+ const repoPath = path.join(workspacePath, repo.directoryName);
52
+
53
+ for (const command of hook.commands) {
54
+ try {
55
+ logInfo(` [${colorize(repo.directoryName, 'cyan')}] ${command}`);
56
+ await execAsync(command, { cwd: repoPath, timeout: HOOK_TIMEOUT });
57
+
58
+ results.push({
59
+ repo: repo.directoryName,
60
+ command,
61
+ status: 'success',
62
+ });
63
+ } catch (error) {
64
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
65
+
66
+ results.push({
67
+ repo: repo.directoryName,
68
+ command,
69
+ status: 'failed',
70
+ error: errorMessage,
71
+ });
72
+
73
+ logError(` [${repo.directoryName}] Failed: ${errorMessage}`);
74
+
75
+ if (!hook.continueOnError) {
76
+ logWarning('Hook execution stopped due to error (continueOnError is false)');
77
+ return results;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ // Log summary
85
+ const successful = results.filter(r => r.status === 'success').length;
86
+ const failed = results.filter(r => r.status === 'failed').length;
87
+
88
+ console.log('');
89
+ logInfo(`Hook results: ${colorize(String(successful), 'green')} successful, ${colorize(String(failed), 'red')} failed`);
90
+
91
+ return results;
92
+ }
@@ -0,0 +1,37 @@
1
+ import { colors, colorize } from './colors';
2
+
3
+ const getTimestamp = (): string => {
4
+ const now = new Date();
5
+ return now.toLocaleTimeString('en-US', {
6
+ hour12: false,
7
+ hour: '2-digit',
8
+ minute: '2-digit',
9
+ second: '2-digit'
10
+ });
11
+ };
12
+
13
+ export const logInfo = (message: string): void => {
14
+ console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${message}`);
15
+ };
16
+
17
+ export const logSuccess = (message: string): void => {
18
+ console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('✓', 'green')} ${message}`);
19
+ };
20
+
21
+ export const logError = (message: string): void => {
22
+ console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('✗', 'red')} ${message}`);
23
+ };
24
+
25
+ export const logWarning = (message: string): void => {
26
+ console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('⚠', 'yellow')} ${message}`);
27
+ };
28
+
29
+ export const logStep = (stepOrMessage: number | string, total?: number, message?: string): void => {
30
+ if (typeof stepOrMessage === 'string') {
31
+ // Single parameter version: just a message
32
+ console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('▸', 'cyan')} ${stepOrMessage}`);
33
+ } else {
34
+ // Three parameter version: step, total, message
35
+ console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize(`[${stepOrMessage}/${total}]`, 'cyan')} ${message}`);
36
+ }
37
+ };