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