@yargote/artifacts-cli 0.0.0-stage → 0.1.0-beta.10

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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -2
  3. package/package.json +42 -4
  4. package/skills/artifacts-cli/SKILL.md +78 -0
  5. package/skills/artifacts-cli/references/commands.md +240 -0
  6. package/skills/artifacts-cli/references/verify-retained.md +119 -0
  7. package/skills/artifacts-cli/scripts/prepare-retained.mjs +149 -0
  8. package/skills/artifacts-design/SKILL.md +92 -0
  9. package/skills/artifacts-design/THIRD-PARTY-NOTICES.md +155 -0
  10. package/skills/artifacts-design/fonts/OFL.txt +96 -0
  11. package/skills/artifacts-design/fonts/bricolage-grotesque.woff2 +0 -0
  12. package/skills/artifacts-design/fonts/jetbrains-mono.woff2 +0 -0
  13. package/skills/artifacts-design/fonts/newsreader-italic.woff2 +0 -0
  14. package/skills/artifacts-design/fonts/newsreader.woff2 +0 -0
  15. package/skills/artifacts-design/fonts/public-sans-italic.woff2 +0 -0
  16. package/skills/artifacts-design/fonts/public-sans.woff2 +0 -0
  17. package/skills/artifacts-design/references/canvas.md +100 -0
  18. package/skills/artifacts-design/references/catppuccin.md +38 -0
  19. package/skills/artifacts-design/references/interface-checks.md +60 -0
  20. package/skills/artifacts-design/references/local-checks.md +70 -0
  21. package/skills/artifacts-design/references/visual-explanations.md +13 -0
  22. package/skills/artifacts-design/scripts/embed-fonts.mjs +246 -0
  23. package/skills/artifacts-design/scripts/verify-local.mjs +333 -0
  24. package/skills/artifacts-design/templates/starter.html +170 -0
  25. package/src/auth.d.ts +21 -0
  26. package/src/auth.js +415 -0
  27. package/src/credentials.d.ts +39 -0
  28. package/src/credentials.js +116 -0
  29. package/src/index.d.ts +2 -0
  30. package/src/index.js +343 -0
  31. package/src/operations.d.ts +174 -0
  32. package/src/operations.js +98 -0
  33. package/src/skills.d.ts +9 -0
  34. package/src/skills.js +181 -0
@@ -0,0 +1,116 @@
1
+ import { constants } from 'node:fs';
2
+ import { chmod, lstat, mkdir, open } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { z } from 'zod';
7
+ import { canonicalResource } from '@yargote/artifacts-contracts/http';
8
+ const registrationSchema = z.strictObject({ issuer: z.string(), clientId: z.string().min(1) });
9
+ const tokensSchema = z.strictObject({ accessToken: z.string().min(1), refreshToken: z.string().min(1).optional(), expiresAt: z.number().finite() });
10
+ const profileSchema = z.strictObject({
11
+ epoch: z.number().int().nonnegative(),
12
+ registration: registrationSchema.nullable(),
13
+ tokens: tokensSchema.nullable(),
14
+ refreshAttempt: z.string().nullable(),
15
+ });
16
+ async function privateFile(path) {
17
+ const file = await open(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
18
+ try {
19
+ const stat = await file.stat();
20
+ if (!stat.isFile() || stat.nlink !== 1 || (process.getuid && stat.uid !== process.getuid()))
21
+ throw new Error('Credential storage must be a private file owned by the current user');
22
+ await file.chmod(0o600);
23
+ }
24
+ finally {
25
+ await file.close();
26
+ }
27
+ }
28
+ export class CredentialStore {
29
+ data;
30
+ lock;
31
+ constructor(data, lock) {
32
+ this.data = data;
33
+ this.lock = lock;
34
+ }
35
+ static async open(stateDir) {
36
+ await mkdir(stateDir, { recursive: true, mode: 0o700 });
37
+ const directory = await lstat(stateDir);
38
+ if (!directory.isDirectory() || directory.isSymbolicLink() || (process.getuid && directory.uid !== process.getuid()))
39
+ throw new Error('Credential storage must be a private directory owned by the current user');
40
+ await chmod(stateDir, 0o700);
41
+ const dataPath = join(stateDir, 'credentials.sqlite');
42
+ const lockPath = join(stateDir, 'credentials-lock.sqlite');
43
+ await privateFile(dataPath);
44
+ await privateFile(lockPath);
45
+ const lock = new DatabaseSync(lockPath);
46
+ let data;
47
+ try {
48
+ data = new DatabaseSync(dataPath);
49
+ lock.exec('PRAGMA busy_timeout = 0;');
50
+ const store = new CredentialStore(data, lock);
51
+ await store.exclusive(() => {
52
+ data.exec('PRAGMA busy_timeout = 0; PRAGMA synchronous = FULL; PRAGMA secure_delete = ON;');
53
+ data.exec('CREATE TABLE IF NOT EXISTS profiles (resource TEXT PRIMARY KEY, profile TEXT NOT NULL) STRICT');
54
+ data.exec('CREATE TABLE IF NOT EXISTS selected_resource (id INTEGER PRIMARY KEY CHECK (id = 1), resource TEXT NOT NULL) STRICT');
55
+ });
56
+ return store;
57
+ }
58
+ catch (error) {
59
+ data?.close();
60
+ lock.close();
61
+ throw error;
62
+ }
63
+ }
64
+ async exclusive(work) {
65
+ const deadline = Date.now() + 45_000;
66
+ for (;;) {
67
+ try {
68
+ this.lock.exec('BEGIN IMMEDIATE');
69
+ break;
70
+ }
71
+ catch (error) {
72
+ if (!(error instanceof Error) || !('errcode' in error) || (error.errcode !== 5 && error.errcode !== 6))
73
+ throw error;
74
+ if (Date.now() >= deadline)
75
+ throw new Error('Credential storage is busy. Try the command again');
76
+ await delay(25);
77
+ }
78
+ }
79
+ try {
80
+ return await work();
81
+ }
82
+ finally {
83
+ this.lock.exec('ROLLBACK');
84
+ }
85
+ }
86
+ read(resource) {
87
+ const row = this.data.prepare('SELECT profile FROM profiles WHERE resource = ?').get(resource);
88
+ if (!row)
89
+ return { epoch: 0, registration: null, tokens: null, refreshAttempt: null };
90
+ return profileSchema.parse(JSON.parse(String(row.profile)));
91
+ }
92
+ write(resource, profile) {
93
+ this.data.prepare('INSERT INTO profiles(resource, profile) VALUES (?, ?) ON CONFLICT(resource) DO UPDATE SET profile = excluded.profile').run(resource, JSON.stringify(profileSchema.parse(profile)));
94
+ }
95
+ readSelectedResource() {
96
+ const row = this.data.prepare('SELECT resource FROM selected_resource WHERE id = 1').get();
97
+ return row ? canonicalResource(String(row.resource)) : undefined;
98
+ }
99
+ commitLogin(resource, epoch, tokens) {
100
+ canonicalResource(resource);
101
+ this.data.exec('BEGIN IMMEDIATE');
102
+ try {
103
+ const profile = this.read(resource);
104
+ if (profile.epoch !== epoch)
105
+ throw new Error('Login was superseded by another login or logout');
106
+ this.write(resource, { ...profile, tokens, refreshAttempt: null });
107
+ this.data.prepare('INSERT INTO selected_resource(id, resource) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET resource = excluded.resource').run(resource);
108
+ this.data.exec('COMMIT');
109
+ }
110
+ catch (error) {
111
+ this.data.exec('ROLLBACK');
112
+ throw error;
113
+ }
114
+ }
115
+ close() { this.data.close(); this.lock.close(); }
116
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/src/index.js ADDED
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { open, readFile } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { parseArgs } from 'node:util';
8
+ import { z } from 'zod';
9
+ import { createSchema, listSharesSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
10
+ import { canonicalResource } from '@yargote/artifacts-contracts/http';
11
+ import { createArtifactHttpClient, HttpError, prepareArtifactEdit } from '@yargote/artifacts-client';
12
+ import { accessToken, login, logout } from "./auth.js";
13
+ import { CredentialStore } from "./credentials.js";
14
+ import { loadOperation, retainOperation, submitOperation } from "./operations.js";
15
+ import { runSkillsCommand } from "./skills.js";
16
+ const help = `Usage: artifacts COMMAND [OPTIONS]
17
+
18
+ Global options:
19
+ --server RESOURCE Exact REST OAuth resource, or ARTIFACTS_SERVER
20
+ Defaults to the last successful login
21
+ --state-dir DIR Private local state, or ARTIFACTS_STATE_DIR
22
+ Default: ~/.local/state/artifacts
23
+ --help Print this reference
24
+ --version Print the installed package version
25
+
26
+ Commands:
27
+ skills [list]
28
+ skills get NAME [--full]
29
+ skills get --all [--full]
30
+ skills path [NAME]
31
+ skills install [NAME] [--agent claude] [--global] [--force]
32
+ login [--no-browser]
33
+ logout
34
+ whoami
35
+ create --snapshot FILE [--project NAME] [--slug SLUG] [--request-id ID]
36
+ [--response full|compact]
37
+ create --command FILE
38
+ list [--limit N] [--cursor CURSOR] [--project NAME]
39
+ projects
40
+ search QUERY [--project NAME] [--limit N] [--cursor CURSOR]
41
+ read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
42
+ read --slug SLUG [--project NAME] [--revision REV] [--response full|compact]
43
+ [--out FILE [--quiet]]
44
+ update ID --snapshot FILE --expected-revision REV [--request-id ID]
45
+ [--response full|compact]
46
+ update [ID] --command FILE
47
+ edit ID --changes FILE [--request-id ID]
48
+ history ID [--limit N] [--before-sequence N] [--summary]
49
+ publish ID --revision REV --expected-version N [--request-id ID]
50
+ publish [ID] --command FILE
51
+ export ID --revision REV --out FILE
52
+ move ID [--project NAME | --no-project] [--slug SLUG | --no-slug]
53
+ [--request-id ID]
54
+ share ID --email EMAIL [--notify] [--request-id ID]
55
+ shares ID
56
+ unshare ID SHARE_ID [--request-id ID]
57
+ retry REQUEST_ID
58
+
59
+ Mutation command files contain the complete create, update, or publish JSON.
60
+ Mutation request IDs default to a random UUID. Commands are retained before
61
+ submission. Retry sends the exact retained command for the same server and owner.
62
+ Projects group artifacts by name without regard to letter case. A project
63
+ exists while it contains an artifact. Move changes the project or the slug of
64
+ an artifact without creating a revision. It needs at least one of its four
65
+ options. --no-project removes the artifact from its project, and --no-slug
66
+ clears its slug. An option left out keeps that value, so a move to another
67
+ project keeps the slug.
68
+ A slug such as concepts/rag names an artifact within its project, or within
69
+ the unfiled artifacts without --project. Read --slug finds it without its ID.
70
+ Create or move fails with slug_conflict when the project already uses the slug.
71
+ Search finds owned Markdown and text artifacts whose title, description, or
72
+ content contains every term of QUERY, ignoring letter case and accents. Quote a
73
+ QUERY with spaces. Results are ordered by relevance with a **bold** snippet.
74
+ Read --out saves the returned artifact JSON. Export --out saves the ZIP.
75
+ Read --quiet requires --out and suppresses successful JSON stdout.
76
+ Output files are created exclusively and never overwrite an existing path.
77
+ JSON results go to stdout. Diagnostics and recovery guidance go to stderr.
78
+ Login accepts an MCP URL when its metadata advertises artifacts_rest_resource.
79
+ Split MCP and REST resources require this relation. Login remembers the verified
80
+ REST URL for later commands. --server takes precedence over ARTIFACTS_SERVER,
81
+ which takes precedence over the saved URL. Ordinary overrides do not replace
82
+ the saved URL. Logout keeps it for a later login.
83
+ Login checks <resource>/v1/identity before browser
84
+ authorization. Legacy /mcp
85
+ resources work when their REST API is at /mcp/v1. If the check fails, confirm
86
+ the configured REST URL with your service administrator.
87
+ Login errors report the phase, method, endpoint, and HTTP status when received.
88
+ Network failures have no HTTP status. Login verifies identity again after OAuth.
89
+ ARTIFACTS_ACCESS_TOKEN supplies a token for automation without local login.
90
+ Skills commands work offline without a server, login, or private state.
91
+ They print plain text. Global server and state options are accepted and ignored.
92
+ Skills install defaults to artifacts-cli under .agents/skills in the current
93
+ directory. --agent claude uses .claude/skills. --global uses your home directory.
94
+ The installed discovery file loads instructions through artifacts skills get.
95
+ Identical files stay unchanged. Differing files require --force.
96
+ `;
97
+ class UsageError extends Error {
98
+ }
99
+ const commandOptions = {
100
+ login: ['no-browser'], logout: [], whoami: [],
101
+ create: ['snapshot', 'project', 'slug', 'request-id', 'response', 'command'],
102
+ list: ['limit', 'cursor', 'project'], projects: [], search: ['project', 'limit', 'cursor'], read: ['slug', 'project', 'revision', 'response', 'out', 'quiet'],
103
+ update: ['snapshot', 'expected-revision', 'request-id', 'response', 'command'],
104
+ edit: ['changes', 'request-id'], history: ['limit', 'before-sequence', 'summary'],
105
+ publish: ['revision', 'expected-version', 'request-id', 'command'],
106
+ share: ['email', 'notify', 'request-id'], shares: [], unshare: ['request-id'],
107
+ export: ['revision', 'out'], move: ['project', 'no-project', 'slug', 'no-slug', 'request-id'], retry: [],
108
+ skills: ['full', 'all', 'agent', 'global', 'force'],
109
+ };
110
+ async function jsonFile(path) {
111
+ const file = await open(path, constants.O_RDONLY);
112
+ try {
113
+ const stat = await file.stat();
114
+ if (!stat.isFile() || stat.size > 8 * 1024 * 1024)
115
+ throw new UsageError('JSON input must be a regular file no larger than 8 MiB');
116
+ return JSON.parse(await file.readFile('utf8'));
117
+ }
118
+ finally {
119
+ await file.close();
120
+ }
121
+ }
122
+ async function writeOutput(path, bytes) {
123
+ const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
124
+ try {
125
+ await file.writeFile(bytes);
126
+ await file.sync();
127
+ }
128
+ finally {
129
+ await file.close();
130
+ }
131
+ }
132
+ async function main() {
133
+ const { values, positionals, tokens } = parseArgs({
134
+ allowPositionals: true, tokens: true,
135
+ options: {
136
+ server: { type: 'string' }, 'state-dir': { type: 'string' }, help: { type: 'boolean' }, version: { type: 'boolean' },
137
+ 'no-browser': { type: 'boolean' }, command: { type: 'string' }, snapshot: { type: 'string' },
138
+ changes: { type: 'string' }, 'request-id': { type: 'string' }, response: { type: 'string' },
139
+ revision: { type: 'string' }, 'expected-revision': { type: 'string' }, 'expected-version': { type: 'string' },
140
+ limit: { type: 'string' }, cursor: { type: 'string' }, 'before-sequence': { type: 'string' },
141
+ summary: { type: 'boolean' }, out: { type: 'string' }, quiet: { type: 'boolean' },
142
+ full: { type: 'boolean' }, all: { type: 'boolean' }, agent: { type: 'string' },
143
+ global: { type: 'boolean' }, force: { type: 'boolean' },
144
+ project: { type: 'string' }, 'no-project': { type: 'boolean' }, slug: { type: 'string' }, 'no-slug': { type: 'boolean' }, email: { type: 'string' }, notify: { type: 'boolean' },
145
+ },
146
+ });
147
+ const seen = new Set();
148
+ for (const token of tokens) {
149
+ if (token.kind !== 'option')
150
+ continue;
151
+ if (seen.has(token.name))
152
+ throw new UsageError(`Option --${token.name} cannot be repeated`);
153
+ seen.add(token.name);
154
+ }
155
+ if (values.version) {
156
+ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
157
+ process.stdout.write(`${manifest.version}\n`);
158
+ return;
159
+ }
160
+ if (values.help) {
161
+ process.stdout.write(help);
162
+ return;
163
+ }
164
+ const [command, ...arguments_] = positionals;
165
+ if (!command || !Object.hasOwn(commandOptions, command))
166
+ throw new UsageError('Specify a command. Use artifacts --help for the reference.');
167
+ for (const option of seen)
168
+ if (!['server', 'state-dir', 'help', ...commandOptions[command]].includes(option))
169
+ throw new UsageError(`Option --${option} is not supported by ${command}`);
170
+ if (command === 'skills')
171
+ return runSkillsCommand(arguments_, values, seen);
172
+ const stateDir = resolve(values['state-dir'] ?? process.env.ARTIFACTS_STATE_DIR ?? join(homedir(), '.local', 'state', 'artifacts'));
173
+ let resourceValue = values.server ?? process.env.ARTIFACTS_SERVER;
174
+ if (resourceValue === undefined) {
175
+ const store = await CredentialStore.open(stateDir);
176
+ try {
177
+ resourceValue = await store.exclusive(() => store.readSelectedResource());
178
+ }
179
+ finally {
180
+ store.close();
181
+ }
182
+ }
183
+ if (resourceValue === undefined)
184
+ throw new UsageError('No server is saved. Run artifacts login --server RESOURCE or set ARTIFACTS_SERVER.');
185
+ const resource = canonicalResource(resourceValue);
186
+ function argument(required = true) {
187
+ if (arguments_.length > 1 || (required && arguments_.length !== 1))
188
+ throw new UsageError(`${command} requires ${required ? 'one' : 'zero or one'} positional argument`);
189
+ return arguments_[0];
190
+ }
191
+ function none() {
192
+ if (arguments_.length)
193
+ throw new UsageError(`${command} does not accept positional arguments`);
194
+ }
195
+ function required(name) {
196
+ const value = values[name];
197
+ if (typeof value !== 'string' || !value)
198
+ throw new UsageError(`Specify --${name}`);
199
+ return value;
200
+ }
201
+ function integer(name) {
202
+ const value = values[name];
203
+ if (value === undefined)
204
+ return undefined;
205
+ if (typeof value !== 'string' || !/^(0|[1-9]\d*)$/.test(value) || !Number.isSafeInteger(Number(value)))
206
+ throw new UsageError(`--${name} requires a nonnegative safe integer`);
207
+ return Number(value);
208
+ }
209
+ function cleared(name, value, clear) {
210
+ if (value !== undefined && clear)
211
+ throw new UsageError(`Specify at most one of --${name} or --no-${name}`);
212
+ return clear ? null : value;
213
+ }
214
+ const response = values.response;
215
+ if (response !== undefined && response !== 'full' && response !== 'compact')
216
+ throw new UsageError('--response requires full or compact');
217
+ const quiet = values.quiet ?? false;
218
+ if (quiet)
219
+ required('out');
220
+ let commandToken;
221
+ const client = createArtifactHttpClient({ resource, accessToken: () => commandToken ??= accessToken({ resource, stateDir }) });
222
+ async function mutate(mutation) {
223
+ const owner = await client.identity();
224
+ const operation = await retainOperation(stateDir, resource, owner.userId, mutation);
225
+ process.stderr.write(`Retained operation ${operation.command.requestId}. Recover with artifacts retry ${operation.command.requestId} using the same server and state directory.\n`);
226
+ return submitOperation(client, operation);
227
+ }
228
+ async function commandFile(kind) {
229
+ const extra = [...seen].filter((option) => !['server', 'state-dir', 'command'].includes(option));
230
+ if (extra.length)
231
+ throw new UsageError('--command cannot be combined with mutation options');
232
+ const value = await jsonFile(required('command'));
233
+ if (kind === 'create') {
234
+ none();
235
+ return { kind, command: createSchema.parse(value) };
236
+ }
237
+ const mutation = kind === 'update' ? { kind, command: updateSchema.parse(value) } : { kind, command: publishSchema.parse(value) };
238
+ const id = argument(false);
239
+ if (id !== undefined && id !== mutation.command.artifactId)
240
+ throw new UsageError('The positional artifact ID must match the command file');
241
+ return mutation;
242
+ }
243
+ switch (command) {
244
+ case 'login':
245
+ none();
246
+ {
247
+ const result = await login({ resource, stateDir, noBrowser: values['no-browser'] ?? false, onAuthorizationUrl: (url) => { process.stderr.write(`Authorize this login in your browser:\n${url}\n`); } });
248
+ if (result.resource !== resource)
249
+ process.stderr.write(`REST resource: ${result.resource}\nThis verified REST URL is saved for subsequent commands.\n`);
250
+ return result.identity;
251
+ }
252
+ case 'logout':
253
+ none();
254
+ return logout({ resource, stateDir });
255
+ case 'whoami':
256
+ none();
257
+ return client.identity();
258
+ case 'create': {
259
+ if (values.command)
260
+ return mutate(await commandFile('create'));
261
+ none();
262
+ return mutate({ kind: 'create', command: createSchema.parse({ requestId: values['request-id'] ?? randomUUID(), snapshot: await jsonFile(required('snapshot')), ...(values.project === undefined ? {} : { project: values.project }), ...(values.slug === undefined ? {} : { slug: values.slug }), ...(response === undefined ? {} : { response }) }) });
263
+ }
264
+ case 'list':
265
+ none();
266
+ return client.list({ ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }), ...(values.project === undefined ? {} : { project: values.project }) });
267
+ case 'projects':
268
+ none();
269
+ return client.projects();
270
+ case 'search': return client.search({ query: argument(), ...(values.project === undefined ? {} : { project: values.project }), ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }) });
271
+ case 'read': {
272
+ if (values.slug === undefined && values.project !== undefined)
273
+ throw new UsageError('read --project requires --slug');
274
+ if ((values.slug === undefined) !== (arguments_.length === 1) || arguments_.length > 1)
275
+ throw new UsageError('read requires exactly one of an artifact ID or --slug SLUG');
276
+ const target = values.slug === undefined ? { artifactId: arguments_[0] } : { slug: values.slug, ...(values.project === undefined ? {} : { project: values.project }) };
277
+ const result = await client.read({ ...target, ...(values.revision === undefined ? {} : { revisionId: values.revision }), ...(response === undefined ? {} : { response }) });
278
+ if (values.out)
279
+ await writeOutput(values.out, `${JSON.stringify(result, null, 2)}\n`);
280
+ return quiet ? undefined : result;
281
+ }
282
+ case 'update': {
283
+ if (values.command)
284
+ return mutate(await commandFile('update'));
285
+ return mutate({ kind: 'update', command: updateSchema.parse({ artifactId: argument(), requestId: values['request-id'] ?? randomUUID(), expectedRevisionId: required('expected-revision'), snapshot: await jsonFile(required('snapshot')), ...(response === undefined ? {} : { response }) }) });
286
+ }
287
+ case 'edit': {
288
+ const artifactId = argument();
289
+ const changes = await jsonFile(required('changes'));
290
+ const input = await prepareArtifactEdit(client, { artifactId, requestId: values['request-id'] ?? randomUUID(), changes: changes });
291
+ return mutate({ kind: 'update', command: updateSchema.parse(input) });
292
+ }
293
+ case 'history': return client.history({ artifactId: argument(), ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values['before-sequence'] === undefined ? {} : { beforeSequence: integer('before-sequence') }), ...(values.summary ? { response: 'summary' } : {}) });
294
+ case 'publish': {
295
+ if (values.command)
296
+ return mutate(await commandFile('publish'));
297
+ required('expected-version');
298
+ return mutate({ kind: 'publish', command: publishSchema.parse({ artifactId: argument(), requestId: values['request-id'] ?? randomUUID(), revisionId: required('revision'), expectedPublicationVersion: integer('expected-version') }) });
299
+ }
300
+ case 'export': {
301
+ const output = required('out');
302
+ const archive = await client.export({ artifactId: argument(), revisionId: required('revision') });
303
+ await writeOutput(output, archive.bytes);
304
+ return { filename: archive.filename, path: resolve(output), mediaType: archive.mediaType, sha256: archive.sha256, bytes: archive.bytes.byteLength };
305
+ }
306
+ case 'move': {
307
+ const artifactId = argument();
308
+ const project = cleared('project', values.project, values['no-project']);
309
+ const slug = cleared('slug', values.slug, values['no-slug']);
310
+ if (project === undefined && slug === undefined)
311
+ throw new UsageError('move requires --project NAME, --no-project, --slug SLUG, or --no-slug');
312
+ return mutate({ kind: 'move', command: moveSchema.parse({ artifactId, requestId: values['request-id'] ?? randomUUID(), ...(project === undefined ? {} : { project }), ...(slug === undefined ? {} : { slug }) }) });
313
+ }
314
+ case 'share': return mutate({ kind: 'share', command: shareSchema.parse({ artifactId: argument(), requestId: values['request-id'] ?? randomUUID(), email: required('email'), ...(values.notify === undefined ? {} : { notify: values.notify }) }) });
315
+ case 'shares': return client.listShares(listSharesSchema.parse({ artifactId: argument() }));
316
+ case 'unshare': {
317
+ if (arguments_.length !== 2)
318
+ throw new UsageError('unshare requires two positional arguments');
319
+ return mutate({ kind: 'unshare', command: unshareSchema.parse({ artifactId: arguments_[0], shareId: arguments_[1], requestId: values['request-id'] ?? randomUUID() }) });
320
+ }
321
+ case 'retry': {
322
+ const requestId = argument();
323
+ const owner = await client.identity();
324
+ const operation = await loadOperation(stateDir, requestId, resource, owner.userId);
325
+ process.stderr.write(`Replaying retained ${operation.kind} operation ${requestId}. Replay confirms the original operation and does not establish current state.\n`);
326
+ return submitOperation(client, operation);
327
+ }
328
+ }
329
+ }
330
+ try {
331
+ const result = await main();
332
+ if (result !== undefined)
333
+ process.stdout.write(`${JSON.stringify(result)}\n`);
334
+ }
335
+ catch (error) {
336
+ const body = error instanceof HttpError
337
+ ? { code: error.code, message: error.message, status: error.status, details: error.details }
338
+ : error instanceof z.ZodError
339
+ ? { code: 'invalid_input', message: 'Input does not match the artifact command schema', details: { issues: error.issues } }
340
+ : { code: error instanceof UsageError || error instanceof TypeError || error instanceof SyntaxError ? 'invalid_arguments' : 'local_error', message: error instanceof Error ? error.message : 'Command failed', details: {} };
341
+ process.stderr.write(`${JSON.stringify({ error: body })}\n`);
342
+ process.exitCode = 1;
343
+ }
@@ -0,0 +1,174 @@
1
+ import { z } from 'zod';
2
+ import { createSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
3
+ import type { ArtifactHttpClient } from '@yargote/artifacts-client';
4
+ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
5
+ kind: z.ZodLiteral<"create">;
6
+ command: z.ZodObject<{
7
+ requestId: z.ZodString;
8
+ snapshot: z.ZodObject<{
9
+ metadata: z.ZodObject<{
10
+ title: z.ZodString;
11
+ description: z.ZodString;
12
+ custom: z.ZodOptional<z.ZodJSONSchema>;
13
+ }, z.core.$strict>;
14
+ content: z.ZodObject<{
15
+ kind: z.ZodEnum<{
16
+ text: "text";
17
+ html: "html";
18
+ markdown: "markdown";
19
+ design: "design";
20
+ }>;
21
+ text: z.ZodString;
22
+ }, z.core.$strict>;
23
+ files: z.ZodArray<z.ZodObject<{
24
+ path: z.ZodString;
25
+ mediaType: z.ZodString;
26
+ encoding: z.ZodEnum<{
27
+ utf8: "utf8";
28
+ base64: "base64";
29
+ }>;
30
+ data: z.ZodString;
31
+ }, z.core.$strict>>;
32
+ }, z.core.$strict>;
33
+ project: z.ZodOptional<z.ZodString>;
34
+ slug: z.ZodOptional<z.ZodString>;
35
+ response: z.ZodOptional<z.ZodEnum<{
36
+ full: "full";
37
+ compact: "compact";
38
+ }>>;
39
+ }, z.core.$strict>;
40
+ version: z.ZodLiteral<1>;
41
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
42
+ ownerId: z.ZodString;
43
+ }, z.core.$strict>, z.ZodObject<{
44
+ kind: z.ZodLiteral<"update">;
45
+ command: z.ZodObject<{
46
+ requestId: z.ZodString;
47
+ artifactId: z.ZodString;
48
+ expectedRevisionId: z.ZodString;
49
+ snapshot: z.ZodObject<{
50
+ metadata: z.ZodObject<{
51
+ title: z.ZodString;
52
+ description: z.ZodString;
53
+ custom: z.ZodOptional<z.ZodJSONSchema>;
54
+ }, z.core.$strict>;
55
+ content: z.ZodObject<{
56
+ kind: z.ZodEnum<{
57
+ text: "text";
58
+ html: "html";
59
+ markdown: "markdown";
60
+ design: "design";
61
+ }>;
62
+ text: z.ZodString;
63
+ }, z.core.$strict>;
64
+ files: z.ZodArray<z.ZodObject<{
65
+ path: z.ZodString;
66
+ mediaType: z.ZodString;
67
+ encoding: z.ZodEnum<{
68
+ utf8: "utf8";
69
+ base64: "base64";
70
+ }>;
71
+ data: z.ZodString;
72
+ }, z.core.$strict>>;
73
+ }, z.core.$strict>;
74
+ response: z.ZodOptional<z.ZodEnum<{
75
+ full: "full";
76
+ compact: "compact";
77
+ }>>;
78
+ }, z.core.$strict>;
79
+ version: z.ZodLiteral<1>;
80
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
81
+ ownerId: z.ZodString;
82
+ }, z.core.$strict>, z.ZodObject<{
83
+ kind: z.ZodLiteral<"publish">;
84
+ command: z.ZodObject<{
85
+ requestId: z.ZodString;
86
+ artifactId: z.ZodString;
87
+ revisionId: z.ZodString;
88
+ expectedPublicationVersion: z.ZodNumber;
89
+ }, z.core.$strict>;
90
+ version: z.ZodLiteral<1>;
91
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
92
+ ownerId: z.ZodString;
93
+ }, z.core.$strict>, z.ZodObject<{
94
+ kind: z.ZodLiteral<"move">;
95
+ command: z.ZodObject<{
96
+ requestId: z.ZodString;
97
+ artifactId: z.ZodString;
98
+ project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
99
+ slug: z.ZodOptional<z.ZodNullable<z.ZodString>>;
100
+ }, z.core.$strict>;
101
+ version: z.ZodLiteral<1>;
102
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
103
+ ownerId: z.ZodString;
104
+ }, z.core.$strict>, z.ZodObject<{
105
+ kind: z.ZodLiteral<"share">;
106
+ command: z.ZodObject<{
107
+ requestId: z.ZodString;
108
+ artifactId: z.ZodString;
109
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
110
+ notify: z.ZodOptional<z.ZodBoolean>;
111
+ requireNew: z.ZodOptional<z.ZodLiteral<true>>;
112
+ }, z.core.$strict>;
113
+ version: z.ZodLiteral<1>;
114
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
115
+ ownerId: z.ZodString;
116
+ }, z.core.$strict>, z.ZodObject<{
117
+ kind: z.ZodLiteral<"unshare">;
118
+ command: z.ZodObject<{
119
+ requestId: z.ZodString;
120
+ artifactId: z.ZodString;
121
+ shareId: z.ZodString;
122
+ }, z.core.$strict>;
123
+ version: z.ZodLiteral<1>;
124
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
125
+ ownerId: z.ZodString;
126
+ }, z.core.$strict>], "kind">;
127
+ export type PreparedOperation = z.infer<typeof operationSchema>;
128
+ export type Mutation = {
129
+ kind: 'create';
130
+ command: z.input<typeof createSchema>;
131
+ } | {
132
+ kind: 'update';
133
+ command: z.input<typeof updateSchema>;
134
+ } | {
135
+ kind: 'publish';
136
+ command: z.input<typeof publishSchema>;
137
+ } | {
138
+ kind: 'move';
139
+ command: z.input<typeof moveSchema>;
140
+ } | {
141
+ kind: 'share';
142
+ command: z.input<typeof shareSchema>;
143
+ } | {
144
+ kind: 'unshare';
145
+ command: z.input<typeof unshareSchema>;
146
+ };
147
+ export declare function operationPath(stateDir: string, resource: string, requestId: string): string;
148
+ export declare function retainOperation(stateDir: string, resource: string, ownerId: string, mutation: Mutation): Promise<PreparedOperation>;
149
+ export declare function loadOperation(stateDir: string, requestId: string, resource: string, ownerId: string): Promise<PreparedOperation>;
150
+ export declare function submitOperation(client: ArtifactHttpClient, operation: PreparedOperation): Promise<void> | Promise<{
151
+ shareId: string;
152
+ artifactId: string;
153
+ kind: "email";
154
+ value: string;
155
+ createdAt: string;
156
+ notification?: {
157
+ status: "requested";
158
+ } | {
159
+ status: "skipped";
160
+ reason: "disabled";
161
+ } | {
162
+ status: "skipped";
163
+ reason: "rate_limited";
164
+ retryAt: string;
165
+ } | undefined;
166
+ }> | Promise<{
167
+ artifactId: string;
168
+ project: string | null;
169
+ }> | Promise<{
170
+ revisionId: string;
171
+ version: number;
172
+ url: string;
173
+ artifactId: string;
174
+ }>;