@yargote/artifacts-cli 0.1.0-beta.1 → 0.1.0-beta.3

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.
@@ -0,0 +1,38 @@
1
+ # Catppuccin roles
2
+
3
+ The starter maps the official [Catppuccin palette](https://github.com/catppuccin/palette/blob/main/palette.json) to semantic tokens. Latte serves the light theme and Mocha the dark theme. Blue is the Artifacts accent.
4
+
5
+ | Token | Role | Latte (light) | Mocha (dark) |
6
+ | --- | --- | --- | --- |
7
+ | `--page` | Page background | Base `#eff1f5` | Mantle `#181825` |
8
+ | `--surface` | Strip, table, or panel | White `#ffffff` | Base `#1e1e2e` |
9
+ | `--sunken` | Table header, hover, code | Mantle `#e6e9ef` | Crust `#11111b` |
10
+ | `--line` | Hairline divider | Surface 0 `#ccd0da` | Surface 0 `#313244` |
11
+ | `--control` | Input and button boundary | Overlay 1 `#8c8fa1` | Overlay 0 `#6c7086` |
12
+ | `--fg` | Main text | Text `#4c4f69` | Text `#cdd6f4` |
13
+ | `--muted` | Secondary text and labels | Subtext 1 `#5c5f77` | Subtext 0 `#a6adc8` |
14
+ | `--accent` | Focus, selection, primary fill | Blue `#1e66f5` | Blue `#89b4fa` |
15
+ | `--accent-ink` | Links and accent text | `#1957d0` | Blue `#89b4fa` |
16
+ | `--ok` / `--ok-ink` | Success mark / text | Green `#40a02b` / `#307820` | Green `#a6e3a1` |
17
+ | `--warn` / `--warn-ink` | Warning mark / text | Yellow `#df8e1d` / `#915c13` | Yellow `#f9e2af` |
18
+ | `--bad` / `--bad-ink` | Error mark / text | Red `#d20f39` / `#b20d30` | Red `#f38ba8` |
19
+
20
+ The Latte ink values darken the palette color so pill text reaches 4.5:1 on its soft background. Each `*-soft` token mixes 14 % of its color into `--surface`, so it adapts to both themes without a second definition.
21
+
22
+ Every text token reaches 4.5:1 on `--page` and `--surface` in both themes. `--control` reaches 3:1 for input boundaries. `--line` is decorative and must not be the only boundary of a control.
23
+
24
+ ## Chart series
25
+
26
+ Use these in order after Blue. Give each series a direct label or marker as well as a color.
27
+
28
+ | Series | Latte | Mocha |
29
+ | --- | --- | --- |
30
+ | 1 | Blue `#1e66f5` | Blue `#89b4fa` |
31
+ | 2 | Peach `#fe640b` | Peach `#fab387` |
32
+ | 3 | Teal `#179299` | Teal `#94e2d5` |
33
+ | 4 | Mauve `#8839ef` | Mauve `#cba6f7` |
34
+ | 5 | Yellow `#df8e1d` | Yellow `#f9e2af` |
35
+
36
+ Mauve is a data color only, never the accent.
37
+
38
+ The palette's license notice is retained in [third-party notices](../THIRD-PARTY-NOTICES.md).
@@ -0,0 +1,50 @@
1
+ # Interface checks for HTML artifacts
2
+
3
+ Use these checks for visual authoring and review. Apply only the checks relevant to the artifact. Keep the requested content and behavior central.
4
+
5
+ ## Accessible, resilient interfaces
6
+
7
+ - Use semantic headings, buttons, links, and tables. Label controls and icon buttons. Give meaningful images alternative text.
8
+ - Make every action keyboard accessible. Keep focus visible and unobscured. Restore focus after dismissing overlays.
9
+ - Announce asynchronous status changes without interrupting reading. Pair errors with a useful next action.
10
+ - Provide click and keyboard alternatives for gestures. Keep essential information available without hover.
11
+ - Preserve zoom and paste. Associate input labels with their controls. Use suitable input types.
12
+ - Support narrow viewports, empty results, and long content. Let flex children shrink. Wrap text before hiding overflow.
13
+ - Reserve image dimensions. Defer below-fold media. Batch layout reads and writes. Paginate or virtualize long lists when needed.
14
+ - Use tabular numerals for comparisons and `Intl` for localized values. Keep units attached to numbers.
15
+ - Set native controls to the document's color scheme. Keep hover, active, and focus states distinguishable.
16
+ - Avoid motion that blocks interaction. Provide pause controls for extended autoplay and a reduced-motion alternative.
17
+
18
+ ## Motion and interaction detail
19
+
20
+ Adapt motion to repetition and purpose. Frequent actions should respond immediately. Add animation when it explains a state change or spatial relationship.
21
+
22
+ Prefer interruptible CSS transitions with named properties. Start around 100 to 160 ms for press feedback and 125 to 250 ms for small overlays. Use an ease-out curve for entry. Anchor popover transforms to their trigger and center modal transforms.
23
+
24
+ Prefer `transform` and `opacity` for movement. Avoid `transition: all`, large blur effects, and entrance scaling from zero. Keep feedback subtle enough that labels remain readable.
25
+
26
+ Under `prefers-reduced-motion: reduce`, remove decorative movement and retain immediate state feedback. Gate hover effects with `(hover: hover) and (pointer: fine)`. Do not make content wait for an entrance animation.
27
+
28
+ ## Adapt to the Artifacts sandbox
29
+
30
+ Keep input-driven calculations in document memory. Use labeled controls without a form submission workflow. Do not imply that an in-document control saves a retained revision or publishes an artifact.
31
+
32
+ Keep filters and tabs local to the document. Do not depend on host URL synchronization, router packages, persistent storage, network requests, or remote fonts. Embed fonts and other required assets as `data:` URIs within the snapshot budget.
33
+
34
+ ## Check the result
35
+
36
+ When browser tools are available, inspect the generated document at a narrow viewport around 375 px and a wide viewport around 1280 px, or the user's requested sizes. Verify that body content fits, labels remain legible, and only intentionally wide tables or code regions scroll locally. Check the artifact itself rather than relying on the surrounding application's appearance.
37
+
38
+ Exercise every distinct action with keyboard and pointer input. Observe the expected output or state change, including input errors, reset behavior, and empty filter results where applicable. Confirm that focus stays visible and follows opened and dismissed overlays. Check reduced motion when animation is present. Inspect the console for runtime errors and confirm that required content and controls work without external requests.
39
+
40
+ Finish when each relevant check passes or an unavailable check is identified. Fix observed failures and repeat affected checks. Source inspection establishes markup and script intent; report browser behavior as verified only after observing it.
41
+
42
+ For a requested audit, identify each issue by element or source location and state its correction. Distinguish source inspection from browser verification. Do not turn ordinary artifact delivery into an unsolicited audit report.
43
+
44
+ ## Sources
45
+
46
+ Adapted on 2026-10-14 from [Emil Kowalski's design engineering skill](https://raw.githubusercontent.com/emilkowalski/skills/refs/heads/main/skills/emil-design-eng/SKILL.md) and [Vercel's web design guidelines skill](https://raw.githubusercontent.com/vercel-labs/agent-skills/refs/heads/main/skills/web-design-guidelines/SKILL.md).
47
+
48
+ The accessibility and interface checks incorporate Vercel's Web Interface Guidelines `command.md` as local guidance. Its remote command workflow and output format are not required. The sandbox adaptations are specific to Artifacts.
49
+
50
+ License notices are retained in [Third-party notices](../THIRD-PARTY-NOTICES.md).
@@ -0,0 +1,13 @@
1
+ # Beginner visual explanations
2
+
3
+ When the user requests a visual HTML explanation for a beginner, assume no prior knowledge. Identify the topic from the conversation. Ask for it only if it is missing or ambiguous.
4
+
5
+ Build a visual sequence with one idea per scene. Start with a concrete example the reader can recognize, then show how the parts connect or change. Use large illustrations, diagrams, or pictures to carry the explanation. Decorative icons and oversized headings do not replace explanatory images.
6
+
7
+ Pair each visual with a short heading and a brief caption. Use familiar words and define necessary terms where they first appear. Keep labels close to what they explain. Preserve essential qualifications and the limits of an analogy even when the user asks for few words.
8
+
9
+ Use inline SVG or CSS for diagrams. Use embedded raster images when the subject benefits from illustrations or photographs. Resize and compress them to fit the snapshot budget while keeping details readable. If image tools are unavailable, use a clear diagram when it can convey the concept faithfully. Report any requested imagery you cannot provide.
10
+
11
+ Make the main visual dominant and its labels legible on narrow and wide screens. Give meaningful images text alternatives. Add interaction only when it helps the reader understand a relationship or change. Keep the core explanation accessible without operating a control.
12
+
13
+ Before saving, check that the sequence explains the topic with its pictures and short captions, without a separate essay. Apply the [interface guidelines](interface-checks.md) visual checks and report any checks you could not run.
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const fonts = join(dirname(fileURLToPath(import.meta.url)), '../fonts');
7
+ const latin = 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD';
8
+ const faces = [
9
+ { family: 'Bricolage Grotesque', style: 'normal', weight: '500 800', file: 'bricolage-grotesque.woff2' },
10
+ { family: 'Public Sans', style: 'normal', weight: '400 700', file: 'public-sans.woff2' },
11
+ { family: 'Public Sans', style: 'italic', weight: '400 700', file: 'public-sans-italic.woff2' },
12
+ { family: 'JetBrains Mono', style: 'normal', weight: '400 700', file: 'jetbrains-mono.woff2' },
13
+ { family: 'Newsreader', style: 'normal', weight: '400 700', file: 'newsreader.woff2' },
14
+ { family: 'Newsreader', style: 'italic', weight: '400 700', file: 'newsreader-italic.woff2' },
15
+ ];
16
+ const limit = 1024 * 1024;
17
+ const embedded = /<style data-artifacts-fonts>[\s\S]*?<\/style>\n?/;
18
+
19
+ const paths = process.argv.slice(2);
20
+ if (paths.length !== 1) {
21
+ process.stderr.write('Usage: node embed-fonts.mjs PAGE.html\nEmbeds the bundled font faces that PAGE.html names, in place.\n');
22
+ process.exit(2);
23
+ }
24
+ const [path] = paths;
25
+ const page = (await readFile(path, 'utf8')).replace(embedded, '');
26
+ const italic = /font-style\s*:\s*italic|<(em|i|cite)[\s>]/i.test(page);
27
+ const used = faces.filter((face) => page.includes(face.family) && (face.style === 'normal' || italic));
28
+ const rules = await Promise.all(used.map(async (face) => {
29
+ const data = (await readFile(join(fonts, face.file))).toString('base64');
30
+ return `@font-face{font-family:"${face.family}";font-style:${face.style};font-weight:${face.weight};font-display:swap;src:url(data:font/woff2;base64,${data}) format("woff2");unicode-range:${latin}}`;
31
+ }));
32
+ const block = rules.length ? `<style data-artifacts-fonts>${rules.join('')}</style>\n` : '';
33
+
34
+ const anchor = page.search(/<style[\s>]/i);
35
+ const head = /<head[^>]*>\n?/i.exec(page) ?? /<!doctype[^>]*>\n?/i.exec(page);
36
+ const at = anchor >= 0 ? anchor : head ? head.index + head[0].length : 0;
37
+ const output = page.slice(0, at) + block + page.slice(at);
38
+ const bytes = Buffer.byteLength(output);
39
+ if (bytes > limit) {
40
+ process.stderr.write(`Not written. ${path} would be ${bytes} bytes, above the 1 MiB revision limit. Drop a family or the italic faces, or shrink the content.\n`);
41
+ process.exit(1);
42
+ }
43
+ await writeFile(path, output);
44
+ const names = used.map((face) => `${face.family}${face.style === 'italic' ? ' italic' : ''}`);
45
+ process.stdout.write(`${names.length ? `Embedded ${names.join(', ')}` : 'No bundled family named; nothing embedded'}. ${path} is ${bytes} bytes.\n`);
@@ -0,0 +1,170 @@
1
+ <!doctype html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Artifact starter</title>
7
+ <style>
8
+ /* Layout: one centered column; summary before detail, then the dominant table, chart, or diagram. */
9
+ :root{
10
+ --page:#eff1f5; --surface:#ffffff; --sunken:#e6e9ef; --line:#ccd0da; --control:#8c8fa1;
11
+ --fg:#4c4f69; --muted:#5c5f77; --accent:#1e66f5; --on-accent:#ffffff; --accent-ink:#1957d0;
12
+ --ok:#40a02b; --ok-ink:#307820; --warn:#df8e1d; --warn-ink:#915c13; --bad:#d20f39; --bad-ink:#b20d30;
13
+ --neutral-soft:color-mix(in srgb,var(--fg) 9%,var(--surface));
14
+ --accent-soft:color-mix(in srgb,var(--accent) 14%,var(--surface));
15
+ --ok-soft:color-mix(in srgb,var(--ok) 14%,var(--surface));
16
+ --warn-soft:color-mix(in srgb,var(--warn) 14%,var(--surface));
17
+ --bad-soft:color-mix(in srgb,var(--bad) 14%,var(--surface));
18
+ --f-display:"Bricolage Grotesque","Avenir Next",system-ui,sans-serif;
19
+ --f-body:"Public Sans","Segoe UI",system-ui,sans-serif;
20
+ --f-mono:"JetBrains Mono",ui-monospace,"SF Mono",Menlo,Consolas,monospace;
21
+ --radius:10px; --width:1120px;
22
+ color-scheme:light;
23
+ }
24
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
25
+ --page:#181825; --surface:#1e1e2e; --sunken:#11111b; --line:#313244; --control:#6c7086;
26
+ --fg:#cdd6f4; --muted:#a6adc8; --accent:#89b4fa; --on-accent:#11111b; --accent-ink:#89b4fa;
27
+ --ok:#a6e3a1; --ok-ink:#a6e3a1; --warn:#f9e2af; --warn-ink:#f9e2af; --bad:#f38ba8; --bad-ink:#f38ba8;
28
+ color-scheme:dark}}
29
+ :root[data-theme="dark"]{
30
+ --page:#181825; --surface:#1e1e2e; --sunken:#11111b; --line:#313244; --control:#6c7086;
31
+ --fg:#cdd6f4; --muted:#a6adc8; --accent:#89b4fa; --on-accent:#11111b; --accent-ink:#89b4fa;
32
+ --ok:#a6e3a1; --ok-ink:#a6e3a1; --warn:#f9e2af; --warn-ink:#f9e2af; --bad:#f38ba8; --bad-ink:#f38ba8;
33
+ color-scheme:dark}
34
+
35
+ *{box-sizing:border-box}
36
+ html{background:var(--page)}
37
+ body{margin:0;background:var(--page);color:var(--fg);font:15px/1.55 var(--f-body);-webkit-font-smoothing:antialiased}
38
+ .wrap{max-width:var(--width);margin:0 auto;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:32px 64px;display:grid;gap:24px}
39
+ .wrap>*{min-width:0}
40
+ h1,h2,h3{font-family:var(--f-display);margin:0;text-wrap:balance;letter-spacing:-.01em}
41
+ h1{font-size:clamp(28px,4vw,40px);line-height:1.05;font-weight:700}
42
+ h2{font-size:clamp(20px,2.4vw,24px);line-height:1.2;font-weight:700}
43
+ h3{font-size:17px;line-height:1.3;font-weight:600}
44
+ p{margin:0}
45
+ a{color:var(--accent-ink);text-underline-offset:3px}
46
+ code,kbd,pre{font-family:var(--f-mono);font-size:.86em}
47
+ pre{margin:0;padding:14px 16px;background:var(--sunken);border-radius:8px;overflow-x:auto;line-height:1.5}
48
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px}
49
+
50
+ .eyebrow,.label{font:600 11.5px/1.2 var(--f-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
51
+ header{display:grid;gap:8px}
52
+ .lede{color:var(--muted);max-width:68ch;font-size:16px}
53
+ .prose{display:grid;gap:12px;max-width:68ch}
54
+ section{display:grid;gap:14px}
55
+
56
+ .strip{display:flex;flex-wrap:wrap;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
57
+ .stat{flex:1 1 150px;display:grid;gap:2px;padding:14px 16px;background:var(--surface)}
58
+ .stat b{font:700 26px/1.1 var(--f-display);font-variant-numeric:tabular-nums}
59
+ .stat span{font-size:13px;color:var(--muted)}
60
+
61
+ .pill{display:inline-flex;align-items:center;gap:6px;font:600 11.5px/1.25 var(--f-body);padding:3px 8px;border-radius:5px;background:var(--neutral-soft);color:var(--muted)}
62
+ .pills{display:flex;flex-wrap:wrap;gap:5px}
63
+ .pill.accent{background:var(--accent-soft);color:var(--accent-ink)}
64
+ .pill.ok{background:var(--ok-soft);color:var(--ok-ink)}
65
+ .pill.warn{background:var(--warn-soft);color:var(--warn-ink)}
66
+ .pill.bad{background:var(--bad-soft);color:var(--bad-ink)}
67
+
68
+ .panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:16px 18px;display:grid;gap:10px}
69
+ .note{border-left:3px solid var(--accent);padding:2px 0 2px 14px;display:grid;gap:4px;max-width:72ch}
70
+ .note.warn{border-color:var(--warn)}
71
+
72
+ .table{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);overflow-x:auto}
73
+ table{width:100%;border-collapse:collapse;font-size:14px}
74
+ th{font:600 11px/1.2 var(--f-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted);text-align:left;padding:10px 14px;background:var(--sunken);border-bottom:1px solid var(--line);white-space:nowrap}
75
+ td{padding:11px 14px;border-bottom:1px solid var(--line);vertical-align:top;font-variant-numeric:tabular-nums}
76
+ tr:last-child td{border-bottom:0}
77
+ td.num,th.num{text-align:right;white-space:nowrap}
78
+
79
+ .rows{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
80
+ .rows-head,.row>summary{display:grid;grid-template-columns:var(--cols,88px minmax(0,2fr) minmax(0,1fr) minmax(0,1.4fr));gap:16px;padding:12px 16px;align-items:start}
81
+ .rows-head{font:600 11px/1.2 var(--f-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted);background:var(--sunken);border-bottom:1px solid var(--line)}
82
+ .row{border-bottom:1px solid var(--line)}
83
+ .row:last-child{border-bottom:0}
84
+ .row>summary{cursor:pointer;list-style:none}
85
+ .row>summary::-webkit-details-marker{display:none}
86
+ .row>summary>*{min-width:0}
87
+ .row>summary>:first-child::before{content:"▸";display:inline-block;margin-right:6px;color:var(--muted);font-size:11px;transition:transform .15s}
88
+ .row[open]>summary>:first-child::before{transform:rotate(90deg)}
89
+ .row-detail{padding:4px 16px 18px;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:14px 28px}
90
+ .row-detail dl{margin:0;display:grid;gap:10px;align-content:start}
91
+ .row-detail dd{margin:2px 0 0;font-size:13.5px;overflow-wrap:anywhere}
92
+ .row.closed>summary{color:var(--muted)}
93
+
94
+ .bar{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
95
+ input,select,button{font:inherit;font-size:14px;color:var(--fg);background:var(--surface);border:1px solid var(--control);border-radius:8px;padding:8px 10px;min-width:0}
96
+ button{cursor:pointer}
97
+ button.primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent);font-weight:600}
98
+ .chip{border:1px solid var(--line);border-radius:999px;padding:5px 11px;font-size:13px;font-weight:500}
99
+ .chip[aria-pressed="true"]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent-ink)}
100
+ .count{margin-left:auto;font:13px var(--f-mono);color:var(--muted)}
101
+
102
+ footer{font-size:12.5px;color:var(--muted);display:grid;gap:4px;border-top:1px solid var(--line);padding-top:16px}
103
+
104
+ @media (max-width:720px){.rows-head{display:none}.row>summary{grid-template-columns:minmax(0,1fr);gap:6px}}
105
+ @media (hover:hover) and (pointer:fine){tbody tr:hover td,.row>summary:hover{background:var(--sunken)}button:hover{border-color:var(--accent)}}
106
+ @media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <main class="wrap">
111
+ <header>
112
+ <div class="eyebrow">Specimen · artifacts-design</div>
113
+ <h1>Starter primitives</h1>
114
+ <p class="lede">Replace this body with the artifact's own composition. Keep the tokens and the primitives the page needs, and delete the rest.</p>
115
+ </header>
116
+
117
+ <section class="strip" aria-label="Summary">
118
+ <div class="stat"><b>34</b><span>Records</span></div>
119
+ <div class="stat"><b>23</b><span>Open</span></div>
120
+ <div class="stat"><b>4</b><span>Need attention</span></div>
121
+ </section>
122
+
123
+ <section>
124
+ <h2>Status and data</h2>
125
+ <div class="bar">
126
+ <input id="q" type="search" placeholder="Search" aria-label="Search">
127
+ <button class="chip" aria-pressed="true">All</button>
128
+ <button class="chip" aria-pressed="false">Open</button>
129
+ <span class="count">3 of 34</span>
130
+ </div>
131
+ <div class="table">
132
+ <table>
133
+ <thead><tr><th>Item</th><th>State</th><th class="num">Value</th></tr></thead>
134
+ <tbody>
135
+ <tr><td>Accent</td><td><span class="pill accent">In review</span></td><td class="num">1,204</td></tr>
136
+ <tr><td>Success</td><td><span class="pill ok">Passed</span></td><td class="num">98.6 %</td></tr>
137
+ <tr><td>Warning and error</td><td><span class="pill warn">Pending</span> <span class="pill bad">Failed</span></td><td class="num">−12</td></tr>
138
+ </tbody>
139
+ </table>
140
+ </div>
141
+ </section>
142
+
143
+ <section>
144
+ <h2>Expandable records</h2>
145
+ <div class="rows">
146
+ <div class="rows-head" aria-hidden="true"><span>ID</span><span>Title</span><span>Group</span><span>State</span></div>
147
+ <details class="row">
148
+ <summary><b>#2088</b><span>Parallelize search and listing data fetching</span><span>Performance</span><span class="pills"><span class="pill">Draft</span><span class="pill ok">Build passed</span></span></summary>
149
+ <div class="row-detail">
150
+ <dl><dt class="label">Next step</dt><dd>Keep separate and wait for team review.</dd></dl>
151
+ <dl><dt class="label">Branch</dt><dd><code>refactor/search-plp-parallel-fetch</code></dd></dl>
152
+ </div>
153
+ </details>
154
+ <details class="row closed">
155
+ <summary><b>#2086</b><span>Temporary workspace configuration</span><span>Temporary</span><span class="pills"><span class="pill">Closed</span></span></summary>
156
+ <div class="row-detail"><dl><dt class="label">Reason</dt><dd>Closed without merge by the owner.</dd></dl></div>
157
+ </details>
158
+ </div>
159
+ </section>
160
+
161
+ <section class="prose">
162
+ <h2>Reading text</h2>
163
+ <p>Body copy stays near 68 characters per line. Use <code>code</code> for identifiers and the <span class="label">label</span> style for metadata.</p>
164
+ <div class="note"><b>Note.</b><p>A note marks one consequential statement. It is not a container for ordinary paragraphs.</p></div>
165
+ </section>
166
+
167
+ <footer><span>Source and date of the data go here.</span></footer>
168
+ </main>
169
+ </body>
170
+ </html>
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ import { canonicalResource } from '@yargote/artifacts-contracts/http';
11
11
  import { createArtifactHttpClient, HttpError, prepareArtifactEdit } from '@yargote/artifacts-client';
12
12
  import { accessToken, login, logout } from "./auth.js";
13
13
  import { loadOperation, retainOperation, submitOperation } from "./operations.js";
14
+ import { runSkillsCommand } from "./skills.js";
14
15
  const help = `Usage: artifacts COMMAND [OPTIONS]
15
16
 
16
17
  Global options:
@@ -21,13 +22,18 @@ Global options:
21
22
  --version Print the installed package version
22
23
 
23
24
  Commands:
25
+ skills [list]
26
+ skills get NAME [--full]
27
+ skills get --all [--full]
28
+ skills path [NAME]
29
+ skills install [NAME] [--agent claude] [--global] [--force]
24
30
  login [--no-browser]
25
31
  logout
26
32
  whoami
27
33
  create --snapshot FILE [--request-id ID] [--response full|compact]
28
34
  create --command FILE
29
35
  list [--limit N] [--cursor CURSOR]
30
- read ID [--revision REV] [--response full|compact] [--out FILE]
36
+ read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
31
37
  update ID --snapshot FILE --expected-revision REV [--request-id ID]
32
38
  [--response full|compact]
33
39
  update [ID] --command FILE
@@ -41,21 +47,29 @@ Commands:
41
47
  Mutation command files contain the complete create, update, or publish JSON.
42
48
  Mutation request IDs default to a random UUID. Commands are retained before
43
49
  submission. Retry sends the exact retained command for the same server and owner.
44
- Read --out saves the complete artifact JSON. Export --out saves the ZIP.
50
+ Read --out saves the returned artifact JSON. Export --out saves the ZIP.
51
+ Read --quiet requires --out and suppresses successful JSON stdout.
45
52
  Output files are created exclusively and never overwrite an existing path.
46
53
  JSON results go to stdout. Diagnostics and recovery guidance go to stderr.
47
54
  ARTIFACTS_ACCESS_TOKEN supplies a token for automation without local login.
55
+ Skills commands work offline without a server, login, or private state.
56
+ They print plain text. Global server and state options are accepted and ignored.
57
+ Skills install defaults to artifacts-cli under .agents/skills in the current
58
+ directory. --agent claude uses .claude/skills. --global uses your home directory.
59
+ The installed discovery file loads instructions through artifacts skills get.
60
+ Identical files stay unchanged. Differing files require --force.
48
61
  `;
49
62
  class UsageError extends Error {
50
63
  }
51
64
  const commandOptions = {
52
65
  login: ['no-browser'], logout: [], whoami: [],
53
66
  create: ['snapshot', 'request-id', 'response', 'command'],
54
- list: ['limit', 'cursor'], read: ['revision', 'response', 'out'],
67
+ list: ['limit', 'cursor'], read: ['revision', 'response', 'out', 'quiet'],
55
68
  update: ['snapshot', 'expected-revision', 'request-id', 'response', 'command'],
56
69
  edit: ['changes', 'request-id'], history: ['limit', 'before-sequence', 'summary'],
57
70
  publish: ['revision', 'expected-version', 'request-id', 'command'],
58
71
  export: ['revision', 'out'], retry: [],
72
+ skills: ['full', 'all', 'agent', 'global', 'force'],
59
73
  };
60
74
  async function jsonFile(path) {
61
75
  const file = await open(path, constants.O_RDONLY);
@@ -88,7 +102,9 @@ async function main() {
88
102
  changes: { type: 'string' }, 'request-id': { type: 'string' }, response: { type: 'string' },
89
103
  revision: { type: 'string' }, 'expected-revision': { type: 'string' }, 'expected-version': { type: 'string' },
90
104
  limit: { type: 'string' }, cursor: { type: 'string' }, 'before-sequence': { type: 'string' },
91
- summary: { type: 'boolean' }, out: { type: 'string' },
105
+ summary: { type: 'boolean' }, out: { type: 'string' }, quiet: { type: 'boolean' },
106
+ full: { type: 'boolean' }, all: { type: 'boolean' }, agent: { type: 'string' },
107
+ global: { type: 'boolean' }, force: { type: 'boolean' },
92
108
  },
93
109
  });
94
110
  const seen = new Set();
@@ -114,6 +130,8 @@ async function main() {
114
130
  for (const option of seen)
115
131
  if (!['server', 'state-dir', 'help', ...commandOptions[command]].includes(option))
116
132
  throw new UsageError(`Option --${option} is not supported by ${command}`);
133
+ if (command === 'skills')
134
+ return runSkillsCommand(arguments_, values, seen);
117
135
  const resourceValue = values.server ?? process.env.ARTIFACTS_SERVER;
118
136
  if (!resourceValue)
119
137
  throw new UsageError('Specify --server RESOURCE or ARTIFACTS_SERVER. No production server is selected by default.');
@@ -145,6 +163,9 @@ async function main() {
145
163
  const response = values.response;
146
164
  if (response !== undefined && response !== 'full' && response !== 'compact')
147
165
  throw new UsageError('--response requires full or compact');
166
+ const quiet = values.quiet ?? false;
167
+ if (quiet)
168
+ required('out');
148
169
  let commandToken;
149
170
  const client = createArtifactHttpClient({ resource, accessToken: () => commandToken ??= accessToken({ resource, stateDir }) });
150
171
  async function mutate(mutation) {
@@ -191,7 +212,7 @@ async function main() {
191
212
  const result = await client.read({ artifactId: argument(), ...(values.revision === undefined ? {} : { revisionId: values.revision }), ...(response === undefined ? {} : { response }) });
192
213
  if (values.out)
193
214
  await writeOutput(values.out, `${JSON.stringify(result, null, 2)}\n`);
194
- return result;
215
+ return quiet ? undefined : result;
195
216
  }
196
217
  case 'update': {
197
218
  if (values.command)
@@ -0,0 +1,9 @@
1
+ interface SkillsOptions {
2
+ full?: boolean;
3
+ all?: boolean;
4
+ agent?: string;
5
+ global?: boolean;
6
+ force?: boolean;
7
+ }
8
+ export declare function runSkillsCommand(positionals: string[], options: SkillsOptions, seen: ReadonlySet<string>): Promise<void>;
9
+ export {};
package/src/skills.js ADDED
@@ -0,0 +1,181 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, readFile, readdir, rename, rm } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ const catalogDirectory = resolve(fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../../../skills/' : '../skills/', import.meta.url)));
8
+ function parseCommand(positionals, options, seen) {
9
+ const [kind = 'list', ...arguments_] = positionals;
10
+ const allowed = { list: [], get: ['all', 'full'], path: [], install: ['agent', 'global', 'force'] };
11
+ if (!Object.hasOwn(allowed, kind))
12
+ throw new TypeError(`Unknown skills command ${kind}. Use artifacts --help for the reference.`);
13
+ for (const option of seen) {
14
+ if (!['server', 'state-dir', ...allowed[kind]].includes(option))
15
+ throw new TypeError(`Option --${option} is not supported by skills ${kind}`);
16
+ }
17
+ switch (kind) {
18
+ case 'list':
19
+ if (arguments_.length)
20
+ throw new TypeError('skills list does not accept positional arguments');
21
+ return { kind };
22
+ case 'get':
23
+ if (arguments_.length > 1 || (options.all ? arguments_.length !== 0 : arguments_.length !== 1))
24
+ throw new TypeError('skills get requires either one skill name or --all');
25
+ return { kind, selection: options.all ? { all: true } : { name: arguments_[0] }, full: options.full ?? false };
26
+ case 'path':
27
+ if (arguments_.length > 1)
28
+ throw new TypeError('skills path accepts zero or one skill name');
29
+ return { kind, name: arguments_[0] };
30
+ case 'install':
31
+ if (arguments_.length > 1)
32
+ throw new TypeError('skills install accepts zero or one skill name');
33
+ if (options.agent !== undefined && options.agent !== 'claude')
34
+ throw new TypeError('--agent requires claude. Omit --agent to install under .agents.');
35
+ return { kind, name: arguments_[0] ?? 'artifacts-cli', agent: options.agent === 'claude' ? 'claude' : 'agents', global: options.global ?? false, force: options.force ?? false };
36
+ default:
37
+ throw new TypeError(`Unknown skills command ${kind}`);
38
+ }
39
+ }
40
+ async function loadCatalog() {
41
+ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
42
+ const names = manifest.artifactsSkills;
43
+ if (!Array.isArray(names) || names.some((name) => typeof name !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) || new Set(names).size !== names.length)
44
+ throw new Error('Invalid bundled skills declaration');
45
+ return Promise.all(names.toSorted().map(async (name) => {
46
+ const directory = join(catalogDirectory, name);
47
+ const skillText = await readFile(join(directory, 'SKILL.md'));
48
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(skillText.toString('utf8'))?.[1];
49
+ const declaredName = frontmatter && /^name: (.+)$/m.exec(frontmatter)?.[1];
50
+ const description = frontmatter && /^description: (.+)$/m.exec(frontmatter)?.[1];
51
+ if (declaredName !== name || !description)
52
+ throw new Error(`Invalid bundled skill frontmatter at ${join(directory, 'SKILL.md')}`);
53
+ return { name, description, directory, skillText };
54
+ }));
55
+ }
56
+ async function fullFiles(directory, prefix = '') {
57
+ const entries = await readdir(directory, { withFileTypes: true });
58
+ const files = [];
59
+ for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name, 'en'))) {
60
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
61
+ if (entry.isDirectory())
62
+ files.push(...await fullFiles(join(directory, entry.name), relative));
63
+ else if (entry.isFile())
64
+ files.push(relative);
65
+ else
66
+ throw new Error(`Bundled skill contains a non-regular file at ${join(directory, entry.name)}`);
67
+ }
68
+ return files;
69
+ }
70
+ function isText(content) {
71
+ if (content.includes(0))
72
+ return false;
73
+ try {
74
+ new TextDecoder('utf-8', { fatal: true }).decode(content);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ async function labeledContent(skills, full) {
82
+ const chunks = [];
83
+ for (const skill of skills) {
84
+ const files = full ? ['SKILL.md', ...(await fullFiles(skill.directory)).filter((path) => path !== 'SKILL.md')] : ['SKILL.md'];
85
+ for (const path of files) {
86
+ const content = path === 'SKILL.md' ? skill.skillText : await readFile(join(skill.directory, path));
87
+ if (!isText(content)) {
88
+ chunks.push(Buffer.from(`=== ${skill.name}/${path} (binary, ${content.length} bytes) ===\n`));
89
+ continue;
90
+ }
91
+ chunks.push(Buffer.from(`=== ${skill.name}/${path} ===\n`), content);
92
+ if (content.at(-1) !== 10)
93
+ chunks.push(Buffer.from('\n'));
94
+ }
95
+ }
96
+ return Buffer.concat(chunks);
97
+ }
98
+ function discoveryText(skill) {
99
+ return `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\nBefore using this skill, run \`artifacts skills get ${skill.name} --full\`.\nLoad and follow the complete instructions, references, and templates bundled with the installed CLI version.\nRelative paths refer to the bundled directory reported by \`artifacts skills path ${skill.name}\`.\n`;
100
+ }
101
+ async function installSkill(skill, command) {
102
+ const destination = resolve(command.global ? homedir() : process.cwd(), command.agent === 'claude' ? '.claude' : '.agents', 'skills', skill.name, 'SKILL.md');
103
+ const content = Buffer.from(discoveryText(skill));
104
+ await mkdir(dirname(destination), { recursive: true });
105
+ try {
106
+ const file = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
107
+ try {
108
+ await file.writeFile(content);
109
+ }
110
+ finally {
111
+ await file.close();
112
+ }
113
+ return destination;
114
+ }
115
+ catch (error) {
116
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
117
+ throw error;
118
+ }
119
+ const existing = await lstat(destination);
120
+ if (!existing.isFile())
121
+ throw new TypeError(`Cannot install ${destination}. The destination must be a regular file, not a directory or symbolic link.`);
122
+ const file = await open(destination, constants.O_RDONLY | constants.O_NOFOLLOW);
123
+ let previous;
124
+ try {
125
+ if (!(await file.stat()).isFile())
126
+ throw new TypeError(`Cannot install ${destination}. The destination must be a regular file.`);
127
+ previous = await file.readFile();
128
+ }
129
+ finally {
130
+ await file.close();
131
+ }
132
+ if (previous.equals(content))
133
+ return destination;
134
+ if (!command.force)
135
+ throw new TypeError(`Cannot replace differing file ${destination}. Use --force to replace it.`);
136
+ const temporary = join(dirname(destination), `.SKILL.md.${randomUUID()}.tmp`);
137
+ try {
138
+ const replacement = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
139
+ try {
140
+ await replacement.writeFile(content);
141
+ }
142
+ finally {
143
+ await replacement.close();
144
+ }
145
+ const current = await lstat(destination);
146
+ if (!current.isFile())
147
+ throw new TypeError(`Cannot install ${destination}. The destination must be a regular file, not a directory or symbolic link.`);
148
+ if (current.ino !== existing.ino || current.dev !== existing.dev || current.mtimeMs !== existing.mtimeMs || current.ctimeMs !== existing.ctimeMs)
149
+ throw new TypeError(`Cannot replace ${destination}. The destination changed during installation. Run the command again.`);
150
+ await rename(temporary, destination);
151
+ }
152
+ finally {
153
+ await rm(temporary, { force: true });
154
+ }
155
+ return destination;
156
+ }
157
+ export async function runSkillsCommand(positionals, options, seen) {
158
+ const command = parseCommand(positionals, options, seen);
159
+ const skills = await loadCatalog();
160
+ function named(name) {
161
+ const skill = skills.find((entry) => entry.name === name);
162
+ if (!skill)
163
+ throw new TypeError(`Unknown bundled skill ${name}. Use artifacts skills list to inspect available skills.`);
164
+ return skill;
165
+ }
166
+ switch (command.kind) {
167
+ case 'list':
168
+ process.stdout.write(skills.map((skill) => `${skill.name}\t${skill.description}\n`).join(''));
169
+ return;
170
+ case 'get': {
171
+ const selection = 'all' in command.selection ? skills : [named(command.selection.name)];
172
+ process.stdout.write(!command.full && 'name' in command.selection ? selection[0].skillText : await labeledContent(selection, command.full));
173
+ return;
174
+ }
175
+ case 'path':
176
+ process.stdout.write(`${command.name === undefined ? catalogDirectory : named(command.name).directory}\n`);
177
+ return;
178
+ case 'install':
179
+ process.stdout.write(`${await installSkill(named(command.name), command)}\n`);
180
+ }
181
+ }