selftune 0.2.29 → 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.
Files changed (37) hide show
  1. package/apps/local-dashboard/dist/assets/index-B7v_o1WC.js +15 -0
  2. package/apps/local-dashboard/dist/assets/index-CrO77SVi.css +1 -0
  3. package/apps/local-dashboard/dist/assets/vendor-ui-B0H8s1mP.js +1 -0
  4. package/apps/local-dashboard/dist/index.html +3 -3
  5. package/cli/selftune/auto-update.ts +40 -8
  6. package/cli/selftune/command-surface.ts +1 -1
  7. package/cli/selftune/constants.ts +5 -0
  8. package/cli/selftune/dashboard-action-events.ts +117 -0
  9. package/cli/selftune/dashboard-action-instrumentation.ts +103 -0
  10. package/cli/selftune/dashboard-action-result.ts +90 -0
  11. package/cli/selftune/dashboard-action-stream.ts +252 -0
  12. package/cli/selftune/dashboard-contract.ts +81 -1
  13. package/cli/selftune/dashboard-server.ts +133 -16
  14. package/cli/selftune/eval/hooks-to-evals.ts +157 -0
  15. package/cli/selftune/eval/synthetic-evals.ts +33 -2
  16. package/cli/selftune/eval/unit-test-cli.ts +53 -5
  17. package/cli/selftune/evolution/validate-host-replay.ts +191 -14
  18. package/cli/selftune/index.ts +4 -0
  19. package/cli/selftune/ingestors/opencode-ingest.ts +117 -8
  20. package/cli/selftune/localdb/schema.ts +34 -0
  21. package/cli/selftune/registry/github-install.ts +256 -0
  22. package/cli/selftune/registry/index.ts +1 -1
  23. package/cli/selftune/registry/install.ts +58 -7
  24. package/cli/selftune/routes/actions.ts +273 -42
  25. package/cli/selftune/testing-readiness.ts +203 -10
  26. package/cli/selftune/utils/llm-call.ts +90 -1
  27. package/package.json +1 -1
  28. package/packages/dashboard-core/src/routes/manifest.ts +2 -2
  29. package/packages/ui/src/components/EvolutionTimeline.tsx +1 -1
  30. package/packages/ui/src/components/SkillReportPanels.tsx +7 -7
  31. package/packages/ui/src/primitives/button.tsx +5 -0
  32. package/skill/SKILL.md +1 -1
  33. package/skill/workflows/Dashboard.md +50 -23
  34. package/skill/workflows/Registry.md +19 -13
  35. package/apps/local-dashboard/dist/assets/index-BcvtYmmL.js +0 -15
  36. package/apps/local-dashboard/dist/assets/index-BpRIxnpS.css +0 -1
  37. package/apps/local-dashboard/dist/assets/vendor-ui-DqH_uxum.js +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 and install a skill from the registry
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<{ id: string; version: string; content_hash: string; is_current: boolean }>;
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: { installations: [{ entry_id: entryId, current_version_hash: "none" }] },
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, { signal: AbortSignal.timeout(60_000) });
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<{ entryId: string; name: string; versionHash: string; installPath: string }> =
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({ entryId, name, versionHash: currentVersion.content_hash, installPath: targetDir });
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