@yargote/artifacts-cli 0.1.0-beta.1 → 0.1.0-beta.11
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/README.md +167 -19
- package/package.json +9 -4
- package/skills/artifacts-cli/SKILL.md +80 -0
- package/skills/artifacts-cli/references/commands.md +250 -0
- package/skills/artifacts-cli/references/verify-retained.md +119 -0
- package/skills/artifacts-cli/scripts/prepare-retained.mjs +149 -0
- package/skills/artifacts-design/SKILL.md +92 -0
- package/skills/artifacts-design/THIRD-PARTY-NOTICES.md +155 -0
- package/skills/artifacts-design/fonts/OFL.txt +96 -0
- package/skills/artifacts-design/fonts/bricolage-grotesque.woff2 +0 -0
- package/skills/artifacts-design/fonts/jetbrains-mono.woff2 +0 -0
- package/skills/artifacts-design/fonts/newsreader-italic.woff2 +0 -0
- package/skills/artifacts-design/fonts/newsreader.woff2 +0 -0
- package/skills/artifacts-design/fonts/public-sans-italic.woff2 +0 -0
- package/skills/artifacts-design/fonts/public-sans.woff2 +0 -0
- package/skills/artifacts-design/references/canvas.md +100 -0
- package/skills/artifacts-design/references/catppuccin.md +38 -0
- package/skills/artifacts-design/references/interface-checks.md +60 -0
- package/skills/artifacts-design/references/local-checks.md +70 -0
- package/skills/artifacts-design/references/visual-explanations.md +13 -0
- package/skills/artifacts-design/scripts/embed-fonts.mjs +246 -0
- package/skills/artifacts-design/scripts/verify-local.mjs +333 -0
- package/skills/artifacts-design/templates/starter.html +170 -0
- package/src/auth.d.ts +4 -1
- package/src/auth.js +184 -45
- package/src/credentials.d.ts +2 -0
- package/src/credentials.js +22 -0
- package/src/index.js +153 -31
- package/src/operations.d.ts +155 -38
- package/src/operations.js +11 -1
- package/src/skills.d.ts +9 -0
- package/src/skills.js +181 -0
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
|
+
}
|