@indigoai-us/hq-cloud 6.1.0 → 6.2.1
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/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +18 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/reindex.d.ts +4 -11
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +336 -30
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.d.ts +3 -3
- package/dist/cli/reindex.test.js +36 -11
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +36 -0
- package/dist/cli/rescue-core.d.ts.map +1 -0
- package/dist/cli/rescue-core.js +1589 -0
- package/dist/cli/rescue-core.js.map +1 -0
- package/dist/cli/rescue-drift-reconcile.test.js +33 -10
- package/dist/cli/rescue-drift-reconcile.test.js.map +1 -1
- package/dist/cli/rescue-journal-reconcile.test.d.ts +2 -0
- package/dist/cli/rescue-journal-reconcile.test.d.ts.map +1 -0
- package/dist/cli/rescue-journal-reconcile.test.js +135 -0
- package/dist/cli/rescue-journal-reconcile.test.js.map +1 -0
- package/dist/cli/rescue-mtime-preserve.test.js +36 -12
- package/dist/cli/rescue-mtime-preserve.test.js.map +1 -1
- package/dist/cli/rescue.d.ts +4 -10
- package/dist/cli/rescue.d.ts.map +1 -1
- package/dist/cli/rescue.js +14 -37
- package/dist/cli/rescue.js.map +1 -1
- package/dist/cli/rescue.reindex.test.js +9 -8
- package/dist/cli/rescue.reindex.test.js.map +1 -1
- package/dist/cli/rescue.test.js +1 -10
- package/dist/cli/rescue.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/conflict-index.d.ts +40 -0
- package/dist/lib/conflict-index.d.ts.map +1 -1
- package/dist/lib/conflict-index.js +121 -0
- package/dist/lib/conflict-index.js.map +1 -1
- package/dist/lib/conflict.test.js +145 -1
- package/dist/lib/conflict.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.ts +18 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/reindex.test.ts +45 -12
- package/src/cli/reindex.ts +345 -36
- package/src/cli/rescue-core.ts +1719 -0
- package/src/cli/rescue-drift-reconcile.test.ts +33 -12
- package/src/cli/rescue-journal-reconcile.test.ts +156 -0
- package/src/cli/rescue-mtime-preserve.test.ts +36 -15
- package/src/cli/rescue.reindex.test.ts +9 -8
- package/src/cli/rescue.test.ts +1 -11
- package/src/cli/rescue.ts +15 -40
- package/src/index.ts +2 -2
- package/src/lib/conflict-index.ts +146 -0
- package/src/lib/conflict.test.ts +171 -0
- package/scripts/reindex.sh +0 -318
- package/scripts/replace-rescue.sh +0 -1522
package/src/lib/conflict.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
appendConflictEntry,
|
|
18
18
|
getConflictIndexPath,
|
|
19
|
+
pruneConflictIndex,
|
|
19
20
|
readConflictIndex,
|
|
20
21
|
removeConflictEntry,
|
|
21
22
|
writeConflictIndex,
|
|
@@ -142,3 +143,173 @@ describe("conflict index", () => {
|
|
|
142
143
|
expect(idx.conflicts).toEqual([]);
|
|
143
144
|
});
|
|
144
145
|
});
|
|
146
|
+
|
|
147
|
+
describe("pruneConflictIndex", () => {
|
|
148
|
+
let tmpHq: string;
|
|
149
|
+
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
tmpHq = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cprune-"));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
fs.rmSync(tmpHq, { recursive: true, force: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/** Write a file at a hq-relative path, creating parent dirs. */
|
|
159
|
+
function put(rel: string, content: string): void {
|
|
160
|
+
const abs = path.join(tmpHq, rel);
|
|
161
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
162
|
+
fs.writeFileSync(abs, content);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Write a symlink at a hq-relative path pointing at `target` (verbatim). */
|
|
166
|
+
function putLink(rel: string, target: string): void {
|
|
167
|
+
const abs = path.join(tmpHq, rel);
|
|
168
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
169
|
+
fs.symlinkSync(target, abs);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build an entry whose original/conflict paths are derived from `id` so each
|
|
174
|
+
* row addresses distinct files on disk (the shared `entry()` helper above
|
|
175
|
+
* reuses one fixed path pair, which would collide across rows here).
|
|
176
|
+
*/
|
|
177
|
+
function rowFor(id: string): ConflictIndexEntry {
|
|
178
|
+
return {
|
|
179
|
+
id,
|
|
180
|
+
originalPath: `dir/${id}.md`,
|
|
181
|
+
conflictPath: `dir/${id}.md.conflict-2026-04-27T22-05-14Z-abc123.md`,
|
|
182
|
+
detectedAt: "2026-04-27T22:05:14Z",
|
|
183
|
+
side: "pull",
|
|
184
|
+
machineId: "abc123",
|
|
185
|
+
localHash: "local",
|
|
186
|
+
remoteHash: "remote",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
it("returns a zeroed result and writes nothing for an empty/absent index", () => {
|
|
191
|
+
const res = pruneConflictIndex(tmpHq);
|
|
192
|
+
expect(res).toEqual({
|
|
193
|
+
prunedOrphans: 0,
|
|
194
|
+
prunedIdentical: 0,
|
|
195
|
+
removedMirrors: 0,
|
|
196
|
+
kept: 0,
|
|
197
|
+
});
|
|
198
|
+
expect(fs.existsSync(getConflictIndexPath(tmpHq))).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("drops a row whose .conflict-* mirror no longer exists (orphan)", () => {
|
|
202
|
+
const row = rowFor("orphan");
|
|
203
|
+
put(row.originalPath, "still here");
|
|
204
|
+
// No mirror file on disk.
|
|
205
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [row] });
|
|
206
|
+
|
|
207
|
+
const res = pruneConflictIndex(tmpHq);
|
|
208
|
+
expect(res.prunedOrphans).toBe(1);
|
|
209
|
+
expect(res.kept).toBe(0);
|
|
210
|
+
expect(readConflictIndex(tmpHq).conflicts).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("drops a byte-identical row and deletes its mirror file", () => {
|
|
214
|
+
const row = rowFor("identical");
|
|
215
|
+
put(row.originalPath, "same bytes");
|
|
216
|
+
put(row.conflictPath, "same bytes");
|
|
217
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [row] });
|
|
218
|
+
|
|
219
|
+
const res = pruneConflictIndex(tmpHq);
|
|
220
|
+
expect(res.prunedIdentical).toBe(1);
|
|
221
|
+
expect(res.removedMirrors).toBe(1);
|
|
222
|
+
expect(readConflictIndex(tmpHq).conflicts).toHaveLength(0);
|
|
223
|
+
expect(fs.existsSync(path.join(tmpHq, row.conflictPath))).toBe(false);
|
|
224
|
+
// The user's original is never touched.
|
|
225
|
+
expect(fs.existsSync(path.join(tmpHq, row.originalPath))).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("keeps a genuinely divergent row and leaves both files in place", () => {
|
|
229
|
+
const row = rowFor("real");
|
|
230
|
+
put(row.originalPath, "local edit");
|
|
231
|
+
put(row.conflictPath, "remote edit");
|
|
232
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [row] });
|
|
233
|
+
|
|
234
|
+
const res = pruneConflictIndex(tmpHq);
|
|
235
|
+
expect(res.kept).toBe(1);
|
|
236
|
+
expect(res.prunedIdentical).toBe(0);
|
|
237
|
+
expect(res.prunedOrphans).toBe(0);
|
|
238
|
+
expect(readConflictIndex(tmpHq).conflicts.map((c) => c.id)).toEqual(["real"]);
|
|
239
|
+
expect(fs.existsSync(path.join(tmpHq, row.conflictPath))).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("keeps a row whose original is missing (local-delete divergence)", () => {
|
|
243
|
+
const row = rowFor("deleted-local");
|
|
244
|
+
// No original; mirror present.
|
|
245
|
+
put(row.conflictPath, "remote bytes");
|
|
246
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [row] });
|
|
247
|
+
|
|
248
|
+
const res = pruneConflictIndex(tmpHq);
|
|
249
|
+
expect(res.kept).toBe(1);
|
|
250
|
+
expect(readConflictIndex(tmpHq).conflicts).toHaveLength(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("treats two symlinks with the same target as identical, differing targets as a conflict", () => {
|
|
254
|
+
const same = rowFor("link-same");
|
|
255
|
+
putLink(same.originalPath, "../target/a");
|
|
256
|
+
putLink(same.conflictPath, "../target/a");
|
|
257
|
+
|
|
258
|
+
const diff = rowFor("link-diff");
|
|
259
|
+
putLink(diff.originalPath, "../target/a");
|
|
260
|
+
putLink(diff.conflictPath, "../target/b");
|
|
261
|
+
|
|
262
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [same, diff] });
|
|
263
|
+
|
|
264
|
+
const res = pruneConflictIndex(tmpHq);
|
|
265
|
+
expect(res.prunedIdentical).toBe(1);
|
|
266
|
+
expect(res.kept).toBe(1);
|
|
267
|
+
expect(readConflictIndex(tmpHq).conflicts.map((c) => c.id)).toEqual([
|
|
268
|
+
"link-diff",
|
|
269
|
+
]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("partitions a mixed batch and preserves only the real conflicts", () => {
|
|
273
|
+
const orphan = rowFor("orphan");
|
|
274
|
+
put(orphan.originalPath, "x"); // mirror missing
|
|
275
|
+
|
|
276
|
+
const identical = rowFor("identical");
|
|
277
|
+
put(identical.originalPath, "dup");
|
|
278
|
+
put(identical.conflictPath, "dup");
|
|
279
|
+
|
|
280
|
+
const real = rowFor("real");
|
|
281
|
+
put(real.originalPath, "L");
|
|
282
|
+
put(real.conflictPath, "R");
|
|
283
|
+
|
|
284
|
+
writeConflictIndex(tmpHq, {
|
|
285
|
+
version: 1,
|
|
286
|
+
conflicts: [orphan, identical, real],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const res = pruneConflictIndex(tmpHq);
|
|
290
|
+
expect(res).toEqual({
|
|
291
|
+
prunedOrphans: 1,
|
|
292
|
+
prunedIdentical: 1,
|
|
293
|
+
removedMirrors: 1,
|
|
294
|
+
kept: 1,
|
|
295
|
+
});
|
|
296
|
+
expect(readConflictIndex(tmpHq).conflicts.map((c) => c.id)).toEqual(["real"]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("does not rewrite the index file when nothing is pruned", () => {
|
|
300
|
+
const real = rowFor("real");
|
|
301
|
+
put(real.originalPath, "L");
|
|
302
|
+
put(real.conflictPath, "R");
|
|
303
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [real] });
|
|
304
|
+
|
|
305
|
+
const indexPath = getConflictIndexPath(tmpHq);
|
|
306
|
+
const before = fs.statSync(indexPath).mtimeMs;
|
|
307
|
+
// Advance the clock past filesystem mtime granularity, then prune.
|
|
308
|
+
const spin = Date.now() + 20;
|
|
309
|
+
while (Date.now() < spin) {
|
|
310
|
+
/* busy-wait ~20ms so a rewrite would move mtime */
|
|
311
|
+
}
|
|
312
|
+
pruneConflictIndex(tmpHq);
|
|
313
|
+
expect(fs.statSync(indexPath).mtimeMs).toBe(before);
|
|
314
|
+
});
|
|
315
|
+
});
|
package/scripts/reindex.sh
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# reindex.sh — surfaces namespace skills as Claude Code skills under
|
|
3
|
-
# .claude/skills/<ns>:<skill>/ with every file in the source skill folder
|
|
4
|
-
# mirrored as a symlink. Also mirrors personal/{knowledge,policies,workers,
|
|
5
|
-
# settings}/* into core/<type>/<name>.
|
|
6
|
-
#
|
|
7
|
-
# This script ships inside the @indigoai-us/hq-cloud package and is invoked via
|
|
8
|
-
# `hq reindex` (which passes the HQ root as $1). It is idempotent and cheap
|
|
9
|
-
# to re-run, so it doesn't gate on whether personal/ was actually touched. Real
|
|
10
|
-
# files/dirs already at the link path are left untouched.
|
|
11
|
-
#
|
|
12
|
-
# REPO_ROOT resolution (in priority order):
|
|
13
|
-
# 1. $1 — passed by `hq reindex` (the HQ root)
|
|
14
|
-
# 2. $HQ_REPO_ROOT — env override
|
|
15
|
-
# 3. $PWD — fallback when invoked directly
|
|
16
|
-
#
|
|
17
|
-
# Sources surfaced as skills under .claude/skills/<namespace>:<skill>/
|
|
18
|
-
# (each contains a symlink per source file, including SKILL.md):
|
|
19
|
-
# companies/<slug>/skills/<skill>/* → .claude/skills/<slug>:<skill>/*
|
|
20
|
-
# core/skills/<skill>/* → .claude/skills/core:<skill>/*
|
|
21
|
-
# personal/skills/<skill>/* → .claude/skills/personal:<skill>/*
|
|
22
|
-
# core/packages/<pack>/skills/<skill>/* → .claude/skills/<pack>:<skill>/*
|
|
23
|
-
#
|
|
24
|
-
# Namespace folders are created lazily. Skill folders starting with '.' or
|
|
25
|
-
# '_' (e.g. _shared, _template) are skipped. Dotfiles inside a skill folder
|
|
26
|
-
# (e.g. .DS_Store, .git) are not mirrored.
|
|
27
|
-
#
|
|
28
|
-
# Cleanup performed each run:
|
|
29
|
-
# 1. Legacy .claude/commands/<ns>/<skill>.md symlinks created by a prior
|
|
30
|
-
# version of this script are removed. Non-symlink files left alone.
|
|
31
|
-
# Empty .claude/commands/<ns>/ dirs are rmdir'd.
|
|
32
|
-
# 2. Stale entries inside a wrapper (symlinks whose source file no longer
|
|
33
|
-
# exists) are pruned.
|
|
34
|
-
# 3. Orphan .claude/skills/<ns>:<skill>/ wrappers (where <ns> is one of
|
|
35
|
-
# the namespaces we manage but the source skill is gone) are deleted.
|
|
36
|
-
#
|
|
37
|
-
# Collision: if the link path already exists as a non-symlink, log + skip.
|
|
38
|
-
|
|
39
|
-
set -uo pipefail
|
|
40
|
-
|
|
41
|
-
REPO_ROOT="${1:-${HQ_REPO_ROOT:-$PWD}}"
|
|
42
|
-
if [ ! -d "$REPO_ROOT" ]; then
|
|
43
|
-
echo "reindex: REPO_ROOT '$REPO_ROOT' is not a directory" >&2
|
|
44
|
-
exit 1
|
|
45
|
-
fi
|
|
46
|
-
REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
|
|
47
|
-
|
|
48
|
-
mkdir -p "$REPO_ROOT/.claude/skills"
|
|
49
|
-
|
|
50
|
-
# Build (namespace, src_rel) pairs.
|
|
51
|
-
namespaces=()
|
|
52
|
-
src_rels=()
|
|
53
|
-
|
|
54
|
-
add_ns() {
|
|
55
|
-
local ns="$1" src_rel="$2"
|
|
56
|
-
[ -d "$REPO_ROOT/$src_rel" ] || return 0
|
|
57
|
-
namespaces+=("$ns")
|
|
58
|
-
src_rels+=("$src_rel")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for company_dir in "$REPO_ROOT"/companies/*/; do
|
|
62
|
-
[ -d "$company_dir" ] || continue
|
|
63
|
-
slug="$(basename "${company_dir%/}")"
|
|
64
|
-
add_ns "$slug" "companies/$slug/skills"
|
|
65
|
-
done
|
|
66
|
-
|
|
67
|
-
add_ns "core" "core/skills"
|
|
68
|
-
add_ns "personal" "personal/skills"
|
|
69
|
-
|
|
70
|
-
for pack_dir in "$REPO_ROOT"/core/packages/*/; do
|
|
71
|
-
[ -d "$pack_dir" ] || continue
|
|
72
|
-
pack="$(basename "${pack_dir%/}")"
|
|
73
|
-
add_ns "$pack" "core/packages/$pack/skills"
|
|
74
|
-
done
|
|
75
|
-
|
|
76
|
-
# --- Cleanup pass A: drop legacy .claude/commands/<ns>/<skill>.md symlinks ---
|
|
77
|
-
# Scan ALL .claude/commands/*/ namespace dirs (not just ones whose source
|
|
78
|
-
# root currently exists). Remove any *.md symlink whose target follows the
|
|
79
|
-
# legacy bridge pattern. This handles namespaces whose entire source root
|
|
80
|
-
# was deleted between runs — e.g. an archived company or removed pack —
|
|
81
|
-
# which otherwise leave broken slash-command symlinks behind.
|
|
82
|
-
# Manual or unrelated *.md files (non-symlinks, or symlinks pointing
|
|
83
|
-
# elsewhere) are preserved.
|
|
84
|
-
if [ -d "$REPO_ROOT/.claude/commands" ]; then
|
|
85
|
-
for cmd_ns_dir in "$REPO_ROOT/.claude/commands"/*/; do
|
|
86
|
-
[ -d "$cmd_ns_dir" ] || continue
|
|
87
|
-
cmd_ns_dir="${cmd_ns_dir%/}"
|
|
88
|
-
# Don't touch folder-level symlinks (could be legacy structure from an
|
|
89
|
-
# even older script version — let a human resolve).
|
|
90
|
-
[ -L "$cmd_ns_dir" ] && continue
|
|
91
|
-
|
|
92
|
-
for f in "$cmd_ns_dir"/*.md; do
|
|
93
|
-
[ -L "$f" ] || continue
|
|
94
|
-
target="$(readlink "$f")"
|
|
95
|
-
case "$target" in
|
|
96
|
-
../../../companies/*/skills/*/SKILL.md|\
|
|
97
|
-
../../../core/skills/*/SKILL.md|\
|
|
98
|
-
../../../personal/skills/*/SKILL.md|\
|
|
99
|
-
../../../core/packages/*/skills/*/SKILL.md)
|
|
100
|
-
rm "$f"
|
|
101
|
-
;;
|
|
102
|
-
esac
|
|
103
|
-
done
|
|
104
|
-
|
|
105
|
-
# rmdir if empty; ignore "directory not empty"
|
|
106
|
-
rmdir "$cmd_ns_dir" 2>/dev/null || true
|
|
107
|
-
done
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
# --- Skill wrapper creation ---
|
|
111
|
-
# Track which <ns>:<skill> wrappers we maintained this run, for orphan
|
|
112
|
-
# cleanup at the end.
|
|
113
|
-
expected_wrappers=()
|
|
114
|
-
|
|
115
|
-
i=0
|
|
116
|
-
seen=()
|
|
117
|
-
while [ "$i" -lt "${#namespaces[@]}" ]; do
|
|
118
|
-
ns="${namespaces[$i]}"
|
|
119
|
-
src_rel="${src_rels[$i]}"
|
|
120
|
-
i=$((i + 1))
|
|
121
|
-
|
|
122
|
-
# First writer for a namespace wins.
|
|
123
|
-
already=0
|
|
124
|
-
for s in ${seen[@]+"${seen[@]}"}; do
|
|
125
|
-
if [ "$s" = "$ns" ]; then
|
|
126
|
-
already=1
|
|
127
|
-
break
|
|
128
|
-
fi
|
|
129
|
-
done
|
|
130
|
-
if [ "$already" -eq 1 ]; then
|
|
131
|
-
echo "reindex: namespace '$ns' already claimed by an earlier source; skipping $src_rel" >&2
|
|
132
|
-
continue
|
|
133
|
-
fi
|
|
134
|
-
seen+=("$ns")
|
|
135
|
-
|
|
136
|
-
for skill_path in "$REPO_ROOT/$src_rel"/*; do
|
|
137
|
-
[ -d "$skill_path" ] || continue
|
|
138
|
-
skill_name="$(basename "$skill_path")"
|
|
139
|
-
case "$skill_name" in
|
|
140
|
-
.*|_*) continue ;;
|
|
141
|
-
esac
|
|
142
|
-
[ -f "$skill_path/SKILL.md" ] || continue
|
|
143
|
-
|
|
144
|
-
wrapper_name="$ns:$skill_name"
|
|
145
|
-
wrapper="$REPO_ROOT/.claude/skills/$wrapper_name"
|
|
146
|
-
expected_wrappers+=("$wrapper_name")
|
|
147
|
-
|
|
148
|
-
# If something non-directory occupies the slot, bail.
|
|
149
|
-
if [ -e "$wrapper" ] && [ ! -L "$wrapper" ] && [ ! -d "$wrapper" ]; then
|
|
150
|
-
echo "reindex: $wrapper exists and is not a directory; skipping" >&2
|
|
151
|
-
continue
|
|
152
|
-
fi
|
|
153
|
-
# If it's a symlink (e.g. legacy directory-symlink form from earlier
|
|
154
|
-
# experimentation), replace with a real directory of per-file symlinks.
|
|
155
|
-
if [ -L "$wrapper" ]; then
|
|
156
|
-
rm "$wrapper"
|
|
157
|
-
fi
|
|
158
|
-
mkdir -p "$wrapper"
|
|
159
|
-
|
|
160
|
-
# Symlink every (non-hidden) entry in the source skill folder into the
|
|
161
|
-
# wrapper. Wrapper lives at .claude/skills/<ns>:<skill>/, three levels
|
|
162
|
-
# below REPO_ROOT.
|
|
163
|
-
for entry_path in "$skill_path"/*; do
|
|
164
|
-
[ -e "$entry_path" ] || continue
|
|
165
|
-
entry="$(basename "$entry_path")"
|
|
166
|
-
link_path="$wrapper/$entry"
|
|
167
|
-
relative_target="../../../$src_rel/$skill_name/$entry"
|
|
168
|
-
|
|
169
|
-
if [ -L "$link_path" ]; then
|
|
170
|
-
current="$(readlink "$link_path")"
|
|
171
|
-
if [ "$current" = "$relative_target" ]; then
|
|
172
|
-
continue
|
|
173
|
-
fi
|
|
174
|
-
echo "reindex: .claude/skills/$wrapper_name/$entry already points to '$current' (expected '$relative_target'); leaving alone" >&2
|
|
175
|
-
continue
|
|
176
|
-
elif [ -e "$link_path" ]; then
|
|
177
|
-
echo "reindex: $link_path already exists and is not a symlink; skipping" >&2
|
|
178
|
-
continue
|
|
179
|
-
fi
|
|
180
|
-
|
|
181
|
-
ln -s "$relative_target" "$link_path"
|
|
182
|
-
done
|
|
183
|
-
|
|
184
|
-
# Prune symlinks in the wrapper whose source entry no longer exists.
|
|
185
|
-
# -e on a symlink dereferences; a stale link fails -e.
|
|
186
|
-
for link_path in "$wrapper"/*; do
|
|
187
|
-
[ -L "$link_path" ] || continue
|
|
188
|
-
[ -e "$link_path" ] && continue
|
|
189
|
-
rm "$link_path"
|
|
190
|
-
done
|
|
191
|
-
done
|
|
192
|
-
done
|
|
193
|
-
|
|
194
|
-
# --- Cleanup pass B: drop orphan <ns>:<skill> wrappers ---
|
|
195
|
-
# A wrapper is an orphan if its <ns> belongs to a managed namespace but the
|
|
196
|
-
# source skill folder no longer exists (so we didn't add it to
|
|
197
|
-
# expected_wrappers this run). Unmanaged-namespace entries are left alone —
|
|
198
|
-
# users can hand-author <ns>:<name> wrappers and we won't clobber them.
|
|
199
|
-
for entry_path in "$REPO_ROOT/.claude/skills"/*; do
|
|
200
|
-
# Accept entries that exist OR are broken symlinks. A bare -e check
|
|
201
|
-
# would skip dangling symlink wrappers, which are exactly the orphans
|
|
202
|
-
# this pass needs to remove.
|
|
203
|
-
[ -e "$entry_path" ] || [ -L "$entry_path" ] || continue
|
|
204
|
-
entry="$(basename "$entry_path")"
|
|
205
|
-
case "$entry" in
|
|
206
|
-
*:*) ;;
|
|
207
|
-
*) continue ;; # not a namespaced wrapper
|
|
208
|
-
esac
|
|
209
|
-
|
|
210
|
-
# Skip wrappers we maintained this run.
|
|
211
|
-
is_expected=0
|
|
212
|
-
for w in ${expected_wrappers[@]+"${expected_wrappers[@]}"}; do
|
|
213
|
-
if [ "$w" = "$entry" ]; then
|
|
214
|
-
is_expected=1
|
|
215
|
-
break
|
|
216
|
-
fi
|
|
217
|
-
done
|
|
218
|
-
if [ "$is_expected" -eq 1 ]; then
|
|
219
|
-
continue
|
|
220
|
-
fi
|
|
221
|
-
|
|
222
|
-
# Detect whether this wrapper was produced by this script. Required:
|
|
223
|
-
# the wrapper's namespace prefix MUST match the namespace encoded in its
|
|
224
|
-
# symlink target. That distinguishes script-produced wrappers
|
|
225
|
-
# (e.g. personal:foo -> personal/skills/foo) from hand-authored composite
|
|
226
|
-
# wrappers (e.g. vendor:tool -> core/skills/some-helper, where 'vendor'
|
|
227
|
-
# is not a namespace we manage). Without the cross-check we'd clobber
|
|
228
|
-
# the user's hand-authored entries.
|
|
229
|
-
#
|
|
230
|
-
# Two wrapper shapes need to be handled:
|
|
231
|
-
# (a) entry_path itself is a symlink — directory-style wrapper from an
|
|
232
|
-
# older script version. Target uses 2-level relative paths.
|
|
233
|
-
# (b) entry_path is a real directory containing per-file symlinks —
|
|
234
|
-
# current shape. Each inner symlink uses 3-level relative paths.
|
|
235
|
-
ns="${entry%%:*}"
|
|
236
|
-
is_managed=0
|
|
237
|
-
match_target() {
|
|
238
|
-
# Args: $1=target, $2=relative prefix (../.. or ../../..)
|
|
239
|
-
# Returns 0 if target matches the expected pattern for $ns.
|
|
240
|
-
local t="$1" p="$2"
|
|
241
|
-
case "$t" in
|
|
242
|
-
"$p"/personal/skills/*) [ "$ns" = "personal" ] && return 0 ;;
|
|
243
|
-
"$p"/core/skills/*) [ "$ns" = "core" ] && return 0 ;;
|
|
244
|
-
"$p"/companies/"$ns"/skills/*) return 0 ;;
|
|
245
|
-
"$p"/core/packages/"$ns"/skills/*) return 0 ;;
|
|
246
|
-
esac
|
|
247
|
-
return 1
|
|
248
|
-
}
|
|
249
|
-
if [ -L "$entry_path" ]; then
|
|
250
|
-
t="$(readlink "$entry_path")"
|
|
251
|
-
if match_target "$t" "../.."; then
|
|
252
|
-
is_managed=1
|
|
253
|
-
fi
|
|
254
|
-
else
|
|
255
|
-
for f in "$entry_path"/*; do
|
|
256
|
-
[ -L "$f" ] || continue
|
|
257
|
-
t="$(readlink "$f")"
|
|
258
|
-
if match_target "$t" "../../.."; then
|
|
259
|
-
is_managed=1
|
|
260
|
-
break
|
|
261
|
-
fi
|
|
262
|
-
done
|
|
263
|
-
fi
|
|
264
|
-
if [ "$is_managed" -eq 0 ]; then
|
|
265
|
-
continue
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
|
-
# It's a managed-namespace wrapper with no corresponding live source → drop.
|
|
269
|
-
if [ -L "$entry_path" ]; then
|
|
270
|
-
rm "$entry_path"
|
|
271
|
-
elif [ -d "$entry_path" ]; then
|
|
272
|
-
rm -rf "$entry_path"
|
|
273
|
-
fi
|
|
274
|
-
done
|
|
275
|
-
|
|
276
|
-
# --- Personal type mirroring (unchanged from prior version) ---
|
|
277
|
-
# Mirror personal/<type>/<entry> into core/<type>/<entry> as symlinks.
|
|
278
|
-
# .gitkeep and dotfiles are ignored.
|
|
279
|
-
for type in knowledge policies workers settings; do
|
|
280
|
-
personal_dir="$REPO_ROOT/personal/$type"
|
|
281
|
-
core_dir="$REPO_ROOT/core/$type"
|
|
282
|
-
|
|
283
|
-
[ -d "$personal_dir" ] || continue
|
|
284
|
-
mkdir -p "$core_dir"
|
|
285
|
-
|
|
286
|
-
for entry_path in "$personal_dir"/*; do
|
|
287
|
-
[ -e "$entry_path" ] || continue
|
|
288
|
-
entry="$(basename "$entry_path")"
|
|
289
|
-
case "$entry" in
|
|
290
|
-
.*) continue ;;
|
|
291
|
-
esac
|
|
292
|
-
|
|
293
|
-
link_path="$core_dir/$entry"
|
|
294
|
-
relative_target="../../personal/$type/$entry"
|
|
295
|
-
|
|
296
|
-
if [ -L "$link_path" ]; then
|
|
297
|
-
current="$(readlink "$link_path")"
|
|
298
|
-
if [ "$current" = "$relative_target" ]; then
|
|
299
|
-
continue
|
|
300
|
-
fi
|
|
301
|
-
echo "reindex: core/$type/$entry already points to '$current' (expected '$relative_target'); leaving alone" >&2
|
|
302
|
-
continue
|
|
303
|
-
elif [ -e "$link_path" ]; then
|
|
304
|
-
echo "reindex: core/$type/$entry already exists and is not a symlink; skipping" >&2
|
|
305
|
-
continue
|
|
306
|
-
fi
|
|
307
|
-
|
|
308
|
-
ln -s "$relative_target" "$link_path"
|
|
309
|
-
done
|
|
310
|
-
done
|
|
311
|
-
|
|
312
|
-
# --- Workers registry regeneration ---
|
|
313
|
-
# Source of truth: each worker.yaml. Registry is a derived index — regenerated
|
|
314
|
-
# here so it stays in sync with worker.yaml edits. Idempotent — only writes
|
|
315
|
-
# when generated content differs.
|
|
316
|
-
if [ -x "$REPO_ROOT/core/scripts/generate-workers-registry.sh" ]; then
|
|
317
|
-
"$REPO_ROOT/core/scripts/generate-workers-registry.sh" >&2 2>&1 || true
|
|
318
|
-
fi
|