@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,196 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { WORKSPACES_DIR } from '../utils/config';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { verifyGhAuth, fetchOrgRepos, displayAuthInstructions } from '../utils/github';
|
|
5
|
+
import { promptRepositorySelection, promptWorkspaceName, confirmWorkspaceCreation, type RepoSelection } from '../utils/prompts';
|
|
6
|
+
import { cloneRepositories, reportCloneResults } from '../utils/git-operations';
|
|
7
|
+
import { warnIfGhqMissing } from '../utils/ghq-integration';
|
|
8
|
+
import { createMetadata, saveMetadata } from '../utils/workspace-meta';
|
|
9
|
+
import { generateClaudeContext } from '../utils/claude-integration';
|
|
10
|
+
import { logInfo, logSuccess, logError, logStep } from '../utils/logger';
|
|
11
|
+
import { colorize } from '../utils/colors';
|
|
12
|
+
import { printBanner } from '../utils/banner';
|
|
13
|
+
import { validateWorkspaceName, checkWorkspaceExists, sanitizeWorkspaceName, resolveWorkspaceNameConflict } from '../utils/validation';
|
|
14
|
+
import { getGlobalOpts, parseList } from '../utils/command-helpers';
|
|
15
|
+
import { resolveRepoSpecs } from '../utils/repo-resolver';
|
|
16
|
+
|
|
17
|
+
export function registerCreateCommand(parent: Command) {
|
|
18
|
+
parent
|
|
19
|
+
.command('create')
|
|
20
|
+
.alias('c')
|
|
21
|
+
.description('Create a new workspace')
|
|
22
|
+
.option('-w, --workspace <name>', 'Workspace name')
|
|
23
|
+
.option('-r, --repos <repos>', 'Comma-separated repository names. Use repo:suffix to add the same repo more than once under a separate folder, e.g. casper:cas-101 -> casper-cas-101')
|
|
24
|
+
.option('--prompt <prompt>', 'Original prompt that triggered workspace creation (saved to metadata)')
|
|
25
|
+
.option('--allow-empty', 'Create the workspace with no repositories (e.g. investigate-first: the agent adds repos later)')
|
|
26
|
+
.action(async (opts, cmd) => {
|
|
27
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
28
|
+
await handleCreate({ ...opts, ...globalOpts });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function handleCreate(opts: {
|
|
33
|
+
workspace?: string;
|
|
34
|
+
repos?: string;
|
|
35
|
+
prompt?: string;
|
|
36
|
+
allowEmpty?: boolean;
|
|
37
|
+
forceRefresh: boolean;
|
|
38
|
+
yes: boolean;
|
|
39
|
+
}) {
|
|
40
|
+
printBanner();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Step 1: Verify gh CLI authentication
|
|
44
|
+
logStep(1, 6, 'Verifying GitHub CLI authentication...');
|
|
45
|
+
const isAuthenticated = await verifyGhAuth();
|
|
46
|
+
|
|
47
|
+
if (!isAuthenticated) {
|
|
48
|
+
logError('GitHub CLI is not authenticated');
|
|
49
|
+
displayAuthInstructions();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
logSuccess('GitHub CLI authenticated');
|
|
54
|
+
|
|
55
|
+
// Investigate-first empty workspace: no repos to resolve, so skip the org
|
|
56
|
+
// catalog fetch entirely — otherwise an empty/failed catalog would abort
|
|
57
|
+
// creation on the guard below even though we need no repos.
|
|
58
|
+
const investigateEmpty = !!opts.allowEmpty && !opts.repos;
|
|
59
|
+
|
|
60
|
+
// Step 2: Fetch repositories (only when we actually need to resolve repos)
|
|
61
|
+
let repos: Awaited<ReturnType<typeof fetchOrgRepos>> = [];
|
|
62
|
+
if (!investigateEmpty) {
|
|
63
|
+
logStep(2, 6, 'Fetching repositories...');
|
|
64
|
+
repos = await fetchOrgRepos({ forceRefresh: opts.forceRefresh });
|
|
65
|
+
|
|
66
|
+
if (repos.length === 0) {
|
|
67
|
+
logError('No repositories found');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 3: Select repositories
|
|
73
|
+
logStep(3, 6, 'Select repositories to clone...');
|
|
74
|
+
let selectedEntries: RepoSelection[];
|
|
75
|
+
|
|
76
|
+
if (investigateEmpty) {
|
|
77
|
+
// Investigate-first: start with no repos; the in-session agent discovers
|
|
78
|
+
// and adds the relevant repos itself.
|
|
79
|
+
logInfo('Creating an empty workspace (no repositories) — repos can be added later.');
|
|
80
|
+
selectedEntries = [];
|
|
81
|
+
} else if (opts.repos) {
|
|
82
|
+
const repoSpecs = parseList(opts.repos);
|
|
83
|
+
const { resolved, notFound, invalidSuffix } = resolveRepoSpecs(repoSpecs, repos);
|
|
84
|
+
|
|
85
|
+
if (invalidSuffix.length > 0) {
|
|
86
|
+
logError(`Invalid instance suffix in: ${invalidSuffix.join(', ')}`);
|
|
87
|
+
logInfo('Use repo:suffix (e.g. casper:cas-101). Suffix may contain letters, numbers, hyphens, underscores.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Warn about fuzzy-resolved names so the user knows what was used
|
|
92
|
+
for (const r of resolved) {
|
|
93
|
+
if (!r.exact) {
|
|
94
|
+
logInfo(`Repo "${r.input}" not found exactly — using "${colorize(r.repo.name, 'cyan')}" (best match)`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (notFound.length > 0) {
|
|
99
|
+
logError(`Repositories not found: ${notFound.join(', ')}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
selectedEntries = resolved.map(r => ({ repo: r.repo, directoryName: r.directoryName }));
|
|
104
|
+
} else {
|
|
105
|
+
selectedEntries = await promptRepositorySelection(repos);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (selectedEntries.length === 0 && !opts.allowEmpty) {
|
|
109
|
+
logInfo('No repositories selected');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 4: Get workspace name
|
|
114
|
+
logStep(4, 6, 'Configure workspace...');
|
|
115
|
+
let workspaceName: string;
|
|
116
|
+
|
|
117
|
+
if (opts.workspace) {
|
|
118
|
+
workspaceName = sanitizeWorkspaceName(opts.workspace);
|
|
119
|
+
const nameError = validateWorkspaceName(workspaceName);
|
|
120
|
+
if (nameError !== true) {
|
|
121
|
+
logError(typeof nameError === 'string' ? nameError : 'Invalid workspace name');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const exists = await checkWorkspaceExists(workspaceName);
|
|
125
|
+
if (exists) {
|
|
126
|
+
const resolved = await resolveWorkspaceNameConflict(workspaceName, selectedEntries.map(e => e.directoryName));
|
|
127
|
+
logInfo(`Workspace "${workspaceName}" already exists — using "${colorize(resolved, 'cyan')}" instead.`);
|
|
128
|
+
workspaceName = resolved;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
workspaceName = await promptWorkspaceName();
|
|
132
|
+
// Auto-resolve conflict in case the interactively chosen name already exists
|
|
133
|
+
// (the prompt validates format but not uniqueness at this point)
|
|
134
|
+
const interactiveExists = await checkWorkspaceExists(workspaceName);
|
|
135
|
+
if (interactiveExists) {
|
|
136
|
+
const resolved = await resolveWorkspaceNameConflict(workspaceName, selectedEntries.map(e => e.directoryName));
|
|
137
|
+
logInfo(`Workspace "${workspaceName}" already exists — using "${colorize(resolved, 'cyan')}" instead.`);
|
|
138
|
+
workspaceName = resolved;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Step 5: Confirm
|
|
143
|
+
const workspacePath = path.join(WORKSPACES_DIR, workspaceName);
|
|
144
|
+
if (!opts.yes) {
|
|
145
|
+
const confirmed = await confirmWorkspaceCreation(workspaceName, selectedEntries.length, workspacePath);
|
|
146
|
+
if (!confirmed) {
|
|
147
|
+
logInfo('Operation cancelled');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Step 6: Clone repositories
|
|
153
|
+
logStep(5, 6, 'Creating workspace...');
|
|
154
|
+
const { mkdir } = await import('fs/promises');
|
|
155
|
+
await mkdir(workspacePath, { recursive: true });
|
|
156
|
+
|
|
157
|
+
if (selectedEntries.length > 0) await warnIfGhqMissing();
|
|
158
|
+
const results = await cloneRepositories(selectedEntries, workspacePath);
|
|
159
|
+
reportCloneResults(results);
|
|
160
|
+
|
|
161
|
+
// Step 7: Save metadata & generate context
|
|
162
|
+
logStep(6, 6, 'Saving workspace metadata...');
|
|
163
|
+
const metadata = createMetadata(workspaceName, results, { prompt: opts.prompt });
|
|
164
|
+
await saveMetadata(workspacePath, metadata);
|
|
165
|
+
|
|
166
|
+
const successfulRepos = results
|
|
167
|
+
.filter(r => r.status === 'success')
|
|
168
|
+
.map(r => r.repo);
|
|
169
|
+
|
|
170
|
+
// Generate context + .mcp.json even for an empty (investigate-first)
|
|
171
|
+
// workspace, so the in-session agent lands with the MCP tools (search-repos
|
|
172
|
+
// / update-workspace) needed to discover and add repos.
|
|
173
|
+
if (successfulRepos.length > 0 || opts.allowEmpty) {
|
|
174
|
+
await generateClaudeContext(workspacePath, workspaceName, successfulRepos, metadata);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
logSuccess(`Workspace "${colorize(workspaceName, 'cyan')}" created successfully!`);
|
|
178
|
+
|
|
179
|
+
// Write workspace path to temp file for shell integration auto-CD
|
|
180
|
+
const { writeFile } = await import('fs/promises');
|
|
181
|
+
const os = await import('os');
|
|
182
|
+
const tempFile = path.join(os.homedir(), '.workspace-last-created');
|
|
183
|
+
try {
|
|
184
|
+
await writeFile(tempFile, workspacePath, 'utf-8');
|
|
185
|
+
} catch {
|
|
186
|
+
// Non-critical — shell integration CD won't work but workspace was created
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logError('Failed to create workspace');
|
|
191
|
+
if (error instanceof Error) {
|
|
192
|
+
logError(error.message);
|
|
193
|
+
}
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook handler for Claude Code hooks.
|
|
4
|
+
* Invoked via ~/.claude/settings.json hooks — reads JSON from stdin,
|
|
5
|
+
* maps the event to an agent status, and writes to state file.
|
|
6
|
+
*
|
|
7
|
+
* Status model:
|
|
8
|
+
* idle — agent is not working (new, finished, waiting for input)
|
|
9
|
+
* working — agent is actively processing (thinking, calling tools)
|
|
10
|
+
* waiting — agent needs user action (permission approval)
|
|
11
|
+
* stopped — session ended
|
|
12
|
+
*
|
|
13
|
+
* Event mapping:
|
|
14
|
+
* SessionStart → idle (session opened, not working yet)
|
|
15
|
+
* UserPromptSubmit → working (user sent prompt, agent starts)
|
|
16
|
+
* PreToolUse → working (agent calling a tool)
|
|
17
|
+
* PostToolUse → working (tool done, agent still processing)
|
|
18
|
+
* Notification:
|
|
19
|
+
* permission_prompt → waiting (needs user to approve tool use)
|
|
20
|
+
* idle_prompt → idle (agent finished, waiting for input)
|
|
21
|
+
* Stop → idle (agent finished its turn)
|
|
22
|
+
* SessionEnd → stopped (session ended)
|
|
23
|
+
*
|
|
24
|
+
* IMPORTANT: Must never write to stderr or throw — always exit 0.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import { AgentState, AgentStatus } from '../../types/dashboard';
|
|
30
|
+
import { readAgentState, writeAgentState, ensureStateDir } from '../../utils/agent-state';
|
|
31
|
+
import { WORKSPACES_DIR } from '../../utils/config';
|
|
32
|
+
|
|
33
|
+
function main() {
|
|
34
|
+
try {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const eventIdx = args.indexOf('--event');
|
|
37
|
+
const event = eventIdx >= 0 ? args[eventIdx + 1] : null;
|
|
38
|
+
|
|
39
|
+
if (!event) {
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let input: any;
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync('/dev/stdin', 'utf-8');
|
|
46
|
+
input = JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sessionId = input.session_id;
|
|
52
|
+
if (!sessionId || !/^[a-zA-Z0-9\-]{1,100}$/.test(sessionId)) {
|
|
53
|
+
process.exit(0); // reject missing or invalid session IDs
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ensureStateDir();
|
|
57
|
+
|
|
58
|
+
let newStatus: AgentStatus;
|
|
59
|
+
switch (event) {
|
|
60
|
+
case 'SessionStart':
|
|
61
|
+
newStatus = 'idle';
|
|
62
|
+
break;
|
|
63
|
+
case 'UserPromptSubmit':
|
|
64
|
+
newStatus = 'working';
|
|
65
|
+
break;
|
|
66
|
+
case 'PreToolUse':
|
|
67
|
+
newStatus = 'working';
|
|
68
|
+
break;
|
|
69
|
+
case 'PostToolUse':
|
|
70
|
+
newStatus = 'working';
|
|
71
|
+
break;
|
|
72
|
+
case 'Notification': {
|
|
73
|
+
const notifType = input.notification_type || '';
|
|
74
|
+
if (notifType === 'permission_prompt') {
|
|
75
|
+
newStatus = 'waiting'; // needs user to approve
|
|
76
|
+
} else if (notifType === 'idle_prompt') {
|
|
77
|
+
newStatus = 'idle'; // just idle, not a special state
|
|
78
|
+
} else {
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'Stop':
|
|
84
|
+
newStatus = 'idle';
|
|
85
|
+
break;
|
|
86
|
+
case 'SessionEnd':
|
|
87
|
+
newStatus = 'stopped';
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
|
|
95
|
+
const existing = readAgentState(sessionId);
|
|
96
|
+
if (existing) {
|
|
97
|
+
existing.status = newStatus;
|
|
98
|
+
existing.lastUpdatedAt = now;
|
|
99
|
+
// Always update PID — handles resumed sessions where the old
|
|
100
|
+
// process is dead but session ID is reused with a new process
|
|
101
|
+
existing.pid = process.ppid || existing.pid;
|
|
102
|
+
writeAgentState(existing);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// New session — infer workspace from cwd
|
|
107
|
+
const cwd = input.cwd || process.cwd();
|
|
108
|
+
let workspace = 'unknown';
|
|
109
|
+
let workspacePath = cwd;
|
|
110
|
+
|
|
111
|
+
const workspacesDir = WORKSPACES_DIR;
|
|
112
|
+
if (cwd.startsWith(workspacesDir + path.sep) || cwd === workspacesDir) {
|
|
113
|
+
const relative = cwd.slice(workspacesDir.length + 1);
|
|
114
|
+
const parts = relative.split(path.sep);
|
|
115
|
+
if (parts[0]) {
|
|
116
|
+
workspace = parts[0];
|
|
117
|
+
workspacePath = path.join(workspacesDir, workspace);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const state: AgentState = {
|
|
122
|
+
sessionId,
|
|
123
|
+
workspace,
|
|
124
|
+
workspacePath,
|
|
125
|
+
pid: process.ppid || 0,
|
|
126
|
+
status: newStatus,
|
|
127
|
+
startedAt: now,
|
|
128
|
+
lastUpdatedAt: now,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
writeAgentState(state);
|
|
132
|
+
process.exit(0);
|
|
133
|
+
} catch {
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main();
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { execFileSync, spawnSync } from 'child_process';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger';
|
|
5
|
+
import { colorize } from '../../utils/colors';
|
|
6
|
+
import { areDashboardHooksInstalled, installDashboardHooks, uninstallDashboardHooks } from '../../utils/dashboard-hooks';
|
|
7
|
+
import { DASHBOARD_DEFAULTS } from '../../types/dashboard';
|
|
8
|
+
import { readAllAgentStates, removeAgentState } from '../../utils/agent-state';
|
|
9
|
+
import { getAgentPanes, findPaneForAgent, resetLayout } from '../../utils/tmux-dashboard';
|
|
10
|
+
import { getPrimaryAgent } from '../../utils/agent-config';
|
|
11
|
+
|
|
12
|
+
const { sessionName, sidebarWidthPercent } = DASHBOARD_DEFAULTS;
|
|
13
|
+
|
|
14
|
+
function isTmuxAvailable(): boolean {
|
|
15
|
+
try {
|
|
16
|
+
execFileSync('which', ['tmux'], { stdio: 'pipe' });
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isInsideTmux(): boolean {
|
|
24
|
+
return !!process.env.TMUX;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerDashboardCommand(parent: Command) {
|
|
28
|
+
parent
|
|
29
|
+
.command('dashboard')
|
|
30
|
+
.alias('dash')
|
|
31
|
+
.description('Launch multi-agent management dashboard')
|
|
32
|
+
.option('--install-hooks', 'Install Claude status hooks without launching dashboard')
|
|
33
|
+
.option('--uninstall-hooks', 'Remove dashboard hooks from Claude settings')
|
|
34
|
+
.option('--list', 'List running agents as JSON (non-interactive)')
|
|
35
|
+
.option('--status', 'Show agent status summary (non-interactive)')
|
|
36
|
+
.option('--kill <sessionId>', 'Kill agent by session ID (non-interactive)')
|
|
37
|
+
.option('--launch <workspace>', 'Launch a new agent in a workspace (non-interactive)')
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
await handleDashboard(opts);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handleDashboard(opts: {
|
|
44
|
+
installHooks?: boolean;
|
|
45
|
+
uninstallHooks?: boolean;
|
|
46
|
+
list?: boolean;
|
|
47
|
+
status?: boolean;
|
|
48
|
+
kill?: string;
|
|
49
|
+
launch?: string;
|
|
50
|
+
}) {
|
|
51
|
+
// Handle hook-only commands
|
|
52
|
+
if (opts.installHooks) {
|
|
53
|
+
installDashboardHooks();
|
|
54
|
+
logSuccess('Dashboard hooks installed in ~/.claude/settings.json');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (opts.uninstallHooks) {
|
|
59
|
+
uninstallDashboardHooks();
|
|
60
|
+
logSuccess('Dashboard hooks removed from ~/.claude/settings.json');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --list: print running agents as JSON
|
|
65
|
+
if (opts.list) {
|
|
66
|
+
const agents = readAllAgentStates().filter(a => a.status !== 'stopped');
|
|
67
|
+
console.log(JSON.stringify(agents, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --status: human-readable status summary
|
|
72
|
+
if (opts.status) {
|
|
73
|
+
const agents = readAllAgentStates().filter(a => a.status !== 'stopped');
|
|
74
|
+
if (agents.length === 0) {
|
|
75
|
+
console.log('No agents running.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const a of agents) {
|
|
79
|
+
console.log(`${colorize(a.workspace, 'cyan')} ${colorize(a.status, 'yellow')} ${a.sessionId.slice(0, 8)}`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --kill: kill agent by session ID
|
|
85
|
+
if (opts.kill) {
|
|
86
|
+
const sessionId = opts.kill;
|
|
87
|
+
const agents = readAllAgentStates();
|
|
88
|
+
const agent = agents.find(a => a.sessionId === sessionId || a.sessionId.startsWith(sessionId));
|
|
89
|
+
if (!agent) {
|
|
90
|
+
logError(`No agent found with session ID: ${sessionId}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
// Try to kill the tmux pane if dashboard is running
|
|
94
|
+
const panes = getAgentPanes();
|
|
95
|
+
const pane = findPaneForAgent(agent, panes);
|
|
96
|
+
if (pane) {
|
|
97
|
+
spawnSync('tmux', ['kill-pane', '-t', `${sessionName}:0.${pane.index}`], { stdio: 'pipe' });
|
|
98
|
+
setTimeout(() => resetLayout(), 100);
|
|
99
|
+
} else {
|
|
100
|
+
// Kill the process directly (guard: pid 0 would kill entire process group)
|
|
101
|
+
if (agent.pid > 0) {
|
|
102
|
+
try { process.kill(agent.pid); } catch { /* already dead */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
removeAgentState(agent.sessionId);
|
|
106
|
+
logSuccess(`Killed agent: ${agent.workspace} (${agent.sessionId.slice(0, 8)})`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --launch: launch a new agent in a workspace
|
|
111
|
+
if (opts.launch) {
|
|
112
|
+
const workspaceName = opts.launch;
|
|
113
|
+
const { listWorkspaces } = await import('../../utils/workspace-meta');
|
|
114
|
+
const workspaces = await listWorkspaces();
|
|
115
|
+
const ws = workspaces.find(w => w.name === workspaceName);
|
|
116
|
+
if (!ws) {
|
|
117
|
+
logError(`Workspace not found: ${workspaceName}`);
|
|
118
|
+
logInfo(`Available: ${workspaces.map(w => w.name).join(', ')}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
// Check if dashboard is running by verifying the session exists
|
|
122
|
+
const sessionCheck = spawnSync('tmux', ['has-session', '-t', sessionName], { stdio: 'pipe' });
|
|
123
|
+
if (sessionCheck.status === 0) {
|
|
124
|
+
// Dashboard running — split a pane
|
|
125
|
+
spawnSync('tmux', [
|
|
126
|
+
'split-window', '-t', `${sessionName}:0`, '-h',
|
|
127
|
+
'-c', ws.path,
|
|
128
|
+
getPrimaryAgent().launchCommand,
|
|
129
|
+
], { stdio: 'pipe' });
|
|
130
|
+
spawnSync('tmux', ['select-layout', '-t', sessionName, 'main-vertical'], { stdio: 'pipe' });
|
|
131
|
+
spawnSync('tmux', ['resize-pane', '-t', `${sessionName}:0.0`, '-x', '32'], { stdio: 'pipe' });
|
|
132
|
+
logSuccess(`Launched agent in workspace: ${workspaceName}`);
|
|
133
|
+
} else {
|
|
134
|
+
logInfo(`Dashboard not running. Start it with: w dash`);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check tmux for interactive dashboard
|
|
140
|
+
if (!isTmuxAvailable()) {
|
|
141
|
+
logError('tmux is required for the agent dashboard');
|
|
142
|
+
console.log(colorize('Install tmux: brew install tmux', 'gray'));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Auto-install hooks if not present
|
|
147
|
+
if (!areDashboardHooksInstalled()) {
|
|
148
|
+
installDashboardHooks();
|
|
149
|
+
logInfo('Installed dashboard hooks in ~/.claude/settings.json');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if dashboard session already exists — reconnect if so
|
|
153
|
+
const sessionExists = spawnSync('tmux', [
|
|
154
|
+
'has-session', '-t', sessionName,
|
|
155
|
+
], { stdio: 'pipe' }).status === 0;
|
|
156
|
+
|
|
157
|
+
if (sessionExists) {
|
|
158
|
+
logInfo('Reconnecting to existing dashboard session...');
|
|
159
|
+
if (isInsideTmux()) {
|
|
160
|
+
spawnSync('tmux', ['switch-client', '-t', sessionName], { stdio: 'inherit' });
|
|
161
|
+
} else {
|
|
162
|
+
spawnSync('tmux', ['attach-session', '-t', sessionName], { stdio: 'inherit' });
|
|
163
|
+
}
|
|
164
|
+
logSuccess('Dashboard session ended');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Resolve paths to compiled scripts
|
|
169
|
+
const sidebarScript = path.join(__dirname, 'sidebar.js');
|
|
170
|
+
|
|
171
|
+
// Create tmux session with sidebar in the left pane
|
|
172
|
+
const newSessionResult = spawnSync('tmux', [
|
|
173
|
+
'new-session', '-d', '-s', sessionName,
|
|
174
|
+
'-x', '200', '-y', '50',
|
|
175
|
+
`node "${sidebarScript}"`,
|
|
176
|
+
], { stdio: 'pipe' });
|
|
177
|
+
|
|
178
|
+
if (newSessionResult.status !== 0) {
|
|
179
|
+
logError('Failed to create tmux session');
|
|
180
|
+
const stderr = newSessionResult.stderr?.toString();
|
|
181
|
+
if (stderr) logError(stderr);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Split right pane (80%) with placeholder message
|
|
186
|
+
spawnSync('tmux', [
|
|
187
|
+
'split-window', '-h', '-t', sessionName,
|
|
188
|
+
'-p', String(100 - sidebarWidthPercent),
|
|
189
|
+
'bash', '-c',
|
|
190
|
+
'echo ""; echo " No agents running."; echo " Press n in the sidebar to launch an agent."; echo ""; sleep infinity',
|
|
191
|
+
], { stdio: 'pipe' });
|
|
192
|
+
|
|
193
|
+
// Focus the sidebar pane (left)
|
|
194
|
+
spawnSync('tmux', ['select-pane', '-t', `${sessionName}:0.0`], { stdio: 'pipe' });
|
|
195
|
+
|
|
196
|
+
// ── Session-scoped key bindings ──
|
|
197
|
+
// Use set-hook to add bindings only while this session is active and
|
|
198
|
+
// restore them on detach — avoids polluting global tmux key tables.
|
|
199
|
+
// prefix + M → focus sidebar + reset layout (M chosen to avoid overriding standard keys)
|
|
200
|
+
spawnSync('tmux', [
|
|
201
|
+
'set-hook', '-t', sessionName, 'session-created',
|
|
202
|
+
`bind-key -T prefix M run-shell "tmux select-layout -t ${sessionName} main-vertical \\\\; resize-pane -t ${sessionName}:0.0 -x 32 \\\\; select-pane -t ${sessionName}:0.0"`,
|
|
203
|
+
], { stdio: 'pipe' });
|
|
204
|
+
|
|
205
|
+
// Bind immediately for the current session
|
|
206
|
+
spawnSync('tmux', [
|
|
207
|
+
'bind-key', '-T', 'prefix', 'M',
|
|
208
|
+
'run-shell',
|
|
209
|
+
`tmux select-layout -t ${sessionName} main-vertical \\; resize-pane -t ${sessionName}:0.0 -x 32 \\; select-pane -t ${sessionName}:0.0`,
|
|
210
|
+
], { stdio: 'pipe' });
|
|
211
|
+
|
|
212
|
+
// Unbind prefix+M when the dashboard session ends (cleanup)
|
|
213
|
+
spawnSync('tmux', [
|
|
214
|
+
'set-hook', '-t', sessionName, 'session-closed',
|
|
215
|
+
'unbind-key -T prefix M',
|
|
216
|
+
], { stdio: 'pipe' });
|
|
217
|
+
|
|
218
|
+
// ── Status bar ──
|
|
219
|
+
spawnSync('tmux', [
|
|
220
|
+
'set-option', '-t', sessionName,
|
|
221
|
+
'status-left', ' 🤖 Agent Dashboard ',
|
|
222
|
+
], { stdio: 'pipe' });
|
|
223
|
+
spawnSync('tmux', [
|
|
224
|
+
'set-option', '-t', sessionName,
|
|
225
|
+
'status-right', ' prefix+M:sidebar/reset q:detach Q:quit ',
|
|
226
|
+
], { stdio: 'pipe' });
|
|
227
|
+
spawnSync('tmux', [
|
|
228
|
+
'set-option', '-t', sessionName,
|
|
229
|
+
'status-style', 'bg=colour235,fg=colour255',
|
|
230
|
+
], { stdio: 'pipe' });
|
|
231
|
+
|
|
232
|
+
// Enable mouse support for pane selection
|
|
233
|
+
spawnSync('tmux', [
|
|
234
|
+
'set-option', '-t', sessionName,
|
|
235
|
+
'mouse', 'on',
|
|
236
|
+
], { stdio: 'pipe' });
|
|
237
|
+
|
|
238
|
+
logInfo('Launching agent dashboard...');
|
|
239
|
+
|
|
240
|
+
// Attach to the session
|
|
241
|
+
if (isInsideTmux()) {
|
|
242
|
+
spawnSync('tmux', ['switch-client', '-t', sessionName], { stdio: 'inherit' });
|
|
243
|
+
} else {
|
|
244
|
+
spawnSync('tmux', ['attach-session', '-t', sessionName], { stdio: 'inherit' });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
logSuccess('Agent dashboard session ended');
|
|
248
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { DASHBOARD_DEFAULTS } from '../../types/dashboard';
|
|
3
|
+
|
|
4
|
+
const { sessionName } = DASHBOARD_DEFAULTS;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Launch an agent in the dashboard by spawning a new tmux pane.
|
|
8
|
+
* Kills any placeholder pane (sleep infinity), splits a new pane,
|
|
9
|
+
* and rebalances the layout.
|
|
10
|
+
*
|
|
11
|
+
* @param workspacePath - absolute path to the workspace directory
|
|
12
|
+
* @param command - shell command to run in the new pane (e.g. 'claude' or 'claude --resume <id> --fork-session')
|
|
13
|
+
*/
|
|
14
|
+
export function launchAgentPane(workspacePath: string, command: string): void {
|
|
15
|
+
// Kill placeholder pane (sleep infinity) if present
|
|
16
|
+
const paneResult = spawnSync('tmux', [
|
|
17
|
+
'list-panes', '-t', sessionName, '-F',
|
|
18
|
+
'#{pane_index} #{pane_current_command}',
|
|
19
|
+
], { stdio: 'pipe' });
|
|
20
|
+
|
|
21
|
+
if (paneResult.stdout) {
|
|
22
|
+
const lines = paneResult.stdout.toString().trim().split('\n');
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const parts = line.split(' ');
|
|
25
|
+
if (parts[0] !== '0' && parts.slice(1).join(' ').includes('sleep')) {
|
|
26
|
+
spawnSync('tmux', ['kill-pane', '-t', `${sessionName}:0.${parts[0]}`], { stdio: 'pipe' });
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create a new pane for the agent
|
|
33
|
+
spawnSync('tmux', [
|
|
34
|
+
'split-window', '-t', `${sessionName}:0`, '-h',
|
|
35
|
+
'-c', workspacePath,
|
|
36
|
+
command,
|
|
37
|
+
], { stdio: 'pipe' });
|
|
38
|
+
|
|
39
|
+
// Rebalance layout: sidebar left, agents stacked right
|
|
40
|
+
spawnSync('tmux', ['select-layout', '-t', sessionName, 'main-vertical'], { stdio: 'pipe' });
|
|
41
|
+
spawnSync('tmux', ['resize-pane', '-t', `${sessionName}:0.0`, '-x', '32'], { stdio: 'pipe' });
|
|
42
|
+
}
|