@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.
- package/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/assets/README.md +53 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +66 -0
- package/assets/favicon-32.png +0 -0
- package/assets/favicon-64.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon.svg +23 -0
- package/assets/logo-256.png +0 -0
- package/assets/logo-512.png +0 -0
- package/assets/logo-wordmark.svg +35 -0
- package/assets/logo.svg +32 -0
- package/bin/workspace.js +130 -0
- package/dist/cli/ai-prompt.js +421 -0
- package/dist/cli/dashboard/AgentList.js +61 -0
- package/dist/cli/dashboard/DashboardSidebar.js +280 -0
- package/dist/cli/dashboard/HelpFooter.js +73 -0
- package/dist/cli/dashboard/StatusBadge.js +50 -0
- package/dist/cli/dashboard/index.js +11 -0
- package/dist/cli/dashboard/types.js +2 -0
- package/dist/cli/tui.js +48 -0
- package/dist/commands/ai-prompt.js +22 -0
- package/dist/commands/analyze-deps.js +138 -0
- package/dist/commands/archive.js +113 -0
- package/dist/commands/branch/create.js +119 -0
- package/dist/commands/branch/index.js +104 -0
- package/dist/commands/branch/merge.js +90 -0
- package/dist/commands/branch/rebase.js +84 -0
- package/dist/commands/branch/switch.js +165 -0
- package/dist/commands/cache/index.js +80 -0
- package/dist/commands/cache/manager.js +198 -0
- package/dist/commands/cleanup.js +153 -0
- package/dist/commands/configure-claude.js +55 -0
- package/dist/commands/configure.js +231 -0
- package/dist/commands/create.js +204 -0
- package/dist/commands/dashboard/hook-handler.js +162 -0
- package/dist/commands/dashboard/index.js +258 -0
- package/dist/commands/dashboard/launcher.js +40 -0
- package/dist/commands/dashboard/session-picker.js +106 -0
- package/dist/commands/dashboard/sidebar.js +39 -0
- package/dist/commands/dashboard/workspace-picker.js +95 -0
- package/dist/commands/delete.js +194 -0
- package/dist/commands/deprecated-aliases.js +44 -0
- package/dist/commands/diff.js +129 -0
- package/dist/commands/doctor.js +141 -0
- package/dist/commands/generate-docs.js +77 -0
- package/dist/commands/ghq-status.js +72 -0
- package/dist/commands/go.js +154 -0
- package/dist/commands/history.js +124 -0
- package/dist/commands/list.js +191 -0
- package/dist/commands/mcp/index.js +69 -0
- package/dist/commands/migrate.js +296 -0
- package/dist/commands/remove-repo.js +217 -0
- package/dist/commands/report-bug.js +112 -0
- package/dist/commands/run.js +147 -0
- package/dist/commands/save-context.js +171 -0
- package/dist/commands/sessions.js +129 -0
- package/dist/commands/status.js +138 -0
- package/dist/commands/suite/create.js +212 -0
- package/dist/commands/suite/delete.js +64 -0
- package/dist/commands/suite/export.js +126 -0
- package/dist/commands/suite/import.js +128 -0
- package/dist/commands/suite/index.js +94 -0
- package/dist/commands/suite/list.js +49 -0
- package/dist/commands/suite/use.js +227 -0
- package/dist/commands/sync.js +117 -0
- package/dist/commands/update.js +203 -0
- package/dist/mcp/install.js +550 -0
- package/dist/mcp/server.js +398 -0
- package/dist/mcp/tools.js +922 -0
- package/dist/pi-extensions/permission-sync.ts.template +41 -0
- package/dist/pi-extensions/permissions-gate.ts.template +69 -0
- package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
- package/dist/pi-extensions/review-reminder.ts.template +58 -0
- package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
- package/dist/program.js +136 -0
- package/dist/scripts/workspace-table.py +394 -0
- package/dist/types/dashboard.js +11 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/agent-config.js +263 -0
- package/dist/utils/agent-state.js +178 -0
- package/dist/utils/banner.js +24 -0
- package/dist/utils/branch-operations.js +232 -0
- package/dist/utils/bug-report.js +355 -0
- package/dist/utils/cache.js +168 -0
- package/dist/utils/claude-integration.js +617 -0
- package/dist/utils/claude-sessions.js +260 -0
- package/dist/utils/cleanup-operations.js +120 -0
- package/dist/utils/colors.js +26 -0
- package/dist/utils/command-helpers.js +44 -0
- package/dist/utils/config.js +150 -0
- package/dist/utils/context-file.js +33 -0
- package/dist/utils/dashboard-hooks.js +172 -0
- package/dist/utils/dependency-analyzer.js +234 -0
- package/dist/utils/diff-operations.js +80 -0
- package/dist/utils/doc-generator.js +184 -0
- package/dist/utils/ghq-integration.js +332 -0
- package/dist/utils/git-operations.js +237 -0
- package/dist/utils/git-status.js +187 -0
- package/dist/utils/github.js +68 -0
- package/dist/utils/health-checks.js +345 -0
- package/dist/utils/history.js +150 -0
- package/dist/utils/hooks.js +100 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/permission-sync.js +681 -0
- package/dist/utils/pi-extensions.js +224 -0
- package/dist/utils/progress.js +92 -0
- package/dist/utils/prompts.js +279 -0
- package/dist/utils/repo-resolver.js +260 -0
- package/dist/utils/retry.js +65 -0
- package/dist/utils/run-operations.js +30 -0
- package/dist/utils/suite.js +159 -0
- package/dist/utils/sync-operations.js +107 -0
- package/dist/utils/tmux-dashboard.js +184 -0
- package/dist/utils/validation.js +153 -0
- package/dist/utils/version-check.js +121 -0
- package/dist/utils/workspace-meta.js +185 -0
- package/install-shell-integration.sh +532 -0
- package/install.sh +401 -0
- package/package.json +90 -0
- package/remove-shell-block.awk +44 -0
- package/scripts/postinstall.js +85 -0
- package/skills/analyze-deps.md +23 -0
- package/skills/archive-workspace.md +26 -0
- package/skills/branch-create.md +18 -0
- package/skills/create-workspace.md +39 -0
- package/skills/delete-workspace.md +37 -0
- package/skills/list-org-repos.md +26 -0
- package/skills/list-suites.md +24 -0
- package/skills/list-workspaces.md +27 -0
- package/skills/refresh-workspace-docs.md +89 -0
- package/skills/remove-repo.md +25 -0
- package/skills/run-command.md +23 -0
- package/skills/save-context.md +68 -0
- package/skills/search-repos.md +31 -0
- package/skills/snapshot-list.md +18 -0
- package/skills/snapshot-save.md +12 -0
- package/skills/switch-branch.md +18 -0
- package/skills/update-cache.md +21 -0
- package/skills/update-workspace.md +66 -0
- package/skills/workspace-cleanup.md +18 -0
- package/skills/workspace-diff.md +23 -0
- package/skills/workspace-doctor.md +23 -0
- package/skills/workspace-info.md +45 -0
- package/skills/workspace-manager/SKILL.md +201 -0
- package/skills/workspace-manager/references/ai-prompt.md +44 -0
- package/skills/workspace-manager/references/analyze-deps.md +45 -0
- package/skills/workspace-manager/references/archive-workspace.md +38 -0
- package/skills/workspace-manager/references/branch-create.md +31 -0
- package/skills/workspace-manager/references/branch-merge.md +22 -0
- package/skills/workspace-manager/references/branch-rebase.md +22 -0
- package/skills/workspace-manager/references/cache-clear.md +18 -0
- package/skills/workspace-manager/references/cache-info.md +22 -0
- package/skills/workspace-manager/references/configure-claude.md +31 -0
- package/skills/workspace-manager/references/configure.md +35 -0
- package/skills/workspace-manager/references/create-workspace.md +45 -0
- package/skills/workspace-manager/references/dashboard.md +61 -0
- package/skills/workspace-manager/references/delete-workspace.md +33 -0
- package/skills/workspace-manager/references/generate-docs.md +36 -0
- package/skills/workspace-manager/references/ghq-status.md +28 -0
- package/skills/workspace-manager/references/go.md +33 -0
- package/skills/workspace-manager/references/history.md +41 -0
- package/skills/workspace-manager/references/list-org-repos.md +26 -0
- package/skills/workspace-manager/references/list-suites.md +21 -0
- package/skills/workspace-manager/references/list-workspaces.md +27 -0
- package/skills/workspace-manager/references/mcp.md +49 -0
- package/skills/workspace-manager/references/remove-repo.md +26 -0
- package/skills/workspace-manager/references/run-command.md +44 -0
- package/skills/workspace-manager/references/search-repos.md +24 -0
- package/skills/workspace-manager/references/sessions.md +27 -0
- package/skills/workspace-manager/references/suite-create.md +22 -0
- package/skills/workspace-manager/references/suite-delete.md +21 -0
- package/skills/workspace-manager/references/suite-export.md +19 -0
- package/skills/workspace-manager/references/suite-import.md +19 -0
- package/skills/workspace-manager/references/suite-use.md +27 -0
- package/skills/workspace-manager/references/switch-branch.md +25 -0
- package/skills/workspace-manager/references/update-cache.md +22 -0
- package/skills/workspace-manager/references/update-workspace.md +50 -0
- package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
- package/skills/workspace-manager/references/workspace-diff.md +27 -0
- package/skills/workspace-manager/references/workspace-doctor.md +27 -0
- package/skills/workspace-manager/references/workspace-info.md +33 -0
- package/skills/workspace-manager/references/workspace-status.md +43 -0
- package/skills/workspace-manager/references/workspace-sync.md +26 -0
- package/skills/workspace-status.md +26 -0
- package/skills/workspace-sync.md +23 -0
- package/src/cli/ai-prompt.test.ts +667 -0
- package/src/cli/ai-prompt.ts +421 -0
- package/src/cli/dashboard/AgentList.tsx +44 -0
- package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
- package/src/cli/dashboard/HelpFooter.tsx +22 -0
- package/src/cli/dashboard/StatusBadge.tsx +23 -0
- package/src/cli/dashboard/index.ts +5 -0
- package/src/cli/dashboard/types.ts +16 -0
- package/src/cli/tui.tsx +51 -0
- package/src/cli/workspace-cli.test.ts +30 -0
- package/src/commands/ai-prompt.ts +21 -0
- package/src/commands/analyze-deps.ts +119 -0
- package/src/commands/archive.ts +114 -0
- package/src/commands/branch/create.ts +94 -0
- package/src/commands/branch/index.ts +74 -0
- package/src/commands/branch/merge.ts +61 -0
- package/src/commands/branch/rebase.ts +54 -0
- package/src/commands/branch/switch.ts +150 -0
- package/src/commands/cache/index.ts +51 -0
- package/src/commands/cache/manager.ts +168 -0
- package/src/commands/cleanup.ts +132 -0
- package/src/commands/configure-claude.ts +55 -0
- package/src/commands/configure.ts +204 -0
- package/src/commands/create.ts +196 -0
- package/src/commands/dashboard/hook-handler.ts +138 -0
- package/src/commands/dashboard/index.ts +248 -0
- package/src/commands/dashboard/launcher.ts +42 -0
- package/src/commands/dashboard/session-picker.ts +73 -0
- package/src/commands/dashboard/sidebar.js +39 -0
- package/src/commands/dashboard/workspace-picker.ts +62 -0
- package/src/commands/delete.test.ts +250 -0
- package/src/commands/delete.ts +171 -0
- package/src/commands/deprecated-aliases.ts +55 -0
- package/src/commands/diff.ts +106 -0
- package/src/commands/doctor.ts +110 -0
- package/src/commands/generate-docs.ts +45 -0
- package/src/commands/ghq-status.ts +70 -0
- package/src/commands/go.ts +125 -0
- package/src/commands/history.ts +136 -0
- package/src/commands/list.test.ts +219 -0
- package/src/commands/list.ts +176 -0
- package/src/commands/mcp/index.ts +39 -0
- package/src/commands/migrate.ts +277 -0
- package/src/commands/remove-repo.ts +194 -0
- package/src/commands/report-bug.ts +88 -0
- package/src/commands/run.ts +125 -0
- package/src/commands/save-context.test.ts +113 -0
- package/src/commands/save-context.ts +141 -0
- package/src/commands/sessions.ts +99 -0
- package/src/commands/status.ts +111 -0
- package/src/commands/suite/create.ts +240 -0
- package/src/commands/suite/delete.ts +65 -0
- package/src/commands/suite/export.ts +97 -0
- package/src/commands/suite/import.ts +100 -0
- package/src/commands/suite/index.ts +66 -0
- package/src/commands/suite/list.ts +54 -0
- package/src/commands/suite/use.ts +232 -0
- package/src/commands/sync.ts +97 -0
- package/src/commands/update.ts +197 -0
- package/src/mcp/install.ts +529 -0
- package/src/mcp/server.ts +579 -0
- package/src/mcp/tools.test.ts +545 -0
- package/src/mcp/tools.ts +1019 -0
- package/src/mcp/update-workspace.test.ts +220 -0
- package/src/pi-extensions/permission-sync.ts.template +41 -0
- package/src/pi-extensions/permissions-gate.ts.template +69 -0
- package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
- package/src/pi-extensions/review-reminder.ts.template +58 -0
- package/src/pi-extensions/workspace-input-status.ts.template +919 -0
- package/src/program.ts +112 -0
- package/src/scripts/workspace-table.py +394 -0
- package/src/types/dashboard.ts +29 -0
- package/src/types/index.ts +109 -0
- package/src/types/ink.d.ts +171 -0
- package/src/utils/agent-config.test.ts +303 -0
- package/src/utils/agent-config.ts +264 -0
- package/src/utils/agent-state.test.ts +166 -0
- package/src/utils/agent-state.ts +135 -0
- package/src/utils/banner.ts +24 -0
- package/src/utils/branch-operations.ts +237 -0
- package/src/utils/bug-report.test.ts +174 -0
- package/src/utils/bug-report.ts +355 -0
- package/src/utils/cache.ts +150 -0
- package/src/utils/claude-integration.test.ts +381 -0
- package/src/utils/claude-integration.ts +649 -0
- package/src/utils/claude-sessions.test.ts +262 -0
- package/src/utils/claude-sessions.ts +251 -0
- package/src/utils/cleanup-operations.ts +94 -0
- package/src/utils/colors.ts +25 -0
- package/src/utils/command-helpers.ts +46 -0
- package/src/utils/config.test.ts +203 -0
- package/src/utils/config.ts +133 -0
- package/src/utils/context-file.ts +36 -0
- package/src/utils/dashboard-hooks.test.ts +168 -0
- package/src/utils/dashboard-hooks.ts +163 -0
- package/src/utils/dependency-analyzer.ts +243 -0
- package/src/utils/diff-operations.ts +59 -0
- package/src/utils/doc-generator.ts +187 -0
- package/src/utils/ghq-integration.test.ts +195 -0
- package/src/utils/ghq-integration.ts +317 -0
- package/src/utils/git-operations.test.ts +265 -0
- package/src/utils/git-operations.ts +233 -0
- package/src/utils/git-status.ts +171 -0
- package/src/utils/github.ts +74 -0
- package/src/utils/health-checks.ts +359 -0
- package/src/utils/history.ts +140 -0
- package/src/utils/hooks.ts +92 -0
- package/src/utils/logger.ts +37 -0
- package/src/utils/permission-sync.test.ts +593 -0
- package/src/utils/permission-sync.ts +720 -0
- package/src/utils/pi-extensions.test.ts +182 -0
- package/src/utils/pi-extensions.ts +226 -0
- package/src/utils/progress.ts +73 -0
- package/src/utils/prompts.test.ts +169 -0
- package/src/utils/prompts.ts +296 -0
- package/src/utils/repo-resolver.test.ts +313 -0
- package/src/utils/repo-resolver.ts +294 -0
- package/src/utils/retry.test.ts +124 -0
- package/src/utils/retry.ts +85 -0
- package/src/utils/run-operations.ts +38 -0
- package/src/utils/suite.test.ts +256 -0
- package/src/utils/suite.ts +144 -0
- package/src/utils/sync-operations.ts +87 -0
- package/src/utils/tmux-dashboard.ts +193 -0
- package/src/utils/validation.test.ts +139 -0
- package/src/utils/validation.ts +128 -0
- package/src/utils/version-check.test.ts +108 -0
- package/src/utils/version-check.ts +96 -0
- package/src/utils/workspace-meta.ts +173 -0
- package/sync-permissions.sh +137 -0
- package/tsconfig.json +20 -0
- package/uninstall.sh +162 -0
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import fuzzy from 'fuzzy';
|
|
5
|
+
import { resolveRepoNames } from '../utils/repo-resolver';
|
|
6
|
+
import { getCloneUrl } from '../utils/config';
|
|
7
|
+
import { formatContextFile, appendToContextFile } from '../utils/context-file';
|
|
8
|
+
import { listWorkspaces, loadMetadata, createMetadata, saveMetadata, archiveWorkspace, unarchiveWorkspace } from '../utils/workspace-meta';
|
|
9
|
+
import { getAllReposStatus } from '../utils/git-status';
|
|
10
|
+
import { getRepoDiff, RepoDiff } from '../utils/diff-operations';
|
|
11
|
+
import { syncRepository, SyncResult } from '../utils/sync-operations';
|
|
12
|
+
import { runInRepo, RunResult } from '../utils/run-operations';
|
|
13
|
+
import { listSuites, getSuite, saveSuite, deleteSuite, exportSuite, exportAllSuites, importSuites, validateSuiteName } from '../utils/suite';
|
|
14
|
+
import { fetchOrgRepos } from '../utils/github';
|
|
15
|
+
import { cloneRepositories, reportCloneResults } from '../utils/git-operations';
|
|
16
|
+
import { generateClaudeContext } from '../utils/claude-integration';
|
|
17
|
+
import { runAllHealthChecks, calculateHealthScore } from '../utils/health-checks';
|
|
18
|
+
import { analyzeDependencies, detectCircularDependencies } from '../utils/dependency-analyzer';
|
|
19
|
+
import { createBranch, switchBranch, mergeBranch, rebaseBranch } from '../utils/branch-operations';
|
|
20
|
+
import { removeNodeModules, removeBuildArtifacts } from '../utils/cleanup-operations';
|
|
21
|
+
import { runPostCloneHooks } from '../utils/hooks';
|
|
22
|
+
import { SuiteEntry, WorkspaceSuite, SuitesStore } from '../types';
|
|
23
|
+
import { resolveWorkspaceNameConflict, sanitizeWorkspaceName, safeWorkspacePath } from '../utils/validation';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Redirects stdout to stderr for the duration of a function call.
|
|
27
|
+
* MCP uses stdout exclusively for JSON-RPC, so any console.log from
|
|
28
|
+
* utility functions (logInfo, logSuccess, etc.) would corrupt the protocol.
|
|
29
|
+
*/
|
|
30
|
+
async function withStdoutProtection<T>(fn: () => Promise<T>): Promise<T> {
|
|
31
|
+
const originalWrite = process.stdout.write;
|
|
32
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
33
|
+
try {
|
|
34
|
+
return await fn();
|
|
35
|
+
} finally {
|
|
36
|
+
process.stdout.write = originalWrite;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function handleListWorkspaces(includeArchived: boolean = false) {
|
|
41
|
+
return withStdoutProtection(async () => {
|
|
42
|
+
const workspaces = await listWorkspaces(includeArchived);
|
|
43
|
+
return workspaces.map(ws => ({
|
|
44
|
+
name: ws.name,
|
|
45
|
+
path: ws.path,
|
|
46
|
+
repoCount: ws.metadata?.repositories.filter(r => r.status === 'success').length ?? 0,
|
|
47
|
+
createdAt: ws.metadata?.createdAt ?? null,
|
|
48
|
+
archivedAt: ws.metadata?.archivedAt ?? null,
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function handleArchiveWorkspace(workspace: string) {
|
|
54
|
+
return withStdoutProtection(async () => {
|
|
55
|
+
await archiveWorkspace(workspace);
|
|
56
|
+
return {
|
|
57
|
+
workspace,
|
|
58
|
+
status: 'archived',
|
|
59
|
+
message: `Workspace "${workspace}" has been archived. It will be auto-deleted in 30 days.`,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function handleUnarchiveWorkspace(workspace: string) {
|
|
65
|
+
return withStdoutProtection(async () => {
|
|
66
|
+
await unarchiveWorkspace(workspace);
|
|
67
|
+
return {
|
|
68
|
+
workspace,
|
|
69
|
+
status: 'active',
|
|
70
|
+
message: `Workspace "${workspace}" has been unarchived and is now active.`,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function handleWorkspaceStatus(workspace: string) {
|
|
76
|
+
return withStdoutProtection(async () => {
|
|
77
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
78
|
+
const metadata = await loadMetadata(workspacePath);
|
|
79
|
+
|
|
80
|
+
if (!metadata) {
|
|
81
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
85
|
+
const repoNames = repos.map(r => r.directoryName);
|
|
86
|
+
const statuses = await getAllReposStatus(workspacePath, repoNames);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
workspace,
|
|
90
|
+
repos: statuses,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function handleWorkspaceDiff(workspace: string, full: boolean = false) {
|
|
96
|
+
return withStdoutProtection(async () => {
|
|
97
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
98
|
+
const metadata = await loadMetadata(workspacePath);
|
|
99
|
+
|
|
100
|
+
if (!metadata) {
|
|
101
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
105
|
+
const diffs: RepoDiff[] = [];
|
|
106
|
+
|
|
107
|
+
for (const repo of repos) {
|
|
108
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
109
|
+
const diff = await getRepoDiff(repoPath, repo.directoryName);
|
|
110
|
+
diffs.push(diff);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const totalStaged = diffs.reduce((sum, d) => sum + d.staged.files, 0);
|
|
114
|
+
const totalUnstaged = diffs.reduce((sum, d) => sum + d.unstaged.files, 0);
|
|
115
|
+
const reposWithChanges = diffs.filter(d => d.staged.files > 0 || d.unstaged.files > 0).length;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
workspace,
|
|
119
|
+
diffs,
|
|
120
|
+
summary: { totalStaged, totalUnstaged, reposWithChanges },
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function handleWorkspaceSync(workspace: string) {
|
|
126
|
+
return withStdoutProtection(async () => {
|
|
127
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
128
|
+
const metadata = await loadMetadata(workspacePath);
|
|
129
|
+
|
|
130
|
+
if (!metadata) {
|
|
131
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
135
|
+
const results: SyncResult[] = [];
|
|
136
|
+
|
|
137
|
+
for (const repo of repos) {
|
|
138
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
139
|
+
const result = await syncRepository(repoPath, repo.directoryName, true);
|
|
140
|
+
results.push(result);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const successful = results.filter(r => r.status === 'success').length;
|
|
144
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
145
|
+
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
workspace,
|
|
149
|
+
results,
|
|
150
|
+
summary: { successful, failed, skipped },
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function handleRunCommand(workspace: string, command: string) {
|
|
156
|
+
return withStdoutProtection(async () => {
|
|
157
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
158
|
+
const metadata = await loadMetadata(workspacePath);
|
|
159
|
+
|
|
160
|
+
if (!metadata) {
|
|
161
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
165
|
+
const results: RunResult[] = [];
|
|
166
|
+
const concurrency = 3;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < repos.length; i += concurrency) {
|
|
169
|
+
const chunk = repos.slice(i, i + concurrency);
|
|
170
|
+
const chunkResults = await Promise.all(
|
|
171
|
+
chunk.map(repo => {
|
|
172
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
173
|
+
return runInRepo(repoPath, repo.directoryName, command);
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
results.push(...chunkResults);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const successful = results.filter(r => r.status === 'success').length;
|
|
180
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
workspace,
|
|
184
|
+
command,
|
|
185
|
+
results,
|
|
186
|
+
summary: { successful, failed },
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function handleListSuites() {
|
|
192
|
+
return withStdoutProtection(async () => {
|
|
193
|
+
const suites = await listSuites();
|
|
194
|
+
return suites.map(s => ({
|
|
195
|
+
name: s.name,
|
|
196
|
+
description: s.description,
|
|
197
|
+
repoCount: s.entries.length,
|
|
198
|
+
hasHooks: (s.postCloneHooks?.length ?? 0) > 0,
|
|
199
|
+
createdAt: s.createdAt,
|
|
200
|
+
updatedAt: s.updatedAt,
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function handleWorkspaceInfo(workspace: string) {
|
|
206
|
+
return withStdoutProtection(async () => {
|
|
207
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
208
|
+
const metadata = await loadMetadata(workspacePath);
|
|
209
|
+
|
|
210
|
+
if (!metadata) {
|
|
211
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
...metadata,
|
|
216
|
+
path: workspacePath,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function handleUpdateWorkspace(workspace: string, repos: Array<string | { name: string; suffix: string }>) {
|
|
222
|
+
return withStdoutProtection(async () => {
|
|
223
|
+
if (!workspace || workspace.trim().length === 0) {
|
|
224
|
+
throw new Error('Workspace name is required');
|
|
225
|
+
}
|
|
226
|
+
if (!repos || repos.length === 0) {
|
|
227
|
+
throw new Error('At least one repository name is required');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
231
|
+
const metadata = await loadMetadata(workspacePath);
|
|
232
|
+
|
|
233
|
+
if (!metadata) {
|
|
234
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const existingDirectoryNames = metadata.repositories.map(r => r.directoryName);
|
|
238
|
+
|
|
239
|
+
// Fetch all repos to resolve names to full repo objects
|
|
240
|
+
const allRepos = await fetchOrgRepos();
|
|
241
|
+
const repoEntries: Array<{ repo: typeof allRepos[0]; directoryName: string }> = [];
|
|
242
|
+
const notFound: string[] = [];
|
|
243
|
+
const alreadyExists: string[] = [];
|
|
244
|
+
const fuzzyMatched: Array<{ requested: string; resolved: string }> = [];
|
|
245
|
+
|
|
246
|
+
for (const repoInput of repos) {
|
|
247
|
+
const repoName = typeof repoInput === 'string' ? repoInput : repoInput.name;
|
|
248
|
+
const suffix = typeof repoInput === 'string' ? undefined : repoInput.suffix;
|
|
249
|
+
|
|
250
|
+
const { resolved, notFound: missing } = resolveRepoNames([repoName], allRepos);
|
|
251
|
+
if (missing.length > 0) {
|
|
252
|
+
notFound.push(repoName);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const found = resolved[0].repo;
|
|
256
|
+
if (!resolved[0].exact) {
|
|
257
|
+
fuzzyMatched.push({ requested: repoName, resolved: found.name });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (suffix && !/^[a-zA-Z0-9_-]+$/.test(suffix)) {
|
|
261
|
+
throw new Error(`Invalid suffix "${suffix}" for repo "${repoName}". Only alphanumeric characters, hyphens, and underscores are allowed.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const directoryName = suffix ? `${found.name}-${suffix}` : found.name;
|
|
265
|
+
|
|
266
|
+
if (existingDirectoryNames.includes(directoryName)) {
|
|
267
|
+
alreadyExists.push(suffix ? `${repoName} (as ${directoryName})` : repoName);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
existingDirectoryNames.push(directoryName);
|
|
272
|
+
repoEntries.push({ repo: found, directoryName });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (repoEntries.length === 0) {
|
|
276
|
+
const reasons: string[] = [];
|
|
277
|
+
if (notFound.length > 0) reasons.push(`not found: ${notFound.join(', ')}`);
|
|
278
|
+
if (alreadyExists.length > 0) reasons.push(`already in workspace: ${alreadyExists.join(', ')}`);
|
|
279
|
+
throw new Error(`No new repos to add (${reasons.join('; ')})`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Clone new repositories
|
|
283
|
+
const cloneResults = await cloneRepositories(repoEntries, workspacePath);
|
|
284
|
+
|
|
285
|
+
// Update metadata
|
|
286
|
+
const newRepoMetadata = cloneResults.map(result => ({
|
|
287
|
+
name: result.repo.name,
|
|
288
|
+
directoryName: result.directoryName,
|
|
289
|
+
owner: result.repo.owner.login,
|
|
290
|
+
clonedAt: result.clonedAt || new Date().toISOString(),
|
|
291
|
+
cloneUrl: getCloneUrl(result.repo),
|
|
292
|
+
status: result.status as 'success' | 'failed',
|
|
293
|
+
error: result.error,
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
metadata.repositories.push(...newRepoMetadata);
|
|
297
|
+
metadata.lastModified = new Date().toISOString();
|
|
298
|
+
await saveMetadata(workspacePath, metadata);
|
|
299
|
+
|
|
300
|
+
const successful = cloneResults.filter(r => r.status === 'success');
|
|
301
|
+
const failed = cloneResults.filter(r => r.status === 'failed');
|
|
302
|
+
|
|
303
|
+
// Regenerate the agent context (CLAUDE.md/AGENTS.md) + .mcp.json so the
|
|
304
|
+
// newly added repos surface — otherwise an investigate-first workspace's
|
|
305
|
+
// context keeps saying "0 repositories". Resolve the FULL set (existing +
|
|
306
|
+
// new) from the catalog so pre-existing repos keep their descriptions/URLs
|
|
307
|
+
// on regenerate (matches the remove-repo path). Best-effort.
|
|
308
|
+
const contextRepoObjects = metadata.repositories
|
|
309
|
+
.filter(r => r.status === 'success')
|
|
310
|
+
.map(r => allRepos.find(ar => ar.name === r.name))
|
|
311
|
+
.filter((r): r is NonNullable<typeof r> => r != null);
|
|
312
|
+
// Only regenerate when this update actually added a repo — a failure-only
|
|
313
|
+
// update (all new clones failed) must not rewrite the existing context.
|
|
314
|
+
try {
|
|
315
|
+
if (successful.length > 0 && contextRepoObjects.length > 0) {
|
|
316
|
+
await generateClaudeContext(workspacePath, workspace, contextRepoObjects, metadata);
|
|
317
|
+
}
|
|
318
|
+
} catch { /* context regen is best-effort */ }
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
workspace,
|
|
322
|
+
path: workspacePath,
|
|
323
|
+
added: cloneResults.map(r => ({
|
|
324
|
+
name: r.repo.name,
|
|
325
|
+
directoryName: r.directoryName,
|
|
326
|
+
status: r.status,
|
|
327
|
+
error: r.error,
|
|
328
|
+
})),
|
|
329
|
+
summary: {
|
|
330
|
+
successful: successful.length,
|
|
331
|
+
failed: failed.length,
|
|
332
|
+
totalRepos: metadata.repositories.filter(r => r.status === 'success').length,
|
|
333
|
+
},
|
|
334
|
+
alreadyExists: alreadyExists.length > 0 ? alreadyExists : undefined,
|
|
335
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
336
|
+
fuzzyMatched: fuzzyMatched.length > 0 ? fuzzyMatched : undefined,
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function handleUpdateCache() {
|
|
342
|
+
return withStdoutProtection(async () => {
|
|
343
|
+
const repos = await fetchOrgRepos({ forceRefresh: true });
|
|
344
|
+
return {
|
|
345
|
+
repos: repos.length,
|
|
346
|
+
message: `Cache updated with ${repos.length} repositories`,
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function handleCreateWorkspace(workspace: string, repos: string[]) {
|
|
352
|
+
return withStdoutProtection(async () => {
|
|
353
|
+
if (!workspace || workspace.trim().length === 0) {
|
|
354
|
+
throw new Error('Workspace name is required');
|
|
355
|
+
}
|
|
356
|
+
if (!repos || repos.length === 0) {
|
|
357
|
+
throw new Error('At least one repository name is required');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const sanitizedWorkspace = sanitizeWorkspaceName(workspace);
|
|
361
|
+
const workspacePath = safeWorkspacePath(sanitizedWorkspace);
|
|
362
|
+
|
|
363
|
+
// Auto-resolve workspace name conflict instead of throwing
|
|
364
|
+
let finalWorkspaceName = sanitizedWorkspace;
|
|
365
|
+
let wsExists = false;
|
|
366
|
+
try {
|
|
367
|
+
await fs.access(workspacePath);
|
|
368
|
+
wsExists = true;
|
|
369
|
+
} catch {
|
|
370
|
+
// Directory doesn't exist — good, use the original name
|
|
371
|
+
}
|
|
372
|
+
if (wsExists) {
|
|
373
|
+
// Workspace already exists — resolve to a unique name using repo names as hints
|
|
374
|
+
// (errors from resolveWorkspaceNameConflict intentionally propagate)
|
|
375
|
+
finalWorkspaceName = await resolveWorkspaceNameConflict(sanitizedWorkspace, repos);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Fetch all repos to resolve names to full repo objects
|
|
379
|
+
const allRepos = await fetchOrgRepos();
|
|
380
|
+
const repoEntries: Array<{ repo: typeof allRepos[0]; directoryName: string }> = [];
|
|
381
|
+
const notFound: string[] = [];
|
|
382
|
+
|
|
383
|
+
const { resolved, notFound: missing } = resolveRepoNames(repos, allRepos);
|
|
384
|
+
notFound.push(...missing);
|
|
385
|
+
const fuzzyMatched: Array<{ requested: string; resolved: string }> = [];
|
|
386
|
+
for (const r of resolved) {
|
|
387
|
+
repoEntries.push({ repo: r.repo, directoryName: r.repo.name });
|
|
388
|
+
if (!r.exact) fuzzyMatched.push({ requested: r.input, resolved: r.repo.name });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (repoEntries.length === 0) {
|
|
392
|
+
throw new Error(`None of the specified repos were found: ${notFound.join(', ')}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Create workspace directory
|
|
396
|
+
const finalWorkspacePath = safeWorkspacePath(finalWorkspaceName);
|
|
397
|
+
await fs.mkdir(finalWorkspacePath, { recursive: true });
|
|
398
|
+
|
|
399
|
+
// Clone repositories
|
|
400
|
+
const cloneResults = await cloneRepositories(repoEntries, finalWorkspacePath);
|
|
401
|
+
|
|
402
|
+
// Save metadata
|
|
403
|
+
const metadata = createMetadata(finalWorkspaceName, cloneResults);
|
|
404
|
+
await saveMetadata(finalWorkspacePath, metadata);
|
|
405
|
+
|
|
406
|
+
// Generate Claude context
|
|
407
|
+
const successfulRepos = cloneResults
|
|
408
|
+
.filter(r => r.status === 'success')
|
|
409
|
+
.map(r => r.repo);
|
|
410
|
+
|
|
411
|
+
if (successfulRepos.length > 0) {
|
|
412
|
+
await generateClaudeContext(finalWorkspacePath, finalWorkspaceName, successfulRepos, metadata);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const successful = cloneResults.filter(r => r.status === 'success');
|
|
416
|
+
const failed = cloneResults.filter(r => r.status === 'failed');
|
|
417
|
+
|
|
418
|
+
// Write workspace path to temp file so shell integration can cd to it
|
|
419
|
+
if (successful.length > 0) {
|
|
420
|
+
const tempFile = path.join(os.homedir(), '.workspace-last-created');
|
|
421
|
+
await fs.writeFile(tempFile, finalWorkspacePath, 'utf-8').catch(() => {});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
workspace: finalWorkspaceName,
|
|
426
|
+
originalWorkspaceName: finalWorkspaceName !== sanitizedWorkspace ? sanitizedWorkspace : undefined,
|
|
427
|
+
path: finalWorkspacePath,
|
|
428
|
+
repos: cloneResults.map(r => ({
|
|
429
|
+
name: r.repo.name,
|
|
430
|
+
directoryName: r.directoryName,
|
|
431
|
+
status: r.status,
|
|
432
|
+
error: r.error,
|
|
433
|
+
})),
|
|
434
|
+
summary: {
|
|
435
|
+
successful: successful.length,
|
|
436
|
+
failed: failed.length,
|
|
437
|
+
},
|
|
438
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
439
|
+
fuzzyMatched: fuzzyMatched.length > 0 ? fuzzyMatched : undefined,
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function handleSearchRepos(query: string, limit: number = 20) {
|
|
445
|
+
return withStdoutProtection(async () => {
|
|
446
|
+
const allRepos = await fetchOrgRepos();
|
|
447
|
+
const results = fuzzy.filter(query, allRepos, {
|
|
448
|
+
extract: (repo: typeof allRepos[0]) => `${repo.name} ${repo.description || ''}`,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return results.slice(0, limit).map(r => ({
|
|
452
|
+
name: r.original.name,
|
|
453
|
+
description: r.original.description,
|
|
454
|
+
url: r.original.url,
|
|
455
|
+
isPrivate: r.original.isPrivate,
|
|
456
|
+
score: r.score,
|
|
457
|
+
}));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function handleListOrgRepos(forceRefresh: boolean = false) {
|
|
462
|
+
return withStdoutProtection(async () => {
|
|
463
|
+
const repos = await fetchOrgRepos({ forceRefresh });
|
|
464
|
+
return {
|
|
465
|
+
repos: repos.map(r => ({
|
|
466
|
+
name: r.name,
|
|
467
|
+
description: r.description,
|
|
468
|
+
url: r.url,
|
|
469
|
+
isPrivate: r.isPrivate,
|
|
470
|
+
})),
|
|
471
|
+
total: repos.length,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function handleDeleteWorkspace(workspaces: string | string[]) {
|
|
477
|
+
return withStdoutProtection(async () => {
|
|
478
|
+
const names = Array.isArray(workspaces) ? workspaces : [workspaces];
|
|
479
|
+
const results: Array<{ workspace: string; path: string; deleted: boolean; error?: string }> = [];
|
|
480
|
+
|
|
481
|
+
for (const workspace of names) {
|
|
482
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await fs.access(workspacePath);
|
|
486
|
+
} catch {
|
|
487
|
+
results.push({ workspace, path: workspacePath, deleted: false, error: `Workspace directory not found: ${workspace}` });
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
493
|
+
results.push({ workspace, path: workspacePath, deleted: true });
|
|
494
|
+
} catch (err) {
|
|
495
|
+
results.push({ workspace, path: workspacePath, deleted: false, error: err instanceof Error ? err.message : 'Unknown error' });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (names.length === 1) {
|
|
500
|
+
const r = results[0];
|
|
501
|
+
if (!r.deleted) throw new Error(r.error!);
|
|
502
|
+
return { deleted: r.workspace, path: r.path };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { results };
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function handleRemoveRepo(workspace: string, repo: string) {
|
|
510
|
+
return withStdoutProtection(async () => {
|
|
511
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
512
|
+
const metadata = await loadMetadata(workspacePath);
|
|
513
|
+
|
|
514
|
+
if (!metadata) {
|
|
515
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const repoIndex = metadata.repositories.findIndex(r => r.directoryName === repo);
|
|
519
|
+
if (repoIndex === -1) {
|
|
520
|
+
throw new Error(`Repository not found in workspace: ${repo}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const repoPath = path.join(workspacePath, repo);
|
|
524
|
+
await fs.rm(repoPath, { recursive: true, force: true });
|
|
525
|
+
|
|
526
|
+
metadata.repositories.splice(repoIndex, 1);
|
|
527
|
+
await saveMetadata(workspacePath, metadata);
|
|
528
|
+
|
|
529
|
+
const successfulRepos = metadata.repositories
|
|
530
|
+
.filter(r => r.status === 'success');
|
|
531
|
+
|
|
532
|
+
// Best-effort regeneration of Claude context
|
|
533
|
+
try {
|
|
534
|
+
if (successfulRepos.length > 0) {
|
|
535
|
+
const allRepos = await fetchOrgRepos();
|
|
536
|
+
const repoObjects = successfulRepos
|
|
537
|
+
.map(r => allRepos.find(ar => ar.name === r.name))
|
|
538
|
+
.filter((r): r is NonNullable<typeof r> => r != null);
|
|
539
|
+
|
|
540
|
+
if (repoObjects.length > 0) {
|
|
541
|
+
await generateClaudeContext(workspacePath, workspace, repoObjects, metadata);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
// Claude context regeneration is non-critical
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
removed: repo,
|
|
550
|
+
workspace,
|
|
551
|
+
remainingRepos: successfulRepos.length,
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export async function handleWorkspaceDoctor(workspace: string) {
|
|
557
|
+
return withStdoutProtection(async () => {
|
|
558
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
559
|
+
const metadata = await loadMetadata(workspacePath);
|
|
560
|
+
|
|
561
|
+
if (!metadata) {
|
|
562
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const results = await runAllHealthChecks(workspacePath, metadata);
|
|
566
|
+
const score = calculateHealthScore(results);
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
workspace,
|
|
570
|
+
score,
|
|
571
|
+
checks: results,
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export async function handleAnalyzeDeps(workspace: string) {
|
|
577
|
+
return withStdoutProtection(async () => {
|
|
578
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
579
|
+
const metadata = await loadMetadata(workspacePath);
|
|
580
|
+
|
|
581
|
+
if (!metadata) {
|
|
582
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
586
|
+
const repoNames = repos.map(r => r.directoryName);
|
|
587
|
+
|
|
588
|
+
const analyses = await analyzeDependencies(workspacePath, repoNames);
|
|
589
|
+
const circularDeps = detectCircularDependencies(analyses);
|
|
590
|
+
|
|
591
|
+
// Convert Map to plain object for JSON serialization
|
|
592
|
+
const analysesObj: Record<string, {
|
|
593
|
+
repoName: string;
|
|
594
|
+
dependencies: string[];
|
|
595
|
+
dependents: string[];
|
|
596
|
+
missingDependencies: string[];
|
|
597
|
+
}> = {};
|
|
598
|
+
for (const [key, value] of analyses) {
|
|
599
|
+
analysesObj[key] = value;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
workspace,
|
|
604
|
+
analyses: analysesObj,
|
|
605
|
+
circularDependencies: circularDeps,
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function handleBranchCreate(workspace: string, branchName: string, baseBranch?: string) {
|
|
611
|
+
return withStdoutProtection(async () => {
|
|
612
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
613
|
+
const metadata = await loadMetadata(workspacePath);
|
|
614
|
+
|
|
615
|
+
if (!metadata) {
|
|
616
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
620
|
+
const results = [];
|
|
621
|
+
|
|
622
|
+
for (const repo of repos) {
|
|
623
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
624
|
+
const result = await createBranch(repoPath, repo.directoryName, branchName, baseBranch);
|
|
625
|
+
results.push(result);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const successful = results.filter(r => r.success).length;
|
|
629
|
+
const failed = results.filter(r => !r.success).length;
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
workspace,
|
|
633
|
+
branchName,
|
|
634
|
+
baseBranch: baseBranch || 'current',
|
|
635
|
+
results,
|
|
636
|
+
summary: { successful, failed },
|
|
637
|
+
};
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function handleSwitchBranch(workspace: string, branch: string) {
|
|
642
|
+
return withStdoutProtection(async () => {
|
|
643
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
644
|
+
const metadata = await loadMetadata(workspacePath);
|
|
645
|
+
|
|
646
|
+
if (!metadata) {
|
|
647
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
651
|
+
const results = [];
|
|
652
|
+
|
|
653
|
+
for (const repo of repos) {
|
|
654
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
655
|
+
const result = await switchBranch(repoPath, repo.directoryName, branch);
|
|
656
|
+
results.push(result);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const successful = results.filter(r => r.status === 'success').length;
|
|
660
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
661
|
+
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
workspace,
|
|
665
|
+
branch,
|
|
666
|
+
results,
|
|
667
|
+
summary: { successful, failed, skipped },
|
|
668
|
+
};
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export async function handleWorkspaceCleanup(
|
|
673
|
+
workspace: string,
|
|
674
|
+
includeNodeModules: boolean = true,
|
|
675
|
+
includeBuildArtifacts: boolean = true
|
|
676
|
+
) {
|
|
677
|
+
return withStdoutProtection(async () => {
|
|
678
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
679
|
+
const metadata = await loadMetadata(workspacePath);
|
|
680
|
+
|
|
681
|
+
if (!metadata) {
|
|
682
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
686
|
+
const results: Array<{
|
|
687
|
+
repo: string;
|
|
688
|
+
nodeModules?: import('../utils/cleanup-operations').CleanupResult;
|
|
689
|
+
buildArtifacts?: import('../utils/cleanup-operations').CleanupResult;
|
|
690
|
+
}> = [];
|
|
691
|
+
|
|
692
|
+
for (const repo of repos) {
|
|
693
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
694
|
+
const repoResult: typeof results[0] = { repo: repo.directoryName };
|
|
695
|
+
|
|
696
|
+
if (includeNodeModules) {
|
|
697
|
+
repoResult.nodeModules = await removeNodeModules(repoPath);
|
|
698
|
+
}
|
|
699
|
+
if (includeBuildArtifacts) {
|
|
700
|
+
repoResult.buildArtifacts = await removeBuildArtifacts(repoPath);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
results.push(repoResult);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Calculate total space freed
|
|
707
|
+
let totalFreedMB = 0;
|
|
708
|
+
for (const r of results) {
|
|
709
|
+
if (r.nodeModules) {
|
|
710
|
+
totalFreedMB += parseFloat(r.nodeModules.spaceFreed) || 0;
|
|
711
|
+
}
|
|
712
|
+
if (r.buildArtifacts) {
|
|
713
|
+
totalFreedMB += parseFloat(r.buildArtifacts.spaceFreed) || 0;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
workspace,
|
|
719
|
+
results,
|
|
720
|
+
totalSpaceFreed: `${totalFreedMB.toFixed(2)} MB`,
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// --- Branch merge/rebase ---
|
|
726
|
+
|
|
727
|
+
export async function handleBranchMerge(
|
|
728
|
+
workspace: string,
|
|
729
|
+
sourceBranch: string,
|
|
730
|
+
targetBranch: string,
|
|
731
|
+
strategy?: 'no-ff' | 'ff-only' | 'squash'
|
|
732
|
+
) {
|
|
733
|
+
return withStdoutProtection(async () => {
|
|
734
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
735
|
+
const metadata = await loadMetadata(workspacePath);
|
|
736
|
+
|
|
737
|
+
if (!metadata) {
|
|
738
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
742
|
+
const results = [];
|
|
743
|
+
|
|
744
|
+
const options = strategy ? {
|
|
745
|
+
noFf: strategy === 'no-ff',
|
|
746
|
+
ffOnly: strategy === 'ff-only',
|
|
747
|
+
squash: strategy === 'squash',
|
|
748
|
+
} : undefined;
|
|
749
|
+
|
|
750
|
+
for (const repo of repos) {
|
|
751
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
752
|
+
const result = await mergeBranch(repoPath, repo.directoryName, sourceBranch, targetBranch, options);
|
|
753
|
+
results.push(result);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const successful = results.filter(r => r.success).length;
|
|
757
|
+
const failed = results.filter(r => !r.success).length;
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
workspace,
|
|
761
|
+
sourceBranch,
|
|
762
|
+
targetBranch,
|
|
763
|
+
strategy: strategy || 'default',
|
|
764
|
+
results,
|
|
765
|
+
summary: { successful, failed },
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export async function handleBranchRebase(workspace: string, targetBranch: string) {
|
|
771
|
+
return withStdoutProtection(async () => {
|
|
772
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
773
|
+
const metadata = await loadMetadata(workspacePath);
|
|
774
|
+
|
|
775
|
+
if (!metadata) {
|
|
776
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const repos = metadata.repositories.filter(r => r.status === 'success');
|
|
780
|
+
const results = [];
|
|
781
|
+
|
|
782
|
+
for (const repo of repos) {
|
|
783
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
784
|
+
const result = await rebaseBranch(repoPath, repo.directoryName, targetBranch);
|
|
785
|
+
results.push(result);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const successful = results.filter(r => r.success).length;
|
|
789
|
+
const failed = results.filter(r => !r.success).length;
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
workspace,
|
|
793
|
+
targetBranch,
|
|
794
|
+
results,
|
|
795
|
+
summary: { successful, failed },
|
|
796
|
+
};
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// --- Suite operations ---
|
|
801
|
+
|
|
802
|
+
export async function handleSuiteCreate(name: string, repos: string[], description?: string) {
|
|
803
|
+
return withStdoutProtection(async () => {
|
|
804
|
+
const validation = validateSuiteName(name);
|
|
805
|
+
if (validation !== true) {
|
|
806
|
+
throw new Error(`Invalid suite name: ${validation}`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!repos || repos.length === 0) {
|
|
810
|
+
throw new Error('At least one repository name is required');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const existing = await getSuite(name);
|
|
814
|
+
if (existing) {
|
|
815
|
+
throw new Error(`Suite already exists: ${name}. Delete it first or use a different name.`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const entries: SuiteEntry[] = repos.map(repoName => ({
|
|
819
|
+
repoName,
|
|
820
|
+
directoryName: repoName,
|
|
821
|
+
}));
|
|
822
|
+
|
|
823
|
+
const suite: WorkspaceSuite = {
|
|
824
|
+
name,
|
|
825
|
+
description: description || '',
|
|
826
|
+
entries,
|
|
827
|
+
createdAt: new Date().toISOString(),
|
|
828
|
+
updatedAt: new Date().toISOString(),
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
await saveSuite(suite);
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
name: suite.name,
|
|
835
|
+
description: suite.description,
|
|
836
|
+
repoCount: entries.length,
|
|
837
|
+
repos: repos,
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export async function handleSuiteDelete(name: string) {
|
|
843
|
+
return withStdoutProtection(async () => {
|
|
844
|
+
const deleted = await deleteSuite(name);
|
|
845
|
+
if (!deleted) {
|
|
846
|
+
throw new Error(`Suite not found: ${name}`);
|
|
847
|
+
}
|
|
848
|
+
return { deleted: name };
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export async function handleSuiteExport(name?: string) {
|
|
853
|
+
return withStdoutProtection(async () => {
|
|
854
|
+
if (name) {
|
|
855
|
+
const result = await exportSuite(name);
|
|
856
|
+
if (!result) {
|
|
857
|
+
throw new Error(`Suite not found: ${name}`);
|
|
858
|
+
}
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
return await exportAllSuites();
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function handleSuiteImport(content: string, overwrite: boolean = false) {
|
|
866
|
+
return withStdoutProtection(async () => {
|
|
867
|
+
let parsed: SuitesStore;
|
|
868
|
+
try {
|
|
869
|
+
parsed = JSON.parse(content);
|
|
870
|
+
} catch {
|
|
871
|
+
throw new Error('Invalid JSON content');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (!parsed.suites || !Array.isArray(parsed.suites)) {
|
|
875
|
+
throw new Error('Invalid suites format: expected { version: 1, suites: [...] }');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const result = await importSuites(parsed, overwrite);
|
|
879
|
+
return result;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export async function handleSuiteUse(suiteName: string, workspace: string) {
|
|
884
|
+
return withStdoutProtection(async () => {
|
|
885
|
+
const suite = await getSuite(suiteName);
|
|
886
|
+
if (!suite) {
|
|
887
|
+
throw new Error(`Suite not found: ${suiteName}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const sanitizedSuiteWorkspace = sanitizeWorkspaceName(workspace);
|
|
891
|
+
const workspacePath = safeWorkspacePath(sanitizedSuiteWorkspace);
|
|
892
|
+
|
|
893
|
+
// Auto-resolve workspace name conflict using suite repo names as hints
|
|
894
|
+
let finalSuiteWorkspaceName = sanitizedSuiteWorkspace;
|
|
895
|
+
let suiteWsExists = false;
|
|
896
|
+
try {
|
|
897
|
+
await fs.access(workspacePath);
|
|
898
|
+
suiteWsExists = true;
|
|
899
|
+
} catch {
|
|
900
|
+
// Directory doesn't exist — good
|
|
901
|
+
}
|
|
902
|
+
if (suiteWsExists) {
|
|
903
|
+
// Workspace already exists — resolve to a unique name
|
|
904
|
+
// (errors from resolveWorkspaceNameConflict intentionally propagate)
|
|
905
|
+
const suiteRepoNames = suite.entries.map(e => e.repoName);
|
|
906
|
+
finalSuiteWorkspaceName = await resolveWorkspaceNameConflict(sanitizedSuiteWorkspace, suiteRepoNames);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Resolve suite entries to GitHub repos
|
|
910
|
+
const allRepos = await fetchOrgRepos();
|
|
911
|
+
const repoEntries: Array<{ repo: typeof allRepos[0]; directoryName: string }> = [];
|
|
912
|
+
const notFound: string[] = [];
|
|
913
|
+
|
|
914
|
+
for (const entry of suite.entries) {
|
|
915
|
+
const found = allRepos.find(r => r.name.toLowerCase() === entry.repoName.toLowerCase());
|
|
916
|
+
if (found) {
|
|
917
|
+
repoEntries.push({ repo: found, directoryName: entry.directoryName });
|
|
918
|
+
} else {
|
|
919
|
+
notFound.push(entry.repoName);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (repoEntries.length === 0) {
|
|
924
|
+
throw new Error(`None of the suite repos were found: ${notFound.join(', ')}`);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Create workspace directory
|
|
928
|
+
const finalSuiteWorkspacePath = safeWorkspacePath(finalSuiteWorkspaceName);
|
|
929
|
+
await fs.mkdir(finalSuiteWorkspacePath, { recursive: true });
|
|
930
|
+
|
|
931
|
+
// Clone repositories
|
|
932
|
+
const cloneResults = await cloneRepositories(repoEntries, finalSuiteWorkspacePath);
|
|
933
|
+
|
|
934
|
+
// Save metadata
|
|
935
|
+
const metadata = createMetadata(finalSuiteWorkspaceName, cloneResults);
|
|
936
|
+
await saveMetadata(finalSuiteWorkspacePath, metadata);
|
|
937
|
+
|
|
938
|
+
// Generate Claude context
|
|
939
|
+
const successfulRepos = cloneResults
|
|
940
|
+
.filter(r => r.status === 'success')
|
|
941
|
+
.map(r => r.repo);
|
|
942
|
+
|
|
943
|
+
if (successfulRepos.length > 0) {
|
|
944
|
+
await generateClaudeContext(finalSuiteWorkspacePath, finalSuiteWorkspaceName, successfulRepos, metadata);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Run post-clone hooks if suite has them
|
|
948
|
+
if (suite.postCloneHooks && suite.postCloneHooks.length > 0) {
|
|
949
|
+
try {
|
|
950
|
+
const repoMetadata = metadata.repositories.filter(r => r.status === 'success');
|
|
951
|
+
await runPostCloneHooks(finalSuiteWorkspacePath, repoMetadata, suite.postCloneHooks);
|
|
952
|
+
} catch {
|
|
953
|
+
// Post-clone hooks are non-critical
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const successful = cloneResults.filter(r => r.status === 'success');
|
|
958
|
+
const failed = cloneResults.filter(r => r.status === 'failed');
|
|
959
|
+
|
|
960
|
+
return {
|
|
961
|
+
workspace: finalSuiteWorkspaceName,
|
|
962
|
+
originalWorkspaceName: finalSuiteWorkspaceName !== sanitizedSuiteWorkspace ? sanitizedSuiteWorkspace : undefined,
|
|
963
|
+
suite: suiteName,
|
|
964
|
+
path: finalSuiteWorkspacePath,
|
|
965
|
+
repos: cloneResults.map(r => ({
|
|
966
|
+
name: r.repo.name,
|
|
967
|
+
directoryName: r.directoryName,
|
|
968
|
+
status: r.status,
|
|
969
|
+
error: r.error,
|
|
970
|
+
})),
|
|
971
|
+
summary: { successful: successful.length, failed: failed.length },
|
|
972
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
export async function handleSaveContext(workspace: string, content: string, append?: boolean) {
|
|
979
|
+
return withStdoutProtection(async () => {
|
|
980
|
+
if (!workspace || workspace.trim().length === 0) {
|
|
981
|
+
throw new Error('Workspace name is required');
|
|
982
|
+
}
|
|
983
|
+
if (!content || content.trim().length === 0) {
|
|
984
|
+
throw new Error('Content is required');
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const workspacePath = safeWorkspacePath(workspace);
|
|
988
|
+
const metadata = await loadMetadata(workspacePath);
|
|
989
|
+
if (!metadata) {
|
|
990
|
+
throw new Error(`Workspace not found: ${workspace}`);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const contextPath = path.join(workspacePath, 'CONTEXT.md');
|
|
994
|
+
const timestamp = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, 'Z');
|
|
995
|
+
|
|
996
|
+
let fileContent: string;
|
|
997
|
+
if (append) {
|
|
998
|
+
let existing: string | null = null;
|
|
999
|
+
try {
|
|
1000
|
+
existing = await fs.readFile(contextPath, 'utf-8');
|
|
1001
|
+
} catch {
|
|
1002
|
+
// File doesn't exist yet
|
|
1003
|
+
}
|
|
1004
|
+
fileContent = appendToContextFile(existing, workspace, content, timestamp);
|
|
1005
|
+
} else {
|
|
1006
|
+
fileContent = formatContextFile(workspace, content, timestamp);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
await fs.writeFile(contextPath, fileContent, 'utf-8');
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
saved: true,
|
|
1013
|
+
path: contextPath,
|
|
1014
|
+
workspace,
|
|
1015
|
+
append: append ?? false,
|
|
1016
|
+
timestamp,
|
|
1017
|
+
};
|
|
1018
|
+
});
|
|
1019
|
+
}
|