greprag 5.85.3 → 5.85.6

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.
@@ -426,10 +426,9 @@ function acquiresTargetLock(dir, target) {
426
426
  return false;
427
427
  }
428
428
  }
429
- /** PowerShell's `try { acquire; if ($LASTEXITCODE -ne 0) { throw ... }; merge }
430
- * finally { release }` is a fail-closed lock sequence too. Inspect only direct
431
- * statements in the try body; a bare semicolon or an acquire nested in a branch
432
- * must not excuse a merge. */
429
+ /** A checked PowerShell acquisition can precede the try or start its body.
430
+ * Inspect direct statements along that execution sequence, keeping nested
431
+ * blocks opaque: a conditional acquire or a caught throw cannot excuse a merge. */
433
432
  function powerShellTryTakesLock(command, mergeStart, cwd, target) {
434
433
  const masked = maskQuoted(command);
435
434
  const tryStart = /(?:^|[;\n])\s*try\s*\{/gi;
@@ -442,7 +441,10 @@ function powerShellTryTakesLock(command, mergeStart, cwd, target) {
442
441
  const finallyMatch = /^\s*finally\s*\{/.exec(suffix);
443
442
  if (!finallyMatch || matchingBrace(masked, close + 1 + finallyMatch[0].lastIndexOf('{')) < 0)
444
443
  continue;
445
- const statements = powerShellStatements(command, masked, open + 1, close);
444
+ const statements = [
445
+ ...powerShellStatements(command, masked, 0, match.index),
446
+ ...powerShellStatements(command, masked, open + 1, close),
447
+ ];
446
448
  const mergeIndex = statements.findIndex(s => mergeStart >= s.start && mergeStart < s.end);
447
449
  if (mergeIndex < 2 || mergeGlobalOpts(statements[mergeIndex].text)?.length !== 0)
448
450
  continue;
@@ -411,7 +411,7 @@ async function runSkillMirrorSync(args) {
411
411
  }
412
412
  const hashes = await (0, skill_git_ingest_1.fetchMirrorHashes)(apiUrl, apiKey, projectId);
413
413
  const ingest = await (0, skill_git_ingest_1.ingestCommittedSkills)({ apiUrl, apiKey, sources, mirrorHashes: hashes, force: args.includes('--force') });
414
- console.log(`mirror: ${ingest.uploaded.length} skill(s) ingested from git${ingest.uploaded.length ? ` (${ingest.uploaded.join(', ')})` : ''}, ${ingest.current} current`);
414
+ console.log(`mirror: ${ingest.uploaded.length} skill(s) ingested from git${ingest.uploaded.length ? ` (${ingest.uploaded.join(', ')})` : ''}, ${ingest.retired.length} retired${ingest.retired.length ? ` (${ingest.retired.join(', ')})` : ''}, ${ingest.current} current`);
415
415
  for (const failure of ingest.failed)
416
416
  console.error(`mirror: ${failure.name} failed — ${failure.error}`);
417
417
  if (requested === 'claude-code')
@@ -583,12 +583,15 @@ function applyNativeSkillMirror(params) {
583
583
  const projectRoot = projectDir ? writerOwnedProjectSkillRoot(params.platform, projectDir, homeDir) : null;
584
584
  if (projectRoot)
585
585
  rootState(projectRoot);
586
+ const keepGlobal = new Set(params.globalRoster || []);
586
587
  for (const [root, entry] of roots) {
587
588
  if (params.pruneGlobalRoot === false && root === globalRoot)
588
589
  continue;
589
590
  for (const [skillName, managed] of Object.entries(entry.state.skills)) {
590
591
  if (entry.present.has(skillName))
591
592
  continue;
593
+ if (root === globalRoot && keepGlobal.has(skillName))
594
+ continue;
592
595
  const target = targetPath(root, skillName);
593
596
  const currentFiles = (0, skill_mirror_files_1.isSkillFolderOwnedElsewhere)(target) ? null : readManagedFiles(target, managed);
594
597
  if (!currentFiles || (0, skill_mirror_files_1.computeSkillFileHash)(currentFiles) !== managed.hash) {
@@ -668,13 +671,25 @@ async function syncNativeSkillMirror(params) {
668
671
  throw new Error('skill mirror snapshot incomplete; existing native adapters preserved');
669
672
  }
670
673
  const skills = fetched;
674
+ // In a project the effective list can hide a replaced global, so the global
675
+ // root is pruned against the plain global list (a skill retired in git).
676
+ let globalRoster;
677
+ if (params.projectId) {
678
+ const globals = await fetch(`${params.apiUrl}/v1/skillmirror?scope=global`, {
679
+ headers: { Authorization: `Bearer ${params.apiKey}` },
680
+ });
681
+ if (!globals.ok)
682
+ throw new Error(`skill mirror list failed: HTTP ${globals.status}`);
683
+ const body = await globals.json();
684
+ globalRoster = (body.skills || []).map(row => row.skillName).filter((n) => typeof n === 'string');
685
+ }
671
686
  return applyNativeSkillMirror({
672
687
  platform: params.platform,
673
688
  skills,
674
689
  homeDir: params.homeDir,
675
690
  projectDir: params.projectDir,
676
691
  prune: true,
677
- pruneGlobalRoot: !params.projectId,
692
+ globalRoster,
678
693
  });
679
694
  }
680
695
  function nativeSkillMirrorSummary(result) {
@@ -4636,12 +4636,15 @@ function applyNativeSkillMirror(params) {
4636
4636
  const projectRoot = projectDir ? writerOwnedProjectSkillRoot(params.platform, projectDir, homeDir) : null;
4637
4637
  if (projectRoot)
4638
4638
  rootState(projectRoot);
4639
+ const keepGlobal = new Set(params.globalRoster || []);
4639
4640
  for (const [root, entry] of roots) {
4640
4641
  if (params.pruneGlobalRoot === false && root === globalRoot)
4641
4642
  continue;
4642
4643
  for (const [skillName, managed] of Object.entries(entry.state.skills)) {
4643
4644
  if (entry.present.has(skillName))
4644
4645
  continue;
4646
+ if (root === globalRoot && keepGlobal.has(skillName))
4647
+ continue;
4645
4648
  const target = targetPath(root, skillName);
4646
4649
  const currentFiles = isSkillFolderOwnedElsewhere(target) ? null : readManagedFiles(target, managed);
4647
4650
  if (!currentFiles || computeSkillFileHash(currentFiles) !== managed.hash) {
@@ -52,6 +52,8 @@ exports.globalSkillSource = globalSkillSource;
52
52
  exports.projectSkillSource = projectSkillSource;
53
53
  exports.listCommittedSkills = listCommittedSkills;
54
54
  exports.readCommittedSkillFiles = readCommittedSkillFiles;
55
+ exports.deletionCommit = deletionCommit;
56
+ exports.deletedSkills = deletedSkills;
55
57
  exports.ingestCommittedSkills = ingestCommittedSkills;
56
58
  exports.fetchMirrorHashes = fetchMirrorHashes;
57
59
  exports.gitSkillSources = gitSkillSources;
@@ -193,6 +195,36 @@ function lastCommit(repo, folder) {
193
195
  const [hash = 'unknown', subject = ''] = out.split('\0');
194
196
  return { hash, subject };
195
197
  }
198
+ /** The commit that deleted a skill folder's SKILL.md from the source, or null
199
+ * when git never held it there (a skill mirrored from elsewhere is not
200
+ * retired by a source that never owned it). */
201
+ function deletionCommit(source, name) {
202
+ const out = (git(source.repo, ['log', '-1', '--diff-filter=D', '--format=%h%x00%s', 'HEAD', '--',
203
+ `${source.prefix}/${name}/SKILL.md`]) || '').trim();
204
+ if (!out)
205
+ return null;
206
+ const [hash = '', subject = ''] = out.split('\0');
207
+ return hash ? { hash, subject } : null;
208
+ }
209
+ /** Mirrored skills of this source's scope whose folder git deleted: absent at
210
+ * HEAD, not bundled, not a link on disk, and deleted by a commit in this repo. */
211
+ function deletedSkills(source, committed, mirrorNames) {
212
+ const present = new Set(committed.map(s => s.name));
213
+ const out = [];
214
+ for (const name of mirrorNames) {
215
+ if (present.has(name) || !SKILL_NAME_RE.test(name))
216
+ continue;
217
+ if (source.scope === 'global' && skill_landing_1.BUNDLED_SKILLS.includes(name))
218
+ continue;
219
+ const folder = path.join(source.repo, ...source.prefix.split('/'), name);
220
+ if (fs.existsSync(folder))
221
+ continue;
222
+ const commit = deletionCommit(source, name);
223
+ if (commit)
224
+ out.push({ name, commit });
225
+ }
226
+ return out;
227
+ }
196
228
  function readState() {
197
229
  try {
198
230
  return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
@@ -215,10 +247,34 @@ function stateKey(source, name) {
215
247
  * `mirrorHashes` maps `<scope>:<name>` to the mirror's contentHash. `force`
216
248
  * re-reads folders whose tree hash is unchanged since the last run. */
217
249
  async function ingestCommittedSkills(params) {
218
- const result = { uploaded: [], current: 0, failed: [] };
250
+ const result = { uploaded: [], retired: [], current: 0, failed: [] };
219
251
  const state = readState();
220
252
  for (const source of params.sources) {
221
253
  const all = listCommittedSkills(source);
254
+ const mirrorNames = [...params.mirrorHashes.keys()]
255
+ .filter(k => k.startsWith(`${source.scope}:`)).map(k => k.slice(source.scope.length + 1));
256
+ for (const gone of deletedSkills(source, all, mirrorNames)) {
257
+ try {
258
+ const res = await fetch(`${params.apiUrl}/v1/skillmirror/${encodeURIComponent(gone.name)}/retire`, {
259
+ method: 'POST',
260
+ headers: { 'Authorization': `Bearer ${params.apiKey}`, 'Content-Type': 'application/json' },
261
+ body: JSON.stringify({
262
+ scope: source.scope,
263
+ ...(source.scope === 'project' ? { projectId: source.projectId } : {}),
264
+ reason: `deleted in git ${gone.commit.hash}: ${gone.commit.subject}`.slice(0, 500),
265
+ author: { via: 'git-ingest', commit: gone.commit.hash },
266
+ }),
267
+ signal: AbortSignal.timeout(15_000),
268
+ });
269
+ if (!res.ok && res.status !== 404)
270
+ throw new Error(`HTTP ${res.status}`);
271
+ delete state[stateKey(source, gone.name)];
272
+ result.retired.push(gone.name);
273
+ }
274
+ catch (err) {
275
+ result.failed.push({ name: gone.name, error: `retire: ${err instanceof Error ? err.message : String(err)}` });
276
+ }
277
+ }
222
278
  const changed = params.force ? all : all.filter(s => state[stateKey(source, s.name)] !== s.tree);
223
279
  result.current += all.length - changed.length;
224
280
  const files = readCommittedSkillFiles(source.repo, changed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.85.3",
3
+ "version": "5.85.6",
4
4
  "description": "Private GrepRAG client for existing Claude Code, Codex, OpenCode, and Grok Build tenants.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {