@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,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bug reporting — capture runtime errors and file them as GitHub issues
|
|
3
|
+
* against the project repo.
|
|
4
|
+
*
|
|
5
|
+
* Design goals (see PR discussion):
|
|
6
|
+
* - Sanitize: never leak home dir, usernames, emails, or token-like strings.
|
|
7
|
+
* - Deduplicate: one issue per distinct error signature (search before create).
|
|
8
|
+
* - Safe: only file genuine tool errors; skip clearly-environmental ones unless
|
|
9
|
+
* forced. Auto-filing is opt-in via config (autoReportBugs).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from 'child_process';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
import { CACHE_DIR } from './config';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Repo that bug issues are filed against. Overridable via NEMUS_BUG_REPORT_REPO
|
|
21
|
+
* so forks/downstreams route reports to their own tracker.
|
|
22
|
+
*/
|
|
23
|
+
export const BUG_REPORT_REPO = process.env.NEMUS_BUG_REPORT_REPO || 'me-public/nemus';
|
|
24
|
+
|
|
25
|
+
/** Where the most recent error is captured for `w report-bug`. */
|
|
26
|
+
export const LAST_ERROR_FILE = path.join(CACHE_DIR, 'last-error.json');
|
|
27
|
+
|
|
28
|
+
/** Label applied to auto-filed bug issues (used for dedup search too). */
|
|
29
|
+
export const BUG_LABEL = 'auto-reported';
|
|
30
|
+
|
|
31
|
+
export interface CapturedError {
|
|
32
|
+
/** The command the user ran, e.g. "nemus update --workspace x". */
|
|
33
|
+
command: string;
|
|
34
|
+
message: string;
|
|
35
|
+
stack?: string;
|
|
36
|
+
/** ISO timestamp. */
|
|
37
|
+
timestamp: string;
|
|
38
|
+
/** Tool version. */
|
|
39
|
+
version?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BugReportEnv {
|
|
43
|
+
version: string;
|
|
44
|
+
node: string;
|
|
45
|
+
os: string;
|
|
46
|
+
arch: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Redact personally-identifiable / sensitive substrings from text:
|
|
51
|
+
* - absolute home dir -> ~
|
|
52
|
+
* - emails -> <email>
|
|
53
|
+
* - token-ish strings -> <redacted>
|
|
54
|
+
*/
|
|
55
|
+
export function sanitize(text: string): string {
|
|
56
|
+
if (typeof text !== 'string' || !text) return text || '';
|
|
57
|
+
let out = text;
|
|
58
|
+
|
|
59
|
+
const home = os.homedir();
|
|
60
|
+
if (home) {
|
|
61
|
+
// Replace the literal home path everywhere it appears.
|
|
62
|
+
out = out.split(home).join('~');
|
|
63
|
+
}
|
|
64
|
+
// /Users/<name>/ and /home/<name>/ (in case home didn't match, e.g. other users)
|
|
65
|
+
out = out.replace(/\/(Users|home)\/[^/\s"']+/g, '/$1/<user>');
|
|
66
|
+
// Emails
|
|
67
|
+
out = out.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '<email>');
|
|
68
|
+
// Bearer / token-like long alnum sequences (>= 24 chars with mixed case/digits)
|
|
69
|
+
out = out.replace(/\b(?=[A-Za-z0-9_-]*[A-Za-z])(?=[A-Za-z0-9_-]*[0-9])[A-Za-z0-9_-]{24,}\b/g, '<redacted>');
|
|
70
|
+
// GitHub tokens
|
|
71
|
+
out = out.replace(/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, '<redacted>');
|
|
72
|
+
|
|
73
|
+
// Defense-in-depth: neutralize shell metacharacters so that even if the
|
|
74
|
+
// text is ever interpolated into a shell (it shouldn't be — we use argv),
|
|
75
|
+
// command substitution can't fire. Also keeps issue bodies clean.
|
|
76
|
+
out = out.replace(/\$\(/g, '$(').replace(/`/g, 'ˋ').replace(/\$\{/g, '${');
|
|
77
|
+
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compute a stable signature for an error so duplicates collapse to one issue.
|
|
83
|
+
* Uses the normalized message (numbers/paths stripped) + top stack frame.
|
|
84
|
+
*/
|
|
85
|
+
export function errorSignature(message: string, stack?: string): string {
|
|
86
|
+
const normMsg = sanitize(message || '')
|
|
87
|
+
.replace(/\d+/g, 'N') // collapse line numbers, counts, ports
|
|
88
|
+
.replace(/\s+/g, ' ')
|
|
89
|
+
.trim()
|
|
90
|
+
.slice(0, 200);
|
|
91
|
+
|
|
92
|
+
// First stack frame that points into our own code (most stable identifier).
|
|
93
|
+
let frame = '';
|
|
94
|
+
if (stack) {
|
|
95
|
+
const m = sanitize(stack).match(/at\s+([^\s(]+)\s*\(?([^\s)]+):\d+:\d+\)?/);
|
|
96
|
+
if (m) frame = `${m[1]} ${m[2].replace(/\d+/g, 'N')}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return createHash('sha1').update(`${normMsg}||${frame}`).digest('hex').slice(0, 12);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Gather non-sensitive environment info for the report. */
|
|
103
|
+
export function gatherEnv(version: string): BugReportEnv {
|
|
104
|
+
return {
|
|
105
|
+
version,
|
|
106
|
+
node: process.version,
|
|
107
|
+
os: `${os.type()} ${os.release()}`,
|
|
108
|
+
arch: os.arch(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Persist the most recent error so `w report-bug` can pick it up later. */
|
|
113
|
+
export function captureLastError(err: CapturedError): void {
|
|
114
|
+
try {
|
|
115
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
116
|
+
// Sanitize before persisting so secrets/paths/emails never sit on disk
|
|
117
|
+
// (the issue body re-sanitizes too — sanitize is idempotent enough here).
|
|
118
|
+
const safe: CapturedError = {
|
|
119
|
+
...err,
|
|
120
|
+
command: sanitize(err.command),
|
|
121
|
+
message: sanitize(err.message),
|
|
122
|
+
stack: err.stack ? sanitize(err.stack) : undefined,
|
|
123
|
+
};
|
|
124
|
+
fs.writeFileSync(LAST_ERROR_FILE, JSON.stringify(safe, null, 2), 'utf-8');
|
|
125
|
+
} catch {
|
|
126
|
+
/* non-critical */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Read the most recent captured error, if any. */
|
|
131
|
+
export function readLastError(): CapturedError | null {
|
|
132
|
+
try {
|
|
133
|
+
const raw = JSON.parse(fs.readFileSync(LAST_ERROR_FILE, 'utf-8'));
|
|
134
|
+
// Validate required fields so a malformed/old file can't crash the reporter.
|
|
135
|
+
if (!raw || typeof raw !== 'object' || typeof raw.message !== 'string' || !raw.message) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
command: typeof raw.command === 'string' ? raw.command : 'unknown',
|
|
140
|
+
message: raw.message,
|
|
141
|
+
stack: typeof raw.stack === 'string' ? raw.stack : undefined,
|
|
142
|
+
timestamp: typeof raw.timestamp === 'string' ? raw.timestamp : new Date().toISOString(),
|
|
143
|
+
version: typeof raw.version === 'string' ? raw.version : undefined,
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Heuristic: is this error clearly environmental (not a code bug)? */
|
|
151
|
+
export function looksEnvironmental(message: string): boolean {
|
|
152
|
+
return /ETIMEDOUT|ENOTFOUND|ECONNREFUSED|ECONNRESET|EACCES|EPERM|ENOSPC|network|rate.?limit|not authenticated|gh auth|sso|credential|token expired|command not found|No such file or directory/i
|
|
153
|
+
.test(message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Build the GitHub issue title for a captured error. */
|
|
157
|
+
export function buildIssueTitle(err: CapturedError): string {
|
|
158
|
+
const sig = errorSignature(err.message, err.stack);
|
|
159
|
+
const shortMsg = sanitize(err.message).split('\n')[0].slice(0, 80);
|
|
160
|
+
return `[auto] ${shortMsg} (${sig})`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Build the GitHub issue body (fully sanitized). */
|
|
164
|
+
export function buildIssueBody(err: CapturedError, env: BugReportEnv): string {
|
|
165
|
+
const sig = errorSignature(err.message, err.stack);
|
|
166
|
+
const stack = err.stack ? sanitize(err.stack).slice(0, 4000) : '(no stack captured)';
|
|
167
|
+
return [
|
|
168
|
+
`**Auto-reported by workspace-manager.**`,
|
|
169
|
+
``,
|
|
170
|
+
`**Signature:** \`${sig}\``,
|
|
171
|
+
`**Command:** \`${sanitize(err.command)}\``,
|
|
172
|
+
`**When:** ${err.timestamp}`,
|
|
173
|
+
``,
|
|
174
|
+
`### Error`,
|
|
175
|
+
'```',
|
|
176
|
+
sanitize(err.message).slice(0, 1000),
|
|
177
|
+
'```',
|
|
178
|
+
``,
|
|
179
|
+
`### Stack`,
|
|
180
|
+
'```',
|
|
181
|
+
stack,
|
|
182
|
+
'```',
|
|
183
|
+
``,
|
|
184
|
+
`### Environment`,
|
|
185
|
+
`| | |`,
|
|
186
|
+
`|---|---|`,
|
|
187
|
+
`| workspace-manager | ${env.version} |`,
|
|
188
|
+
`| node | ${env.node} |`,
|
|
189
|
+
`| os | ${env.os} |`,
|
|
190
|
+
`| arch | ${env.arch} |`,
|
|
191
|
+
``,
|
|
192
|
+
`<sub>Paths, usernames, emails, and token-like strings are redacted automatically.</sub>`,
|
|
193
|
+
].join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the argv for listing candidate duplicate issues. Exported for testing
|
|
198
|
+
* so we can assert no shell metacharacters / values are passed inertly.
|
|
199
|
+
*/
|
|
200
|
+
export function buildIssueListArgs(): string[] {
|
|
201
|
+
return [
|
|
202
|
+
'issue', 'list',
|
|
203
|
+
'--repo', BUG_REPORT_REPO,
|
|
204
|
+
'--state', 'open',
|
|
205
|
+
'--label', BUG_LABEL,
|
|
206
|
+
'--json', 'url,title',
|
|
207
|
+
'--limit', '200',
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Result of a duplicate lookup. `ok: false` means the lookup itself failed
|
|
213
|
+
* (gh error/timeout) — callers must NOT assume "no duplicate" in that case.
|
|
214
|
+
*/
|
|
215
|
+
export type DedupResult = { ok: true; url: string | null } | { ok: false };
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Find an existing open auto-reported issue with the same signature.
|
|
219
|
+
*
|
|
220
|
+
* Lists recent auto-reported issues and filters client-side on the
|
|
221
|
+
* parenthesized signature in the title. We do NOT rely on GitHub's
|
|
222
|
+
* free-text --search matching a hex substring, which is unreliable.
|
|
223
|
+
*
|
|
224
|
+
* Returns { ok:true, url } on a successful lookup (url=null = no duplicate),
|
|
225
|
+
* or { ok:false } if the lookup could not be performed.
|
|
226
|
+
*/
|
|
227
|
+
export function findExistingIssue(signature: string): DedupResult {
|
|
228
|
+
try {
|
|
229
|
+
const out = execFileSync('gh', buildIssueListArgs(), {
|
|
230
|
+
encoding: 'utf-8',
|
|
231
|
+
timeout: 20_000,
|
|
232
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
233
|
+
});
|
|
234
|
+
const issues = JSON.parse(out) as Array<{ url: string; title: string }>;
|
|
235
|
+
// Match the signature only where we put it: parenthesized at the end of
|
|
236
|
+
// the title ("[auto] … (<sig>)"). Avoids matching an unrelated issue whose
|
|
237
|
+
// free text happens to contain the same 12-hex substring.
|
|
238
|
+
const match = issues.find((i) => i.title.includes(`(${signature})`));
|
|
239
|
+
return { ok: true, url: match ? match.url : null };
|
|
240
|
+
} catch {
|
|
241
|
+
return { ok: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Ensure the auto-report label exists (read-or-create, best-effort).
|
|
247
|
+
* Avoids re-writing the label on every issue by checking existence first.
|
|
248
|
+
*/
|
|
249
|
+
function ensureLabel(): void {
|
|
250
|
+
try {
|
|
251
|
+
const out = execFileSync('gh', ['label', 'list', '--repo', BUG_REPORT_REPO, '--json', 'name'], {
|
|
252
|
+
encoding: 'utf-8', timeout: 15_000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
253
|
+
});
|
|
254
|
+
const labels = JSON.parse(out) as Array<{ name: string }>;
|
|
255
|
+
if (labels.some((l) => l.name === BUG_LABEL)) return; // already exists
|
|
256
|
+
} catch {
|
|
257
|
+
// couldn't list — fall through and try to create
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
execFileSync('gh', [
|
|
261
|
+
'label', 'create', BUG_LABEL,
|
|
262
|
+
'--repo', BUG_REPORT_REPO,
|
|
263
|
+
'--color', 'B60205',
|
|
264
|
+
'--description', 'Auto-reported by workspace-manager',
|
|
265
|
+
], { encoding: 'utf-8', timeout: 15_000, stdio: 'ignore' });
|
|
266
|
+
} catch {
|
|
267
|
+
/* may already exist or no perms — ignore */
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Build the argv for creating an issue. Exported for testing: a title/body
|
|
273
|
+
* containing shell metacharacters ($(…), backticks) must be passed as inert
|
|
274
|
+
* argv elements, never interpolated into a shell.
|
|
275
|
+
*/
|
|
276
|
+
export function buildIssueCreateArgs(title: string, body: string): string[] {
|
|
277
|
+
return [
|
|
278
|
+
'issue', 'create',
|
|
279
|
+
'--repo', BUG_REPORT_REPO,
|
|
280
|
+
'--title', title,
|
|
281
|
+
'--body', body,
|
|
282
|
+
'--label', BUG_LABEL,
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create a bug issue. Returns the created issue URL, or null on failure.
|
|
288
|
+
*
|
|
289
|
+
* Uses execFileSync with an argv array — NO shell — so title/body (derived
|
|
290
|
+
* from arbitrary error text) can never trigger command substitution.
|
|
291
|
+
*/
|
|
292
|
+
export function createIssue(title: string, body: string): string | null {
|
|
293
|
+
try {
|
|
294
|
+
ensureLabel();
|
|
295
|
+
const url = execFileSync('gh', buildIssueCreateArgs(title, body), {
|
|
296
|
+
encoding: 'utf-8',
|
|
297
|
+
timeout: 30_000,
|
|
298
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
299
|
+
}).trim();
|
|
300
|
+
const m = url.match(/https?:\/\/\S+/);
|
|
301
|
+
return m ? m[0] : url || null;
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface ReportResult {
|
|
308
|
+
status: 'created' | 'duplicate' | 'skipped' | 'failed';
|
|
309
|
+
url?: string;
|
|
310
|
+
reason?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* File (or dedupe) a bug report for a captured error.
|
|
315
|
+
*
|
|
316
|
+
* @param err The captured error.
|
|
317
|
+
* @param version Tool version string.
|
|
318
|
+
* @param opts.force Report even if the error looks environmental.
|
|
319
|
+
*/
|
|
320
|
+
export function reportBug(
|
|
321
|
+
err: CapturedError,
|
|
322
|
+
version: string,
|
|
323
|
+
opts: { force?: boolean } = {},
|
|
324
|
+
): ReportResult {
|
|
325
|
+
if (!opts.force && looksEnvironmental(err.message)) {
|
|
326
|
+
return {
|
|
327
|
+
status: 'skipped',
|
|
328
|
+
reason: 'Error looks environmental (network/auth/credentials), not a code bug. Use --force to report anyway.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// gh must be available + authenticated.
|
|
333
|
+
try {
|
|
334
|
+
execFileSync('gh', ['auth', 'status'], { timeout: 15_000, stdio: 'ignore' });
|
|
335
|
+
} catch {
|
|
336
|
+
return { status: 'failed', reason: 'GitHub CLI (gh) not available or not authenticated.' };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const sig = errorSignature(err.message, err.stack);
|
|
340
|
+
const dedup = findExistingIssue(sig);
|
|
341
|
+
if (!dedup.ok) {
|
|
342
|
+
return {
|
|
343
|
+
status: 'failed',
|
|
344
|
+
reason: 'Could not check for existing reports (gh issue list failed). Not filing, to avoid creating a duplicate. Please retry.',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (dedup.url) {
|
|
348
|
+
return { status: 'duplicate', url: dedup.url };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const env = gatherEnv(version);
|
|
352
|
+
const url = createIssue(buildIssueTitle(err), buildIssueBody(err, env));
|
|
353
|
+
if (!url) return { status: 'failed', reason: 'Failed to create the GitHub issue.' };
|
|
354
|
+
return { status: 'created', url };
|
|
355
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { GitHubRepo } from '../types';
|
|
4
|
+
import { CACHE_DIR } from './config';
|
|
5
|
+
import { logInfo, logWarning } from './logger';
|
|
6
|
+
import { colorize } from './colors';
|
|
7
|
+
|
|
8
|
+
const REPOS_CACHE_FILE = path.join(CACHE_DIR, 'repos-cache.json');
|
|
9
|
+
const DEFAULT_TTL = 86400000; // 24 hours in milliseconds
|
|
10
|
+
|
|
11
|
+
interface CacheEntry {
|
|
12
|
+
data: GitHubRepo[];
|
|
13
|
+
timestamp: number;
|
|
14
|
+
ttl: number;
|
|
15
|
+
org?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CacheOptions {
|
|
19
|
+
ttl?: number;
|
|
20
|
+
force?: boolean;
|
|
21
|
+
org?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure cache directory exists
|
|
26
|
+
*/
|
|
27
|
+
async function ensureCacheDir(): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logWarning('Failed to create cache directory');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if cache is valid (not expired)
|
|
37
|
+
*/
|
|
38
|
+
function isCacheValid(entry: CacheEntry): boolean {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const age = now - entry.timestamp;
|
|
41
|
+
return age < entry.ttl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get cache age in human-readable format
|
|
46
|
+
*/
|
|
47
|
+
function getCacheAge(timestamp: number): string {
|
|
48
|
+
const ageMs = Date.now() - timestamp;
|
|
49
|
+
const ageMinutes = Math.floor(ageMs / 60000);
|
|
50
|
+
const ageHours = Math.floor(ageMinutes / 60);
|
|
51
|
+
const ageDays = Math.floor(ageHours / 24);
|
|
52
|
+
|
|
53
|
+
if (ageDays > 0) {
|
|
54
|
+
return `${ageDays} day${ageDays > 1 ? 's' : ''} ago`;
|
|
55
|
+
} else if (ageHours > 0) {
|
|
56
|
+
return `${ageHours} hour${ageHours > 1 ? 's' : ''} ago`;
|
|
57
|
+
} else if (ageMinutes > 0) {
|
|
58
|
+
return `${ageMinutes} minute${ageMinutes > 1 ? 's' : ''} ago`;
|
|
59
|
+
} else {
|
|
60
|
+
return 'just now';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read cached repositories
|
|
66
|
+
*/
|
|
67
|
+
export async function getCachedRepos(options: CacheOptions = {}): Promise<GitHubRepo[] | null> {
|
|
68
|
+
const { force = false, org } = options;
|
|
69
|
+
|
|
70
|
+
if (force) {
|
|
71
|
+
logInfo('Skipping cache (forced refresh)');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const content = await fs.readFile(REPOS_CACHE_FILE, 'utf-8');
|
|
77
|
+
const entry: CacheEntry = JSON.parse(content);
|
|
78
|
+
|
|
79
|
+
if (org && entry.org !== org) {
|
|
80
|
+
logInfo(`Cache is for org "${entry.org || 'unknown'}", but current org is "${org}". Refreshing...`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isCacheValid(entry)) {
|
|
85
|
+
const age = getCacheAge(entry.timestamp);
|
|
86
|
+
logInfo(`Using cached repositories (${colorize(String(entry.data.length), 'cyan')} repos, cached ${age})`);
|
|
87
|
+
return entry.data;
|
|
88
|
+
} else {
|
|
89
|
+
logInfo('Cache expired, fetching fresh data...');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Cache file doesn't exist or is invalid
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Write repositories to cache
|
|
100
|
+
*/
|
|
101
|
+
export async function setCachedRepos(repos: GitHubRepo[], ttl: number = DEFAULT_TTL, org?: string): Promise<void> {
|
|
102
|
+
try {
|
|
103
|
+
await ensureCacheDir();
|
|
104
|
+
|
|
105
|
+
const entry: CacheEntry = {
|
|
106
|
+
data: repos,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
ttl,
|
|
109
|
+
org,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await fs.writeFile(REPOS_CACHE_FILE, JSON.stringify(entry, null, 2), 'utf-8');
|
|
113
|
+
logInfo(`Cached ${colorize(String(repos.length), 'cyan')} repositories (TTL: ${Math.floor(ttl / 60000)} minutes)`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logWarning('Failed to cache repositories');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clear the cache
|
|
121
|
+
*/
|
|
122
|
+
export async function clearCache(): Promise<void> {
|
|
123
|
+
try {
|
|
124
|
+
await fs.unlink(REPOS_CACHE_FILE);
|
|
125
|
+
logInfo('Cache cleared successfully');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Cache file doesn't exist, nothing to clear
|
|
128
|
+
logInfo('No cache to clear');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get cache statistics
|
|
134
|
+
*/
|
|
135
|
+
export async function getCacheStats(): Promise<{ exists: boolean; age?: string; repoCount?: number; size?: number }> {
|
|
136
|
+
try {
|
|
137
|
+
const content = await fs.readFile(REPOS_CACHE_FILE, 'utf-8');
|
|
138
|
+
const entry: CacheEntry = JSON.parse(content);
|
|
139
|
+
const stats = await fs.stat(REPOS_CACHE_FILE);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
exists: true,
|
|
143
|
+
age: getCacheAge(entry.timestamp),
|
|
144
|
+
repoCount: entry.data.length,
|
|
145
|
+
size: stats.size,
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return { exists: false };
|
|
149
|
+
}
|
|
150
|
+
}
|