@astudioplus/compressor 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/CHANGELOG.md +52 -0
- package/LICENSE +20 -0
- package/README.md +167 -0
- package/dist/adapters/agents-md.d.ts +2 -0
- package/dist/adapters/agents-md.js +91 -0
- package/dist/adapters/apply.d.ts +3 -0
- package/dist/adapters/apply.js +83 -0
- package/dist/adapters/claude-code.d.ts +2 -0
- package/dist/adapters/claude-code.js +403 -0
- package/dist/adapters/copilot.d.ts +2 -0
- package/dist/adapters/copilot.js +418 -0
- package/dist/adapters/cursor.d.ts +2 -0
- package/dist/adapters/cursor.js +149 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +19 -0
- package/dist/adapters/markers.d.ts +7 -0
- package/dist/adapters/markers.js +129 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +1 -0
- package/dist/bench/ablate.d.ts +35 -0
- package/dist/bench/ablate.js +163 -0
- package/dist/bench/cell.d.ts +33 -0
- package/dist/bench/cell.js +437 -0
- package/dist/bench/results.d.ts +37 -0
- package/dist/bench/results.js +157 -0
- package/dist/bench/runner.d.ts +24 -0
- package/dist/bench/runner.js +121 -0
- package/dist/bench/tasks.d.ts +4 -0
- package/dist/bench/tasks.js +147 -0
- package/dist/bench/types.d.ts +109 -0
- package/dist/bench/types.js +1 -0
- package/dist/claude/transcripts.d.ts +30 -0
- package/dist/claude/transcripts.js +154 -0
- package/dist/cli/commands/benchmark.d.ts +33 -0
- package/dist/cli/commands/benchmark.js +203 -0
- package/dist/cli/commands/compress.d.ts +8 -0
- package/dist/cli/commands/compress.js +45 -0
- package/dist/cli/commands/count.d.ts +5 -0
- package/dist/cli/commands/count.js +25 -0
- package/dist/cli/commands/hook.d.ts +6 -0
- package/dist/cli/commands/hook.js +30 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +76 -0
- package/dist/cli/commands/report.d.ts +90 -0
- package/dist/cli/commands/report.js +464 -0
- package/dist/cli/commands/savings.d.ts +38 -0
- package/dist/cli/commands/savings.js +196 -0
- package/dist/cli/commands/set-mode.d.ts +5 -0
- package/dist/cli/commands/set-mode.js +13 -0
- package/dist/cli/commands/stats.d.ts +5 -0
- package/dist/cli/commands/stats.js +51 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +11 -0
- package/dist/cli/commands/uninstall.d.ts +7 -0
- package/dist/cli/commands/uninstall.js +22 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +146 -0
- package/dist/copilot-hook-entry.d.ts +1 -0
- package/dist/copilot-hook-entry.js +36 -0
- package/dist/copilot-hook.js +1000 -0
- package/dist/engine/detect.d.ts +2 -0
- package/dist/engine/detect.js +47 -0
- package/dist/engine/index.d.ts +4 -0
- package/dist/engine/index.js +90 -0
- package/dist/engine/policy.d.ts +2 -0
- package/dist/engine/policy.js +48 -0
- package/dist/engine/tiers/code.d.ts +7 -0
- package/dist/engine/tiers/code.js +206 -0
- package/dist/engine/tiers/logs.d.ts +4 -0
- package/dist/engine/tiers/logs.js +139 -0
- package/dist/engine/tiers/structural.d.ts +28 -0
- package/dist/engine/tiers/structural.js +199 -0
- package/dist/engine/types.d.ts +71 -0
- package/dist/engine/types.js +5 -0
- package/dist/hook/copilot.d.ts +5 -0
- package/dist/hook/copilot.js +136 -0
- package/dist/hook/core.d.ts +36 -0
- package/dist/hook/core.js +138 -0
- package/dist/hook/exit.d.ts +22 -0
- package/dist/hook/exit.js +56 -0
- package/dist/hook/post-tool-use.d.ts +5 -0
- package/dist/hook/post-tool-use.js +57 -0
- package/dist/hook-entry.d.ts +1 -0
- package/dist/hook-entry.js +35 -0
- package/dist/hook.js +946 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/ledger/read.d.ts +9 -0
- package/dist/ledger/read.js +91 -0
- package/dist/ledger/write.d.ts +29 -0
- package/dist/ledger/write.js +61 -0
- package/dist/packs/atoms.d.ts +3 -0
- package/dist/packs/atoms.js +108 -0
- package/dist/packs/modes.d.ts +3 -0
- package/dist/packs/modes.js +34 -0
- package/dist/packs/render.d.ts +24 -0
- package/dist/packs/render.js +115 -0
- package/dist/packs/types.d.ts +32 -0
- package/dist/packs/types.js +1 -0
- package/dist/paths.d.ts +29 -0
- package/dist/paths.js +87 -0
- package/dist/tokens/estimate.d.ts +12 -0
- package/dist/tokens/estimate.js +23 -0
- package/dist/tokens/exact.d.ts +5 -0
- package/dist/tokens/exact.js +16 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +2 -0
- package/package.json +77 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseAtomManifest, renderOutputStyle } from "../packs/render.js";
|
|
4
|
+
const PACK_MODES = ['optimized', 'slim'];
|
|
5
|
+
const SETTINGS_FILE = 'settings.json';
|
|
6
|
+
const LOCAL_SETTINGS_FILE = 'settings.local.json';
|
|
7
|
+
function invalidSettingsError(fileName) {
|
|
8
|
+
return `${fileName} is not valid JSON — not touching it (fix or remove it, then re-run)`;
|
|
9
|
+
}
|
|
10
|
+
const HOOK_MATCHER = 'Read|Bash|Grep|Glob';
|
|
11
|
+
function isPackMode(value) {
|
|
12
|
+
return value === 'optimized' || value === 'slim';
|
|
13
|
+
}
|
|
14
|
+
function scopeRoot(ctx, global) {
|
|
15
|
+
return global ? ctx.homeDir : ctx.projectDir;
|
|
16
|
+
}
|
|
17
|
+
function stylePath(root, mode) {
|
|
18
|
+
return path.join(root, '.claude', 'output-styles', `compressor-${mode}.md`);
|
|
19
|
+
}
|
|
20
|
+
function settingsPath(root) {
|
|
21
|
+
return path.join(root, '.claude', SETTINGS_FILE);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Where the hook entry lives. Project-scope settings.json is the SHARED file
|
|
25
|
+
* (conventionally committed); the hook command embeds a machine-specific
|
|
26
|
+
* absolute path, so at project scope it goes into settings.local.json.
|
|
27
|
+
* Global scope (~/.claude) is personal — settings.json holds everything.
|
|
28
|
+
*/
|
|
29
|
+
function hookSettingsPath(root, global) {
|
|
30
|
+
return global ? settingsPath(root) : path.join(root, '.claude', LOCAL_SETTINGS_FILE);
|
|
31
|
+
}
|
|
32
|
+
function isErrnoException(error) {
|
|
33
|
+
return error instanceof Error && 'code' in error;
|
|
34
|
+
}
|
|
35
|
+
async function readFileOrNull(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
return await readFile(filePath, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function dirExists(dirPath) {
|
|
47
|
+
try {
|
|
48
|
+
return (await stat(dirPath)).isDirectory();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function asRecord(value) {
|
|
55
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
56
|
+
? value
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
function asArray(value) {
|
|
60
|
+
return Array.isArray(value) ? value : null;
|
|
61
|
+
}
|
|
62
|
+
function parseSettings(text, fileName = SETTINGS_FILE) {
|
|
63
|
+
if (text === null) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(invalidSettingsError(fileName));
|
|
72
|
+
}
|
|
73
|
+
const record = asRecord(parsed);
|
|
74
|
+
if (record === null) {
|
|
75
|
+
throw new Error(invalidSettingsError(fileName));
|
|
76
|
+
}
|
|
77
|
+
return { ...record };
|
|
78
|
+
}
|
|
79
|
+
function detectIndent(original) {
|
|
80
|
+
if (original === null) {
|
|
81
|
+
return ' ';
|
|
82
|
+
}
|
|
83
|
+
return /\n([ \t]+)"/.exec(original)?.[1] ?? ' ';
|
|
84
|
+
}
|
|
85
|
+
function serializeSettings(settings, original) {
|
|
86
|
+
return `${JSON.stringify(settings, null, detectIndent(original))}\n`;
|
|
87
|
+
}
|
|
88
|
+
function hookCommandBase(hookCommand) {
|
|
89
|
+
return hookCommand.replace(/ --mode \S+$/, '');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Base forms we accept as ours: the current command (hook.js path quoted
|
|
93
|
+
* against spaces) and the legacy unquoted form written by older installs.
|
|
94
|
+
*/
|
|
95
|
+
function ourBases(hookCommand) {
|
|
96
|
+
const base = hookCommandBase(hookCommand);
|
|
97
|
+
const unquoted = base.replaceAll('"', '');
|
|
98
|
+
return unquoted === base ? [base] : [base, unquoted];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Ownership predicate: exact match on our resolved hook command, allowing a
|
|
102
|
+
* different --mode value (mode switches rewrite the flag; the absolute path
|
|
103
|
+
* identifies us). Generic substrings like 'dist/hook.js' are NOT ours —
|
|
104
|
+
* other tools use the same bundling layout.
|
|
105
|
+
*/
|
|
106
|
+
function isOurHookEntry(entry, hookCommand) {
|
|
107
|
+
const record = asRecord(entry);
|
|
108
|
+
const hooks = asArray(record?.hooks);
|
|
109
|
+
if (hooks === null) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const bases = ourBases(hookCommand);
|
|
113
|
+
return hooks.some((hook) => {
|
|
114
|
+
const command = asRecord(hook)?.command;
|
|
115
|
+
return (typeof command === 'string' &&
|
|
116
|
+
(command === hookCommand ||
|
|
117
|
+
bases.some((base) => command === base || command.startsWith(`${base} --mode `))));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function mergeHookEntry(settings, hookCommand) {
|
|
121
|
+
const ourEntry = {
|
|
122
|
+
matcher: HOOK_MATCHER,
|
|
123
|
+
hooks: [{ type: 'command', command: hookCommand }],
|
|
124
|
+
};
|
|
125
|
+
const hooks = { ...(asRecord(settings.hooks) ?? {}) };
|
|
126
|
+
const post = asArray(hooks.PostToolUse) ?? [];
|
|
127
|
+
const next = [];
|
|
128
|
+
let replaced = false;
|
|
129
|
+
for (const entry of post) {
|
|
130
|
+
if (isOurHookEntry(entry, hookCommand)) {
|
|
131
|
+
if (!replaced) {
|
|
132
|
+
next.push(ourEntry);
|
|
133
|
+
replaced = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
next.push(entry);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!replaced) {
|
|
141
|
+
next.push(ourEntry);
|
|
142
|
+
}
|
|
143
|
+
hooks.PostToolUse = next;
|
|
144
|
+
settings.hooks = hooks;
|
|
145
|
+
}
|
|
146
|
+
function stripOurHooks(settings, hookCommand) {
|
|
147
|
+
const hooks = asRecord(settings.hooks);
|
|
148
|
+
const post = asArray(hooks?.PostToolUse);
|
|
149
|
+
if (hooks === null || post === null) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const kept = post.filter((entry) => !isOurHookEntry(entry, hookCommand));
|
|
153
|
+
if (kept.length === post.length) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const next = { ...hooks };
|
|
157
|
+
if (kept.length === 0) {
|
|
158
|
+
delete next.PostToolUse;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
next.PostToolUse = kept;
|
|
162
|
+
}
|
|
163
|
+
if (Object.keys(next).length === 0) {
|
|
164
|
+
delete settings.hooks;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
settings.hooks = next;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function stripOurSettings(settings, hookCommand, priorStyle) {
|
|
171
|
+
if (typeof settings.outputStyle === 'string' &&
|
|
172
|
+
settings.outputStyle.startsWith('compressor-')) {
|
|
173
|
+
if (priorStyle !== null) {
|
|
174
|
+
settings.outputStyle = priorStyle;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
delete settings.outputStyle;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
stripOurHooks(settings, hookCommand);
|
|
181
|
+
}
|
|
182
|
+
// A pre-existing foreign outputStyle is stashed in our style file (an
|
|
183
|
+
// artifact we own) so uninstall can restore it instead of deleting it.
|
|
184
|
+
const PRIOR_STYLE_RE = /<!-- compressor:prior-output-style (.*) -->/;
|
|
185
|
+
function stashPriorStyle(body, prior) {
|
|
186
|
+
if (prior === null) {
|
|
187
|
+
return body;
|
|
188
|
+
}
|
|
189
|
+
return `${body}<!-- compressor:prior-output-style ${JSON.stringify(prior)} -->\n`;
|
|
190
|
+
}
|
|
191
|
+
function priorStyleFrom(body) {
|
|
192
|
+
if (body === null) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const raw = PRIOR_STYLE_RE.exec(body)?.[1];
|
|
196
|
+
if (raw === undefined) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(raw);
|
|
201
|
+
return typeof parsed === 'string' ? parsed : null;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function hasArtifacts(artifacts) {
|
|
208
|
+
return artifacts.styleMode !== null || artifacts.settingsMode !== null;
|
|
209
|
+
}
|
|
210
|
+
async function hasOurHook(filePath, fileName, hookCommand) {
|
|
211
|
+
const text = await readFileOrNull(filePath);
|
|
212
|
+
if (text === null) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
let settings = null;
|
|
216
|
+
try {
|
|
217
|
+
settings = parseSettings(text, fileName);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
settings = null;
|
|
221
|
+
}
|
|
222
|
+
const post = asArray(asRecord(settings?.hooks)?.PostToolUse);
|
|
223
|
+
return post?.some((entry) => isOurHookEntry(entry, hookCommand)) ?? false;
|
|
224
|
+
}
|
|
225
|
+
async function inspectScope(root, global, hookCommand) {
|
|
226
|
+
let styleMode = null;
|
|
227
|
+
for (const mode of PACK_MODES) {
|
|
228
|
+
const body = await readFileOrNull(stylePath(root, mode));
|
|
229
|
+
if (body !== null) {
|
|
230
|
+
styleMode = parseAtomManifest(body)?.mode ?? mode;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
let settingsMode = null;
|
|
235
|
+
const text = await readFileOrNull(settingsPath(root));
|
|
236
|
+
if (text !== null) {
|
|
237
|
+
let settings = null;
|
|
238
|
+
try {
|
|
239
|
+
settings = parseSettings(text);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
settings = null;
|
|
243
|
+
}
|
|
244
|
+
if (settings !== null) {
|
|
245
|
+
const outputStyle = settings.outputStyle;
|
|
246
|
+
if (typeof outputStyle === 'string' &&
|
|
247
|
+
outputStyle.startsWith('compressor-')) {
|
|
248
|
+
const mode = outputStyle.slice('compressor-'.length);
|
|
249
|
+
if (isPackMode(mode)) {
|
|
250
|
+
settingsMode = mode;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let hookPresent = await hasOurHook(hookSettingsPath(root, global), global ? SETTINGS_FILE : LOCAL_SETTINGS_FILE, hookCommand);
|
|
256
|
+
if (!hookPresent && !global) {
|
|
257
|
+
hookPresent = await hasOurHook(settingsPath(root), SETTINGS_FILE, hookCommand);
|
|
258
|
+
}
|
|
259
|
+
return { styleMode, settingsMode, hookPresent };
|
|
260
|
+
}
|
|
261
|
+
export const claudeCodeAdapter = {
|
|
262
|
+
name: 'claude-code',
|
|
263
|
+
async detect(ctx) {
|
|
264
|
+
if (await dirExists(path.join(scopeRoot(ctx, ctx.global), '.claude'))) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (await dirExists(path.join(scopeRoot(ctx, !ctx.global), '.claude'))) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
// Claude Code is the primary target: project scope always detects in v1.
|
|
271
|
+
return !ctx.global;
|
|
272
|
+
},
|
|
273
|
+
async install(mode, ctx) {
|
|
274
|
+
const root = scopeRoot(ctx, ctx.global);
|
|
275
|
+
const changes = [];
|
|
276
|
+
const styleFile = stylePath(root, mode);
|
|
277
|
+
const styleBefore = await readFileOrNull(styleFile);
|
|
278
|
+
const otherMode = mode === 'slim' ? 'optimized' : 'slim';
|
|
279
|
+
const otherFile = stylePath(root, otherMode);
|
|
280
|
+
const otherBefore = await readFileOrNull(otherFile);
|
|
281
|
+
const settingsFile = settingsPath(root);
|
|
282
|
+
const settingsBefore = await readFileOrNull(settingsFile);
|
|
283
|
+
const settings = parseSettings(settingsBefore);
|
|
284
|
+
// Preserve a pre-existing foreign outputStyle (or one already stashed by
|
|
285
|
+
// a previous install) so uninstall can restore it.
|
|
286
|
+
const currentStyle = typeof settings.outputStyle === 'string' ? settings.outputStyle : null;
|
|
287
|
+
const foreignStyle = currentStyle !== null && !currentStyle.startsWith('compressor-')
|
|
288
|
+
? currentStyle
|
|
289
|
+
: null;
|
|
290
|
+
const prior = foreignStyle ?? priorStyleFrom(styleBefore) ?? priorStyleFrom(otherBefore);
|
|
291
|
+
changes.push({
|
|
292
|
+
path: styleFile,
|
|
293
|
+
before: styleBefore,
|
|
294
|
+
after: stashPriorStyle(renderOutputStyle(mode).body, prior),
|
|
295
|
+
});
|
|
296
|
+
if (otherBefore !== null) {
|
|
297
|
+
changes.push({ path: otherFile, before: otherBefore, after: null });
|
|
298
|
+
}
|
|
299
|
+
settings.outputStyle = `compressor-${mode}`;
|
|
300
|
+
if (ctx.global) {
|
|
301
|
+
mergeHookEntry(settings, ctx.hookCommand);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// shared settings.json must never carry our machine-specific command
|
|
305
|
+
stripOurHooks(settings, ctx.hookCommand);
|
|
306
|
+
const localFile = hookSettingsPath(root, false);
|
|
307
|
+
const localBefore = await readFileOrNull(localFile);
|
|
308
|
+
const local = parseSettings(localBefore, LOCAL_SETTINGS_FILE);
|
|
309
|
+
mergeHookEntry(local, ctx.hookCommand);
|
|
310
|
+
changes.push({
|
|
311
|
+
path: localFile,
|
|
312
|
+
before: localBefore,
|
|
313
|
+
after: serializeSettings(local, localBefore),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
changes.push({
|
|
317
|
+
path: settingsFile,
|
|
318
|
+
before: settingsBefore,
|
|
319
|
+
after: serializeSettings(settings, settingsBefore),
|
|
320
|
+
});
|
|
321
|
+
return changes.filter((change) => change.before !== change.after);
|
|
322
|
+
},
|
|
323
|
+
async uninstall(ctx) {
|
|
324
|
+
const root = scopeRoot(ctx, ctx.global);
|
|
325
|
+
const changes = [];
|
|
326
|
+
let priorStyle = null;
|
|
327
|
+
for (const mode of PACK_MODES) {
|
|
328
|
+
const file = stylePath(root, mode);
|
|
329
|
+
const before = await readFileOrNull(file);
|
|
330
|
+
if (before !== null) {
|
|
331
|
+
priorStyle ??= priorStyleFrom(before);
|
|
332
|
+
changes.push({ path: file, before, after: null });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const settingsFile = settingsPath(root);
|
|
336
|
+
const settingsBefore = await readFileOrNull(settingsFile);
|
|
337
|
+
if (settingsBefore !== null) {
|
|
338
|
+
const settings = parseSettings(settingsBefore);
|
|
339
|
+
stripOurSettings(settings, ctx.hookCommand, priorStyle);
|
|
340
|
+
const after = Object.keys(settings).length === 0
|
|
341
|
+
? null
|
|
342
|
+
: serializeSettings(settings, settingsBefore);
|
|
343
|
+
if (after !== settingsBefore) {
|
|
344
|
+
changes.push({ path: settingsFile, before: settingsBefore, after });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!ctx.global) {
|
|
348
|
+
const localFile = hookSettingsPath(root, false);
|
|
349
|
+
const localBefore = await readFileOrNull(localFile);
|
|
350
|
+
if (localBefore !== null) {
|
|
351
|
+
const local = parseSettings(localBefore, LOCAL_SETTINGS_FILE);
|
|
352
|
+
stripOurHooks(local, ctx.hookCommand);
|
|
353
|
+
const after = Object.keys(local).length === 0
|
|
354
|
+
? null
|
|
355
|
+
: serializeSettings(local, localBefore);
|
|
356
|
+
if (after !== localBefore) {
|
|
357
|
+
changes.push({ path: localFile, before: localBefore, after });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return changes;
|
|
362
|
+
},
|
|
363
|
+
async status(ctx) {
|
|
364
|
+
const primary = await inspectScope(scopeRoot(ctx, ctx.global), ctx.global, ctx.hookCommand);
|
|
365
|
+
const secondary = await inspectScope(scopeRoot(ctx, !ctx.global), !ctx.global, ctx.hookCommand);
|
|
366
|
+
const primaryLabel = ctx.global ? 'global' : 'project';
|
|
367
|
+
const secondaryWhere = ctx.global ? 'at project level' : 'globally';
|
|
368
|
+
const primaryInstalled = hasArtifacts(primary);
|
|
369
|
+
const secondaryInstalled = hasArtifacts(secondary);
|
|
370
|
+
const mode = primary.styleMode ??
|
|
371
|
+
primary.settingsMode ??
|
|
372
|
+
secondary.styleMode ??
|
|
373
|
+
secondary.settingsMode ??
|
|
374
|
+
undefined;
|
|
375
|
+
let detail;
|
|
376
|
+
if (primaryInstalled) {
|
|
377
|
+
const parts = [];
|
|
378
|
+
const primaryMode = primary.styleMode ?? primary.settingsMode;
|
|
379
|
+
if (primaryMode !== null) {
|
|
380
|
+
parts.push(`output style (${primaryMode})`);
|
|
381
|
+
}
|
|
382
|
+
if (primary.hookPresent) {
|
|
383
|
+
parts.push('hook installed');
|
|
384
|
+
}
|
|
385
|
+
detail = `${parts.join(' + ')} (${primaryLabel})`;
|
|
386
|
+
if (secondaryInstalled) {
|
|
387
|
+
detail += `; also installed ${secondaryWhere}`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (secondaryInstalled) {
|
|
391
|
+
detail = `not installed (${primaryLabel}); installed ${secondaryWhere}`;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
detail = 'not installed';
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
agent: 'claude-code',
|
|
398
|
+
installed: primaryInstalled || secondaryInstalled,
|
|
399
|
+
...(mode !== undefined ? { mode } : {}),
|
|
400
|
+
detail,
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
};
|