artifact-cafe 0.0.2 → 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 +631 -197
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,102 +6,149 @@ import { defineCommand, runMain } from "citty";
6
6
  // src/comments.ts
7
7
  import path2 from "path";
8
8
 
9
- // ../shared/src/comments.ts
9
+ // ../shared/src/account.ts
10
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()
11
+ var createApiKeySchema = z.object({
12
+ name: z.string().trim().min(1).max(60).optional()
13
+ });
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()
40
+ });
41
+ var artifactListItemSchema = z.object({
42
+ createdAt: z.string(),
43
+ currentVersionNumber: z.number().int().positive().nullable(),
44
+ id: z.string(),
45
+ openCommentCount: z.number().int().nonnegative(),
46
+ slug: z.string(),
47
+ title: z.string(),
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)
16
54
  });
17
- var pointAnchorSchema = z.object({
55
+
56
+ // ../shared/src/comments.ts
57
+ import { z as z2 } from "zod";
58
+ var elementSnapshotSchema = z2.object({
59
+ classes: z2.string().max(256).optional(),
60
+ id: z2.string().max(256).optional(),
61
+ tag: z2.string().min(1).max(128),
62
+ text: z2.string().max(200).optional()
63
+ });
64
+ var pointAnchorSchema = z2.object({
18
65
  element: elementSnapshotSchema.optional(),
19
66
  // elementPath is the primary anchor; x/y are offsets within the
20
67
  // 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", [
68
+ elementPath: z2.string().max(2048),
69
+ type: z2.literal("point"),
70
+ viewportHeight: z2.number().int().positive().max(1e5),
71
+ viewportWidth: z2.number().int().positive().max(1e5),
72
+ x: z2.number().min(0).max(1),
73
+ y: z2.number().min(0).max(1)
74
+ });
75
+ var textAnchorSchema = z2.object({
76
+ elementPath: z2.string().max(2048),
77
+ endOffset: z2.number().int().nonnegative().max(1e8),
78
+ prefix: z2.string().max(200).optional(),
79
+ quote: z2.string().min(1).max(1e3),
80
+ startOffset: z2.number().int().nonnegative().max(1e8),
81
+ suffix: z2.string().max(200).optional(),
82
+ type: z2.literal("text")
83
+ });
84
+ var anchorSchema = z2.discriminatedUnion("type", [
38
85
  pointAnchorSchema,
39
86
  textAnchorSchema
40
87
  ]);
41
- var guestSchema = z.object({
42
- displayName: z.string().min(1).max(64),
43
- guestId: z.string().min(16)
88
+ var guestSchema = z2.object({
89
+ displayName: z2.string().min(1).max(64),
90
+ guestId: z2.string().min(16)
44
91
  });
45
- var createCommentThreadSchema = z.object({
92
+ var createCommentThreadSchema = z2.object({
46
93
  anchor: anchorSchema,
47
- body: z.string().min(1).max(1e4),
94
+ body: z2.string().min(1).max(1e4),
48
95
  guest: guestSchema.optional(),
49
- versionId: z.string()
96
+ versionId: z2.string()
50
97
  });
51
- var createCommentMessageSchema = z.object({
52
- body: z.string().min(1).max(1e4),
98
+ var createCommentMessageSchema = z2.object({
99
+ body: z2.string().min(1).max(1e4),
53
100
  guest: guestSchema.optional()
54
101
  });
55
- var threadActionSchema = z.object({
102
+ var threadActionSchema = z2.object({
56
103
  guest: guestSchema.optional()
57
104
  });
58
- var commentAuthorSchema = z.object({
59
- displayName: z.string(),
60
- guestId: z.string().nullable()
105
+ var commentAuthorSchema = z2.object({
106
+ displayName: z2.string(),
107
+ guestId: z2.string().nullable()
61
108
  });
62
- var commentMessageSchema = z.object({
109
+ var commentMessageSchema = z2.object({
63
110
  author: commentAuthorSchema,
64
- body: z.string(),
65
- createdAt: z.string(),
66
- id: z.string()
111
+ body: z2.string(),
112
+ createdAt: z2.string(),
113
+ id: z2.string()
67
114
  });
68
- var commentThreadSchema = z.object({
115
+ var commentThreadSchema = z2.object({
69
116
  anchor: anchorSchema,
70
- artifactId: z.string(),
71
- createdAt: z.string(),
117
+ artifactId: z2.string(),
118
+ createdAt: z2.string(),
72
119
  createdBy: commentAuthorSchema,
73
- id: z.string(),
74
- messages: z.array(commentMessageSchema),
75
- status: z.enum(["open", "resolved"]),
76
- versionId: z.string()
120
+ id: z2.string(),
121
+ messages: z2.array(commentMessageSchema),
122
+ status: z2.enum(["open", "resolved"]),
123
+ versionId: z2.string()
77
124
  });
78
- var commentThreadListSchema = z.object({
79
- threads: z.array(commentThreadSchema)
125
+ var commentThreadListSchema = z2.object({
126
+ threads: z2.array(commentThreadSchema)
80
127
  });
81
- var commentExportQuerySchema = z.object({
82
- status: z.enum(["open", "resolved", "all"]).default("open"),
83
- version: z.enum(["current", "all"]).default("current")
128
+ var commentExportQuerySchema = z2.object({
129
+ status: z2.enum(["open", "resolved", "all"]).default("open"),
130
+ version: z2.enum(["current", "all"]).default("current")
84
131
  });
85
- var commentExportReplySchema = z.object({
86
- author: z.string(),
87
- body: z.string(),
88
- createdAt: z.string()
132
+ var commentExportReplySchema = z2.object({
133
+ author: z2.string(),
134
+ body: z2.string(),
135
+ createdAt: z2.string()
89
136
  });
90
- var commentExportThreadSchema = z.object({
137
+ var commentExportThreadSchema = z2.object({
91
138
  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()
139
+ author: z2.string(),
140
+ body: z2.string(),
141
+ createdAt: z2.string(),
142
+ id: z2.string(),
143
+ replies: z2.array(commentExportReplySchema),
144
+ status: z2.enum(["open", "resolved"]),
145
+ versionNumber: z2.number().int().positive()
99
146
  });
100
- var commentExportSchema = z.object({
101
- artifactId: z.string(),
102
- comments: z.array(commentExportThreadSchema),
147
+ var commentExportSchema = z2.object({
148
+ artifactId: z2.string(),
149
+ comments: z2.array(commentExportThreadSchema),
103
150
  // The artifact's current version (null before the first finalize).
104
- versionNumber: z.number().int().positive().nullable()
151
+ versionNumber: z2.number().int().positive().nullable()
105
152
  });
106
153
  function elementLabel(el) {
107
154
  if (el.id) {
@@ -112,55 +159,55 @@ function elementLabel(el) {
112
159
  }
113
160
 
114
161
  // ../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()
162
+ import { z as z3 } from "zod";
163
+ var apiErrorSchema = z3.object({
164
+ code: z3.string(),
165
+ docsUrl: z3.string().url(),
166
+ message: z3.string(),
167
+ retryAfter: z3.number().int().positive().optional()
121
168
  });
122
169
 
123
170
  // ../shared/src/owner.ts
124
- import { z as z3 } from "zod";
171
+ import { z as z4 } from "zod";
125
172
  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, {
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, {
134
181
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
135
182
  }).optional(),
136
- title: z3.string().min(1).max(200).optional()
183
+ title: z4.string().min(1).max(200).optional()
137
184
  }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
138
185
  message: "provide at least one of title, slug"
139
186
  });
140
- var deleteArtifactResponseSchema = z3.object({
141
- deleted: z3.literal(true)
187
+ var deleteArtifactResponseSchema = z4.object({
188
+ deleted: z4.literal(true)
142
189
  });
143
190
 
144
191
  // ../shared/src/protection.ts
145
- import { z as z4 } from "zod";
192
+ import { z as z5 } from "zod";
146
193
  var PASSWORD_MIN = 6;
147
194
  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({
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({
150
197
  password: gatePasswordSchema.nullable()
151
198
  });
152
- var protectionResponseSchema = z4.object({
153
- protected: z4.boolean()
199
+ var protectionResponseSchema = z5.object({
200
+ protected: z5.boolean()
154
201
  });
155
- var unlockSchema = z4.object({
156
- password: z4.string().min(1).max(1024)
202
+ var unlockSchema = z5.object({
203
+ password: z5.string().min(1).max(1024)
157
204
  });
158
- var unlockResponseSchema = z4.object({
159
- unlocked: z4.literal(true)
205
+ var unlockResponseSchema = z5.object({
206
+ unlocked: z5.literal(true)
160
207
  });
161
208
 
162
209
  // ../shared/src/publish.ts
163
- import { z as z5 } from "zod";
210
+ import { z as z6 } from "zod";
164
211
  function isSafeArtifactPath(p) {
165
212
  if (p.length === 0 || p.length > 1024) {
166
213
  return false;
@@ -173,95 +220,128 @@ function isSafeArtifactPath(p) {
173
220
  }
174
221
  return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
175
222
  }
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, {
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, {
180
227
  message: "path must be a safe relative path"
181
228
  }),
182
- size: z5.number().int().positive()
229
+ size: z6.number().int().positive()
183
230
  });
184
- var publishSourceSchema = z5.object({
185
- agent: z5.string().optional(),
186
- runId: z5.string().optional(),
187
- type: z5.literal("cli")
231
+ var publishSourceSchema = z6.object({
232
+ agent: z6.string().optional(),
233
+ runId: z6.string().optional(),
234
+ type: z6.literal("cli")
188
235
  });
189
- var createPublishSessionSchema = z5.object({
190
- artifactId: z5.string().nullable(),
191
- entryPath: z5.string().optional(),
192
- files: z5.array(manifestFileSchema).min(1).max(1e3),
236
+ var createPublishSessionSchema = z6.object({
237
+ artifactId: z6.string().nullable(),
238
+ entryPath: z6.string().optional(),
239
+ files: z6.array(manifestFileSchema).min(1).max(1e3),
193
240
  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()
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()
204
251
  })
205
252
  ),
206
- publishSessionId: z5.string(),
207
- skippedFiles: z5.array(z5.string()),
208
- versionId: z5.string()
253
+ publishSessionId: z6.string(),
254
+ skippedFiles: z6.array(z6.string()),
255
+ versionId: z6.string()
209
256
  });
210
- var finalizeResponseSchema = z5.object({
211
- artifactId: z5.string(),
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(),
212
262
  // 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()
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()
225
275
  }).nullable(),
226
- id: z5.string(),
227
- slug: z5.string(),
228
- title: z5.string()
276
+ id: z6.string(),
277
+ slug: z6.string(),
278
+ title: z6.string()
229
279
  });
230
280
 
231
281
  // ../shared/src/report.ts
232
- import { z as z6 } from "zod";
233
- var reportRequestSchema = z6.object({
234
- reason: z6.string().max(1e3).optional()
282
+ import { z as z7 } from "zod";
283
+ var reportRequestSchema = z7.object({
284
+ reason: z7.string().max(1e3).optional()
235
285
  });
236
- var reportResponseSchema = z6.object({
237
- reported: z6.literal(true)
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)
238
318
  });
239
319
 
240
320
  // ../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()
321
+ import { z as z9 } from "zod";
322
+ var versionSourceSchema = z9.object({
323
+ agentName: z9.string().nullable(),
324
+ type: z9.string()
245
325
  });
246
- var versionChangesSchema = z7.object({
247
- added: z7.array(z7.string()),
248
- changed: z7.array(z7.string()),
249
- removed: z7.array(z7.string())
326
+ var versionChangesSchema = z9.object({
327
+ added: z9.array(z9.string()),
328
+ changed: z9.array(z9.string()),
329
+ removed: z9.array(z9.string())
250
330
  });
251
- var versionSummarySchema = z7.object({
331
+ var versionSummarySchema = z9.object({
252
332
  // Absent for the first version — there is no previous version to diff against.
253
333
  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(),
334
+ createdAt: z9.string(),
335
+ entryUrl: z9.string().url(),
336
+ fileCount: z9.number().int().nonnegative(),
337
+ id: z9.string(),
338
+ isCurrent: z9.boolean(),
259
339
  source: versionSourceSchema,
260
- totalSizeBytes: z7.number().int().nonnegative(),
261
- versionNumber: z7.number().int().positive()
340
+ totalSizeBytes: z9.number().int().nonnegative(),
341
+ versionNumber: z9.number().int().positive()
262
342
  });
263
- var versionListResponseSchema = z7.object({
264
- versions: z7.array(versionSummarySchema)
343
+ var versionListResponseSchema = z9.object({
344
+ versions: z9.array(versionSummarySchema)
265
345
  });
266
346
 
267
347
  // src/api.ts
@@ -306,8 +386,8 @@ async function request(url, init, parse) {
306
386
  }
307
387
  return parse(body);
308
388
  }
309
- function createApiClient(apiUrl, publishToken) {
310
- const base = apiUrl.replace(/\/$/, "");
389
+ function createApiClient(apiUrl2, publishToken) {
390
+ const base = apiUrl2.replace(/\/$/, "");
311
391
  const headers = {
312
392
  "content-type": "application/json"
313
393
  };
@@ -347,6 +427,24 @@ function createApiClient(apiUrl, publishToken) {
347
427
  (d) => artifactResponseSchema.parse(d)
348
428
  );
349
429
  },
430
+ // SPEC §6.18 — everything owned by the account this client's API
431
+ // key (or session) belongs to.
432
+ async listArtifacts() {
433
+ return request(
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
+ },
350
448
  // SPEC §6.16 — set a password (string) or clear it (null). Authorized
351
449
  // by the publish token this client was created with.
352
450
  async setProtection(artifactId, password) {
@@ -383,13 +481,13 @@ import {
383
481
  writeFileSync
384
482
  } from "fs";
385
483
  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()
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()
393
491
  });
394
492
  var CONFIG_DIR = ".artifactcafe";
395
493
  var CONFIG_FILE = "config.json";
@@ -458,8 +556,8 @@ async function comments(opts) {
458
556
  `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
459
557
  );
460
558
  }
461
- const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
462
- const client = createApiClient(apiUrl);
559
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
560
+ const client = createApiClient(apiUrl2);
463
561
  const data = await client.exportComments(config.artifactId, {
464
562
  status,
465
563
  version
@@ -543,18 +641,131 @@ function printCliError(err, json) {
543
641
  }
544
642
  }
545
643
 
546
- // src/open.ts
547
- 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";
548
759
 
549
760
  // src/publish.ts
550
761
  import { exec } from "child_process";
551
- import { readFileSync as readFileSync3 } from "fs";
552
- import path4 from "path";
762
+ import { readFileSync as readFileSync4 } from "fs";
763
+ import path5 from "path";
553
764
 
554
765
  // src/manifest.ts
555
766
  import { createHash } from "crypto";
556
- import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
557
- import path3 from "path";
767
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
768
+ import path4 from "path";
558
769
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
559
770
  var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
560
771
  function isIgnoredFile(name) {
@@ -603,7 +814,7 @@ var MIME_TYPES = {
603
814
  ".xml": "application/xml"
604
815
  };
605
816
  function mimeFor(filePath) {
606
- return MIME_TYPES[path3.extname(filePath).toLowerCase()] ?? "application/octet-stream";
817
+ return MIME_TYPES[path4.extname(filePath).toLowerCase()] ?? "application/octet-stream";
607
818
  }
608
819
  function scanDirectory(dir) {
609
820
  const files = [];
@@ -611,7 +822,7 @@ function scanDirectory(dir) {
611
822
  function walk(current, relPrefix) {
612
823
  for (const entry of readdirSync(current, { withFileTypes: true })) {
613
824
  const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
614
- const abs = path3.join(current, entry.name);
825
+ const abs = path4.join(current, entry.name);
615
826
  if (entry.isSymbolicLink()) {
616
827
  skipped.push(rel);
617
828
  continue;
@@ -636,7 +847,7 @@ function scanDirectory(dir) {
636
847
  skipped.push(rel);
637
848
  continue;
638
849
  }
639
- const content = readFileSync2(abs);
850
+ const content = readFileSync3(abs);
640
851
  files.push({
641
852
  absPath: abs,
642
853
  hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
@@ -675,10 +886,9 @@ function resolveEntry(files, explicit) {
675
886
  }
676
887
 
677
888
  // src/publish.ts
678
- var DEFAULT_API_URL = "https://artifact.cafe";
679
889
  var UPLOAD_CONCURRENCY = 6;
680
890
  async function publish(opts) {
681
- const dir = path4.resolve(opts.dir);
891
+ const dir = path5.resolve(opts.dir);
682
892
  const config = readConfig(dir);
683
893
  if (opts.password !== void 0) {
684
894
  const parsed = gatePasswordSchema.safeParse(opts.password);
@@ -688,7 +898,8 @@ async function publish(opts) {
688
898
  }
689
899
  const artifactId = opts.artifact ?? config?.artifactId ?? null;
690
900
  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;
901
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
902
+ const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
692
903
  const { files, skipped } = scanDirectory(dir);
693
904
  if (files.length === 0) {
694
905
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
@@ -702,8 +913,7 @@ async function publish(opts) {
702
913
  if (skipped.length) {
703
914
  log(` ignoring ${skipped.length} item(s): ${preview(skipped)}`);
704
915
  }
705
- const client = createApiClient(apiUrl, token);
706
- const session = await client.createSession({
916
+ const sessionBody = {
707
917
  artifactId,
708
918
  entryPath,
709
919
  files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
@@ -713,7 +923,21 @@ async function publish(opts) {
713
923
  type: "cli"
714
924
  },
715
925
  title: opts.title
716
- });
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;
936
+ }
937
+ credential = token;
938
+ client = createApiClient(apiUrl2, credential);
939
+ session = await client.createSession(sessionBody);
940
+ }
717
941
  const byPath = new Map(files.map((f) => [f.path, f]));
718
942
  log(
719
943
  ` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
@@ -725,14 +949,14 @@ async function publish(opts) {
725
949
  }
726
950
  await uploadFile(
727
951
  upload.uploadUrl,
728
- readFileSync3(file.absPath),
952
+ readFileSync4(file.absPath),
729
953
  file.mimeType
730
954
  );
731
955
  });
732
956
  const result = await client.finalize(session.publishSessionId);
733
957
  if (result.publishToken) {
734
958
  writeConfig(dir, {
735
- apiUrl,
959
+ apiUrl: apiUrl2,
736
960
  artifactId: result.artifactId,
737
961
  publishToken: result.publishToken,
738
962
  slug: slugFromUrl(result.url),
@@ -740,7 +964,10 @@ async function publish(opts) {
740
964
  });
741
965
  }
742
966
  if (opts.password !== void 0) {
743
- const protectClient = createApiClient(apiUrl, token ?? result.publishToken);
967
+ const protectClient = createApiClient(
968
+ apiUrl2,
969
+ credential ?? result.publishToken
970
+ );
744
971
  await protectClient.setProtection(result.artifactId, opts.password);
745
972
  }
746
973
  if (opts.json) {
@@ -748,6 +975,7 @@ async function publish(opts) {
748
975
  JSON.stringify(
749
976
  {
750
977
  artifactId: result.artifactId,
978
+ claimed: result.claimed,
751
979
  url: result.url,
752
980
  versionNumber: result.versionNumber,
753
981
  ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
@@ -771,6 +999,8 @@ ${result.url}
771
999
  ${result.claimUrl}
772
1000
  `
773
1001
  );
1002
+ } else if (result.claimed) {
1003
+ console.log("Claimed by your account \u2014 no expiry.\n");
774
1004
  }
775
1005
  if (opts.password !== void 0) {
776
1006
  console.log("Password protection: on\n");
@@ -823,9 +1053,123 @@ function openInBrowser(url) {
823
1053
  });
824
1054
  }
825
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
+
826
1169
  // src/open.ts
1170
+ import path6 from "path";
827
1171
  function openArtifact(opts) {
828
- const dir = path5.resolve(opts.dir);
1172
+ const dir = path6.resolve(opts.dir);
829
1173
  const config = readConfig(dir);
830
1174
  if (!config) {
831
1175
  throw new Error(
@@ -839,7 +1183,7 @@ function openArtifact(opts) {
839
1183
  }
840
1184
 
841
1185
  // src/protect.ts
842
- import path6 from "path";
1186
+ import path7 from "path";
843
1187
  async function protect(opts) {
844
1188
  if (opts.off && opts.password !== void 0) {
845
1189
  throw new Error("Pass either --password or --off, not both.");
@@ -856,16 +1200,16 @@ async function protect(opts) {
856
1200
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
857
1201
  }
858
1202
  }
859
- const dir = path6.resolve(opts.dir);
1203
+ const dir = path7.resolve(opts.dir);
860
1204
  const config = readConfig(dir);
861
1205
  if (!config) {
862
1206
  throw new Error(
863
1207
  `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
864
1208
  );
865
1209
  }
866
- const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
1210
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
867
1211
  const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
868
- const client = createApiClient(apiUrl, token);
1212
+ const client = createApiClient(apiUrl2, token);
869
1213
  const res = await client.setProtection(config.artifactId, password);
870
1214
  if (opts.json) {
871
1215
  console.log(JSON.stringify({ protected: res.protected }, null, 2));
@@ -883,6 +1227,10 @@ async function protect(opts) {
883
1227
  // src/index.ts
884
1228
  var publish2 = defineCommand({
885
1229
  args: {
1230
+ "api-key": {
1231
+ description: "Publish with an account API key (default: stored login)",
1232
+ type: "string"
1233
+ },
886
1234
  artifact: {
887
1235
  description: "Publish to an existing artifact ID",
888
1236
  type: "string"
@@ -905,6 +1253,7 @@ var publish2 = defineCommand({
905
1253
  async run({ args }) {
906
1254
  try {
907
1255
  await publish({
1256
+ apiKey: args["api-key"],
908
1257
  artifact: args.artifact,
909
1258
  dir: args.dir,
910
1259
  entry: args.entry,
@@ -919,6 +1268,82 @@ var publish2 = defineCommand({
919
1268
  }
920
1269
  }
921
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
+ });
922
1347
  var comments2 = defineCommand({
923
1348
  args: {
924
1349
  dir: {
@@ -1008,6 +1433,15 @@ var main = defineCommand({
1008
1433
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1009
1434
  name: "artifact-cafe"
1010
1435
  },
1011
- subCommands: { comments: comments2, open, protect: protect2, publish: publish2 }
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
+ }
1012
1446
  });
1013
1447
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-cafe",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Publish AI-generated artifacts for review on artifact.cafe",
5
5
  "type": "module",
6
6
  "bin": {