selftune 0.2.30 → 0.2.31
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/apps/local-dashboard/dist/assets/{index-Coq42hE4.js → index-B7v_o1WC.js} +2 -2
- package/apps/local-dashboard/dist/assets/index-CrO77SVi.css +1 -0
- package/apps/local-dashboard/dist/index.html +2 -2
- package/cli/selftune/registry/github-install.ts +256 -0
- package/cli/selftune/registry/index.ts +1 -1
- package/cli/selftune/registry/install.ts +58 -7
- package/package.json +1 -1
- package/packages/dashboard-core/src/routes/manifest.ts +2 -2
- package/packages/ui/src/components/SkillReportPanels.tsx +7 -7
- package/packages/ui/src/primitives/button.tsx +5 -0
- package/skill/SKILL.md +1 -1
- package/skill/workflows/Registry.md +19 -13
- package/apps/local-dashboard/dist/assets/index-BcXquWFB.css +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cp, mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export interface GithubRegistryInstallTarget {
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
repoFullName: string;
|
|
14
|
+
ref: string | null;
|
|
15
|
+
skillPath: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeGithubSkillPath(skillPath: string): string {
|
|
19
|
+
const trimmed = skillPath.trim().replace(/\\/g, "/");
|
|
20
|
+
if (!trimmed || trimmed === ".") {
|
|
21
|
+
return ".";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const segments = trimmed.split("/").filter(Boolean);
|
|
25
|
+
if (segments.includes("..")) {
|
|
26
|
+
throw new Error("GitHub skill path must stay within the repository");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalized = path.posix.normalize(trimmed).replace(/^\/+|\/+$/g, "");
|
|
30
|
+
return normalized || ".";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseGithubRegistryInstallTarget(
|
|
34
|
+
rawTarget: string,
|
|
35
|
+
): GithubRegistryInstallTarget | null {
|
|
36
|
+
if (!rawTarget.startsWith("github:")) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const spec = rawTarget.slice("github:".length).trim();
|
|
41
|
+
if (!spec) {
|
|
42
|
+
throw new Error("GitHub install target must be github:owner/repo[@ref][//path]");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pathSeparatorIndex = spec.indexOf("//");
|
|
46
|
+
const repoWithMaybeRef = pathSeparatorIndex === -1 ? spec : spec.slice(0, pathSeparatorIndex);
|
|
47
|
+
const pathWithMaybeRef = pathSeparatorIndex === -1 ? null : spec.slice(pathSeparatorIndex + 2);
|
|
48
|
+
|
|
49
|
+
let ref: string | null = null;
|
|
50
|
+
let repoSpec = repoWithMaybeRef;
|
|
51
|
+
|
|
52
|
+
const repoRefIndex = repoWithMaybeRef.lastIndexOf("@");
|
|
53
|
+
if (repoRefIndex !== -1) {
|
|
54
|
+
repoSpec = repoWithMaybeRef.slice(0, repoRefIndex);
|
|
55
|
+
ref = repoWithMaybeRef.slice(repoRefIndex + 1) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let skillPath: string | null = null;
|
|
59
|
+
if (pathWithMaybeRef != null) {
|
|
60
|
+
const pathRefIndex = pathWithMaybeRef.lastIndexOf("@");
|
|
61
|
+
if (pathRefIndex !== -1) {
|
|
62
|
+
skillPath = pathWithMaybeRef.slice(0, pathRefIndex) || ".";
|
|
63
|
+
ref = pathWithMaybeRef.slice(pathRefIndex + 1) || ref;
|
|
64
|
+
} else {
|
|
65
|
+
skillPath = pathWithMaybeRef || ".";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const match = repoSpec.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
70
|
+
if (!match) {
|
|
71
|
+
throw new Error("GitHub install target must look like github:owner/repo[@ref][//path]");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
owner: match[1],
|
|
76
|
+
repo: match[2],
|
|
77
|
+
repoFullName: `${match[1]}/${match[2]}`,
|
|
78
|
+
ref,
|
|
79
|
+
skillPath: skillPath ? normalizeGithubSkillPath(skillPath) : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isExcludedEntry(name: string): boolean {
|
|
84
|
+
return name === ".git" || name === "node_modules" || name === ".env" || name.startsWith(".env.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function discoverLocalSkillPaths(rootDir: string): Promise<string[]> {
|
|
88
|
+
async function walk(currentDir: string, basePath: string): Promise<string[]> {
|
|
89
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
90
|
+
const discovered: string[] = [];
|
|
91
|
+
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (isExcludedEntry(entry.name)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
98
|
+
const relativePath = basePath ? path.join(basePath, entry.name) : entry.name;
|
|
99
|
+
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
discovered.push(...(await walk(fullPath, relativePath)));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
106
|
+
discovered.push(basePath ? basePath.split(path.sep).join("/") : ".");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return discovered;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const discovered = await walk(rootDir, "");
|
|
114
|
+
return [...new Set(discovered)].sort((a, b) => a.localeCompare(b));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function resolveGithubSkillPath(
|
|
118
|
+
repoDir: string,
|
|
119
|
+
requestedSkillPath: string | null,
|
|
120
|
+
): Promise<{ skillPath: string; availablePaths: string[] }> {
|
|
121
|
+
const availablePaths = await discoverLocalSkillPaths(repoDir);
|
|
122
|
+
|
|
123
|
+
if (requestedSkillPath) {
|
|
124
|
+
const normalized = normalizeGithubSkillPath(requestedSkillPath);
|
|
125
|
+
const skillMdPath =
|
|
126
|
+
normalized === "."
|
|
127
|
+
? path.join(repoDir, "SKILL.md")
|
|
128
|
+
: path.join(repoDir, ...normalized.split("/"), "SKILL.md");
|
|
129
|
+
await stat(skillMdPath);
|
|
130
|
+
return { skillPath: normalized, availablePaths };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (availablePaths.length === 1) {
|
|
134
|
+
return { skillPath: availablePaths[0] ?? ".", availablePaths };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (availablePaths.length === 0) {
|
|
138
|
+
throw new Error("No SKILL.md found in the GitHub repository");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Multiple skills found in the GitHub repository. Choose one with github:owner/repo//path (available: ${availablePaths.join(", ")})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function deriveGithubInstallSkillName(
|
|
147
|
+
frontmatterName: string,
|
|
148
|
+
skillPath: string,
|
|
149
|
+
skillDir: string,
|
|
150
|
+
repoName: string,
|
|
151
|
+
): string {
|
|
152
|
+
const trimmedName = frontmatterName.trim();
|
|
153
|
+
if (trimmedName) {
|
|
154
|
+
return trimmedName;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return skillPath === "." ? repoName : path.basename(skillDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cloneGithubRepository(
|
|
161
|
+
target: GithubRegistryInstallTarget,
|
|
162
|
+
cloneDir: string,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const repoUrl = `https://github.com/${target.repoFullName}.git`;
|
|
165
|
+
const args = ["clone", "--depth=1"];
|
|
166
|
+
|
|
167
|
+
if (target.ref) {
|
|
168
|
+
args.push("--branch", target.ref);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
args.push(repoUrl, cloneDir);
|
|
172
|
+
|
|
173
|
+
await execFileAsync("git", args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function copySkillDirectory(sourceDir: string, targetDir: string): Promise<void> {
|
|
177
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
178
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
179
|
+
|
|
180
|
+
await cp(sourceDir, targetDir, {
|
|
181
|
+
recursive: true,
|
|
182
|
+
filter: (entryPath) => {
|
|
183
|
+
const basename = path.basename(entryPath);
|
|
184
|
+
return !isExcludedEntry(basename);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function installFromGithubTarget(
|
|
190
|
+
rawTarget: string,
|
|
191
|
+
globalFlag: boolean,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const target = parseGithubRegistryInstallTarget(rawTarget);
|
|
194
|
+
if (!target) {
|
|
195
|
+
throw new Error("GitHub install target must start with github:");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "selftune-github-install-"));
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const cloneDir = path.join(tempRoot, "repo");
|
|
202
|
+
await cloneGithubRepository(target, cloneDir);
|
|
203
|
+
|
|
204
|
+
const { skillPath, availablePaths } = await resolveGithubSkillPath(cloneDir, target.skillPath);
|
|
205
|
+
const skillDir = skillPath === "." ? cloneDir : path.join(cloneDir, ...skillPath.split("/"));
|
|
206
|
+
const skillContent = await readFile(path.join(skillDir, "SKILL.md"), "utf-8");
|
|
207
|
+
const frontmatter = parseFrontmatter(skillContent);
|
|
208
|
+
const skillName = deriveGithubInstallSkillName(
|
|
209
|
+
frontmatter.name,
|
|
210
|
+
skillPath,
|
|
211
|
+
skillDir,
|
|
212
|
+
target.repo,
|
|
213
|
+
);
|
|
214
|
+
const resolvedCommit = (
|
|
215
|
+
await execFileAsync("git", ["-C", cloneDir, "rev-parse", "HEAD"])
|
|
216
|
+
).stdout.trim();
|
|
217
|
+
|
|
218
|
+
const targetBase = globalFlag
|
|
219
|
+
? path.join(process.env.HOME || "~", ".claude", "skills")
|
|
220
|
+
: path.join(process.cwd(), ".claude", "skills");
|
|
221
|
+
const targetDir = path.join(targetBase, skillName);
|
|
222
|
+
|
|
223
|
+
await copySkillDirectory(skillDir, targetDir);
|
|
224
|
+
await writeFile(
|
|
225
|
+
path.join(targetDir, ".selftune-source.json"),
|
|
226
|
+
JSON.stringify(
|
|
227
|
+
{
|
|
228
|
+
source: "github-direct",
|
|
229
|
+
repo: target.repoFullName,
|
|
230
|
+
ref: target.ref ?? "HEAD",
|
|
231
|
+
commit: resolvedCommit,
|
|
232
|
+
skill_path: skillPath,
|
|
233
|
+
available_paths: availablePaths,
|
|
234
|
+
},
|
|
235
|
+
null,
|
|
236
|
+
2,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
success: true,
|
|
243
|
+
source: "github-direct",
|
|
244
|
+
name: skillName,
|
|
245
|
+
repo: target.repoFullName,
|
|
246
|
+
ref: target.ref ?? "HEAD",
|
|
247
|
+
commit: resolvedCommit,
|
|
248
|
+
skill_path: skillPath,
|
|
249
|
+
path: targetDir,
|
|
250
|
+
global: globalFlag,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
} finally {
|
|
254
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -24,7 +24,7 @@ Usage:
|
|
|
24
24
|
|
|
25
25
|
Subcommands:
|
|
26
26
|
push [name] Push current skill folder as a new version
|
|
27
|
-
install <name> Download
|
|
27
|
+
install <name> Download from the registry or install github:owner/repo[@ref][//path]
|
|
28
28
|
sync Check for updates and pull latest versions
|
|
29
29
|
status Show installed entries and version drift
|
|
30
30
|
rollback <name> Rollback to a previous version
|
|
@@ -8,6 +8,7 @@ import { hostname } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { registryRequest } from "./client.js";
|
|
11
|
+
import { installFromGithubTarget, parseGithubRegistryInstallTarget } from "./github-install.js";
|
|
11
12
|
|
|
12
13
|
export async function cliMain() {
|
|
13
14
|
const args = process.argv.slice(2);
|
|
@@ -17,13 +18,45 @@ export async function cliMain() {
|
|
|
17
18
|
if (!name) {
|
|
18
19
|
console.error(
|
|
19
20
|
JSON.stringify({
|
|
20
|
-
error: "Usage: selftune registry install <name>",
|
|
21
|
+
error: "Usage: selftune registry install <name|github:owner/repo[@ref][//path]>",
|
|
21
22
|
guidance: { next_command: "selftune registry list" },
|
|
22
23
|
}),
|
|
23
24
|
);
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
let githubTarget = null;
|
|
29
|
+
try {
|
|
30
|
+
githubTarget = parseGithubRegistryInstallTarget(name);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
error: error instanceof Error ? error.message : "Invalid GitHub install target",
|
|
35
|
+
guidance: {
|
|
36
|
+
next_command: "selftune registry install github:owner/repo//path",
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (githubTarget) {
|
|
44
|
+
try {
|
|
45
|
+
await installFromGithubTarget(name, globalFlag);
|
|
46
|
+
return;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
error: error instanceof Error ? error.message : "GitHub install failed",
|
|
51
|
+
guidance: {
|
|
52
|
+
next_command: "selftune registry install github:owner/repo//path",
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
// Find entry by name
|
|
28
61
|
const listResult = await registryRequest<{
|
|
29
62
|
entries: Array<{
|
|
@@ -49,7 +82,12 @@ export async function cliMain() {
|
|
|
49
82
|
// Get detail with versions
|
|
50
83
|
const detailResult = await registryRequest<{
|
|
51
84
|
entry: { id: string; name: string };
|
|
52
|
-
versions: Array<{
|
|
85
|
+
versions: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
version: string;
|
|
88
|
+
content_hash: string;
|
|
89
|
+
is_current: boolean;
|
|
90
|
+
}>;
|
|
53
91
|
}>("GET", `/${entryId}`);
|
|
54
92
|
|
|
55
93
|
if (!detailResult.success) {
|
|
@@ -71,7 +109,9 @@ export async function cliMain() {
|
|
|
71
109
|
latest_content_hash: string;
|
|
72
110
|
}>;
|
|
73
111
|
}>("POST", "/sync", {
|
|
74
|
-
body: {
|
|
112
|
+
body: {
|
|
113
|
+
installations: [{ entry_id: entryId, current_version_hash: "none" }],
|
|
114
|
+
},
|
|
75
115
|
});
|
|
76
116
|
|
|
77
117
|
const downloadUrl = syncResult.data?.entries?.[0]?.download_url;
|
|
@@ -82,7 +122,9 @@ export async function cliMain() {
|
|
|
82
122
|
|
|
83
123
|
// Download archive
|
|
84
124
|
console.log(`Installing ${name} v${currentVersion.version}...`);
|
|
85
|
-
const response = await fetch(downloadUrl, {
|
|
125
|
+
const response = await fetch(downloadUrl, {
|
|
126
|
+
signal: AbortSignal.timeout(60_000),
|
|
127
|
+
});
|
|
86
128
|
if (!response.ok) {
|
|
87
129
|
console.error(JSON.stringify({ error: `Download failed: HTTP ${response.status}` }));
|
|
88
130
|
process.exit(1);
|
|
@@ -119,13 +161,22 @@ export async function cliMain() {
|
|
|
119
161
|
|
|
120
162
|
// Update local state
|
|
121
163
|
const statePath = join(process.env.HOME || "~", ".selftune", "registry-state.json");
|
|
122
|
-
let state: Array<{
|
|
123
|
-
|
|
164
|
+
let state: Array<{
|
|
165
|
+
entryId: string;
|
|
166
|
+
name: string;
|
|
167
|
+
versionHash: string;
|
|
168
|
+
installPath: string;
|
|
169
|
+
}> = [];
|
|
124
170
|
try {
|
|
125
171
|
state = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
126
172
|
} catch {}
|
|
127
173
|
state = state.filter((s) => s.entryId !== entryId);
|
|
128
|
-
state.push({
|
|
174
|
+
state.push({
|
|
175
|
+
entryId,
|
|
176
|
+
name,
|
|
177
|
+
versionHash: currentVersion.content_hash,
|
|
178
|
+
installPath: targetDir,
|
|
179
|
+
});
|
|
129
180
|
await mkdir(join(process.env.HOME || "~", ".selftune"), { recursive: true });
|
|
130
181
|
await writeFile(statePath, JSON.stringify(state, null, 2));
|
|
131
182
|
|
package/package.json
CHANGED
|
@@ -217,7 +217,7 @@ export const DASHBOARD_ROUTE_MANIFEST: readonly DashboardRouteManifestEntry[] =
|
|
|
217
217
|
icon: PackageIcon,
|
|
218
218
|
feature: "registry",
|
|
219
219
|
discoverableFeature: "registry",
|
|
220
|
-
lockedTitle: "Cloud Registry lives in
|
|
220
|
+
lockedTitle: "Cloud Registry lives in selftune Cloud",
|
|
221
221
|
lockedBody:
|
|
222
222
|
"Publish versioned skills, watch installations across projects, and roll back bad versions from a single cloud workspace.",
|
|
223
223
|
lockedHighlights: [
|
|
@@ -255,7 +255,7 @@ export const DASHBOARD_ROUTE_MANIFEST: readonly DashboardRouteManifestEntry[] =
|
|
|
255
255
|
icon: UsersIcon,
|
|
256
256
|
feature: "signals",
|
|
257
257
|
discoverableFeature: "signals",
|
|
258
|
-
lockedTitle: "Contributor signals run through
|
|
258
|
+
lockedTitle: "Contributor signals run through selftune Cloud",
|
|
259
259
|
lockedBody:
|
|
260
260
|
"See anonymized contributor signals, compare bundle submissions, and turn real-world usage into proposals without leaving the shared dashboard.",
|
|
261
261
|
lockedHighlights: [
|
|
@@ -379,7 +379,7 @@ function narrativeObservedText({
|
|
|
379
379
|
promptLinkRate != null
|
|
380
380
|
? ` It could link ${formatRate(promptLinkRate)} of those checks back to prompts.`
|
|
381
381
|
: "";
|
|
382
|
-
return `
|
|
382
|
+
return `selftune watched ${checks} skill checks across ${sessions} sessions.${promptClause}`;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
function narrativeDiagnosisText({
|
|
@@ -411,17 +411,17 @@ function narrativeDecisionText({
|
|
|
411
411
|
}) {
|
|
412
412
|
switch (trustState) {
|
|
413
413
|
case "validated":
|
|
414
|
-
return `
|
|
414
|
+
return `selftune found a candidate that looks promising, but it has not been deployed yet. ${nextActionText}`;
|
|
415
415
|
case "deployed":
|
|
416
|
-
return `A change has already been deployed for this skill.
|
|
416
|
+
return `A change has already been deployed for this skill. selftune is now watching for regressions in real use.`;
|
|
417
417
|
case "rolled_back":
|
|
418
418
|
return `A previous change was rolled back, so the live skill is back on the safer version while selftune keeps observing.`;
|
|
419
419
|
case "watch":
|
|
420
|
-
return `
|
|
420
|
+
return `selftune sees enough signal to keep a close eye on this skill, but not enough to blindly change it. ${nextActionText}`;
|
|
421
421
|
case "observed":
|
|
422
|
-
return `
|
|
422
|
+
return `selftune is still learning how people use this skill before making stronger recommendations.`;
|
|
423
423
|
case "low_sample":
|
|
424
|
-
return `There is not enough evidence yet to trust a big change here.
|
|
424
|
+
return `There is not enough evidence yet to trust a big change here. selftune is still collecting examples.`;
|
|
425
425
|
default:
|
|
426
426
|
return latestAction
|
|
427
427
|
? `The latest automated decision for this skill was ${latestAction}. ${nextActionText}`
|
|
@@ -552,7 +552,7 @@ export function SkillTrustNarrativePanel({
|
|
|
552
552
|
/>
|
|
553
553
|
</div>
|
|
554
554
|
<div className="rounded-xl border border-border/10 bg-muted/15 px-4 py-3 text-sm text-muted-foreground">
|
|
555
|
-
If a proposal is rejected or still pending, your live skill has not changed yet.
|
|
555
|
+
If a proposal is rejected or still pending, your live skill has not changed yet. selftune
|
|
556
556
|
only earns trust by testing changes before deployment.
|
|
557
557
|
</div>
|
|
558
558
|
</CardContent>
|
|
@@ -18,6 +18,10 @@ const buttonVariants = cva(
|
|
|
18
18
|
destructive:
|
|
19
19
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
20
20
|
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
"glass-primary":
|
|
22
|
+
"border-cyan-300 bg-background/75 text-foreground shadow-[0_10px_28px_rgba(34,211,238,0.14),inset_0_1px_0_rgba(255,255,255,0.09)] hover:border-cyan-200 hover:bg-background/85 hover:shadow-[0_14px_34px_rgba(34,211,238,0.18),inset_0_1px_0_rgba(255,255,255,0.12)]",
|
|
23
|
+
"glass-secondary":
|
|
24
|
+
"border-border/70 bg-background/60 text-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.06)] hover:border-cyan-400/20 hover:bg-background/75 hover:shadow-[0_10px_24px_rgba(34,211,238,0.08),inset_0_1px_0_rgba(255,255,255,0.1)]",
|
|
21
25
|
},
|
|
22
26
|
size: {
|
|
23
27
|
default:
|
|
@@ -31,6 +35,7 @@ const buttonVariants = cva(
|
|
|
31
35
|
"icon-sm":
|
|
32
36
|
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
33
37
|
"icon-lg": "size-9",
|
|
38
|
+
glass: "gap-2 px-4 py-2 backdrop-blur-sm",
|
|
34
39
|
},
|
|
35
40
|
},
|
|
36
41
|
defaultVariants: {
|
package/skill/SKILL.md
CHANGED
|
@@ -4,20 +4,21 @@ Manage versioned skill distribution across your team. Push skill folders to the
|
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
7
|
-
| Command
|
|
8
|
-
|
|
9
|
-
| `selftune registry push [name]`
|
|
10
|
-
| `selftune registry install <name>` | `--global`
|
|
11
|
-
| `selftune registry sync`
|
|
12
|
-
| `selftune registry status`
|
|
13
|
-
| `selftune registry rollback <name>`
|
|
14
|
-
| `selftune registry history <name>`
|
|
15
|
-
| `selftune registry list`
|
|
7
|
+
| Command | Flags | What It Does |
|
|
8
|
+
| ------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------- |
|
|
9
|
+
| `selftune registry push [name]` | `--version=<semver>` `--summary=<text>` | Archive current skill folder and push as a new version |
|
|
10
|
+
| `selftune registry install <name\|github:owner/repo[@ref][//path]>` | `--global` | Download from the registry or clone/install directly from GitHub |
|
|
11
|
+
| `selftune registry sync` | | Check all installed entries for updates, pull latest |
|
|
12
|
+
| `selftune registry status` | | Show installed entries with version drift |
|
|
13
|
+
| `selftune registry rollback <name>` | `--to=<version>` `--reason=<text>` | Rollback a skill to a previous version |
|
|
14
|
+
| `selftune registry history <name>` | | Show version timeline with quality data |
|
|
15
|
+
| `selftune registry list` | | Show all published entries in the org |
|
|
16
16
|
|
|
17
17
|
## When to Use
|
|
18
18
|
|
|
19
19
|
- User says "push this skill to the team" → `selftune registry push`
|
|
20
20
|
- User says "install the deploy skill" → `selftune registry install deploy`
|
|
21
|
+
- User says "install this GitHub skill repo" → `selftune registry install github:owner/repo`
|
|
21
22
|
- User says "update my skills" or "sync registry" → `selftune registry sync`
|
|
22
23
|
- User says "check for updates" → `selftune registry status`
|
|
23
24
|
- User says "rollback the deploy skill" → `selftune registry rollback deploy`
|
|
@@ -34,10 +35,13 @@ Manage versioned skill distribution across your team. Push skill folders to the
|
|
|
34
35
|
|
|
35
36
|
## Install Workflow
|
|
36
37
|
|
|
37
|
-
1. Run `selftune registry install <name>` to pull from the registry
|
|
38
|
+
1. Run `selftune registry install <name>` to pull from the registry, or
|
|
39
|
+
`selftune registry install github:owner/repo[@ref][//path]` to clone and
|
|
40
|
+
install directly from GitHub using local git credentials
|
|
38
41
|
2. By default, installs to `.claude/skills/<name>/` in the current project
|
|
39
42
|
3. Use `--global` to install to `~/.claude/skills/<name>/` (available everywhere)
|
|
40
|
-
4.
|
|
43
|
+
4. Registry installs are tracked by `selftune registry status`; direct GitHub
|
|
44
|
+
installs are local-only and do not participate in `registry sync`
|
|
41
45
|
|
|
42
46
|
## Sync Workflow
|
|
43
47
|
|
|
@@ -82,8 +86,10 @@ All commands output JSON for agent consumption:
|
|
|
82
86
|
|
|
83
87
|
**User wants to install a shared skill**
|
|
84
88
|
|
|
85
|
-
> Run `selftune registry install <name
|
|
86
|
-
>
|
|
89
|
+
> Run `selftune registry install <name>` for a cloud-published skill, or
|
|
90
|
+
> `selftune registry install github:owner/repo[@ref][//path]` if they want to
|
|
91
|
+
> install directly from GitHub. Use `--global` if they want it available across
|
|
92
|
+
> all projects.
|
|
87
93
|
|
|
88
94
|
**User wants to check what's outdated**
|
|
89
95
|
|