@cairnkeep/cli 2.3.1 → 2.5.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/CHANGELOG.md +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, open, rename, rm, } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { CAPABILITY_REGISTRY } from "./capability-registry.js";
|
|
6
|
+
import { CAPABILITY_IDS, CAPABILITY_SCHEMA_VERSION, capabilityIdSchema, capabilityManagedConfigSchema, capabilityStatusSchema, } from "./capability-schema.js";
|
|
7
|
+
const MAX_CONFIG_BYTES = 64 * 1024;
|
|
8
|
+
const LOCK_WAIT_MS = 10;
|
|
9
|
+
const LOCK_ATTEMPTS = 200;
|
|
10
|
+
const truthyPattern = /^(?:1|true|yes|on)$/i;
|
|
11
|
+
const falsyPattern = /^(?:0|false|no|off)$/i;
|
|
12
|
+
function emptyConfig() {
|
|
13
|
+
return {
|
|
14
|
+
schema_version: CAPABILITY_SCHEMA_VERSION,
|
|
15
|
+
capabilities: {},
|
|
16
|
+
logging: {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function configPaths(projectRoot) {
|
|
20
|
+
if (typeof projectRoot !== "string" || projectRoot.length === 0) {
|
|
21
|
+
throw new Error("A project root is required.");
|
|
22
|
+
}
|
|
23
|
+
const root = resolve(projectRoot);
|
|
24
|
+
const directory = join(root, ".ai");
|
|
25
|
+
const config = join(directory, "capabilities.json");
|
|
26
|
+
const lock = join(directory, ".capabilities.lock");
|
|
27
|
+
if (dirname(directory) !== root || dirname(config) !== directory || dirname(lock) !== directory) {
|
|
28
|
+
throw new Error("Capability configuration path is outside the project root.");
|
|
29
|
+
}
|
|
30
|
+
return { root, directory, config, lock };
|
|
31
|
+
}
|
|
32
|
+
async function assertSafeDirectory(directory, create) {
|
|
33
|
+
try {
|
|
34
|
+
const value = await lstat(directory);
|
|
35
|
+
if (!value.isDirectory() || value.isSymbolicLink()) {
|
|
36
|
+
throw new Error("Capability configuration directory is unsafe.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT")
|
|
41
|
+
throw error;
|
|
42
|
+
if (!create)
|
|
43
|
+
return;
|
|
44
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
45
|
+
const value = await lstat(directory);
|
|
46
|
+
if (!value.isDirectory() || value.isSymbolicLink()) {
|
|
47
|
+
throw new Error("Capability configuration directory is unsafe.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function readBoundedConfig(path) {
|
|
52
|
+
let handle;
|
|
53
|
+
try {
|
|
54
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === "ENOENT")
|
|
58
|
+
return undefined;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const info = await handle.stat();
|
|
63
|
+
if (!info.isFile() || info.size > MAX_CONFIG_BYTES) {
|
|
64
|
+
throw new Error("Capability configuration is invalid.");
|
|
65
|
+
}
|
|
66
|
+
const bytes = await handle.readFile();
|
|
67
|
+
if (bytes.byteLength > MAX_CONFIG_BYTES) {
|
|
68
|
+
throw new Error("Capability configuration is invalid.");
|
|
69
|
+
}
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function isRecord(value) {
|
|
77
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
78
|
+
}
|
|
79
|
+
function parseConfig(value) {
|
|
80
|
+
const config = emptyConfig();
|
|
81
|
+
const issues = [];
|
|
82
|
+
if (!isRecord(value)
|
|
83
|
+
|| value.schema_version !== CAPABILITY_SCHEMA_VERSION
|
|
84
|
+
|| !isRecord(value.capabilities)
|
|
85
|
+
|| !isRecord(value.logging)
|
|
86
|
+
|| Object.keys(value).some((key) => !["schema_version", "capabilities", "logging"].includes(key))) {
|
|
87
|
+
return { config, issues: [{ code: "invalid-config" }] };
|
|
88
|
+
}
|
|
89
|
+
for (const [id, raw] of Object.entries(value.capabilities)) {
|
|
90
|
+
const parsedId = capabilityIdSchema.safeParse(id);
|
|
91
|
+
if (!parsedId.success) {
|
|
92
|
+
issues.push({ code: "unknown-capability" });
|
|
93
|
+
}
|
|
94
|
+
else if (typeof raw !== "boolean") {
|
|
95
|
+
issues.push({ code: "invalid-capability-value", capability_id: parsedId.data });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
config.capabilities[parsedId.data] = raw;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const loggingKeys = Object.keys(value.logging);
|
|
102
|
+
if (loggingKeys.some((key) => key !== "callbacks")
|
|
103
|
+
|| (Object.hasOwn(value.logging, "callbacks") && typeof value.logging.callbacks !== "boolean")) {
|
|
104
|
+
issues.push({ code: "invalid-logging-value", setting: "logging.callbacks" });
|
|
105
|
+
}
|
|
106
|
+
else if (typeof value.logging.callbacks === "boolean") {
|
|
107
|
+
config.logging.callbacks = value.logging.callbacks;
|
|
108
|
+
}
|
|
109
|
+
return { config, issues };
|
|
110
|
+
}
|
|
111
|
+
export function isCapabilityContractEnabled(value = process.env.CAIRN_CAPABILITY_CONTRACT) {
|
|
112
|
+
return truthyPattern.test(value?.trim() ?? "");
|
|
113
|
+
}
|
|
114
|
+
export async function readCapabilityConfig(options = {}) {
|
|
115
|
+
const { directory, config: configPath } = configPaths(options.projectRoot ?? process.cwd());
|
|
116
|
+
try {
|
|
117
|
+
await assertSafeDirectory(directory, false);
|
|
118
|
+
try {
|
|
119
|
+
const info = await lstat(configPath);
|
|
120
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
121
|
+
return { config: emptyConfig(), issues: [{ code: "invalid-config" }] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code === "ENOENT") {
|
|
126
|
+
return { config: emptyConfig(), issues: [] };
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const bytes = await readBoundedConfig(configPath);
|
|
131
|
+
if (!bytes)
|
|
132
|
+
return { config: emptyConfig(), issues: [] };
|
|
133
|
+
return parseConfig(JSON.parse(bytes.toString("utf8")));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { config: emptyConfig(), issues: [{ code: "invalid-config" }] };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function parseEnvironmentBoolean(value) {
|
|
140
|
+
if (value === undefined)
|
|
141
|
+
return undefined;
|
|
142
|
+
const normalized = value.trim();
|
|
143
|
+
if (truthyPattern.test(normalized))
|
|
144
|
+
return true;
|
|
145
|
+
if (falsyPattern.test(normalized))
|
|
146
|
+
return false;
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
async function resolveBoolean(environmentValue, projectHasValue, projectValue, compatibility, invalidIssue, issues) {
|
|
150
|
+
const parsedEnvironment = parseEnvironmentBoolean(environmentValue);
|
|
151
|
+
if (parsedEnvironment === null) {
|
|
152
|
+
issues.push(invalidIssue);
|
|
153
|
+
return { enabled: await compatibility(), source: "compatibility" };
|
|
154
|
+
}
|
|
155
|
+
if (parsedEnvironment !== undefined) {
|
|
156
|
+
return { enabled: parsedEnvironment, source: "environment" };
|
|
157
|
+
}
|
|
158
|
+
if (projectHasValue) {
|
|
159
|
+
return { enabled: projectValue, source: "project" };
|
|
160
|
+
}
|
|
161
|
+
return { enabled: await compatibility(), source: "compatibility" };
|
|
162
|
+
}
|
|
163
|
+
export async function resolveCapabilityStatus(options = {}) {
|
|
164
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
165
|
+
const env = options.env ?? process.env;
|
|
166
|
+
const contractEnabled = isCapabilityContractEnabled(env.CAIRN_CAPABILITY_CONTRACT);
|
|
167
|
+
const { config, issues } = await readCapabilityConfig({ projectRoot });
|
|
168
|
+
const capabilities = [];
|
|
169
|
+
for (const row of CAPABILITY_REGISTRY) {
|
|
170
|
+
const state = await resolveBoolean(env[row.environment], Object.hasOwn(config.capabilities, row.id), config.capabilities[row.id], () => row.resolveCompatibility({ projectRoot, env, graphifyEnabled: options.graphifyEnabled }), { code: "invalid-capability-value", capability_id: row.id }, issues);
|
|
171
|
+
capabilities.push({
|
|
172
|
+
id: row.id,
|
|
173
|
+
kind: row.kind,
|
|
174
|
+
enabled: state.enabled,
|
|
175
|
+
source: state.source,
|
|
176
|
+
restart_required: row.restart_required,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const logging = await resolveBoolean(env.CAIRN_CAPABILITY_LOGGING, Object.hasOwn(config.logging, "callbacks"), config.logging.callbacks, () => false, { code: "invalid-logging-value", setting: "logging.callbacks" }, issues);
|
|
180
|
+
const digestInput = {
|
|
181
|
+
schema_version: CAPABILITY_SCHEMA_VERSION,
|
|
182
|
+
contract_enabled: contractEnabled,
|
|
183
|
+
capabilities: capabilities.map(({ id, enabled }) => ({ id, enabled })),
|
|
184
|
+
logging: { callbacks: logging.enabled },
|
|
185
|
+
};
|
|
186
|
+
const configurationDigest = createHash("sha256")
|
|
187
|
+
.update(JSON.stringify(digestInput), "utf8")
|
|
188
|
+
.digest("hex");
|
|
189
|
+
return capabilityStatusSchema.parse({
|
|
190
|
+
schema_version: CAPABILITY_SCHEMA_VERSION,
|
|
191
|
+
contract_enabled: contractEnabled,
|
|
192
|
+
logging,
|
|
193
|
+
configuration_digest: configurationDigest,
|
|
194
|
+
capabilities,
|
|
195
|
+
issues,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function orderedConfig(config) {
|
|
199
|
+
const capabilities = {};
|
|
200
|
+
for (const id of CAPABILITY_IDS) {
|
|
201
|
+
if (Object.hasOwn(config.capabilities, id))
|
|
202
|
+
capabilities[id] = config.capabilities[id];
|
|
203
|
+
}
|
|
204
|
+
return capabilityManagedConfigSchema.parse({
|
|
205
|
+
schema_version: CAPABILITY_SCHEMA_VERSION,
|
|
206
|
+
capabilities,
|
|
207
|
+
logging: Object.hasOwn(config.logging, "callbacks")
|
|
208
|
+
? { callbacks: config.logging.callbacks }
|
|
209
|
+
: {},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function delay(milliseconds) {
|
|
213
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
|
|
214
|
+
}
|
|
215
|
+
async function acquireLock(lockPath) {
|
|
216
|
+
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
|
|
217
|
+
try {
|
|
218
|
+
await mkdir(lockPath, { mode: 0o700 });
|
|
219
|
+
return () => rm(lockPath, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error.code !== "EEXIST")
|
|
223
|
+
throw error;
|
|
224
|
+
const info = await lstat(lockPath);
|
|
225
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
226
|
+
throw new Error("Capability configuration lock is unsafe.");
|
|
227
|
+
}
|
|
228
|
+
await delay(LOCK_WAIT_MS);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
throw new Error("Capability configuration is locked; retry after the active update finishes.");
|
|
232
|
+
}
|
|
233
|
+
function requireCapabilityId(value) {
|
|
234
|
+
const parsed = capabilityIdSchema.safeParse(value);
|
|
235
|
+
if (!parsed.success)
|
|
236
|
+
throw new Error("Unknown capability ID.");
|
|
237
|
+
return parsed.data;
|
|
238
|
+
}
|
|
239
|
+
async function atomicWriteConfig(path, config) {
|
|
240
|
+
const parsed = orderedConfig(config);
|
|
241
|
+
const bytes = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
242
|
+
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
243
|
+
let handle;
|
|
244
|
+
try {
|
|
245
|
+
handle = await open(temporary, "wx", 0o600);
|
|
246
|
+
await handle.writeFile(bytes, { encoding: "utf8" });
|
|
247
|
+
await handle.sync();
|
|
248
|
+
await handle.close();
|
|
249
|
+
handle = undefined;
|
|
250
|
+
await chmod(temporary, 0o600);
|
|
251
|
+
await rename(temporary, path);
|
|
252
|
+
await chmod(path, 0o600);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
if (handle)
|
|
256
|
+
await handle.close().catch(() => undefined);
|
|
257
|
+
await rm(temporary, { force: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function mutateConfig(projectRoot, mutate) {
|
|
261
|
+
const { directory, config: configPath, lock } = configPaths(projectRoot);
|
|
262
|
+
await assertSafeDirectory(directory, true);
|
|
263
|
+
const release = await acquireLock(lock);
|
|
264
|
+
try {
|
|
265
|
+
const current = await readCapabilityConfig({ projectRoot });
|
|
266
|
+
mutate(current.config);
|
|
267
|
+
await atomicWriteConfig(configPath, current.config);
|
|
268
|
+
return orderedConfig(current.config);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
await release();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export async function setCapabilityOverride(options) {
|
|
275
|
+
const id = requireCapabilityId(options.id);
|
|
276
|
+
if (typeof options.enabled !== "boolean")
|
|
277
|
+
throw new Error("Capability state must be boolean.");
|
|
278
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
279
|
+
return mutateConfig(projectRoot, (config) => {
|
|
280
|
+
config.capabilities[id] = options.enabled;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
export async function resetCapabilityOverride(options) {
|
|
284
|
+
if (options.id !== "all")
|
|
285
|
+
requireCapabilityId(options.id);
|
|
286
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
287
|
+
return mutateConfig(projectRoot, (config) => {
|
|
288
|
+
if (options.id === "all") {
|
|
289
|
+
config.capabilities = {};
|
|
290
|
+
config.logging = {};
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
delete config.capabilities[options.id];
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
export async function setCapabilityLogging(options) {
|
|
298
|
+
if (typeof options.enabled !== "boolean")
|
|
299
|
+
throw new Error("Capability logging state must be boolean.");
|
|
300
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
301
|
+
return mutateConfig(projectRoot, (config) => {
|
|
302
|
+
config.logging.callbacks = options.enabled;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
export async function resetCapabilityLogging(options = {}) {
|
|
306
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
307
|
+
return mutateConfig(projectRoot, (config) => {
|
|
308
|
+
delete config.logging.callbacks;
|
|
309
|
+
});
|
|
310
|
+
}
|