Add sssf skill, installable via the skills CLI
Port the sssf skill from ~/.agents/skills/sssf into this repo so it can be distributed and installed with the skills CLI (skills add INDigitalStudio/skills --skill sssf). - Copy the skill (SKILL.md, cookbooks, references, scripts, templates, and the visualizer app source) into sssf/. - Gitignore build/runtime artifacts: the visualizer's node_modules/ and dist/, Python bytecode, and the machine-specific repos.json. - Make the skill location-independent: install.py now stamps the skill's real path into the stamped justfile's skill_dir (replacing the hardcoded ~/.agents/skills/sssf), so 'just obs' finds the visualizer wherever the CLI installed the skill. - Update cookbooks to use <skill>/scripts/... instead of the hardcoded path, and document the skills CLI install command. - Update the repo README with install instructions.
This commit is contained in:
parent
a42608f602
commit
2cc766aabe
98 changed files with 12508 additions and 0 deletions
146
sssf/apps/visualizer/server/repos.ts
Normal file
146
sssf/apps/visualizer/server/repos.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Repo registry for the multi-repo visualizer.
|
||||
*
|
||||
* The visualizer can serve several repos' trace DBs from one process. A repo
|
||||
* is a slug → sssf.db mapping, declared in a JSON file:
|
||||
*
|
||||
* [
|
||||
* { "slug": "tailsandstays", "name": "Tails & Stays",
|
||||
* "db": "/home/ima/projects/tailsandstays/adws/adw_data/sssf.db" },
|
||||
* { "slug": "mempalace", "name": "MemPalace",
|
||||
* "db": "/home/ima/projects/mempalace/adws/adw_data/sssf.db" }
|
||||
* ]
|
||||
*
|
||||
* Point the server at it with SSSF_REPOS=/path/to/repos.json. When SSSF_REPOS
|
||||
* is unset the server auto-discovers repos under a projects root (default
|
||||
* ~/projects, override with SSSF_PROJECTS_ROOT) by scanning for a trace db at
|
||||
* <root>/<dir>/adws/adw_data/sssf.db. If nothing is discovered it falls back to
|
||||
* the single-repo behaviour (--db / SSSF_DB / <cwd>/adws/adw_data/sssf.db)
|
||||
* under a slug derived from the repo dir name, so the original single-repo
|
||||
* deployment keeps working.
|
||||
*/
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { SssfDb, resolveDbPath } from "./db.ts";
|
||||
|
||||
export interface RepoEntry {
|
||||
slug: string;
|
||||
name: string;
|
||||
db: string;
|
||||
}
|
||||
|
||||
export interface RepoRuntime {
|
||||
slug: string;
|
||||
name: string;
|
||||
db: SssfDb;
|
||||
}
|
||||
|
||||
const SAFE_SLUG = /^[A-Za-z0-9._-]+$/;
|
||||
|
||||
function isSafeSlug(slug: string): boolean {
|
||||
return SAFE_SLUG.test(slug) && slug !== "." && slug !== "..";
|
||||
}
|
||||
|
||||
/** Derive a display slug from a db path: <repo>/adws/adw_data/sssf.db → repo dir name. */
|
||||
function slugFromDb(dbPath: string): string {
|
||||
// adw_data → adws → repo root
|
||||
const repoDir = dirname(dirname(dirname(dbPath)));
|
||||
const name = basename(repoDir);
|
||||
return name && isSafeSlug(name) ? name : "default";
|
||||
}
|
||||
|
||||
async function loadReposFile(path: string): Promise<RepoEntry[]> {
|
||||
const raw = Bun.file(path);
|
||||
if (!existsSync(path)) {
|
||||
throw new Error(`SSSF_REPOS file not found: ${path}`);
|
||||
}
|
||||
const data = JSON.parse(await raw.text()) as unknown;
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(`SSSF_REPOS file must be a JSON array of { slug, name, db }`);
|
||||
}
|
||||
const entries: RepoEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of data) {
|
||||
const entry = item as Partial<RepoEntry>;
|
||||
if (typeof entry.slug !== "string" || !isSafeSlug(entry.slug)) {
|
||||
throw new Error(`invalid repo slug: ${String(entry.slug)}`);
|
||||
}
|
||||
if (typeof entry.db !== "string") {
|
||||
throw new Error(`repo "${entry.slug}" is missing a db path`);
|
||||
}
|
||||
if (seen.has(entry.slug)) {
|
||||
throw new Error(`duplicate repo slug: ${entry.slug}`);
|
||||
}
|
||||
seen.add(entry.slug);
|
||||
const db = isAbsolute(entry.db) ? entry.db : resolve(dirname(path), entry.db);
|
||||
entries.push({
|
||||
slug: entry.slug,
|
||||
name: typeof entry.name === "string" && entry.name ? entry.name : entry.slug,
|
||||
db,
|
||||
});
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
throw new Error("SSSF_REPOS file declares no repos");
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-discover repos under a projects root by scanning for a trace db at
|
||||
* <root>/<dir>/adws/adw_data/sssf.db. Used when SSSF_REPOS is unset so new
|
||||
* repos appear without editing a registry file. The root defaults to
|
||||
* ~/projects and can be overridden with SSSF_PROJECTS_ROOT.
|
||||
*/
|
||||
function discoverRepos(root: string): RepoEntry[] {
|
||||
if (!existsSync(root)) {
|
||||
return [];
|
||||
}
|
||||
const entries: RepoEntry[] = [];
|
||||
for (const name of readdirSync(root, { withFileTypes: true })) {
|
||||
if (!name.isDirectory() || name.name.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
const db = join(root, name.name, "adws", "adw_data", "sssf.db");
|
||||
if (existsSync(db) && isSafeSlug(name.name)) {
|
||||
entries.push({ slug: name.name, name: name.name, db });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the repo registry. The first repo is served under the legacy
|
||||
* unprefixed /api routes (the single repo in single-repo mode, or the first
|
||||
* repo in multi-repo mode).
|
||||
*/
|
||||
export async function buildRepos(): Promise<{ repos: RepoRuntime[] }> {
|
||||
const reposFile = process.env.SSSF_REPOS;
|
||||
const entries: RepoEntry[] = reposFile
|
||||
? await loadReposFile(reposFile)
|
||||
: discoverRepos(process.env.SSSF_PROJECTS_ROOT ?? join(homedir(), "projects"));
|
||||
if (entries.length === 0) {
|
||||
// No discovered repos — fall back to the single-repo behaviour so the
|
||||
// original deployment (--db / SSSF_DB / <cwd>/adws/adw_data/sssf.db) works.
|
||||
entries.push({ slug: slugFromDb(resolveDbPath()), name: "", db: resolveDbPath() });
|
||||
}
|
||||
|
||||
const repos: RepoRuntime[] = [];
|
||||
for (const entry of entries) {
|
||||
let db: SssfDb;
|
||||
try {
|
||||
db = new SssfDb(entry.db);
|
||||
} catch (error) {
|
||||
console.error(`[sssf] repo "${entry.slug}": ${(error as Error).message}`);
|
||||
continue;
|
||||
}
|
||||
repos.push({ slug: entry.slug, name: entry.name, db });
|
||||
}
|
||||
|
||||
if (repos.length === 0) {
|
||||
console.error("[sssf] no repos could be opened — exiting");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { repos };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue