@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,74 @@
|
|
|
1
|
+
import { exec, execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { GitHubRepo } from '../types';
|
|
4
|
+
import { logError, logInfo } from './logger';
|
|
5
|
+
import { getCachedRepos, setCachedRepos } from './cache';
|
|
6
|
+
import { getUserConfig } from './config';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
export const verifyGhAuth = async (): Promise<boolean> => {
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await execAsync('gh auth status');
|
|
14
|
+
return stdout.includes('Logged in') || stdout.includes('✓');
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const fetchOrgRepos = async (options: { forceRefresh?: boolean } = {}): Promise<GitHubRepo[]> => {
|
|
21
|
+
const { forceRefresh = false } = options;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { githubOrg } = getUserConfig();
|
|
25
|
+
|
|
26
|
+
// Try to get cached repos first
|
|
27
|
+
if (!forceRefresh) {
|
|
28
|
+
const cachedRepos = await getCachedRepos({ org: githubOrg });
|
|
29
|
+
if (cachedRepos) {
|
|
30
|
+
return cachedRepos.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
logInfo(
|
|
34
|
+
githubOrg
|
|
35
|
+
? `Fetching ${githubOrg} repositories from GitHub...`
|
|
36
|
+
: 'Fetching your GitHub repositories... (set an org with `nemus configure` to list an organization)'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// With no configured org, `gh repo list` (no positional) lists the
|
|
40
|
+
// authenticated user's own repos. With an org, it lists that org's repos.
|
|
41
|
+
const args = [
|
|
42
|
+
'repo', 'list',
|
|
43
|
+
...(githubOrg ? [githubOrg] : []),
|
|
44
|
+
'--limit', '1000',
|
|
45
|
+
'--json', 'name,url,sshUrl,owner,description,isPrivate',
|
|
46
|
+
];
|
|
47
|
+
const { stdout } = await execFileAsync('gh', args);
|
|
48
|
+
|
|
49
|
+
const repos: GitHubRepo[] = JSON.parse(stdout);
|
|
50
|
+
|
|
51
|
+
logInfo(`Found ${repos.length} repositories`);
|
|
52
|
+
|
|
53
|
+
// Cache the results (24 hour TTL)
|
|
54
|
+
await setCachedRepos(repos, 86400000, githubOrg);
|
|
55
|
+
|
|
56
|
+
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logError('Failed to fetch repositories from GitHub');
|
|
59
|
+
if (error instanceof Error) {
|
|
60
|
+
logError(error.message);
|
|
61
|
+
}
|
|
62
|
+
throw new Error('Unable to fetch repositories. Please ensure gh CLI is authenticated.');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const displayAuthInstructions = (): void => {
|
|
67
|
+
console.log('\n' + '='.repeat(60));
|
|
68
|
+
console.log('GitHub CLI Authentication Required');
|
|
69
|
+
console.log('='.repeat(60));
|
|
70
|
+
console.log('\nPlease authenticate with GitHub CLI by running:');
|
|
71
|
+
console.log('\n gh auth login\n');
|
|
72
|
+
console.log('Then try running this command again.');
|
|
73
|
+
console.log('='.repeat(60) + '\n');
|
|
74
|
+
};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { HealthCheckResult, WorkspaceMetadata } from '../types';
|
|
6
|
+
import { isGitRepository, hasUncommittedChanges } from './git-status';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
export const checkMissingRepositories = async (
|
|
11
|
+
workspacePath: string,
|
|
12
|
+
metadata: WorkspaceMetadata
|
|
13
|
+
): Promise<HealthCheckResult> => {
|
|
14
|
+
const missingRepos: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (const repo of metadata.repositories) {
|
|
17
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(repoPath);
|
|
20
|
+
} catch {
|
|
21
|
+
missingRepos.push(repo.directoryName);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (missingRepos.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
category: 'Missing Repositories',
|
|
28
|
+
status: 'healthy',
|
|
29
|
+
message: 'All repositories are present',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
category: 'Missing Repositories',
|
|
35
|
+
status: 'error',
|
|
36
|
+
message: `${missingRepos.length} repository(ies) missing`,
|
|
37
|
+
details: missingRepos.join(', '),
|
|
38
|
+
actionable: `Run 'workspace sync ${metadata.workspaceName}' to clone missing repositories`,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const checkGitHealth = async (
|
|
43
|
+
workspacePath: string,
|
|
44
|
+
repoNames: string[]
|
|
45
|
+
): Promise<HealthCheckResult> => {
|
|
46
|
+
const issues: string[] = [];
|
|
47
|
+
|
|
48
|
+
for (const repoName of repoNames) {
|
|
49
|
+
const repoPath = path.join(workspacePath, repoName);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(repoPath);
|
|
53
|
+
} catch {
|
|
54
|
+
continue; // Skip missing repos (handled by checkMissingRepositories)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isRepo = await isGitRepository(repoPath);
|
|
58
|
+
if (!isRepo) {
|
|
59
|
+
issues.push(`${repoName}: not a git repository`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for corrupted repo
|
|
64
|
+
try {
|
|
65
|
+
await execAsync('git fsck --no-progress', {
|
|
66
|
+
cwd: repoPath,
|
|
67
|
+
timeout: 30000,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
issues.push(`${repoName}: repository may be corrupted`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (issues.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
category: 'Git Health',
|
|
77
|
+
status: 'healthy',
|
|
78
|
+
message: 'All repositories are healthy',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
category: 'Git Health',
|
|
84
|
+
status: 'error',
|
|
85
|
+
message: `${issues.length} repository(ies) have issues`,
|
|
86
|
+
details: issues.join('\n'),
|
|
87
|
+
actionable: 'Check repository integrity and consider re-cloning problematic repos',
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const checkUncommittedChanges = async (
|
|
92
|
+
workspacePath: string,
|
|
93
|
+
repoNames: string[]
|
|
94
|
+
): Promise<HealthCheckResult> => {
|
|
95
|
+
const dirtyRepos: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const repoName of repoNames) {
|
|
98
|
+
const repoPath = path.join(workspacePath, repoName);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await fs.access(repoPath);
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hasChanges = await hasUncommittedChanges(repoPath);
|
|
107
|
+
if (hasChanges) {
|
|
108
|
+
dirtyRepos.push(repoName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (dirtyRepos.length === 0) {
|
|
113
|
+
return {
|
|
114
|
+
category: 'Uncommitted Changes',
|
|
115
|
+
status: 'healthy',
|
|
116
|
+
message: 'No uncommitted changes',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
category: 'Uncommitted Changes',
|
|
122
|
+
status: 'warning',
|
|
123
|
+
message: `${dirtyRepos.length} repository(ies) have uncommitted changes`,
|
|
124
|
+
details: dirtyRepos.join(', '),
|
|
125
|
+
actionable: `Review and commit or stash changes before switching branches`,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const checkAuthentication = async (): Promise<HealthCheckResult> => {
|
|
130
|
+
const issues: string[] = [];
|
|
131
|
+
|
|
132
|
+
// Check GitHub CLI
|
|
133
|
+
try {
|
|
134
|
+
await execAsync('gh auth status', { timeout: 5000 });
|
|
135
|
+
} catch {
|
|
136
|
+
issues.push('GitHub CLI (gh) not authenticated');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check SSH keys
|
|
140
|
+
try {
|
|
141
|
+
await execAsync('ssh -T git@github.com', { timeout: 5000 });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
144
|
+
if (!errorMessage.includes('successfully authenticated')) {
|
|
145
|
+
issues.push('SSH key not configured for GitHub');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (issues.length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
category: 'Authentication',
|
|
152
|
+
status: 'healthy',
|
|
153
|
+
message: 'Authentication configured correctly',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
category: 'Authentication',
|
|
159
|
+
status: 'warning',
|
|
160
|
+
message: 'Authentication issues detected',
|
|
161
|
+
details: issues.join('\n'),
|
|
162
|
+
actionable: 'Run "gh auth login" or configure SSH keys',
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const checkDependencies = async (
|
|
167
|
+
workspacePath: string,
|
|
168
|
+
repoNames: string[]
|
|
169
|
+
): Promise<HealthCheckResult> => {
|
|
170
|
+
const missingDeps: string[] = [];
|
|
171
|
+
|
|
172
|
+
for (const repoName of repoNames) {
|
|
173
|
+
const repoPath = path.join(workspacePath, repoName);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await fs.access(repoPath);
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check for package.json
|
|
182
|
+
const packageJsonPath = path.join(repoPath, 'package.json');
|
|
183
|
+
try {
|
|
184
|
+
await fs.access(packageJsonPath);
|
|
185
|
+
|
|
186
|
+
// Check if node_modules exists
|
|
187
|
+
const nodeModulesPath = path.join(repoPath, 'node_modules');
|
|
188
|
+
try {
|
|
189
|
+
await fs.access(nodeModulesPath);
|
|
190
|
+
} catch {
|
|
191
|
+
missingDeps.push(`${repoName}: missing node_modules`);
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// No package.json, skip
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (missingDeps.length === 0) {
|
|
199
|
+
return {
|
|
200
|
+
category: 'Dependencies',
|
|
201
|
+
status: 'healthy',
|
|
202
|
+
message: 'All dependencies installed',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
category: 'Dependencies',
|
|
208
|
+
status: 'warning',
|
|
209
|
+
message: `${missingDeps.length} repository(ies) missing dependencies`,
|
|
210
|
+
details: missingDeps.join('\n'),
|
|
211
|
+
actionable: 'Run "npm install" or "yarn install" in affected repositories',
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const checkDiskSpace = async (workspacePath: string): Promise<HealthCheckResult> => {
|
|
216
|
+
try {
|
|
217
|
+
// Get disk usage for workspace
|
|
218
|
+
const { stdout } = await execAsync(`du -sh "${workspacePath}"`, { timeout: 30000 });
|
|
219
|
+
const sizeStr = stdout.trim().split('\t')[0];
|
|
220
|
+
|
|
221
|
+
// Parse size (rough check for > 10GB)
|
|
222
|
+
const sizeValue = parseFloat(sizeStr);
|
|
223
|
+
const unit = sizeStr.replace(/[0-9.]/g, '').trim();
|
|
224
|
+
|
|
225
|
+
const isLarge = (unit === 'G' && sizeValue > 10) || unit === 'T';
|
|
226
|
+
|
|
227
|
+
if (isLarge) {
|
|
228
|
+
return {
|
|
229
|
+
category: 'Disk Space',
|
|
230
|
+
status: 'warning',
|
|
231
|
+
message: `Workspace is large (${sizeStr})`,
|
|
232
|
+
actionable: 'Consider running "workspace cleanup" to free up space',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
category: 'Disk Space',
|
|
238
|
+
status: 'healthy',
|
|
239
|
+
message: `Workspace size: ${sizeStr}`,
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
category: 'Disk Space',
|
|
244
|
+
status: 'warning',
|
|
245
|
+
message: 'Could not calculate disk usage',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const checkMetadataIntegrity = async (
|
|
251
|
+
workspacePath: string,
|
|
252
|
+
metadata: WorkspaceMetadata
|
|
253
|
+
): Promise<HealthCheckResult> => {
|
|
254
|
+
const issues: string[] = [];
|
|
255
|
+
|
|
256
|
+
// Check required fields
|
|
257
|
+
if (!metadata.workspaceName) {
|
|
258
|
+
issues.push('Missing workspace name');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!metadata.createdAt) {
|
|
262
|
+
issues.push('Missing creation timestamp');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!metadata.repositories || metadata.repositories.length === 0) {
|
|
266
|
+
issues.push('No repositories in metadata');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for duplicate directory names
|
|
270
|
+
const dirNames = metadata.repositories.map(r => r.directoryName);
|
|
271
|
+
const uniqueDirNames = new Set(dirNames);
|
|
272
|
+
if (dirNames.length !== uniqueDirNames.size) {
|
|
273
|
+
issues.push('Duplicate directory names in metadata');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for extra directories not in metadata
|
|
277
|
+
try {
|
|
278
|
+
const entries = await fs.readdir(workspacePath, { withFileTypes: true });
|
|
279
|
+
const directories = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
280
|
+
const metadataDirNames = new Set(dirNames);
|
|
281
|
+
const extraDirs = directories.filter(
|
|
282
|
+
dir => !metadataDirNames.has(dir) && !dir.startsWith('.')
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (extraDirs.length > 0) {
|
|
286
|
+
issues.push(`${extraDirs.length} directories not tracked in metadata: ${extraDirs.join(', ')}`);
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Ignore read errors
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (issues.length === 0) {
|
|
293
|
+
return {
|
|
294
|
+
category: 'Metadata Integrity',
|
|
295
|
+
status: 'healthy',
|
|
296
|
+
message: 'Metadata is valid',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
category: 'Metadata Integrity',
|
|
302
|
+
status: 'warning',
|
|
303
|
+
message: 'Metadata issues detected',
|
|
304
|
+
details: issues.join('\n'),
|
|
305
|
+
actionable: 'Review workspace metadata file',
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export const checkGhqInstalled = async (): Promise<HealthCheckResult> => {
|
|
310
|
+
try {
|
|
311
|
+
await execAsync('which ghq', { timeout: 5000 });
|
|
312
|
+
return {
|
|
313
|
+
category: 'ghq',
|
|
314
|
+
status: 'healthy',
|
|
315
|
+
message: 'ghq is installed (fast cached clones enabled)',
|
|
316
|
+
};
|
|
317
|
+
} catch {
|
|
318
|
+
return {
|
|
319
|
+
category: 'ghq',
|
|
320
|
+
status: 'warning',
|
|
321
|
+
message: 'ghq is not installed',
|
|
322
|
+
details: 'Without ghq, repos are cloned directly with git — slower, no shared cache, and large repos can hit clone timeouts.',
|
|
323
|
+
actionable: 'Install ghq: brew install ghq (macOS) — https://github.com/x-motemen/ghq#installation',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const runAllHealthChecks = async (
|
|
329
|
+
workspacePath: string,
|
|
330
|
+
metadata: WorkspaceMetadata
|
|
331
|
+
): Promise<HealthCheckResult[]> => {
|
|
332
|
+
const repoNames = metadata.repositories.map(r => r.directoryName);
|
|
333
|
+
|
|
334
|
+
const checks = await Promise.all([
|
|
335
|
+
checkMissingRepositories(workspacePath, metadata),
|
|
336
|
+
checkGitHealth(workspacePath, repoNames),
|
|
337
|
+
checkUncommittedChanges(workspacePath, repoNames),
|
|
338
|
+
checkAuthentication(),
|
|
339
|
+
checkGhqInstalled(),
|
|
340
|
+
checkDependencies(workspacePath, repoNames),
|
|
341
|
+
checkDiskSpace(workspacePath),
|
|
342
|
+
checkMetadataIntegrity(workspacePath, metadata),
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
return checks;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const calculateHealthScore = (results: HealthCheckResult[]): number => {
|
|
349
|
+
const weights = {
|
|
350
|
+
healthy: 100,
|
|
351
|
+
warning: 60,
|
|
352
|
+
error: 0,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const totalWeight = results.reduce((sum, result) => sum + weights[result.status], 0);
|
|
356
|
+
const maxWeight = results.length * 100;
|
|
357
|
+
|
|
358
|
+
return Math.round((totalWeight / maxWeight) * 100);
|
|
359
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
const CACHE_DIR = path.join(os.homedir(), '.workspace-manager-cache');
|
|
6
|
+
const HISTORY_FILE = path.join(CACHE_DIR, 'history.jsonl');
|
|
7
|
+
|
|
8
|
+
export interface OperationRecord {
|
|
9
|
+
timestamp: string;
|
|
10
|
+
command: string;
|
|
11
|
+
workspace?: string;
|
|
12
|
+
duration: number;
|
|
13
|
+
success: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ensureCacheDir = async (): Promise<void> => {
|
|
18
|
+
try {
|
|
19
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore if already exists
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const logOperation = async (record: OperationRecord): Promise<void> => {
|
|
26
|
+
await ensureCacheDir();
|
|
27
|
+
|
|
28
|
+
const line = JSON.stringify(record) + '\n';
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await fs.appendFile(HISTORY_FILE, line, 'utf-8');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Silently fail - don't block operations if logging fails
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const readHistory = async (limit?: number): Promise<OperationRecord[]> => {
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(HISTORY_FILE, 'utf-8');
|
|
40
|
+
const lines = content.trim().split('\n');
|
|
41
|
+
const records = lines
|
|
42
|
+
.filter(line => line.trim())
|
|
43
|
+
.map(line => {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(line) as OperationRecord;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.filter((record): record is OperationRecord => record !== null);
|
|
51
|
+
|
|
52
|
+
// Return most recent first
|
|
53
|
+
const reversed = records.reverse();
|
|
54
|
+
|
|
55
|
+
return limit ? reversed.slice(0, limit) : reversed;
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getOperationStats = async (): Promise<{
|
|
62
|
+
totalOperations: number;
|
|
63
|
+
successRate: number;
|
|
64
|
+
avgDuration: number;
|
|
65
|
+
commandCounts: Record<string, number>;
|
|
66
|
+
}> => {
|
|
67
|
+
const history = await readHistory();
|
|
68
|
+
|
|
69
|
+
if (history.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
totalOperations: 0,
|
|
72
|
+
successRate: 0,
|
|
73
|
+
avgDuration: 0,
|
|
74
|
+
commandCounts: {},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const successful = history.filter(r => r.success).length;
|
|
79
|
+
const totalDuration = history.reduce((sum, r) => sum + r.duration, 0);
|
|
80
|
+
const commandCounts: Record<string, number> = {};
|
|
81
|
+
|
|
82
|
+
for (const record of history) {
|
|
83
|
+
commandCounts[record.command] = (commandCounts[record.command] || 0) + 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
totalOperations: history.length,
|
|
88
|
+
successRate: (successful / history.length) * 100,
|
|
89
|
+
avgDuration: totalDuration / history.length,
|
|
90
|
+
commandCounts,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const filterHistory = async (
|
|
95
|
+
command?: string,
|
|
96
|
+
workspace?: string,
|
|
97
|
+
successOnly?: boolean
|
|
98
|
+
): Promise<OperationRecord[]> => {
|
|
99
|
+
const history = await readHistory();
|
|
100
|
+
|
|
101
|
+
return history.filter(record => {
|
|
102
|
+
if (command && record.command !== command) return false;
|
|
103
|
+
if (workspace && record.workspace !== workspace) return false;
|
|
104
|
+
if (successOnly && !record.success) return false;
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const clearHistory = async (): Promise<void> => {
|
|
110
|
+
try {
|
|
111
|
+
await fs.unlink(HISTORY_FILE);
|
|
112
|
+
} catch {
|
|
113
|
+
// Ignore if file doesn't exist
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export class OperationTimer {
|
|
118
|
+
private startTime: number;
|
|
119
|
+
private command: string;
|
|
120
|
+
private workspace?: string;
|
|
121
|
+
|
|
122
|
+
constructor(command: string, workspace?: string) {
|
|
123
|
+
this.command = command;
|
|
124
|
+
this.workspace = workspace;
|
|
125
|
+
this.startTime = Date.now();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async end(success: boolean, error?: string): Promise<void> {
|
|
129
|
+
const duration = Date.now() - this.startTime;
|
|
130
|
+
|
|
131
|
+
await logOperation({
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
command: this.command,
|
|
134
|
+
workspace: this.workspace,
|
|
135
|
+
duration,
|
|
136
|
+
success,
|
|
137
|
+
error,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { RepositoryMetadata, PostCloneHook } from '../types';
|
|
5
|
+
import { logInfo, logSuccess, logError, logWarning } from './logger';
|
|
6
|
+
import { colorize } from './colors';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
const HOOK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
11
|
+
|
|
12
|
+
export interface HookResult {
|
|
13
|
+
repo: string;
|
|
14
|
+
command: string;
|
|
15
|
+
status: 'success' | 'failed';
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runPostCloneHooks(
|
|
20
|
+
workspacePath: string,
|
|
21
|
+
repositories: RepositoryMetadata[],
|
|
22
|
+
hooks?: PostCloneHook[]
|
|
23
|
+
): Promise<HookResult[]> {
|
|
24
|
+
if (!hooks || hooks.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const results: HookResult[] = [];
|
|
29
|
+
|
|
30
|
+
logInfo('Running post-clone hooks...');
|
|
31
|
+
console.log('');
|
|
32
|
+
|
|
33
|
+
for (const hook of hooks) {
|
|
34
|
+
if (hook.description) {
|
|
35
|
+
logInfo(`Hook: ${colorize(hook.description, 'cyan')}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Determine target repos
|
|
39
|
+
const targetRepos = hook.repoName
|
|
40
|
+
? repositories.filter(r => r.name === hook.repoName || r.directoryName === hook.repoName)
|
|
41
|
+
: repositories;
|
|
42
|
+
|
|
43
|
+
if (targetRepos.length === 0) {
|
|
44
|
+
if (hook.repoName) {
|
|
45
|
+
logWarning(`No matching repo found for hook target: ${hook.repoName}`);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const repo of targetRepos) {
|
|
51
|
+
const repoPath = path.join(workspacePath, repo.directoryName);
|
|
52
|
+
|
|
53
|
+
for (const command of hook.commands) {
|
|
54
|
+
try {
|
|
55
|
+
logInfo(` [${colorize(repo.directoryName, 'cyan')}] ${command}`);
|
|
56
|
+
await execAsync(command, { cwd: repoPath, timeout: HOOK_TIMEOUT });
|
|
57
|
+
|
|
58
|
+
results.push({
|
|
59
|
+
repo: repo.directoryName,
|
|
60
|
+
command,
|
|
61
|
+
status: 'success',
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
65
|
+
|
|
66
|
+
results.push({
|
|
67
|
+
repo: repo.directoryName,
|
|
68
|
+
command,
|
|
69
|
+
status: 'failed',
|
|
70
|
+
error: errorMessage,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
logError(` [${repo.directoryName}] Failed: ${errorMessage}`);
|
|
74
|
+
|
|
75
|
+
if (!hook.continueOnError) {
|
|
76
|
+
logWarning('Hook execution stopped due to error (continueOnError is false)');
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Log summary
|
|
85
|
+
const successful = results.filter(r => r.status === 'success').length;
|
|
86
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
87
|
+
|
|
88
|
+
console.log('');
|
|
89
|
+
logInfo(`Hook results: ${colorize(String(successful), 'green')} successful, ${colorize(String(failed), 'red')} failed`);
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { colors, colorize } from './colors';
|
|
2
|
+
|
|
3
|
+
const getTimestamp = (): string => {
|
|
4
|
+
const now = new Date();
|
|
5
|
+
return now.toLocaleTimeString('en-US', {
|
|
6
|
+
hour12: false,
|
|
7
|
+
hour: '2-digit',
|
|
8
|
+
minute: '2-digit',
|
|
9
|
+
second: '2-digit'
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const logInfo = (message: string): void => {
|
|
14
|
+
console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${message}`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const logSuccess = (message: string): void => {
|
|
18
|
+
console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('✓', 'green')} ${message}`);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const logError = (message: string): void => {
|
|
22
|
+
console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('✗', 'red')} ${message}`);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const logWarning = (message: string): void => {
|
|
26
|
+
console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('⚠', 'yellow')} ${message}`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const logStep = (stepOrMessage: number | string, total?: number, message?: string): void => {
|
|
30
|
+
if (typeof stepOrMessage === 'string') {
|
|
31
|
+
// Single parameter version: just a message
|
|
32
|
+
console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize('▸', 'cyan')} ${stepOrMessage}`);
|
|
33
|
+
} else {
|
|
34
|
+
// Three parameter version: step, total, message
|
|
35
|
+
console.log(`${colors.gray}[${getTimestamp()}]${colors.reset} ${colorize(`[${stepOrMessage}/${total}]`, 'cyan')} ${message}`);
|
|
36
|
+
}
|
|
37
|
+
};
|