skillspub 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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/catalog.js +352 -0
- package/dist/cli.js +1562 -0
- package/dist/core.js +31 -0
- package/dist/explain.js +307 -0
- package/dist/harnesses/claude.js +95 -0
- package/dist/harnesses/grok.js +746 -0
- package/dist/harnesses/pi.js +1380 -0
- package/dist/harnesses/registry.js +30 -0
- package/dist/harnesses/target.js +5 -0
- package/dist/harnesses/types.js +1 -0
- package/dist/inventory.js +1437 -0
- package/dist/npx-skills.js +273 -0
- package/dist/reconcile.js +1014 -0
- package/dist/shared.js +1411 -0
- package/dist/source-verification.js +297 -0
- package/dist/targets/shared.js +13 -0
- package/dist/tui.js +2373 -0
- package/dist/view.js +321 -0
- package/package.json +51 -0
package/dist/view.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inspectHarnesses } from "./harnesses/registry.js";
|
|
4
|
+
import { sharedOutdatedFromInventory, } from "./shared.js";
|
|
5
|
+
import { readStateFile, targetSlotId, pendingTargetDefinitions, scanGlobalInventory, scanProjectInventory, } from "./inventory.js";
|
|
6
|
+
export function harnessStatusBadge(harness) {
|
|
7
|
+
if (harness.support !== 'managed')
|
|
8
|
+
return { text: `[${harness.support}]`, tone: 'muted' };
|
|
9
|
+
if (harness.isolation.status === 'managed' || harness.isolation.status === 'not-required')
|
|
10
|
+
return { text: '[managed]', tone: 'success' };
|
|
11
|
+
if (harness.isolation.status === 'unmanaged')
|
|
12
|
+
return { text: '[manageable]', tone: 'muted' };
|
|
13
|
+
return harness.isolation.status === 'drift'
|
|
14
|
+
? { text: '[drift]', tone: 'danger' }
|
|
15
|
+
: { text: '[unknown]', tone: 'warning' };
|
|
16
|
+
}
|
|
17
|
+
function toInfo(relationship, target) {
|
|
18
|
+
return {
|
|
19
|
+
presence: relationship.realPath ? relationship.activation : 'deadlink',
|
|
20
|
+
path: relationship.path,
|
|
21
|
+
realPath: relationship.realPath,
|
|
22
|
+
form: relationship.form,
|
|
23
|
+
linked: relationship.form === 'link',
|
|
24
|
+
mirrored: relationship.form === 'mirror',
|
|
25
|
+
diverged: relationship.diverged === true,
|
|
26
|
+
underOff: relationship.activation === 'off',
|
|
27
|
+
target: relationship.target,
|
|
28
|
+
scope: target?.scope,
|
|
29
|
+
readOnly: target ? !target.writable : undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** One Skill × Target cell for a project scan: inherited ON wins unless the project is ON. */
|
|
33
|
+
function effectiveProjectRelationship(relationships) {
|
|
34
|
+
const project = relationships.find((relationship) => relationship.scope === 'project');
|
|
35
|
+
const inheritedOn = relationships.find((relationship) => relationship.scope !== 'project' && relationship.info.presence === 'on');
|
|
36
|
+
if (project?.info.presence === 'on')
|
|
37
|
+
return project;
|
|
38
|
+
if (inheritedOn)
|
|
39
|
+
return inheritedOn;
|
|
40
|
+
return project ?? relationships[0];
|
|
41
|
+
}
|
|
42
|
+
export function viewTargets(report) {
|
|
43
|
+
return report.targets.map((target) => ({
|
|
44
|
+
name: target.key,
|
|
45
|
+
dir: target.discoveryRoot,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
function descriptionFrom(content) {
|
|
49
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
50
|
+
if (!frontmatter)
|
|
51
|
+
return undefined;
|
|
52
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
53
|
+
const index = lines.findIndex((line) => /^description\s*:/.test(line));
|
|
54
|
+
if (index === -1)
|
|
55
|
+
return undefined;
|
|
56
|
+
const value = lines[index].replace(/^description\s*:\s*/, '').trim();
|
|
57
|
+
if (value === '|' || value === '>') {
|
|
58
|
+
const parts = [];
|
|
59
|
+
for (const line of lines.slice(index + 1)) {
|
|
60
|
+
if (!/^\s+/.test(line))
|
|
61
|
+
break;
|
|
62
|
+
parts.push(line.trim());
|
|
63
|
+
}
|
|
64
|
+
return parts.join(value === '>' ? ' ' : '\n') || undefined;
|
|
65
|
+
}
|
|
66
|
+
return value.replace(/^(['"])(.*)\1$/, '$2') || undefined;
|
|
67
|
+
}
|
|
68
|
+
function readDescription(realPath) {
|
|
69
|
+
if (!realPath)
|
|
70
|
+
return undefined;
|
|
71
|
+
try {
|
|
72
|
+
return descriptionFrom(fs.readFileSync(path.join(realPath, 'SKILL.md'), 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Project one scan into matrix rows. Pure: no disk or state reads beyond the report. */
|
|
79
|
+
export function projectRows(report) {
|
|
80
|
+
const keys = new Map(report.targets.map((target) => [target.id, target.key]));
|
|
81
|
+
const targetById = new Map(report.targets.map((target) => [target.id, target]));
|
|
82
|
+
const provenanceBySlot = new Map(report.slots.map((slot) => [slot.id, slot.provenance]));
|
|
83
|
+
const grouped = new Map();
|
|
84
|
+
for (const relationship of report.relationships) {
|
|
85
|
+
const id = relationship.resourceId ?? relationship.realPath ?? `broken:${relationship.path}`;
|
|
86
|
+
let instance = grouped.get(id);
|
|
87
|
+
if (!instance) {
|
|
88
|
+
instance = {
|
|
89
|
+
id,
|
|
90
|
+
name: relationship.name,
|
|
91
|
+
displayName: relationship.name,
|
|
92
|
+
realPath: relationship.resourceId ?? relationship.realPath,
|
|
93
|
+
description: readDescription(relationship.realPath),
|
|
94
|
+
provenance: {},
|
|
95
|
+
sourceLabel: 'Source unknown',
|
|
96
|
+
relationships: [],
|
|
97
|
+
targets: {},
|
|
98
|
+
};
|
|
99
|
+
grouped.set(id, instance);
|
|
100
|
+
}
|
|
101
|
+
const targetName = keys.get(relationship.targetId) ?? relationship.targetKey;
|
|
102
|
+
const target = targetById.get(relationship.targetId);
|
|
103
|
+
// Discovery entries scan before parking entries, so ??= prefers ON.
|
|
104
|
+
instance.targets[targetName] ??= toInfo(relationship, target);
|
|
105
|
+
instance.relationships.push({
|
|
106
|
+
target: targetName,
|
|
107
|
+
name: relationship.name,
|
|
108
|
+
info: toInfo(relationship, target),
|
|
109
|
+
targetId: relationship.targetId,
|
|
110
|
+
slot: relationship.slot,
|
|
111
|
+
scope: target?.scope,
|
|
112
|
+
readOnly: target ? !target.writable : undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (report.scope === 'project') {
|
|
116
|
+
for (const instance of grouped.values()) {
|
|
117
|
+
instance.observedRelationships = instance.relationships;
|
|
118
|
+
const byTarget = new Map();
|
|
119
|
+
for (const relationship of instance.relationships) {
|
|
120
|
+
const list = byTarget.get(relationship.target) ?? [];
|
|
121
|
+
list.push(relationship);
|
|
122
|
+
byTarget.set(relationship.target, list);
|
|
123
|
+
}
|
|
124
|
+
instance.relationships = [...byTarget.values()].map(effectiveProjectRelationship);
|
|
125
|
+
instance.targets = Object.fromEntries(instance.relationships.map((relationship) => [relationship.target, relationship.info]));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const instances = [...grouped.values()].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
129
|
+
const counts = new Map();
|
|
130
|
+
for (const instance of instances)
|
|
131
|
+
counts.set(instance.name, (counts.get(instance.name) ?? 0) + 1);
|
|
132
|
+
for (const instance of instances) {
|
|
133
|
+
const provenance = instance.relationships
|
|
134
|
+
.map((relationship) => provenanceBySlot.get(targetSlotId(relationship.targetId, relationship.slot)))
|
|
135
|
+
.find((candidate) => candidate && Object.values(candidate).some(Boolean));
|
|
136
|
+
instance.provenance = provenance ?? {};
|
|
137
|
+
instance.sourceLabel = instance.provenance.sourceUrl
|
|
138
|
+
?? instance.provenance.source
|
|
139
|
+
?? 'Source unknown';
|
|
140
|
+
if ((counts.get(instance.name) ?? 0) > 1) {
|
|
141
|
+
const suffix = instance.provenance.source
|
|
142
|
+
?? instance.provenance.sourceUrl
|
|
143
|
+
?? instance.realPath
|
|
144
|
+
?? instance.relationships[0].info.path;
|
|
145
|
+
instance.displayName = `${instance.name} (${suffix})`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return instances;
|
|
149
|
+
}
|
|
150
|
+
function sourceSortValue(row) {
|
|
151
|
+
return row.provenance.sourceUrl
|
|
152
|
+
?? row.provenance.source
|
|
153
|
+
?? row.provenance.skillPath
|
|
154
|
+
?? row.realPath
|
|
155
|
+
?? row.relationships.map(({ info }) => info.path).sort()[0]
|
|
156
|
+
?? row.displayName;
|
|
157
|
+
}
|
|
158
|
+
/** Match only the inventory metadata already loaded for a skill instance. */
|
|
159
|
+
export function matchesSearch(row, query) {
|
|
160
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
161
|
+
if (!needle)
|
|
162
|
+
return true;
|
|
163
|
+
return [
|
|
164
|
+
row.name,
|
|
165
|
+
row.displayName,
|
|
166
|
+
row.description,
|
|
167
|
+
row.provenance.source,
|
|
168
|
+
row.provenance.sourceUrl,
|
|
169
|
+
row.provenance.skillPath,
|
|
170
|
+
].some((value) => value?.toLocaleLowerCase().includes(needle));
|
|
171
|
+
}
|
|
172
|
+
export function searchRows(rows, query) {
|
|
173
|
+
return rows.filter((row) => matchesSearch(row, query));
|
|
174
|
+
}
|
|
175
|
+
/** Compare rows in a projection; callers provide its active status when needed. */
|
|
176
|
+
export function compareRows(a, b, sort, statusA = '', statusB = '') {
|
|
177
|
+
const primary = sort === 'name'
|
|
178
|
+
? a.displayName.localeCompare(b.displayName)
|
|
179
|
+
: sort === 'source'
|
|
180
|
+
? sourceSortValue(a).localeCompare(sourceSortValue(b))
|
|
181
|
+
: statusA.localeCompare(statusB);
|
|
182
|
+
return primary || a.displayName.localeCompare(b.displayName) || a.id.localeCompare(b.id);
|
|
183
|
+
}
|
|
184
|
+
export function sortRows(rows, sort, statusFor = () => '') {
|
|
185
|
+
return [...rows].sort((a, b) => compareRows(a, b, sort, statusFor(a), statusFor(b)));
|
|
186
|
+
}
|
|
187
|
+
export function filterRows(rows, filter, tags) {
|
|
188
|
+
return rows.filter((r) => {
|
|
189
|
+
if (filter.target !== undefined && r.targets[filter.target] === undefined)
|
|
190
|
+
return false;
|
|
191
|
+
if (filter.tag !== undefined && !(tags[r.id] ?? []).includes(filter.tag))
|
|
192
|
+
return false;
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
export function untagged(rows, tags) {
|
|
197
|
+
return rows
|
|
198
|
+
.filter((row) => (tags[row.id] ?? []).length === 0)
|
|
199
|
+
.map((row) => row.displayName);
|
|
200
|
+
}
|
|
201
|
+
function stringListRecord(value) {
|
|
202
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
203
|
+
return {};
|
|
204
|
+
return Object.fromEntries(Object.entries(value)
|
|
205
|
+
.filter(([, members]) => Array.isArray(members) && members.every((member) => typeof member === 'string')));
|
|
206
|
+
}
|
|
207
|
+
function presetRecord(value) {
|
|
208
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
209
|
+
return {};
|
|
210
|
+
return Object.fromEntries(Object.entries(value)
|
|
211
|
+
.filter(([, preset]) => Boolean(preset) && typeof preset === 'object' && !Array.isArray(preset) &&
|
|
212
|
+
Array.isArray(preset.selectors) &&
|
|
213
|
+
preset.selectors.every((s) => typeof s === 'string')));
|
|
214
|
+
}
|
|
215
|
+
/** Tags, bundles, claims and preset definitions are catalog/policy metadata;
|
|
216
|
+
* unreadable or invalid state projects as empty. */
|
|
217
|
+
export function readViewState(home) {
|
|
218
|
+
try {
|
|
219
|
+
const state = readStateFile(path.join(home.configDir, 'state.json'));
|
|
220
|
+
return {
|
|
221
|
+
bundles: stringListRecord(state.bundles),
|
|
222
|
+
tags: stringListRecord(state.tags),
|
|
223
|
+
claims: stringListRecord(state.claims),
|
|
224
|
+
presets: presetRecord(state.presets),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return { bundles: {}, tags: {}, claims: {}, presets: {} };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const snapshotInventories = new WeakMap();
|
|
232
|
+
/** Reuse the exact inventory evidence that produced a TUI snapshot. */
|
|
233
|
+
export function tuiSnapshotInventory(snapshot) {
|
|
234
|
+
return snapshotInventories.get(snapshot);
|
|
235
|
+
}
|
|
236
|
+
function rememberInventory(snapshot, report) {
|
|
237
|
+
snapshotInventories.set(snapshot, report);
|
|
238
|
+
return snapshot;
|
|
239
|
+
}
|
|
240
|
+
export function attachUpdateAvailability(rows, home, report) {
|
|
241
|
+
try {
|
|
242
|
+
const bySlot = new Map(sharedOutdatedFromInventory(home, report).entries.map((entry) => [entry.slot, entry]));
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
const shared = row.relationships.find((relationship) => relationship.target === 'shared' &&
|
|
245
|
+
relationship.scope === report.scope);
|
|
246
|
+
row.updateAvailability = shared ? bySlot.get(shared.slot) : undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// A missing/unreadable installer lock is not an Inventory failure.
|
|
251
|
+
}
|
|
252
|
+
return rows;
|
|
253
|
+
}
|
|
254
|
+
function visibleTargets(report, harnesses) {
|
|
255
|
+
const detected = new Set(harnesses.detected.map(({ key }) => key));
|
|
256
|
+
return report.targets
|
|
257
|
+
.filter(({ kind, key }) => kind !== 'harness' || detected.has(key))
|
|
258
|
+
.map((target) => ({ name: target.key, dir: target.discoveryRoot }));
|
|
259
|
+
}
|
|
260
|
+
/** Read the live disk state. Call again after every mutation (ADR-0001). */
|
|
261
|
+
export function tuiSnapshot(home) {
|
|
262
|
+
const report = scanGlobalInventory(home, undefined, { persist: false });
|
|
263
|
+
const harnesses = inspectHarnesses(home, report.targets);
|
|
264
|
+
return rememberInventory({
|
|
265
|
+
targets: visibleTargets(report, harnesses),
|
|
266
|
+
harnesses,
|
|
267
|
+
pendingTargetKeys: pendingTargetDefinitions(home).map(({ key }) => key),
|
|
268
|
+
rows: attachUpdateAvailability(projectRows(report), home, report),
|
|
269
|
+
catalog: readViewState(home),
|
|
270
|
+
}, report);
|
|
271
|
+
}
|
|
272
|
+
/** Project-scope snapshot (ADR-0010): target columns are the project targets only;
|
|
273
|
+
* rows are the project + parent + global union, inherited cells marked read-only. */
|
|
274
|
+
export function projectTuiSnapshot(home, projectPath) {
|
|
275
|
+
const report = scanProjectInventory(home, projectPath, undefined, { persist: false });
|
|
276
|
+
const harnesses = inspectHarnesses(home, report.targets, report.projectPath);
|
|
277
|
+
return rememberInventory({
|
|
278
|
+
targets: visibleTargets({ ...report, targets: report.targets.filter((target) => target.scope === 'project') }, harnesses),
|
|
279
|
+
harnesses,
|
|
280
|
+
pendingTargetKeys: pendingTargetDefinitions(home).map(({ key }) => key),
|
|
281
|
+
rows: attachUpdateAvailability(projectRows(report), home, report),
|
|
282
|
+
catalog: readViewState(home),
|
|
283
|
+
project: report.projectPath,
|
|
284
|
+
}, report);
|
|
285
|
+
}
|
|
286
|
+
/** Assemble detail for one explicit instance identity. */
|
|
287
|
+
export function skillDetail(home, instanceId, projectPath) {
|
|
288
|
+
const report = projectPath
|
|
289
|
+
? scanProjectInventory(home, projectPath, undefined, { persist: false })
|
|
290
|
+
: scanGlobalInventory(home, undefined, { persist: false });
|
|
291
|
+
const rows = projectRows(report);
|
|
292
|
+
const instance = rows.find((candidate) => candidate.id === instanceId);
|
|
293
|
+
if (!instance)
|
|
294
|
+
return undefined;
|
|
295
|
+
const state = readViewState(home);
|
|
296
|
+
const unambiguousName = rows.filter((candidate) => candidate.name === instance.name).length === 1;
|
|
297
|
+
const contentPath = instance.realPath
|
|
298
|
+
? path.join(instance.realPath, 'SKILL.md')
|
|
299
|
+
: undefined;
|
|
300
|
+
return {
|
|
301
|
+
id: instance.id,
|
|
302
|
+
name: instance.name,
|
|
303
|
+
displayName: instance.displayName,
|
|
304
|
+
description: instance.description,
|
|
305
|
+
targets: instance.targets,
|
|
306
|
+
relationships: instance.relationships,
|
|
307
|
+
realPaths: instance.realPath ? [instance.realPath] : [],
|
|
308
|
+
source: instance.provenance.source,
|
|
309
|
+
sourceUrl: instance.provenance.sourceUrl,
|
|
310
|
+
sourceLabel: instance.sourceLabel,
|
|
311
|
+
skillPath: instance.provenance.skillPath,
|
|
312
|
+
bundles: Object.entries(state.bundles)
|
|
313
|
+
.filter(([, members]) => members.includes(instance.id) ||
|
|
314
|
+
(unambiguousName && members.includes(instance.name)))
|
|
315
|
+
.map(([bundle]) => bundle)
|
|
316
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
317
|
+
tags: state.tags[instance.id] ?? [],
|
|
318
|
+
content: contentPath ? fs.readFileSync(contentPath, 'utf8') : undefined,
|
|
319
|
+
contentPath,
|
|
320
|
+
};
|
|
321
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillspub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SkillsPub multi-agent skills on/off manager — disk is the source of truth",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"skillspub": "dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.20.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"check:package": "node scripts/check-package.mjs",
|
|
22
|
+
"audit:release": "node scripts/audit-release.mjs",
|
|
23
|
+
"prepack": "npm run build",
|
|
24
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build && npm run audit:release && npm run check:package"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/AlligatorT/SkillsPub.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/AlligatorT/SkillsPub",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/AlligatorT/SkillsPub/issues"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"author": "AlligatorT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22",
|
|
41
|
+
"@types/react": "^19.2.17",
|
|
42
|
+
"skills": "1.5.21",
|
|
43
|
+
"typescript": "^5.8"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"ink": "^7.1.1",
|
|
47
|
+
"react": "^19.2.8",
|
|
48
|
+
"smol-toml": "^1.8.0",
|
|
49
|
+
"wrap-ansi": "^10.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|