@heart-of-gold/toolkit 0.1.37 → 0.1.38

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.
@@ -15,19 +15,19 @@
15
15
  "name": "deep-thought",
16
16
  "source": "./plugins/deep-thought",
17
17
  "description": "The Answer Computer — reasoning tools for brainstorming, planning, and deep thinking",
18
- "version": "0.2.7"
18
+ "version": "0.2.8"
19
19
  },
20
20
  {
21
21
  "name": "marvin",
22
22
  "source": "./plugins/marvin",
23
23
  "description": "The Paranoid Android — quality tools for code review, knowledge compounding, and work execution",
24
- "version": "0.3.7"
24
+ "version": "0.3.8"
25
25
  },
26
26
  {
27
27
  "name": "babel-fish",
28
28
  "source": "./plugins/babel-fish",
29
29
  "description": "Universal Translator — media generation tools for audio, image, and video content",
30
- "version": "0.2.5"
30
+ "version": "0.2.6"
31
31
  },
32
32
  {
33
33
  "name": "quellis",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heart-of-gold/toolkit",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "description": "Cross-platform installer for Heart of Gold skills — works with Codex, OpenCode, Pi, Claude Code, and more",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babel-fish",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Universal Translator — media generation tools for audio, image, video, and visualization",
5
5
  "author": {
6
6
  "name": "ondrej-svec",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deep-thought",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "The Answer Computer — reasoning tools for brainstorming, planning, architecture design, and deep thinking",
5
5
  "author": {
6
6
  "name": "ondrej-svec",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marvin",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "The Paranoid Android — quality tools for code review, knowledge compounding, and work execution",
5
5
  "author": {
6
6
  "name": "ondrej-svec",
@@ -59,6 +59,16 @@ bash scripts/enable-viewer.sh --public-base-url "https://<machine>.<tailnet>.ts.
59
59
  bash scripts/disable-viewer.sh
60
60
  ```
61
61
 
62
+ ### Delete a published artifact by slug
63
+ ```bash
64
+ heart-of-gold share-server delete <slug>
65
+ ```
66
+
67
+ ### Delete only an alias pointer
68
+ ```bash
69
+ heart-of-gold share-server delete --alias <alias> --onlyAlias
70
+ ```
71
+
62
72
  ## Notes
63
73
 
64
74
  - In v1, lifecycle control assumes the reference server was installed as a macOS LaunchAgent.
@@ -73,6 +83,8 @@ bash scripts/disable-viewer.sh
73
83
  - `scripts/restart.sh` — restart the LaunchAgent cleanly
74
84
  - `scripts/enable-viewer.sh` — enable private Tailscale Serve exposure for the viewer listener
75
85
  - `scripts/disable-viewer.sh` — turn off private Tailscale Serve exposure
86
+ - `heart-of-gold share-server delete <slug>` — remove a published artifact, its aliases, and its metadata entries
87
+ - `heart-of-gold share-server delete --alias <alias> --onlyAlias` — remove only an alias pointer
76
88
 
77
89
  ## References
78
90
 
@@ -59,6 +59,18 @@ bun share-server/src/index.ts init
59
59
  bun share-server/src/index.ts health
60
60
  ```
61
61
 
62
+ ### Delete a published share
63
+
64
+ ```bash
65
+ heart-of-gold share-server delete <slug>
66
+ ```
67
+
68
+ ### Delete only an alias pointer
69
+
70
+ ```bash
71
+ heart-of-gold share-server delete --alias <alias> --onlyAlias
72
+ ```
73
+
62
74
  ### Install stable local server files
63
75
 
64
76
  ```bash
@@ -93,6 +105,19 @@ curl -fsS -X POST \
93
105
  http://127.0.0.1:4815/publish
94
106
  ```
95
107
 
108
+ ## Cleanup model
109
+
110
+ Artifacts are immutable when published, but the admin surface can remove them when cleanup is needed.
111
+
112
+ Supported cleanup operations:
113
+ - delete a share by slug
114
+ - delete an alias pointer without deleting the underlying artifact
115
+
116
+ Deleting a share removes:
117
+ - the artifact directory
118
+ - any aliases pointing at that slug
119
+ - matching metadata entries in `metadata/shares.jsonl`
120
+
96
121
  ## Security model
97
122
 
98
123
  - publish/admin API binds to localhost only
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from "node:path";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { ensureConfigAndDataDirs, loadConfig, resolveConfigPath, writeConfig } from "./config";
6
6
  import { publishArtifact } from "./publish";
7
- import { ensureStorageLayout } from "./storage";
7
+ import { deleteAlias, deleteArtifact, ensureStorageLayout, readAlias, readMetadata, removeAliasesForSlug, rewriteMetadata } from "./storage";
8
8
  import { createViewerHandler } from "./viewer";
9
9
  import type { HealthResponse } from "./types";
10
10
 
@@ -92,14 +92,7 @@ async function startServer(flags: Record<string, string | boolean>): Promise<voi
92
92
  }
93
93
 
94
94
  if (request.method === "GET" && url.pathname === "/shares") {
95
- const sharesPath = join(dataRoot, "metadata", "shares.jsonl");
96
- const text = existsSync(sharesPath) ? await Bun.file(sharesPath).text() : "";
97
- const items = text
98
- .split(/\r?\n/)
99
- .map((line) => line.trim())
100
- .filter(Boolean)
101
- .map((line) => JSON.parse(line));
102
- return Response.json({ ok: true, items });
95
+ return Response.json({ ok: true, items: readMetadata(dataRoot) });
103
96
  }
104
97
 
105
98
  if (request.method === "POST" && url.pathname === "/publish") {
@@ -127,6 +120,39 @@ async function startServer(flags: Record<string, string | boolean>): Promise<voi
127
120
  }
128
121
  }
129
122
 
123
+ if (request.method === "DELETE" && url.pathname.startsWith("/shares/")) {
124
+ const slug = decodeURIComponent(url.pathname.replace(/^\/shares\//, "")).trim();
125
+ if (!slug) {
126
+ return Response.json({ ok: false, error: { code: "MISSING_SLUG", message: "Share slug is required." } }, { status: 400 });
127
+ }
128
+
129
+ const removedArtifact = deleteArtifact(dataRoot, slug);
130
+ const removedAliases = removeAliasesForSlug(dataRoot, slug);
131
+ const existing = readMetadata(dataRoot);
132
+ const filtered = existing.filter((record) => record.slug !== slug);
133
+ const metadataRemoved = filtered.length !== existing.length;
134
+ rewriteMetadata(dataRoot, filtered);
135
+
136
+ if (!removedArtifact && !metadataRemoved && removedAliases.length === 0) {
137
+ return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `Share not found: ${slug}` } }, { status: 404 });
138
+ }
139
+
140
+ return Response.json({ ok: true, slug, removedArtifact, removedAliases, metadataRemoved });
141
+ }
142
+
143
+ if (request.method === "DELETE" && url.pathname.startsWith("/aliases/")) {
144
+ const alias = decodeURIComponent(url.pathname.replace(/^\/aliases\//, "")).trim();
145
+ if (!alias) {
146
+ return Response.json({ ok: false, error: { code: "MISSING_ALIAS", message: "Alias is required." } }, { status: 400 });
147
+ }
148
+ const slug = readAlias(dataRoot, alias);
149
+ const removed = deleteAlias(dataRoot, alias);
150
+ if (!removed) {
151
+ return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `Alias not found: ${alias}` } }, { status: 404 });
152
+ }
153
+ return Response.json({ ok: true, alias, slug, removed: true });
154
+ }
155
+
130
156
  return Response.json({ ok: false, error: { code: "INVALID_REQUEST", message: "Unknown route" } }, { status: 404 });
131
157
  },
132
158
  });
@@ -39,23 +39,81 @@ export function artifactPath(dataRoot: string, slug: string): string {
39
39
  return join(artifactsDir(dataRoot), slug);
40
40
  }
41
41
 
42
+ export function metadataFilePath(dataRoot: string): string {
43
+ return join(metadataDir(dataRoot), "shares.jsonl");
44
+ }
45
+
42
46
  export function writeMetadata(dataRoot: string, record: ShareRecord): void {
43
- const filePath = join(metadataDir(dataRoot), "shares.jsonl");
47
+ const filePath = metadataFilePath(dataRoot);
44
48
  writeFileSync(filePath, `${JSON.stringify(record)}\n`, { flag: "a" });
45
49
  }
46
50
 
51
+ export function readMetadata(dataRoot: string): ShareRecord[] {
52
+ const filePath = metadataFilePath(dataRoot);
53
+ if (!existsSync(filePath)) return [];
54
+ return readFileSync(filePath, "utf-8")
55
+ .split(/\r?\n/)
56
+ .map((line) => line.trim())
57
+ .filter(Boolean)
58
+ .map((line) => JSON.parse(line) as ShareRecord);
59
+ }
60
+
61
+ export function rewriteMetadata(dataRoot: string, records: ShareRecord[]): void {
62
+ const filePath = metadataFilePath(dataRoot);
63
+ const content = records.map((record) => JSON.stringify(record)).join("\n");
64
+ writeFileSync(filePath, `${content}${content ? "\n" : ""}`, "utf-8");
65
+ }
66
+
47
67
  export function writeAlias(dataRoot: string, alias: string, slug: string): void {
48
68
  const aliasPath = join(aliasesDir(dataRoot), `${slugify(alias)}.json`);
49
69
  writeFileSync(aliasPath, `${JSON.stringify({ alias: slugify(alias), slug }, null, 2)}\n`, "utf-8");
50
70
  }
51
71
 
72
+ export function aliasPath(dataRoot: string, alias: string): string {
73
+ return join(aliasesDir(dataRoot), `${slugify(alias)}.json`);
74
+ }
75
+
52
76
  export function readAlias(dataRoot: string, alias: string): string | null {
53
- const aliasPath = join(aliasesDir(dataRoot), `${slugify(alias)}.json`);
54
- if (!existsSync(aliasPath)) return null;
55
- const parsed = JSON.parse(readFileSync(aliasPath, "utf-8")) as { slug?: string };
77
+ const filePath = aliasPath(dataRoot, alias);
78
+ if (!existsSync(filePath)) return null;
79
+ const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as { slug?: string };
56
80
  return parsed.slug ?? null;
57
81
  }
58
82
 
83
+ export function deleteAlias(dataRoot: string, alias: string): boolean {
84
+ const filePath = aliasPath(dataRoot, alias);
85
+ if (!existsSync(filePath)) return false;
86
+ rmSync(filePath, { force: true });
87
+ return true;
88
+ }
89
+
90
+ export function removeAliasesForSlug(dataRoot: string, slug: string): string[] {
91
+ const removed: string[] = [];
92
+ const root = aliasesDir(dataRoot);
93
+ if (!existsSync(root)) return removed;
94
+ for (const entry of readdirSync(root)) {
95
+ if (!entry.endsWith('.json')) continue;
96
+ const filePath = join(root, entry);
97
+ try {
98
+ const parsed = JSON.parse(readFileSync(filePath, 'utf-8')) as { alias?: string; slug?: string };
99
+ if (parsed.slug === slug) {
100
+ rmSync(filePath, { force: true });
101
+ removed.push(parsed.alias ?? entry.replace(/\.json$/, ''));
102
+ }
103
+ } catch {
104
+ // ignore malformed alias records during cleanup
105
+ }
106
+ }
107
+ return removed;
108
+ }
109
+
110
+ export function deleteArtifact(dataRoot: string, slug: string): boolean {
111
+ const path = artifactPath(dataRoot, slug);
112
+ if (!existsSync(path)) return false;
113
+ rmSync(path, { recursive: true, force: true });
114
+ return true;
115
+ }
116
+
59
117
  export function copyDirectoryContents(sourceDir: string, destinationDir: string): void {
60
118
  mkdirSync(destinationDir, { recursive: true });
61
119
  for (const entry of readdirSync(sourceDir)) {
@@ -2,6 +2,8 @@ import { defineCommand } from "citty";
2
2
  import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
+ import { dataRootForConfig } from "../../share-server/src/config";
6
+ import { deleteAlias, deleteArtifact, readAlias, readMetadata, removeAliasesForSlug, rewriteMetadata } from "../../share-server/src/storage";
5
7
 
6
8
  function packageRoot(): string {
7
9
  let dir = dirname(new URL(import.meta.url).pathname);
@@ -86,6 +88,48 @@ export const shareServerCommand = defineCommand({
86
88
  console.log(JSON.stringify({ ok: true, serverDir: targetDir }, null, 2));
87
89
  },
88
90
  }),
91
+ delete: defineCommand({
92
+ meta: { name: "delete", description: "Delete a published share by slug, or remove an alias" },
93
+ args: {
94
+ slug: { type: "positional", required: false },
95
+ alias: { type: "string", required: false },
96
+ config: { type: "string", required: false },
97
+ onlyAlias: { type: "boolean", required: false },
98
+ },
99
+ run({ args }) {
100
+ const dataRoot = dataRootForConfig(args.config ? String(args.config) : undefined);
101
+
102
+ if (args.alias && args.onlyAlias) {
103
+ const alias = String(args.alias);
104
+ const slug = readAlias(dataRoot, alias);
105
+ const removed = deleteAlias(dataRoot, alias);
106
+ console.log(JSON.stringify(removed
107
+ ? { ok: true, alias, slug, removed: true }
108
+ : { ok: false, error: { code: "NOT_FOUND", message: `Alias not found: ${alias}` } }
109
+ ));
110
+ process.exit(removed ? 0 : 1);
111
+ }
112
+
113
+ const slug = args.slug ? String(args.slug) : undefined;
114
+ if (!slug) {
115
+ console.error("share-server delete requires either <slug> or --alias <alias> --onlyAlias");
116
+ process.exit(1);
117
+ }
118
+
119
+ const removedArtifact = deleteArtifact(dataRoot, slug);
120
+ const removedAliases = removeAliasesForSlug(dataRoot, slug);
121
+ const existing = readMetadata(dataRoot);
122
+ const filtered = existing.filter((record) => record.slug !== slug);
123
+ const metadataRemoved = filtered.length !== existing.length;
124
+ rewriteMetadata(dataRoot, filtered);
125
+ const ok = removedArtifact || metadataRemoved || removedAliases.length > 0;
126
+ console.log(JSON.stringify(ok
127
+ ? { ok: true, slug, removedArtifact, removedAliases, metadataRemoved }
128
+ : { ok: false, error: { code: "NOT_FOUND", message: `Share not found: ${slug}` } }
129
+ ));
130
+ process.exit(ok ? 0 : 1);
131
+ },
132
+ }),
89
133
  "install-launch-agent": defineCommand({
90
134
  meta: { name: "install-launch-agent", description: "Write a macOS LaunchAgent for the installed reference server" },
91
135
  args: {