@guilz-dev/belay 0.9.2 → 0.9.4
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/README.md +36 -2
- package/dist/adapters/codex/hooks.d.ts +1 -0
- package/dist/adapters/codex/hooks.js +3 -0
- package/dist/adapters/codex/runtime-entry.d.ts +3 -0
- package/dist/adapters/codex/runtime-entry.js +27 -4
- package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
- package/dist/adapters/cursor/cwd-resolution.js +58 -0
- package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
- package/dist/adapters/cursor/dispatcher-generation.js +5 -0
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +266 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -0
- package/dist/adapters/cursor/hooks.js +115 -3
- package/dist/adapters/cursor/routing-layout.d.ts +5 -0
- package/dist/adapters/cursor/routing-layout.js +29 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +18 -0
- package/dist/adapters/cursor/runtime-entry.js +135 -34
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +27 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +2660 -1951
- package/dist/bundle/codex-runtime.mjs +2680 -1953
- package/dist/bundle/cursor-dispatcher.mjs +443 -0
- package/dist/bundle/cursor-runtime.mjs +5638 -4697
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +215 -16
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +61 -0
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- package/dist/commands/report.js +14 -0
- package/dist/commands/status.js +15 -0
- package/dist/commands/where.d.ts +4 -0
- package/dist/commands/where.js +52 -0
- package/dist/config-io.js +11 -2
- package/dist/core/approval-repo-lookup.d.ts +16 -0
- package/dist/core/approval-repo-lookup.js +48 -0
- package/dist/core/audit-io.d.ts +1 -1
- package/dist/core/audit-io.js +1 -1
- package/dist/core/audit-legacy-archive.js +43 -25
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +7 -0
- package/dist/core/audit-serialize.d.ts +3 -0
- package/dist/core/audit-serialize.js +39 -3
- package/dist/core/audit-summary.d.ts +9 -0
- package/dist/core/audit-summary.js +58 -1
- package/dist/core/audit-types.d.ts +4 -0
- package/dist/core/classify-tool.js +16 -8
- package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
- package/dist/core/effect-ir/argv-delegate.js +42 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
- package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/augment.js +97 -0
- package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
- package/dist/core/effect-ir/shell-lower/context.js +1 -0
- package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
- package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
- package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
- package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/segment.js +82 -0
- package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
- package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
- package/dist/core/effect-ir/shell-lower.d.ts +2 -7
- package/dist/core/effect-ir/shell-lower.js +122 -1511
- package/dist/core/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/replay-scrub.d.ts +3 -0
- package/dist/core/replay-scrub.js +30 -3
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/shell-unparseable.js +3 -1
- package/dist/core/verdict/docker-compose-run.d.ts +18 -0
- package/dist/core/verdict/docker-compose-run.js +136 -0
- package/dist/core/verdict/launcher-resolve.js +28 -28
- package/dist/core/verdict/parser.d.ts +3 -0
- package/dist/core/verdict/parser.js +23 -40
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/defaults.d.ts +7 -3
- package/dist/defaults.js +35 -21
- package/dist/installer/bootstrap.d.ts +1 -0
- package/dist/installer/bootstrap.js +2 -2
- package/dist/installer/runtime-artifacts.js +43 -13
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +83 -7
- package/dist/node-resolution.d.ts +1 -0
- package/dist/node-resolution.js +61 -0
- package/dist/templates.d.ts +7 -4
- package/dist/templates.js +42 -4
- package/dist/types.d.ts +35 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +5 -0
- package/skills/belay/belay-report.md +4 -1
package/dist/cli.js
CHANGED
|
@@ -17,9 +17,10 @@ import { formatSessionStatusReport, sessionStartProject, sessionStatusProject, }
|
|
|
17
17
|
import { formatSimulateReport, simulateProject } from './commands/simulate.js';
|
|
18
18
|
import { revokeStandingAllow } from './commands/standing-allow.js';
|
|
19
19
|
import { formatStatusReport, statusProject } from './commands/status.js';
|
|
20
|
+
import { formatWhereReport, whereProject } from './commands/where.js';
|
|
20
21
|
import { loadConfigFile } from './config-io.js';
|
|
21
22
|
import { rejectDeprecatedJudgeModelAuto } from './core/judge-model-policy.js';
|
|
22
|
-
import { initProject, upgradeProject } from './installer.js';
|
|
23
|
+
import { initProject, uninstallProject, upgradeProject } from './installer.js';
|
|
23
24
|
import { egressEnv, egressStatus, formatEgressStatusReport, startEgressProxy, stopEgressProxy, } from './services/egress-service.js';
|
|
24
25
|
import { formatSandboxStatusReport, sandboxStatus } from './services/sandbox-service.js';
|
|
25
26
|
import { PACKAGE_VERSION } from './version.js';
|
|
@@ -297,14 +298,17 @@ function parseArgs(argv) {
|
|
|
297
298
|
}
|
|
298
299
|
options.approveScope = next;
|
|
299
300
|
}
|
|
300
|
-
else if (command === 'init' ||
|
|
301
|
+
else if (command === 'init' ||
|
|
302
|
+
command === 'upgrade' ||
|
|
303
|
+
command === 'uninstall' ||
|
|
304
|
+
command === 'where') {
|
|
301
305
|
if (!next || !['project', 'global'].includes(next)) {
|
|
302
306
|
throw new Error('--scope requires project or global.');
|
|
303
307
|
}
|
|
304
308
|
options.installScope = next;
|
|
305
309
|
}
|
|
306
310
|
else {
|
|
307
|
-
throw new Error('--scope is only valid for init, upgrade, or approve.');
|
|
311
|
+
throw new Error('--scope is only valid for init, upgrade, uninstall, where, or approve.');
|
|
308
312
|
}
|
|
309
313
|
index += 1;
|
|
310
314
|
continue;
|
|
@@ -540,6 +544,8 @@ Usage:
|
|
|
540
544
|
(--adapter selects host; fresh init picks matching judge providerId: cursor/claude/codex)
|
|
541
545
|
(--dogfood runs after --preset and sets mode: audit, overriding preset enforce mode)
|
|
542
546
|
${c} upgrade [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--with-skill] [--migrate-judge-default]
|
|
547
|
+
${c} uninstall [--target <dir>] [--adapter cursor] [--scope project|global]
|
|
548
|
+
${c} where [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--json]
|
|
543
549
|
${c} dogfood [--target <dir>] [--adapter cursor|claude|codex] [--enforce] [--force]
|
|
544
550
|
${c} doctor [--target <dir>] [--adapter cursor|claude|codex] [--json] [--fix] [--dry-run]
|
|
545
551
|
${c} metrics [--target <dir>] [--json]
|
|
@@ -634,6 +640,30 @@ async function main() {
|
|
|
634
640
|
process.stdout.write(`Upgraded belay (${result.adapter}) in ${result.repoRoot}.\n`);
|
|
635
641
|
return;
|
|
636
642
|
}
|
|
643
|
+
if (command === 'uninstall') {
|
|
644
|
+
const result = await uninstallProject({
|
|
645
|
+
targetDir: options.targetDir,
|
|
646
|
+
adapter: options.adapter,
|
|
647
|
+
scope: options.installScope,
|
|
648
|
+
});
|
|
649
|
+
process.stdout.write(`Removed belay hooks (${result.adapter}, scope=${result.scope}) from ${result.repoRoot}.\n`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (command === 'where') {
|
|
653
|
+
const report = await whereProject({
|
|
654
|
+
targetDir: options.targetDir,
|
|
655
|
+
adapter: options.adapter,
|
|
656
|
+
scope: options.installScope,
|
|
657
|
+
json: options.json,
|
|
658
|
+
});
|
|
659
|
+
if (options.json) {
|
|
660
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
process.stdout.write(formatWhereReport(report));
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
637
667
|
if (command === 'judge') {
|
|
638
668
|
const { runJudgeCommand } = await import('./commands/judge.js');
|
|
639
669
|
if (!options.judgeSubcommand) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
4
6
|
import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
|
|
5
|
-
import { getCodexManagedHookEntries } from '../adapters/codex/hooks.js';
|
|
6
|
-
import {
|
|
7
|
+
import { codexHooksTomlIncludesCommand, getCodexManagedHookEntries, } from '../adapters/codex/hooks.js';
|
|
8
|
+
import { hasCurrentCursorDispatcherGeneration } from '../adapters/cursor/dispatcher-generation.js';
|
|
9
|
+
import { hasLegacyCursorDoubleShellGates, hasManagedCursorHookEntries, } from '../adapters/cursor/hooks.js';
|
|
7
10
|
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
8
11
|
import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
|
|
9
12
|
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
@@ -15,7 +18,7 @@ import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summar
|
|
|
15
18
|
import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
|
|
16
19
|
import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
|
|
17
20
|
import { configuredControlPlaneDir, defaultControlPlaneDir, hasForbiddenShellOverrideLists, stripForbiddenShellOverrideLists, } from '../core/config.js';
|
|
18
|
-
import { verifyIntegrityManifest } from '../core/integrity.js';
|
|
21
|
+
import { runtimeIntegrityFiles, verifyIntegrityManifest } from '../core/integrity.js';
|
|
19
22
|
import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
|
|
20
23
|
import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
21
24
|
import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
|
|
@@ -33,6 +36,7 @@ import { PACKAGE_VERSION } from '../version.js';
|
|
|
33
36
|
import { loadAuditRecords } from './audit.js';
|
|
34
37
|
import { collectHealthSnapshot } from './health-snapshot.js';
|
|
35
38
|
import { metricsProject } from './metrics.js';
|
|
39
|
+
const execFileAsync = promisify(execFile);
|
|
36
40
|
function resolveDoctorAdapter(options, configAdapter) {
|
|
37
41
|
if (options.adapter) {
|
|
38
42
|
return options.adapter;
|
|
@@ -42,6 +46,120 @@ function resolveDoctorAdapter(options, configAdapter) {
|
|
|
42
46
|
}
|
|
43
47
|
return 'cursor';
|
|
44
48
|
}
|
|
49
|
+
function hasCursorGlobalWorkspaceResolver(runtimeSource) {
|
|
50
|
+
// Cursor global hooks must derive workspace cwd from payload fields, not process cwd.
|
|
51
|
+
const hasScopedToolResolver = runtimeSource.includes('resolveCursorToolActionCwd') ||
|
|
52
|
+
runtimeSource.includes('includeToolInputCwd');
|
|
53
|
+
return (runtimeSource.includes('resolveCursorActionCwd') &&
|
|
54
|
+
hasScopedToolResolver &&
|
|
55
|
+
runtimeSource.includes('workspace_roots') &&
|
|
56
|
+
runtimeSource.includes('working_directory') &&
|
|
57
|
+
runtimeSource.includes('tool_input'));
|
|
58
|
+
}
|
|
59
|
+
const CURSOR_HOOK_SHIMS = [
|
|
60
|
+
'belay-before-submit.mjs',
|
|
61
|
+
'belay-shell-gate.mjs',
|
|
62
|
+
'belay-tool-gate.mjs',
|
|
63
|
+
'belay-audit.mjs',
|
|
64
|
+
];
|
|
65
|
+
function runnerFileName(adapterName, platform) {
|
|
66
|
+
if (platform !== 'win32') {
|
|
67
|
+
return 'belay-runner';
|
|
68
|
+
}
|
|
69
|
+
return adapterName === 'cursor' ? 'belay-runner.ps1' : 'belay-runner.cmd';
|
|
70
|
+
}
|
|
71
|
+
async function cursorOriginIssues(hooksDir, installScope, repoRoot) {
|
|
72
|
+
const canonicalRepoRoot = realpathSync(repoRoot);
|
|
73
|
+
const issues = [];
|
|
74
|
+
for (const fileName of CURSOR_HOOK_SHIMS) {
|
|
75
|
+
const shimPath = path.join(hooksDir, fileName);
|
|
76
|
+
if (!existsSync(shimPath)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let source;
|
|
80
|
+
try {
|
|
81
|
+
source = await readFile(shimPath, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
85
|
+
issues.push(`Unable to inspect Cursor hook shim for intended ${installScope} owner: ${shimPath}: ${detail}. Run belay upgrade --scope ${installScope}.`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!source.includes("from '../belay/runtime/dispatcher.mjs'")) {
|
|
89
|
+
issues.push(`Cursor router generation mismatch for intended ${installScope} owner: ${shimPath}. Run belay upgrade --scope ${installScope}.`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const originMatch = source.match(/origin:\s*(\{[^\n]+\})/);
|
|
93
|
+
let originMatches = false;
|
|
94
|
+
if (originMatch?.[1]) {
|
|
95
|
+
try {
|
|
96
|
+
const origin = JSON.parse(originMatch[1]);
|
|
97
|
+
originMatches =
|
|
98
|
+
installScope === 'global'
|
|
99
|
+
? origin.scope === 'global'
|
|
100
|
+
: origin.scope === 'project' &&
|
|
101
|
+
typeof origin.repoRoot === 'string' &&
|
|
102
|
+
realpathSync(origin.repoRoot) === canonicalRepoRoot;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
originMatches = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!originMatches) {
|
|
109
|
+
issues.push(`Cursor hook origin mismatch for intended ${installScope} owner: ${shimPath}. Run belay upgrade --scope ${installScope}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return issues;
|
|
113
|
+
}
|
|
114
|
+
async function listLinkedWorktreePaths(repoRoot) {
|
|
115
|
+
try {
|
|
116
|
+
const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {
|
|
117
|
+
cwd: repoRoot,
|
|
118
|
+
encoding: 'utf8',
|
|
119
|
+
});
|
|
120
|
+
return stdout
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.filter((line) => line.startsWith('worktree '))
|
|
123
|
+
.map((line) => line.slice('worktree '.length).trim())
|
|
124
|
+
.filter((entry) => entry.length > 0);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function detectUndogfoodedLinkedWorktrees(params) {
|
|
131
|
+
const repoRootCanonical = realpathSync(params.repoRoot);
|
|
132
|
+
const worktrees = await listLinkedWorktreePaths(params.repoRoot);
|
|
133
|
+
const warnings = [];
|
|
134
|
+
for (const worktreePath of worktrees) {
|
|
135
|
+
let canonicalPath = worktreePath;
|
|
136
|
+
try {
|
|
137
|
+
canonicalPath = realpathSync(worktreePath);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Keep the original path for warning output when the entry is stale.
|
|
141
|
+
}
|
|
142
|
+
if (canonicalPath === repoRootCanonical) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const configPath = params.layout.configPath(worktreePath);
|
|
146
|
+
if (!existsSync(configPath)) {
|
|
147
|
+
warnings.push(`Dogfood is active here but ${path.basename(worktreePath)} has no belay.config.json (defaults to enforce). Run belay dogfood in each worktree you use with Cursor.`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const candidate = await loadLayeredConfig(worktreePath, params.adapterName);
|
|
152
|
+
const dogfoodEnabled = candidate.config.mode === 'audit' && candidate.config.policy.unknownLocalEffect === 'deny';
|
|
153
|
+
if (!dogfoodEnabled) {
|
|
154
|
+
warnings.push(`Dogfood is active here but ${path.basename(worktreePath)} is not in dogfood mode (mode=${candidate.config.mode}, unknownLocalEffect=${candidate.config.policy.unknownLocalEffect}). Run belay dogfood in each worktree you use with Cursor.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
warnings.push(`Dogfood is active here but ${path.basename(worktreePath)} has an unreadable belay.config.json. Run belay doctor and belay dogfood in that worktree.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return warnings;
|
|
162
|
+
}
|
|
45
163
|
export async function doctorProject(options = {}) {
|
|
46
164
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
47
165
|
const issues = [];
|
|
@@ -97,7 +215,9 @@ export async function doctorProject(options = {}) {
|
|
|
97
215
|
hooksPath = scopedPaths.hooksSettingsPath;
|
|
98
216
|
corePath = path.join(scopedPaths.runtimeDir, 'core.mjs');
|
|
99
217
|
notes.push(installScope === 'global'
|
|
100
|
-
?
|
|
218
|
+
? adapterName === 'cursor'
|
|
219
|
+
? `Install scope: global (hooks/runtime at ${scopedPaths.hooksDir}). To remove global hooks: belay uninstall --scope global`
|
|
220
|
+
: `Install scope: global (hooks/runtime at ${scopedPaths.hooksDir})`
|
|
101
221
|
: 'Install scope: project');
|
|
102
222
|
notes.push(`Config mode: ${loadedConfig.mode}`);
|
|
103
223
|
const dogfoodActive = loadedConfig.mode === 'audit' && loadedConfig.policy.unknownLocalEffect === 'deny';
|
|
@@ -133,9 +253,9 @@ export async function doctorProject(options = {}) {
|
|
|
133
253
|
}
|
|
134
254
|
if (loadedConfig.controlPlane.integrity === 'hash-pinned') {
|
|
135
255
|
notes.push('Integrity: hash-pinned (verify with belay upgrade after runtime changes).');
|
|
136
|
-
const integrity = await verifyIntegrityManifest(repoRoot, activeLayout);
|
|
256
|
+
const integrity = await verifyIntegrityManifest(repoRoot, activeLayout, runtimeIntegrityFiles(activeLayout, scopedPaths));
|
|
137
257
|
if (!integrity.ok) {
|
|
138
|
-
issues.push(`Integrity verification failed: ${integrity.mismatches.
|
|
258
|
+
issues.push(`Integrity verification failed: ${integrity.mismatches.join(', ')}`);
|
|
139
259
|
}
|
|
140
260
|
}
|
|
141
261
|
}
|
|
@@ -151,14 +271,16 @@ export async function doctorProject(options = {}) {
|
|
|
151
271
|
const repoLocalDir = loadedConfig
|
|
152
272
|
? repoLocalStateDirFor(repoRoot, loadedConfig)
|
|
153
273
|
: activeLayout.repoLocalStateDir(repoRoot);
|
|
154
|
-
const
|
|
155
|
-
path.join(hooksDir,
|
|
156
|
-
path.join(hooksDir, 'belay-runner.cmd'),
|
|
274
|
+
const routingOwnerPaths = [
|
|
275
|
+
path.join(hooksDir, runnerFileName(adapterName, process.platform)),
|
|
157
276
|
path.join(hooksDir, 'belay-before-submit.mjs'),
|
|
158
277
|
path.join(hooksDir, 'belay-shell-gate.mjs'),
|
|
159
278
|
path.join(hooksDir, 'belay-tool-gate.mjs'),
|
|
160
279
|
path.join(hooksDir, 'belay-audit.mjs'),
|
|
280
|
+
...(adapterName === 'cursor' ? [path.join(scopedPaths.runtimeDir, 'dispatcher.mjs')] : []),
|
|
161
281
|
corePath,
|
|
282
|
+
];
|
|
283
|
+
const operationalPaths = [
|
|
162
284
|
loadedConfig
|
|
163
285
|
? pendingApprovalsPath(repoRoot, loadedConfig)
|
|
164
286
|
: path.join(repoLocalDir, 'pending-approvals.json'),
|
|
@@ -167,9 +289,67 @@ export async function doctorProject(options = {}) {
|
|
|
167
289
|
: path.join(repoLocalDir, 'approved-approvals.json'),
|
|
168
290
|
path.join(repoRoot, loadedConfig?.audit.logPath ?? activeLayout.defaultAuditLogPath(repoRoot)),
|
|
169
291
|
];
|
|
292
|
+
const requiredPaths = [...routingOwnerPaths, ...operationalPaths];
|
|
170
293
|
for (const requiredPath of requiredPaths) {
|
|
171
294
|
if (!existsSync(requiredPath)) {
|
|
172
295
|
issues.push(`Missing generated file: ${requiredPath}`);
|
|
296
|
+
if (adapterName === 'cursor' && routingOwnerPaths.includes(requiredPath)) {
|
|
297
|
+
issues.push(`Cursor intended ${installScope} owner is incomplete: missing ${requiredPath}. Run belay upgrade --scope ${installScope}.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (adapterName === 'cursor') {
|
|
302
|
+
issues.push(...(await cursorOriginIssues(hooksDir, installScope, repoRoot)));
|
|
303
|
+
const dispatcherPath = path.join(scopedPaths.runtimeDir, 'dispatcher.mjs');
|
|
304
|
+
if (existsSync(dispatcherPath) &&
|
|
305
|
+
!hasCurrentCursorDispatcherGeneration(await readFile(dispatcherPath, 'utf8'))) {
|
|
306
|
+
issues.push(`Cursor router generation mismatch for intended ${installScope} owner dispatcher: ${dispatcherPath}. Run belay upgrade --scope ${installScope}.`);
|
|
307
|
+
}
|
|
308
|
+
if (installScope === 'project') {
|
|
309
|
+
const globalPaths = resolveScopedPaths(activeLayout, 'global', repoRoot);
|
|
310
|
+
if (existsSync(globalPaths.hooksSettingsPath)) {
|
|
311
|
+
try {
|
|
312
|
+
const { loadHooksFile } = await import('../installer.js');
|
|
313
|
+
const globalHooks = await loadHooksFile(globalPaths.hooksSettingsPath);
|
|
314
|
+
if (hasManagedCursorHookEntries(globalHooks, process.platform, globalPaths.hooksDir, repoRoot)) {
|
|
315
|
+
const globalOwnerFiles = [
|
|
316
|
+
path.join(globalPaths.hooksDir, runnerFileName('cursor', process.platform)),
|
|
317
|
+
path.join(globalPaths.runtimeDir, 'core.mjs'),
|
|
318
|
+
path.join(globalPaths.runtimeDir, 'dispatcher.mjs'),
|
|
319
|
+
...CURSOR_HOOK_SHIMS.map((fileName) => path.join(globalPaths.hooksDir, fileName)),
|
|
320
|
+
];
|
|
321
|
+
const globalOriginProblems = await cursorOriginIssues(globalPaths.hooksDir, 'global', repoRoot);
|
|
322
|
+
issues.push(...globalOriginProblems);
|
|
323
|
+
const globalProblems = [
|
|
324
|
+
...globalOwnerFiles.filter((filePath) => !existsSync(filePath)),
|
|
325
|
+
...globalOriginProblems,
|
|
326
|
+
];
|
|
327
|
+
const globalDispatcherPath = path.join(globalPaths.runtimeDir, 'dispatcher.mjs');
|
|
328
|
+
if (existsSync(globalDispatcherPath)) {
|
|
329
|
+
try {
|
|
330
|
+
if (!hasCurrentCursorDispatcherGeneration(await readFile(globalDispatcherPath, 'utf8'))) {
|
|
331
|
+
globalProblems.push(globalDispatcherPath);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
336
|
+
issues.push(`Unable to inspect shadowed global Cursor dispatcher at ${globalDispatcherPath}: ${detail}.`);
|
|
337
|
+
globalProblems.push(globalDispatcherPath);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (globalProblems.length > 0) {
|
|
341
|
+
issues.push('Global Cursor installation cannot yield to the project owner. Run belay upgrade from this project to refresh the global router.');
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
notes.push('Healthy global Cursor install is shadowed by this project owner and will return neutral for this repository.');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
350
|
+
issues.push(`Unable to inspect shadowed global Cursor hooks at ${globalPaths.hooksSettingsPath}: ${detail}.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
173
353
|
}
|
|
174
354
|
}
|
|
175
355
|
let hooksOk = true;
|
|
@@ -184,15 +364,17 @@ export async function doctorProject(options = {}) {
|
|
|
184
364
|
const hooksFile = await loadHooksFile(hooksPath);
|
|
185
365
|
for (const { event, definition } of managedEntries) {
|
|
186
366
|
const entries = hooksFile.hooks[event] ?? [];
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
367
|
+
const matchingEntry = entries.find((entry) => entry.command === definition.command && entry.matcher === definition.matcher);
|
|
368
|
+
if (!matchingEntry || matchingEntry.failClosed !== true) {
|
|
189
369
|
hooksOk = false;
|
|
190
370
|
const matcherSuffix = definition.matcher ? ` (matcher: ${definition.matcher})` : '';
|
|
191
|
-
issues.push(
|
|
371
|
+
issues.push(matchingEntry
|
|
372
|
+
? `Managed Cursor hook for ${event} must set failClosed: true: ${definition.command}${matcherSuffix}`
|
|
373
|
+
: `Missing managed hook for ${event}: ${definition.command}${matcherSuffix}`);
|
|
192
374
|
}
|
|
193
375
|
}
|
|
194
|
-
if (
|
|
195
|
-
warnings.push('
|
|
376
|
+
if (hasLegacyCursorDoubleShellGates(hooksFile, process.platform, hooksDir, repoRoot)) {
|
|
377
|
+
warnings.push('Legacy Cursor Shell preToolUse gate detected alongside beforeShellExecution. Run belay upgrade to remove the managed Shell matcher.');
|
|
196
378
|
}
|
|
197
379
|
}
|
|
198
380
|
else if (adapterName === 'codex') {
|
|
@@ -200,7 +382,7 @@ export async function doctorProject(options = {}) {
|
|
|
200
382
|
// are present in the rendered TOML block.
|
|
201
383
|
const toml = await readFile(hooksPath, 'utf8');
|
|
202
384
|
for (const { event, definition } of managedEntries) {
|
|
203
|
-
if (!toml
|
|
385
|
+
if (!codexHooksTomlIncludesCommand(toml, definition.command)) {
|
|
204
386
|
hooksOk = false;
|
|
205
387
|
issues.push(`Missing Codex managed hook for ${event}: ${definition.command}`);
|
|
206
388
|
}
|
|
@@ -248,6 +430,17 @@ export async function doctorProject(options = {}) {
|
|
|
248
430
|
if (runtimeVersions.stamp?.startsWith(`${PACKAGE_VERSION}@`)) {
|
|
249
431
|
notes.push(`Runtime version matches package (${PACKAGE_VERSION}).`);
|
|
250
432
|
}
|
|
433
|
+
if (adapterName === 'cursor' && installScope === 'global') {
|
|
434
|
+
try {
|
|
435
|
+
const runtimeSource = await readFile(corePath, 'utf8');
|
|
436
|
+
if (!hasCursorGlobalWorkspaceResolver(runtimeSource)) {
|
|
437
|
+
warnings.push('Global Cursor runtime appears to resolve hook context from hook process cwd. Per-repository belay.config.json can be bypassed (audit may act as enforce). Run belay upgrade --scope global from the latest package.');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
warnings.push('Unable to inspect global Cursor runtime for workspace-cwd resolution. Run belay upgrade --scope global from the latest package.');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
251
444
|
}
|
|
252
445
|
if (options.fix && loadedConfig) {
|
|
253
446
|
if (hasForbiddenShellOverrideLists(loadedConfig)) {
|
|
@@ -323,6 +516,12 @@ export async function doctorProject(options = {}) {
|
|
|
323
516
|
};
|
|
324
517
|
if (dogfood.active) {
|
|
325
518
|
notes.push(`Dogfood active cohort: ${dogfood.gateEvents} gate events, ${dogfood.wouldBlockCount} would-block (${(dogfood.wouldBlockRate * 100).toFixed(1)}%); ${dogfood.excludedGateEvents} historical/mismatched event(s) excluded.`);
|
|
519
|
+
notes.push(`Host denied after Belay allow (cohort): ${auditVisibility.hostDeniedAfterAllowCount}.`);
|
|
520
|
+
warnings.push(...(await detectUndogfoodedLinkedWorktrees({
|
|
521
|
+
repoRoot,
|
|
522
|
+
adapterName,
|
|
523
|
+
layout: activeLayout,
|
|
524
|
+
})));
|
|
326
525
|
if (dogfood.readyForEnforce) {
|
|
327
526
|
notes.push('Dogfood metrics suggest enforce mode is ready (belay dogfood --enforce).');
|
|
328
527
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { AdapterName } from '../adapters/layouts/types.js';
|
|
2
2
|
import type { HealthSnapshot, HealthSnapshotOptions } from '../types.js';
|
|
3
|
+
export declare function cursorCliConfigPaths(homeDir: string, env: Partial<Pick<NodeJS.ProcessEnv, 'CURSOR_CONFIG_DIR' | 'XDG_CONFIG_HOME'>>): string[];
|
|
4
|
+
export declare function cursorApprovalModeRequiresHostDenialWarning(approvalMode: unknown): boolean;
|
|
5
|
+
export declare function formatCursorApprovalModeLabel(approvalMode: unknown): string;
|
|
3
6
|
/** Lightweight floor check without judge doctor / sandbox probes. */
|
|
4
7
|
export declare function isBelayFloorInstalled(options?: {
|
|
5
8
|
targetDir?: string;
|
|
@@ -48,6 +48,58 @@ async function managedHooksPresent(adapter, hooksPath, hooksDir, repoRoot) {
|
|
|
48
48
|
}
|
|
49
49
|
return managedEntries.every(({ definition }) => content.includes(definition.command));
|
|
50
50
|
}
|
|
51
|
+
/** Cursor modes confirmed not to apply host-level denial after Belay allows. */
|
|
52
|
+
const CURSOR_APPROVAL_MODES_WITHOUT_HOST_DENIAL = new Set(['unrestricted']);
|
|
53
|
+
export function cursorCliConfigPaths(homeDir, env) {
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const addCandidate = (candidate) => {
|
|
56
|
+
if (!candidates.includes(candidate)) {
|
|
57
|
+
candidates.push(candidate);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const cursorConfigDir = env.CURSOR_CONFIG_DIR?.trim();
|
|
61
|
+
if (cursorConfigDir) {
|
|
62
|
+
addCandidate(path.join(cursorConfigDir, 'cli-config.json'));
|
|
63
|
+
}
|
|
64
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
|
|
65
|
+
if (xdgConfigHome) {
|
|
66
|
+
addCandidate(path.join(xdgConfigHome, 'cursor', 'cli-config.json'));
|
|
67
|
+
}
|
|
68
|
+
addCandidate(path.join(homeDir, '.cursor', 'cli-config.json'));
|
|
69
|
+
return candidates;
|
|
70
|
+
}
|
|
71
|
+
export function cursorApprovalModeRequiresHostDenialWarning(approvalMode) {
|
|
72
|
+
if (typeof approvalMode !== 'string' || !approvalMode.trim()) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return !CURSOR_APPROVAL_MODES_WITHOUT_HOST_DENIAL.has(approvalMode);
|
|
76
|
+
}
|
|
77
|
+
export function formatCursorApprovalModeLabel(approvalMode) {
|
|
78
|
+
if (typeof approvalMode !== 'string' || !approvalMode.trim()) {
|
|
79
|
+
return 'default (prompts on each action)';
|
|
80
|
+
}
|
|
81
|
+
return approvalMode;
|
|
82
|
+
}
|
|
83
|
+
async function cursorRunModeRiskSignal(homeDir, belayMode, env) {
|
|
84
|
+
const configPath = cursorCliConfigPaths(homeDir, env).find((candidate) => existsSync(candidate));
|
|
85
|
+
if (!configPath) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
90
|
+
if (!cursorApprovalModeRequiresHostDenialWarning(config.approvalMode)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const approvalModeLabel = formatCursorApprovalModeLabel(config.approvalMode);
|
|
94
|
+
const auditGuidance = belayMode === 'audit'
|
|
95
|
+
? ' Belay is in audit mode; keep the Cursor protection or approve the exact host prompt, and do not switch Cursor to unrestricted until Belay enforce mode and fail-closed hook health are verified.'
|
|
96
|
+
: ' If Belay is intended to be authoritative, verify fail-closed hook health before configuring Cursor unrestricted mode.';
|
|
97
|
+
return `Cursor approval mode is ${approvalModeLabel}; Cursor can deny shell actions after Belay allows them. Such a denial is a host-policy denial, not a Belay approval.${auditGuidance}`;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
51
103
|
/** Lightweight floor check without judge doctor / sandbox probes. */
|
|
52
104
|
export async function isBelayFloorInstalled(options = {}) {
|
|
53
105
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
@@ -125,6 +177,15 @@ export async function collectHealthSnapshot(options = {}) {
|
|
|
125
177
|
if (layered.config.judge.provider === 'openai-compatible') {
|
|
126
178
|
additionalRiskSignals.push('cloud judge enabled: redacted command text may be sent to an external provider');
|
|
127
179
|
}
|
|
180
|
+
if (adapter === 'cursor') {
|
|
181
|
+
const cursorRunModeRisk = await cursorRunModeRiskSignal(options.homeDir ?? os.homedir(), layered.config.mode, options.cursorConfigEnv ?? {
|
|
182
|
+
CURSOR_CONFIG_DIR: process.env.CURSOR_CONFIG_DIR,
|
|
183
|
+
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
|
184
|
+
});
|
|
185
|
+
if (cursorRunModeRisk) {
|
|
186
|
+
additionalRiskSignals.push(cursorRunModeRisk);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
128
189
|
}
|
|
129
190
|
catch {
|
|
130
191
|
configPresent = false;
|
|
@@ -86,7 +86,7 @@ export declare function recoveryCheckpointCommand(options: {
|
|
|
86
86
|
state: import("../core/recovery/types.js").RecoveryCheckpointStateV1;
|
|
87
87
|
manifestHash: string;
|
|
88
88
|
receipt: import("../core/index.js").RecoveryReceiptV1 | undefined;
|
|
89
|
-
repoRoot?: undefined;
|
|
89
|
+
readonly repoRoot?: undefined;
|
|
90
90
|
backend?: undefined;
|
|
91
91
|
availableBackends?: undefined;
|
|
92
92
|
backendCounts?: undefined;
|
|
@@ -118,7 +118,7 @@ export declare function recoveryCheckpointCommand(options: {
|
|
|
118
118
|
paths: string[];
|
|
119
119
|
auditRecorded: boolean;
|
|
120
120
|
message: string;
|
|
121
|
-
repoRoot?: undefined;
|
|
121
|
+
readonly repoRoot?: undefined;
|
|
122
122
|
backend?: undefined;
|
|
123
123
|
availableBackends?: undefined;
|
|
124
124
|
backendCounts?: undefined;
|
|
@@ -146,7 +146,7 @@ export declare function recoveryCheckpointCommand(options: {
|
|
|
146
146
|
subcommand: string;
|
|
147
147
|
verdict: string;
|
|
148
148
|
checkpointId: string;
|
|
149
|
-
repoRoot?: undefined;
|
|
149
|
+
readonly repoRoot?: undefined;
|
|
150
150
|
backend?: undefined;
|
|
151
151
|
availableBackends?: undefined;
|
|
152
152
|
backendCounts?: undefined;
|
package/dist/commands/report.js
CHANGED
|
@@ -26,6 +26,7 @@ export async function reportProject(options = {}) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
export function formatReport(report) {
|
|
29
|
+
const recentHostDenials = report.recentHostDenials ?? [];
|
|
29
30
|
const lines = [
|
|
30
31
|
`belay report for ${report.repoRoot}`,
|
|
31
32
|
`Audit log: ${report.auditLogPath}`,
|
|
@@ -34,6 +35,10 @@ export function formatReport(report) {
|
|
|
34
35
|
...formatAskBreakdown(report),
|
|
35
36
|
`Flag (allow_flagged): ${report.flagCount}`,
|
|
36
37
|
`Allow (silent pass): ${report.allowCount}`,
|
|
38
|
+
`Host denied after Belay allow: ${report.hostDeniedAfterAllowCount ?? 0}`,
|
|
39
|
+
...(report.unrecognizedHostFailureCount && report.unrecognizedHostFailureCount > 0
|
|
40
|
+
? [`Unrecognized host tool failures: ${report.unrecognizedHostFailureCount}`]
|
|
41
|
+
: []),
|
|
37
42
|
`Silent-pass rate: ${(report.silentPassRate * 100).toFixed(1)}%`,
|
|
38
43
|
'',
|
|
39
44
|
];
|
|
@@ -51,6 +56,15 @@ export function formatReport(report) {
|
|
|
51
56
|
}
|
|
52
57
|
lines.push('');
|
|
53
58
|
}
|
|
59
|
+
if (recentHostDenials.length > 0) {
|
|
60
|
+
lines.push('Host denials after Belay allow:');
|
|
61
|
+
for (const denial of recentHostDenials) {
|
|
62
|
+
const when = denial.failureTimestamp ?? 'unknown-time';
|
|
63
|
+
const detail = denial.errorMessage ? ` — ${denial.errorMessage}` : '';
|
|
64
|
+
lines.push(`- [${when}] ${denial.summary}${detail}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push('');
|
|
67
|
+
}
|
|
54
68
|
if (report.recentAsks.length === 0) {
|
|
55
69
|
lines.push('No recent asks in the selected period.');
|
|
56
70
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -31,6 +31,7 @@ export async function statusProject(options = {}) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
export function formatStatusReport(report) {
|
|
34
|
+
const recentHostDenials = report.visibility.recentHostDenials ?? [];
|
|
34
35
|
const { health } = report;
|
|
35
36
|
const lines = [
|
|
36
37
|
`belay status for ${report.repoRoot}`,
|
|
@@ -64,6 +65,11 @@ export function formatStatusReport(report) {
|
|
|
64
65
|
...formatAskBreakdown(report.visibility, ' '),
|
|
65
66
|
` Flag (allow_flagged): ${report.visibility.flagCount}`,
|
|
66
67
|
` Allow (silent pass): ${report.visibility.allowCount}`,
|
|
68
|
+
` Host denied after Belay allow: ${report.visibility.hostDeniedAfterAllowCount ?? 0}`,
|
|
69
|
+
...(report.visibility.unrecognizedHostFailureCount &&
|
|
70
|
+
report.visibility.unrecognizedHostFailureCount > 0
|
|
71
|
+
? [` Unrecognized host tool failures: ${report.visibility.unrecognizedHostFailureCount}`]
|
|
72
|
+
: []),
|
|
67
73
|
` Silent-pass rate: ${(report.visibility.silentPassRate * 100).toFixed(1)}%`,
|
|
68
74
|
'',
|
|
69
75
|
];
|
|
@@ -81,6 +87,15 @@ export function formatStatusReport(report) {
|
|
|
81
87
|
}
|
|
82
88
|
lines.push('');
|
|
83
89
|
}
|
|
90
|
+
if (recentHostDenials.length > 0) {
|
|
91
|
+
lines.push('Host denials after Belay allow:');
|
|
92
|
+
for (const denial of recentHostDenials.slice(0, 5)) {
|
|
93
|
+
const when = denial.failureTimestamp ?? 'unknown-time';
|
|
94
|
+
const detail = denial.errorMessage ? ` — ${denial.errorMessage}` : '';
|
|
95
|
+
lines.push(`- [${when}] ${denial.summary}${detail}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push('');
|
|
98
|
+
}
|
|
84
99
|
if (report.visibility.recentAsks.length > 0) {
|
|
85
100
|
lines.push('Recent asks:');
|
|
86
101
|
for (const ask of report.visibility.recentAsks.slice(0, 5)) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WhereOptions, WhereReport } from '../types.js';
|
|
2
|
+
export declare function resolveCliPackageRoot(): string;
|
|
3
|
+
export declare function whereProject(options?: WhereOptions): Promise<WhereReport>;
|
|
4
|
+
export declare function formatWhereReport(report: WhereReport): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
5
|
+
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
6
|
+
import { detectAdapterName } from '../config-io.js';
|
|
7
|
+
import { resolveOperationScope } from '../installer/scope-config.js';
|
|
8
|
+
export function resolveCliPackageRoot() {
|
|
9
|
+
return path.resolve(fileURLToPath(new URL('../..', import.meta.url)));
|
|
10
|
+
}
|
|
11
|
+
export async function whereProject(options = {}) {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const repoRoot = path.resolve(options.targetDir ?? cwd);
|
|
14
|
+
const adapter = options.adapter ?? detectAdapterName(repoRoot);
|
|
15
|
+
const scope = await resolveOperationScope(repoRoot, adapter, options);
|
|
16
|
+
const paths = resolveScopedPaths(getAdapterLayout(adapter), scope, repoRoot);
|
|
17
|
+
const configPresent = existsSync(paths.configPath);
|
|
18
|
+
return {
|
|
19
|
+
cwd,
|
|
20
|
+
repoRoot,
|
|
21
|
+
adapter,
|
|
22
|
+
installScope: scope,
|
|
23
|
+
configPresent,
|
|
24
|
+
cliExecutable: process.argv[1] ? path.resolve(process.argv[1]) : undefined,
|
|
25
|
+
cliPackageRoot: resolveCliPackageRoot(),
|
|
26
|
+
configPath: paths.configPath,
|
|
27
|
+
hooksSettingsPath: paths.hooksSettingsPath,
|
|
28
|
+
hooksDir: paths.hooksDir,
|
|
29
|
+
runtimeDir: paths.runtimeDir,
|
|
30
|
+
skillsDir: paths.skillsDir,
|
|
31
|
+
commandsDir: paths.commandsDir,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function formatWhereReport(report) {
|
|
35
|
+
const lines = [
|
|
36
|
+
`cwd: ${report.cwd}`,
|
|
37
|
+
`target dir: ${report.repoRoot}`,
|
|
38
|
+
`adapter: ${report.adapter} (scope=${report.installScope})`,
|
|
39
|
+
`config present: ${report.configPresent ? 'yes' : 'no'}`,
|
|
40
|
+
`cli executable: ${report.cliExecutable ?? '(unknown)'}`,
|
|
41
|
+
`cli package: ${report.cliPackageRoot}`,
|
|
42
|
+
`config: ${report.configPath}`,
|
|
43
|
+
`hooks settings: ${report.hooksSettingsPath}`,
|
|
44
|
+
`hooks: ${report.hooksDir}`,
|
|
45
|
+
`runtime: ${report.runtimeDir}`,
|
|
46
|
+
`skills: ${report.skillsDir}`,
|
|
47
|
+
];
|
|
48
|
+
if (report.commandsDir) {
|
|
49
|
+
lines.push(`commands: ${report.commandsDir}`);
|
|
50
|
+
}
|
|
51
|
+
return `${lines.join('\n')}\n`;
|
|
52
|
+
}
|
package/dist/config-io.js
CHANGED
|
@@ -209,8 +209,17 @@ export async function loadConfigFile(repoRoot, adapter = detectAdapterName(repoR
|
|
|
209
209
|
}
|
|
210
210
|
export async function writeConfigFile(repoRoot, config, adapter = resolveAdapterName(config)) {
|
|
211
211
|
const configPath = configPathFor(repoRoot, adapter);
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const directory = path.dirname(configPath);
|
|
213
|
+
const temporaryPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
214
|
+
const content = `${JSON.stringify(stripForbiddenShellOverrideLists(config), null, 2)}\n`;
|
|
215
|
+
await mkdir(directory, { recursive: true });
|
|
216
|
+
try {
|
|
217
|
+
await writeFile(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
|
|
218
|
+
await rename(temporaryPath, configPath);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
222
|
+
}
|
|
214
223
|
}
|
|
215
224
|
export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
216
225
|
const layout = getAdapterLayout(adapter);
|