artifact-cafe 0.0.0 → 0.0.1

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 (3) hide show
  1. package/dist/index.js +760 -0
  2. package/package.json +24 -6
  3. package/README.md +0 -11
package/dist/index.js ADDED
@@ -0,0 +1,760 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { defineCommand, runMain } from "citty";
5
+
6
+ // src/comments.ts
7
+ import path2 from "path";
8
+
9
+ // ../shared/src/publish.ts
10
+ import { z } from "zod";
11
+ function isSafeArtifactPath(p) {
12
+ if (p.length === 0 || p.length > 1024) return false;
13
+ if (p.includes("\\") || p.includes("\0")) return false;
14
+ if (p.startsWith("/") || p.startsWith("~")) return false;
15
+ return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
16
+ }
17
+ var manifestFileSchema = z.object({
18
+ path: z.string().min(1).max(1024).refine(isSafeArtifactPath, { message: "path must be a safe relative path" }),
19
+ hash: z.string().regex(/^sha256_[a-f0-9]{64}$/),
20
+ size: z.number().int().positive(),
21
+ mimeType: z.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" })
22
+ });
23
+ var publishSourceSchema = z.object({
24
+ type: z.literal("cli"),
25
+ agent: z.string().optional(),
26
+ runId: z.string().optional()
27
+ });
28
+ var createPublishSessionSchema = z.object({
29
+ artifactId: z.string().nullable(),
30
+ title: z.string().max(200).optional(),
31
+ entryPath: z.string().optional(),
32
+ files: z.array(manifestFileSchema).min(1).max(1e3),
33
+ source: publishSourceSchema
34
+ });
35
+ var publishSessionResponseSchema = z.object({
36
+ publishSessionId: z.string(),
37
+ artifactId: z.string(),
38
+ versionId: z.string(),
39
+ filesToUpload: z.array(
40
+ z.object({
41
+ path: z.string(),
42
+ uploadUrl: z.string().url(),
43
+ method: z.literal("PUT")
44
+ })
45
+ ),
46
+ skippedFiles: z.array(z.string()),
47
+ expiresAt: z.string()
48
+ });
49
+ var finalizeResponseSchema = z.object({
50
+ artifactId: z.string(),
51
+ versionId: z.string(),
52
+ versionNumber: z.number().int().positive(),
53
+ url: z.string().url(),
54
+ // Returned only on first publish of a new artifact (one-time).
55
+ claimUrl: z.string().url().optional(),
56
+ publishToken: z.string().optional()
57
+ });
58
+ var artifactResponseSchema = z.object({
59
+ id: z.string(),
60
+ slug: z.string(),
61
+ title: z.string(),
62
+ currentVersion: z.object({
63
+ id: z.string(),
64
+ versionNumber: z.number().int().positive(),
65
+ entryUrl: z.string().url()
66
+ }).nullable(),
67
+ claimed: z.boolean()
68
+ });
69
+
70
+ // ../shared/src/comments.ts
71
+ import { z as z2 } from "zod";
72
+ var elementSnapshotSchema = z2.object({
73
+ tag: z2.string().min(1).max(128),
74
+ id: z2.string().max(256).optional(),
75
+ classes: z2.string().max(256).optional(),
76
+ text: z2.string().max(200).optional()
77
+ });
78
+ var pointAnchorSchema = z2.object({
79
+ type: z2.literal("point"),
80
+ // elementPath is the primary anchor; x/y are offsets within the
81
+ // element's box, normalized 0..1 (SPEC.md §8.1).
82
+ elementPath: z2.string().max(2048),
83
+ x: z2.number().min(0).max(1),
84
+ y: z2.number().min(0).max(1),
85
+ viewportWidth: z2.number().int().positive().max(1e5),
86
+ viewportHeight: z2.number().int().positive().max(1e5),
87
+ element: elementSnapshotSchema.optional()
88
+ });
89
+ var textAnchorSchema = z2.object({
90
+ type: z2.literal("text"),
91
+ quote: z2.string().min(1).max(1e3),
92
+ prefix: z2.string().max(200).optional(),
93
+ suffix: z2.string().max(200).optional(),
94
+ startOffset: z2.number().int().nonnegative().max(1e8),
95
+ endOffset: z2.number().int().nonnegative().max(1e8),
96
+ elementPath: z2.string().max(2048)
97
+ });
98
+ var anchorSchema = z2.discriminatedUnion("type", [
99
+ pointAnchorSchema,
100
+ textAnchorSchema
101
+ ]);
102
+ var guestSchema = z2.object({
103
+ guestId: z2.string().min(16),
104
+ displayName: z2.string().min(1).max(64)
105
+ });
106
+ var createCommentThreadSchema = z2.object({
107
+ versionId: z2.string(),
108
+ guest: guestSchema.optional(),
109
+ anchor: anchorSchema,
110
+ body: z2.string().min(1).max(1e4)
111
+ });
112
+ var createCommentMessageSchema = z2.object({
113
+ guest: guestSchema.optional(),
114
+ body: z2.string().min(1).max(1e4)
115
+ });
116
+ var threadActionSchema = z2.object({
117
+ guest: guestSchema.optional()
118
+ });
119
+ var commentAuthorSchema = z2.object({
120
+ guestId: z2.string().nullable(),
121
+ displayName: z2.string()
122
+ });
123
+ var commentMessageSchema = z2.object({
124
+ id: z2.string(),
125
+ body: z2.string(),
126
+ author: commentAuthorSchema,
127
+ createdAt: z2.string()
128
+ });
129
+ var commentThreadSchema = z2.object({
130
+ id: z2.string(),
131
+ artifactId: z2.string(),
132
+ versionId: z2.string(),
133
+ anchor: anchorSchema,
134
+ status: z2.enum(["open", "resolved"]),
135
+ createdBy: commentAuthorSchema,
136
+ createdAt: z2.string(),
137
+ messages: z2.array(commentMessageSchema)
138
+ });
139
+ var commentThreadListSchema = z2.object({
140
+ threads: z2.array(commentThreadSchema)
141
+ });
142
+ var commentExportQuerySchema = z2.object({
143
+ status: z2.enum(["open", "resolved", "all"]).default("open"),
144
+ version: z2.enum(["current", "all"]).default("current")
145
+ });
146
+ var commentExportReplySchema = z2.object({
147
+ author: z2.string(),
148
+ body: z2.string(),
149
+ createdAt: z2.string()
150
+ });
151
+ var commentExportThreadSchema = z2.object({
152
+ id: z2.string(),
153
+ status: z2.enum(["open", "resolved"]),
154
+ versionNumber: z2.number().int().positive(),
155
+ author: z2.string(),
156
+ body: z2.string(),
157
+ anchor: anchorSchema,
158
+ createdAt: z2.string(),
159
+ replies: z2.array(commentExportReplySchema)
160
+ });
161
+ var commentExportSchema = z2.object({
162
+ artifactId: z2.string(),
163
+ // The artifact's current version (null before the first finalize).
164
+ versionNumber: z2.number().int().positive().nullable(),
165
+ comments: z2.array(commentExportThreadSchema)
166
+ });
167
+ function elementLabel(el) {
168
+ if (el.id) return `<${el.tag}#${el.id}>`;
169
+ const classes = el.classes ? el.classes.split(" ").slice(0, 2) : [];
170
+ return classes.length ? `<${el.tag}.${classes.join(".")}>` : `<${el.tag}>`;
171
+ }
172
+
173
+ // ../shared/src/errors.ts
174
+ import { z as z3 } from "zod";
175
+ var apiErrorSchema = z3.object({
176
+ code: z3.string(),
177
+ message: z3.string(),
178
+ retryAfter: z3.number().int().positive().optional(),
179
+ docsUrl: z3.string().url()
180
+ });
181
+
182
+ // ../shared/src/versions.ts
183
+ import { z as z4 } from "zod";
184
+ var versionSourceSchema = z4.object({
185
+ type: z4.string(),
186
+ agentName: z4.string().nullable()
187
+ });
188
+ var versionChangesSchema = z4.object({
189
+ added: z4.array(z4.string()),
190
+ changed: z4.array(z4.string()),
191
+ removed: z4.array(z4.string())
192
+ });
193
+ var versionSummarySchema = z4.object({
194
+ id: z4.string(),
195
+ versionNumber: z4.number().int().positive(),
196
+ createdAt: z4.string(),
197
+ entryUrl: z4.string().url(),
198
+ fileCount: z4.number().int().nonnegative(),
199
+ totalSizeBytes: z4.number().int().nonnegative(),
200
+ source: versionSourceSchema,
201
+ isCurrent: z4.boolean(),
202
+ // Absent for the first version — there is no previous version to diff against.
203
+ changes: versionChangesSchema.nullable()
204
+ });
205
+ var versionListResponseSchema = z4.object({
206
+ versions: z4.array(versionSummarySchema)
207
+ });
208
+
209
+ // ../shared/src/owner.ts
210
+ import { z as z5 } from "zod";
211
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
212
+ var claimResponseSchema = z5.object({
213
+ artifactId: z5.string(),
214
+ slug: z5.string(),
215
+ title: z5.string(),
216
+ url: z5.string().url()
217
+ });
218
+ var updateArtifactSchema = z5.object({
219
+ title: z5.string().min(1).max(200).optional(),
220
+ slug: z5.string().min(3).max(63).regex(SLUG_PATTERN, {
221
+ message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
222
+ }).optional()
223
+ }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
224
+ message: "provide at least one of title, slug"
225
+ });
226
+ var deleteArtifactResponseSchema = z5.object({
227
+ deleted: z5.literal(true)
228
+ });
229
+
230
+ // src/api.ts
231
+ var ApiRequestError = class extends Error {
232
+ constructor(status, code, message, retryAfter) {
233
+ super(message);
234
+ this.status = status;
235
+ this.code = code;
236
+ this.retryAfter = retryAfter;
237
+ this.name = "ApiRequestError";
238
+ }
239
+ status;
240
+ code;
241
+ retryAfter;
242
+ };
243
+ async function request(url, init, parse) {
244
+ let res;
245
+ try {
246
+ res = await fetch(url, init);
247
+ } catch (err) {
248
+ throw new Error(
249
+ `Could not reach ${new URL(url).origin} \u2014 is the API up? (${err instanceof Error ? err.message : err})`
250
+ );
251
+ }
252
+ const body = await res.json().catch(() => null);
253
+ if (!res.ok) {
254
+ const parsed = apiErrorSchema.safeParse(body);
255
+ if (parsed.success) {
256
+ throw new ApiRequestError(
257
+ res.status,
258
+ parsed.data.code,
259
+ parsed.data.message,
260
+ parsed.data.retryAfter
261
+ );
262
+ }
263
+ throw new ApiRequestError(res.status, "unknown_error", `API returned ${res.status}`);
264
+ }
265
+ return parse(body);
266
+ }
267
+ function createApiClient(apiUrl, publishToken) {
268
+ const base = apiUrl.replace(/\/$/, "");
269
+ const headers = { "content-type": "application/json" };
270
+ if (publishToken) headers.authorization = `Bearer ${publishToken}`;
271
+ return {
272
+ async createSession(body) {
273
+ return request(
274
+ `${base}/api/v1/publish`,
275
+ { method: "POST", headers, body: JSON.stringify(body) },
276
+ (d) => publishSessionResponseSchema.parse(d)
277
+ );
278
+ },
279
+ async finalize(publishSessionId) {
280
+ return request(
281
+ `${base}/api/v1/publish/${publishSessionId}/finalize`,
282
+ { method: "POST", headers },
283
+ (d) => finalizeResponseSchema.parse(d)
284
+ );
285
+ },
286
+ async getArtifact(artifactId) {
287
+ return request(
288
+ `${base}/api/v1/artifacts/${artifactId}`,
289
+ { method: "GET" },
290
+ (d) => artifactResponseSchema.parse(d)
291
+ );
292
+ },
293
+ async exportComments(artifactId, query) {
294
+ const params = new URLSearchParams({ status: query.status, version: query.version });
295
+ return request(
296
+ `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
297
+ { method: "GET" },
298
+ (d) => commentExportSchema.parse(d)
299
+ );
300
+ }
301
+ };
302
+ }
303
+ async function uploadFile(uploadUrl, content, mimeType) {
304
+ const res = await fetch(uploadUrl, {
305
+ method: "PUT",
306
+ headers: { "content-type": mimeType, "content-length": String(content.byteLength) },
307
+ body: new Uint8Array(content)
308
+ });
309
+ if (!res.ok) {
310
+ throw new Error(`Upload failed (${res.status}): ${(await res.text()).slice(0, 200)}`);
311
+ }
312
+ }
313
+
314
+ // src/config.ts
315
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "fs";
316
+ import path from "path";
317
+ import { z as z6 } from "zod";
318
+ var configSchema = z6.object({
319
+ artifactId: z6.string(),
320
+ slug: z6.string(),
321
+ url: z6.string(),
322
+ apiUrl: z6.string(),
323
+ publishToken: z6.string()
324
+ });
325
+ var CONFIG_DIR = ".artifactcafe";
326
+ var CONFIG_FILE = "config.json";
327
+ function readConfig(dir) {
328
+ const file = path.join(dir, CONFIG_DIR, CONFIG_FILE);
329
+ if (!existsSync(file)) return null;
330
+ try {
331
+ return configSchema.parse(JSON.parse(readFileSync(file, "utf8")));
332
+ } catch {
333
+ return null;
334
+ }
335
+ }
336
+ function writeConfig(dir, config) {
337
+ mkdirSync(path.join(dir, CONFIG_DIR), { recursive: true });
338
+ writeFileSync(
339
+ path.join(dir, CONFIG_DIR, CONFIG_FILE),
340
+ JSON.stringify(config, null, 2) + "\n",
341
+ { mode: 384 }
342
+ );
343
+ ensureGitignored(dir);
344
+ }
345
+ function ensureGitignored(dir) {
346
+ const gitignore = path.join(dir, ".gitignore");
347
+ if (!existsSync(gitignore)) return;
348
+ const lines = readFileSync(gitignore, "utf8").split(/\r?\n/);
349
+ const present = lines.some((l) => {
350
+ const t = l.trim();
351
+ return t === CONFIG_DIR || t === `${CONFIG_DIR}/` || t === `/${CONFIG_DIR}` || t === `/${CONFIG_DIR}/`;
352
+ });
353
+ if (!present) {
354
+ appendFileSync(gitignore, `
355
+ # artifact.cafe publish token
356
+ ${CONFIG_DIR}/
357
+ `);
358
+ }
359
+ }
360
+
361
+ // src/comments.ts
362
+ var STATUSES = ["open", "resolved", "all"];
363
+ var VERSIONS = ["current", "all"];
364
+ async function comments(opts) {
365
+ if (!STATUSES.includes(opts.status)) {
366
+ throw new Error(`--status must be one of: ${STATUSES.join(", ")} (got "${opts.status}").`);
367
+ }
368
+ if (!VERSIONS.includes(opts.version)) {
369
+ throw new Error(`--version must be one of: ${VERSIONS.join(", ")} (got "${opts.version}").`);
370
+ }
371
+ const status = opts.status;
372
+ const version = opts.version;
373
+ const dir = path2.resolve(opts.dir);
374
+ const config = readConfig(dir);
375
+ if (!config) {
376
+ throw new Error(
377
+ `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
378
+ );
379
+ }
380
+ const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
381
+ const client = createApiClient(apiUrl);
382
+ const data = await client.exportComments(config.artifactId, { status, version });
383
+ if (opts.json) {
384
+ console.log(JSON.stringify(data, null, 2));
385
+ return;
386
+ }
387
+ printHuman(data, config.url, status, version);
388
+ }
389
+ function printHuman(data, url, status, version) {
390
+ const statusLabel = status === "all" ? "" : `${status} `;
391
+ const scope = version === "current" ? data.versionNumber !== null ? `on v${data.versionNumber}` : "on the current version" : "across all versions";
392
+ if (data.comments.length === 0) {
393
+ console.log(`No ${statusLabel}comments ${scope}.`);
394
+ return;
395
+ }
396
+ const plural = data.comments.length === 1 ? "" : "s";
397
+ console.log(`${data.comments.length} ${statusLabel}thread${plural} ${scope} \u2014 ${url}
398
+ `);
399
+ for (const thread of data.comments) {
400
+ printThread(thread);
401
+ }
402
+ }
403
+ function printThread(t) {
404
+ const marker = t.status === "resolved" ? "\u2713" : "\u25CF";
405
+ const date = t.createdAt.slice(0, 10);
406
+ console.log(`${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`);
407
+ if (t.anchor.type === "text") {
408
+ console.log(` \u21B3 "${t.anchor.quote}"`);
409
+ } else if (t.anchor.element) {
410
+ const el = t.anchor.element;
411
+ const text = el.text ? ` "${el.text}"` : "";
412
+ console.log(` \u21B3 at ${elementLabel(el)}${text} \u2014 ${t.anchor.elementPath}`);
413
+ } else {
414
+ console.log(` \u21B3 at ${t.anchor.elementPath}`);
415
+ }
416
+ console.log(indent(t.body, " "));
417
+ for (const reply of t.replies) {
418
+ console.log(indent(`${reply.author}: ${reply.body}`, " "));
419
+ }
420
+ console.log("");
421
+ }
422
+ function indent(text, pad) {
423
+ return text.split("\n").map((line) => pad + line).join("\n");
424
+ }
425
+
426
+ // src/errors.ts
427
+ function printCliError(err, json) {
428
+ if (err instanceof ApiRequestError) {
429
+ if (json) {
430
+ console.log(
431
+ JSON.stringify({
432
+ error: { code: err.code, message: err.message, retryAfter: err.retryAfter }
433
+ })
434
+ );
435
+ } else {
436
+ console.error(`\u2717 ${err.message} (${err.code})`);
437
+ if (err.retryAfter) console.error(` Retry in ${err.retryAfter}s.`);
438
+ }
439
+ return;
440
+ }
441
+ const message = err instanceof Error ? err.message : String(err);
442
+ if (json) console.log(JSON.stringify({ error: { code: "cli_error", message } }));
443
+ else console.error(`\u2717 ${message}`);
444
+ }
445
+
446
+ // src/open.ts
447
+ import path5 from "path";
448
+
449
+ // src/publish.ts
450
+ import { readFileSync as readFileSync3 } from "fs";
451
+ import path4 from "path";
452
+ import { exec } from "child_process";
453
+
454
+ // src/manifest.ts
455
+ import { createHash } from "crypto";
456
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
457
+ import path3 from "path";
458
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
459
+ var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
460
+ function isIgnoredFile(name) {
461
+ if (IGNORED_FILES.has(name)) return true;
462
+ if (name === ".env" || name.startsWith(".env.")) return true;
463
+ if (name.endsWith(".pem") || name.endsWith(".key")) return true;
464
+ if (name.startsWith(".git")) return true;
465
+ return false;
466
+ }
467
+ var MIME_TYPES = {
468
+ ".html": "text/html",
469
+ ".htm": "text/html",
470
+ ".css": "text/css",
471
+ ".js": "text/javascript",
472
+ ".mjs": "text/javascript",
473
+ ".json": "application/json",
474
+ ".map": "application/json",
475
+ ".txt": "text/plain",
476
+ ".md": "text/markdown",
477
+ ".xml": "application/xml",
478
+ ".svg": "image/svg+xml",
479
+ ".png": "image/png",
480
+ ".jpg": "image/jpeg",
481
+ ".jpeg": "image/jpeg",
482
+ ".gif": "image/gif",
483
+ ".webp": "image/webp",
484
+ ".avif": "image/avif",
485
+ ".ico": "image/x-icon",
486
+ ".woff": "font/woff",
487
+ ".woff2": "font/woff2",
488
+ ".ttf": "font/ttf",
489
+ ".otf": "font/otf",
490
+ ".pdf": "application/pdf",
491
+ ".mp4": "video/mp4",
492
+ ".webm": "video/webm",
493
+ ".mp3": "audio/mpeg",
494
+ ".wav": "audio/wav",
495
+ ".wasm": "application/wasm"
496
+ };
497
+ function mimeFor(filePath) {
498
+ return MIME_TYPES[path3.extname(filePath).toLowerCase()] ?? "application/octet-stream";
499
+ }
500
+ function scanDirectory(dir) {
501
+ const files = [];
502
+ const skipped = [];
503
+ function walk(current, relPrefix) {
504
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
505
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
506
+ const abs = path3.join(current, entry.name);
507
+ if (entry.isSymbolicLink()) {
508
+ skipped.push(rel);
509
+ continue;
510
+ }
511
+ if (entry.isDirectory()) {
512
+ if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".git")) {
513
+ skipped.push(`${rel}/`);
514
+ } else {
515
+ walk(abs, rel);
516
+ }
517
+ continue;
518
+ }
519
+ if (!entry.isFile()) continue;
520
+ if (isIgnoredFile(entry.name) || !isSafeArtifactPath(rel)) {
521
+ skipped.push(rel);
522
+ continue;
523
+ }
524
+ const size = statSync(abs).size;
525
+ if (size === 0) {
526
+ skipped.push(rel);
527
+ continue;
528
+ }
529
+ const content = readFileSync2(abs);
530
+ files.push({
531
+ path: rel,
532
+ hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
533
+ size,
534
+ mimeType: mimeFor(rel),
535
+ absPath: abs
536
+ });
537
+ }
538
+ }
539
+ walk(dir, "");
540
+ files.sort((a, b) => a.path.localeCompare(b.path));
541
+ return { files, skipped };
542
+ }
543
+ function resolveEntry(files, explicit) {
544
+ if (explicit) {
545
+ if (!files.some((f) => f.path === explicit)) {
546
+ throw new Error(`Entry file "${explicit}" not found in the publish folder.`);
547
+ }
548
+ return explicit;
549
+ }
550
+ if (files.some((f) => f.path === "index.html")) return "index.html";
551
+ const htmlFiles = files.filter((f) => f.path.endsWith(".html") && !f.path.includes("/"));
552
+ if (htmlFiles.length === 1) return htmlFiles[0].path;
553
+ throw new Error(
554
+ htmlFiles.length === 0 ? "No entry file found. Add an index.html or pass --entry <file>." : `Multiple HTML files found (${htmlFiles.map((f) => f.path).join(", ")}). Pass --entry <file>.`
555
+ );
556
+ }
557
+
558
+ // src/publish.ts
559
+ var DEFAULT_API_URL = "https://artifact.cafe";
560
+ var UPLOAD_CONCURRENCY = 6;
561
+ async function publish(opts) {
562
+ const dir = path4.resolve(opts.dir);
563
+ const config = readConfig(dir);
564
+ const artifactId = opts.artifact ?? config?.artifactId ?? null;
565
+ const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
566
+ const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
567
+ const { files, skipped } = scanDirectory(dir);
568
+ if (files.length === 0) {
569
+ throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
570
+ }
571
+ const entryPath = resolveEntry(files, opts.entry);
572
+ const log = (msg) => {
573
+ if (!opts.json) console.log(msg);
574
+ };
575
+ if (skipped.length) {
576
+ log(` ignoring ${skipped.length} item(s): ${preview(skipped)}`);
577
+ }
578
+ const client = createApiClient(apiUrl, token);
579
+ const session = await client.createSession({
580
+ artifactId,
581
+ title: opts.title,
582
+ entryPath,
583
+ files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
584
+ source: {
585
+ type: "cli",
586
+ agent: process.env.ARTIFACT_CAFE_AGENT,
587
+ runId: process.env.ARTIFACT_CAFE_RUN_ID
588
+ }
589
+ });
590
+ const byPath = new Map(files.map((f) => [f.path, f]));
591
+ log(
592
+ ` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
593
+ );
594
+ await inBatches(session.filesToUpload, UPLOAD_CONCURRENCY, async (upload) => {
595
+ const file = byPath.get(upload.path);
596
+ if (!file) throw new Error(`Server asked for unknown file ${upload.path}`);
597
+ await uploadFile(upload.uploadUrl, readFileSync3(file.absPath), file.mimeType);
598
+ });
599
+ const result = await client.finalize(session.publishSessionId);
600
+ if (result.publishToken) {
601
+ writeConfig(dir, {
602
+ artifactId: result.artifactId,
603
+ slug: slugFromUrl(result.url),
604
+ url: result.url,
605
+ apiUrl,
606
+ publishToken: result.publishToken
607
+ });
608
+ }
609
+ if (opts.json) {
610
+ console.log(
611
+ JSON.stringify(
612
+ {
613
+ artifactId: result.artifactId,
614
+ versionNumber: result.versionNumber,
615
+ url: result.url,
616
+ ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
617
+ filesUploaded: session.filesToUpload.map((f) => f.path),
618
+ skippedFiles: session.skippedFiles,
619
+ ignored: skipped
620
+ },
621
+ null,
622
+ 2
623
+ )
624
+ );
625
+ } else if (result.versionNumber === 1) {
626
+ console.log(`
627
+ \u2713 Published to artifact.cafe
628
+ `);
629
+ console.log(`Review URL:
630
+ ${result.url}
631
+ `);
632
+ if (result.claimUrl) {
633
+ console.log(`Claim URL (shown once \u2014 claim to keep this artifact):
634
+ ${result.claimUrl}
635
+ `);
636
+ }
637
+ } else {
638
+ console.log(`
639
+ \u2713 Published v${result.versionNumber}
640
+ `);
641
+ console.log(`${result.url}
642
+ `);
643
+ }
644
+ if (opts.open && !opts.json && process.stdout.isTTY) {
645
+ openInBrowser(result.url);
646
+ }
647
+ }
648
+ async function inBatches(items, concurrency, fn) {
649
+ let next = 0;
650
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
651
+ while (next < items.length) {
652
+ const item = items[next++];
653
+ await fn(item);
654
+ }
655
+ });
656
+ await Promise.all(workers);
657
+ }
658
+ function slugFromUrl(url) {
659
+ return url.split("/a/")[1] ?? "";
660
+ }
661
+ function preview(items) {
662
+ return items.slice(0, 5).join(", ") + (items.length > 5 ? ", \u2026" : "");
663
+ }
664
+ function openInBrowser(url) {
665
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
666
+ exec(`${cmd} ${JSON.stringify(url)}`, () => {
667
+ });
668
+ }
669
+
670
+ // src/open.ts
671
+ function openArtifact(opts) {
672
+ const dir = path5.resolve(opts.dir);
673
+ const config = readConfig(dir);
674
+ if (!config) {
675
+ throw new Error(
676
+ `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
677
+ );
678
+ }
679
+ console.log(config.url);
680
+ if (process.stdout.isTTY) openInBrowser(config.url);
681
+ }
682
+
683
+ // src/index.ts
684
+ var publish2 = defineCommand({
685
+ meta: { name: "publish", description: "Publish a folder as an artifact" },
686
+ args: {
687
+ dir: { type: "positional", description: "Directory to publish", default: "." },
688
+ title: { type: "string", description: "Artifact title" },
689
+ entry: { type: "string", description: "Entry file (default: auto-detect)" },
690
+ artifact: { type: "string", description: "Publish to an existing artifact ID" },
691
+ json: { type: "boolean", description: "Machine-readable output" },
692
+ "no-open": { type: "boolean", description: "Don't open the review URL" }
693
+ },
694
+ async run({ args }) {
695
+ try {
696
+ await publish({
697
+ dir: args.dir,
698
+ title: args.title,
699
+ entry: args.entry,
700
+ artifact: args.artifact,
701
+ json: Boolean(args.json),
702
+ open: !args["no-open"]
703
+ });
704
+ } catch (err) {
705
+ printCliError(err, Boolean(args.json));
706
+ process.exit(1);
707
+ }
708
+ }
709
+ });
710
+ var comments2 = defineCommand({
711
+ meta: { name: "comments", description: "Pull comments for the linked artifact" },
712
+ args: {
713
+ dir: { type: "positional", description: "Published directory", default: "." },
714
+ json: { type: "boolean", description: "Machine-readable output" },
715
+ status: { type: "string", description: "Filter: open | resolved | all", default: "open" },
716
+ version: { type: "string", description: "Filter: current | all", default: "current" }
717
+ },
718
+ async run({ args }) {
719
+ try {
720
+ await comments({
721
+ dir: args.dir,
722
+ json: Boolean(args.json),
723
+ status: args.status,
724
+ version: args.version
725
+ });
726
+ } catch (err) {
727
+ printCliError(err, Boolean(args.json));
728
+ process.exit(1);
729
+ }
730
+ }
731
+ });
732
+ var open = defineCommand({
733
+ meta: { name: "open", description: "Open the linked artifact's review URL" },
734
+ args: {
735
+ dir: { type: "positional", description: "Published directory", default: "." }
736
+ },
737
+ run({ args }) {
738
+ try {
739
+ openArtifact({ dir: args.dir });
740
+ } catch (err) {
741
+ printCliError(err, false);
742
+ process.exit(1);
743
+ }
744
+ }
745
+ });
746
+ var login = defineCommand({
747
+ meta: { name: "login", description: "Log in (optional; needed for owner actions)" },
748
+ async run() {
749
+ console.error("login not implemented yet");
750
+ process.exit(1);
751
+ }
752
+ });
753
+ var main = defineCommand({
754
+ meta: {
755
+ name: "artifact-cafe",
756
+ description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe"
757
+ },
758
+ subCommands: { publish: publish2, comments: comments2, open, login }
759
+ });
760
+ runMain(main);
package/package.json CHANGED
@@ -1,9 +1,27 @@
1
1
  {
2
2
  "name": "artifact-cafe",
3
- "version": "0.0.0",
4
- "description": "Publish AI-generated artifacts for review on artifact.cafe. Name reserved — the CLI is in active development.",
5
- "keywords": ["artifact", "artifact.cafe", "review", "cli"],
6
- "homepage": "https://artifact.cafe",
7
- "license": "MIT",
8
- "files": ["README.md"]
3
+ "version": "0.0.1",
4
+ "description": "Publish AI-generated artifacts for review on artifact.cafe",
5
+ "type": "module",
6
+ "bin": {
7
+ "artifact-cafe": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "typecheck": "tsc --noEmit",
15
+ "dev": "tsup --watch"
16
+ },
17
+ "dependencies": {
18
+ "citty": "^0.1.6",
19
+ "zod": "^4.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@artifact-cafe/shared": "workspace:*",
23
+ "@types/node": "^24.0.0",
24
+ "tsup": "^8.5.0",
25
+ "typescript": "^5.9.0"
26
+ }
9
27
  }
package/README.md DELETED
@@ -1,11 +0,0 @@
1
- # artifact-cafe
2
-
3
- The home for AI-generated work. Agents publish interactive static artifacts
4
- via CLI; humans review and comment through a share link.
5
-
6
- ```
7
- npx artifact-cafe publish .
8
- ```
9
-
10
- > **This package name is reserved.** `0.0.0` is a placeholder — the real CLI
11
- > is in active development and will ship as `0.0.1`+. See https://artifact.cafe