@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,203 @@
|
|
|
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
|
+
vi.mock('os', async () => {
|
|
7
|
+
const actual = await vi.importActual<typeof import('os')>('os');
|
|
8
|
+
return { ...actual, homedir: vi.fn(actual.homedir) };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('config', () => {
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
const originalEnv = { ...process.env };
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-config-test-'));
|
|
17
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
18
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
19
|
+
process.env.WORKSPACE_MANAGER_DIR = path.join(tempDir, 'workspaces');
|
|
20
|
+
process.env.WORKSPACE_MANAGER_CACHE_DIR = cacheDir;
|
|
21
|
+
vi.mocked(os.homedir).mockReturnValue(tempDir);
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.env = { ...originalEnv };
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function loadModule() {
|
|
32
|
+
return await import('./config');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('getCloneUrl', () => {
|
|
36
|
+
it('returns sshUrl by default (ssh protocol)', async () => {
|
|
37
|
+
const { getCloneUrl } = await loadModule();
|
|
38
|
+
const repo = { url: 'https://github.com/acme/my-repo', sshUrl: 'git@github.com:acme/my-repo.git' };
|
|
39
|
+
expect(getCloneUrl(repo)).toBe('git@github.com:acme/my-repo.git');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns https url with .git suffix when protocol is https', async () => {
|
|
43
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(cacheDir, 'config.json'),
|
|
46
|
+
JSON.stringify({ cloneProtocol: 'https' })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const { getCloneUrl } = await loadModule();
|
|
50
|
+
const repo = { url: 'https://github.com/acme/my-repo', sshUrl: 'git@github.com:acme/my-repo.git' };
|
|
51
|
+
expect(getCloneUrl(repo)).toBe('https://github.com/acme/my-repo.git');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not double-append .git', async () => {
|
|
55
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.join(cacheDir, 'config.json'),
|
|
58
|
+
JSON.stringify({ cloneProtocol: 'https' })
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const { getCloneUrl } = await loadModule();
|
|
62
|
+
const repo = { url: 'https://github.com/acme/my-repo.git', sshUrl: 'git@github.com:acme/my-repo.git' };
|
|
63
|
+
expect(getCloneUrl(repo)).toBe('https://github.com/acme/my-repo.git');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getPackageVersion', () => {
|
|
68
|
+
it('returns a semver-like string', async () => {
|
|
69
|
+
const { getPackageVersion } = await loadModule();
|
|
70
|
+
const version = getPackageVersion();
|
|
71
|
+
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getUserConfig', () => {
|
|
76
|
+
it('returns defaults when no config file exists', async () => {
|
|
77
|
+
const { getUserConfig } = await loadModule();
|
|
78
|
+
const cfg = getUserConfig();
|
|
79
|
+
expect(cfg.githubOrg).toBe('');
|
|
80
|
+
expect(cfg.cloneProtocol).toBe('ssh');
|
|
81
|
+
expect(cfg.autoLaunchClaude).toBe(true);
|
|
82
|
+
expect(cfg.generateClaudeContext).toBe(true);
|
|
83
|
+
expect(cfg.installMcp).toBe(true);
|
|
84
|
+
expect(cfg.aiAgent).toBe('auto');
|
|
85
|
+
expect(cfg.primaryAgent).toBe('auto');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reads fresh values from disk on each call', async () => {
|
|
89
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
path.join(cacheDir, 'config.json'),
|
|
92
|
+
JSON.stringify({ githubOrg: 'org-a' })
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const { getUserConfig } = await loadModule();
|
|
96
|
+
expect(getUserConfig().githubOrg).toBe('org-a');
|
|
97
|
+
|
|
98
|
+
// Update config on disk
|
|
99
|
+
fs.writeFileSync(
|
|
100
|
+
path.join(cacheDir, 'config.json'),
|
|
101
|
+
JSON.stringify({ githubOrg: 'org-b' })
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Should see the new value without re-importing
|
|
105
|
+
expect(getUserConfig().githubOrg).toBe('org-b');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('ignores invalid field types from config file', async () => {
|
|
109
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
path.join(cacheDir, 'config.json'),
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
githubOrg: 123,
|
|
114
|
+
cloneProtocol: 'ftp',
|
|
115
|
+
autoLaunchClaude: 'yes',
|
|
116
|
+
workspacesDir: null,
|
|
117
|
+
installMcp: 'true',
|
|
118
|
+
aiAgent: 'invalid',
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const { getUserConfig } = await loadModule();
|
|
123
|
+
const cfg = getUserConfig();
|
|
124
|
+
// All invalid values should be ignored, defaults used
|
|
125
|
+
expect(cfg.githubOrg).toBe('');
|
|
126
|
+
expect(cfg.cloneProtocol).toBe('ssh');
|
|
127
|
+
expect(cfg.autoLaunchClaude).toBe(true);
|
|
128
|
+
expect(cfg.workspacesDir).toContain('workspaces');
|
|
129
|
+
expect(cfg.installMcp).toBe(true);
|
|
130
|
+
expect(cfg.aiAgent).toBe('auto');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('saveUserConfig', () => {
|
|
135
|
+
it('writes config to disk and can be read back', async () => {
|
|
136
|
+
const { saveUserConfig, getUserConfig } = await loadModule();
|
|
137
|
+
|
|
138
|
+
saveUserConfig({
|
|
139
|
+
workspacesDir: '/custom/path',
|
|
140
|
+
githubOrg: 'test-org',
|
|
141
|
+
autoLaunchClaude: false,
|
|
142
|
+
generateClaudeContext: false,
|
|
143
|
+
cloneProtocol: 'https',
|
|
144
|
+
installMcp: false,
|
|
145
|
+
aiAgent: 'pi',
|
|
146
|
+
primaryAgent: 'pi',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const cfg = getUserConfig();
|
|
150
|
+
expect(cfg.githubOrg).toBe('test-org');
|
|
151
|
+
expect(cfg.cloneProtocol).toBe('https');
|
|
152
|
+
expect(cfg.autoLaunchClaude).toBe(false);
|
|
153
|
+
expect(cfg.aiAgent).toBe('pi');
|
|
154
|
+
expect(cfg.primaryAgent).toBe('pi');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('aiAgent config', () => {
|
|
159
|
+
it('reads aiAgent "claude" from config file', async () => {
|
|
160
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
path.join(cacheDir, 'config.json'),
|
|
163
|
+
JSON.stringify({ aiAgent: 'claude' })
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const { getUserConfig } = await loadModule();
|
|
167
|
+
expect(getUserConfig().aiAgent).toBe('claude');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('reads aiAgent "pi" from config file', async () => {
|
|
171
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
172
|
+
fs.writeFileSync(
|
|
173
|
+
path.join(cacheDir, 'config.json'),
|
|
174
|
+
JSON.stringify({ aiAgent: 'pi' })
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const { getUserConfig } = await loadModule();
|
|
178
|
+
expect(getUserConfig().aiAgent).toBe('pi');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('reads aiAgent "both" from config file', async () => {
|
|
182
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
path.join(cacheDir, 'config.json'),
|
|
185
|
+
JSON.stringify({ aiAgent: 'both' })
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const { getUserConfig } = await loadModule();
|
|
189
|
+
expect(getUserConfig().aiAgent).toBe('both');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('ignores invalid aiAgent values', async () => {
|
|
193
|
+
const cacheDir = path.join(tempDir, '.workspace-manager-cache');
|
|
194
|
+
fs.writeFileSync(
|
|
195
|
+
path.join(cacheDir, 'config.json'),
|
|
196
|
+
JSON.stringify({ aiAgent: 'copilot' })
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const { getUserConfig } = await loadModule();
|
|
200
|
+
expect(getUserConfig().aiAgent).toBe('auto');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
const HOME_DIR = os.homedir();
|
|
6
|
+
|
|
7
|
+
// Config file lives outside CACHE_DIR so we can read it to determine CACHE_DIR
|
|
8
|
+
const CONFIG_FILE = path.join(HOME_DIR, '.workspace-manager-cache', 'config.json');
|
|
9
|
+
|
|
10
|
+
/** Coding-agent CLIs Nemus can integrate with. */
|
|
11
|
+
export type ConcreteAgentType = 'claude' | 'pi' | 'opencode' | 'codex' | 'gemini';
|
|
12
|
+
export type AgentType = ConcreteAgentType | 'both' | 'auto';
|
|
13
|
+
export type PrimaryAgentType = ConcreteAgentType | 'auto';
|
|
14
|
+
|
|
15
|
+
export interface UserConfig {
|
|
16
|
+
workspacesDir: string;
|
|
17
|
+
githubOrg: string;
|
|
18
|
+
autoLaunchClaude: boolean;
|
|
19
|
+
generateClaudeContext: boolean;
|
|
20
|
+
cloneProtocol: 'ssh' | 'https';
|
|
21
|
+
installMcp: boolean;
|
|
22
|
+
/** Which AI agent(s) to integrate with: 'claude', 'pi', 'both', or 'auto' (detect from installed CLIs) */
|
|
23
|
+
aiAgent: AgentType;
|
|
24
|
+
/** Which agent to launch when opening workspaces: 'claude', 'pi', or 'auto' (first available) */
|
|
25
|
+
primaryAgent: PrimaryAgentType;
|
|
26
|
+
/** Show workspace status widget in Pi's input area (branch, PRs, CI). Pi only. */
|
|
27
|
+
piWorkspaceInputStatus: boolean;
|
|
28
|
+
/** Show workspace repo table in Claude Code's status line. Claude only. */
|
|
29
|
+
claudeWorkspaceStatusLine: boolean;
|
|
30
|
+
/** Automatically file a GitHub issue when a command crashes (deduped, sanitized). */
|
|
31
|
+
autoReportBugs: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULTS: UserConfig = {
|
|
35
|
+
workspacesDir: path.join(HOME_DIR, 'workspaces'),
|
|
36
|
+
githubOrg: '',
|
|
37
|
+
autoLaunchClaude: true,
|
|
38
|
+
generateClaudeContext: true,
|
|
39
|
+
cloneProtocol: 'ssh',
|
|
40
|
+
installMcp: true,
|
|
41
|
+
aiAgent: 'auto',
|
|
42
|
+
primaryAgent: 'auto',
|
|
43
|
+
piWorkspaceInputStatus: true,
|
|
44
|
+
claudeWorkspaceStatusLine: true,
|
|
45
|
+
autoReportBugs: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function loadConfigFileSync(): Partial<UserConfig> {
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
51
|
+
const raw = JSON.parse(content);
|
|
52
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {};
|
|
53
|
+
|
|
54
|
+
const result: Partial<UserConfig> = {};
|
|
55
|
+
if (typeof raw.workspacesDir === 'string' && raw.workspacesDir) result.workspacesDir = raw.workspacesDir;
|
|
56
|
+
if (typeof raw.githubOrg === 'string' && raw.githubOrg) result.githubOrg = raw.githubOrg;
|
|
57
|
+
if (typeof raw.autoLaunchClaude === 'boolean') result.autoLaunchClaude = raw.autoLaunchClaude;
|
|
58
|
+
if (typeof raw.generateClaudeContext === 'boolean') result.generateClaudeContext = raw.generateClaudeContext;
|
|
59
|
+
if (raw.cloneProtocol === 'ssh' || raw.cloneProtocol === 'https') result.cloneProtocol = raw.cloneProtocol;
|
|
60
|
+
if (typeof raw.installMcp === 'boolean') result.installMcp = raw.installMcp;
|
|
61
|
+
const agentValues = ['claude', 'pi', 'opencode', 'codex', 'gemini'];
|
|
62
|
+
if (agentValues.includes(raw.aiAgent) || raw.aiAgent === 'both' || raw.aiAgent === 'auto') result.aiAgent = raw.aiAgent;
|
|
63
|
+
if (agentValues.includes(raw.primaryAgent) || raw.primaryAgent === 'auto') result.primaryAgent = raw.primaryAgent;
|
|
64
|
+
if (typeof raw.piWorkspaceInputStatus === 'boolean') result.piWorkspaceInputStatus = raw.piWorkspaceInputStatus;
|
|
65
|
+
if (typeof raw.claudeWorkspaceStatusLine === 'boolean') result.claudeWorkspaceStatusLine = raw.claudeWorkspaceStatusLine;
|
|
66
|
+
if (typeof raw.autoReportBugs === 'boolean') result.autoReportBugs = raw.autoReportBugs;
|
|
67
|
+
return result;
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get the full resolved user config (defaults + file overrides). Always reads from disk. */
|
|
74
|
+
export function getUserConfig(): UserConfig {
|
|
75
|
+
return { ...DEFAULTS, ...loadConfigFileSync() };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Bootstrap constants from initial config read (env vars take precedence)
|
|
79
|
+
const initialConfig = getUserConfig();
|
|
80
|
+
|
|
81
|
+
export const WORKSPACES_DIR = process.env.WORKSPACE_MANAGER_DIR || initialConfig.workspacesDir;
|
|
82
|
+
export const CACHE_DIR = process.env.WORKSPACE_MANAGER_CACHE_DIR || path.join(HOME_DIR, '.workspace-manager-cache');
|
|
83
|
+
export const HISTORY_FILE = path.join(CACHE_DIR, 'history.jsonl');
|
|
84
|
+
export const SUITES_FILE = path.join(CACHE_DIR, 'suites.json');
|
|
85
|
+
export const META_FILENAME = '.workspace-meta.json';
|
|
86
|
+
export const CONFIG_PATH = CONFIG_FILE;
|
|
87
|
+
|
|
88
|
+
// ── Clone execution limits ──────────────────────────────────────────────────
|
|
89
|
+
// Git clone runs via child_process.exec, which (a) kills the process on
|
|
90
|
+
// timeout and (b) has a small default maxBuffer (1 MB) that a large repo's
|
|
91
|
+
// progress output can blow past. Both surface as confusing "Command failed"
|
|
92
|
+
// errors, so we set generous, overridable limits here.
|
|
93
|
+
export const CLONE_TIMEOUT_MS = (() => {
|
|
94
|
+
const parsed = Number(process.env.WORKSPACE_CLONE_TIMEOUT_MS);
|
|
95
|
+
// Invalid or non-positive values (incl. 0) fall back to the 15-min default.
|
|
96
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 15 * 60 * 1000;
|
|
97
|
+
})();
|
|
98
|
+
export const CLONE_MAX_BUFFER = 64 * 1024 * 1024; // 64 MB
|
|
99
|
+
|
|
100
|
+
export const config = {
|
|
101
|
+
workspacesDir: WORKSPACES_DIR,
|
|
102
|
+
cacheDir: CACHE_DIR,
|
|
103
|
+
historyFile: HISTORY_FILE,
|
|
104
|
+
suitesFile: SUITES_FILE,
|
|
105
|
+
metaFilename: META_FILENAME,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Read the package version from package.json. */
|
|
109
|
+
export function getPackageVersion(): string {
|
|
110
|
+
try {
|
|
111
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
112
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
113
|
+
return pkg.version;
|
|
114
|
+
} catch {
|
|
115
|
+
return '0.0.0';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Get the clone URL for a repo based on the configured protocol. */
|
|
120
|
+
export function getCloneUrl(repo: { url: string; sshUrl: string }): string {
|
|
121
|
+
const { cloneProtocol } = getUserConfig();
|
|
122
|
+
if (cloneProtocol === 'https') {
|
|
123
|
+
return repo.url.endsWith('.git') ? repo.url : `${repo.url}.git`;
|
|
124
|
+
}
|
|
125
|
+
return repo.sshUrl;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Save user config to disk. */
|
|
129
|
+
export function saveUserConfig(cfg: UserConfig): void {
|
|
130
|
+
const dir = path.dirname(CONFIG_FILE);
|
|
131
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
|
|
133
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for CONTEXT.md file formatting.
|
|
3
|
+
* Used by both the CLI (save-context command) and MCP tool (handleSaveContext).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a fresh CONTEXT.md file with header and body.
|
|
8
|
+
*/
|
|
9
|
+
export function formatContextFile(workspaceName: string, body: string, timestamp?: string): string {
|
|
10
|
+
const ts = timestamp || new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, 'Z');
|
|
11
|
+
return `# Workspace Context: ${workspaceName}
|
|
12
|
+
|
|
13
|
+
> This file contains saved progress and context for AI agents.
|
|
14
|
+
> It persists across \`/clear\` and session restarts.
|
|
15
|
+
> Last updated: ${ts}
|
|
16
|
+
|
|
17
|
+
${body}
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Append content to an existing CONTEXT.md, adding a timestamped separator.
|
|
23
|
+
* If the existing file can't be read, returns a fresh formatted file.
|
|
24
|
+
*/
|
|
25
|
+
export function appendToContextFile(
|
|
26
|
+
existingContent: string | null,
|
|
27
|
+
workspaceName: string,
|
|
28
|
+
newContent: string,
|
|
29
|
+
timestamp?: string
|
|
30
|
+
): string {
|
|
31
|
+
const ts = timestamp || new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, 'Z');
|
|
32
|
+
if (existingContent) {
|
|
33
|
+
return existingContent.trimEnd() + `\n\n---\n\n### Update (${ts})\n\n${newContent}`;
|
|
34
|
+
}
|
|
35
|
+
return formatContextFile(workspaceName, newContent, ts);
|
|
36
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
import { HOOK_MARKER } from '../types/dashboard';
|
|
6
|
+
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
let settingsPath: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-'));
|
|
12
|
+
settingsPath = path.join(tmpDir, '.claude', 'settings.json');
|
|
13
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'), { recursive: true });
|
|
14
|
+
// Reset module cache so each test gets a fresh import
|
|
15
|
+
vi.resetModules();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function writeSettings(content: any) {
|
|
24
|
+
fs.writeFileSync(settingsPath, JSON.stringify(content, null, 2), 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readSettings(): any {
|
|
28
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function importModule() {
|
|
32
|
+
// Mock os.homedir to return our temp dir before importing the module
|
|
33
|
+
vi.doMock('os', () => ({
|
|
34
|
+
...os,
|
|
35
|
+
homedir: () => tmpDir,
|
|
36
|
+
}));
|
|
37
|
+
return await import('./dashboard-hooks');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('dashboard-hooks', () => {
|
|
41
|
+
it('installDashboardHooks into empty settings', async () => {
|
|
42
|
+
writeSettings({});
|
|
43
|
+
const { installDashboardHooks } = await importModule();
|
|
44
|
+
installDashboardHooks();
|
|
45
|
+
|
|
46
|
+
const settings = readSettings();
|
|
47
|
+
expect(settings.hooks).toBeDefined();
|
|
48
|
+
expect(settings.hooks.SessionStart).toBeDefined();
|
|
49
|
+
expect(settings.hooks.PreToolUse).toBeDefined();
|
|
50
|
+
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
51
|
+
expect(settings.hooks.UserPromptSubmit).toBeDefined();
|
|
52
|
+
expect(settings.hooks.Notification).toBeDefined();
|
|
53
|
+
expect(settings.hooks.Stop).toBeDefined();
|
|
54
|
+
expect(settings.hooks.SessionEnd).toBeDefined();
|
|
55
|
+
|
|
56
|
+
// Each should have exactly one group with our marker
|
|
57
|
+
for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Notification', 'Stop', 'SessionEnd']) {
|
|
58
|
+
const groups = settings.hooks[event];
|
|
59
|
+
expect(groups.length).toBeGreaterThanOrEqual(1);
|
|
60
|
+
const ourGroup = groups.find((g: any) =>
|
|
61
|
+
g.hooks.some((h: any) => h.command?.includes(HOOK_MARKER))
|
|
62
|
+
);
|
|
63
|
+
expect(ourGroup).toBeDefined();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('installDashboardHooks preserves existing hooks', async () => {
|
|
68
|
+
writeSettings({
|
|
69
|
+
hooks: {
|
|
70
|
+
Notification: [
|
|
71
|
+
{ hooks: [{ type: 'command', command: 'echo existing-notification' }] }
|
|
72
|
+
],
|
|
73
|
+
Stop: [
|
|
74
|
+
{ hooks: [{ type: 'command', command: 'echo existing-stop' }] }
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { installDashboardHooks } = await importModule();
|
|
80
|
+
installDashboardHooks();
|
|
81
|
+
|
|
82
|
+
const settings = readSettings();
|
|
83
|
+
|
|
84
|
+
// Existing hooks should still be there
|
|
85
|
+
expect(settings.hooks.Notification[0].hooks[0].command).toBe('echo existing-notification');
|
|
86
|
+
expect(settings.hooks.Stop[0].hooks[0].command).toBe('echo existing-stop');
|
|
87
|
+
|
|
88
|
+
// Our hooks should be appended
|
|
89
|
+
expect(settings.hooks.Notification.length).toBe(2);
|
|
90
|
+
expect(settings.hooks.Stop.length).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('installDashboardHooks is idempotent', async () => {
|
|
94
|
+
writeSettings({});
|
|
95
|
+
const { installDashboardHooks } = await importModule();
|
|
96
|
+
|
|
97
|
+
installDashboardHooks();
|
|
98
|
+
const firstInstall = readSettings();
|
|
99
|
+
|
|
100
|
+
installDashboardHooks();
|
|
101
|
+
const secondInstall = readSettings();
|
|
102
|
+
|
|
103
|
+
// Should have same number of hook groups
|
|
104
|
+
for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Notification', 'Stop', 'SessionEnd']) {
|
|
105
|
+
expect(secondInstall.hooks[event].length).toBe(firstInstall.hooks[event].length);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('areDashboardHooksInstalled returns false when not installed', async () => {
|
|
110
|
+
writeSettings({});
|
|
111
|
+
const { areDashboardHooksInstalled } = await importModule();
|
|
112
|
+
expect(areDashboardHooksInstalled()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('areDashboardHooksInstalled returns true after install', async () => {
|
|
116
|
+
writeSettings({});
|
|
117
|
+
const { installDashboardHooks, areDashboardHooksInstalled } = await importModule();
|
|
118
|
+
installDashboardHooks();
|
|
119
|
+
expect(areDashboardHooksInstalled()).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uninstallDashboardHooks removes only our hooks', async () => {
|
|
123
|
+
writeSettings({
|
|
124
|
+
hooks: {
|
|
125
|
+
Notification: [
|
|
126
|
+
{ hooks: [{ type: 'command', command: 'echo keep-this' }] },
|
|
127
|
+
{ hooks: [{ type: 'command', command: `node /path/handler --marker ${HOOK_MARKER}` }] },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const { uninstallDashboardHooks } = await importModule();
|
|
133
|
+
uninstallDashboardHooks();
|
|
134
|
+
|
|
135
|
+
const settings = readSettings();
|
|
136
|
+
expect(settings.hooks.Notification.length).toBe(1);
|
|
137
|
+
expect(settings.hooks.Notification[0].hooks[0].command).toBe('echo keep-this');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('uninstallDashboardHooks handles missing settings gracefully', async () => {
|
|
141
|
+
// No settings file — should not throw
|
|
142
|
+
const { uninstallDashboardHooks } = await importModule();
|
|
143
|
+
expect(() => uninstallDashboardHooks()).not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('settings file created when missing', async () => {
|
|
147
|
+
// Ensure no settings file exists
|
|
148
|
+
try { fs.unlinkSync(settingsPath); } catch { /* fine */ }
|
|
149
|
+
|
|
150
|
+
const { installDashboardHooks } = await importModule();
|
|
151
|
+
installDashboardHooks();
|
|
152
|
+
|
|
153
|
+
expect(fs.existsSync(settingsPath)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('PreToolUse hook has empty matcher', async () => {
|
|
157
|
+
writeSettings({});
|
|
158
|
+
const { installDashboardHooks } = await importModule();
|
|
159
|
+
installDashboardHooks();
|
|
160
|
+
|
|
161
|
+
const settings = readSettings();
|
|
162
|
+
const ourGroup = settings.hooks.PreToolUse.find((g: any) =>
|
|
163
|
+
g.hooks.some((h: any) => h.command?.includes(HOOK_MARKER))
|
|
164
|
+
);
|
|
165
|
+
expect(ourGroup).toBeDefined();
|
|
166
|
+
expect(ourGroup.matcher).toBe('');
|
|
167
|
+
});
|
|
168
|
+
});
|