xtrm-cli 2.1.19 → 2.1.20
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/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/install-project.ts +97 -1
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -139,6 +139,94 @@ export async function getAvailableProjectSkills(): Promise<string[]> {
|
|
|
139
139
|
* Deep merge settings.json hooks without overwriting existing user hooks.
|
|
140
140
|
* Appends new hooks to existing events intelligently.
|
|
141
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* Extract script filename from a hook command.
|
|
144
|
+
*/
|
|
145
|
+
function getScriptFilename(hook: any): string | null {
|
|
146
|
+
const cmd = hook.command || hook.hooks?.[0]?.command || '';
|
|
147
|
+
if (typeof cmd !== 'string') return null;
|
|
148
|
+
// Match script filename including subdirectory (e.g., "gitnexus/gitnexus-hook.cjs")
|
|
149
|
+
const m = cmd.match(/\/hooks\/([A-Za-z0-9_/-]+\.(?:py|cjs|mjs|js))/);
|
|
150
|
+
if (m) return m[1];
|
|
151
|
+
const m2 = cmd.match(/([A-Za-z0-9_-]+\.(?:py|cjs|mjs|js))(?!.*[A-Za-z0-9._-]+\.(?:py|cjs|mjs|js))/);
|
|
152
|
+
return m2?.[1] ?? null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Prune hooks from settings.json that are NOT in the canonical config.
|
|
157
|
+
* This removes stale entries from old versions before merging new ones.
|
|
158
|
+
*
|
|
159
|
+
* @param existing Current settings.json hooks
|
|
160
|
+
* @param canonical Canonical hooks config from hooks.json
|
|
161
|
+
* @returns Pruned settings with stale hooks removed
|
|
162
|
+
*/
|
|
163
|
+
export function pruneStaleHooks(
|
|
164
|
+
existing: Record<string, any>,
|
|
165
|
+
canonical: Record<string, any>,
|
|
166
|
+
): { result: Record<string, any>; removed: string[] } {
|
|
167
|
+
const result = { ...existing };
|
|
168
|
+
const removed: string[] = [];
|
|
169
|
+
|
|
170
|
+
if (!result.hooks || typeof result.hooks !== 'object') {
|
|
171
|
+
return { result, removed };
|
|
172
|
+
}
|
|
173
|
+
if (!canonical.hooks || typeof canonical.hooks !== 'object') {
|
|
174
|
+
return { result, removed };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Collect all canonical script filenames
|
|
178
|
+
const canonicalScripts = new Set<string>();
|
|
179
|
+
for (const [event, hooks] of Object.entries(canonical.hooks)) {
|
|
180
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
181
|
+
for (const wrapper of hookList) {
|
|
182
|
+
const innerHooks = wrapper.hooks || [wrapper];
|
|
183
|
+
for (const hook of innerHooks) {
|
|
184
|
+
const script = getScriptFilename(hook);
|
|
185
|
+
if (script) canonicalScripts.add(script);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Prune existing hooks not in canonical
|
|
191
|
+
for (const [event, hooks] of Object.entries(result.hooks)) {
|
|
192
|
+
if (!Array.isArray(hooks)) continue;
|
|
193
|
+
|
|
194
|
+
const prunedWrappers: any[] = [];
|
|
195
|
+
for (const wrapper of hooks) {
|
|
196
|
+
const innerHooks = wrapper.hooks || [wrapper];
|
|
197
|
+
const keptInner: any[] = [];
|
|
198
|
+
|
|
199
|
+
for (const hook of innerHooks) {
|
|
200
|
+
const script = getScriptFilename(hook);
|
|
201
|
+
// Keep if: no script (not a file-based hook) OR script is canonical
|
|
202
|
+
if (!script || canonicalScripts.has(script)) {
|
|
203
|
+
keptInner.push(hook);
|
|
204
|
+
} else {
|
|
205
|
+
removed.push(`${event}:${script}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (keptInner.length > 0) {
|
|
210
|
+
if (wrapper.hooks) {
|
|
211
|
+
prunedWrappers.push({ ...wrapper, hooks: keptInner });
|
|
212
|
+
} else if (keptInner.length === 1) {
|
|
213
|
+
prunedWrappers.push(keptInner[0]);
|
|
214
|
+
} else {
|
|
215
|
+
prunedWrappers.push({ ...wrapper, hooks: keptInner });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (prunedWrappers.length > 0) {
|
|
221
|
+
result.hooks[event] = prunedWrappers;
|
|
222
|
+
} else {
|
|
223
|
+
delete result.hooks[event];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { result, removed };
|
|
228
|
+
}
|
|
229
|
+
|
|
142
230
|
export function deepMergeHooks(existing: Record<string, any>, incoming: Record<string, any>): Record<string, any> {
|
|
143
231
|
const result = { ...existing };
|
|
144
232
|
|
|
@@ -266,7 +354,15 @@ export async function installProjectSkill(toolName: string, projectRootOverride?
|
|
|
266
354
|
}
|
|
267
355
|
|
|
268
356
|
const incomingSettings = JSON.parse(await fs.readFile(skillSettingsPath, 'utf8'));
|
|
269
|
-
|
|
357
|
+
|
|
358
|
+
// First prune stale hooks not in canonical config
|
|
359
|
+
const { result: prunedSettings, removed } = pruneStaleHooks(existingSettings, incomingSettings);
|
|
360
|
+
if (removed.length > 0) {
|
|
361
|
+
console.log(kleur.yellow(` ↳ Pruned ${removed.length} stale hook(s): ${removed.join(', ')}`));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Then merge canonical hooks
|
|
365
|
+
const mergedSettings = deepMergeHooks(prunedSettings, incomingSettings);
|
|
270
366
|
|
|
271
367
|
await fs.writeFile(targetSettingsPath, JSON.stringify(mergedSettings, null, 2) + '\n');
|
|
272
368
|
console.log(`${kleur.green(' ✓')} settings.json (hooks merged)`);
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { createProjectCommand } from './commands/install-project.js';
|
|
|
13
13
|
import { createStatusCommand } from './commands/status.js';
|
|
14
14
|
import { createResetCommand } from './commands/reset.js';
|
|
15
15
|
import { createHelpCommand } from './commands/help.js';
|
|
16
|
+
import { createCleanCommand } from './commands/clean.js';
|
|
16
17
|
import { printBanner } from './utils/banner.js';
|
|
17
18
|
|
|
18
19
|
const program = new Command();
|
|
@@ -36,6 +37,7 @@ program.addCommand(createInstallCommand());
|
|
|
36
37
|
program.addCommand(createProjectCommand());
|
|
37
38
|
program.addCommand(createStatusCommand());
|
|
38
39
|
program.addCommand(createResetCommand());
|
|
40
|
+
program.addCommand(createCleanCommand());
|
|
39
41
|
program.addCommand(createHelpCommand());
|
|
40
42
|
|
|
41
43
|
// Default action: show help
|