artifact-cafe 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +962 -275
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,135 +6,121 @@ import { defineCommand, runMain } from "citty";
6
6
  // src/comments.ts
7
7
  import path2 from "path";
8
8
 
9
- // ../shared/src/publish.ts
9
+ // ../shared/src/account.ts
10
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()
11
+ var createApiKeySchema = z.object({
12
+ name: z.string().trim().min(1).max(60).optional()
48
13
  });
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()
14
+ var createdApiKeySchema = z.object({
15
+ createdAt: z.string(),
16
+ id: z.string(),
17
+ key: z.string(),
18
+ keyPrefix: z.string(),
19
+ name: z.string()
20
+ });
21
+ var apiKeySummarySchema = z.object({
22
+ createdAt: z.string(),
23
+ id: z.string(),
24
+ keyPrefix: z.string(),
25
+ lastUsedAt: z.string().nullable(),
26
+ name: z.string()
27
+ });
28
+ var apiKeyListResponseSchema = z.object({
29
+ keys: z.array(apiKeySummarySchema)
30
+ });
31
+ var revokeApiKeyResponseSchema = z.object({
32
+ revoked: z.literal(true)
33
+ });
34
+ var meResponseSchema = z.object({
35
+ // Best-effort profile field from our own user table; null when unknown.
36
+ email: z.string().nullable(),
37
+ // Present when the request was authenticated with an API key.
38
+ keyName: z.string().optional(),
39
+ userId: z.string()
57
40
  });
58
- var artifactResponseSchema = z.object({
41
+ var artifactListItemSchema = z.object({
42
+ createdAt: z.string(),
43
+ currentVersionNumber: z.number().int().positive().nullable(),
59
44
  id: z.string(),
45
+ openCommentCount: z.number().int().nonnegative(),
60
46
  slug: z.string(),
61
47
  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()
48
+ updatedAt: z.string(),
49
+ url: z.string().url(),
50
+ versionCount: z.number().int().nonnegative()
51
+ });
52
+ var artifactListResponseSchema = z.object({
53
+ artifacts: z.array(artifactListItemSchema)
68
54
  });
69
55
 
70
56
  // ../shared/src/comments.ts
71
57
  import { z as z2 } from "zod";
72
58
  var elementSnapshotSchema = z2.object({
73
- tag: z2.string().min(1).max(128),
74
- id: z2.string().max(256).optional(),
75
59
  classes: z2.string().max(256).optional(),
60
+ id: z2.string().max(256).optional(),
61
+ tag: z2.string().min(1).max(128),
76
62
  text: z2.string().max(200).optional()
77
63
  });
78
64
  var pointAnchorSchema = z2.object({
79
- type: z2.literal("point"),
65
+ element: elementSnapshotSchema.optional(),
80
66
  // elementPath is the primary anchor; x/y are offsets within the
81
67
  // element's box, normalized 0..1 (SPEC.md §8.1).
82
68
  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),
69
+ type: z2.literal("point"),
86
70
  viewportHeight: z2.number().int().positive().max(1e5),
87
- element: elementSnapshotSchema.optional()
71
+ viewportWidth: z2.number().int().positive().max(1e5),
72
+ x: z2.number().min(0).max(1),
73
+ y: z2.number().min(0).max(1)
88
74
  });
89
75
  var textAnchorSchema = z2.object({
90
- type: z2.literal("text"),
91
- quote: z2.string().min(1).max(1e3),
76
+ elementPath: z2.string().max(2048),
77
+ endOffset: z2.number().int().nonnegative().max(1e8),
92
78
  prefix: z2.string().max(200).optional(),
93
- suffix: z2.string().max(200).optional(),
79
+ quote: z2.string().min(1).max(1e3),
94
80
  startOffset: z2.number().int().nonnegative().max(1e8),
95
- endOffset: z2.number().int().nonnegative().max(1e8),
96
- elementPath: z2.string().max(2048)
81
+ suffix: z2.string().max(200).optional(),
82
+ type: z2.literal("text")
97
83
  });
98
84
  var anchorSchema = z2.discriminatedUnion("type", [
99
85
  pointAnchorSchema,
100
86
  textAnchorSchema
101
87
  ]);
102
88
  var guestSchema = z2.object({
103
- guestId: z2.string().min(16),
104
- displayName: z2.string().min(1).max(64)
89
+ displayName: z2.string().min(1).max(64),
90
+ guestId: z2.string().min(16)
105
91
  });
106
92
  var createCommentThreadSchema = z2.object({
107
- versionId: z2.string(),
108
- guest: guestSchema.optional(),
109
93
  anchor: anchorSchema,
110
- body: z2.string().min(1).max(1e4)
94
+ body: z2.string().min(1).max(1e4),
95
+ guest: guestSchema.optional(),
96
+ versionId: z2.string()
111
97
  });
112
98
  var createCommentMessageSchema = z2.object({
113
- guest: guestSchema.optional(),
114
- body: z2.string().min(1).max(1e4)
99
+ body: z2.string().min(1).max(1e4),
100
+ guest: guestSchema.optional()
115
101
  });
116
102
  var threadActionSchema = z2.object({
117
103
  guest: guestSchema.optional()
118
104
  });
119
105
  var commentAuthorSchema = z2.object({
120
- guestId: z2.string().nullable(),
121
- displayName: z2.string()
106
+ displayName: z2.string(),
107
+ guestId: z2.string().nullable()
122
108
  });
123
109
  var commentMessageSchema = z2.object({
124
- id: z2.string(),
125
- body: z2.string(),
126
110
  author: commentAuthorSchema,
127
- createdAt: z2.string()
111
+ body: z2.string(),
112
+ createdAt: z2.string(),
113
+ id: z2.string()
128
114
  });
129
115
  var commentThreadSchema = z2.object({
130
- id: z2.string(),
131
- artifactId: z2.string(),
132
- versionId: z2.string(),
133
116
  anchor: anchorSchema,
134
- status: z2.enum(["open", "resolved"]),
135
- createdBy: commentAuthorSchema,
117
+ artifactId: z2.string(),
136
118
  createdAt: z2.string(),
137
- messages: z2.array(commentMessageSchema)
119
+ createdBy: commentAuthorSchema,
120
+ id: z2.string(),
121
+ messages: z2.array(commentMessageSchema),
122
+ status: z2.enum(["open", "resolved"]),
123
+ versionId: z2.string()
138
124
  });
139
125
  var commentThreadListSchema = z2.object({
140
126
  threads: z2.array(commentThreadSchema)
@@ -149,23 +135,25 @@ var commentExportReplySchema = z2.object({
149
135
  createdAt: z2.string()
150
136
  });
151
137
  var commentExportThreadSchema = z2.object({
152
- id: z2.string(),
153
- status: z2.enum(["open", "resolved"]),
154
- versionNumber: z2.number().int().positive(),
138
+ anchor: anchorSchema,
155
139
  author: z2.string(),
156
140
  body: z2.string(),
157
- anchor: anchorSchema,
158
141
  createdAt: z2.string(),
159
- replies: z2.array(commentExportReplySchema)
142
+ id: z2.string(),
143
+ replies: z2.array(commentExportReplySchema),
144
+ status: z2.enum(["open", "resolved"]),
145
+ versionNumber: z2.number().int().positive()
160
146
  });
161
147
  var commentExportSchema = z2.object({
162
148
  artifactId: z2.string(),
149
+ comments: z2.array(commentExportThreadSchema),
163
150
  // The artifact's current version (null before the first finalize).
164
- versionNumber: z2.number().int().positive().nullable(),
165
- comments: z2.array(commentExportThreadSchema)
151
+ versionNumber: z2.number().int().positive().nullable()
166
152
  });
167
153
  function elementLabel(el) {
168
- if (el.id) return `<${el.tag}#${el.id}>`;
154
+ if (el.id) {
155
+ return `<${el.tag}#${el.id}>`;
156
+ }
169
157
  const classes = el.classes ? el.classes.split(" ").slice(0, 2) : [];
170
158
  return classes.length ? `<${el.tag}.${classes.join(".")}>` : `<${el.tag}>`;
171
159
  }
@@ -174,57 +162,186 @@ function elementLabel(el) {
174
162
  import { z as z3 } from "zod";
175
163
  var apiErrorSchema = z3.object({
176
164
  code: z3.string(),
165
+ docsUrl: z3.string().url(),
177
166
  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)
167
+ retryAfter: z3.number().int().positive().optional()
207
168
  });
208
169
 
209
170
  // ../shared/src/owner.ts
210
- import { z as z5 } from "zod";
171
+ import { z as z4 } from "zod";
211
172
  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, {
173
+ var claimResponseSchema = z4.object({
174
+ artifactId: z4.string(),
175
+ slug: z4.string(),
176
+ title: z4.string(),
177
+ url: z4.string().url()
178
+ });
179
+ var updateArtifactSchema = z4.object({
180
+ slug: z4.string().min(3).max(63).regex(SLUG_PATTERN, {
221
181
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
222
- }).optional()
182
+ }).optional(),
183
+ title: z4.string().min(1).max(200).optional()
223
184
  }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
224
185
  message: "provide at least one of title, slug"
225
186
  });
226
- var deleteArtifactResponseSchema = z5.object({
227
- deleted: z5.literal(true)
187
+ var deleteArtifactResponseSchema = z4.object({
188
+ deleted: z4.literal(true)
189
+ });
190
+
191
+ // ../shared/src/protection.ts
192
+ import { z as z5 } from "zod";
193
+ var PASSWORD_MIN = 6;
194
+ var PASSWORD_MAX = 128;
195
+ var gatePasswordSchema = z5.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
196
+ var setProtectionSchema = z5.object({
197
+ password: gatePasswordSchema.nullable()
198
+ });
199
+ var protectionResponseSchema = z5.object({
200
+ protected: z5.boolean()
201
+ });
202
+ var unlockSchema = z5.object({
203
+ password: z5.string().min(1).max(1024)
204
+ });
205
+ var unlockResponseSchema = z5.object({
206
+ unlocked: z5.literal(true)
207
+ });
208
+
209
+ // ../shared/src/publish.ts
210
+ import { z as z6 } from "zod";
211
+ function isSafeArtifactPath(p) {
212
+ if (p.length === 0 || p.length > 1024) {
213
+ return false;
214
+ }
215
+ if (p.includes("\\") || p.includes("\0")) {
216
+ return false;
217
+ }
218
+ if (p.startsWith("/") || p.startsWith("~")) {
219
+ return false;
220
+ }
221
+ return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
222
+ }
223
+ var manifestFileSchema = z6.object({
224
+ hash: z6.string().regex(/^sha256_[a-f0-9]{64}$/),
225
+ mimeType: z6.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
226
+ path: z6.string().min(1).max(1024).refine(isSafeArtifactPath, {
227
+ message: "path must be a safe relative path"
228
+ }),
229
+ size: z6.number().int().positive()
230
+ });
231
+ var publishSourceSchema = z6.object({
232
+ agent: z6.string().optional(),
233
+ runId: z6.string().optional(),
234
+ type: z6.literal("cli")
235
+ });
236
+ var createPublishSessionSchema = z6.object({
237
+ artifactId: z6.string().nullable(),
238
+ entryPath: z6.string().optional(),
239
+ files: z6.array(manifestFileSchema).min(1).max(1e3),
240
+ source: publishSourceSchema,
241
+ title: z6.string().max(200).optional()
242
+ });
243
+ var publishSessionResponseSchema = z6.object({
244
+ artifactId: z6.string(),
245
+ expiresAt: z6.string(),
246
+ filesToUpload: z6.array(
247
+ z6.object({
248
+ method: z6.literal("PUT"),
249
+ path: z6.string(),
250
+ uploadUrl: z6.string().url()
251
+ })
252
+ ),
253
+ publishSessionId: z6.string(),
254
+ skippedFiles: z6.array(z6.string()),
255
+ versionId: z6.string()
256
+ });
257
+ var finalizeResponseSchema = z6.object({
258
+ artifactId: z6.string(),
259
+ // API-key publishes create the artifact already claimed (SPEC §5.4) —
260
+ // the CLI uses this to tell "claim URL pending" from "already yours".
261
+ claimed: z6.boolean(),
262
+ // Returned only on first publish of a new artifact (one-time).
263
+ claimUrl: z6.string().url().optional(),
264
+ publishToken: z6.string().optional(),
265
+ url: z6.string().url(),
266
+ versionId: z6.string(),
267
+ versionNumber: z6.number().int().positive()
268
+ });
269
+ var artifactResponseSchema = z6.object({
270
+ claimed: z6.boolean(),
271
+ currentVersion: z6.object({
272
+ entryUrl: z6.string().url(),
273
+ id: z6.string(),
274
+ versionNumber: z6.number().int().positive()
275
+ }).nullable(),
276
+ id: z6.string(),
277
+ slug: z6.string(),
278
+ title: z6.string()
279
+ });
280
+
281
+ // ../shared/src/report.ts
282
+ import { z as z7 } from "zod";
283
+ var reportRequestSchema = z7.object({
284
+ reason: z7.string().max(1e3).optional()
285
+ });
286
+ var reportResponseSchema = z7.object({
287
+ reported: z7.literal(true)
288
+ });
289
+
290
+ // ../shared/src/stats.ts
291
+ import { z as z8 } from "zod";
292
+ var avgViewSecondsSchema = z8.number().int().nonnegative().nullable();
293
+ var versionViewStatsSchema = z8.object({
294
+ avgViewSeconds: avgViewSecondsSchema,
295
+ // Views from inside the review shell's iframe vs. direct visits to
296
+ // the render URL. views = embeddedViews + directViews.
297
+ directViews: z8.number().int().nonnegative(),
298
+ embeddedViews: z8.number().int().nonnegative(),
299
+ // Distinct hashed viewer IPs across the version's lifetime.
300
+ uniqueViewers: z8.number().int().nonnegative(),
301
+ versionNumber: z8.number().int().positive(),
302
+ views: z8.number().int().nonnegative()
303
+ });
304
+ var dailyViewsSchema = z8.object({
305
+ avgViewSeconds: avgViewSecondsSchema,
306
+ // UTC day, YYYY-MM-DD.
307
+ day: z8.string(),
308
+ uniqueViewers: z8.number().int().nonnegative(),
309
+ views: z8.number().int().nonnegative()
310
+ });
311
+ var artifactStatsResponseSchema = z8.object({
312
+ avgViewSeconds: avgViewSecondsSchema,
313
+ daily: z8.array(dailyViewsSchema),
314
+ // Distinct hashed viewer IPs across all versions and days.
315
+ totalUniqueViewers: z8.number().int().nonnegative(),
316
+ totalViews: z8.number().int().nonnegative(),
317
+ versions: z8.array(versionViewStatsSchema)
318
+ });
319
+
320
+ // ../shared/src/versions.ts
321
+ import { z as z9 } from "zod";
322
+ var versionSourceSchema = z9.object({
323
+ agentName: z9.string().nullable(),
324
+ type: z9.string()
325
+ });
326
+ var versionChangesSchema = z9.object({
327
+ added: z9.array(z9.string()),
328
+ changed: z9.array(z9.string()),
329
+ removed: z9.array(z9.string())
330
+ });
331
+ var versionSummarySchema = z9.object({
332
+ // Absent for the first version — there is no previous version to diff against.
333
+ changes: versionChangesSchema.nullable(),
334
+ createdAt: z9.string(),
335
+ entryUrl: z9.string().url(),
336
+ fileCount: z9.number().int().nonnegative(),
337
+ id: z9.string(),
338
+ isCurrent: z9.boolean(),
339
+ source: versionSourceSchema,
340
+ totalSizeBytes: z9.number().int().nonnegative(),
341
+ versionNumber: z9.number().int().positive()
342
+ });
343
+ var versionListResponseSchema = z9.object({
344
+ versions: z9.array(versionSummarySchema)
228
345
  });
229
346
 
230
347
  // src/api.ts
@@ -246,7 +363,8 @@ async function request(url, init, parse) {
246
363
  res = await fetch(url, init);
247
364
  } catch (err) {
248
365
  throw new Error(
249
- `Could not reach ${new URL(url).origin} \u2014 is the API up? (${err instanceof Error ? err.message : err})`
366
+ `Could not reach ${new URL(url).origin} \u2014 is the API up? (${err instanceof Error ? err.message : err})`,
367
+ { cause: err }
250
368
  );
251
369
  }
252
370
  const body = await res.json().catch(() => null);
@@ -260,26 +378,45 @@ async function request(url, init, parse) {
260
378
  parsed.data.retryAfter
261
379
  );
262
380
  }
263
- throw new ApiRequestError(res.status, "unknown_error", `API returned ${res.status}`);
381
+ throw new ApiRequestError(
382
+ res.status,
383
+ "unknown_error",
384
+ `API returned ${res.status}`
385
+ );
264
386
  }
265
387
  return parse(body);
266
388
  }
267
- function createApiClient(apiUrl, publishToken) {
268
- const base = apiUrl.replace(/\/$/, "");
269
- const headers = { "content-type": "application/json" };
270
- if (publishToken) headers.authorization = `Bearer ${publishToken}`;
389
+ function createApiClient(apiUrl2, publishToken) {
390
+ const base = apiUrl2.replace(/\/$/, "");
391
+ const headers = {
392
+ "content-type": "application/json"
393
+ };
394
+ if (publishToken) {
395
+ headers.authorization = `Bearer ${publishToken}`;
396
+ }
271
397
  return {
272
398
  async createSession(body) {
273
399
  return request(
274
400
  `${base}/api/v1/publish`,
275
- { method: "POST", headers, body: JSON.stringify(body) },
401
+ { body: JSON.stringify(body), headers, method: "POST" },
276
402
  (d) => publishSessionResponseSchema.parse(d)
277
403
  );
278
404
  },
405
+ async exportComments(artifactId, query) {
406
+ const params = new URLSearchParams({
407
+ status: query.status,
408
+ version: query.version
409
+ });
410
+ return request(
411
+ `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
412
+ { method: "GET" },
413
+ (d) => commentExportSchema.parse(d)
414
+ );
415
+ },
279
416
  async finalize(publishSessionId) {
280
417
  return request(
281
418
  `${base}/api/v1/publish/${publishSessionId}/finalize`,
282
- { method: "POST", headers },
419
+ { headers, method: "POST" },
283
420
  (d) => finalizeResponseSchema.parse(d)
284
421
  );
285
422
  },
@@ -290,43 +427,75 @@ function createApiClient(apiUrl, publishToken) {
290
427
  (d) => artifactResponseSchema.parse(d)
291
428
  );
292
429
  },
293
- async exportComments(artifactId, query) {
294
- const params = new URLSearchParams({ status: query.status, version: query.version });
430
+ // SPEC §6.18 — everything owned by the account this client's API
431
+ // key (or session) belongs to.
432
+ async listArtifacts() {
295
433
  return request(
296
- `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
297
- { method: "GET" },
298
- (d) => commentExportSchema.parse(d)
434
+ `${base}/api/v1/artifacts`,
435
+ { headers, method: "GET" },
436
+ (d) => artifactListResponseSchema.parse(d)
437
+ );
438
+ },
439
+ // SPEC §6.14 — who the presented credential belongs to. Used by
440
+ // `login` to verify a pasted key and by `whoami`.
441
+ async me() {
442
+ return request(
443
+ `${base}/api/v1/me`,
444
+ { headers, method: "GET" },
445
+ (d) => meResponseSchema.parse(d)
446
+ );
447
+ },
448
+ // SPEC §6.16 — set a password (string) or clear it (null). Authorized
449
+ // by the publish token this client was created with.
450
+ async setProtection(artifactId, password) {
451
+ return request(
452
+ `${base}/api/v1/artifacts/${artifactId}/protection`,
453
+ { body: JSON.stringify({ password }), headers, method: "PUT" },
454
+ (d) => protectionResponseSchema.parse(d)
299
455
  );
300
456
  }
301
457
  };
302
458
  }
303
459
  async function uploadFile(uploadUrl, content, mimeType) {
304
460
  const res = await fetch(uploadUrl, {
305
- method: "PUT",
306
- headers: { "content-type": mimeType, "content-length": String(content.byteLength) },
307
- body: new Uint8Array(content)
461
+ body: new Uint8Array(content),
462
+ headers: {
463
+ "content-length": String(content.byteLength),
464
+ "content-type": mimeType
465
+ },
466
+ method: "PUT"
308
467
  });
309
468
  if (!res.ok) {
310
- throw new Error(`Upload failed (${res.status}): ${(await res.text()).slice(0, 200)}`);
469
+ throw new Error(
470
+ `Upload failed (${res.status}): ${(await res.text()).slice(0, 200)}`
471
+ );
311
472
  }
312
473
  }
313
474
 
314
475
  // src/config.ts
315
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "fs";
476
+ import {
477
+ appendFileSync,
478
+ existsSync,
479
+ mkdirSync,
480
+ readFileSync,
481
+ writeFileSync
482
+ } from "fs";
316
483
  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()
484
+ import { z as z10 } from "zod";
485
+ var configSchema = z10.object({
486
+ apiUrl: z10.string(),
487
+ artifactId: z10.string(),
488
+ publishToken: z10.string(),
489
+ slug: z10.string(),
490
+ url: z10.string()
324
491
  });
325
492
  var CONFIG_DIR = ".artifactcafe";
326
493
  var CONFIG_FILE = "config.json";
327
494
  function readConfig(dir) {
328
495
  const file = path.join(dir, CONFIG_DIR, CONFIG_FILE);
329
- if (!existsSync(file)) return null;
496
+ if (!existsSync(file)) {
497
+ return null;
498
+ }
330
499
  try {
331
500
  return configSchema.parse(JSON.parse(readFileSync(file, "utf8")));
332
501
  } catch {
@@ -337,24 +506,30 @@ function writeConfig(dir, config) {
337
506
  mkdirSync(path.join(dir, CONFIG_DIR), { recursive: true });
338
507
  writeFileSync(
339
508
  path.join(dir, CONFIG_DIR, CONFIG_FILE),
340
- JSON.stringify(config, null, 2) + "\n",
509
+ `${JSON.stringify(config, null, 2)}
510
+ `,
341
511
  { mode: 384 }
342
512
  );
343
513
  ensureGitignored(dir);
344
514
  }
345
515
  function ensureGitignored(dir) {
346
516
  const gitignore = path.join(dir, ".gitignore");
347
- if (!existsSync(gitignore)) return;
517
+ if (!existsSync(gitignore)) {
518
+ return;
519
+ }
348
520
  const lines = readFileSync(gitignore, "utf8").split(/\r?\n/);
349
521
  const present = lines.some((l) => {
350
522
  const t = l.trim();
351
523
  return t === CONFIG_DIR || t === `${CONFIG_DIR}/` || t === `/${CONFIG_DIR}` || t === `/${CONFIG_DIR}/`;
352
524
  });
353
525
  if (!present) {
354
- appendFileSync(gitignore, `
526
+ appendFileSync(
527
+ gitignore,
528
+ `
355
529
  # artifact.cafe publish token
356
530
  ${CONFIG_DIR}/
357
- `);
531
+ `
532
+ );
358
533
  }
359
534
  }
360
535
 
@@ -363,10 +538,14 @@ var STATUSES = ["open", "resolved", "all"];
363
538
  var VERSIONS = ["current", "all"];
364
539
  async function comments(opts) {
365
540
  if (!STATUSES.includes(opts.status)) {
366
- throw new Error(`--status must be one of: ${STATUSES.join(", ")} (got "${opts.status}").`);
541
+ throw new Error(
542
+ `--status must be one of: ${STATUSES.join(", ")} (got "${opts.status}").`
543
+ );
367
544
  }
368
545
  if (!VERSIONS.includes(opts.version)) {
369
- throw new Error(`--version must be one of: ${VERSIONS.join(", ")} (got "${opts.version}").`);
546
+ throw new Error(
547
+ `--version must be one of: ${VERSIONS.join(", ")} (got "${opts.version}").`
548
+ );
370
549
  }
371
550
  const status = opts.status;
372
551
  const version = opts.version;
@@ -377,9 +556,12 @@ async function comments(opts) {
377
556
  `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
378
557
  );
379
558
  }
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 });
559
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
560
+ const client = createApiClient(apiUrl2);
561
+ const data = await client.exportComments(config.artifactId, {
562
+ status,
563
+ version
564
+ });
383
565
  if (opts.json) {
384
566
  console.log(JSON.stringify(data, null, 2));
385
567
  return;
@@ -388,14 +570,19 @@ async function comments(opts) {
388
570
  }
389
571
  function printHuman(data, url, status, version) {
390
572
  const statusLabel = status === "all" ? "" : `${status} `;
391
- const scope = version === "current" ? data.versionNumber !== null ? `on v${data.versionNumber}` : "on the current version" : "across all versions";
573
+ let scope = "across all versions";
574
+ if (version === "current") {
575
+ scope = data.versionNumber === null ? "on the current version" : `on v${data.versionNumber}`;
576
+ }
392
577
  if (data.comments.length === 0) {
393
578
  console.log(`No ${statusLabel}comments ${scope}.`);
394
579
  return;
395
580
  }
396
581
  const plural = data.comments.length === 1 ? "" : "s";
397
- console.log(`${data.comments.length} ${statusLabel}thread${plural} ${scope} \u2014 ${url}
398
- `);
582
+ console.log(
583
+ `${data.comments.length} ${statusLabel}thread${plural} ${scope} \u2014 ${url}
584
+ `
585
+ );
399
586
  for (const thread of data.comments) {
400
587
  printThread(thread);
401
588
  }
@@ -403,7 +590,9 @@ function printHuman(data, url, status, version) {
403
590
  function printThread(t) {
404
591
  const marker = t.status === "resolved" ? "\u2713" : "\u25CF";
405
592
  const date = t.createdAt.slice(0, 10);
406
- console.log(`${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`);
593
+ console.log(
594
+ `${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`
595
+ );
407
596
  if (t.anchor.type === "text") {
408
597
  console.log(` \u21B3 "${t.anchor.quote}"`);
409
598
  } else if (t.anchor.element) {
@@ -429,73 +618,203 @@ function printCliError(err, json) {
429
618
  if (json) {
430
619
  console.log(
431
620
  JSON.stringify({
432
- error: { code: err.code, message: err.message, retryAfter: err.retryAfter }
621
+ error: {
622
+ code: err.code,
623
+ message: err.message,
624
+ retryAfter: err.retryAfter
625
+ }
433
626
  })
434
627
  );
435
628
  } else {
436
629
  console.error(`\u2717 ${err.message} (${err.code})`);
437
- if (err.retryAfter) console.error(` Retry in ${err.retryAfter}s.`);
630
+ if (err.retryAfter) {
631
+ console.error(` Retry in ${err.retryAfter}s.`);
632
+ }
438
633
  }
439
634
  return;
440
635
  }
441
636
  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}`);
637
+ if (json) {
638
+ console.log(JSON.stringify({ error: { code: "cli_error", message } }));
639
+ } else {
640
+ console.error(`\u2717 ${message}`);
641
+ }
444
642
  }
445
643
 
446
- // src/open.ts
447
- import path5 from "path";
644
+ // src/credentials.ts
645
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
646
+ import { homedir } from "os";
647
+ import path3 from "path";
648
+ import { z as z11 } from "zod";
649
+ var DEFAULT_API_URL = "https://artifact.cafe";
650
+ var credentialsSchema = z11.object({
651
+ keys: z11.record(z11.string(), z11.string()),
652
+ version: z11.literal(1)
653
+ });
654
+ function credentialsPath() {
655
+ return path3.join(homedir(), ".artifactcafe", "credentials");
656
+ }
657
+ function apiOrigin(apiUrl2) {
658
+ return new URL(apiUrl2).origin;
659
+ }
660
+ function readCredentials() {
661
+ const file = credentialsPath();
662
+ if (!existsSync2(file)) {
663
+ return { keys: {}, version: 1 };
664
+ }
665
+ try {
666
+ return credentialsSchema.parse(JSON.parse(readFileSync2(file, "utf8")));
667
+ } catch {
668
+ return { keys: {}, version: 1 };
669
+ }
670
+ }
671
+ function writeCredentials(credentials) {
672
+ const file = credentialsPath();
673
+ mkdirSync2(path3.dirname(file), { mode: 448, recursive: true });
674
+ writeFileSync2(file, `${JSON.stringify(credentials, null, 2)}
675
+ `, {
676
+ mode: 384
677
+ });
678
+ }
679
+ function storedApiKey(apiUrl2) {
680
+ return readCredentials().keys[apiOrigin(apiUrl2)] ?? null;
681
+ }
682
+ function storeApiKey(apiUrl2, key) {
683
+ const credentials = readCredentials();
684
+ credentials.keys[apiOrigin(apiUrl2)] = key;
685
+ writeCredentials(credentials);
686
+ }
687
+ function removeApiKey(apiUrl2) {
688
+ const credentials = readCredentials();
689
+ const origin = apiOrigin(apiUrl2);
690
+ if (!(origin in credentials.keys)) {
691
+ return false;
692
+ }
693
+ delete credentials.keys[origin];
694
+ writeCredentials(credentials);
695
+ return true;
696
+ }
697
+ function resolveApiKey(apiUrl2, flag) {
698
+ return flag ?? process.env.ARTIFACT_CAFE_API_KEY ?? storedApiKey(apiUrl2) ?? void 0;
699
+ }
700
+
701
+ // src/list.ts
702
+ async function list(opts) {
703
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
704
+ /\/$/,
705
+ ""
706
+ );
707
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
708
+ if (!key) {
709
+ throw new Error(
710
+ `Listing your artifacts needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
711
+ );
712
+ }
713
+ const data = await createApiClient(apiUrl2, key).listArtifacts();
714
+ if (opts.json) {
715
+ console.log(JSON.stringify(data, null, 2));
716
+ return;
717
+ }
718
+ if (data.artifacts.length === 0) {
719
+ console.log(
720
+ "No artifacts in your account yet \u2014 publish while logged in, or claim one via its claim link."
721
+ );
722
+ return;
723
+ }
724
+ const plural = data.artifacts.length === 1 ? "" : "s";
725
+ console.log(`${data.artifacts.length} artifact${plural}
726
+ `);
727
+ for (const artifact of data.artifacts) {
728
+ printArtifact(artifact);
729
+ }
730
+ }
731
+ function printArtifact(a) {
732
+ const version = a.currentVersionNumber === null ? "no version" : `v${a.currentVersionNumber}`;
733
+ const open2 = a.openCommentCount === 0 ? "no open comments" : `${a.openCommentCount} open comment${a.openCommentCount === 1 ? "" : "s"}`;
734
+ const marker = a.openCommentCount > 0 ? "\u25CF" : "\u25CB";
735
+ console.log(`${marker} ${a.title}`);
736
+ console.log(` ${version} \xB7 ${open2} \xB7 updated ${relativeTime(a.updatedAt)}`);
737
+ console.log(` ${a.url}
738
+ `);
739
+ }
740
+ function relativeTime(iso) {
741
+ const seconds = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1e3);
742
+ if (seconds < 60) {
743
+ return "just now";
744
+ }
745
+ if (seconds < 3600) {
746
+ return `${Math.floor(seconds / 60)}m ago`;
747
+ }
748
+ if (seconds < 86400) {
749
+ return `${Math.floor(seconds / 3600)}h ago`;
750
+ }
751
+ if (seconds < 30 * 86400) {
752
+ return `${Math.floor(seconds / 86400)}d ago`;
753
+ }
754
+ return iso.slice(0, 10);
755
+ }
756
+
757
+ // src/login.ts
758
+ import { createInterface } from "readline";
448
759
 
449
760
  // src/publish.ts
450
- import { readFileSync as readFileSync3 } from "fs";
451
- import path4 from "path";
452
761
  import { exec } from "child_process";
762
+ import { readFileSync as readFileSync4 } from "fs";
763
+ import path5 from "path";
453
764
 
454
765
  // src/manifest.ts
455
766
  import { createHash } from "crypto";
456
- import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
457
- import path3 from "path";
767
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
768
+ import path4 from "path";
458
769
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
459
770
  var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
460
771
  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;
772
+ if (IGNORED_FILES.has(name)) {
773
+ return true;
774
+ }
775
+ if (name === ".env" || name.startsWith(".env.")) {
776
+ return true;
777
+ }
778
+ if (name.endsWith(".pem") || name.endsWith(".key")) {
779
+ return true;
780
+ }
781
+ if (name.startsWith(".git")) {
782
+ return true;
783
+ }
465
784
  return false;
466
785
  }
467
786
  var MIME_TYPES = {
468
- ".html": "text/html",
469
- ".htm": "text/html",
787
+ ".avif": "image/avif",
470
788
  ".css": "text/css",
789
+ ".gif": "image/gif",
790
+ ".htm": "text/html",
791
+ ".html": "text/html",
792
+ ".ico": "image/x-icon",
793
+ ".jpeg": "image/jpeg",
794
+ ".jpg": "image/jpeg",
471
795
  ".js": "text/javascript",
472
- ".mjs": "text/javascript",
473
796
  ".json": "application/json",
474
797
  ".map": "application/json",
475
- ".txt": "text/plain",
476
798
  ".md": "text/markdown",
477
- ".xml": "application/xml",
478
- ".svg": "image/svg+xml",
799
+ ".mjs": "text/javascript",
800
+ ".mp3": "audio/mpeg",
801
+ ".mp4": "video/mp4",
802
+ ".otf": "font/otf",
803
+ ".pdf": "application/pdf",
479
804
  ".png": "image/png",
480
- ".jpg": "image/jpeg",
481
- ".jpeg": "image/jpeg",
482
- ".gif": "image/gif",
805
+ ".svg": "image/svg+xml",
806
+ ".ttf": "font/ttf",
807
+ ".txt": "text/plain",
808
+ ".wasm": "application/wasm",
809
+ ".wav": "audio/wav",
810
+ ".webm": "video/webm",
483
811
  ".webp": "image/webp",
484
- ".avif": "image/avif",
485
- ".ico": "image/x-icon",
486
812
  ".woff": "font/woff",
487
813
  ".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"
814
+ ".xml": "application/xml"
496
815
  };
497
816
  function mimeFor(filePath) {
498
- return MIME_TYPES[path3.extname(filePath).toLowerCase()] ?? "application/octet-stream";
817
+ return MIME_TYPES[path4.extname(filePath).toLowerCase()] ?? "application/octet-stream";
499
818
  }
500
819
  function scanDirectory(dir) {
501
820
  const files = [];
@@ -503,7 +822,7 @@ function scanDirectory(dir) {
503
822
  function walk(current, relPrefix) {
504
823
  for (const entry of readdirSync(current, { withFileTypes: true })) {
505
824
  const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
506
- const abs = path3.join(current, entry.name);
825
+ const abs = path4.join(current, entry.name);
507
826
  if (entry.isSymbolicLink()) {
508
827
  skipped.push(rel);
509
828
  continue;
@@ -516,23 +835,25 @@ function scanDirectory(dir) {
516
835
  }
517
836
  continue;
518
837
  }
519
- if (!entry.isFile()) continue;
838
+ if (!entry.isFile()) {
839
+ continue;
840
+ }
520
841
  if (isIgnoredFile(entry.name) || !isSafeArtifactPath(rel)) {
521
842
  skipped.push(rel);
522
843
  continue;
523
844
  }
524
- const size = statSync(abs).size;
845
+ const { size } = statSync(abs);
525
846
  if (size === 0) {
526
847
  skipped.push(rel);
527
848
  continue;
528
849
  }
529
- const content = readFileSync2(abs);
850
+ const content = readFileSync3(abs);
530
851
  files.push({
531
- path: rel,
852
+ absPath: abs,
532
853
  hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
533
- size,
534
854
  mimeType: mimeFor(rel),
535
- absPath: abs
855
+ path: rel,
856
+ size
536
857
  });
537
858
  }
538
859
  }
@@ -543,96 +864,146 @@ function scanDirectory(dir) {
543
864
  function resolveEntry(files, explicit) {
544
865
  if (explicit) {
545
866
  if (!files.some((f) => f.path === explicit)) {
546
- throw new Error(`Entry file "${explicit}" not found in the publish folder.`);
867
+ throw new Error(
868
+ `Entry file "${explicit}" not found in the publish folder.`
869
+ );
547
870
  }
548
871
  return explicit;
549
872
  }
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;
873
+ if (files.some((f) => f.path === "index.html")) {
874
+ return "index.html";
875
+ }
876
+ const htmlFiles = files.filter(
877
+ (f) => f.path.endsWith(".html") && !f.path.includes("/")
878
+ );
879
+ const [onlyHtmlFile] = htmlFiles;
880
+ if (onlyHtmlFile && htmlFiles.length === 1) {
881
+ return onlyHtmlFile.path;
882
+ }
553
883
  throw new Error(
554
884
  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
885
  );
556
886
  }
557
887
 
558
888
  // src/publish.ts
559
- var DEFAULT_API_URL = "https://artifact.cafe";
560
889
  var UPLOAD_CONCURRENCY = 6;
561
890
  async function publish(opts) {
562
- const dir = path4.resolve(opts.dir);
891
+ const dir = path5.resolve(opts.dir);
563
892
  const config = readConfig(dir);
893
+ if (opts.password !== void 0) {
894
+ const parsed = gatePasswordSchema.safeParse(opts.password);
895
+ if (!parsed.success) {
896
+ throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
897
+ }
898
+ }
564
899
  const artifactId = opts.artifact ?? config?.artifactId ?? null;
565
900
  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;
901
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
902
+ const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
567
903
  const { files, skipped } = scanDirectory(dir);
568
904
  if (files.length === 0) {
569
905
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
570
906
  }
571
907
  const entryPath = resolveEntry(files, opts.entry);
572
908
  const log = (msg) => {
573
- if (!opts.json) console.log(msg);
909
+ if (!opts.json) {
910
+ console.log(msg);
911
+ }
574
912
  };
575
913
  if (skipped.length) {
576
914
  log(` ignoring ${skipped.length} item(s): ${preview(skipped)}`);
577
915
  }
578
- const client = createApiClient(apiUrl, token);
579
- const session = await client.createSession({
916
+ const sessionBody = {
580
917
  artifactId,
581
- title: opts.title,
582
918
  entryPath,
583
919
  files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
584
920
  source: {
585
- type: "cli",
586
921
  agent: process.env.ARTIFACT_CAFE_AGENT,
587
- runId: process.env.ARTIFACT_CAFE_RUN_ID
922
+ runId: process.env.ARTIFACT_CAFE_RUN_ID,
923
+ type: "cli"
924
+ },
925
+ title: opts.title
926
+ };
927
+ let credential = apiKey ?? token;
928
+ let client = createApiClient(apiUrl2, credential);
929
+ let session;
930
+ try {
931
+ session = await client.createSession(sessionBody);
932
+ } catch (err) {
933
+ const keyRefused = apiKey !== void 0 && token !== void 0 && apiKey !== token && artifactId !== null && err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
934
+ if (!keyRefused) {
935
+ throw err;
588
936
  }
589
- });
937
+ credential = token;
938
+ client = createApiClient(apiUrl2, credential);
939
+ session = await client.createSession(sessionBody);
940
+ }
590
941
  const byPath = new Map(files.map((f) => [f.path, f]));
591
942
  log(
592
943
  ` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
593
944
  );
594
945
  await inBatches(session.filesToUpload, UPLOAD_CONCURRENCY, async (upload) => {
595
946
  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);
947
+ if (!file) {
948
+ throw new Error(`Server asked for unknown file ${upload.path}`);
949
+ }
950
+ await uploadFile(
951
+ upload.uploadUrl,
952
+ readFileSync4(file.absPath),
953
+ file.mimeType
954
+ );
598
955
  });
599
956
  const result = await client.finalize(session.publishSessionId);
600
957
  if (result.publishToken) {
601
958
  writeConfig(dir, {
959
+ apiUrl: apiUrl2,
602
960
  artifactId: result.artifactId,
961
+ publishToken: result.publishToken,
603
962
  slug: slugFromUrl(result.url),
604
- url: result.url,
605
- apiUrl,
606
- publishToken: result.publishToken
963
+ url: result.url
607
964
  });
608
965
  }
966
+ if (opts.password !== void 0) {
967
+ const protectClient = createApiClient(
968
+ apiUrl2,
969
+ credential ?? result.publishToken
970
+ );
971
+ await protectClient.setProtection(result.artifactId, opts.password);
972
+ }
609
973
  if (opts.json) {
610
974
  console.log(
611
975
  JSON.stringify(
612
976
  {
613
977
  artifactId: result.artifactId,
614
- versionNumber: result.versionNumber,
978
+ claimed: result.claimed,
615
979
  url: result.url,
980
+ versionNumber: result.versionNumber,
616
981
  ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
982
+ ...opts.password === void 0 ? {} : { protected: true },
617
983
  filesUploaded: session.filesToUpload.map((f) => f.path),
618
- skippedFiles: session.skippedFiles,
619
- ignored: skipped
984
+ ignored: skipped,
985
+ skippedFiles: session.skippedFiles
620
986
  },
621
987
  null,
622
988
  2
623
989
  )
624
990
  );
625
991
  } else if (result.versionNumber === 1) {
626
- console.log(`
627
- \u2713 Published to artifact.cafe
628
- `);
992
+ console.log("\n\u2713 Published to artifact.cafe\n");
629
993
  console.log(`Review URL:
630
994
  ${result.url}
631
995
  `);
632
996
  if (result.claimUrl) {
633
- console.log(`Claim URL (shown once \u2014 claim to keep this artifact):
997
+ console.log(
998
+ `Claim URL (shown once \u2014 claim to keep this artifact):
634
999
  ${result.claimUrl}
635
- `);
1000
+ `
1001
+ );
1002
+ } else if (result.claimed) {
1003
+ console.log("Claimed by your account \u2014 no expiry.\n");
1004
+ }
1005
+ if (opts.password !== void 0) {
1006
+ console.log("Password protection: on\n");
636
1007
  }
637
1008
  } else {
638
1009
  console.log(`
@@ -640,6 +1011,9 @@ ${result.claimUrl}
640
1011
  `);
641
1012
  console.log(`${result.url}
642
1013
  `);
1014
+ if (opts.password !== void 0) {
1015
+ console.log("Password protection: on\n");
1016
+ }
643
1017
  }
644
1018
  if (opts.open && !opts.json && process.stdout.isTTY) {
645
1019
  openInBrowser(result.url);
@@ -647,12 +1021,19 @@ ${result.claimUrl}
647
1021
  }
648
1022
  async function inBatches(items, concurrency, fn) {
649
1023
  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);
1024
+ const workers = Array.from(
1025
+ { length: Math.min(concurrency, items.length) },
1026
+ async () => {
1027
+ while (next < items.length) {
1028
+ const item = items[next];
1029
+ next += 1;
1030
+ if (item === void 0) {
1031
+ continue;
1032
+ }
1033
+ await fn(item);
1034
+ }
654
1035
  }
655
- });
1036
+ );
656
1037
  await Promise.all(workers);
657
1038
  }
658
1039
  function slugFromUrl(url) {
@@ -662,14 +1043,133 @@ function preview(items) {
662
1043
  return items.slice(0, 5).join(", ") + (items.length > 5 ? ", \u2026" : "");
663
1044
  }
664
1045
  function openInBrowser(url) {
665
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1046
+ let cmd = "xdg-open";
1047
+ if (process.platform === "darwin") {
1048
+ cmd = "open";
1049
+ } else if (process.platform === "win32") {
1050
+ cmd = "start";
1051
+ }
666
1052
  exec(`${cmd} ${JSON.stringify(url)}`, () => {
667
1053
  });
668
1054
  }
669
1055
 
1056
+ // src/login.ts
1057
+ function apiUrl() {
1058
+ return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1059
+ }
1060
+ function promptSecret(question) {
1061
+ return new Promise((resolve) => {
1062
+ const rl = createInterface({
1063
+ input: process.stdin,
1064
+ output: process.stdout
1065
+ });
1066
+ let muted = false;
1067
+ const raw = rl;
1068
+ raw._writeToOutput = (s) => {
1069
+ if (!muted) {
1070
+ process.stdout.write(s);
1071
+ }
1072
+ };
1073
+ rl.question(question, (answer) => {
1074
+ rl.close();
1075
+ process.stdout.write("\n");
1076
+ resolve(answer);
1077
+ });
1078
+ muted = true;
1079
+ });
1080
+ }
1081
+ async function login(opts) {
1082
+ const base = apiUrl();
1083
+ const keysUrl = `${base}/cli`;
1084
+ let key = opts.apiKey?.trim();
1085
+ if (!key) {
1086
+ if (!opts.json) {
1087
+ console.log(`
1088
+ Opening ${keysUrl}`);
1089
+ console.log("Create a CLI key there, then paste it below.\n");
1090
+ }
1091
+ if (process.stdout.isTTY) {
1092
+ openInBrowser(keysUrl);
1093
+ }
1094
+ key = (await promptSecret("API key: ")).trim();
1095
+ }
1096
+ if (!key) {
1097
+ throw new Error("No key entered \u2014 nothing was saved.");
1098
+ }
1099
+ if (!key.startsWith("ack_")) {
1100
+ throw new Error(
1101
+ `That doesn't look like an artifact.cafe API key (they start with "ack_").`
1102
+ );
1103
+ }
1104
+ const me = await createApiClient(base, key).me();
1105
+ storeApiKey(base, key);
1106
+ const who = me.email ?? me.userId;
1107
+ if (opts.json) {
1108
+ console.log(
1109
+ JSON.stringify(
1110
+ { apiUrl: base, email: me.email, loggedIn: true, userId: me.userId },
1111
+ null,
1112
+ 2
1113
+ )
1114
+ );
1115
+ return;
1116
+ }
1117
+ console.log(`
1118
+ \u2713 Logged in to ${apiOrigin(base)} as ${who}`);
1119
+ console.log(
1120
+ "New publishes are claimed by your account automatically. Try `artifact-cafe list`.\n"
1121
+ );
1122
+ }
1123
+ async function whoami(opts) {
1124
+ const base = apiUrl();
1125
+ const key = resolveApiKey(base);
1126
+ if (!key) {
1127
+ throw new Error(
1128
+ `Not logged in to ${apiOrigin(base)} \u2014 run \`artifact-cafe login\` first.`
1129
+ );
1130
+ }
1131
+ const me = await createApiClient(base, key).me();
1132
+ if (opts.json) {
1133
+ console.log(
1134
+ JSON.stringify(
1135
+ {
1136
+ apiUrl: base,
1137
+ email: me.email,
1138
+ keyName: me.keyName ?? null,
1139
+ userId: me.userId
1140
+ },
1141
+ null,
1142
+ 2
1143
+ )
1144
+ );
1145
+ return;
1146
+ }
1147
+ const keyLabel = me.keyName ? ` (key: ${me.keyName})` : "";
1148
+ console.log(
1149
+ `Logged in to ${apiOrigin(base)} as ${me.email ?? me.userId}${keyLabel}`
1150
+ );
1151
+ }
1152
+ function logout(opts) {
1153
+ const base = apiUrl();
1154
+ const removed = removeApiKey(base);
1155
+ if (opts.json) {
1156
+ console.log(JSON.stringify({ apiUrl: base, loggedOut: removed }, null, 2));
1157
+ return;
1158
+ }
1159
+ if (!removed) {
1160
+ console.log(`No stored key for ${apiOrigin(base)} \u2014 already logged out.`);
1161
+ return;
1162
+ }
1163
+ console.log(`\u2713 Logged out of ${apiOrigin(base)} on this machine.`);
1164
+ console.log(
1165
+ `The key itself is still active \u2014 revoke it at ${base}/cli if this machine is shared.`
1166
+ );
1167
+ }
1168
+
670
1169
  // src/open.ts
1170
+ import path6 from "path";
671
1171
  function openArtifact(opts) {
672
- const dir = path5.resolve(opts.dir);
1172
+ const dir = path6.resolve(opts.dir);
673
1173
  const config = readConfig(dir);
674
1174
  if (!config) {
675
1175
  throw new Error(
@@ -677,29 +1177,90 @@ function openArtifact(opts) {
677
1177
  );
678
1178
  }
679
1179
  console.log(config.url);
680
- if (process.stdout.isTTY) openInBrowser(config.url);
1180
+ if (process.stdout.isTTY) {
1181
+ openInBrowser(config.url);
1182
+ }
1183
+ }
1184
+
1185
+ // src/protect.ts
1186
+ import path7 from "path";
1187
+ async function protect(opts) {
1188
+ if (opts.off && opts.password !== void 0) {
1189
+ throw new Error("Pass either --password or --off, not both.");
1190
+ }
1191
+ if (!opts.off && opts.password === void 0) {
1192
+ throw new Error(
1193
+ "Provide --password <password> to set a gate, or --off to remove it."
1194
+ );
1195
+ }
1196
+ const password = opts.off ? null : opts.password;
1197
+ if (password !== null) {
1198
+ const parsed = gatePasswordSchema.safeParse(password);
1199
+ if (!parsed.success) {
1200
+ throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
1201
+ }
1202
+ }
1203
+ const dir = path7.resolve(opts.dir);
1204
+ const config = readConfig(dir);
1205
+ if (!config) {
1206
+ throw new Error(
1207
+ `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
1208
+ );
1209
+ }
1210
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
1211
+ const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
1212
+ const client = createApiClient(apiUrl2, token);
1213
+ const res = await client.setProtection(config.artifactId, password);
1214
+ if (opts.json) {
1215
+ console.log(JSON.stringify({ protected: res.protected }, null, 2));
1216
+ return;
1217
+ }
1218
+ console.log(
1219
+ res.protected ? `
1220
+ \u2713 Password protection on \u2014 ${config.url}
1221
+ ` : `
1222
+ \u2713 Password protection removed \u2014 ${config.url}
1223
+ `
1224
+ );
681
1225
  }
682
1226
 
683
1227
  // src/index.ts
684
1228
  var publish2 = defineCommand({
685
- meta: { name: "publish", description: "Publish a folder as an artifact" },
686
1229
  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" }
1230
+ "api-key": {
1231
+ description: "Publish with an account API key (default: stored login)",
1232
+ type: "string"
1233
+ },
1234
+ artifact: {
1235
+ description: "Publish to an existing artifact ID",
1236
+ type: "string"
1237
+ },
1238
+ dir: {
1239
+ default: ".",
1240
+ description: "Directory to publish",
1241
+ type: "positional"
1242
+ },
1243
+ entry: { description: "Entry file (default: auto-detect)", type: "string" },
1244
+ json: { description: "Machine-readable output", type: "boolean" },
1245
+ "no-open": { description: "Don't open the review URL", type: "boolean" },
1246
+ password: {
1247
+ description: "Password-protect the artifact (min 6 chars)",
1248
+ type: "string"
1249
+ },
1250
+ title: { description: "Artifact title", type: "string" }
693
1251
  },
1252
+ meta: { description: "Publish a folder as an artifact", name: "publish" },
694
1253
  async run({ args }) {
695
1254
  try {
696
1255
  await publish({
1256
+ apiKey: args["api-key"],
1257
+ artifact: args.artifact,
697
1258
  dir: args.dir,
698
- title: args.title,
699
1259
  entry: args.entry,
700
- artifact: args.artifact,
701
1260
  json: Boolean(args.json),
702
- open: !args["no-open"]
1261
+ open: !args["no-open"],
1262
+ password: args.password,
1263
+ title: args.title
703
1264
  });
704
1265
  } catch (err) {
705
1266
  printCliError(err, Boolean(args.json));
@@ -707,13 +1268,104 @@ var publish2 = defineCommand({
707
1268
  }
708
1269
  }
709
1270
  });
1271
+ var login2 = defineCommand({
1272
+ args: {
1273
+ "api-key": {
1274
+ description: "Store this key without the browser prompt (agents, CI)",
1275
+ type: "string"
1276
+ },
1277
+ json: { description: "Machine-readable output", type: "boolean" }
1278
+ },
1279
+ meta: {
1280
+ description: "Log in \u2014 mint an API key in the browser and store it",
1281
+ name: "login"
1282
+ },
1283
+ async run({ args }) {
1284
+ try {
1285
+ await login({ apiKey: args["api-key"], json: Boolean(args.json) });
1286
+ } catch (err) {
1287
+ printCliError(err, Boolean(args.json));
1288
+ process.exit(1);
1289
+ }
1290
+ }
1291
+ });
1292
+ var logout2 = defineCommand({
1293
+ args: {
1294
+ json: { description: "Machine-readable output", type: "boolean" }
1295
+ },
1296
+ meta: {
1297
+ description: "Remove the stored API key for this machine",
1298
+ name: "logout"
1299
+ },
1300
+ run({ args }) {
1301
+ try {
1302
+ logout({ json: Boolean(args.json) });
1303
+ } catch (err) {
1304
+ printCliError(err, Boolean(args.json));
1305
+ process.exit(1);
1306
+ }
1307
+ }
1308
+ });
1309
+ var whoami2 = defineCommand({
1310
+ args: {
1311
+ json: { description: "Machine-readable output", type: "boolean" }
1312
+ },
1313
+ meta: {
1314
+ description: "Show which account the stored API key belongs to",
1315
+ name: "whoami"
1316
+ },
1317
+ async run({ args }) {
1318
+ try {
1319
+ await whoami({ json: Boolean(args.json) });
1320
+ } catch (err) {
1321
+ printCliError(err, Boolean(args.json));
1322
+ process.exit(1);
1323
+ }
1324
+ }
1325
+ });
1326
+ var list2 = defineCommand({
1327
+ args: {
1328
+ "api-key": {
1329
+ description: "List with this API key (default: stored login)",
1330
+ type: "string"
1331
+ },
1332
+ json: { description: "Machine-readable output", type: "boolean" }
1333
+ },
1334
+ meta: {
1335
+ description: "List the artifacts your account owns",
1336
+ name: "list"
1337
+ },
1338
+ async run({ args }) {
1339
+ try {
1340
+ await list({ apiKey: args["api-key"], json: Boolean(args.json) });
1341
+ } catch (err) {
1342
+ printCliError(err, Boolean(args.json));
1343
+ process.exit(1);
1344
+ }
1345
+ }
1346
+ });
710
1347
  var comments2 = defineCommand({
711
- meta: { name: "comments", description: "Pull comments for the linked artifact" },
712
1348
  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" }
1349
+ dir: {
1350
+ default: ".",
1351
+ description: "Published directory",
1352
+ type: "positional"
1353
+ },
1354
+ json: { description: "Machine-readable output", type: "boolean" },
1355
+ status: {
1356
+ default: "open",
1357
+ description: "Filter: open | resolved | all",
1358
+ type: "string"
1359
+ },
1360
+ version: {
1361
+ default: "current",
1362
+ description: "Filter: current | all",
1363
+ type: "string"
1364
+ }
1365
+ },
1366
+ meta: {
1367
+ description: "Pull comments for the linked artifact",
1368
+ name: "comments"
717
1369
  },
718
1370
  async run({ args }) {
719
1371
  try {
@@ -729,11 +1381,44 @@ var comments2 = defineCommand({
729
1381
  }
730
1382
  }
731
1383
  });
1384
+ var protect2 = defineCommand({
1385
+ args: {
1386
+ dir: {
1387
+ default: ".",
1388
+ description: "Published directory",
1389
+ type: "positional"
1390
+ },
1391
+ json: { description: "Machine-readable output", type: "boolean" },
1392
+ off: { description: "Remove the password gate", type: "boolean" },
1393
+ password: { description: "Gate password (min 6 chars)", type: "string" }
1394
+ },
1395
+ meta: {
1396
+ description: "Password-protect the linked artifact (or --off to remove)",
1397
+ name: "protect"
1398
+ },
1399
+ async run({ args }) {
1400
+ try {
1401
+ await protect({
1402
+ dir: args.dir,
1403
+ json: Boolean(args.json),
1404
+ off: Boolean(args.off),
1405
+ password: args.password
1406
+ });
1407
+ } catch (err) {
1408
+ printCliError(err, Boolean(args.json));
1409
+ process.exit(1);
1410
+ }
1411
+ }
1412
+ });
732
1413
  var open = defineCommand({
733
- meta: { name: "open", description: "Open the linked artifact's review URL" },
734
1414
  args: {
735
- dir: { type: "positional", description: "Published directory", default: "." }
1415
+ dir: {
1416
+ default: ".",
1417
+ description: "Published directory",
1418
+ type: "positional"
1419
+ }
736
1420
  },
1421
+ meta: { description: "Open the linked artifact's review URL", name: "open" },
737
1422
  run({ args }) {
738
1423
  try {
739
1424
  openArtifact({ dir: args.dir });
@@ -743,18 +1428,20 @@ var open = defineCommand({
743
1428
  }
744
1429
  }
745
1430
  });
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
1431
  var main = defineCommand({
754
1432
  meta: {
755
- name: "artifact-cafe",
756
- description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe"
1433
+ description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1434
+ name: "artifact-cafe"
757
1435
  },
758
- subCommands: { publish: publish2, comments: comments2, open, login }
1436
+ subCommands: {
1437
+ comments: comments2,
1438
+ list: list2,
1439
+ login: login2,
1440
+ logout: logout2,
1441
+ open,
1442
+ protect: protect2,
1443
+ publish: publish2,
1444
+ whoami: whoami2
1445
+ }
759
1446
  });
760
1447
  runMain(main);