vskill 0.5.139 → 0.5.141
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/agents.json +1 -1
- package/dist/commands/stamp-versions.d.ts +25 -0
- package/dist/commands/stamp-versions.js +153 -0
- package/dist/commands/stamp-versions.js.map +1 -0
- package/dist/eval-server/api-routes.d.ts +35 -0
- package/dist/eval-server/api-routes.js +129 -6
- package/dist/eval-server/api-routes.js.map +1 -1
- package/dist/eval-server/skill-create-routes.d.ts +20 -0
- package/dist/eval-server/skill-create-routes.js +50 -2
- package/dist/eval-server/skill-create-routes.js.map +1 -1
- package/dist/eval-server/skill-dir-registry.d.ts +2 -14
- package/dist/eval-server/skill-dir-registry.js +18 -0
- package/dist/eval-server/skill-dir-registry.js.map +1 -1
- package/dist/eval-ui/assets/{CommandPalette-DeEo1aM2.js → CommandPalette-COMOl8Vg.js} +1 -1
- package/dist/eval-ui/assets/{CreateSkillPage-BGi_wZ1y.js → CreateSkillPage-DqUUj-0q.js} +1 -1
- package/dist/eval-ui/assets/{FindSkillsPalette-BbJJSef2.js → FindSkillsPalette-Cy98Ygh7.js} +2 -2
- package/dist/eval-ui/assets/{SearchPaletteCore-BK0cFJb6.js → SearchPaletteCore-CV6YIjYd.js} +1 -1
- package/dist/eval-ui/assets/{SkillDetailPanel-DxidXsNP.js → SkillDetailPanel-ctAUsQxo.js} +1 -1
- package/dist/eval-ui/assets/{UpdateDropdown-CcaDVKoz.js → UpdateDropdown-WsXxpeur.js} +1 -1
- package/dist/eval-ui/assets/{index-C12VKBb6.js → index-B1nvsGfw.js} +40 -40
- package/dist/eval-ui/index.html +1 -1
- package/dist/first-run-onboarding.d.ts +10 -0
- package/dist/first-run-onboarding.js +50 -3
- package/dist/first-run-onboarding.js.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/agents.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface StampOptions {
|
|
2
|
+
/** Roots to walk. When omitted, defaults to ~/.claude/skills + ~/.claude/plugins/cache. */
|
|
3
|
+
root?: string[];
|
|
4
|
+
/** Default version to stamp when missing. */
|
|
5
|
+
version?: string;
|
|
6
|
+
/** When true, write changes to disk; otherwise dry-run (just report). */
|
|
7
|
+
write?: boolean;
|
|
8
|
+
/** When true, emit one path per line instead of human report. */
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface FileResult {
|
|
12
|
+
path: string;
|
|
13
|
+
action: "stamped" | "skipped-has-version" | "skipped-no-frontmatter" | "error";
|
|
14
|
+
current?: string | null;
|
|
15
|
+
next?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function stampVersionsCommand(opts?: StampOptions): Promise<{
|
|
19
|
+
scanned: number;
|
|
20
|
+
stamped: number;
|
|
21
|
+
alreadyHadVersion: number;
|
|
22
|
+
errors: number;
|
|
23
|
+
results: FileResult[];
|
|
24
|
+
}>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// 0759 Phase 7 (B1) — `vskill stamp-versions` command.
|
|
3
|
+
//
|
|
4
|
+
// Walks local skill installation roots and injects `version: "1.0.0"` into the
|
|
5
|
+
// frontmatter of any SKILL.md that is missing a version field. Opt-in only —
|
|
6
|
+
// never runs automatically. Defaults to dry-run; pass `--write` to actually
|
|
7
|
+
// modify files.
|
|
8
|
+
//
|
|
9
|
+
// Default scopes (when no --root passed):
|
|
10
|
+
// - ~/.claude/skills/ (Claude Code personal skills)
|
|
11
|
+
// - ~/.claude/plugins/cache/*/skills/ (plugin-cached skills)
|
|
12
|
+
//
|
|
13
|
+
// Skips files whose frontmatter already declares a version (idempotent).
|
|
14
|
+
// Skips files outside the user's home dir as a safety check.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
import { promises as fs } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join, resolve, sep } from "node:path";
|
|
19
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---(?:\n|$)/;
|
|
20
|
+
const VERSION_LINE_RE = /^version:\s*["']?([^"'\n]+?)["']?\s*$/m;
|
|
21
|
+
async function findSkillMdFiles(roots) {
|
|
22
|
+
const found = [];
|
|
23
|
+
for (const root of roots) {
|
|
24
|
+
try {
|
|
25
|
+
await walk(root, found);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Missing root — silently skip.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
async function walk(dir, out) {
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const full = join(dir, entry.name);
|
|
43
|
+
if (entry.isSymbolicLink())
|
|
44
|
+
continue;
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await walk(full, out);
|
|
47
|
+
}
|
|
48
|
+
else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
49
|
+
out.push(full);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function injectVersion(content, version) {
|
|
54
|
+
const versionLine = `version: "${version}"`;
|
|
55
|
+
const fm = content.match(FRONTMATTER_RE);
|
|
56
|
+
if (!fm) {
|
|
57
|
+
// No frontmatter at all — prepend a minimal one.
|
|
58
|
+
return `---\n${versionLine}\n---\n${content}`;
|
|
59
|
+
}
|
|
60
|
+
if (VERSION_LINE_RE.test(fm[1])) {
|
|
61
|
+
return content; // already has version → no-op
|
|
62
|
+
}
|
|
63
|
+
const newBlock = `${versionLine}\n${fm[1]}`;
|
|
64
|
+
const closer = fm[0].endsWith("---\n") ? "---\n" : "---";
|
|
65
|
+
const before = content.slice(0, fm.index ?? 0);
|
|
66
|
+
const after = content.slice((fm.index ?? 0) + fm[0].length);
|
|
67
|
+
return `${before}---\n${newBlock}\n${closer}${after}`;
|
|
68
|
+
}
|
|
69
|
+
export async function stampVersionsCommand(opts = {}) {
|
|
70
|
+
const home = homedir();
|
|
71
|
+
const defaultRoots = [
|
|
72
|
+
join(home, ".claude", "skills"),
|
|
73
|
+
join(home, ".claude", "plugins", "cache"),
|
|
74
|
+
];
|
|
75
|
+
const roots = (opts.root && opts.root.length > 0 ? opts.root : defaultRoots).map((r) => resolve(r));
|
|
76
|
+
const version = opts.version ?? "1.0.0";
|
|
77
|
+
const write = !!opts.write;
|
|
78
|
+
// Safety: refuse to walk a root that isn't under $HOME (prevents ./. or /).
|
|
79
|
+
const safeRoots = [];
|
|
80
|
+
for (const r of roots) {
|
|
81
|
+
if (r === home || r === resolve(home)) {
|
|
82
|
+
// Refuse a literal home root — would scan EVERYTHING.
|
|
83
|
+
console.error(`vskill stamp-versions: refusing to walk full home dir: ${r}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!r.startsWith(home + sep)) {
|
|
87
|
+
console.error(`vskill stamp-versions: refusing to walk outside home: ${r}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
safeRoots.push(r);
|
|
91
|
+
}
|
|
92
|
+
const files = await findSkillMdFiles(safeRoots);
|
|
93
|
+
const results = [];
|
|
94
|
+
let stamped = 0;
|
|
95
|
+
let already = 0;
|
|
96
|
+
let errors = 0;
|
|
97
|
+
for (const path of files) {
|
|
98
|
+
try {
|
|
99
|
+
const content = await fs.readFile(path, "utf8");
|
|
100
|
+
const fm = content.match(FRONTMATTER_RE);
|
|
101
|
+
if (!fm) {
|
|
102
|
+
// No frontmatter — we'd be creating one. That's invasive; skip in dry-run
|
|
103
|
+
// by default and only act on existing-frontmatter cases for stamp-versions.
|
|
104
|
+
// The full publish flow stamps unconditionally; this CLI is conservative.
|
|
105
|
+
results.push({ path, action: "skipped-no-frontmatter" });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (VERSION_LINE_RE.test(fm[1])) {
|
|
109
|
+
const m = fm[1].match(VERSION_LINE_RE);
|
|
110
|
+
results.push({ path, action: "skipped-has-version", current: m?.[1] ?? null });
|
|
111
|
+
already++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const next = injectVersion(content, version);
|
|
115
|
+
if (write) {
|
|
116
|
+
await fs.writeFile(path, next, "utf8");
|
|
117
|
+
}
|
|
118
|
+
results.push({ path, action: "stamped", next: version });
|
|
119
|
+
stamped++;
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
123
|
+
results.push({ path, action: "error", error: msg });
|
|
124
|
+
errors++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Reporting
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
process.stdout.write(JSON.stringify({ scanned: files.length, stamped, alreadyHadVersion: already, errors, results, dryRun: !write, version }, null, 2) + "\n");
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const dryNote = write ? "(applied)" : "(dry-run — pass --write to apply)";
|
|
133
|
+
process.stdout.write(`vskill stamp-versions ${dryNote}\n`);
|
|
134
|
+
process.stdout.write(` scanned: ${files.length}\n`);
|
|
135
|
+
process.stdout.write(` stamped: ${stamped} (with version "${version}")\n`);
|
|
136
|
+
process.stdout.write(` already had version: ${already}\n`);
|
|
137
|
+
process.stdout.write(` errors: ${errors}\n`);
|
|
138
|
+
if (stamped > 0) {
|
|
139
|
+
process.stdout.write(`\nFiles ${write ? "stamped" : "would stamp"}:\n`);
|
|
140
|
+
for (const r of results.filter((r) => r.action === "stamped")) {
|
|
141
|
+
process.stdout.write(` + ${r.path}\n`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (errors > 0) {
|
|
145
|
+
process.stdout.write(`\nErrors:\n`);
|
|
146
|
+
for (const r of results.filter((r) => r.action === "error")) {
|
|
147
|
+
process.stdout.write(` ! ${r.path}: ${r.error}\n`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { scanned: files.length, stamped, alreadyHadVersion: already, errors, results };
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=stamp-versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stamp-versions.js","sourceRoot":"","sources":["../../src/commands/stamp-versions.ts"],"names":[],"mappings":"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"}
|
|
@@ -28,6 +28,7 @@ export interface AgentScopeEntry {
|
|
|
28
28
|
isDefault: boolean;
|
|
29
29
|
localSkillCount: number;
|
|
30
30
|
globalSkillCount: number;
|
|
31
|
+
pluginSkillCount: number;
|
|
31
32
|
resolvedLocalDir: string;
|
|
32
33
|
resolvedGlobalDir: string;
|
|
33
34
|
lastSync: string | null;
|
|
@@ -111,6 +112,40 @@ export declare function parseSkillFrontmatter(content: string): Record<string, s
|
|
|
111
112
|
* for `origin="source"` or if no registry entry matches.
|
|
112
113
|
*/
|
|
113
114
|
export declare function deriveSourceAgent(skillDir: string, root: string, origin: "source" | "installed"): string | null;
|
|
115
|
+
/**
|
|
116
|
+
* 0770 — Pure regex parser. Normalizes any github.com origin remote
|
|
117
|
+
* (SSH, HTTPS, ssh://) to its canonical `https://github.com/owner/repo`
|
|
118
|
+
* form (no `.git` suffix, no trailing path). Returns null for non-github
|
|
119
|
+
* hosts, malformed input, empty/whitespace strings.
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseGithubRemote(remote: string | null | undefined): string | null;
|
|
122
|
+
/**
|
|
123
|
+
* 0770 — Walk parent directories from `startDir` looking for a `.git` entry
|
|
124
|
+
* (directory OR file — git worktrees use a `.git` file). Bails at the
|
|
125
|
+
* filesystem root or after `maxLevels` iterations. Returns the absolute
|
|
126
|
+
* path of the discovered git root, or null.
|
|
127
|
+
*/
|
|
128
|
+
export declare function walkUpForGitRoot(startDir: string, maxLevels?: number): string | null;
|
|
129
|
+
/**
|
|
130
|
+
* 0770 — Test-only helper to clear the module-level memoization cache so
|
|
131
|
+
* tests can isolate detection runs across `beforeEach`.
|
|
132
|
+
*/
|
|
133
|
+
export declare function resetAuthoredSourceLinkCache(): void;
|
|
134
|
+
/**
|
|
135
|
+
* 0770 — Detect source-repo provenance for a locally-authored skill (no
|
|
136
|
+
* lockfile entry). Walks for `.git`, reads `origin` remote, normalizes via
|
|
137
|
+
* `parseGithubRemote`, and computes `skillPath` from `git ls-files` (with a
|
|
138
|
+
* filesystem fallback for untracked SKILL.md files). Memoized per absolute
|
|
139
|
+
* skill dir for the eval-server process lifetime.
|
|
140
|
+
*
|
|
141
|
+
* All git invocations use `execFileSync` with explicit argv (no shell), a
|
|
142
|
+
* 1500ms hard timeout, and silenced stderr. Any error converts to
|
|
143
|
+
* `{null, null}` — `buildSkillMetadata` never throws because of git.
|
|
144
|
+
*/
|
|
145
|
+
export declare function detectAuthoredSourceLink(skillDir: string): {
|
|
146
|
+
repoUrl: string | null;
|
|
147
|
+
skillPath: string | null;
|
|
148
|
+
};
|
|
114
149
|
/**
|
|
115
150
|
* Build the T-025 metadata payload for a single skill. Reads SKILL.md from
|
|
116
151
|
* disk if present; returns EMPTY_METADATA on any error so the /api/skills
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
5
5
|
import { execSync, execFileSync } from "node:child_process";
|
|
6
|
-
import { join, resolve, dirname, basename } from "node:path";
|
|
6
|
+
import { join, resolve, dirname, basename, relative } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { sendJson, readBody } from "./router.js";
|
|
9
9
|
import { initSSE, sendSSE, sendSSEDone, withHeartbeat, startDynamicHeartbeat } from "./sse-helpers.js";
|
|
@@ -122,6 +122,14 @@ export async function buildAgentsResponse(opts) {
|
|
|
122
122
|
agentPresenceCache.binariesKey === cacheKey.binariesKey) {
|
|
123
123
|
return agentPresenceCache.data;
|
|
124
124
|
}
|
|
125
|
+
// 0772 US-002: count plugin skills once for claude-code. The plugin scanner
|
|
126
|
+
// walks ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/, so
|
|
127
|
+
// the result is independent of agent identity (plugins are CC-only by
|
|
128
|
+
// current registry design). Pass `home` so tests can override the homedir.
|
|
129
|
+
const claudePluginCount = scanInstalledPluginSkills({
|
|
130
|
+
agentId: "claude-code",
|
|
131
|
+
home,
|
|
132
|
+
}).length;
|
|
125
133
|
// Map each agent → resolved local + global dir. For tests, `home` overrides
|
|
126
134
|
// the homedir-derived global path. In production, resolveGlobalSkillsDir()
|
|
127
135
|
// handles cross-platform resolution (darwin / linux / win32).
|
|
@@ -167,6 +175,7 @@ export async function buildAgentsResponse(opts) {
|
|
|
167
175
|
isDefault,
|
|
168
176
|
localSkillCount,
|
|
169
177
|
globalSkillCount,
|
|
178
|
+
pluginSkillCount: agent.id === "claude-code" ? claudePluginCount : 0,
|
|
170
179
|
resolvedLocalDir,
|
|
171
180
|
resolvedGlobalDir,
|
|
172
181
|
lastSync,
|
|
@@ -597,6 +606,116 @@ export function deriveSourceAgent(skillDir, root, origin) {
|
|
|
597
606
|
}
|
|
598
607
|
return null;
|
|
599
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* 0770 — Pure regex parser. Normalizes any github.com origin remote
|
|
611
|
+
* (SSH, HTTPS, ssh://) to its canonical `https://github.com/owner/repo`
|
|
612
|
+
* form (no `.git` suffix, no trailing path). Returns null for non-github
|
|
613
|
+
* hosts, malformed input, empty/whitespace strings.
|
|
614
|
+
*/
|
|
615
|
+
export function parseGithubRemote(remote) {
|
|
616
|
+
const trimmed = (remote ?? "").trim();
|
|
617
|
+
if (!trimmed)
|
|
618
|
+
return null;
|
|
619
|
+
// SSH: git@github.com:owner/repo[.git]
|
|
620
|
+
let m = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/.exec(trimmed);
|
|
621
|
+
if (m)
|
|
622
|
+
return `https://github.com/${m[1]}/${m[2]}`;
|
|
623
|
+
// ssh://git@github.com/owner/repo[.git]
|
|
624
|
+
m = /^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/.exec(trimmed);
|
|
625
|
+
if (m)
|
|
626
|
+
return `https://github.com/${m[1]}/${m[2]}`;
|
|
627
|
+
// http(s)://github.com/owner/repo[.git][/...]
|
|
628
|
+
m = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s?#]+?)(?:\.git)?(?:[/?#].*)?$/.exec(trimmed);
|
|
629
|
+
if (m)
|
|
630
|
+
return `https://github.com/${m[1]}/${m[2]}`;
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 0770 — Walk parent directories from `startDir` looking for a `.git` entry
|
|
635
|
+
* (directory OR file — git worktrees use a `.git` file). Bails at the
|
|
636
|
+
* filesystem root or after `maxLevels` iterations. Returns the absolute
|
|
637
|
+
* path of the discovered git root, or null.
|
|
638
|
+
*/
|
|
639
|
+
export function walkUpForGitRoot(startDir, maxLevels = 12) {
|
|
640
|
+
let current = resolve(startDir);
|
|
641
|
+
for (let i = 0; i < maxLevels; i++) {
|
|
642
|
+
if (existsSync(join(current, ".git")))
|
|
643
|
+
return current;
|
|
644
|
+
const parent = dirname(current);
|
|
645
|
+
if (parent === current)
|
|
646
|
+
return null;
|
|
647
|
+
current = parent;
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
const authoredSourceLinkCache = new Map();
|
|
652
|
+
/**
|
|
653
|
+
* 0770 — Test-only helper to clear the module-level memoization cache so
|
|
654
|
+
* tests can isolate detection runs across `beforeEach`.
|
|
655
|
+
*/
|
|
656
|
+
export function resetAuthoredSourceLinkCache() {
|
|
657
|
+
authoredSourceLinkCache.clear();
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* 0770 — Detect source-repo provenance for a locally-authored skill (no
|
|
661
|
+
* lockfile entry). Walks for `.git`, reads `origin` remote, normalizes via
|
|
662
|
+
* `parseGithubRemote`, and computes `skillPath` from `git ls-files` (with a
|
|
663
|
+
* filesystem fallback for untracked SKILL.md files). Memoized per absolute
|
|
664
|
+
* skill dir for the eval-server process lifetime.
|
|
665
|
+
*
|
|
666
|
+
* All git invocations use `execFileSync` with explicit argv (no shell), a
|
|
667
|
+
* 1500ms hard timeout, and silenced stderr. Any error converts to
|
|
668
|
+
* `{null, null}` — `buildSkillMetadata` never throws because of git.
|
|
669
|
+
*/
|
|
670
|
+
export function detectAuthoredSourceLink(skillDir) {
|
|
671
|
+
const absDir = resolve(skillDir);
|
|
672
|
+
const cached = authoredSourceLinkCache.get(absDir);
|
|
673
|
+
if (cached)
|
|
674
|
+
return cached;
|
|
675
|
+
const compute = () => {
|
|
676
|
+
const gitRoot = walkUpForGitRoot(absDir);
|
|
677
|
+
if (!gitRoot)
|
|
678
|
+
return { repoUrl: null, skillPath: null };
|
|
679
|
+
let remote = "";
|
|
680
|
+
try {
|
|
681
|
+
remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
682
|
+
cwd: gitRoot,
|
|
683
|
+
timeout: 1500,
|
|
684
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
685
|
+
encoding: "utf-8",
|
|
686
|
+
}).trim();
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return { repoUrl: null, skillPath: null };
|
|
690
|
+
}
|
|
691
|
+
const repoUrl = parseGithubRemote(remote);
|
|
692
|
+
if (!repoUrl)
|
|
693
|
+
return { repoUrl: null, skillPath: null };
|
|
694
|
+
let skillPath = null;
|
|
695
|
+
try {
|
|
696
|
+
const tracked = execFileSync("git", ["ls-files", "--full-name", "SKILL.md"], {
|
|
697
|
+
cwd: absDir,
|
|
698
|
+
timeout: 1500,
|
|
699
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
700
|
+
encoding: "utf-8",
|
|
701
|
+
}).trim();
|
|
702
|
+
if (tracked)
|
|
703
|
+
skillPath = tracked;
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
// fall through to filesystem fallback
|
|
707
|
+
}
|
|
708
|
+
if (!skillPath) {
|
|
709
|
+
// Filesystem fallback for untracked SKILL.md — same path the file will
|
|
710
|
+
// have on github.com once committed and pushed.
|
|
711
|
+
skillPath = relative(gitRoot, join(absDir, "SKILL.md")).replace(/\\/g, "/");
|
|
712
|
+
}
|
|
713
|
+
return { repoUrl, skillPath };
|
|
714
|
+
};
|
|
715
|
+
const result = compute();
|
|
716
|
+
authoredSourceLinkCache.set(absDir, result);
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
600
719
|
/**
|
|
601
720
|
* 0737 — Resolve the source-repo provenance (repoUrl + skillPath) for a
|
|
602
721
|
* skill by looking up its lockfile entry. Two precedences:
|
|
@@ -608,19 +727,20 @@ export function deriveSourceAgent(skillDir, root, origin) {
|
|
|
608
727
|
* skill dir basename and the lockfile key differ — fall back to the parent
|
|
609
728
|
* directory's basename when no exact match exists.
|
|
610
729
|
*
|
|
611
|
-
*
|
|
612
|
-
* `
|
|
613
|
-
*
|
|
730
|
+
* 0770 — When no lockfile entry resolves provenance, fall through to
|
|
731
|
+
* `detectAuthoredSourceLink` which inspects the parent git repo's origin
|
|
732
|
+
* remote. Lockfile-derived values still take precedence to preserve
|
|
733
|
+
* install-time provenance when the workspace itself is a git repo.
|
|
614
734
|
*/
|
|
615
735
|
function resolveSourceLink(skillDir, root) {
|
|
616
736
|
const lock = readLockfile(root);
|
|
617
737
|
if (!lock)
|
|
618
|
-
return
|
|
738
|
+
return detectAuthoredSourceLink(skillDir);
|
|
619
739
|
const skillName = basename(skillDir);
|
|
620
740
|
const parentName = basename(dirname(skillDir));
|
|
621
741
|
const entry = lock.skills[skillName] ?? lock.skills[parentName];
|
|
622
742
|
if (!entry)
|
|
623
|
-
return
|
|
743
|
+
return detectAuthoredSourceLink(skillDir);
|
|
624
744
|
if (entry.sourceRepoUrl) {
|
|
625
745
|
return {
|
|
626
746
|
repoUrl: entry.sourceRepoUrl,
|
|
@@ -636,6 +756,9 @@ function resolveSourceLink(skillDir, root) {
|
|
|
636
756
|
// copy-chip (local path); a fresh `vskill add` writes the explicit
|
|
637
757
|
// `sourceSkillPath` and restores the working anchor via the branch above.
|
|
638
758
|
const m = /^github:([^/]+)\/([^/#]+)/.exec(entry.source ?? "");
|
|
759
|
+
// 0770: do NOT fall through here — an installed skill with a non-github
|
|
760
|
+
// `source` (e.g. `marketplace:...`) is still installed, not authored. Local
|
|
761
|
+
// git detection would leak the workspace remote (umbrella, etc.).
|
|
639
762
|
if (!m)
|
|
640
763
|
return { repoUrl: null, skillPath: null };
|
|
641
764
|
return {
|