@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,264 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { getUserConfig, AgentType, PrimaryAgentType, ConcreteAgentType } from './config';
|
|
5
|
+
|
|
6
|
+
// Re-export for convenience
|
|
7
|
+
export { AgentType, PrimaryAgentType, ConcreteAgentType } from './config';
|
|
8
|
+
|
|
9
|
+
export interface AgentPaths {
|
|
10
|
+
/** Agent identifier */
|
|
11
|
+
type: ConcreteAgentType;
|
|
12
|
+
/** Directory for skills (e.g. ~/.claude/skills or ~/.pi/agent/skills) */
|
|
13
|
+
skillsDir: string;
|
|
14
|
+
/** Settings file (e.g. ~/.claude/settings.json or ~/.pi/agent/settings.json) */
|
|
15
|
+
settingsFile: string;
|
|
16
|
+
/** Context file name generated in workspaces (e.g. CLAUDE.md or AGENTS.md) */
|
|
17
|
+
contextFileName: string;
|
|
18
|
+
/** CLI command to launch the agent */
|
|
19
|
+
launchCommand: string;
|
|
20
|
+
/** CLI command to resume a session */
|
|
21
|
+
resumeCommand: (sessionId: string) => string;
|
|
22
|
+
/** Whether this agent supports MCP (Model Context Protocol) */
|
|
23
|
+
supportsMcp: boolean;
|
|
24
|
+
/** Whether this agent supports hooks in settings.json (Claude Code format) */
|
|
25
|
+
supportsHooks: boolean;
|
|
26
|
+
/** Session projects directory (for session discovery) */
|
|
27
|
+
sessionProjectsDir: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HOME = os.homedir();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detection order for `auto` / `both` / `getPrimaryAgent`. Also the order agents
|
|
34
|
+
* appear in `getActiveAgents()`. Add a new agent's config below and to this list.
|
|
35
|
+
*/
|
|
36
|
+
export const AGENT_ORDER: ConcreteAgentType[] = ['claude', 'pi', 'opencode', 'codex', 'gemini'];
|
|
37
|
+
|
|
38
|
+
const CLAUDE_PATHS: AgentPaths = {
|
|
39
|
+
type: 'claude',
|
|
40
|
+
skillsDir: path.join(HOME, '.claude', 'skills'),
|
|
41
|
+
settingsFile: path.join(HOME, '.claude', 'settings.json'),
|
|
42
|
+
// Must be exactly 'CLAUDE.md': Claude Code only auto-discovers CLAUDE.md /
|
|
43
|
+
// AGENTS.md up the cwd/ancestor chain. A hidden lowercase '.claude.md' is
|
|
44
|
+
// invisible to its project-memory loader, so the workspace context (and its
|
|
45
|
+
// 'Saved Context' pointer) would never surface automatically (issue #186).
|
|
46
|
+
contextFileName: 'CLAUDE.md',
|
|
47
|
+
launchCommand: 'claude',
|
|
48
|
+
resumeCommand: (id: string) => `claude --resume ${id} --fork-session`,
|
|
49
|
+
supportsMcp: true,
|
|
50
|
+
supportsHooks: true,
|
|
51
|
+
sessionProjectsDir: path.join(HOME, '.claude', 'projects'),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PI_PATHS: AgentPaths = {
|
|
55
|
+
type: 'pi',
|
|
56
|
+
skillsDir: path.join(HOME, '.pi', 'agent', 'skills'),
|
|
57
|
+
settingsFile: path.join(HOME, '.pi', 'agent', 'settings.json'),
|
|
58
|
+
contextFileName: 'AGENTS.md',
|
|
59
|
+
launchCommand: 'pi',
|
|
60
|
+
resumeCommand: (id: string) => `pi --session ${id} --fork`,
|
|
61
|
+
supportsMcp: false,
|
|
62
|
+
supportsHooks: false,
|
|
63
|
+
sessionProjectsDir: path.join(HOME, '.pi', 'agent', 'sessions'),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// OpenCode uses XDG paths: ~/.local/share/opencode for data, ~/.config/opencode for config
|
|
67
|
+
// It reads skills from ~/.claude/skills/ automatically (Claude-compatible)
|
|
68
|
+
// Project config lives in .opencode/ within the project directory
|
|
69
|
+
const OPENCODE_DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(HOME, '.local', 'share'), 'opencode');
|
|
70
|
+
const OPENCODE_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(HOME, '.config'), 'opencode');
|
|
71
|
+
|
|
72
|
+
const OPENCODE_PATHS: AgentPaths = {
|
|
73
|
+
type: 'opencode',
|
|
74
|
+
// OpenCode reads skills from ~/.claude/skills/ natively — no separate install needed
|
|
75
|
+
skillsDir: path.join(HOME, '.claude', 'skills'),
|
|
76
|
+
settingsFile: path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
|
|
77
|
+
contextFileName: 'AGENTS.md',
|
|
78
|
+
launchCommand: 'opencode',
|
|
79
|
+
resumeCommand: (_id: string) => `opencode --continue`,
|
|
80
|
+
supportsMcp: true,
|
|
81
|
+
supportsHooks: false,
|
|
82
|
+
sessionProjectsDir: OPENCODE_DATA_DIR,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Codex (OpenAI Codex CLI) — config under ~/.codex, reads AGENTS.md for context,
|
|
86
|
+
// supports MCP servers via ~/.codex/config.toml. No native skills or hooks.
|
|
87
|
+
const CODEX_HOME = process.env.CODEX_HOME || path.join(HOME, '.codex');
|
|
88
|
+
const CODEX_PATHS: AgentPaths = {
|
|
89
|
+
type: 'codex',
|
|
90
|
+
skillsDir: path.join(CODEX_HOME, 'skills'),
|
|
91
|
+
settingsFile: path.join(CODEX_HOME, 'config.toml'),
|
|
92
|
+
contextFileName: 'AGENTS.md',
|
|
93
|
+
launchCommand: 'codex',
|
|
94
|
+
resumeCommand: (id: string) => `codex resume ${id}`,
|
|
95
|
+
supportsMcp: true,
|
|
96
|
+
supportsHooks: false,
|
|
97
|
+
sessionProjectsDir: path.join(CODEX_HOME, 'sessions'),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Gemini (Google Gemini CLI) — config under ~/.gemini, reads GEMINI.md for
|
|
101
|
+
// context, supports MCP servers via ~/.gemini/settings.json. No hooks.
|
|
102
|
+
const GEMINI_HOME = path.join(HOME, '.gemini');
|
|
103
|
+
const GEMINI_PATHS: AgentPaths = {
|
|
104
|
+
type: 'gemini',
|
|
105
|
+
skillsDir: path.join(GEMINI_HOME, 'skills'),
|
|
106
|
+
settingsFile: path.join(GEMINI_HOME, 'settings.json'),
|
|
107
|
+
contextFileName: 'GEMINI.md',
|
|
108
|
+
launchCommand: 'gemini',
|
|
109
|
+
resumeCommand: (_id: string) => `gemini`,
|
|
110
|
+
supportsMcp: true,
|
|
111
|
+
supportsHooks: false,
|
|
112
|
+
sessionProjectsDir: path.join(GEMINI_HOME, 'tmp'),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const AGENT_REGISTRY: Record<ConcreteAgentType, AgentPaths> = {
|
|
116
|
+
claude: CLAUDE_PATHS,
|
|
117
|
+
pi: PI_PATHS,
|
|
118
|
+
opencode: OPENCODE_PATHS,
|
|
119
|
+
codex: CODEX_PATHS,
|
|
120
|
+
gemini: GEMINI_PATHS,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get AgentPaths for a specific agent type.
|
|
125
|
+
*/
|
|
126
|
+
export function getAgentPaths(agent: ConcreteAgentType): AgentPaths {
|
|
127
|
+
return { ...AGENT_REGISTRY[agent] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve 'auto' to a concrete agent type by detecting installed CLIs.
|
|
132
|
+
* Cached after first call to avoid repeated shell exec.
|
|
133
|
+
*/
|
|
134
|
+
let resolvedAutoAgent: ConcreteAgentType | 'both' | null = null;
|
|
135
|
+
|
|
136
|
+
function resolveAutoAgent(): ConcreteAgentType | 'both' {
|
|
137
|
+
if (resolvedAutoAgent) return resolvedAutoAgent;
|
|
138
|
+
|
|
139
|
+
const available = AGENT_ORDER.filter(a => isAgentCliAvailable(a));
|
|
140
|
+
|
|
141
|
+
// If multiple agents detected, use 'both' (meaning: all available agents).
|
|
142
|
+
if (available.length > 1) resolvedAutoAgent = 'both';
|
|
143
|
+
else if (available.length === 1) resolvedAutoAgent = available[0];
|
|
144
|
+
else resolvedAutoAgent = 'claude'; // default to claude even if not installed
|
|
145
|
+
|
|
146
|
+
return resolvedAutoAgent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get all active agent configurations based on user config.
|
|
151
|
+
* Resolves 'auto' by detecting installed CLIs. 'both' means every available agent.
|
|
152
|
+
*/
|
|
153
|
+
export function getActiveAgents(): AgentPaths[] {
|
|
154
|
+
let { aiAgent } = getUserConfig();
|
|
155
|
+
if (aiAgent === 'auto') aiAgent = resolveAutoAgent();
|
|
156
|
+
|
|
157
|
+
if (aiAgent === 'both') {
|
|
158
|
+
const agents = AGENT_ORDER.filter(a => isAgentCliAvailable(a)).map(a => ({ ...AGENT_REGISTRY[a] }));
|
|
159
|
+
// Fallback to Claude if nothing detected (shouldn't happen in 'both' mode)
|
|
160
|
+
return agents.length > 0 ? agents : [{ ...CLAUDE_PATHS }];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [{ ...AGENT_REGISTRY[aiAgent] }];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get the preferred (primary) agent — the one used for launching, dashboards, etc.
|
|
168
|
+
* Uses the `primaryAgent` config. When 'auto', picks the first available CLI.
|
|
169
|
+
*/
|
|
170
|
+
export function getPrimaryAgent(): AgentPaths {
|
|
171
|
+
const { primaryAgent } = getUserConfig();
|
|
172
|
+
|
|
173
|
+
if (primaryAgent !== 'auto') return { ...AGENT_REGISTRY[primaryAgent] };
|
|
174
|
+
|
|
175
|
+
// 'auto' — pick first available in detection order
|
|
176
|
+
const first = AGENT_ORDER.find(a => isAgentCliAvailable(a));
|
|
177
|
+
return { ...AGENT_REGISTRY[first ?? 'claude'] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if a specific agent CLI is available on the system.
|
|
182
|
+
* Result is cached per agent to avoid repeated execSync overhead.
|
|
183
|
+
*/
|
|
184
|
+
const cliAvailabilityCache = new Map<string, boolean>();
|
|
185
|
+
|
|
186
|
+
export function isAgentCliAvailable(agent: ConcreteAgentType): boolean {
|
|
187
|
+
if (cliAvailabilityCache.has(agent)) return cliAvailabilityCache.get(agent)!;
|
|
188
|
+
|
|
189
|
+
const command = `${AGENT_REGISTRY[agent].launchCommand} --version`;
|
|
190
|
+
let available: boolean;
|
|
191
|
+
try {
|
|
192
|
+
execSync(command, { stdio: 'pipe' });
|
|
193
|
+
available = true;
|
|
194
|
+
} catch {
|
|
195
|
+
available = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
cliAvailabilityCache.set(agent, available);
|
|
199
|
+
return available;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Reset all cached values. For use in tests to ensure clean state.
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
export function resetAgentConfigCache(): void {
|
|
207
|
+
resolvedAutoAgent = null;
|
|
208
|
+
cliAvailabilityCache.clear();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get all skills directories that should be written to.
|
|
213
|
+
*/
|
|
214
|
+
export function getSkillsTargetDirs(): string[] {
|
|
215
|
+
const dirs = getActiveAgents().map(a => a.skillsDir);
|
|
216
|
+
return [...new Set(dirs)];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all context file names that should be generated in workspaces.
|
|
221
|
+
* Deduplicates if multiple agents use the same file.
|
|
222
|
+
*/
|
|
223
|
+
export function getContextFileNames(): string[] {
|
|
224
|
+
const names = getActiveAgents().map(a => a.contextFileName);
|
|
225
|
+
return [...new Set(names)];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get all settings files where hooks should be installed.
|
|
230
|
+
* Only returns agents that support hooks.
|
|
231
|
+
*/
|
|
232
|
+
export function getHookTargetFiles(): string[] {
|
|
233
|
+
return getActiveAgents()
|
|
234
|
+
.filter(a => a.supportsHooks)
|
|
235
|
+
.map(a => a.settingsFile);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get all agents that support MCP.
|
|
240
|
+
*/
|
|
241
|
+
export function getMcpAgents(): AgentPaths[] {
|
|
242
|
+
return getActiveAgents().filter(a => a.supportsMcp);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get every context file name that any known agent can produce, regardless
|
|
247
|
+
* of which agents are currently configured. Used when scanning repos for
|
|
248
|
+
* existing context files (e.g. a Pi workspace may contain repos that were
|
|
249
|
+
* previously set up for Claude Code, and vice-versa).
|
|
250
|
+
*
|
|
251
|
+
* Also includes legacy '.claude.md' — the filename generated for Claude before
|
|
252
|
+
* issue #186 — so repos/workspaces that still contain one are still recognized.
|
|
253
|
+
*/
|
|
254
|
+
export function getAllKnownContextFileNames(): string[] {
|
|
255
|
+
// Order matters: this is the per-repo scan PRIORITY (findContextFilesInDir
|
|
256
|
+
// embeds the first existing candidate per directory). AGENTS.md must come
|
|
257
|
+
// before CLAUDE.md — in dual-agent repos CLAUDE.md is commonly a thin
|
|
258
|
+
// "@AGENTS.md" shim, so AGENTS.md holds the real content. '.claude.md' is the
|
|
259
|
+
// legacy output (pre-#186), kept last so any lingering files are still
|
|
260
|
+
// recognized.
|
|
261
|
+
const priority = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.claude.md'];
|
|
262
|
+
const fromAgents = AGENT_ORDER.map(a => AGENT_REGISTRY[a].contextFileName);
|
|
263
|
+
return [...new Set([...priority, ...fromAgents])];
|
|
264
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
// We need to mock the AGENTS_STATE_DIR to use a temp dir for tests
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
|
|
9
|
+
vi.mock('../types/dashboard', async () => {
|
|
10
|
+
const actual = await vi.importActual('../types/dashboard');
|
|
11
|
+
return {
|
|
12
|
+
...actual as any,
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-state-test-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Helper to write a state file directly
|
|
26
|
+
function writeStateFile(sessionId: string, state: any) {
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(tmpDir, `${sessionId}.json`),
|
|
29
|
+
JSON.stringify(state, null, 2),
|
|
30
|
+
'utf-8'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('agent-state', () => {
|
|
35
|
+
it('writeAgentState + readAgentState round-trip', async () => {
|
|
36
|
+
// Override the module's AGENTS_STATE_DIR
|
|
37
|
+
vi.doMock('./agent-state', async () => {
|
|
38
|
+
const mod = await vi.importActual('./agent-state') as any;
|
|
39
|
+
return {
|
|
40
|
+
...mod,
|
|
41
|
+
AGENTS_STATE_DIR: tmpDir,
|
|
42
|
+
ensureStateDir: () => fs.mkdirSync(tmpDir, { recursive: true }),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Direct file-based test instead of mocking
|
|
47
|
+
const state = {
|
|
48
|
+
sessionId: 'test-123',
|
|
49
|
+
workspace: 'my-workspace',
|
|
50
|
+
workspacePath: '/home/user/workspaces/my-workspace',
|
|
51
|
+
pid: process.pid, // Use current PID so it's alive
|
|
52
|
+
status: 'thinking' as const,
|
|
53
|
+
startedAt: new Date().toISOString(),
|
|
54
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const filePath = path.join(tmpDir, `${state.sessionId}.json`);
|
|
58
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
59
|
+
|
|
60
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
61
|
+
const read = JSON.parse(content);
|
|
62
|
+
expect(read.sessionId).toBe('test-123');
|
|
63
|
+
expect(read.workspace).toBe('my-workspace');
|
|
64
|
+
expect(read.status).toBe('thinking');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('handles malformed JSON gracefully', () => {
|
|
68
|
+
fs.writeFileSync(path.join(tmpDir, 'bad.json'), 'not json{{{', 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Reading should return null equivalent
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(path.join(tmpDir, 'bad.json'), 'utf-8');
|
|
73
|
+
const parsed = JSON.parse(content);
|
|
74
|
+
// Should not get here
|
|
75
|
+
expect(parsed).toBeUndefined();
|
|
76
|
+
} catch {
|
|
77
|
+
// Expected — malformed JSON
|
|
78
|
+
expect(true).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('state file structure matches AgentState interface', () => {
|
|
83
|
+
const state = {
|
|
84
|
+
sessionId: 'sess-abc',
|
|
85
|
+
workspace: 'test-ws',
|
|
86
|
+
workspacePath: '/tmp/ws',
|
|
87
|
+
pid: 12345,
|
|
88
|
+
status: 'waiting',
|
|
89
|
+
startedAt: '2026-03-24T00:00:00.000Z',
|
|
90
|
+
lastUpdatedAt: '2026-03-24T00:01:00.000Z',
|
|
91
|
+
tmuxPane: 'ws-dashboard:0.1',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
writeStateFile('sess-abc', state);
|
|
95
|
+
|
|
96
|
+
const content = fs.readFileSync(path.join(tmpDir, 'sess-abc.json'), 'utf-8');
|
|
97
|
+
const read = JSON.parse(content);
|
|
98
|
+
|
|
99
|
+
expect(read).toEqual(state);
|
|
100
|
+
expect(read.sessionId).toBe('sess-abc');
|
|
101
|
+
expect(read.status).toBe('waiting');
|
|
102
|
+
expect(read.tmuxPane).toBe('ws-dashboard:0.1');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('filters .json files only', () => {
|
|
106
|
+
writeStateFile('valid', { sessionId: 'valid', status: 'idle' });
|
|
107
|
+
fs.writeFileSync(path.join(tmpDir, 'not-json.txt'), 'hello', 'utf-8');
|
|
108
|
+
fs.writeFileSync(path.join(tmpDir, '.hidden.json'), '{}', 'utf-8');
|
|
109
|
+
|
|
110
|
+
const files = fs.readdirSync(tmpDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
|
|
111
|
+
expect(files).toEqual(['valid.json']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sorts states by workspace then startedAt', () => {
|
|
115
|
+
const states = [
|
|
116
|
+
{ workspace: 'beta', startedAt: '2026-03-24T02:00:00Z' },
|
|
117
|
+
{ workspace: 'alpha', startedAt: '2026-03-24T01:00:00Z' },
|
|
118
|
+
{ workspace: 'alpha', startedAt: '2026-03-24T00:00:00Z' },
|
|
119
|
+
{ workspace: 'beta', startedAt: '2026-03-24T01:00:00Z' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
states.sort((a, b) => {
|
|
123
|
+
const wsCmp = a.workspace.localeCompare(b.workspace);
|
|
124
|
+
if (wsCmp !== 0) return wsCmp;
|
|
125
|
+
return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(states.map(s => `${s.workspace}:${s.startedAt}`)).toEqual([
|
|
129
|
+
'alpha:2026-03-24T00:00:00Z',
|
|
130
|
+
'alpha:2026-03-24T01:00:00Z',
|
|
131
|
+
'beta:2026-03-24T01:00:00Z',
|
|
132
|
+
'beta:2026-03-24T02:00:00Z',
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('rejects invalid session IDs (path traversal protection)', () => {
|
|
137
|
+
const invalidIds = [
|
|
138
|
+
'../../etc/passwd',
|
|
139
|
+
'../secret',
|
|
140
|
+
'valid/../evil',
|
|
141
|
+
'a'.repeat(101), // too long
|
|
142
|
+
'has spaces',
|
|
143
|
+
'has/slash',
|
|
144
|
+
'',
|
|
145
|
+
];
|
|
146
|
+
for (const id of invalidIds) {
|
|
147
|
+
// isValidSessionId is internal, but we can verify that reading returns null
|
|
148
|
+
// and that writing to these IDs doesn't create files
|
|
149
|
+
const filePath = path.join(tmpDir, `${id}.json`);
|
|
150
|
+
expect(fs.existsSync(filePath)).toBe(false);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('accepts valid session IDs', () => {
|
|
155
|
+
const validIds = [
|
|
156
|
+
'abc123',
|
|
157
|
+
'a1bf93e4-c587-441b-bac8-0c20b6d0cbe4',
|
|
158
|
+
'dash-1774438882722-6sl23p',
|
|
159
|
+
'a',
|
|
160
|
+
];
|
|
161
|
+
for (const id of validIds) {
|
|
162
|
+
// All valid — just check the regex pattern matches
|
|
163
|
+
expect(/^[a-zA-Z0-9\-]{1,100}$/.test(id)).toBe(true);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { AgentState, DASHBOARD_DEFAULTS } from '../types/dashboard';
|
|
5
|
+
|
|
6
|
+
export const AGENTS_STATE_DIR = path.join(os.homedir(), '.workspace-manager', 'agents');
|
|
7
|
+
|
|
8
|
+
export function ensureStateDir(): void {
|
|
9
|
+
fs.mkdirSync(AGENTS_STATE_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate that a session ID is safe to use as a filename.
|
|
14
|
+
* Allows only alphanumeric characters and hyphens, max 100 chars.
|
|
15
|
+
*/
|
|
16
|
+
function isValidSessionId(id: string): boolean {
|
|
17
|
+
return typeof id === 'string' && /^[a-zA-Z0-9\-]{1,100}$/.test(id);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write agent state atomically (write tmp + rename).
|
|
22
|
+
*/
|
|
23
|
+
export function writeAgentState(state: AgentState): void {
|
|
24
|
+
if (!isValidSessionId(state.sessionId)) return; // reject invalid IDs silently
|
|
25
|
+
ensureStateDir();
|
|
26
|
+
const filePath = path.join(AGENTS_STATE_DIR, `${state.sessionId}.json`);
|
|
27
|
+
const tmpPath = filePath + '.tmp';
|
|
28
|
+
try {
|
|
29
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
30
|
+
fs.renameSync(tmpPath, filePath);
|
|
31
|
+
} catch {
|
|
32
|
+
// Clean up tmp on failure
|
|
33
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read a single agent state by session ID.
|
|
39
|
+
*/
|
|
40
|
+
export function readAgentState(sessionId: string): AgentState | null {
|
|
41
|
+
if (!isValidSessionId(sessionId)) return null;
|
|
42
|
+
try {
|
|
43
|
+
const filePath = path.join(AGENTS_STATE_DIR, `${sessionId}.json`);
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
45
|
+
return JSON.parse(content) as AgentState;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a process is alive by sending signal 0.
|
|
53
|
+
*/
|
|
54
|
+
function isProcessAlive(pid: number): boolean {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read all agent states, check PID liveness, and clean stale entries.
|
|
65
|
+
* Returns active states sorted by workspace, then startedAt.
|
|
66
|
+
*/
|
|
67
|
+
export function readAllAgentStates(): AgentState[] {
|
|
68
|
+
ensureStateDir();
|
|
69
|
+
|
|
70
|
+
let entries: string[];
|
|
71
|
+
try {
|
|
72
|
+
entries = fs.readdirSync(AGENTS_STATE_DIR);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const states: AgentState[] = [];
|
|
79
|
+
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (!entry.endsWith('.json') || entry.startsWith('.')) continue;
|
|
82
|
+
|
|
83
|
+
const sessionId = entry.replace('.json', '');
|
|
84
|
+
const state = readAgentState(sessionId);
|
|
85
|
+
if (!state) {
|
|
86
|
+
// Malformed file — remove it
|
|
87
|
+
try { fs.unlinkSync(path.join(AGENTS_STATE_DIR, entry)); } catch { /* ignore */ }
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// PID liveness check for non-stopped states
|
|
92
|
+
// Skip the check if the state was updated very recently (within 10s)
|
|
93
|
+
// to allow hooks time to update the PID on session resume
|
|
94
|
+
if (state.status !== 'stopped' && state.pid > 0) {
|
|
95
|
+
const age = now - new Date(state.lastUpdatedAt).getTime();
|
|
96
|
+
if (age > 10_000 && !isProcessAlive(state.pid)) {
|
|
97
|
+
state.status = 'stopped';
|
|
98
|
+
state.lastUpdatedAt = new Date().toISOString();
|
|
99
|
+
writeAgentState(state);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Filter out stale stopped states
|
|
104
|
+
if (state.status === 'stopped') {
|
|
105
|
+
const age = now - new Date(state.lastUpdatedAt).getTime();
|
|
106
|
+
if (age > DASHBOARD_DEFAULTS.staleTtlMs) {
|
|
107
|
+
try { fs.unlinkSync(path.join(AGENTS_STATE_DIR, entry)); } catch { /* ignore */ }
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
states.push(state);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Sort by workspace ascending, then startedAt ascending
|
|
116
|
+
states.sort((a, b) => {
|
|
117
|
+
const wsCmp = a.workspace.localeCompare(b.workspace);
|
|
118
|
+
if (wsCmp !== 0) return wsCmp;
|
|
119
|
+
return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return states;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Remove a single agent state file.
|
|
127
|
+
*/
|
|
128
|
+
export function removeAgentState(sessionId: string): void {
|
|
129
|
+
if (!isValidSessionId(sessionId)) return;
|
|
130
|
+
try {
|
|
131
|
+
fs.unlinkSync(path.join(AGENTS_STATE_DIR, `${sessionId}.json`));
|
|
132
|
+
} catch {
|
|
133
|
+
// File doesn't exist — fine
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { colors } from './colors';
|
|
2
|
+
|
|
3
|
+
const g = colors.green;
|
|
4
|
+
const d = colors.dim;
|
|
5
|
+
const b = colors.bright;
|
|
6
|
+
const r = colors.reset;
|
|
7
|
+
|
|
8
|
+
export const BANNER = `
|
|
9
|
+
${d} ╭──────────────────────────────────────╮${r}
|
|
10
|
+
${d} │${r} ${g}>_${r} ${b}Nemus${r} ${d}│${r}
|
|
11
|
+
${d} │${r} ${d}multi-repo workspaces${r} ${d}│${r}
|
|
12
|
+
${d} ╰──────────────────────────────────────╯${r}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const BANNER_PLAIN = `
|
|
16
|
+
╭──────────────────────────────────────╮
|
|
17
|
+
│ >_ Nemus │
|
|
18
|
+
│ multi-repo workspaces │
|
|
19
|
+
╰──────────────────────────────────────╯
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export const printBanner = (): void => {
|
|
23
|
+
console.log(BANNER);
|
|
24
|
+
};
|