artifact-cafe 0.0.0 → 0.0.2

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 +1013 -0
  2. package/package.json +24 -6
  3. package/README.md +0 -11
package/dist/index.js ADDED
@@ -0,0 +1,1013 @@
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/comments.ts
10
+ import { z } from "zod";
11
+ var elementSnapshotSchema = z.object({
12
+ classes: z.string().max(256).optional(),
13
+ id: z.string().max(256).optional(),
14
+ tag: z.string().min(1).max(128),
15
+ text: z.string().max(200).optional()
16
+ });
17
+ var pointAnchorSchema = z.object({
18
+ element: elementSnapshotSchema.optional(),
19
+ // elementPath is the primary anchor; x/y are offsets within the
20
+ // element's box, normalized 0..1 (SPEC.md §8.1).
21
+ elementPath: z.string().max(2048),
22
+ type: z.literal("point"),
23
+ viewportHeight: z.number().int().positive().max(1e5),
24
+ viewportWidth: z.number().int().positive().max(1e5),
25
+ x: z.number().min(0).max(1),
26
+ y: z.number().min(0).max(1)
27
+ });
28
+ var textAnchorSchema = z.object({
29
+ elementPath: z.string().max(2048),
30
+ endOffset: z.number().int().nonnegative().max(1e8),
31
+ prefix: z.string().max(200).optional(),
32
+ quote: z.string().min(1).max(1e3),
33
+ startOffset: z.number().int().nonnegative().max(1e8),
34
+ suffix: z.string().max(200).optional(),
35
+ type: z.literal("text")
36
+ });
37
+ var anchorSchema = z.discriminatedUnion("type", [
38
+ pointAnchorSchema,
39
+ textAnchorSchema
40
+ ]);
41
+ var guestSchema = z.object({
42
+ displayName: z.string().min(1).max(64),
43
+ guestId: z.string().min(16)
44
+ });
45
+ var createCommentThreadSchema = z.object({
46
+ anchor: anchorSchema,
47
+ body: z.string().min(1).max(1e4),
48
+ guest: guestSchema.optional(),
49
+ versionId: z.string()
50
+ });
51
+ var createCommentMessageSchema = z.object({
52
+ body: z.string().min(1).max(1e4),
53
+ guest: guestSchema.optional()
54
+ });
55
+ var threadActionSchema = z.object({
56
+ guest: guestSchema.optional()
57
+ });
58
+ var commentAuthorSchema = z.object({
59
+ displayName: z.string(),
60
+ guestId: z.string().nullable()
61
+ });
62
+ var commentMessageSchema = z.object({
63
+ author: commentAuthorSchema,
64
+ body: z.string(),
65
+ createdAt: z.string(),
66
+ id: z.string()
67
+ });
68
+ var commentThreadSchema = z.object({
69
+ anchor: anchorSchema,
70
+ artifactId: z.string(),
71
+ createdAt: z.string(),
72
+ createdBy: commentAuthorSchema,
73
+ id: z.string(),
74
+ messages: z.array(commentMessageSchema),
75
+ status: z.enum(["open", "resolved"]),
76
+ versionId: z.string()
77
+ });
78
+ var commentThreadListSchema = z.object({
79
+ threads: z.array(commentThreadSchema)
80
+ });
81
+ var commentExportQuerySchema = z.object({
82
+ status: z.enum(["open", "resolved", "all"]).default("open"),
83
+ version: z.enum(["current", "all"]).default("current")
84
+ });
85
+ var commentExportReplySchema = z.object({
86
+ author: z.string(),
87
+ body: z.string(),
88
+ createdAt: z.string()
89
+ });
90
+ var commentExportThreadSchema = z.object({
91
+ anchor: anchorSchema,
92
+ author: z.string(),
93
+ body: z.string(),
94
+ createdAt: z.string(),
95
+ id: z.string(),
96
+ replies: z.array(commentExportReplySchema),
97
+ status: z.enum(["open", "resolved"]),
98
+ versionNumber: z.number().int().positive()
99
+ });
100
+ var commentExportSchema = z.object({
101
+ artifactId: z.string(),
102
+ comments: z.array(commentExportThreadSchema),
103
+ // The artifact's current version (null before the first finalize).
104
+ versionNumber: z.number().int().positive().nullable()
105
+ });
106
+ function elementLabel(el) {
107
+ if (el.id) {
108
+ return `<${el.tag}#${el.id}>`;
109
+ }
110
+ const classes = el.classes ? el.classes.split(" ").slice(0, 2) : [];
111
+ return classes.length ? `<${el.tag}.${classes.join(".")}>` : `<${el.tag}>`;
112
+ }
113
+
114
+ // ../shared/src/errors.ts
115
+ import { z as z2 } from "zod";
116
+ var apiErrorSchema = z2.object({
117
+ code: z2.string(),
118
+ docsUrl: z2.string().url(),
119
+ message: z2.string(),
120
+ retryAfter: z2.number().int().positive().optional()
121
+ });
122
+
123
+ // ../shared/src/owner.ts
124
+ import { z as z3 } from "zod";
125
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
126
+ var claimResponseSchema = z3.object({
127
+ artifactId: z3.string(),
128
+ slug: z3.string(),
129
+ title: z3.string(),
130
+ url: z3.string().url()
131
+ });
132
+ var updateArtifactSchema = z3.object({
133
+ slug: z3.string().min(3).max(63).regex(SLUG_PATTERN, {
134
+ message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
135
+ }).optional(),
136
+ title: z3.string().min(1).max(200).optional()
137
+ }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
138
+ message: "provide at least one of title, slug"
139
+ });
140
+ var deleteArtifactResponseSchema = z3.object({
141
+ deleted: z3.literal(true)
142
+ });
143
+
144
+ // ../shared/src/protection.ts
145
+ import { z as z4 } from "zod";
146
+ var PASSWORD_MIN = 6;
147
+ var PASSWORD_MAX = 128;
148
+ var gatePasswordSchema = z4.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
149
+ var setProtectionSchema = z4.object({
150
+ password: gatePasswordSchema.nullable()
151
+ });
152
+ var protectionResponseSchema = z4.object({
153
+ protected: z4.boolean()
154
+ });
155
+ var unlockSchema = z4.object({
156
+ password: z4.string().min(1).max(1024)
157
+ });
158
+ var unlockResponseSchema = z4.object({
159
+ unlocked: z4.literal(true)
160
+ });
161
+
162
+ // ../shared/src/publish.ts
163
+ import { z as z5 } from "zod";
164
+ function isSafeArtifactPath(p) {
165
+ if (p.length === 0 || p.length > 1024) {
166
+ return false;
167
+ }
168
+ if (p.includes("\\") || p.includes("\0")) {
169
+ return false;
170
+ }
171
+ if (p.startsWith("/") || p.startsWith("~")) {
172
+ return false;
173
+ }
174
+ return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
175
+ }
176
+ var manifestFileSchema = z5.object({
177
+ hash: z5.string().regex(/^sha256_[a-f0-9]{64}$/),
178
+ mimeType: z5.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
179
+ path: z5.string().min(1).max(1024).refine(isSafeArtifactPath, {
180
+ message: "path must be a safe relative path"
181
+ }),
182
+ size: z5.number().int().positive()
183
+ });
184
+ var publishSourceSchema = z5.object({
185
+ agent: z5.string().optional(),
186
+ runId: z5.string().optional(),
187
+ type: z5.literal("cli")
188
+ });
189
+ var createPublishSessionSchema = z5.object({
190
+ artifactId: z5.string().nullable(),
191
+ entryPath: z5.string().optional(),
192
+ files: z5.array(manifestFileSchema).min(1).max(1e3),
193
+ source: publishSourceSchema,
194
+ title: z5.string().max(200).optional()
195
+ });
196
+ var publishSessionResponseSchema = z5.object({
197
+ artifactId: z5.string(),
198
+ expiresAt: z5.string(),
199
+ filesToUpload: z5.array(
200
+ z5.object({
201
+ method: z5.literal("PUT"),
202
+ path: z5.string(),
203
+ uploadUrl: z5.string().url()
204
+ })
205
+ ),
206
+ publishSessionId: z5.string(),
207
+ skippedFiles: z5.array(z5.string()),
208
+ versionId: z5.string()
209
+ });
210
+ var finalizeResponseSchema = z5.object({
211
+ artifactId: z5.string(),
212
+ // Returned only on first publish of a new artifact (one-time).
213
+ claimUrl: z5.string().url().optional(),
214
+ publishToken: z5.string().optional(),
215
+ url: z5.string().url(),
216
+ versionId: z5.string(),
217
+ versionNumber: z5.number().int().positive()
218
+ });
219
+ var artifactResponseSchema = z5.object({
220
+ claimed: z5.boolean(),
221
+ currentVersion: z5.object({
222
+ entryUrl: z5.string().url(),
223
+ id: z5.string(),
224
+ versionNumber: z5.number().int().positive()
225
+ }).nullable(),
226
+ id: z5.string(),
227
+ slug: z5.string(),
228
+ title: z5.string()
229
+ });
230
+
231
+ // ../shared/src/report.ts
232
+ import { z as z6 } from "zod";
233
+ var reportRequestSchema = z6.object({
234
+ reason: z6.string().max(1e3).optional()
235
+ });
236
+ var reportResponseSchema = z6.object({
237
+ reported: z6.literal(true)
238
+ });
239
+
240
+ // ../shared/src/versions.ts
241
+ import { z as z7 } from "zod";
242
+ var versionSourceSchema = z7.object({
243
+ agentName: z7.string().nullable(),
244
+ type: z7.string()
245
+ });
246
+ var versionChangesSchema = z7.object({
247
+ added: z7.array(z7.string()),
248
+ changed: z7.array(z7.string()),
249
+ removed: z7.array(z7.string())
250
+ });
251
+ var versionSummarySchema = z7.object({
252
+ // Absent for the first version — there is no previous version to diff against.
253
+ changes: versionChangesSchema.nullable(),
254
+ createdAt: z7.string(),
255
+ entryUrl: z7.string().url(),
256
+ fileCount: z7.number().int().nonnegative(),
257
+ id: z7.string(),
258
+ isCurrent: z7.boolean(),
259
+ source: versionSourceSchema,
260
+ totalSizeBytes: z7.number().int().nonnegative(),
261
+ versionNumber: z7.number().int().positive()
262
+ });
263
+ var versionListResponseSchema = z7.object({
264
+ versions: z7.array(versionSummarySchema)
265
+ });
266
+
267
+ // src/api.ts
268
+ var ApiRequestError = class extends Error {
269
+ constructor(status, code, message, retryAfter) {
270
+ super(message);
271
+ this.status = status;
272
+ this.code = code;
273
+ this.retryAfter = retryAfter;
274
+ this.name = "ApiRequestError";
275
+ }
276
+ status;
277
+ code;
278
+ retryAfter;
279
+ };
280
+ async function request(url, init, parse) {
281
+ let res;
282
+ try {
283
+ res = await fetch(url, init);
284
+ } catch (err) {
285
+ throw new Error(
286
+ `Could not reach ${new URL(url).origin} \u2014 is the API up? (${err instanceof Error ? err.message : err})`,
287
+ { cause: err }
288
+ );
289
+ }
290
+ const body = await res.json().catch(() => null);
291
+ if (!res.ok) {
292
+ const parsed = apiErrorSchema.safeParse(body);
293
+ if (parsed.success) {
294
+ throw new ApiRequestError(
295
+ res.status,
296
+ parsed.data.code,
297
+ parsed.data.message,
298
+ parsed.data.retryAfter
299
+ );
300
+ }
301
+ throw new ApiRequestError(
302
+ res.status,
303
+ "unknown_error",
304
+ `API returned ${res.status}`
305
+ );
306
+ }
307
+ return parse(body);
308
+ }
309
+ function createApiClient(apiUrl, publishToken) {
310
+ const base = apiUrl.replace(/\/$/, "");
311
+ const headers = {
312
+ "content-type": "application/json"
313
+ };
314
+ if (publishToken) {
315
+ headers.authorization = `Bearer ${publishToken}`;
316
+ }
317
+ return {
318
+ async createSession(body) {
319
+ return request(
320
+ `${base}/api/v1/publish`,
321
+ { body: JSON.stringify(body), headers, method: "POST" },
322
+ (d) => publishSessionResponseSchema.parse(d)
323
+ );
324
+ },
325
+ async exportComments(artifactId, query) {
326
+ const params = new URLSearchParams({
327
+ status: query.status,
328
+ version: query.version
329
+ });
330
+ return request(
331
+ `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
332
+ { method: "GET" },
333
+ (d) => commentExportSchema.parse(d)
334
+ );
335
+ },
336
+ async finalize(publishSessionId) {
337
+ return request(
338
+ `${base}/api/v1/publish/${publishSessionId}/finalize`,
339
+ { headers, method: "POST" },
340
+ (d) => finalizeResponseSchema.parse(d)
341
+ );
342
+ },
343
+ async getArtifact(artifactId) {
344
+ return request(
345
+ `${base}/api/v1/artifacts/${artifactId}`,
346
+ { method: "GET" },
347
+ (d) => artifactResponseSchema.parse(d)
348
+ );
349
+ },
350
+ // SPEC §6.16 — set a password (string) or clear it (null). Authorized
351
+ // by the publish token this client was created with.
352
+ async setProtection(artifactId, password) {
353
+ return request(
354
+ `${base}/api/v1/artifacts/${artifactId}/protection`,
355
+ { body: JSON.stringify({ password }), headers, method: "PUT" },
356
+ (d) => protectionResponseSchema.parse(d)
357
+ );
358
+ }
359
+ };
360
+ }
361
+ async function uploadFile(uploadUrl, content, mimeType) {
362
+ const res = await fetch(uploadUrl, {
363
+ body: new Uint8Array(content),
364
+ headers: {
365
+ "content-length": String(content.byteLength),
366
+ "content-type": mimeType
367
+ },
368
+ method: "PUT"
369
+ });
370
+ if (!res.ok) {
371
+ throw new Error(
372
+ `Upload failed (${res.status}): ${(await res.text()).slice(0, 200)}`
373
+ );
374
+ }
375
+ }
376
+
377
+ // src/config.ts
378
+ import {
379
+ appendFileSync,
380
+ existsSync,
381
+ mkdirSync,
382
+ readFileSync,
383
+ writeFileSync
384
+ } from "fs";
385
+ import path from "path";
386
+ import { z as z8 } from "zod";
387
+ var configSchema = z8.object({
388
+ apiUrl: z8.string(),
389
+ artifactId: z8.string(),
390
+ publishToken: z8.string(),
391
+ slug: z8.string(),
392
+ url: z8.string()
393
+ });
394
+ var CONFIG_DIR = ".artifactcafe";
395
+ var CONFIG_FILE = "config.json";
396
+ function readConfig(dir) {
397
+ const file = path.join(dir, CONFIG_DIR, CONFIG_FILE);
398
+ if (!existsSync(file)) {
399
+ return null;
400
+ }
401
+ try {
402
+ return configSchema.parse(JSON.parse(readFileSync(file, "utf8")));
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+ function writeConfig(dir, config) {
408
+ mkdirSync(path.join(dir, CONFIG_DIR), { recursive: true });
409
+ writeFileSync(
410
+ path.join(dir, CONFIG_DIR, CONFIG_FILE),
411
+ `${JSON.stringify(config, null, 2)}
412
+ `,
413
+ { mode: 384 }
414
+ );
415
+ ensureGitignored(dir);
416
+ }
417
+ function ensureGitignored(dir) {
418
+ const gitignore = path.join(dir, ".gitignore");
419
+ if (!existsSync(gitignore)) {
420
+ return;
421
+ }
422
+ const lines = readFileSync(gitignore, "utf8").split(/\r?\n/);
423
+ const present = lines.some((l) => {
424
+ const t = l.trim();
425
+ return t === CONFIG_DIR || t === `${CONFIG_DIR}/` || t === `/${CONFIG_DIR}` || t === `/${CONFIG_DIR}/`;
426
+ });
427
+ if (!present) {
428
+ appendFileSync(
429
+ gitignore,
430
+ `
431
+ # artifact.cafe publish token
432
+ ${CONFIG_DIR}/
433
+ `
434
+ );
435
+ }
436
+ }
437
+
438
+ // src/comments.ts
439
+ var STATUSES = ["open", "resolved", "all"];
440
+ var VERSIONS = ["current", "all"];
441
+ async function comments(opts) {
442
+ if (!STATUSES.includes(opts.status)) {
443
+ throw new Error(
444
+ `--status must be one of: ${STATUSES.join(", ")} (got "${opts.status}").`
445
+ );
446
+ }
447
+ if (!VERSIONS.includes(opts.version)) {
448
+ throw new Error(
449
+ `--version must be one of: ${VERSIONS.join(", ")} (got "${opts.version}").`
450
+ );
451
+ }
452
+ const status = opts.status;
453
+ const version = opts.version;
454
+ const dir = path2.resolve(opts.dir);
455
+ const config = readConfig(dir);
456
+ if (!config) {
457
+ throw new Error(
458
+ `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
459
+ );
460
+ }
461
+ const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
462
+ const client = createApiClient(apiUrl);
463
+ const data = await client.exportComments(config.artifactId, {
464
+ status,
465
+ version
466
+ });
467
+ if (opts.json) {
468
+ console.log(JSON.stringify(data, null, 2));
469
+ return;
470
+ }
471
+ printHuman(data, config.url, status, version);
472
+ }
473
+ function printHuman(data, url, status, version) {
474
+ const statusLabel = status === "all" ? "" : `${status} `;
475
+ let scope = "across all versions";
476
+ if (version === "current") {
477
+ scope = data.versionNumber === null ? "on the current version" : `on v${data.versionNumber}`;
478
+ }
479
+ if (data.comments.length === 0) {
480
+ console.log(`No ${statusLabel}comments ${scope}.`);
481
+ return;
482
+ }
483
+ const plural = data.comments.length === 1 ? "" : "s";
484
+ console.log(
485
+ `${data.comments.length} ${statusLabel}thread${plural} ${scope} \u2014 ${url}
486
+ `
487
+ );
488
+ for (const thread of data.comments) {
489
+ printThread(thread);
490
+ }
491
+ }
492
+ function printThread(t) {
493
+ const marker = t.status === "resolved" ? "\u2713" : "\u25CF";
494
+ const date = t.createdAt.slice(0, 10);
495
+ console.log(
496
+ `${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`
497
+ );
498
+ if (t.anchor.type === "text") {
499
+ console.log(` \u21B3 "${t.anchor.quote}"`);
500
+ } else if (t.anchor.element) {
501
+ const el = t.anchor.element;
502
+ const text = el.text ? ` "${el.text}"` : "";
503
+ console.log(` \u21B3 at ${elementLabel(el)}${text} \u2014 ${t.anchor.elementPath}`);
504
+ } else {
505
+ console.log(` \u21B3 at ${t.anchor.elementPath}`);
506
+ }
507
+ console.log(indent(t.body, " "));
508
+ for (const reply of t.replies) {
509
+ console.log(indent(`${reply.author}: ${reply.body}`, " "));
510
+ }
511
+ console.log("");
512
+ }
513
+ function indent(text, pad) {
514
+ return text.split("\n").map((line) => pad + line).join("\n");
515
+ }
516
+
517
+ // src/errors.ts
518
+ function printCliError(err, json) {
519
+ if (err instanceof ApiRequestError) {
520
+ if (json) {
521
+ console.log(
522
+ JSON.stringify({
523
+ error: {
524
+ code: err.code,
525
+ message: err.message,
526
+ retryAfter: err.retryAfter
527
+ }
528
+ })
529
+ );
530
+ } else {
531
+ console.error(`\u2717 ${err.message} (${err.code})`);
532
+ if (err.retryAfter) {
533
+ console.error(` Retry in ${err.retryAfter}s.`);
534
+ }
535
+ }
536
+ return;
537
+ }
538
+ const message = err instanceof Error ? err.message : String(err);
539
+ if (json) {
540
+ console.log(JSON.stringify({ error: { code: "cli_error", message } }));
541
+ } else {
542
+ console.error(`\u2717 ${message}`);
543
+ }
544
+ }
545
+
546
+ // src/open.ts
547
+ import path5 from "path";
548
+
549
+ // src/publish.ts
550
+ import { exec } from "child_process";
551
+ import { readFileSync as readFileSync3 } from "fs";
552
+ import path4 from "path";
553
+
554
+ // src/manifest.ts
555
+ import { createHash } from "crypto";
556
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
557
+ import path3 from "path";
558
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
559
+ var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
560
+ function isIgnoredFile(name) {
561
+ if (IGNORED_FILES.has(name)) {
562
+ return true;
563
+ }
564
+ if (name === ".env" || name.startsWith(".env.")) {
565
+ return true;
566
+ }
567
+ if (name.endsWith(".pem") || name.endsWith(".key")) {
568
+ return true;
569
+ }
570
+ if (name.startsWith(".git")) {
571
+ return true;
572
+ }
573
+ return false;
574
+ }
575
+ var MIME_TYPES = {
576
+ ".avif": "image/avif",
577
+ ".css": "text/css",
578
+ ".gif": "image/gif",
579
+ ".htm": "text/html",
580
+ ".html": "text/html",
581
+ ".ico": "image/x-icon",
582
+ ".jpeg": "image/jpeg",
583
+ ".jpg": "image/jpeg",
584
+ ".js": "text/javascript",
585
+ ".json": "application/json",
586
+ ".map": "application/json",
587
+ ".md": "text/markdown",
588
+ ".mjs": "text/javascript",
589
+ ".mp3": "audio/mpeg",
590
+ ".mp4": "video/mp4",
591
+ ".otf": "font/otf",
592
+ ".pdf": "application/pdf",
593
+ ".png": "image/png",
594
+ ".svg": "image/svg+xml",
595
+ ".ttf": "font/ttf",
596
+ ".txt": "text/plain",
597
+ ".wasm": "application/wasm",
598
+ ".wav": "audio/wav",
599
+ ".webm": "video/webm",
600
+ ".webp": "image/webp",
601
+ ".woff": "font/woff",
602
+ ".woff2": "font/woff2",
603
+ ".xml": "application/xml"
604
+ };
605
+ function mimeFor(filePath) {
606
+ return MIME_TYPES[path3.extname(filePath).toLowerCase()] ?? "application/octet-stream";
607
+ }
608
+ function scanDirectory(dir) {
609
+ const files = [];
610
+ const skipped = [];
611
+ function walk(current, relPrefix) {
612
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
613
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
614
+ const abs = path3.join(current, entry.name);
615
+ if (entry.isSymbolicLink()) {
616
+ skipped.push(rel);
617
+ continue;
618
+ }
619
+ if (entry.isDirectory()) {
620
+ if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".git")) {
621
+ skipped.push(`${rel}/`);
622
+ } else {
623
+ walk(abs, rel);
624
+ }
625
+ continue;
626
+ }
627
+ if (!entry.isFile()) {
628
+ continue;
629
+ }
630
+ if (isIgnoredFile(entry.name) || !isSafeArtifactPath(rel)) {
631
+ skipped.push(rel);
632
+ continue;
633
+ }
634
+ const { size } = statSync(abs);
635
+ if (size === 0) {
636
+ skipped.push(rel);
637
+ continue;
638
+ }
639
+ const content = readFileSync2(abs);
640
+ files.push({
641
+ absPath: abs,
642
+ hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
643
+ mimeType: mimeFor(rel),
644
+ path: rel,
645
+ size
646
+ });
647
+ }
648
+ }
649
+ walk(dir, "");
650
+ files.sort((a, b) => a.path.localeCompare(b.path));
651
+ return { files, skipped };
652
+ }
653
+ function resolveEntry(files, explicit) {
654
+ if (explicit) {
655
+ if (!files.some((f) => f.path === explicit)) {
656
+ throw new Error(
657
+ `Entry file "${explicit}" not found in the publish folder.`
658
+ );
659
+ }
660
+ return explicit;
661
+ }
662
+ if (files.some((f) => f.path === "index.html")) {
663
+ return "index.html";
664
+ }
665
+ const htmlFiles = files.filter(
666
+ (f) => f.path.endsWith(".html") && !f.path.includes("/")
667
+ );
668
+ const [onlyHtmlFile] = htmlFiles;
669
+ if (onlyHtmlFile && htmlFiles.length === 1) {
670
+ return onlyHtmlFile.path;
671
+ }
672
+ throw new Error(
673
+ 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>.`
674
+ );
675
+ }
676
+
677
+ // src/publish.ts
678
+ var DEFAULT_API_URL = "https://artifact.cafe";
679
+ var UPLOAD_CONCURRENCY = 6;
680
+ async function publish(opts) {
681
+ const dir = path4.resolve(opts.dir);
682
+ const config = readConfig(dir);
683
+ if (opts.password !== void 0) {
684
+ const parsed = gatePasswordSchema.safeParse(opts.password);
685
+ if (!parsed.success) {
686
+ throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
687
+ }
688
+ }
689
+ const artifactId = opts.artifact ?? config?.artifactId ?? null;
690
+ const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
691
+ const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
692
+ const { files, skipped } = scanDirectory(dir);
693
+ if (files.length === 0) {
694
+ throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
695
+ }
696
+ const entryPath = resolveEntry(files, opts.entry);
697
+ const log = (msg) => {
698
+ if (!opts.json) {
699
+ console.log(msg);
700
+ }
701
+ };
702
+ if (skipped.length) {
703
+ log(` ignoring ${skipped.length} item(s): ${preview(skipped)}`);
704
+ }
705
+ const client = createApiClient(apiUrl, token);
706
+ const session = await client.createSession({
707
+ artifactId,
708
+ entryPath,
709
+ files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
710
+ source: {
711
+ agent: process.env.ARTIFACT_CAFE_AGENT,
712
+ runId: process.env.ARTIFACT_CAFE_RUN_ID,
713
+ type: "cli"
714
+ },
715
+ title: opts.title
716
+ });
717
+ const byPath = new Map(files.map((f) => [f.path, f]));
718
+ log(
719
+ ` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
720
+ );
721
+ await inBatches(session.filesToUpload, UPLOAD_CONCURRENCY, async (upload) => {
722
+ const file = byPath.get(upload.path);
723
+ if (!file) {
724
+ throw new Error(`Server asked for unknown file ${upload.path}`);
725
+ }
726
+ await uploadFile(
727
+ upload.uploadUrl,
728
+ readFileSync3(file.absPath),
729
+ file.mimeType
730
+ );
731
+ });
732
+ const result = await client.finalize(session.publishSessionId);
733
+ if (result.publishToken) {
734
+ writeConfig(dir, {
735
+ apiUrl,
736
+ artifactId: result.artifactId,
737
+ publishToken: result.publishToken,
738
+ slug: slugFromUrl(result.url),
739
+ url: result.url
740
+ });
741
+ }
742
+ if (opts.password !== void 0) {
743
+ const protectClient = createApiClient(apiUrl, token ?? result.publishToken);
744
+ await protectClient.setProtection(result.artifactId, opts.password);
745
+ }
746
+ if (opts.json) {
747
+ console.log(
748
+ JSON.stringify(
749
+ {
750
+ artifactId: result.artifactId,
751
+ url: result.url,
752
+ versionNumber: result.versionNumber,
753
+ ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
754
+ ...opts.password === void 0 ? {} : { protected: true },
755
+ filesUploaded: session.filesToUpload.map((f) => f.path),
756
+ ignored: skipped,
757
+ skippedFiles: session.skippedFiles
758
+ },
759
+ null,
760
+ 2
761
+ )
762
+ );
763
+ } else if (result.versionNumber === 1) {
764
+ console.log("\n\u2713 Published to artifact.cafe\n");
765
+ console.log(`Review URL:
766
+ ${result.url}
767
+ `);
768
+ if (result.claimUrl) {
769
+ console.log(
770
+ `Claim URL (shown once \u2014 claim to keep this artifact):
771
+ ${result.claimUrl}
772
+ `
773
+ );
774
+ }
775
+ if (opts.password !== void 0) {
776
+ console.log("Password protection: on\n");
777
+ }
778
+ } else {
779
+ console.log(`
780
+ \u2713 Published v${result.versionNumber}
781
+ `);
782
+ console.log(`${result.url}
783
+ `);
784
+ if (opts.password !== void 0) {
785
+ console.log("Password protection: on\n");
786
+ }
787
+ }
788
+ if (opts.open && !opts.json && process.stdout.isTTY) {
789
+ openInBrowser(result.url);
790
+ }
791
+ }
792
+ async function inBatches(items, concurrency, fn) {
793
+ let next = 0;
794
+ const workers = Array.from(
795
+ { length: Math.min(concurrency, items.length) },
796
+ async () => {
797
+ while (next < items.length) {
798
+ const item = items[next];
799
+ next += 1;
800
+ if (item === void 0) {
801
+ continue;
802
+ }
803
+ await fn(item);
804
+ }
805
+ }
806
+ );
807
+ await Promise.all(workers);
808
+ }
809
+ function slugFromUrl(url) {
810
+ return url.split("/a/")[1] ?? "";
811
+ }
812
+ function preview(items) {
813
+ return items.slice(0, 5).join(", ") + (items.length > 5 ? ", \u2026" : "");
814
+ }
815
+ function openInBrowser(url) {
816
+ let cmd = "xdg-open";
817
+ if (process.platform === "darwin") {
818
+ cmd = "open";
819
+ } else if (process.platform === "win32") {
820
+ cmd = "start";
821
+ }
822
+ exec(`${cmd} ${JSON.stringify(url)}`, () => {
823
+ });
824
+ }
825
+
826
+ // src/open.ts
827
+ function openArtifact(opts) {
828
+ const dir = path5.resolve(opts.dir);
829
+ const config = readConfig(dir);
830
+ if (!config) {
831
+ throw new Error(
832
+ `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
833
+ );
834
+ }
835
+ console.log(config.url);
836
+ if (process.stdout.isTTY) {
837
+ openInBrowser(config.url);
838
+ }
839
+ }
840
+
841
+ // src/protect.ts
842
+ import path6 from "path";
843
+ async function protect(opts) {
844
+ if (opts.off && opts.password !== void 0) {
845
+ throw new Error("Pass either --password or --off, not both.");
846
+ }
847
+ if (!opts.off && opts.password === void 0) {
848
+ throw new Error(
849
+ "Provide --password <password> to set a gate, or --off to remove it."
850
+ );
851
+ }
852
+ const password = opts.off ? null : opts.password;
853
+ if (password !== null) {
854
+ const parsed = gatePasswordSchema.safeParse(password);
855
+ if (!parsed.success) {
856
+ throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
857
+ }
858
+ }
859
+ const dir = path6.resolve(opts.dir);
860
+ const config = readConfig(dir);
861
+ if (!config) {
862
+ throw new Error(
863
+ `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
864
+ );
865
+ }
866
+ const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
867
+ const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
868
+ const client = createApiClient(apiUrl, token);
869
+ const res = await client.setProtection(config.artifactId, password);
870
+ if (opts.json) {
871
+ console.log(JSON.stringify({ protected: res.protected }, null, 2));
872
+ return;
873
+ }
874
+ console.log(
875
+ res.protected ? `
876
+ \u2713 Password protection on \u2014 ${config.url}
877
+ ` : `
878
+ \u2713 Password protection removed \u2014 ${config.url}
879
+ `
880
+ );
881
+ }
882
+
883
+ // src/index.ts
884
+ var publish2 = defineCommand({
885
+ args: {
886
+ artifact: {
887
+ description: "Publish to an existing artifact ID",
888
+ type: "string"
889
+ },
890
+ dir: {
891
+ default: ".",
892
+ description: "Directory to publish",
893
+ type: "positional"
894
+ },
895
+ entry: { description: "Entry file (default: auto-detect)", type: "string" },
896
+ json: { description: "Machine-readable output", type: "boolean" },
897
+ "no-open": { description: "Don't open the review URL", type: "boolean" },
898
+ password: {
899
+ description: "Password-protect the artifact (min 6 chars)",
900
+ type: "string"
901
+ },
902
+ title: { description: "Artifact title", type: "string" }
903
+ },
904
+ meta: { description: "Publish a folder as an artifact", name: "publish" },
905
+ async run({ args }) {
906
+ try {
907
+ await publish({
908
+ artifact: args.artifact,
909
+ dir: args.dir,
910
+ entry: args.entry,
911
+ json: Boolean(args.json),
912
+ open: !args["no-open"],
913
+ password: args.password,
914
+ title: args.title
915
+ });
916
+ } catch (err) {
917
+ printCliError(err, Boolean(args.json));
918
+ process.exit(1);
919
+ }
920
+ }
921
+ });
922
+ var comments2 = defineCommand({
923
+ args: {
924
+ dir: {
925
+ default: ".",
926
+ description: "Published directory",
927
+ type: "positional"
928
+ },
929
+ json: { description: "Machine-readable output", type: "boolean" },
930
+ status: {
931
+ default: "open",
932
+ description: "Filter: open | resolved | all",
933
+ type: "string"
934
+ },
935
+ version: {
936
+ default: "current",
937
+ description: "Filter: current | all",
938
+ type: "string"
939
+ }
940
+ },
941
+ meta: {
942
+ description: "Pull comments for the linked artifact",
943
+ name: "comments"
944
+ },
945
+ async run({ args }) {
946
+ try {
947
+ await comments({
948
+ dir: args.dir,
949
+ json: Boolean(args.json),
950
+ status: args.status,
951
+ version: args.version
952
+ });
953
+ } catch (err) {
954
+ printCliError(err, Boolean(args.json));
955
+ process.exit(1);
956
+ }
957
+ }
958
+ });
959
+ var protect2 = defineCommand({
960
+ args: {
961
+ dir: {
962
+ default: ".",
963
+ description: "Published directory",
964
+ type: "positional"
965
+ },
966
+ json: { description: "Machine-readable output", type: "boolean" },
967
+ off: { description: "Remove the password gate", type: "boolean" },
968
+ password: { description: "Gate password (min 6 chars)", type: "string" }
969
+ },
970
+ meta: {
971
+ description: "Password-protect the linked artifact (or --off to remove)",
972
+ name: "protect"
973
+ },
974
+ async run({ args }) {
975
+ try {
976
+ await protect({
977
+ dir: args.dir,
978
+ json: Boolean(args.json),
979
+ off: Boolean(args.off),
980
+ password: args.password
981
+ });
982
+ } catch (err) {
983
+ printCliError(err, Boolean(args.json));
984
+ process.exit(1);
985
+ }
986
+ }
987
+ });
988
+ var open = defineCommand({
989
+ args: {
990
+ dir: {
991
+ default: ".",
992
+ description: "Published directory",
993
+ type: "positional"
994
+ }
995
+ },
996
+ meta: { description: "Open the linked artifact's review URL", name: "open" },
997
+ run({ args }) {
998
+ try {
999
+ openArtifact({ dir: args.dir });
1000
+ } catch (err) {
1001
+ printCliError(err, false);
1002
+ process.exit(1);
1003
+ }
1004
+ }
1005
+ });
1006
+ var main = defineCommand({
1007
+ meta: {
1008
+ description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1009
+ name: "artifact-cafe"
1010
+ },
1011
+ subCommands: { comments: comments2, open, protect: protect2, publish: publish2 }
1012
+ });
1013
+ 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.2",
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