@guilz-dev/belay 0.1.0
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/LICENSE +21 -0
- package/README.md +268 -0
- package/agent-belay-logo.png +0 -0
- package/dist/adapters/claude/adapter.d.ts +7 -0
- package/dist/adapters/claude/adapter.js +114 -0
- package/dist/adapters/claude/hooks.d.ts +13 -0
- package/dist/adapters/claude/hooks.js +49 -0
- package/dist/adapters/claude/runtime-entry.d.ts +4 -0
- package/dist/adapters/claude/runtime-entry.js +260 -0
- package/dist/adapters/codex/adapter.d.ts +7 -0
- package/dist/adapters/codex/adapter.js +73 -0
- package/dist/adapters/codex/hooks.d.ts +21 -0
- package/dist/adapters/codex/hooks.js +78 -0
- package/dist/adapters/codex/runtime-entry.d.ts +4 -0
- package/dist/adapters/codex/runtime-entry.js +237 -0
- package/dist/adapters/cursor/adapter.d.ts +7 -0
- package/dist/adapters/cursor/adapter.js +29 -0
- package/dist/adapters/cursor/hooks.d.ts +2 -0
- package/dist/adapters/cursor/hooks.js +26 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
- package/dist/adapters/cursor/runtime-entry.js +143 -0
- package/dist/adapters/layouts/claude.d.ts +2 -0
- package/dist/adapters/layouts/claude.js +40 -0
- package/dist/adapters/layouts/codex.d.ts +2 -0
- package/dist/adapters/layouts/codex.js +43 -0
- package/dist/adapters/layouts/cursor.d.ts +2 -0
- package/dist/adapters/layouts/cursor.js +40 -0
- package/dist/adapters/layouts/index.d.ts +7 -0
- package/dist/adapters/layouts/index.js +23 -0
- package/dist/adapters/layouts/protected-paths.d.ts +3 -0
- package/dist/adapters/layouts/protected-paths.js +15 -0
- package/dist/adapters/layouts/scope.d.ts +19 -0
- package/dist/adapters/layouts/scope.js +86 -0
- package/dist/adapters/layouts/types.d.ts +14 -0
- package/dist/adapters/layouts/types.js +1 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +14 -0
- package/dist/adapters/shared/gate-runtime.d.ts +51 -0
- package/dist/adapters/shared/gate-runtime.js +518 -0
- package/dist/adapters/shared/repo-root.d.ts +2 -0
- package/dist/adapters/shared/repo-root.js +17 -0
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +3 -0
- package/dist/bundle/claude-runtime.mjs +5323 -0
- package/dist/bundle/codex-runtime.mjs +5310 -0
- package/dist/bundle/cursor-runtime.mjs +5208 -0
- package/dist/cleanup-orphans.d.ts +7 -0
- package/dist/cleanup-orphans.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +631 -0
- package/dist/commands/approve.d.ts +14 -0
- package/dist/commands/approve.js +65 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.js +132 -0
- package/dist/commands/classify-for-report.d.ts +9 -0
- package/dist/commands/classify-for-report.js +85 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +366 -0
- package/dist/commands/dogfood.d.ts +5 -0
- package/dist/commands/dogfood.js +71 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.js +133 -0
- package/dist/commands/health-snapshot.d.ts +2 -0
- package/dist/commands/health-snapshot.js +166 -0
- package/dist/commands/init-wizard.d.ts +16 -0
- package/dist/commands/init-wizard.js +50 -0
- package/dist/commands/metrics.d.ts +7 -0
- package/dist/commands/metrics.js +89 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +105 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +65 -0
- package/dist/commands/revoke.d.ts +5 -0
- package/dist/commands/revoke.js +22 -0
- package/dist/commands/simulate.d.ts +14 -0
- package/dist/commands/simulate.js +55 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +107 -0
- package/dist/config-io.d.ts +23 -0
- package/dist/config-io.js +180 -0
- package/dist/conformance/guarantee-table.d.ts +14 -0
- package/dist/conformance/guarantee-table.js +95 -0
- package/dist/conformance/types.d.ts +6 -0
- package/dist/conformance/types.js +1 -0
- package/dist/core/approval-service.d.ts +26 -0
- package/dist/core/approval-service.js +41 -0
- package/dist/core/approval-token.d.ts +11 -0
- package/dist/core/approval-token.js +61 -0
- package/dist/core/approval.d.ts +19 -0
- package/dist/core/approval.js +58 -0
- package/dist/core/audit-analysis.d.ts +10 -0
- package/dist/core/audit-analysis.js +147 -0
- package/dist/core/audit-metrics.d.ts +51 -0
- package/dist/core/audit-metrics.js +155 -0
- package/dist/core/audit-query.d.ts +11 -0
- package/dist/core/audit-query.js +142 -0
- package/dist/core/audit-summary.d.ts +33 -0
- package/dist/core/audit-summary.js +111 -0
- package/dist/core/audit-types.d.ts +65 -0
- package/dist/core/audit-types.js +2 -0
- package/dist/core/capability/allowlist.d.ts +10 -0
- package/dist/core/capability/allowlist.js +53 -0
- package/dist/core/capability/broker.d.ts +17 -0
- package/dist/core/capability/broker.js +29 -0
- package/dist/core/capability/index.d.ts +5 -0
- package/dist/core/capability/index.js +4 -0
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +20 -0
- package/dist/core/capability/reasons.d.ts +2 -0
- package/dist/core/capability/reasons.js +4 -0
- package/dist/core/capability/types.d.ts +10 -0
- package/dist/core/capability/types.js +1 -0
- package/dist/core/capability-approval.d.ts +28 -0
- package/dist/core/capability-approval.js +43 -0
- package/dist/core/classify-subagent.d.ts +2 -0
- package/dist/core/classify-subagent.js +69 -0
- package/dist/core/classify-tool.d.ts +3 -0
- package/dist/core/classify-tool.js +311 -0
- package/dist/core/config-layers.d.ts +23 -0
- package/dist/core/config-layers.js +59 -0
- package/dist/core/config.d.ts +219 -0
- package/dist/core/config.js +720 -0
- package/dist/core/control-plane-isolation.d.ts +10 -0
- package/dist/core/control-plane-isolation.js +83 -0
- package/dist/core/custom-command-match.d.ts +2 -0
- package/dist/core/custom-command-match.js +8 -0
- package/dist/core/egress/allowlist.d.ts +7 -0
- package/dist/core/egress/allowlist.js +33 -0
- package/dist/core/egress/env.d.ts +3 -0
- package/dist/core/egress/env.js +17 -0
- package/dist/core/egress/fingerprint.d.ts +3 -0
- package/dist/core/egress/fingerprint.js +35 -0
- package/dist/core/egress/policy.d.ts +8 -0
- package/dist/core/egress/policy.js +47 -0
- package/dist/core/egress/proxy-server.d.ts +21 -0
- package/dist/core/egress/proxy-server.js +263 -0
- package/dist/core/egress/types.d.ts +25 -0
- package/dist/core/egress/types.js +1 -0
- package/dist/core/egress-approval.d.ts +48 -0
- package/dist/core/egress-approval.js +129 -0
- package/dist/core/fingerprint.d.ts +6 -0
- package/dist/core/fingerprint.js +24 -0
- package/dist/core/gate-contract.d.ts +48 -0
- package/dist/core/gate-contract.js +50 -0
- package/dist/core/gate-engine.d.ts +20 -0
- package/dist/core/gate-engine.js +172 -0
- package/dist/core/glob.d.ts +1 -0
- package/dist/core/glob.js +39 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +15 -0
- package/dist/core/integrity.d.ts +15 -0
- package/dist/core/integrity.js +68 -0
- package/dist/core/judge-api-key.d.ts +4 -0
- package/dist/core/judge-api-key.js +11 -0
- package/dist/core/judge-config.d.ts +29 -0
- package/dist/core/judge-config.js +85 -0
- package/dist/core/judge-doctor.d.ts +7 -0
- package/dist/core/judge-doctor.js +124 -0
- package/dist/core/judgment.d.ts +6 -0
- package/dist/core/judgment.js +38 -0
- package/dist/core/notify.d.ts +13 -0
- package/dist/core/notify.js +44 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +107 -0
- package/dist/core/reclassify.d.ts +15 -0
- package/dist/core/reclassify.js +82 -0
- package/dist/core/recover-advice.d.ts +30 -0
- package/dist/core/recover-advice.js +177 -0
- package/dist/core/recover-git-probe.d.ts +8 -0
- package/dist/core/recover-git-probe.js +50 -0
- package/dist/core/recover-select.d.ts +10 -0
- package/dist/core/recover-select.js +60 -0
- package/dist/core/scrub.d.ts +3 -0
- package/dist/core/scrub.js +87 -0
- package/dist/core/shell-substitution.d.ts +6 -0
- package/dist/core/shell-substitution.js +130 -0
- package/dist/core/shell-tokenizer.d.ts +5 -0
- package/dist/core/shell-tokenizer.js +129 -0
- package/dist/core/shell-unparseable.d.ts +4 -0
- package/dist/core/shell-unparseable.js +96 -0
- package/dist/core/transactional/diff-evaluator.d.ts +2 -0
- package/dist/core/transactional/diff-evaluator.js +84 -0
- package/dist/core/transactional/eligibility.d.ts +4 -0
- package/dist/core/transactional/eligibility.js +44 -0
- package/dist/core/transactional/git-worktree.d.ts +13 -0
- package/dist/core/transactional/git-worktree.js +189 -0
- package/dist/core/transactional/index.d.ts +5 -0
- package/dist/core/transactional/index.js +4 -0
- package/dist/core/transactional/reasons.d.ts +4 -0
- package/dist/core/transactional/reasons.js +8 -0
- package/dist/core/transactional/runner.d.ts +2 -0
- package/dist/core/transactional/runner.js +113 -0
- package/dist/core/transactional/types.d.ts +46 -0
- package/dist/core/transactional/types.js +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +1 -0
- package/dist/core/v2/adapter.d.ts +14 -0
- package/dist/core/v2/adapter.js +118 -0
- package/dist/core/v2/containment.d.ts +19 -0
- package/dist/core/v2/containment.js +91 -0
- package/dist/core/v2/egress-classify.d.ts +7 -0
- package/dist/core/v2/egress-classify.js +216 -0
- package/dist/core/v2/fingerprint.d.ts +1 -0
- package/dist/core/v2/fingerprint.js +4 -0
- package/dist/core/v2/index.d.ts +12 -0
- package/dist/core/v2/index.js +10 -0
- package/dist/core/v2/judge-audit.d.ts +2 -0
- package/dist/core/v2/judge-audit.js +15 -0
- package/dist/core/v2/judge-factory.d.ts +25 -0
- package/dist/core/v2/judge-factory.js +75 -0
- package/dist/core/v2/judge-outbound.d.ts +12 -0
- package/dist/core/v2/judge-outbound.js +73 -0
- package/dist/core/v2/judge.d.ts +47 -0
- package/dist/core/v2/judge.js +264 -0
- package/dist/core/v2/launcher-resolve.d.ts +12 -0
- package/dist/core/v2/launcher-resolve.js +190 -0
- package/dist/core/v2/overrides.d.ts +7 -0
- package/dist/core/v2/overrides.js +37 -0
- package/dist/core/v2/parser.d.ts +21 -0
- package/dist/core/v2/parser.js +213 -0
- package/dist/core/v2/types.d.ts +67 -0
- package/dist/core/v2/types.js +1 -0
- package/dist/core/v2/verdict.d.ts +2 -0
- package/dist/core/v2/verdict.js +699 -0
- package/dist/corpus/evaluate.d.ts +24 -0
- package/dist/corpus/evaluate.js +69 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +155 -0
- package/dist/egress-daemon.d.ts +1 -0
- package/dist/egress-daemon.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/installer/bootstrap.d.ts +5 -0
- package/dist/installer/bootstrap.js +61 -0
- package/dist/installer/runtime-artifacts.d.ts +3 -0
- package/dist/installer/runtime-artifacts.js +23 -0
- package/dist/installer/scope-config.d.ts +8 -0
- package/dist/installer/scope-config.js +25 -0
- package/dist/installer.d.ts +22 -0
- package/dist/installer.js +169 -0
- package/dist/node-resolution.d.ts +8 -0
- package/dist/node-resolution.js +237 -0
- package/dist/operational-insights.d.ts +19 -0
- package/dist/operational-insights.js +24 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +95 -0
- package/dist/services/egress-service.d.ts +57 -0
- package/dist/services/egress-service.js +334 -0
- package/dist/services/sandbox-service.d.ts +38 -0
- package/dist/services/sandbox-service.js +95 -0
- package/dist/templates.d.ts +7 -0
- package/dist/templates.js +56 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +65 -0
- package/skills/belay/SKILL.md +52 -0
- package/skills/belay/belay-approve.md +7 -0
- package/skills/belay/belay-explain.md +11 -0
- package/skills/belay/belay-recover.md +13 -0
- package/skills/belay/belay-report.md +7 -0
- package/skills/belay/belay-status.md +9 -0
- package/skills/belay/belay-why.md +11 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export const RECOVER_DISCLAIMER = [
|
|
2
|
+
'Advisory only — belay does not run recovery commands.',
|
|
3
|
+
'Advice is based on what belay observed through hooks; actions outside hook scope may not be visible.',
|
|
4
|
+
'Recovery commands themselves pass through belay hooks — destructive undo steps may be blocked again.',
|
|
5
|
+
];
|
|
6
|
+
export const SHOW_DONT_RUN_LEAD = 'These steps may help undo the observed effect — confirm each step before running:';
|
|
7
|
+
const IRREVERSIBLE_REASON_PREFIXES = ['tier0_'];
|
|
8
|
+
const IRREVERSIBLE_REASONS = new Set([
|
|
9
|
+
'external_effect',
|
|
10
|
+
'exfiltration',
|
|
11
|
+
'force_push',
|
|
12
|
+
'remote_destructive',
|
|
13
|
+
]);
|
|
14
|
+
const DENIED_RECOVERY_PATTERNS = [
|
|
15
|
+
/reset\s+--hard/i,
|
|
16
|
+
/push\s+--force/i,
|
|
17
|
+
/clean\s+-[a-z]*f/i,
|
|
18
|
+
/dropdb\b/i,
|
|
19
|
+
/destroy\b/i,
|
|
20
|
+
];
|
|
21
|
+
export function containsDeniedRecoveryPattern(text) {
|
|
22
|
+
return DENIED_RECOVERY_PATTERNS.some((pattern) => pattern.test(text));
|
|
23
|
+
}
|
|
24
|
+
function isIrreversibleTarget(target) {
|
|
25
|
+
if (target.effect === 'external_effect') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (target.assessment?.reversibility === 'irreversible') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (target.assessment?.external) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (IRREVERSIBLE_REASONS.has(target.reason)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
for (const prefix of IRREVERSIBLE_REASON_PREFIXES) {
|
|
38
|
+
if (target.reason.startsWith(prefix)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function extractPathHints(summary) {
|
|
45
|
+
const hints = new Set();
|
|
46
|
+
const patterns = [
|
|
47
|
+
/\b(?:git\s+)?restore\s+--\s+(\S+)/i,
|
|
48
|
+
/\b(?:git\s+)?checkout\s+--\s+(\S+)/i,
|
|
49
|
+
/\brm\s+(?:-[a-z]+\s+)*([^\s;&|]+)/i,
|
|
50
|
+
/\b(?:edit|write|delete)\s+(\S+)/i,
|
|
51
|
+
];
|
|
52
|
+
for (const pattern of patterns) {
|
|
53
|
+
const match = summary.match(pattern);
|
|
54
|
+
if (match?.[1] && !match[1].startsWith('-')) {
|
|
55
|
+
hints.add(match[1]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...hints];
|
|
59
|
+
}
|
|
60
|
+
function localMutationAdvice(target, git) {
|
|
61
|
+
const advice = [];
|
|
62
|
+
const paths = extractPathHints(target.summary);
|
|
63
|
+
if (git?.inWorkTree) {
|
|
64
|
+
if (paths.length > 0) {
|
|
65
|
+
for (const filePath of paths.slice(0, 3)) {
|
|
66
|
+
advice.push(`git restore -- ${filePath}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
advice.push('git status --porcelain # inspect changed paths first');
|
|
71
|
+
advice.push('git restore -- <path> # restore specific tracked files');
|
|
72
|
+
}
|
|
73
|
+
if (/commit/i.test(target.summary) || target.reason.includes('commit')) {
|
|
74
|
+
advice.push('git log -n 5 # identify the commit to revert');
|
|
75
|
+
advice.push('git revert <commit> # prefer revert over reset for shared history');
|
|
76
|
+
}
|
|
77
|
+
if (git.reflog) {
|
|
78
|
+
advice.push('git reflog -n 10 # locate a prior HEAD if a local commit was lost');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
advice.push('Restore from version control or backups if the change was not committed.');
|
|
83
|
+
if (paths.length > 0) {
|
|
84
|
+
advice.push(`Check backups or trash for: ${paths.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return advice.filter((line) => !containsDeniedRecoveryPattern(line));
|
|
88
|
+
}
|
|
89
|
+
function isLowConfidenceAssessment(target, minAssessmentConfidence) {
|
|
90
|
+
if (typeof target.assessment?.confidence !== 'number' || minAssessmentConfidence === undefined) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return target.assessment.confidence < minAssessmentConfidence;
|
|
94
|
+
}
|
|
95
|
+
const NO_COMMAND_ADVICE = {
|
|
96
|
+
low_assessment: [
|
|
97
|
+
'No reliable recovery path can be determined from the observed assessment confidence.',
|
|
98
|
+
'Review the audit entry and your VCS or backups manually before attempting undo steps.',
|
|
99
|
+
],
|
|
100
|
+
insufficient_signal: [
|
|
101
|
+
'Insufficient signal to suggest a safe recovery path.',
|
|
102
|
+
'Review the audit entry and your VCS or backups manually before attempting undo steps.',
|
|
103
|
+
],
|
|
104
|
+
no_safe_command: [
|
|
105
|
+
'No safe, specific recovery command can be suggested from the observed audit record.',
|
|
106
|
+
'Inspect git history, backups, or hosting provider recovery tools manually.',
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
function noCommandRecoverResult(disclaimer, reason, extraWarnings = []) {
|
|
110
|
+
return {
|
|
111
|
+
recoverable: false,
|
|
112
|
+
confidence: 'medium',
|
|
113
|
+
disclaimer,
|
|
114
|
+
advice: NO_COMMAND_ADVICE[reason],
|
|
115
|
+
warnings: extraWarnings.length > 0
|
|
116
|
+
? extraWarnings
|
|
117
|
+
: ['Low confidence — no specific recovery commands are suggested.'],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function buildRecoverAdvice(input) {
|
|
121
|
+
const { target, git, minAssessmentConfidence } = input;
|
|
122
|
+
const warnings = [];
|
|
123
|
+
const disclaimer = [...RECOVER_DISCLAIMER];
|
|
124
|
+
if (isIrreversibleTarget(target)) {
|
|
125
|
+
return {
|
|
126
|
+
recoverable: false,
|
|
127
|
+
confidence: 'high',
|
|
128
|
+
disclaimer,
|
|
129
|
+
advice: [
|
|
130
|
+
'This action is likely not recoverable through local undo.',
|
|
131
|
+
'External or irreversible effects (publish, remote delete, force-push, exfiltration, etc.) cannot be reliably reversed.',
|
|
132
|
+
'Treat this as incident response: revoke credentials, notify stakeholders, and follow your org runbook.',
|
|
133
|
+
],
|
|
134
|
+
warnings: [
|
|
135
|
+
'Belay blocked this because it looked catastrophic and irreversible — do not expect a safe automatic undo.',
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (target.effect === 'read_only') {
|
|
140
|
+
return {
|
|
141
|
+
recoverable: true,
|
|
142
|
+
confidence: 'high',
|
|
143
|
+
disclaimer,
|
|
144
|
+
advice: ['No local mutation was recorded — there may be nothing to undo.'],
|
|
145
|
+
warnings: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (isLowConfidenceAssessment(target, minAssessmentConfidence)) {
|
|
149
|
+
return noCommandRecoverResult(disclaimer, 'low_assessment');
|
|
150
|
+
}
|
|
151
|
+
const advice = [SHOW_DONT_RUN_LEAD];
|
|
152
|
+
if (target.effect === 'local_mutation' ||
|
|
153
|
+
target.assessment?.reversibility === 'recoverable_with_cost') {
|
|
154
|
+
advice.push(...localMutationAdvice(target, git));
|
|
155
|
+
}
|
|
156
|
+
else if (inferWouldBlockFromTarget(target)) {
|
|
157
|
+
advice.push(...localMutationAdvice(target, git));
|
|
158
|
+
warnings.push('Effect axis was unclear — showing conservative file/git guidance only.');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
return noCommandRecoverResult(disclaimer, 'insufficient_signal');
|
|
162
|
+
}
|
|
163
|
+
const filteredAdvice = advice.filter((line) => !containsDeniedRecoveryPattern(line));
|
|
164
|
+
if (filteredAdvice.length <= 1) {
|
|
165
|
+
return noCommandRecoverResult(disclaimer, 'no_safe_command', warnings);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
recoverable: true,
|
|
169
|
+
confidence: git?.inWorkTree ? 'high' : 'medium',
|
|
170
|
+
disclaimer,
|
|
171
|
+
advice: filteredAdvice,
|
|
172
|
+
warnings,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function inferWouldBlockFromTarget(target) {
|
|
176
|
+
return target.permission === 'ask' || target.reason === 'unknown_local_effect';
|
|
177
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface GitProbeResult {
|
|
2
|
+
inWorkTree: boolean;
|
|
3
|
+
porcelain?: string;
|
|
4
|
+
reflog?: string;
|
|
5
|
+
notes: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function isReadOnlyGitProbe(commandKey: string): boolean;
|
|
8
|
+
export declare function probeGitState(repoRoot: string): Promise<GitProbeResult>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const GIT_PROBE_TIMEOUT_MS = 5_000;
|
|
5
|
+
const READ_ONLY_GIT_COMMANDS = new Set([
|
|
6
|
+
'rev-parse --is-inside-work-tree',
|
|
7
|
+
'status --porcelain',
|
|
8
|
+
'reflog -n 10',
|
|
9
|
+
]);
|
|
10
|
+
export function isReadOnlyGitProbe(commandKey) {
|
|
11
|
+
return READ_ONLY_GIT_COMMANDS.has(commandKey);
|
|
12
|
+
}
|
|
13
|
+
async function runGit(repoRoot, args) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
16
|
+
cwd: repoRoot,
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
maxBuffer: 1024 * 1024,
|
|
19
|
+
timeout: GIT_PROBE_TIMEOUT_MS,
|
|
20
|
+
});
|
|
21
|
+
return { ok: true, stdout: stdout.trimEnd() };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { ok: false, stdout: '' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function probeGitState(repoRoot) {
|
|
28
|
+
const notes = [];
|
|
29
|
+
const workTree = await runGit(repoRoot, ['rev-parse', '--is-inside-work-tree']);
|
|
30
|
+
if (!workTree.ok || workTree.stdout !== 'true') {
|
|
31
|
+
return {
|
|
32
|
+
inWorkTree: false,
|
|
33
|
+
notes: ['Not a git work tree — file-level git restore advice may not apply.'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const status = await runGit(repoRoot, ['status', '--porcelain']);
|
|
37
|
+
if (status.ok) {
|
|
38
|
+
notes.push('Read git status (porcelain) for context.');
|
|
39
|
+
}
|
|
40
|
+
const reflog = await runGit(repoRoot, ['reflog', '-n', '10']);
|
|
41
|
+
if (reflog.ok) {
|
|
42
|
+
notes.push('Read recent reflog entries for context.');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
inWorkTree: true,
|
|
46
|
+
porcelain: status.ok ? status.stdout : undefined,
|
|
47
|
+
reflog: reflog.ok ? reflog.stdout : undefined,
|
|
48
|
+
notes,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AuditRecord } from './audit-types.js';
|
|
2
|
+
import type { RecoverTargetInput } from './recover-advice.js';
|
|
3
|
+
export interface RecoverSelectOptions {
|
|
4
|
+
since?: string;
|
|
5
|
+
fingerprint?: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function recoverCandidatePriority(record: AuditRecord): number;
|
|
9
|
+
export declare function recordToRecoverTarget(record: AuditRecord): RecoverTargetInput;
|
|
10
|
+
export declare function selectRecoverTarget(records: AuditRecord[], options?: RecoverSelectOptions): RecoverTargetInput | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { filterAuditRecords, inferWouldBlock, isApprovalRecorded, isGateRecord, isSilentPassRecord, parseTimestamp, recordStringField, } from './audit-query.js';
|
|
2
|
+
function isRecoverCandidate(record) {
|
|
3
|
+
if (!isGateRecord(record) || isApprovalRecorded(record)) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
if (inferWouldBlock(record)) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
const effect = recordStringField(record, 'effect');
|
|
10
|
+
return effect === 'local_mutation' || effect === 'external_effect';
|
|
11
|
+
}
|
|
12
|
+
export function recoverCandidatePriority(record) {
|
|
13
|
+
const effect = recordStringField(record, 'effect');
|
|
14
|
+
if (effect === 'local_mutation') {
|
|
15
|
+
return isSilentPassRecord(record) ? 0 : 1;
|
|
16
|
+
}
|
|
17
|
+
if (inferWouldBlock(record) || effect === 'external_effect') {
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
return 3;
|
|
21
|
+
}
|
|
22
|
+
function compareRecoverCandidates(left, right) {
|
|
23
|
+
const priorityDelta = recoverCandidatePriority(left) - recoverCandidatePriority(right);
|
|
24
|
+
if (priorityDelta !== 0) {
|
|
25
|
+
return priorityDelta;
|
|
26
|
+
}
|
|
27
|
+
const leftMs = parseTimestamp(left.timestamp) ?? 0;
|
|
28
|
+
const rightMs = parseTimestamp(right.timestamp) ?? 0;
|
|
29
|
+
return rightMs - leftMs;
|
|
30
|
+
}
|
|
31
|
+
export function recordToRecoverTarget(record) {
|
|
32
|
+
return {
|
|
33
|
+
timestamp: record.timestamp,
|
|
34
|
+
fingerprint: recordStringField(record, 'fingerprint') || undefined,
|
|
35
|
+
summary: recordStringField(record, 'summary'),
|
|
36
|
+
reason: recordStringField(record, 'reason') || 'unknown',
|
|
37
|
+
effect: recordStringField(record, 'effect') || undefined,
|
|
38
|
+
location: recordStringField(record, 'location') || undefined,
|
|
39
|
+
permission: recordStringField(record, 'permission') || undefined,
|
|
40
|
+
assessment: record.assessment,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function selectRecoverTarget(records, options = {}) {
|
|
44
|
+
const filtered = filterAuditRecords(records, {
|
|
45
|
+
since: options.since,
|
|
46
|
+
fingerprint: options.fingerprint,
|
|
47
|
+
});
|
|
48
|
+
const candidates = filtered.filter(isRecoverCandidate);
|
|
49
|
+
if (candidates.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
candidates.sort(compareRecoverCandidates);
|
|
53
|
+
const limit = options.limit ?? 1;
|
|
54
|
+
const index = Math.min(limit, candidates.length) - 1;
|
|
55
|
+
const selected = candidates[index] ?? candidates[0];
|
|
56
|
+
if (!selected) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return recordToRecoverTarget(selected);
|
|
60
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
|
|
2
|
+
const TIMESTAMP_PATTERN = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g;
|
|
3
|
+
const APPROVAL_ID_PATTERN = /\bbelay_[a-z0-9]{8,}\b/gi;
|
|
4
|
+
const TOKEN_PREFIX_PATTERN = /\/belay-approve\s+\S+/gi;
|
|
5
|
+
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi;
|
|
6
|
+
const AUTH_HEADER_PATTERN = /(?<!["'])\bAuthorization:\s*(?:Bearer|Basic|Token)?\s*\S+/gi;
|
|
7
|
+
const DOUBLE_QUOTED_AUTH_HEADER_PATTERN = /"Authorization:\s*[^"]*"/gi;
|
|
8
|
+
const SINGLE_QUOTED_AUTH_HEADER_PATTERN = /'Authorization:\s*[^']*'/gi;
|
|
9
|
+
const GENERIC_AUTH_HEADER_PATTERN = /(?<!["'])\b(?:X-Api-Key|X-Auth-Token|Private-Token):\s*\S+/gi;
|
|
10
|
+
const DOUBLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN = /"(X-Api-Key|X-Auth-Token|Private-Token):\s*[^"]*"/gi;
|
|
11
|
+
const SINGLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN = /'(X-Api-Key|X-Auth-Token|Private-Token):\s*[^']*'/gi;
|
|
12
|
+
const KEY_VALUE_SECRET_PATTERN = /\b(api[_-]?key|token|secret|password|passwd|credential)\b\s*[:=]\s*['"]?[^\s'"]{4,}/gi;
|
|
13
|
+
const URL_CREDENTIALS_PATTERN = /\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/g;
|
|
14
|
+
const MYSQL_INLINE_PASSWORD_PATTERN = /(\s-p)([^\s]+)/g;
|
|
15
|
+
const HIGH_ENTROPY_PATTERN = /\b[A-Za-z0-9+/]{40,}={0,2}\b/g;
|
|
16
|
+
const DEFAULT_SCRUB_OPTIONS = {
|
|
17
|
+
maskApprovalIds: true,
|
|
18
|
+
maskBearerTokens: true,
|
|
19
|
+
maskAuthHeaders: true,
|
|
20
|
+
maskKeyValueSecrets: true,
|
|
21
|
+
maskHighEntropyStrings: true,
|
|
22
|
+
};
|
|
23
|
+
function resolvedScrubOptions(options = {}) {
|
|
24
|
+
return {
|
|
25
|
+
maskApprovalIds: options.maskApprovalIds !== false,
|
|
26
|
+
maskBearerTokens: options.maskBearerTokens !== false,
|
|
27
|
+
maskAuthHeaders: options.maskAuthHeaders !== false,
|
|
28
|
+
maskKeyValueSecrets: options.maskKeyValueSecrets !== false,
|
|
29
|
+
maskHighEntropyStrings: options.maskHighEntropyStrings === true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function scrubString(value, options = {}) {
|
|
33
|
+
const resolved = resolvedScrubOptions(options);
|
|
34
|
+
let scrubbed = value.replace(UUID_PATTERN, '<uuid>').replace(TIMESTAMP_PATTERN, '<timestamp>');
|
|
35
|
+
if (resolved.maskApprovalIds) {
|
|
36
|
+
scrubbed = scrubbed
|
|
37
|
+
.replace(APPROVAL_ID_PATTERN, '<approval-id>')
|
|
38
|
+
.replace(TOKEN_PREFIX_PATTERN, '/belay-approve <approval-id>');
|
|
39
|
+
}
|
|
40
|
+
if (resolved.maskBearerTokens) {
|
|
41
|
+
scrubbed = scrubbed.replace(BEARER_PATTERN, 'Bearer <redacted>');
|
|
42
|
+
}
|
|
43
|
+
if (resolved.maskAuthHeaders) {
|
|
44
|
+
scrubbed = scrubbed
|
|
45
|
+
.replace(DOUBLE_QUOTED_AUTH_HEADER_PATTERN, '"Authorization: <redacted>"')
|
|
46
|
+
.replace(SINGLE_QUOTED_AUTH_HEADER_PATTERN, "'Authorization: <redacted>'")
|
|
47
|
+
.replace(DOUBLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN, (_match, header) => `"${header}: <redacted>"`)
|
|
48
|
+
.replace(SINGLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN, (_match, header) => `'${header}: <redacted>'`)
|
|
49
|
+
.replace(AUTH_HEADER_PATTERN, 'Authorization: <redacted>')
|
|
50
|
+
.replace(GENERIC_AUTH_HEADER_PATTERN, (match) => {
|
|
51
|
+
const separatorIndex = match.indexOf(':');
|
|
52
|
+
return `${match.slice(0, separatorIndex + 1)} <redacted>`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (resolved.maskKeyValueSecrets) {
|
|
56
|
+
scrubbed = scrubbed
|
|
57
|
+
.replace(URL_CREDENTIALS_PATTERN, '$1<redacted>:<redacted>@')
|
|
58
|
+
.replace(MYSQL_INLINE_PASSWORD_PATTERN, '$1<redacted>');
|
|
59
|
+
scrubbed = scrubbed.replace(KEY_VALUE_SECRET_PATTERN, (match) => {
|
|
60
|
+
const separatorMatch = match.match(/\s*[:=]\s*/);
|
|
61
|
+
if (!separatorMatch || separatorMatch.index === undefined) {
|
|
62
|
+
return '<secret>';
|
|
63
|
+
}
|
|
64
|
+
return `${match.slice(0, separatorMatch.index)}${separatorMatch[0]}<redacted>`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (resolved.maskHighEntropyStrings) {
|
|
68
|
+
scrubbed = scrubbed.replace(HIGH_ENTROPY_PATTERN, '<high-entropy>');
|
|
69
|
+
}
|
|
70
|
+
return scrubbed;
|
|
71
|
+
}
|
|
72
|
+
export function scrubValue(value, options = DEFAULT_SCRUB_OPTIONS) {
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
return scrubString(value, options);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.map((item) => scrubValue(item, options));
|
|
78
|
+
}
|
|
79
|
+
if (value && typeof value === 'object') {
|
|
80
|
+
const result = {};
|
|
81
|
+
for (const [key, child] of Object.entries(value)) {
|
|
82
|
+
result[key] = scrubValue(child, options);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const MAX_SUBSTITUTION_DEPTH = 8;
|
|
2
|
+
export { MAX_SUBSTITUTION_DEPTH };
|
|
3
|
+
/**
|
|
4
|
+
* Finds inner commands for $(...) and backtick substitution, respecting escapes and nesting.
|
|
5
|
+
*/
|
|
6
|
+
export function findCommandSubstitutions(command) {
|
|
7
|
+
const results = [];
|
|
8
|
+
let index = 0;
|
|
9
|
+
let inSingle = false;
|
|
10
|
+
let inDouble = false;
|
|
11
|
+
let escaping = false;
|
|
12
|
+
while (index < command.length) {
|
|
13
|
+
const char = command[index];
|
|
14
|
+
if (escaping) {
|
|
15
|
+
escaping = false;
|
|
16
|
+
index += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (char === '\\' && (inSingle || inDouble)) {
|
|
20
|
+
escaping = true;
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!inDouble && char === "'") {
|
|
25
|
+
inSingle = !inSingle;
|
|
26
|
+
index += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!inSingle && char === '"') {
|
|
30
|
+
inDouble = !inDouble;
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (inSingle || inDouble) {
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === '\\' && index + 1 < command.length) {
|
|
39
|
+
index += 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '`') {
|
|
43
|
+
const end = findClosingBacktick(command, index + 1);
|
|
44
|
+
if (end === -1) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const inner = command.slice(index + 1, end).trim();
|
|
48
|
+
if (inner) {
|
|
49
|
+
results.push(inner);
|
|
50
|
+
}
|
|
51
|
+
index = end + 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '$' && command[index + 1] === '(') {
|
|
55
|
+
const closed = extractBalancedParenContent(command, index + 2);
|
|
56
|
+
if (!closed) {
|
|
57
|
+
index += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const inner = closed.content.trim();
|
|
61
|
+
if (inner) {
|
|
62
|
+
results.push(inner);
|
|
63
|
+
}
|
|
64
|
+
index = closed.endIndex;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
index += 1;
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
function findClosingBacktick(command, start) {
|
|
72
|
+
let index = start;
|
|
73
|
+
while (index < command.length) {
|
|
74
|
+
if (command[index] === '\\' && index + 1 < command.length) {
|
|
75
|
+
index += 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (command[index] === '`') {
|
|
79
|
+
return index;
|
|
80
|
+
}
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
return -1;
|
|
84
|
+
}
|
|
85
|
+
function extractBalancedParenContent(command, start) {
|
|
86
|
+
let depth = 1;
|
|
87
|
+
let index = start;
|
|
88
|
+
let inSingle = false;
|
|
89
|
+
let inDouble = false;
|
|
90
|
+
let escaping = false;
|
|
91
|
+
while (index < command.length && depth > 0) {
|
|
92
|
+
const char = command[index];
|
|
93
|
+
if (escaping) {
|
|
94
|
+
escaping = false;
|
|
95
|
+
index += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (char === '\\' && (inSingle || inDouble)) {
|
|
99
|
+
escaping = true;
|
|
100
|
+
index += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!inDouble && char === "'") {
|
|
104
|
+
inSingle = !inSingle;
|
|
105
|
+
index += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!inSingle && char === '"') {
|
|
109
|
+
inDouble = !inDouble;
|
|
110
|
+
index += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!inSingle && !inDouble) {
|
|
114
|
+
if (char === '(') {
|
|
115
|
+
depth += 1;
|
|
116
|
+
}
|
|
117
|
+
else if (char === ')') {
|
|
118
|
+
depth -= 1;
|
|
119
|
+
if (depth === 0) {
|
|
120
|
+
return {
|
|
121
|
+
content: command.slice(start, index),
|
|
122
|
+
endIndex: index + 1,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
index += 1;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function tokenizeShell(input: string): string[];
|
|
2
|
+
export declare function normalizeShellCommand(command: string, repoRoot: string, normalizeToken: (t: string, r: string) => string): string;
|
|
3
|
+
export declare function splitTopLevelSegments(tokens: string[]): string[][];
|
|
4
|
+
export declare function commandKey(tokens: string[]): string;
|
|
5
|
+
export declare function extractRedirectTargets(tokens: string[]): string[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
2
|
+
export function tokenizeShell(input) {
|
|
3
|
+
const tokens = [];
|
|
4
|
+
let buffer = '';
|
|
5
|
+
let quote = null;
|
|
6
|
+
let escaping = false;
|
|
7
|
+
const flush = () => {
|
|
8
|
+
if (buffer.length > 0) {
|
|
9
|
+
tokens.push(buffer);
|
|
10
|
+
buffer = '';
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
14
|
+
const char = input[index];
|
|
15
|
+
const next = input[index + 1] ?? '';
|
|
16
|
+
if (escaping) {
|
|
17
|
+
buffer += char;
|
|
18
|
+
escaping = false;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === '\\') {
|
|
22
|
+
escaping = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (quote) {
|
|
26
|
+
if (char === quote) {
|
|
27
|
+
quote = null;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
buffer += char;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (char === '"' || char === "'") {
|
|
35
|
+
quote = char;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === '&' && next === '&') {
|
|
39
|
+
flush();
|
|
40
|
+
tokens.push('&&');
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '|' && next === '|') {
|
|
45
|
+
flush();
|
|
46
|
+
tokens.push('||');
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === '>' && next === '>') {
|
|
51
|
+
flush();
|
|
52
|
+
tokens.push('>>');
|
|
53
|
+
index += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (char === '|' || char === ';' || char === '>' || char === '<') {
|
|
57
|
+
flush();
|
|
58
|
+
tokens.push(char);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (char === '\n' || char === '\r') {
|
|
62
|
+
flush();
|
|
63
|
+
tokens.push(';');
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (/\s/.test(char)) {
|
|
67
|
+
flush();
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
buffer += char;
|
|
71
|
+
}
|
|
72
|
+
flush();
|
|
73
|
+
return tokens;
|
|
74
|
+
}
|
|
75
|
+
export function normalizeShellCommand(command, repoRoot, normalizeToken) {
|
|
76
|
+
const tokens = tokenizeShell(command);
|
|
77
|
+
while (tokens.length > 0 && ENV_PREFIX_PATTERN.test(tokens[0] ?? '')) {
|
|
78
|
+
tokens.shift();
|
|
79
|
+
}
|
|
80
|
+
const normalized = tokens.map((token) => normalizeToken(token, repoRoot));
|
|
81
|
+
return normalized.join(' ').trim();
|
|
82
|
+
}
|
|
83
|
+
export function splitTopLevelSegments(tokens) {
|
|
84
|
+
const segments = [];
|
|
85
|
+
let current = [];
|
|
86
|
+
for (const token of tokens) {
|
|
87
|
+
if (token === '&&' || token === '||' || token === ';' || token === '|') {
|
|
88
|
+
if (current.length > 0) {
|
|
89
|
+
segments.push(current);
|
|
90
|
+
}
|
|
91
|
+
current = [];
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
current.push(token);
|
|
95
|
+
}
|
|
96
|
+
if (current.length > 0) {
|
|
97
|
+
segments.push(current);
|
|
98
|
+
}
|
|
99
|
+
return segments;
|
|
100
|
+
}
|
|
101
|
+
export function commandKey(tokens) {
|
|
102
|
+
const filtered = tokens.filter((token) => token !== 'sudo');
|
|
103
|
+
const first = filtered[0] ?? '';
|
|
104
|
+
const second = filtered[1] ?? '';
|
|
105
|
+
if ((first === 'git' ||
|
|
106
|
+
first === 'npm' ||
|
|
107
|
+
first === 'pnpm' ||
|
|
108
|
+
first === 'docker' ||
|
|
109
|
+
first === 'terraform' ||
|
|
110
|
+
first === 'fly' ||
|
|
111
|
+
first === 'firebase') &&
|
|
112
|
+
second) {
|
|
113
|
+
return `${first} ${second}`;
|
|
114
|
+
}
|
|
115
|
+
return first;
|
|
116
|
+
}
|
|
117
|
+
export function extractRedirectTargets(tokens) {
|
|
118
|
+
const targets = [];
|
|
119
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
120
|
+
const token = tokens[index];
|
|
121
|
+
if (token === '>' || token === '>>' || token === '<') {
|
|
122
|
+
const next = tokens[index + 1];
|
|
123
|
+
if (next) {
|
|
124
|
+
targets.push(next);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return targets;
|
|
129
|
+
}
|