@heart-of-gold/toolkit 0.1.37 → 0.1.39
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/.claude-plugin/marketplace.json +3 -3
- package/package.json +1 -1
- package/plugins/babel-fish/.claude-plugin/plugin.json +1 -1
- package/plugins/babel-fish/skills/visualize/SKILL.md +7 -0
- package/plugins/babel-fish/skills/visualize/scripts/agent-artifact-template.html +450 -79
- package/plugins/deep-thought/.claude-plugin/plugin.json +1 -1
- package/plugins/marvin/.claude-plugin/plugin.json +1 -1
- package/plugins/marvin/skills/share-server-control/SKILL.md +12 -0
- package/share-server/README.md +25 -0
- package/share-server/src/index.ts +35 -9
- package/share-server/src/storage.ts +62 -4
- package/src/commands/share-server.ts +44 -0
|
@@ -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.
|
|
18
|
+
"version": "0.2.9"
|
|
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.
|
|
24
|
+
"version": "0.3.9"
|
|
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.
|
|
30
|
+
"version": "0.2.7"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"name": "quellis",
|
package/package.json
CHANGED
|
@@ -116,6 +116,8 @@ Apply these defaults unless the user asks for something else:
|
|
|
116
116
|
- readable max-widths for prose
|
|
117
117
|
- sticky navigation only when it helps, never as the dominant element
|
|
118
118
|
- polished but restrained effects; no gimmicky AI-demo chrome
|
|
119
|
+
- use the shared theme-ready scaffold with curated palette tokens rather than ad hoc colors
|
|
120
|
+
- default to a calm Rosé Pine-inspired palette that works well for both dark and light modes
|
|
119
121
|
|
|
120
122
|
See also:
|
|
121
123
|
- `docs/architecture/visualize-design-rules.md`
|
|
@@ -376,6 +378,7 @@ Harness note:
|
|
|
376
378
|
- prefer the Node helpers for cross-platform behavior
|
|
377
379
|
- avoid assuming `mktemp /tmp/name-XXXXXX.html` works on every shell; use the provided helper instead
|
|
378
380
|
- if a harness only supports shell comfortably, use the `.sh` helpers as fallback
|
|
381
|
+
- the shared scaffold already includes a light/dark/auto theme toggle and responsive viewport behavior; preserve those unless the user explicitly wants something else
|
|
379
382
|
|
|
380
383
|
For plans specifically:
|
|
381
384
|
- do not dump the full task prose into the primary lanes
|
|
@@ -450,6 +453,8 @@ When the artifact still feels too markdown-like, do one or more of these:
|
|
|
450
453
|
- surface one key takeaway per section before the detail
|
|
451
454
|
- split `what / why / risk / next` into separate visual units
|
|
452
455
|
- move citations, raw notes, and source text into a secondary appendix
|
|
456
|
+
- reduce density before widening the layout
|
|
457
|
+
- stack or split crowded regions rather than cramming more into one viewport row
|
|
453
458
|
|
|
454
459
|
## Guideline Authoring Workflow
|
|
455
460
|
|
|
@@ -474,6 +479,8 @@ Before returning a shared HTML result, check mentally:
|
|
|
474
479
|
- If this is a brainstorm, is it actually branch-shaped enough for a mind map?
|
|
475
480
|
- Do the section titles help scanning?
|
|
476
481
|
- Is secondary detail actually secondary?
|
|
482
|
+
- Does the current viewport feel comfortable, or is content being cramped into too few rows?
|
|
483
|
+
- Does the page work in both dark and light mode unless intentionally single-theme?
|
|
477
484
|
|
|
478
485
|
If the answer to several of these is no, reconsider the mode or ask the user.
|
|
479
486
|
|
|
@@ -1,121 +1,453 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
2
|
+
<html lang="en" data-theme="auto">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="theme-color" content="#1f1d2e" />
|
|
6
7
|
<title>Heart of Gold Visual Artifact</title>
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
9
|
-
--
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
10
|
+
--radius-2xl: 28px;
|
|
11
|
+
--radius-xl: 22px;
|
|
12
|
+
--radius-lg: 18px;
|
|
13
|
+
--radius-md: 14px;
|
|
14
|
+
--shadow-lg: 0 22px 80px rgba(0, 0, 0, 0.28);
|
|
15
|
+
--shadow-md: 0 14px 36px rgba(0, 0, 0, 0.16);
|
|
16
|
+
--space-1: 8px;
|
|
17
|
+
--space-2: 12px;
|
|
18
|
+
--space-3: 16px;
|
|
19
|
+
--space-4: 20px;
|
|
20
|
+
--space-5: 24px;
|
|
21
|
+
--space-6: 32px;
|
|
22
|
+
--space-7: 40px;
|
|
23
|
+
--max-main: 1480px;
|
|
24
|
+
--body-max: 76ch;
|
|
25
|
+
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
26
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
:root,
|
|
30
|
+
:root[data-theme="dark"] {
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
--bg: #191724;
|
|
33
|
+
--bg-elevated: #1f1d2e;
|
|
34
|
+
--bg-soft: #26233a;
|
|
35
|
+
--panel: rgba(49, 46, 68, 0.78);
|
|
36
|
+
--panel-strong: rgba(38, 35, 58, 0.94);
|
|
37
|
+
--panel-soft: rgba(255, 255, 255, 0.04);
|
|
38
|
+
--text: #e0def4;
|
|
39
|
+
--text-strong: #f6f3ff;
|
|
40
|
+
--muted: #b4b0d0;
|
|
41
|
+
--muted-2: #908caa;
|
|
42
|
+
--border: rgba(224, 222, 244, 0.12);
|
|
43
|
+
--accent: #c4a7e7;
|
|
44
|
+
--accent-2: #9ccfd8;
|
|
45
|
+
--accent-3: #f6c177;
|
|
46
|
+
--success: #9ccfd8;
|
|
47
|
+
--warn: #f6c177;
|
|
48
|
+
--danger: #eb6f92;
|
|
49
|
+
--surface-glow-a: rgba(196, 167, 231, 0.18);
|
|
50
|
+
--surface-glow-b: rgba(156, 207, 216, 0.12);
|
|
51
|
+
--link: #e0def4;
|
|
52
|
+
--hero-border: rgba(224, 222, 244, 0.14);
|
|
53
|
+
--hero-overlay: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:root[data-theme="light"] {
|
|
57
|
+
color-scheme: light;
|
|
58
|
+
--bg: #faf4ed;
|
|
59
|
+
--bg-elevated: #fffaf3;
|
|
60
|
+
--bg-soft: #f2e9e1;
|
|
61
|
+
--panel: rgba(255, 250, 243, 0.86);
|
|
62
|
+
--panel-strong: rgba(255, 250, 243, 0.98);
|
|
63
|
+
--panel-soft: rgba(86, 61, 56, 0.04);
|
|
64
|
+
--text: #575279;
|
|
65
|
+
--text-strong: #2a273f;
|
|
66
|
+
--muted: #6e6a86;
|
|
67
|
+
--muted-2: #797593;
|
|
68
|
+
--border: rgba(87, 82, 121, 0.12);
|
|
69
|
+
--accent: #907aa9;
|
|
70
|
+
--accent-2: #56949f;
|
|
71
|
+
--accent-3: #ea9d34;
|
|
72
|
+
--success: #56949f;
|
|
73
|
+
--warn: #d7827e;
|
|
74
|
+
--danger: #b4637a;
|
|
75
|
+
--surface-glow-a: rgba(144, 122, 169, 0.12);
|
|
76
|
+
--surface-glow-b: rgba(86, 148, 159, 0.08);
|
|
77
|
+
--link: #3b3756;
|
|
78
|
+
--hero-border: rgba(87, 82, 121, 0.12);
|
|
79
|
+
--hero-overlay: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.74));
|
|
80
|
+
--shadow-lg: 0 18px 44px rgba(87, 82, 121, 0.1);
|
|
81
|
+
--shadow-md: 0 10px 24px rgba(87, 82, 121, 0.08);
|
|
82
|
+
}
|
|
83
|
+
|
|
27
84
|
* { box-sizing: border-box; }
|
|
28
|
-
html
|
|
29
|
-
html {
|
|
85
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
86
|
+
html, body { margin: 0; padding: 0; min-height: 100%; }
|
|
30
87
|
body {
|
|
31
|
-
font-family:
|
|
32
|
-
line-height: 1.
|
|
88
|
+
font-family: var(--font-sans);
|
|
89
|
+
line-height: 1.6;
|
|
33
90
|
color: var(--text);
|
|
34
91
|
background:
|
|
35
|
-
radial-gradient(circle at top left,
|
|
36
|
-
radial-gradient(circle at top right,
|
|
37
|
-
linear-gradient(180deg, var(--bg-
|
|
92
|
+
radial-gradient(circle at top left, var(--surface-glow-a), transparent 30%),
|
|
93
|
+
radial-gradient(circle at top right, var(--surface-glow-b), transparent 25%),
|
|
94
|
+
linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg) 100%);
|
|
95
|
+
text-rendering: optimizeLegibility;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
a {
|
|
99
|
+
color: var(--link);
|
|
100
|
+
text-decoration-thickness: 1px;
|
|
101
|
+
text-underline-offset: 0.18em;
|
|
38
102
|
}
|
|
39
|
-
|
|
40
|
-
|
|
103
|
+
|
|
104
|
+
a:hover { text-decoration-thickness: 2px; }
|
|
105
|
+
|
|
106
|
+
button {
|
|
107
|
+
font: inherit;
|
|
108
|
+
color: inherit;
|
|
109
|
+
background: none;
|
|
110
|
+
border: 0;
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
button:focus-visible,
|
|
115
|
+
a:focus-visible,
|
|
116
|
+
summary:focus-visible {
|
|
117
|
+
outline: 2px solid var(--accent-2);
|
|
118
|
+
outline-offset: 3px;
|
|
119
|
+
border-radius: 12px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main {
|
|
123
|
+
width: min(var(--max-main), calc(100vw - 24px));
|
|
124
|
+
margin: 0 auto;
|
|
125
|
+
padding: clamp(18px, 3vw, 36px) clamp(10px, 2vw, 22px) calc(72px + env(safe-area-inset-bottom));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.topbar {
|
|
129
|
+
display: flex;
|
|
130
|
+
justify-content: flex-end;
|
|
131
|
+
margin-bottom: var(--space-3);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.theme-toggle {
|
|
135
|
+
display: inline-flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 10px;
|
|
138
|
+
padding: 10px 14px;
|
|
139
|
+
border-radius: 999px;
|
|
41
140
|
border: 1px solid var(--border);
|
|
42
|
-
|
|
141
|
+
background: var(--panel-soft);
|
|
142
|
+
box-shadow: var(--shadow-md);
|
|
143
|
+
color: var(--muted);
|
|
43
144
|
backdrop-filter: blur(14px);
|
|
44
145
|
}
|
|
45
|
-
|
|
146
|
+
|
|
147
|
+
.theme-toggle strong {
|
|
148
|
+
color: var(--text-strong);
|
|
149
|
+
font-size: 13px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.hero,
|
|
154
|
+
.section,
|
|
155
|
+
.rail-card,
|
|
156
|
+
.card,
|
|
157
|
+
.stat,
|
|
158
|
+
.callout,
|
|
159
|
+
.lane,
|
|
160
|
+
.chip {
|
|
161
|
+
border: 1px solid var(--border);
|
|
162
|
+
box-shadow: var(--shadow-lg);
|
|
163
|
+
backdrop-filter: blur(16px);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.hero,
|
|
167
|
+
.section,
|
|
168
|
+
.rail-card {
|
|
169
|
+
background: var(--panel);
|
|
170
|
+
}
|
|
171
|
+
|
|
46
172
|
.hero {
|
|
47
|
-
|
|
173
|
+
position: relative;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
padding: clamp(20px, 3vw, 32px);
|
|
48
176
|
border-radius: 32px;
|
|
49
|
-
margin-bottom:
|
|
177
|
+
margin-bottom: var(--space-5);
|
|
50
178
|
display: grid;
|
|
51
|
-
gap:
|
|
179
|
+
gap: var(--space-3);
|
|
180
|
+
background-image: var(--hero-overlay);
|
|
181
|
+
border-color: var(--hero-border);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.hero::after {
|
|
185
|
+
content: "";
|
|
186
|
+
position: absolute;
|
|
187
|
+
inset: auto -10% -50% auto;
|
|
188
|
+
width: 280px;
|
|
189
|
+
height: 280px;
|
|
190
|
+
border-radius: 999px;
|
|
191
|
+
background: radial-gradient(circle, var(--surface-glow-a), transparent 68%);
|
|
192
|
+
pointer-events: none;
|
|
52
193
|
}
|
|
53
|
-
|
|
194
|
+
|
|
195
|
+
.eyebrow,
|
|
196
|
+
.label {
|
|
54
197
|
color: var(--accent-2);
|
|
55
198
|
text-transform: uppercase;
|
|
56
|
-
letter-spacing: 0.
|
|
199
|
+
letter-spacing: 0.14em;
|
|
57
200
|
font-size: 12px;
|
|
58
201
|
font-weight: 700;
|
|
59
202
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
203
|
+
|
|
204
|
+
h1, h2, h3 {
|
|
205
|
+
margin: 0;
|
|
206
|
+
color: var(--text-strong);
|
|
207
|
+
text-wrap: balance;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
h1 {
|
|
211
|
+
font-size: clamp(2rem, 5vw, 4.3rem);
|
|
212
|
+
line-height: 0.98;
|
|
213
|
+
max-width: 14ch;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
h2 {
|
|
217
|
+
font-size: clamp(1.45rem, 2.3vw, 2.1rem);
|
|
218
|
+
line-height: 1.08;
|
|
219
|
+
margin-bottom: var(--space-2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
h3 {
|
|
223
|
+
font-size: clamp(1.05rem, 1.6vw, 1.25rem);
|
|
224
|
+
line-height: 1.18;
|
|
225
|
+
margin-bottom: var(--space-1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
p {
|
|
229
|
+
margin: 0;
|
|
230
|
+
color: var(--muted);
|
|
231
|
+
max-width: var(--body-max);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.hero p {
|
|
235
|
+
font-size: clamp(1rem, 1.3vw, 1.1rem);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.page {
|
|
239
|
+
display: grid;
|
|
240
|
+
grid-template-columns: minmax(0, 1fr) minmax(250px, 310px);
|
|
241
|
+
gap: var(--space-5);
|
|
242
|
+
align-items: start;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.content {
|
|
246
|
+
min-width: 0;
|
|
247
|
+
container-type: inline-size;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.rail {
|
|
251
|
+
position: sticky;
|
|
252
|
+
top: 16px;
|
|
253
|
+
display: grid;
|
|
254
|
+
gap: var(--space-3);
|
|
255
|
+
min-width: 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.rail-card,
|
|
259
|
+
.section {
|
|
260
|
+
padding: clamp(18px, 2vw, 24px);
|
|
261
|
+
border-radius: var(--radius-2xl);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.section {
|
|
265
|
+
margin-bottom: var(--space-4);
|
|
266
|
+
overflow: clip;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.stack { display: grid; gap: var(--space-3); }
|
|
270
|
+
.grid-2,
|
|
271
|
+
.grid-3,
|
|
272
|
+
.grid-4,
|
|
273
|
+
.lane-grid {
|
|
274
|
+
display: grid;
|
|
275
|
+
gap: var(--space-3);
|
|
276
|
+
min-width: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
70
279
|
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
71
280
|
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
72
281
|
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
73
|
-
.lane-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
282
|
+
.lane-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
|
|
283
|
+
|
|
284
|
+
.card,
|
|
285
|
+
.stat,
|
|
286
|
+
.lane,
|
|
287
|
+
.callout {
|
|
288
|
+
min-width: 0;
|
|
289
|
+
padding: clamp(16px, 1.6vw, 20px);
|
|
290
|
+
border-radius: var(--radius-lg);
|
|
77
291
|
background: var(--panel-soft);
|
|
78
292
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
293
|
+
|
|
294
|
+
.card > p,
|
|
295
|
+
.lane > p,
|
|
296
|
+
.callout > p,
|
|
297
|
+
.rail-card > p {
|
|
298
|
+
max-width: 62ch;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.stat .value {
|
|
302
|
+
margin-top: 6px;
|
|
303
|
+
color: var(--text-strong);
|
|
304
|
+
font-size: clamp(1.8rem, 3vw, 2.2rem);
|
|
305
|
+
font-weight: 700;
|
|
306
|
+
font-variant-numeric: tabular-nums;
|
|
85
307
|
}
|
|
86
|
-
|
|
87
|
-
.stat .hint {
|
|
88
|
-
|
|
308
|
+
|
|
309
|
+
.stat .hint {
|
|
310
|
+
margin-top: 6px;
|
|
311
|
+
color: var(--muted-2);
|
|
312
|
+
font-size: 13px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.chip-row {
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-wrap: wrap;
|
|
318
|
+
gap: var(--space-1);
|
|
319
|
+
margin-top: var(--space-2);
|
|
320
|
+
min-width: 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
89
323
|
.chip {
|
|
90
|
-
display: inline-flex;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
324
|
+
display: inline-flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: 6px;
|
|
327
|
+
max-width: 100%;
|
|
328
|
+
padding: 8px 11px;
|
|
329
|
+
border-radius: 999px;
|
|
330
|
+
background: color-mix(in srgb, var(--panel-strong) 70%, transparent);
|
|
331
|
+
color: var(--muted-2);
|
|
332
|
+
font-size: 12px;
|
|
333
|
+
box-shadow: var(--shadow-md);
|
|
334
|
+
white-space: nowrap;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.chip.good { color: var(--success); }
|
|
338
|
+
.chip.warn { color: var(--warn); }
|
|
339
|
+
.chip.danger { color: var(--danger); }
|
|
340
|
+
|
|
341
|
+
.callout {
|
|
342
|
+
background: color-mix(in srgb, var(--accent) 8%, var(--panel-soft));
|
|
343
|
+
border-color: color-mix(in srgb, var(--accent) 22%, var(--border));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
details {
|
|
347
|
+
margin-top: var(--space-2);
|
|
348
|
+
min-width: 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
summary {
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
font-weight: 600;
|
|
354
|
+
color: var(--text-strong);
|
|
355
|
+
}
|
|
356
|
+
|
|
98
357
|
.raw {
|
|
99
|
-
margin-top:
|
|
100
|
-
|
|
101
|
-
|
|
358
|
+
margin-top: var(--space-2);
|
|
359
|
+
padding: 14px;
|
|
360
|
+
border-radius: var(--radius-md);
|
|
361
|
+
border: 1px solid var(--border);
|
|
362
|
+
background: color-mix(in srgb, var(--bg-soft) 70%, transparent);
|
|
363
|
+
color: var(--text);
|
|
364
|
+
white-space: pre-wrap;
|
|
365
|
+
overflow-wrap: anywhere;
|
|
366
|
+
font: 13px/1.55 var(--font-mono);
|
|
102
367
|
}
|
|
103
|
-
|
|
104
|
-
ul
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
368
|
+
|
|
369
|
+
ul,
|
|
370
|
+
ol {
|
|
371
|
+
margin: 0;
|
|
372
|
+
padding-left: 18px;
|
|
373
|
+
color: var(--muted);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
li + li { margin-top: 6px; }
|
|
377
|
+
|
|
378
|
+
code {
|
|
379
|
+
font-family: var(--font-mono);
|
|
380
|
+
font-size: 0.94em;
|
|
381
|
+
background: color-mix(in srgb, var(--bg-soft) 65%, transparent);
|
|
382
|
+
padding: 0.15em 0.4em;
|
|
383
|
+
border-radius: 8px;
|
|
384
|
+
overflow-wrap: anywhere;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.viewport-note {
|
|
388
|
+
margin-top: var(--space-2);
|
|
389
|
+
color: var(--muted-2);
|
|
390
|
+
font-size: 13px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@container (max-width: 860px) {
|
|
108
394
|
.grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
395
|
+
.grid-3 { grid-template-columns: 1fr; }
|
|
396
|
+
.grid-2 { grid-template-columns: 1fr; }
|
|
109
397
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.
|
|
113
|
-
.
|
|
398
|
+
|
|
399
|
+
@media (max-width: 1180px) {
|
|
400
|
+
.page { grid-template-columns: 1fr; }
|
|
401
|
+
.rail {
|
|
402
|
+
position: static;
|
|
403
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@media (max-width: 760px) {
|
|
408
|
+
main {
|
|
409
|
+
width: min(var(--max-main), calc(100vw - 8px));
|
|
410
|
+
padding: 14px 4px calc(60px + env(safe-area-inset-bottom));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.topbar {
|
|
414
|
+
justify-content: stretch;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.theme-toggle {
|
|
418
|
+
width: 100%;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.hero,
|
|
423
|
+
.section,
|
|
424
|
+
.rail-card {
|
|
425
|
+
border-radius: 22px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.grid-4,
|
|
429
|
+
.grid-3,
|
|
430
|
+
.grid-2,
|
|
431
|
+
.lane-grid,
|
|
432
|
+
.rail {
|
|
433
|
+
grid-template-columns: 1fr;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.chip {
|
|
437
|
+
white-space: normal;
|
|
438
|
+
}
|
|
114
439
|
}
|
|
115
440
|
</style>
|
|
116
441
|
</head>
|
|
117
442
|
<body>
|
|
118
443
|
<main>
|
|
444
|
+
<div class="topbar">
|
|
445
|
+
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Toggle light and dark theme">
|
|
446
|
+
<strong>Theme</strong>
|
|
447
|
+
<span id="theme-label">Auto</span>
|
|
448
|
+
</button>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
119
451
|
<section class="hero">
|
|
120
452
|
<div class="eyebrow">Artifact Family</div>
|
|
121
453
|
<h1>Title</h1>
|
|
@@ -125,10 +457,11 @@
|
|
|
125
457
|
<span class="chip">Audience</span>
|
|
126
458
|
<span class="chip">Purpose</span>
|
|
127
459
|
</div>
|
|
460
|
+
<div class="viewport-note">Design for the current viewport first. If a section feels cramped, split it, stack it, or reduce its density before adding more content.</div>
|
|
128
461
|
</section>
|
|
129
462
|
|
|
130
463
|
<div class="page">
|
|
131
|
-
<section>
|
|
464
|
+
<section class="content">
|
|
132
465
|
<section class="section">
|
|
133
466
|
<div class="label">Summary</div>
|
|
134
467
|
<h2>Start With the Story</h2>
|
|
@@ -165,5 +498,43 @@
|
|
|
165
498
|
</aside>
|
|
166
499
|
</div>
|
|
167
500
|
</main>
|
|
501
|
+
|
|
502
|
+
<script>
|
|
503
|
+
(() => {
|
|
504
|
+
const root = document.documentElement;
|
|
505
|
+
const metaTheme = document.querySelector('meta[name="theme-color"]');
|
|
506
|
+
const button = document.getElementById('theme-toggle');
|
|
507
|
+
const label = document.getElementById('theme-label');
|
|
508
|
+
const storageKey = 'hog-artifact-theme';
|
|
509
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
510
|
+
|
|
511
|
+
const computeTheme = (mode) => {
|
|
512
|
+
if (mode === 'auto') return media.matches ? 'dark' : 'light';
|
|
513
|
+
return mode;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const themeColor = (mode) => computeTheme(mode) === 'dark' ? '#1f1d2e' : '#fffaf3';
|
|
517
|
+
|
|
518
|
+
const apply = (mode) => {
|
|
519
|
+
root.dataset.theme = mode;
|
|
520
|
+
if (label) label.textContent = mode[0].toUpperCase() + mode.slice(1);
|
|
521
|
+
if (metaTheme) metaTheme.setAttribute('content', themeColor(mode));
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const saved = localStorage.getItem(storageKey) || 'auto';
|
|
525
|
+
apply(saved);
|
|
526
|
+
|
|
527
|
+
button?.addEventListener('click', () => {
|
|
528
|
+
const current = root.dataset.theme || 'auto';
|
|
529
|
+
const next = current === 'auto' ? 'dark' : current === 'dark' ? 'light' : 'auto';
|
|
530
|
+
localStorage.setItem(storageKey, next);
|
|
531
|
+
apply(next);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
media.addEventListener?.('change', () => {
|
|
535
|
+
if ((root.dataset.theme || 'auto') === 'auto') apply('auto');
|
|
536
|
+
});
|
|
537
|
+
})();
|
|
538
|
+
</script>
|
|
168
539
|
</body>
|
|
169
540
|
</html>
|
|
@@ -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
|
|
package/share-server/README.md
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
54
|
-
if (!existsSync(
|
|
55
|
-
const parsed = JSON.parse(readFileSync(
|
|
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: {
|