aiblueprint-cli 1.4.103 → 1.4.105

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 (33) hide show
  1. package/README.md +2 -7
  2. package/agents-config/skills/audit-skills/SKILL.md +79 -0
  3. package/agents-config/skills/audit-skills/agents/openai.yaml +7 -0
  4. package/agents-config/skills/audit-skills/scripts/audit-skills.mjs +346 -0
  5. package/package.json +1 -1
  6. package/agents-config/skills/audit/SKILL.md +0 -126
  7. package/agents-config/skills/audit/agents/openai.yaml +0 -10
  8. package/agents-config/skills/audit/assets/codex-icon.svg +0 -20
  9. package/agents-config/skills/commit/SKILL.md +0 -44
  10. package/agents-config/skills/commit/agents/openai.yaml +0 -10
  11. package/agents-config/skills/commit/assets/codex-icon.svg +0 -17
  12. package/agents-config/skills/create-pr/SKILL.md +0 -55
  13. package/agents-config/skills/create-pr/agents/openai.yaml +0 -10
  14. package/agents-config/skills/create-pr/assets/codex-icon.svg +0 -17
  15. package/agents-config/skills/oneshot/SKILL.md +0 -44
  16. package/agents-config/skills/oneshot/agents/openai.yaml +0 -10
  17. package/agents-config/skills/oneshot/assets/codex-icon.svg +0 -18
  18. package/agents-config/skills/tools/SKILL.md +0 -149
  19. package/agents-config/skills/use-artifacts/SKILL.md +0 -211
  20. package/agents-config/skills/use-artifacts/agents/openai.yaml +0 -7
  21. package/agents-config/skills/use-artifacts/assets/codex-icon.svg +0 -18
  22. package/agents-config/skills/use-artifacts/assets/local-runtime.js +0 -299
  23. package/agents-config/skills/use-artifacts/scripts/create_artifact.py +0 -317
  24. package/agents-config/skills/use-delegate/SKILL.md +0 -97
  25. package/agents-config/skills/use-delegate/agents/openai.yaml +0 -10
  26. package/agents-config/skills/use-delegate/assets/codex-icon.svg +0 -20
  27. package/agents-config/skills/use-delegate/references/models.md +0 -32
  28. package/agents-config/skills/use-goal/SKILL.md +0 -121
  29. package/agents-config/skills/use-goal/agents/openai.yaml +0 -7
  30. package/agents-config/skills/use-goal/assets/codex-icon.svg +0 -18
  31. package/agents-config/skills/use-goal/references/claude-code-goal.md +0 -65
  32. package/agents-config/skills/use-goal/references/codex-goal.md +0 -70
  33. package/agents-config/skills/use-goal/references/verification-harnesses.md +0 -108
@@ -1,299 +0,0 @@
1
- /*
2
- * local-runtime.js: local artifact capabilities shim. No Anthropic API,
3
- * no claude.ai runtime, no network calls. Everything runs on plain
4
- * browser APIs (Blob, <a download>, timers, in-memory cache).
5
- *
6
- * It mirrors the claude.ai artifact runtime surface (contract 0.1.14):
7
- * - window.claude.downloads.save({filename, data})
8
- * - window.claude.mcp.callTool / watchTool / listTools / invalidate
9
- *
10
- * If a real claude.ai runtime already installed a member, the shim
11
- * leaves it untouched, so the same feature code is portable to a
12
- * genuinely published artifact without changes.
13
- *
14
- * Local data sources replace viewer connectors. Register them BEFORE
15
- * feature code runs:
16
- *
17
- * window.claudeLocal.registerTool("Analytics", "get_stats",
18
- * async (input) => ({ visits: 1234 })); // live local source
19
- * window.claudeLocal.registerTool("Analytics", "snapshot",
20
- * { visits: 1234 }, { description: "Static snapshot 2026-07-22" });
21
- *
22
- * Then use the standard surface:
23
- * const r = await window.claude.mcp.callTool("Analytics", "get_stats", { range: "7d" });
24
- * render(r.payload);
25
- */
26
- (function () {
27
- "use strict";
28
- if (typeof window === "undefined") return;
29
-
30
- var root = window.claude;
31
- if (!root || typeof root !== "object") {
32
- root = {};
33
- try { window.claude = root; } catch (e) { return; }
34
- }
35
-
36
- /* ------------------------------ downloads ------------------------------ */
37
-
38
- var EXT_ALLOWLIST = ["gif", "png", "jpg", "jpeg", "webp", "mp4", "webm", "txt", "json", "md"];
39
- var MIME_BY_EXT = {
40
- gif: "image/gif", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
41
- webp: "image/webp", mp4: "video/mp4", webm: "video/webm",
42
- txt: "text/plain", json: "application/json", md: "text/markdown"
43
- };
44
- var MAX_BYTES = 16 * 1024 * 1024;
45
- var promptOpen = false;
46
-
47
- function downloadsError(code, message) {
48
- return { code: code, message: message };
49
- }
50
-
51
- function toBlob(data, mime) {
52
- if (typeof data === "string" && data.length) return new Blob([data], { type: mime });
53
- if (data instanceof Blob && data.size) return new Blob([data], { type: mime });
54
- if (data instanceof ArrayBuffer && data.byteLength) return new Blob([data], { type: mime });
55
- if (ArrayBuffer.isView(data) && data.byteLength) return new Blob([data], { type: mime });
56
- return null;
57
- }
58
-
59
- if (!root.downloads) {
60
- root.downloads = {
61
- save: function (request) {
62
- return new Promise(function (resolve, reject) {
63
- if (promptOpen) {
64
- return reject(downloadsError("rate_limited", "A save prompt is already open."));
65
- }
66
- if (!request || typeof request !== "object") {
67
- return reject(downloadsError("bad_request", "save() takes {filename, data}."));
68
- }
69
- var filename = request.filename;
70
- if (typeof filename !== "string" || !filename || filename.length > 512) {
71
- return reject(downloadsError("bad_request", "filename must be a non-empty string of at most 512 chars."));
72
- }
73
- var clean = filename.replace(/[\/\\:*?"<>|]+/g, "-").trim();
74
- var dot = clean.lastIndexOf(".");
75
- var ext = (dot > 0 ? clean.slice(dot + 1) : "").toLowerCase();
76
- if (EXT_ALLOWLIST.indexOf(ext) === -1) {
77
- return reject(downloadsError("rejected_extension",
78
- "Extension \"." + ext + "\" is outside the allowlist: " + EXT_ALLOWLIST.join(" ")));
79
- }
80
- var blob = toBlob(request.data, MIME_BY_EXT[ext]);
81
- if (!blob) {
82
- return reject(downloadsError("bad_request",
83
- "data must be a non-empty string, Blob, ArrayBuffer, or ArrayBufferView."));
84
- }
85
- if (blob.size > MAX_BYTES) {
86
- return reject(downloadsError("too_large", "File is over 16 MiB (" + blob.size + " bytes)."));
87
- }
88
- promptOpen = true;
89
- var accepted;
90
- try {
91
- accepted = window.confirm('Save "' + clean + '" (' + blob.size + " bytes)?");
92
- } finally {
93
- promptOpen = false;
94
- }
95
- if (!accepted) {
96
- return reject(downloadsError("declined", "The viewer declined the save."));
97
- }
98
- var url = URL.createObjectURL(blob);
99
- var a = document.createElement("a");
100
- a.href = url;
101
- a.download = clean;
102
- document.body.appendChild(a);
103
- a.click();
104
- a.remove();
105
- setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
106
- resolve({ status: "saved" });
107
- });
108
- }
109
- };
110
- }
111
-
112
- /* --------------------------------- mcp --------------------------------- */
113
-
114
- var SEP = "\u0000";
115
- var registry = {}; // "server\0tool" -> {source, isStatic, description}
116
- var cache = {}; // identity -> {result, storedAt}
117
- var watchers = {}; // identity -> [{handler, timer, refresh}]
118
-
119
- function key(server, tool) { return server + SEP + tool; }
120
-
121
- function canonical(value) {
122
- if (value === undefined || value === null) return "null";
123
- if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
124
- if (typeof value === "object") {
125
- return "{" + Object.keys(value).sort().map(function (k) {
126
- return JSON.stringify(k) + ":" + canonical(value[k]);
127
- }).join(",") + "}";
128
- }
129
- return JSON.stringify(value);
130
- }
131
-
132
- function identity(server, tool, input) {
133
- return key(server, tool) + SEP + canonical(input);
134
- }
135
-
136
- function mcpError(code, server, message, extra) {
137
- var err = { code: code, message: message };
138
- if (server !== undefined) err.server = server;
139
- if (extra) Object.keys(extra).forEach(function (k) { err[k] = extra[k]; });
140
- return err;
141
- }
142
-
143
- function envelope(payload) {
144
- var text = typeof payload === "string" ? payload : JSON.stringify(payload);
145
- var result = { content: [{ type: "text", text: text }], payload: payload };
146
- if (payload !== null && typeof payload === "object") result.structuredContent = payload;
147
- return result;
148
- }
149
-
150
- function cachedCopy(entry, revalidating) {
151
- var copy = {};
152
- Object.keys(entry.result).forEach(function (k) { copy[k] = entry.result[k]; });
153
- copy.cache = { storedAt: entry.storedAt, revalidating: !!revalidating };
154
- return copy;
155
- }
156
-
157
- function notifyData(id, result) {
158
- (watchers[id] || []).forEach(function (w) {
159
- try { w.handler({ type: "data", result: result }); } catch (e) { /* page bug */ }
160
- });
161
- }
162
-
163
- function execute(server, tool, input) {
164
- var entry = registry[key(server, tool)];
165
- if (!entry) {
166
- return Promise.reject(mcpError("server_not_connected", server,
167
- 'No local source registered for "' + server + '" / "' + tool +
168
- '". Call window.claudeLocal.registerTool(...) before feature code runs.'));
169
- }
170
- try { JSON.stringify(input === undefined ? null : input); } catch (e) {
171
- return Promise.reject(mcpError("bad_request", server, "input must be plain JSON."));
172
- }
173
- return Promise.resolve()
174
- .then(function () {
175
- return entry.isStatic ? entry.source : entry.source(input === undefined ? null : input);
176
- })
177
- .then(function (payload) {
178
- var result = envelope(payload);
179
- cache[identity(server, tool, input)] = { result: result, storedAt: Date.now() };
180
- return result;
181
- })
182
- .catch(function (e) {
183
- if (e && e.code) throw e;
184
- throw mcpError("tool_error", server, (e && e.message) || String(e), { result: e });
185
- });
186
- }
187
-
188
- if (!root.mcp) {
189
- root.mcp = {
190
- callTool: function (server, tool, input, options) {
191
- if (typeof server !== "string" || typeof tool !== "string") {
192
- return Promise.reject(mcpError("bad_request", undefined, "server and tool must be strings."));
193
- }
194
- var opts = options || {};
195
- var id = identity(server, tool, input);
196
- var entry = cache[id];
197
- var staleTime = 0;
198
- var refresh = false;
199
- if (opts.cache && typeof opts.cache === "object") {
200
- staleTime = Math.min(opts.cache.staleTime || 0, 300000);
201
- refresh = !!opts.cache.refresh;
202
- }
203
- if (opts.cache === false) entry = null;
204
- if (refresh) entry = null;
205
- if (entry && Date.now() - entry.storedAt < staleTime) {
206
- return Promise.resolve(cachedCopy(entry, false));
207
- }
208
- return execute(server, tool, input).then(function (result) {
209
- notifyData(id, result);
210
- return result;
211
- });
212
- },
213
-
214
- watchTool: function (server, tool, input, handler, options) {
215
- if (typeof handler !== "function") {
216
- throw new TypeError("watchTool handler must be a function.");
217
- }
218
- var opts = options || {};
219
- var id = identity(server, tool, input);
220
- var w = { handler: handler, timer: null, refresh: null };
221
- (watchers[id] = watchers[id] || []).push(w);
222
-
223
- w.refresh = function () {
224
- execute(server, tool, input)
225
- .then(function (result) { notifyData(id, result); })
226
- .catch(function (err) {
227
- try { w.handler({ type: "error", error: err }); } catch (e) { /* page bug */ }
228
- });
229
- };
230
-
231
- Promise.resolve().then(function () {
232
- var entry = cache[id];
233
- if (entry) {
234
- try { w.handler({ type: "data", result: cachedCopy(entry, false) }); } catch (e) { /* page bug */ }
235
- } else {
236
- w.refresh();
237
- }
238
- });
239
-
240
- if (opts.refetchInterval) {
241
- w.timer = setInterval(w.refresh, Math.max(5000, opts.refetchInterval));
242
- }
243
-
244
- var done = false;
245
- return function unsubscribe() {
246
- if (done) return;
247
- done = true;
248
- if (w.timer) clearInterval(w.timer);
249
- watchers[id] = (watchers[id] || []).filter(function (x) { return x !== w; });
250
- };
251
- },
252
-
253
- invalidate: function (server, tool, input) {
254
- Object.keys(cache).forEach(function (id) {
255
- var match = true;
256
- if (server !== undefined) match = id.indexOf(server + SEP) === 0;
257
- if (match && tool !== undefined) match = id.indexOf(key(server, tool) + SEP) === 0;
258
- if (match && input !== undefined) match = id === identity(server, tool, input);
259
- if (match) {
260
- delete cache[id];
261
- var subs = watchers[id] || [];
262
- if (subs.length) subs[0].refresh();
263
- }
264
- });
265
- return Promise.resolve();
266
- },
267
-
268
- listTools: function () {
269
- var byServer = {};
270
- Object.keys(registry).forEach(function (k) {
271
- var parts = k.split(SEP);
272
- (byServer[parts[0]] = byServer[parts[0]] || []).push({
273
- name: parts[1],
274
- description: registry[k].description || ""
275
- });
276
- });
277
- return Promise.resolve({
278
- servers: Object.keys(byServer).map(function (s) {
279
- return { server: s, authStatus: "connected", tools: byServer[s] };
280
- })
281
- });
282
- }
283
- };
284
- }
285
-
286
- /* --------------------------- local registration ------------------------ */
287
-
288
- window.claudeLocal = window.claudeLocal || {};
289
- window.claudeLocal.registerTool = function (server, tool, source, opts) {
290
- if (typeof server !== "string" || typeof tool !== "string") {
291
- throw new TypeError("registerTool(server, tool, source): server and tool must be strings.");
292
- }
293
- registry[key(server, tool)] = {
294
- source: source,
295
- isStatic: typeof source !== "function",
296
- description: (opts && opts.description) || ""
297
- };
298
- };
299
- })();
@@ -1,317 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Scaffold a Claude-style local HTML artifact workspace."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import re
9
- from datetime import datetime, timezone
10
- from pathlib import Path
11
-
12
- GLOBAL_ARTIFACTS_DIR = Path.home() / ".agents" / "artifacts"
13
-
14
-
15
- def slugify(value: str) -> str:
16
- slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
17
- return slug[:48].strip("-") or "artifact"
18
-
19
-
20
- def unique_dir(base: Path, slug: str) -> Path:
21
- candidate = base / slug
22
- if not candidate.exists():
23
- return candidate
24
-
25
- stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
26
- return base / f"{slug}-{stamp}"
27
-
28
-
29
- def html_template(title: str, style: str, kind: str) -> str:
30
- escaped_title = (
31
- title.replace("&", "&amp;")
32
- .replace("<", "&lt;")
33
- .replace(">", "&gt;")
34
- .replace('"', "&quot;")
35
- )
36
- return f"""<!doctype html>
37
- <html lang="en">
38
- <head>
39
- <meta charset="utf-8">
40
- <meta name="viewport" content="width=device-width, initial-scale=1">
41
- <title>{escaped_title}</title>
42
- <style>
43
- :root {{
44
- color-scheme: dark;
45
- --bg: #000000;
46
- --panel: #111111;
47
- --ink: #ffffff;
48
- --muted: #888888;
49
- --border: #333333;
50
- --accent: #0070f3;
51
- }}
52
- * {{ box-sizing: border-box; }}
53
- body {{
54
- margin: 0;
55
- min-height: 100vh;
56
- background: var(--bg);
57
- color: var(--ink);
58
- font-family: Geist, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
59
- }}
60
- main {{
61
- width: min(1120px, calc(100vw - 32px));
62
- margin: 0 auto;
63
- padding: 48px 0;
64
- }}
65
- .shell {{
66
- display: grid;
67
- gap: 24px;
68
- }}
69
- .header {{
70
- display: flex;
71
- flex-wrap: wrap;
72
- align-items: end;
73
- justify-content: space-between;
74
- gap: 16px;
75
- border-bottom: 1px solid var(--border);
76
- padding-bottom: 20px;
77
- }}
78
- .eyebrow {{
79
- margin: 0 0 8px;
80
- color: var(--accent);
81
- font-size: 12px;
82
- font-weight: 700;
83
- letter-spacing: .08em;
84
- text-transform: uppercase;
85
- }}
86
- h1 {{
87
- margin: 0;
88
- max-width: 760px;
89
- font-size: clamp(40px, 7vw, 88px);
90
- line-height: .95;
91
- letter-spacing: -.03em;
92
- }}
93
- .meta {{
94
- color: var(--muted);
95
- font-size: 14px;
96
- line-height: 1.5;
97
- }}
98
- .panel {{
99
- min-height: 420px;
100
- border: 1px solid var(--border);
101
- border-radius: 8px;
102
- background: var(--panel);
103
- padding: 28px;
104
- }}
105
- .panel h2 {{
106
- margin: 0 0 12px;
107
- font-size: 24px;
108
- letter-spacing: 0;
109
- }}
110
- .panel p {{
111
- max-width: 680px;
112
- color: var(--muted);
113
- font-size: 17px;
114
- line-height: 1.65;
115
- }}
116
- button {{
117
- border: 0;
118
- border-radius: 6px;
119
- background: var(--ink);
120
- color: var(--bg);
121
- cursor: pointer;
122
- font: inherit;
123
- font-weight: 650;
124
- padding: 10px 16px;
125
- }}
126
- .callout {{
127
- border: 1px solid var(--border);
128
- border-radius: 0;
129
- background: var(--panel);
130
- padding: 20px;
131
- }}
132
- .callout h2 {{ color: var(--ink); }}
133
- .grid {{
134
- display: grid;
135
- gap: 16px;
136
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
137
- }}
138
- .croquis {{
139
- min-height: 180px;
140
- border: 1px solid var(--border);
141
- border-radius: 0;
142
- background: var(--bg);
143
- padding: 14px;
144
- }}
145
- .bar {{
146
- height: 14px;
147
- border-radius: 0;
148
- background: #666666;
149
- margin-bottom: 10px;
150
- }}
151
- .box {{
152
- height: 52px;
153
- border: 1px solid var(--border);
154
- border-radius: 0;
155
- background: var(--panel);
156
- margin-bottom: 10px;
157
- }}
158
- .note {{
159
- color: var(--muted);
160
- font-size: 14px;
161
- line-height: 1.45;
162
- }}
163
- pre {{
164
- overflow-x: auto;
165
- border: 1px solid var(--border);
166
- border-radius: 0;
167
- background: var(--bg);
168
- padding: 16px;
169
- }}
170
- code {{
171
- border-radius: 0;
172
- background: var(--panel);
173
- padding: 2px 5px;
174
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
175
- font-size: .92em;
176
- }}
177
- </style>
178
- </head>
179
- <body>
180
- <main>
181
- <section class="shell" aria-label="{escaped_title}">
182
- <div class="header">
183
- <div>
184
- <p class="eyebrow">{kind} artifact</p>
185
- <h1>{escaped_title}</h1>
186
- </div>
187
- <p class="meta">Style: {style}<br>Entrypoint: index.html</p>
188
- </div>
189
- <div class="callout">
190
- <h2>Key finding or recommendation</h2>
191
- <p>
192
- Put the highest-signal conclusion near the top. Use this area for the
193
- main security finding, product decision, implementation warning, or
194
- core recommendation that frames the rest of the artifact.
195
- </p>
196
- </div>
197
- <div class="panel">
198
- <h2>Public reasoning surface</h2>
199
- <p>
200
- Turn the answer into a readable page: context, model, tradeoffs,
201
- examples, edge cases, rollout steps, and verification notes. Show
202
- conclusions and evidence, not private chain-of-thought.
203
- </p>
204
- <pre><code>// Add focused snippets when they clarify the plan.
205
- function example() {{
206
- return "replace this with the requested artifact content";
207
- }}</code></pre>
208
- </div>
209
- <div class="panel">
210
- <h2>Page draft</h2>
211
- <p>
212
- For plan artifacts, draft the actual page or content structure here:
213
- title, lede, sections, key copy, calls to action, states, and the
214
- narrative blocks the user should see.
215
- </p>
216
- </div>
217
- <div class="grid">
218
- <article class="panel">
219
- <h2>Page croquis A</h2>
220
- <div class="croquis">
221
- <div class="bar" style="width: 72%;"></div>
222
- <div class="box"></div>
223
- <div class="box" style="width: 64%;"></div>
224
- </div>
225
- <p class="note">Sketch the page hierarchy quickly. This is for seeing and understanding, not final UI.</p>
226
- </article>
227
- <article class="panel">
228
- <h2>Page croquis B</h2>
229
- <div class="croquis">
230
- <div class="box" style="height: 84px;"></div>
231
- <div class="bar" style="width: 46%;"></div>
232
- <div class="bar" style="width: 78%; opacity: .35;"></div>
233
- </div>
234
- <p class="note">Annotate what changes: layout, emphasis, rhythm, audience, or tradeoff.</p>
235
- </article>
236
- </div>
237
- </section>
238
- </main>
239
- </body>
240
- </html>
241
- """
242
-
243
-
244
- def highlogic_template(title: str, style: str, kind: str, created_at: str) -> str:
245
- return f"""# {title}
246
-
247
- ## Intent
248
-
249
- - Kind: {kind}
250
- - Style: {style}
251
- - Created: {created_at}
252
-
253
- ## User Request
254
-
255
- TODO: Summarize the user's artifact request.
256
-
257
- ## Core Logic
258
-
259
- TODO: Describe the public reasoning structure, sections, data model, interactions, and key design decisions. Capture conclusions, evidence, assumptions, and tradeoffs without private chain-of-thought.
260
-
261
- ## Verification
262
-
263
- TODO: Record how the artifact was opened or tested, including browser/runtime checks when relevant.
264
-
265
- ## Iteration Notes
266
-
267
- - Initial scaffold created.
268
- """
269
-
270
-
271
- def main() -> int:
272
- parser = argparse.ArgumentParser(description="Create a local HTML artifact workspace.")
273
- parser.add_argument("title", help="Short artifact title or slug")
274
- parser.add_argument("--style", default="custom", help="style label recorded in the manifest (requested, project:<app-name>, or subject-specific)")
275
- parser.add_argument("--kind", default="thinking", help="artifact kind")
276
- args = parser.parse_args()
277
-
278
- base = GLOBAL_ARTIFACTS_DIR.resolve()
279
- artifact_id = slugify(args.title)
280
- artifact_dir = unique_dir(base, artifact_id)
281
- created_at = datetime.now(timezone.utc).isoformat()
282
-
283
- artifact_dir.mkdir(parents=True, exist_ok=False)
284
- (artifact_dir / "versions").mkdir()
285
-
286
- title = args.title.strip()
287
- (artifact_dir / "index.html").write_text(
288
- html_template(title, args.style, args.kind),
289
- encoding="utf-8",
290
- )
291
- (artifact_dir / "HIGHLOGIC.md").write_text(
292
- highlogic_template(title, args.style, args.kind, created_at),
293
- encoding="utf-8",
294
- )
295
- manifest = {
296
- "id": artifact_dir.name,
297
- "title": title,
298
- "kind": args.kind,
299
- "style": args.style,
300
- "created_at": created_at,
301
- "entrypoint": "index.html",
302
- "files": ["index.html", "HIGHLOGIC.md", "manifest.json"],
303
- }
304
- (artifact_dir / "manifest.json").write_text(
305
- json.dumps(manifest, indent=2) + "\n",
306
- encoding="utf-8",
307
- )
308
-
309
- index_path = artifact_dir / "index.html"
310
- print(f"created={artifact_dir}")
311
- print(f"index={index_path}")
312
- print(f"url={index_path.as_uri()}")
313
- return 0
314
-
315
-
316
- if __name__ == "__main__":
317
- raise SystemExit(main())