artifact-cafe 0.0.4 → 0.0.7

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 -243
  2. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -1,10 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { defineCommand, runMain } from "citty";
4
+ import { defineCommand, runMain, showUsage } from "citty";
5
+
6
+ // src/banner.ts
7
+ var AMBER = "\x1B[38;2;214;138;57m";
8
+ var BOLD = "\x1B[1m";
9
+ var RESET = "\x1B[0m";
10
+ var MUG = [
11
+ " \u2596",
12
+ " \u2598 \u259D",
13
+ " \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
14
+ " \u2502 \u251C\u2500\u256E",
15
+ " \u2502 \u2502 \u2502",
16
+ " \u2502 \u251C\u2500\u256F",
17
+ " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F",
18
+ " \u203E\u203E\u203E\u203E\u203E\u203E\u203E\u203E"
19
+ ];
20
+ var WORDMARK = " artifact.cafe";
21
+ function printBanner() {
22
+ const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
23
+ const paint = (line, codes) => color ? `${codes}${line}${RESET}` : line;
24
+ console.log();
25
+ for (const line of MUG) {
26
+ console.log(paint(line, AMBER));
27
+ }
28
+ console.log();
29
+ console.log(paint(WORDMARK, AMBER + BOLD));
30
+ console.log();
31
+ }
5
32
 
6
33
  // src/comments.ts
7
- import path2 from "path";
34
+ import path3 from "path";
8
35
 
9
36
  // ../shared/src/account.ts
10
37
  import { z } from "zod";
@@ -103,8 +130,11 @@ var threadActionSchema = z2.object({
103
130
  guest: guestSchema.optional()
104
131
  });
105
132
  var commentAuthorSchema = z2.object({
133
+ avatarUrl: z2.string().nullable(),
106
134
  displayName: z2.string(),
107
- guestId: z2.string().nullable()
135
+ guestId: z2.string().nullable(),
136
+ isViewer: z2.boolean(),
137
+ kind: z2.enum(["member", "guest"])
108
138
  });
109
139
  var commentMessageSchema = z2.object({
110
140
  author: commentAuthorSchema,
@@ -131,12 +161,14 @@ var commentExportQuerySchema = z2.object({
131
161
  });
132
162
  var commentExportReplySchema = z2.object({
133
163
  author: z2.string(),
164
+ authorKind: z2.enum(["member", "guest"]),
134
165
  body: z2.string(),
135
166
  createdAt: z2.string()
136
167
  });
137
168
  var commentExportThreadSchema = z2.object({
138
169
  anchor: anchorSchema,
139
170
  author: z2.string(),
171
+ authorKind: z2.enum(["member", "guest"]),
140
172
  body: z2.string(),
141
173
  createdAt: z2.string(),
142
174
  id: z2.string(),
@@ -172,6 +204,15 @@ import { z as z4 } from "zod";
172
204
  var ownArtifactSchema = z4.object({
173
205
  createdAt: z4.string(),
174
206
  currentVersionNumber: z4.number().nullable(),
207
+ // Owned artifacts normally never expire; non-null only while an
208
+ // expiry is still set (e.g. an unclaimed publish surfaced here later).
209
+ expiresAt: z4.string().nullable(),
210
+ // SPEC-2 §8.4 — the containing folder's name for workspace-owned
211
+ // artifacts; always null for personal artifacts.
212
+ folderName: z4.string().nullable(),
213
+ // Whether a publish token is still active — revocable from the
214
+ // dashboard (§6.18); the token itself is never returned.
215
+ hasPublishToken: z4.boolean(),
175
216
  id: z4.string(),
176
217
  openThreads: z4.number(),
177
218
  protected: z4.boolean(),
@@ -185,64 +226,217 @@ var ownArtifactSchema = z4.object({
185
226
  var ownArtifactsResponseSchema = z4.object({
186
227
  artifacts: z4.array(ownArtifactSchema)
187
228
  });
229
+ var revokePublishTokenResponseSchema = z4.object({
230
+ revoked: z4.boolean()
231
+ });
188
232
 
189
233
  // ../shared/src/owner.ts
234
+ import { z as z6 } from "zod";
235
+
236
+ // ../shared/src/workspaces.ts
190
237
  import { z as z5 } from "zod";
191
- var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
192
- var claimResponseSchema = z5.object({
193
- artifactId: z5.string(),
238
+ var WORKSPACE_HANDLE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])$/;
239
+ var workspaceRoleSchema = z5.enum(["owner", "admin", "member"]);
240
+ var workspaceInviteRoleSchema = z5.enum(["admin", "member"]);
241
+ var workspaceHandleSchema = z5.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
242
+ message: "handle must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
243
+ });
244
+ var createWorkspaceSchema = z5.object({
245
+ handle: workspaceHandleSchema.optional(),
246
+ name: z5.string().trim().min(1).max(100)
247
+ });
248
+ var updateWorkspaceSchema = z5.object({
249
+ handle: workspaceHandleSchema.optional(),
250
+ name: z5.string().trim().min(1).max(100).optional()
251
+ }).refine((body) => body.handle !== void 0 || body.name !== void 0, {
252
+ message: "provide at least one of name or handle"
253
+ });
254
+ var workspaceSchema = z5.object({
255
+ createdAt: z5.string(),
256
+ handle: z5.string(),
257
+ id: z5.string(),
258
+ name: z5.string(),
259
+ role: workspaceRoleSchema.nullable(),
260
+ updatedAt: z5.string()
261
+ });
262
+ var workspaceListResponseSchema = z5.object({
263
+ workspaces: z5.array(workspaceSchema)
264
+ });
265
+ var deleteWorkspaceResponseSchema = z5.object({
266
+ deleted: z5.literal(true)
267
+ });
268
+ var workspaceMemberSchema = z5.object({
269
+ createdAt: z5.string(),
270
+ email: z5.string().email(),
271
+ image: z5.string().nullable(),
272
+ name: z5.string(),
273
+ role: workspaceRoleSchema,
274
+ userId: z5.string()
275
+ });
276
+ var workspaceMemberListResponseSchema = z5.object({
277
+ members: z5.array(workspaceMemberSchema)
278
+ });
279
+ var updateWorkspaceMemberSchema = z5.object({
280
+ role: workspaceRoleSchema
281
+ });
282
+ var removeWorkspaceMemberResponseSchema = z5.object({
283
+ removed: z5.literal(true)
284
+ });
285
+ var createWorkspaceInviteSchema = z5.object({
286
+ email: z5.string().trim().toLowerCase().email().optional(),
287
+ expiresAt: z5.string().datetime(),
288
+ maxUses: z5.number().int().positive().optional(),
289
+ role: workspaceInviteRoleSchema
290
+ });
291
+ var workspaceInviteSchema = z5.object({
292
+ createdAt: z5.string(),
293
+ email: z5.string().email().nullable(),
294
+ expiresAt: z5.string(),
295
+ id: z5.string(),
296
+ maxUses: z5.number().int().positive().nullable(),
297
+ role: workspaceInviteRoleSchema,
298
+ tokenPrefix: z5.string(),
299
+ useCount: z5.number().int().nonnegative()
300
+ });
301
+ var createdWorkspaceInviteSchema = workspaceInviteSchema.extend({
302
+ url: z5.string().url()
303
+ });
304
+ var workspaceInviteListResponseSchema = z5.object({
305
+ invites: z5.array(workspaceInviteSchema)
306
+ });
307
+ var revokeWorkspaceInviteResponseSchema = z5.object({
308
+ revoked: z5.literal(true)
309
+ });
310
+ var acceptWorkspaceInviteSchema = z5.object({
311
+ token: z5.string().min(1)
312
+ });
313
+ var acceptWorkspaceInviteResponseSchema = z5.object({
314
+ role: workspaceRoleSchema,
315
+ workspace: workspaceSchema
316
+ });
317
+ var folderVisibilitySchema = z5.enum(["public", "workspace"]);
318
+ var artifactVisibilitySchema = z5.enum([
319
+ "public",
320
+ "unlisted",
321
+ "workspace"
322
+ ]);
323
+ var createFolderSchema = z5.object({
324
+ name: z5.string().trim().min(1).max(100),
325
+ visibility: folderVisibilitySchema
326
+ });
327
+ var updateFolderSchema = z5.object({
328
+ // With `visibility`: bulk-update the folder's artifacts to match
329
+ // (SPEC-2 §3.4 "Changing a folder's visibility offers a bulk update").
330
+ cascade: z5.boolean().optional(),
331
+ name: z5.string().trim().min(1).max(100).optional(),
332
+ sortOrder: z5.number().int().min(0).optional(),
333
+ visibility: folderVisibilitySchema.optional()
334
+ }).refine(
335
+ (body) => body.name !== void 0 || body.visibility !== void 0 || body.sortOrder !== void 0,
336
+ { message: "provide at least one of name, visibility, sortOrder" }
337
+ ).refine((body) => !body.cascade || body.visibility !== void 0, {
338
+ message: "cascade requires visibility"
339
+ });
340
+ var folderSchema = z5.object({
341
+ createdAt: z5.string(),
342
+ id: z5.string(),
343
+ name: z5.string(),
344
+ // Permalink segment for the public folder page (/w/{handle}/f/{slug}).
345
+ // Server-derived from the name; renames regenerate it.
346
+ slug: z5.string(),
347
+ sortOrder: z5.number().int(),
348
+ updatedAt: z5.string(),
349
+ visibility: folderVisibilitySchema,
350
+ workspaceId: z5.string()
351
+ });
352
+ var folderListResponseSchema = z5.object({
353
+ folders: z5.array(folderSchema)
354
+ });
355
+ var deleteFolderResponseSchema = z5.object({
356
+ deleted: z5.literal(true)
357
+ });
358
+ var workspaceArtifactSchema = z5.object({
359
+ createdAt: z5.string(),
360
+ currentVersionNumber: z5.number().int().nullable(),
361
+ folderId: z5.string().nullable(),
362
+ id: z5.string(),
363
+ openCommentCount: z5.number().int().nonnegative(),
194
364
  slug: z5.string(),
195
365
  title: z5.string(),
196
- url: z5.string().url()
366
+ updatedAt: z5.string(),
367
+ url: z5.string().url(),
368
+ versionCount: z5.number().int().nonnegative(),
369
+ visibility: artifactVisibilitySchema
370
+ });
371
+ var workspaceArtifactListResponseSchema = z5.object({
372
+ artifacts: z5.array(workspaceArtifactSchema)
373
+ });
374
+
375
+ // ../shared/src/owner.ts
376
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
377
+ var claimResponseSchema = z6.object({
378
+ artifactId: z6.string(),
379
+ slug: z6.string(),
380
+ title: z6.string(),
381
+ url: z6.string().url()
197
382
  });
198
- var updateArtifactSchema = z5.object({
199
- slug: z5.string().min(3).max(63).regex(SLUG_PATTERN, {
383
+ var updateArtifactSchema = z6.object({
384
+ // SPEC-2 §6.4 — placement + visibility. `folderId: null` clears the
385
+ // folder; `workspaceId: null` moves the artifact out of its workspace
386
+ // (§5.5 ownership transfer).
387
+ folderId: z6.string().min(1).nullable().optional(),
388
+ slug: z6.string().min(3).max(63).regex(SLUG_PATTERN, {
200
389
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
201
390
  }).optional(),
202
- title: z5.string().min(1).max(200).optional()
203
- }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
204
- message: "provide at least one of title, slug"
205
- });
206
- var deleteArtifactResponseSchema = z5.object({
207
- deleted: z5.literal(true)
391
+ title: z6.string().min(1).max(200).optional(),
392
+ visibility: artifactVisibilitySchema.optional(),
393
+ workspaceId: z6.string().min(1).nullable().optional()
394
+ }).refine(
395
+ (body) => body.title !== void 0 || body.slug !== void 0 || body.visibility !== void 0 || body.folderId !== void 0 || body.workspaceId !== void 0,
396
+ {
397
+ message: "provide at least one of title, slug, visibility, folderId, workspaceId"
398
+ }
399
+ );
400
+ var deleteArtifactResponseSchema = z6.object({
401
+ deleted: z6.literal(true)
208
402
  });
209
403
 
210
404
  // ../shared/src/presence.ts
211
- import { z as z6 } from "zod";
405
+ import { z as z7 } from "zod";
212
406
  var AGENT_PRESENCE = {
213
407
  heartbeatSeconds: 15,
214
408
  shellPollSeconds: 15,
215
409
  ttlSeconds: 45
216
410
  };
217
- var agentHeartbeatResponseSchema = z6.object({
218
- listening: z6.literal(true),
219
- ttlSeconds: z6.number().int().positive()
411
+ var agentHeartbeatResponseSchema = z7.object({
412
+ listening: z7.literal(true),
413
+ ttlSeconds: z7.number().int().positive()
220
414
  });
221
- var agentPresenceSchema = z6.object({
222
- listening: z6.boolean(),
223
- versionNumber: z6.number().int().positive().nullable()
415
+ var agentPresenceSchema = z7.object({
416
+ listening: z7.boolean(),
417
+ versionNumber: z7.number().int().positive().nullable()
224
418
  });
225
419
 
226
420
  // ../shared/src/protection.ts
227
- import { z as z7 } from "zod";
421
+ import { z as z8 } from "zod";
228
422
  var PASSWORD_MIN = 6;
229
423
  var PASSWORD_MAX = 128;
230
- var gatePasswordSchema = z7.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
231
- var setProtectionSchema = z7.object({
424
+ var gatePasswordSchema = z8.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
425
+ var setProtectionSchema = z8.object({
232
426
  password: gatePasswordSchema.nullable()
233
427
  });
234
- var protectionResponseSchema = z7.object({
235
- protected: z7.boolean()
428
+ var protectionResponseSchema = z8.object({
429
+ protected: z8.boolean()
236
430
  });
237
- var unlockSchema = z7.object({
238
- password: z7.string().min(1).max(1024)
431
+ var unlockSchema = z8.object({
432
+ password: z8.string().min(1).max(1024)
239
433
  });
240
- var unlockResponseSchema = z7.object({
241
- unlocked: z7.literal(true)
434
+ var unlockResponseSchema = z8.object({
435
+ unlocked: z8.literal(true)
242
436
  });
243
437
 
244
438
  // ../shared/src/publish.ts
245
- import { z as z8 } from "zod";
439
+ import { z as z9 } from "zod";
246
440
  function isSafeArtifactPath(p) {
247
441
  if (p.length === 0 || p.length > 1024) {
248
442
  return false;
@@ -255,128 +449,136 @@ function isSafeArtifactPath(p) {
255
449
  }
256
450
  return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
257
451
  }
258
- var manifestFileSchema = z8.object({
259
- hash: z8.string().regex(/^sha256_[a-f0-9]{64}$/),
260
- mimeType: z8.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
261
- path: z8.string().min(1).max(1024).refine(isSafeArtifactPath, {
452
+ var manifestFileSchema = z9.object({
453
+ hash: z9.string().regex(/^sha256_[a-f0-9]{64}$/),
454
+ mimeType: z9.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
455
+ path: z9.string().min(1).max(1024).refine(isSafeArtifactPath, {
262
456
  message: "path must be a safe relative path"
263
457
  }),
264
- size: z8.number().int().positive()
458
+ size: z9.number().int().positive()
265
459
  });
266
- var publishSourceSchema = z8.object({
267
- agent: z8.string().optional(),
268
- runId: z8.string().optional(),
269
- type: z8.literal("cli")
460
+ var publishSourceSchema = z9.object({
461
+ agent: z9.string().optional(),
462
+ runId: z9.string().optional(),
463
+ type: z9.literal("cli")
270
464
  });
271
- var createPublishSessionSchema = z8.object({
272
- artifactId: z8.string().nullable(),
273
- entryPath: z8.string().optional(),
274
- files: z8.array(manifestFileSchema).min(1).max(1e3),
465
+ var createPublishSessionSchema = z9.object({
466
+ artifactId: z9.string().nullable(),
467
+ entryPath: z9.string().optional(),
468
+ files: z9.array(manifestFileSchema).min(1).max(1e3),
469
+ // SPEC-2 §6.4 — workspace placement is available only to authenticated
470
+ // members; the API enforces membership and folder ownership.
471
+ folderId: z9.string().min(1).optional(),
275
472
  source: publishSourceSchema,
276
- title: z8.string().max(200).optional()
277
- });
278
- var publishSessionResponseSchema = z8.object({
279
- artifactId: z8.string(),
280
- expiresAt: z8.string(),
281
- filesToUpload: z8.array(
282
- z8.object({
283
- method: z8.literal("PUT"),
284
- path: z8.string(),
285
- uploadUrl: z8.string().url()
473
+ title: z9.string().max(200).optional(),
474
+ workspaceId: z9.string().min(1).optional()
475
+ });
476
+ var publishSessionResponseSchema = z9.object({
477
+ artifactId: z9.string(),
478
+ expiresAt: z9.string(),
479
+ filesToUpload: z9.array(
480
+ z9.object({
481
+ method: z9.literal("PUT"),
482
+ path: z9.string(),
483
+ uploadUrl: z9.string().url()
286
484
  })
287
485
  ),
288
- publishSessionId: z8.string(),
289
- skippedFiles: z8.array(z8.string()),
290
- versionId: z8.string()
486
+ publishSessionId: z9.string(),
487
+ skippedFiles: z9.array(z9.string()),
488
+ versionId: z9.string()
291
489
  });
292
- var finalizeResponseSchema = z8.object({
293
- artifactId: z8.string(),
490
+ var finalizeResponseSchema = z9.object({
491
+ artifactId: z9.string(),
294
492
  // API-key publishes create the artifact already claimed (SPEC §5.4) —
295
493
  // the CLI uses this to tell "claim URL pending" from "already yours".
296
- claimed: z8.boolean(),
494
+ claimed: z9.boolean(),
297
495
  // Returned only on first publish of a new artifact (one-time).
298
- claimUrl: z8.string().url().optional(),
299
- publishToken: z8.string().optional(),
300
- url: z8.string().url(),
301
- versionId: z8.string(),
302
- versionNumber: z8.number().int().positive()
303
- });
304
- var artifactResponseSchema = z8.object({
305
- claimed: z8.boolean(),
306
- currentVersion: z8.object({
307
- entryUrl: z8.string().url(),
308
- id: z8.string(),
309
- versionNumber: z8.number().int().positive()
496
+ claimUrl: z9.string().url().optional(),
497
+ publishToken: z9.string().optional(),
498
+ url: z9.string().url(),
499
+ versionId: z9.string(),
500
+ versionNumber: z9.number().int().positive()
501
+ });
502
+ var artifactResponseSchema = z9.object({
503
+ claimed: z9.boolean(),
504
+ currentVersion: z9.object({
505
+ entryUrl: z9.string().url(),
506
+ id: z9.string(),
507
+ versionNumber: z9.number().int().positive()
310
508
  }).nullable(),
311
- id: z8.string(),
312
- slug: z8.string(),
313
- title: z8.string()
509
+ // SPEC-2 §6.4 — placement + visibility surface on the artifact.
510
+ folderId: z9.string().nullable(),
511
+ id: z9.string(),
512
+ slug: z9.string(),
513
+ title: z9.string(),
514
+ visibility: z9.enum(["public", "unlisted", "workspace"]),
515
+ workspaceId: z9.string().nullable()
314
516
  });
315
517
 
316
518
  // ../shared/src/report.ts
317
- import { z as z9 } from "zod";
318
- var reportRequestSchema = z9.object({
319
- reason: z9.string().max(1e3).optional()
519
+ import { z as z10 } from "zod";
520
+ var reportRequestSchema = z10.object({
521
+ reason: z10.string().max(1e3).optional()
320
522
  });
321
- var reportResponseSchema = z9.object({
322
- reported: z9.literal(true)
523
+ var reportResponseSchema = z10.object({
524
+ reported: z10.literal(true)
323
525
  });
324
526
 
325
527
  // ../shared/src/stats.ts
326
- import { z as z10 } from "zod";
327
- var avgViewSecondsSchema = z10.number().int().nonnegative().nullable();
328
- var versionViewStatsSchema = z10.object({
528
+ import { z as z11 } from "zod";
529
+ var avgViewSecondsSchema = z11.number().int().nonnegative().nullable();
530
+ var versionViewStatsSchema = z11.object({
329
531
  avgViewSeconds: avgViewSecondsSchema,
330
532
  // Views from inside the review shell's iframe vs. direct visits to
331
533
  // the render URL. views = embeddedViews + directViews.
332
- directViews: z10.number().int().nonnegative(),
333
- embeddedViews: z10.number().int().nonnegative(),
534
+ directViews: z11.number().int().nonnegative(),
535
+ embeddedViews: z11.number().int().nonnegative(),
334
536
  // Distinct hashed viewer IPs across the version's lifetime.
335
- uniqueViewers: z10.number().int().nonnegative(),
336
- versionNumber: z10.number().int().positive(),
337
- views: z10.number().int().nonnegative()
537
+ uniqueViewers: z11.number().int().nonnegative(),
538
+ versionNumber: z11.number().int().positive(),
539
+ views: z11.number().int().nonnegative()
338
540
  });
339
- var dailyViewsSchema = z10.object({
541
+ var dailyViewsSchema = z11.object({
340
542
  avgViewSeconds: avgViewSecondsSchema,
341
543
  // UTC day, YYYY-MM-DD.
342
- day: z10.string(),
343
- uniqueViewers: z10.number().int().nonnegative(),
344
- views: z10.number().int().nonnegative()
544
+ day: z11.string(),
545
+ uniqueViewers: z11.number().int().nonnegative(),
546
+ views: z11.number().int().nonnegative()
345
547
  });
346
- var artifactStatsResponseSchema = z10.object({
548
+ var artifactStatsResponseSchema = z11.object({
347
549
  avgViewSeconds: avgViewSecondsSchema,
348
- daily: z10.array(dailyViewsSchema),
550
+ daily: z11.array(dailyViewsSchema),
349
551
  // Distinct hashed viewer IPs across all versions and days.
350
- totalUniqueViewers: z10.number().int().nonnegative(),
351
- totalViews: z10.number().int().nonnegative(),
352
- versions: z10.array(versionViewStatsSchema)
552
+ totalUniqueViewers: z11.number().int().nonnegative(),
553
+ totalViews: z11.number().int().nonnegative(),
554
+ versions: z11.array(versionViewStatsSchema)
353
555
  });
354
556
 
355
557
  // ../shared/src/versions.ts
356
- import { z as z11 } from "zod";
357
- var versionSourceSchema = z11.object({
358
- agentName: z11.string().nullable(),
359
- type: z11.string()
558
+ import { z as z12 } from "zod";
559
+ var versionSourceSchema = z12.object({
560
+ agentName: z12.string().nullable(),
561
+ type: z12.string()
360
562
  });
361
- var versionChangesSchema = z11.object({
362
- added: z11.array(z11.string()),
363
- changed: z11.array(z11.string()),
364
- removed: z11.array(z11.string())
563
+ var versionChangesSchema = z12.object({
564
+ added: z12.array(z12.string()),
565
+ changed: z12.array(z12.string()),
566
+ removed: z12.array(z12.string())
365
567
  });
366
- var versionSummarySchema = z11.object({
568
+ var versionSummarySchema = z12.object({
367
569
  // Absent for the first version — there is no previous version to diff against.
368
570
  changes: versionChangesSchema.nullable(),
369
- createdAt: z11.string(),
370
- entryUrl: z11.string().url(),
371
- fileCount: z11.number().int().nonnegative(),
372
- id: z11.string(),
373
- isCurrent: z11.boolean(),
571
+ createdAt: z12.string(),
572
+ entryUrl: z12.string().url(),
573
+ fileCount: z12.number().int().nonnegative(),
574
+ id: z12.string(),
575
+ isCurrent: z12.boolean(),
374
576
  source: versionSourceSchema,
375
- totalSizeBytes: z11.number().int().nonnegative(),
376
- versionNumber: z11.number().int().positive()
577
+ totalSizeBytes: z12.number().int().nonnegative(),
578
+ versionNumber: z12.number().int().positive()
377
579
  });
378
- var versionListResponseSchema = z11.object({
379
- versions: z11.array(versionSummarySchema)
580
+ var versionListResponseSchema = z12.object({
581
+ versions: z12.array(versionSummarySchema)
380
582
  });
381
583
 
382
584
  // src/api.ts
@@ -480,6 +682,20 @@ function createApiClient(apiUrl2, publishToken) {
480
682
  (d) => artifactListResponseSchema.parse(d)
481
683
  );
482
684
  },
685
+ async listFolders(workspaceId) {
686
+ return request(
687
+ `${base}/api/v1/workspaces/${workspaceId}/folders`,
688
+ { headers, method: "GET" },
689
+ (d) => folderListResponseSchema.parse(d)
690
+ );
691
+ },
692
+ async listWorkspaces() {
693
+ return request(
694
+ `${base}/api/v1/workspaces`,
695
+ { headers, method: "GET" },
696
+ (d) => workspaceListResponseSchema.parse(d)
697
+ );
698
+ },
483
699
  // SPEC §6.14 — who the presented credential belongs to. Used by
484
700
  // `login` to verify a pasted key and by `whoami`.
485
701
  async me() {
@@ -519,37 +735,103 @@ async function uploadFile(uploadUrl, content, mimeType) {
519
735
  // src/config.ts
520
736
  import {
521
737
  appendFileSync,
522
- existsSync,
523
- mkdirSync,
524
- readFileSync,
525
- writeFileSync
738
+ existsSync as existsSync2,
739
+ mkdirSync as mkdirSync2,
740
+ readFileSync as readFileSync2,
741
+ writeFileSync as writeFileSync2
526
742
  } from "fs";
743
+ import path2 from "path";
744
+ import { z as z14 } from "zod";
745
+
746
+ // src/credentials.ts
747
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
748
+ import { homedir } from "os";
527
749
  import path from "path";
528
- import { z as z12 } from "zod";
529
- var configSchema = z12.object({
530
- apiUrl: z12.string(),
531
- artifactId: z12.string(),
532
- publishToken: z12.string(),
533
- slug: z12.string(),
534
- url: z12.string()
750
+ import { z as z13 } from "zod";
751
+ var DEFAULT_API_URL = "https://artifact.cafe";
752
+ var credentialsSchema = z13.object({
753
+ keys: z13.record(z13.string(), z13.string()),
754
+ version: z13.literal(1)
755
+ });
756
+ function credentialsPath() {
757
+ return path.join(homedir(), ".artifactcafe", "credentials");
758
+ }
759
+ function apiOrigin(apiUrl2) {
760
+ return new URL(apiUrl2).origin;
761
+ }
762
+ function readCredentials() {
763
+ const file = credentialsPath();
764
+ if (!existsSync(file)) {
765
+ return { keys: {}, version: 1 };
766
+ }
767
+ try {
768
+ return credentialsSchema.parse(JSON.parse(readFileSync(file, "utf8")));
769
+ } catch {
770
+ return { keys: {}, version: 1 };
771
+ }
772
+ }
773
+ function writeCredentials(credentials) {
774
+ const file = credentialsPath();
775
+ mkdirSync(path.dirname(file), { mode: 448, recursive: true });
776
+ writeFileSync(file, `${JSON.stringify(credentials, null, 2)}
777
+ `, {
778
+ mode: 384
779
+ });
780
+ }
781
+ function storedApiKey(apiUrl2) {
782
+ return readCredentials().keys[apiOrigin(apiUrl2)] ?? null;
783
+ }
784
+ function storeApiKey(apiUrl2, key) {
785
+ const credentials = readCredentials();
786
+ credentials.keys[apiOrigin(apiUrl2)] = key;
787
+ writeCredentials(credentials);
788
+ }
789
+ function removeApiKey(apiUrl2) {
790
+ const credentials = readCredentials();
791
+ const origin = apiOrigin(apiUrl2);
792
+ if (!(origin in credentials.keys)) {
793
+ return false;
794
+ }
795
+ delete credentials.keys[origin];
796
+ writeCredentials(credentials);
797
+ return true;
798
+ }
799
+ function resolveApiKey(apiUrl2, flag) {
800
+ return flag ?? process.env.ARTIFACT_CAFE_API_KEY ?? storedApiKey(apiUrl2) ?? void 0;
801
+ }
802
+
803
+ // src/config.ts
804
+ var configSchema = z14.object({
805
+ apiUrl: z14.string(),
806
+ artifactId: z14.string(),
807
+ folder: z14.string().optional(),
808
+ folderId: z14.string().optional(),
809
+ // Optional: a folder linked by `link` to an artifact you own carries no
810
+ // token — owner actions authorize via your stored login instead. A first
811
+ // publish still writes the anonymous-mode token here.
812
+ publishToken: z14.string().optional(),
813
+ slug: z14.string(),
814
+ url: z14.string(),
815
+ workspace: z14.string().optional(),
816
+ workspaceId: z14.string().optional()
535
817
  });
536
818
  var CONFIG_DIR = ".artifactcafe";
537
819
  var CONFIG_FILE = "config.json";
538
820
  function readConfig(dir) {
539
- const file = path.join(dir, CONFIG_DIR, CONFIG_FILE);
540
- if (!existsSync(file)) {
821
+ const file = path2.join(dir, CONFIG_DIR, CONFIG_FILE);
822
+ if (!existsSync2(file)) {
541
823
  return null;
542
824
  }
543
825
  try {
544
- return configSchema.parse(JSON.parse(readFileSync(file, "utf8")));
826
+ return configSchema.parse(JSON.parse(readFileSync2(file, "utf8")));
545
827
  } catch {
546
828
  return null;
547
829
  }
548
830
  }
549
831
  function writeConfig(dir, config) {
550
- mkdirSync(path.join(dir, CONFIG_DIR), { recursive: true });
551
- writeFileSync(
552
- path.join(dir, CONFIG_DIR, CONFIG_FILE),
832
+ mkdirSync2(path2.join(dir, CONFIG_DIR), { recursive: true });
833
+ writeFileSync2(
834
+ path2.join(dir, CONFIG_DIR, CONFIG_FILE),
553
835
  `${JSON.stringify(config, null, 2)}
554
836
  `,
555
837
  { mode: 384 }
@@ -557,11 +839,11 @@ function writeConfig(dir, config) {
557
839
  ensureGitignored(dir);
558
840
  }
559
841
  function ensureGitignored(dir) {
560
- const gitignore = path.join(dir, ".gitignore");
561
- if (!existsSync(gitignore)) {
842
+ const gitignore = path2.join(dir, ".gitignore");
843
+ if (!existsSync2(gitignore)) {
562
844
  return;
563
845
  }
564
- const lines = readFileSync(gitignore, "utf8").split(/\r?\n/);
846
+ const lines = readFileSync2(gitignore, "utf8").split(/\r?\n/);
565
847
  const present = lines.some((l) => {
566
848
  const t = l.trim();
567
849
  return t === CONFIG_DIR || t === `${CONFIG_DIR}/` || t === `/${CONFIG_DIR}` || t === `/${CONFIG_DIR}/`;
@@ -576,6 +858,23 @@ ${CONFIG_DIR}/
576
858
  );
577
859
  }
578
860
  }
861
+ function reviewUrl(apiUrl2, slug) {
862
+ return `${apiUrl2.replace(/\/$/, "")}/a/${slug}`;
863
+ }
864
+ function resolveArtifactTarget(dir, opts = {}) {
865
+ const config = readConfig(dir);
866
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
867
+ const artifactId = opts.artifact ?? process.env.ARTIFACT_CAFE_ARTIFACT_ID ?? config?.artifactId ?? null;
868
+ if (!artifactId) {
869
+ throw new Error(
870
+ `No artifact linked in ${dir}. Run \`artifact-cafe publish\` here, \`artifact-cafe link\` to bind one you own, or pass --artifact <id> (or set ARTIFACT_CAFE_ARTIFACT_ID) to act on an artifact linked elsewhere.`
871
+ );
872
+ }
873
+ const folderToken = artifactId === config?.artifactId ? config.publishToken : void 0;
874
+ const credential = folderToken ?? process.env.ARTIFACT_CAFE_TOKEN ?? resolveApiKey(apiUrl2, opts.apiKey);
875
+ const url = artifactId === config?.artifactId ? config.url : void 0;
876
+ return { apiUrl: apiUrl2, artifactId, credential, url };
877
+ }
579
878
 
580
879
  // src/comments.ts
581
880
  var STATUSES = ["open", "resolved", "all"];
@@ -596,17 +895,16 @@ async function comments(opts) {
596
895
  }
597
896
  const status = opts.status;
598
897
  const version = opts.version;
599
- const dir = path2.resolve(opts.dir);
600
- const config = readConfig(dir);
601
- if (!config) {
602
- throw new Error(
603
- `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
604
- );
605
- }
606
- const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
607
- const client = createApiClient(apiUrl2, config.publishToken);
898
+ const dir = path3.resolve(opts.dir);
899
+ const target = resolveArtifactTarget(dir, {
900
+ apiKey: opts.apiKey,
901
+ artifact: opts.artifact
902
+ });
903
+ const client = createApiClient(target.apiUrl, target.credential);
904
+ const url = target.url ?? (opts.json ? "" : await deriveReviewUrl(client, target));
905
+ const ref = { artifactId: target.artifactId, url };
608
906
  if (opts.wait) {
609
- await waitForFeedback(client, config, {
907
+ await waitForFeedback(client, ref, {
610
908
  json: opts.json,
611
909
  status,
612
910
  timeoutSeconds: opts.timeout,
@@ -614,7 +912,7 @@ async function comments(opts) {
614
912
  });
615
913
  return;
616
914
  }
617
- const data = await client.exportComments(config.artifactId, {
915
+ const data = await client.exportComments(target.artifactId, {
618
916
  status,
619
917
  version
620
918
  });
@@ -622,7 +920,11 @@ async function comments(opts) {
622
920
  console.log(JSON.stringify(data, null, 2));
623
921
  return;
624
922
  }
625
- printHuman(data, config.url, status, version);
923
+ printHuman(data, url, status, version);
924
+ }
925
+ async function deriveReviewUrl(client, target) {
926
+ const artifact = await client.getArtifact(target.artifactId);
927
+ return reviewUrl(target.apiUrl, artifact.slug);
626
928
  }
627
929
  var sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
628
930
  function messageCounts(data) {
@@ -777,63 +1079,6 @@ function indent(text, pad) {
777
1079
  // src/dashboard.ts
778
1080
  import path6 from "path";
779
1081
 
780
- // src/credentials.ts
781
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
782
- import { homedir } from "os";
783
- import path3 from "path";
784
- import { z as z13 } from "zod";
785
- var DEFAULT_API_URL = "https://artifact.cafe";
786
- var credentialsSchema = z13.object({
787
- keys: z13.record(z13.string(), z13.string()),
788
- version: z13.literal(1)
789
- });
790
- function credentialsPath() {
791
- return path3.join(homedir(), ".artifactcafe", "credentials");
792
- }
793
- function apiOrigin(apiUrl2) {
794
- return new URL(apiUrl2).origin;
795
- }
796
- function readCredentials() {
797
- const file = credentialsPath();
798
- if (!existsSync2(file)) {
799
- return { keys: {}, version: 1 };
800
- }
801
- try {
802
- return credentialsSchema.parse(JSON.parse(readFileSync2(file, "utf8")));
803
- } catch {
804
- return { keys: {}, version: 1 };
805
- }
806
- }
807
- function writeCredentials(credentials) {
808
- const file = credentialsPath();
809
- mkdirSync2(path3.dirname(file), { mode: 448, recursive: true });
810
- writeFileSync2(file, `${JSON.stringify(credentials, null, 2)}
811
- `, {
812
- mode: 384
813
- });
814
- }
815
- function storedApiKey(apiUrl2) {
816
- return readCredentials().keys[apiOrigin(apiUrl2)] ?? null;
817
- }
818
- function storeApiKey(apiUrl2, key) {
819
- const credentials = readCredentials();
820
- credentials.keys[apiOrigin(apiUrl2)] = key;
821
- writeCredentials(credentials);
822
- }
823
- function removeApiKey(apiUrl2) {
824
- const credentials = readCredentials();
825
- const origin = apiOrigin(apiUrl2);
826
- if (!(origin in credentials.keys)) {
827
- return false;
828
- }
829
- delete credentials.keys[origin];
830
- writeCredentials(credentials);
831
- return true;
832
- }
833
- function resolveApiKey(apiUrl2, flag) {
834
- return flag ?? process.env.ARTIFACT_CAFE_API_KEY ?? storedApiKey(apiUrl2) ?? void 0;
835
- }
836
-
837
1082
  // src/publish.ts
838
1083
  import { exec } from "child_process";
839
1084
  import { readFileSync as readFileSync4 } from "fs";
@@ -977,6 +1222,7 @@ async function publish(opts) {
977
1222
  const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
978
1223
  const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
979
1224
  const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
1225
+ const placement = await resolvePlacement({ apiKey, apiUrl: apiUrl2, config, opts });
980
1226
  const { files, skipped } = scanDirectory(dir);
981
1227
  if (files.length === 0) {
982
1228
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
@@ -994,12 +1240,14 @@ async function publish(opts) {
994
1240
  artifactId,
995
1241
  entryPath,
996
1242
  files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
1243
+ ...placement.folderId ? { folderId: placement.folderId } : {},
997
1244
  source: {
998
1245
  agent: process.env.ARTIFACT_CAFE_AGENT,
999
1246
  runId: process.env.ARTIFACT_CAFE_RUN_ID,
1000
1247
  type: "cli"
1001
1248
  },
1002
- title: opts.title
1249
+ title: opts.title,
1250
+ ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1003
1251
  };
1004
1252
  let credential = apiKey ?? token;
1005
1253
  let client = createApiClient(apiUrl2, credential);
@@ -1031,13 +1279,19 @@ async function publish(opts) {
1031
1279
  );
1032
1280
  });
1033
1281
  const result = await client.finalize(session.publishSessionId);
1034
- if (result.publishToken) {
1282
+ const existingToken = config?.artifactId === result.artifactId ? config.publishToken : void 0;
1283
+ const shouldWriteConfig = result.publishToken !== void 0 || config?.artifactId === result.artifactId || opts.workspace !== void 0 || opts.folder !== void 0;
1284
+ if (shouldWriteConfig) {
1035
1285
  writeConfig(dir, {
1036
1286
  apiUrl: apiUrl2,
1037
1287
  artifactId: result.artifactId,
1038
- publishToken: result.publishToken,
1288
+ ...placement.folder ? { folder: placement.folder } : {},
1289
+ ...placement.folderId ? { folderId: placement.folderId } : {},
1290
+ ...result.publishToken ?? existingToken ? { publishToken: result.publishToken ?? existingToken } : {},
1039
1291
  slug: slugFromUrl(result.url),
1040
- url: result.url
1292
+ url: result.url,
1293
+ ...placement.workspace ? { workspace: placement.workspace } : {},
1294
+ ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1041
1295
  });
1042
1296
  }
1043
1297
  if (opts.password !== void 0) {
@@ -1053,8 +1307,12 @@ async function publish(opts) {
1053
1307
  {
1054
1308
  artifactId: result.artifactId,
1055
1309
  claimed: result.claimed,
1310
+ folder: placement.folder ?? null,
1311
+ folderId: placement.folderId ?? null,
1056
1312
  url: result.url,
1057
1313
  versionNumber: result.versionNumber,
1314
+ workspace: placement.workspace ?? null,
1315
+ workspaceId: placement.workspaceId ?? null,
1058
1316
  ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
1059
1317
  ...opts.password === void 0 ? {} : { protected: true },
1060
1318
  filesUploaded: session.filesToUpload.map((f) => f.path),
@@ -1096,6 +1354,76 @@ ${result.claimUrl}
1096
1354
  openInBrowser(result.url);
1097
1355
  }
1098
1356
  }
1357
+ async function resolvePlacement(params) {
1358
+ const { apiKey, apiUrl: apiUrl2, config, opts } = params;
1359
+ const hasPlacementFlag = opts.workspace !== void 0 || opts.folder !== void 0;
1360
+ if (!hasPlacementFlag && config?.workspaceId) {
1361
+ if (!apiKey) {
1362
+ throw new Error(
1363
+ `Publishing into workspace ${config.workspace ?? config.workspaceId} needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1364
+ );
1365
+ }
1366
+ return {
1367
+ ...config.folder ? { folder: config.folder } : {},
1368
+ ...config.folderId ? { folderId: config.folderId } : {},
1369
+ ...config.workspace ? { workspace: config.workspace } : {},
1370
+ workspaceId: config.workspaceId
1371
+ };
1372
+ }
1373
+ if (!hasPlacementFlag) {
1374
+ return {};
1375
+ }
1376
+ if (!apiKey) {
1377
+ throw new Error(
1378
+ "Publishing into a workspace needs an account \u2014 run `artifact-cafe login` first."
1379
+ );
1380
+ }
1381
+ const workspaceHandle = opts.workspace ?? config?.workspace;
1382
+ if (!workspaceHandle) {
1383
+ throw new Error("--folder requires --workspace or a stored workspace.");
1384
+ }
1385
+ const client = createApiClient(apiUrl2, apiKey);
1386
+ let workspaceId;
1387
+ if (opts.workspace === void 0 && config?.workspaceId) {
1388
+ ({ workspaceId } = config);
1389
+ } else {
1390
+ const data2 = await client.listWorkspaces();
1391
+ const workspace = data2.workspaces.find(
1392
+ (candidate) => candidate.handle === workspaceHandle
1393
+ );
1394
+ if (!workspace) {
1395
+ throw new Error(
1396
+ `No workspace with handle "${workspaceHandle}" belongs to this account.`
1397
+ );
1398
+ }
1399
+ workspaceId = workspace.id;
1400
+ }
1401
+ const folderName = opts.workspace !== void 0 && opts.folder === void 0 ? void 0 : opts.folder ?? config?.folder;
1402
+ if (!folderName) {
1403
+ return { workspace: workspaceHandle, workspaceId };
1404
+ }
1405
+ const data = await client.listFolders(workspaceId);
1406
+ const matches = data.folders.filter(
1407
+ (candidate) => candidate.name === folderName || candidate.id === folderName
1408
+ );
1409
+ if (matches.length === 0) {
1410
+ throw new Error(
1411
+ `No folder named "${folderName}" exists in workspace ${workspaceHandle}.`
1412
+ );
1413
+ }
1414
+ if (matches.length > 1) {
1415
+ throw new Error(
1416
+ `More than one folder is named "${folderName}" in workspace ${workspaceHandle}; pass the folder id instead.`
1417
+ );
1418
+ }
1419
+ const folder = matches[0];
1420
+ return {
1421
+ folder: folder.name,
1422
+ folderId: folder.id,
1423
+ workspace: workspaceHandle,
1424
+ workspaceId
1425
+ };
1426
+ }
1099
1427
  async function inBatches(items, concurrency, fn) {
1100
1428
  let next = 0;
1101
1429
  const workers = Array.from(
@@ -1169,6 +1497,148 @@ function printCliError(err, json) {
1169
1497
  }
1170
1498
  }
1171
1499
 
1500
+ // src/folders.ts
1501
+ async function folders(opts) {
1502
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1503
+ /\/$/,
1504
+ ""
1505
+ );
1506
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1507
+ if (!key) {
1508
+ throw new Error(
1509
+ `Listing folders needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1510
+ );
1511
+ }
1512
+ const client = createApiClient(apiUrl2, key);
1513
+ const workspaceData = await client.listWorkspaces();
1514
+ const workspace = workspaceData.workspaces.find(
1515
+ (candidate) => candidate.handle === opts.workspace
1516
+ );
1517
+ if (!workspace) {
1518
+ throw new Error(
1519
+ `No workspace with handle "${opts.workspace}" belongs to this account.`
1520
+ );
1521
+ }
1522
+ const data = await client.listFolders(workspace.id);
1523
+ if (opts.json) {
1524
+ console.log(JSON.stringify(data, null, 2));
1525
+ return;
1526
+ }
1527
+ if (data.folders.length === 0) {
1528
+ console.log(`No folders in workspace ${workspace.handle}.`);
1529
+ return;
1530
+ }
1531
+ const plural = data.folders.length === 1 ? "" : "s";
1532
+ console.log(
1533
+ `${data.folders.length} folder${plural} in ${workspace.handle}
1534
+ `
1535
+ );
1536
+ for (const folder of data.folders) {
1537
+ printFolder(folder);
1538
+ }
1539
+ }
1540
+ function printFolder(folder) {
1541
+ console.log(`${folder.name} (${folder.visibility})`);
1542
+ console.log(` ${folder.id}
1543
+ `);
1544
+ }
1545
+
1546
+ // src/link.ts
1547
+ import path7 from "path";
1548
+ import { createInterface } from "readline";
1549
+ async function link(opts) {
1550
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1551
+ /\/$/,
1552
+ ""
1553
+ );
1554
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1555
+ if (!key) {
1556
+ throw new Error(
1557
+ `Linking needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1558
+ );
1559
+ }
1560
+ const { artifacts } = await createApiClient(apiUrl2, key).listArtifacts();
1561
+ if (artifacts.length === 0) {
1562
+ throw new Error(
1563
+ "No artifacts in your account yet \u2014 publish one first, or claim one via its claim link."
1564
+ );
1565
+ }
1566
+ const selector = opts.artifact ?? opts.slug;
1567
+ let chosen;
1568
+ if (opts.artifact) {
1569
+ chosen = artifacts.find((a) => a.id === opts.artifact);
1570
+ } else if (opts.slug) {
1571
+ chosen = artifacts.find((a) => a.slug === opts.slug);
1572
+ }
1573
+ if (selector && !chosen) {
1574
+ throw new Error(
1575
+ `No artifact "${selector}" in your account \u2014 run \`artifact-cafe list\` to see them.`
1576
+ );
1577
+ }
1578
+ if (opts.json && !chosen) {
1579
+ console.log(JSON.stringify({ artifacts }, null, 2));
1580
+ return;
1581
+ }
1582
+ if (!chosen) {
1583
+ if (!process.stdin.isTTY) {
1584
+ throw new Error(
1585
+ "Pick an artifact with --artifact <id> or --slug <slug> (or --json to list them). Interactive selection needs a terminal."
1586
+ );
1587
+ }
1588
+ chosen = await pickArtifact(artifacts);
1589
+ }
1590
+ const dir = path7.resolve(opts.dir);
1591
+ writeConfig(dir, {
1592
+ apiUrl: apiUrl2,
1593
+ artifactId: chosen.id,
1594
+ slug: chosen.slug,
1595
+ url: chosen.url
1596
+ // No publishToken — this folder authorizes via your login, not a secret.
1597
+ });
1598
+ if (opts.json) {
1599
+ console.log(
1600
+ JSON.stringify(
1601
+ {
1602
+ artifactId: chosen.id,
1603
+ linked: true,
1604
+ slug: chosen.slug,
1605
+ url: chosen.url
1606
+ },
1607
+ null,
1608
+ 2
1609
+ )
1610
+ );
1611
+ return;
1612
+ }
1613
+ console.log(`
1614
+ \u2713 Linked ${dir} \u2192 "${chosen.title}" \u2014 ${chosen.url}`);
1615
+ console.log(
1616
+ " Owner actions (publish, comments --wait, protect) use your login.\n"
1617
+ );
1618
+ }
1619
+ function pickArtifact(artifacts) {
1620
+ console.log("Your artifacts:\n");
1621
+ artifacts.forEach((a, i) => {
1622
+ const v = a.currentVersionNumber ? `v${a.currentVersionNumber}` : "\u2014";
1623
+ console.log(
1624
+ ` ${String(i + 1).padStart(2)}. ${a.title} \xB7 ${v} \xB7 ${a.url}`
1625
+ );
1626
+ });
1627
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1628
+ return new Promise((resolve, reject) => {
1629
+ rl.question(`
1630
+ Link which? [1-${artifacts.length}]: `, (answer) => {
1631
+ rl.close();
1632
+ const n = Number(answer.trim());
1633
+ if (!(Number.isInteger(n) && n >= 1 && n <= artifacts.length)) {
1634
+ reject(new Error(`Enter a number between 1 and ${artifacts.length}.`));
1635
+ return;
1636
+ }
1637
+ resolve(artifacts[n - 1]);
1638
+ });
1639
+ });
1640
+ }
1641
+
1172
1642
  // src/list.ts
1173
1643
  async function list(opts) {
1174
1644
  const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
@@ -1226,13 +1696,13 @@ function relativeTime(iso) {
1226
1696
  }
1227
1697
 
1228
1698
  // src/login.ts
1229
- import { createInterface } from "readline";
1699
+ import { createInterface as createInterface2 } from "readline";
1230
1700
  function apiUrl() {
1231
1701
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1232
1702
  }
1233
1703
  function promptSecret(question) {
1234
1704
  return new Promise((resolve) => {
1235
- const rl = createInterface({
1705
+ const rl = createInterface2({
1236
1706
  input: process.stdin,
1237
1707
  output: process.stdout
1238
1708
  });
@@ -1253,7 +1723,7 @@ function promptSecret(question) {
1253
1723
  }
1254
1724
  async function login(opts) {
1255
1725
  const base = apiUrl();
1256
- const keysUrl = `${base}/cli`;
1726
+ const keysUrl = `${base}/dashboard?view=api-keys`;
1257
1727
  let key = opts.apiKey?.trim();
1258
1728
  if (!key) {
1259
1729
  if (!opts.json) {
@@ -1335,28 +1805,32 @@ function logout(opts) {
1335
1805
  }
1336
1806
  console.log(`\u2713 Logged out of ${apiOrigin(base)} on this machine.`);
1337
1807
  console.log(
1338
- `The key itself is still active \u2014 revoke it at ${base}/cli if this machine is shared.`
1808
+ `The key itself is still active \u2014 revoke it at ${base}/dashboard?view=api-keys if this machine is shared.`
1339
1809
  );
1340
1810
  }
1341
1811
 
1342
1812
  // src/open.ts
1343
- import path7 from "path";
1344
- function openArtifact(opts) {
1345
- const dir = path7.resolve(opts.dir);
1346
- const config = readConfig(dir);
1347
- if (!config) {
1348
- throw new Error(
1349
- `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
1350
- );
1351
- }
1352
- console.log(config.url);
1813
+ import path8 from "path";
1814
+ async function openArtifact(opts) {
1815
+ const dir = path8.resolve(opts.dir);
1816
+ const target = resolveArtifactTarget(dir, {
1817
+ apiKey: opts.apiKey,
1818
+ artifact: opts.artifact
1819
+ });
1820
+ const url = target.url ?? reviewUrl(
1821
+ target.apiUrl,
1822
+ (await createApiClient(target.apiUrl, target.credential).getArtifact(
1823
+ target.artifactId
1824
+ )).slug
1825
+ );
1826
+ console.log(url);
1353
1827
  if (process.stdout.isTTY) {
1354
- openInBrowser(config.url);
1828
+ openInBrowser(url);
1355
1829
  }
1356
1830
  }
1357
1831
 
1358
1832
  // src/protect.ts
1359
- import path8 from "path";
1833
+ import path9 from "path";
1360
1834
  async function protect(opts) {
1361
1835
  if (opts.off && opts.password !== void 0) {
1362
1836
  throw new Error("Pass either --password or --off, not both.");
@@ -1373,30 +1847,64 @@ async function protect(opts) {
1373
1847
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
1374
1848
  }
1375
1849
  }
1376
- const dir = path8.resolve(opts.dir);
1377
- const config = readConfig(dir);
1378
- if (!config) {
1379
- throw new Error(
1380
- `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
1381
- );
1382
- }
1383
- const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
1384
- const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
1385
- const client = createApiClient(apiUrl2, token);
1386
- const res = await client.setProtection(config.artifactId, password);
1850
+ const dir = path9.resolve(opts.dir);
1851
+ const target = resolveArtifactTarget(dir, {
1852
+ apiKey: opts.apiKey,
1853
+ artifact: opts.artifact
1854
+ });
1855
+ const client = createApiClient(target.apiUrl, target.credential);
1856
+ const res = await client.setProtection(target.artifactId, password);
1387
1857
  if (opts.json) {
1388
1858
  console.log(JSON.stringify({ protected: res.protected }, null, 2));
1389
1859
  return;
1390
1860
  }
1861
+ const url = target.url ?? reviewUrl(
1862
+ target.apiUrl,
1863
+ (await client.getArtifact(target.artifactId)).slug
1864
+ );
1391
1865
  console.log(
1392
1866
  res.protected ? `
1393
- \u2713 Password protection on \u2014 ${config.url}
1867
+ \u2713 Password protection on \u2014 ${url}
1394
1868
  ` : `
1395
- \u2713 Password protection removed \u2014 ${config.url}
1869
+ \u2713 Password protection removed \u2014 ${url}
1396
1870
  `
1397
1871
  );
1398
1872
  }
1399
1873
 
1874
+ // src/workspaces.ts
1875
+ async function workspaces(opts) {
1876
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1877
+ /\/$/,
1878
+ ""
1879
+ );
1880
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1881
+ if (!key) {
1882
+ throw new Error(
1883
+ `Listing workspaces needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1884
+ );
1885
+ }
1886
+ const data = await createApiClient(apiUrl2, key).listWorkspaces();
1887
+ if (opts.json) {
1888
+ console.log(JSON.stringify(data, null, 2));
1889
+ return;
1890
+ }
1891
+ if (data.workspaces.length === 0) {
1892
+ console.log("No workspaces yet \u2014 create one from the dashboard.");
1893
+ return;
1894
+ }
1895
+ const plural = data.workspaces.length === 1 ? "" : "s";
1896
+ console.log(`${data.workspaces.length} workspace${plural}
1897
+ `);
1898
+ for (const workspace of data.workspaces) {
1899
+ printWorkspace(workspace);
1900
+ }
1901
+ }
1902
+ function printWorkspace(workspace) {
1903
+ console.log(`${workspace.name} (${workspace.role ?? "non-member"})`);
1904
+ console.log(` ${workspace.handle} \xB7 ${workspace.id}
1905
+ `);
1906
+ }
1907
+
1400
1908
  // src/index.ts
1401
1909
  var publish2 = defineCommand({
1402
1910
  args: {
@@ -1414,13 +1922,21 @@ var publish2 = defineCommand({
1414
1922
  type: "positional"
1415
1923
  },
1416
1924
  entry: { description: "Entry file (default: auto-detect)", type: "string" },
1925
+ folder: {
1926
+ description: "Publish into this workspace folder name (or id)",
1927
+ type: "string"
1928
+ },
1417
1929
  json: { description: "Machine-readable output", type: "boolean" },
1418
1930
  "no-open": { description: "Don't open the review URL", type: "boolean" },
1419
1931
  password: {
1420
1932
  description: "Password-protect the artifact (min 6 chars)",
1421
1933
  type: "string"
1422
1934
  },
1423
- title: { description: "Artifact title", type: "string" }
1935
+ title: { description: "Artifact title", type: "string" },
1936
+ workspace: {
1937
+ description: "Publish into this workspace handle",
1938
+ type: "string"
1939
+ }
1424
1940
  },
1425
1941
  meta: { description: "Publish a folder as an artifact", name: "publish" },
1426
1942
  async run({ args }) {
@@ -1430,10 +1946,12 @@ var publish2 = defineCommand({
1430
1946
  artifact: args.artifact,
1431
1947
  dir: args.dir,
1432
1948
  entry: args.entry,
1949
+ folder: args.folder,
1433
1950
  json: Boolean(args.json),
1434
1951
  open: !args["no-open"],
1435
1952
  password: args.password,
1436
- title: args.title
1953
+ title: args.title,
1954
+ workspace: args.workspace
1437
1955
  });
1438
1956
  } catch (err) {
1439
1957
  printCliError(err, Boolean(args.json));
@@ -1517,8 +2035,110 @@ var list2 = defineCommand({
1517
2035
  }
1518
2036
  }
1519
2037
  });
2038
+ var folders2 = defineCommand({
2039
+ args: {
2040
+ "api-key": {
2041
+ description: "List with this API key (default: stored login)",
2042
+ type: "string"
2043
+ },
2044
+ json: { description: "Machine-readable output", type: "boolean" },
2045
+ workspace: {
2046
+ description: "Workspace handle whose folders to list",
2047
+ required: true,
2048
+ type: "string"
2049
+ }
2050
+ },
2051
+ meta: {
2052
+ description: "List folders in a workspace",
2053
+ name: "folders"
2054
+ },
2055
+ async run({ args }) {
2056
+ try {
2057
+ await folders({
2058
+ apiKey: args["api-key"],
2059
+ json: Boolean(args.json),
2060
+ workspace: args.workspace
2061
+ });
2062
+ } catch (err) {
2063
+ printCliError(err, Boolean(args.json));
2064
+ process.exit(1);
2065
+ }
2066
+ }
2067
+ });
2068
+ var workspaces2 = defineCommand({
2069
+ args: {
2070
+ "api-key": {
2071
+ description: "List with this API key (default: stored login)",
2072
+ type: "string"
2073
+ },
2074
+ json: { description: "Machine-readable output", type: "boolean" }
2075
+ },
2076
+ meta: {
2077
+ description: "List the workspaces your account belongs to",
2078
+ name: "workspaces"
2079
+ },
2080
+ async run({ args }) {
2081
+ try {
2082
+ await workspaces({
2083
+ apiKey: args["api-key"],
2084
+ json: Boolean(args.json)
2085
+ });
2086
+ } catch (err) {
2087
+ printCliError(err, Boolean(args.json));
2088
+ process.exit(1);
2089
+ }
2090
+ }
2091
+ });
2092
+ var link2 = defineCommand({
2093
+ args: {
2094
+ "api-key": {
2095
+ description: "Link with this API key (default: stored login)",
2096
+ type: "string"
2097
+ },
2098
+ artifact: {
2099
+ description: "Link this artifact id (skip the picker)",
2100
+ type: "string"
2101
+ },
2102
+ dir: {
2103
+ default: ".",
2104
+ description: "Directory to link",
2105
+ type: "positional"
2106
+ },
2107
+ json: { description: "Machine-readable output", type: "boolean" },
2108
+ slug: {
2109
+ description: "Link this artifact slug (skip the picker)",
2110
+ type: "string"
2111
+ }
2112
+ },
2113
+ meta: {
2114
+ description: "Link a folder to one of your artifacts (pick from your account)",
2115
+ name: "link"
2116
+ },
2117
+ async run({ args }) {
2118
+ try {
2119
+ await link({
2120
+ apiKey: args["api-key"],
2121
+ artifact: args.artifact,
2122
+ dir: args.dir,
2123
+ json: Boolean(args.json),
2124
+ slug: args.slug
2125
+ });
2126
+ } catch (err) {
2127
+ printCliError(err, Boolean(args.json));
2128
+ process.exit(1);
2129
+ }
2130
+ }
2131
+ });
1520
2132
  var comments2 = defineCommand({
1521
2133
  args: {
2134
+ "api-key": {
2135
+ description: "Authorize with an account API key (default: stored login)",
2136
+ type: "string"
2137
+ },
2138
+ artifact: {
2139
+ description: "Act on an artifact ID instead of the linked folder",
2140
+ type: "string"
2141
+ },
1522
2142
  dir: {
1523
2143
  default: ".",
1524
2144
  description: "Published directory",
@@ -1558,6 +2178,8 @@ var comments2 = defineCommand({
1558
2178
  );
1559
2179
  }
1560
2180
  await comments({
2181
+ apiKey: args["api-key"],
2182
+ artifact: args.artifact,
1561
2183
  dir: args.dir,
1562
2184
  json: Boolean(args.json),
1563
2185
  status: args.status,
@@ -1571,8 +2193,76 @@ var comments2 = defineCommand({
1571
2193
  }
1572
2194
  }
1573
2195
  });
2196
+ var listen = defineCommand({
2197
+ args: {
2198
+ "api-key": {
2199
+ description: "Authorize with an account API key (default: stored login)",
2200
+ type: "string"
2201
+ },
2202
+ artifact: {
2203
+ description: "Act on an artifact ID instead of the linked folder",
2204
+ type: "string"
2205
+ },
2206
+ dir: {
2207
+ default: ".",
2208
+ description: "Published directory",
2209
+ type: "positional"
2210
+ },
2211
+ json: { description: "Machine-readable output", type: "boolean" },
2212
+ status: {
2213
+ default: "open",
2214
+ description: "Filter: open | resolved | all",
2215
+ type: "string"
2216
+ },
2217
+ timeout: {
2218
+ default: "900",
2219
+ description: "Give up after this many seconds (exit 2)",
2220
+ type: "string"
2221
+ },
2222
+ version: {
2223
+ default: "current",
2224
+ description: "Filter: current | all",
2225
+ type: "string"
2226
+ }
2227
+ },
2228
+ meta: {
2229
+ description: "Wait for new reviewer feedback (alias for comments --wait)",
2230
+ name: "listen"
2231
+ },
2232
+ async run({ args }) {
2233
+ try {
2234
+ const timeout = Number(args.timeout);
2235
+ if (!Number.isFinite(timeout) || timeout <= 0) {
2236
+ throw new Error(
2237
+ `--timeout must be a positive number of seconds (got "${args.timeout}").`
2238
+ );
2239
+ }
2240
+ await comments({
2241
+ apiKey: args["api-key"],
2242
+ artifact: args.artifact,
2243
+ dir: args.dir,
2244
+ json: Boolean(args.json),
2245
+ status: args.status,
2246
+ timeout,
2247
+ version: args.version,
2248
+ wait: true
2249
+ });
2250
+ } catch (err) {
2251
+ printCliError(err, Boolean(args.json));
2252
+ process.exit(1);
2253
+ }
2254
+ }
2255
+ });
1574
2256
  var protect2 = defineCommand({
1575
2257
  args: {
2258
+ "api-key": {
2259
+ description: "Authorize with an account API key (default: stored login)",
2260
+ type: "string"
2261
+ },
2262
+ artifact: {
2263
+ description: "Act on an artifact ID instead of the linked folder",
2264
+ type: "string"
2265
+ },
1576
2266
  dir: {
1577
2267
  default: ".",
1578
2268
  description: "Published directory",
@@ -1589,6 +2279,8 @@ var protect2 = defineCommand({
1589
2279
  async run({ args }) {
1590
2280
  try {
1591
2281
  await protect({
2282
+ apiKey: args["api-key"],
2283
+ artifact: args.artifact,
1592
2284
  dir: args.dir,
1593
2285
  json: Boolean(args.json),
1594
2286
  off: Boolean(args.off),
@@ -1602,6 +2294,14 @@ var protect2 = defineCommand({
1602
2294
  });
1603
2295
  var open = defineCommand({
1604
2296
  args: {
2297
+ "api-key": {
2298
+ description: "Authorize with an account API key (default: stored login)",
2299
+ type: "string"
2300
+ },
2301
+ artifact: {
2302
+ description: "Open an artifact ID instead of the linked folder",
2303
+ type: "string"
2304
+ },
1605
2305
  dir: {
1606
2306
  default: ".",
1607
2307
  description: "Published directory",
@@ -1609,9 +2309,13 @@ var open = defineCommand({
1609
2309
  }
1610
2310
  },
1611
2311
  meta: { description: "Open the linked artifact's review URL", name: "open" },
1612
- run({ args }) {
2312
+ async run({ args }) {
1613
2313
  try {
1614
- openArtifact({ dir: args.dir });
2314
+ await openArtifact({
2315
+ apiKey: args["api-key"],
2316
+ artifact: args.artifact,
2317
+ dir: args.dir
2318
+ });
1615
2319
  } catch (err) {
1616
2320
  printCliError(err, false);
1617
2321
  process.exit(1);
@@ -1644,16 +2348,31 @@ var main = defineCommand({
1644
2348
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1645
2349
  name: "artifact-cafe"
1646
2350
  },
2351
+ // Bare `artifact-cafe` (no subcommand) → brand banner + usage. citty
2352
+ // still calls this parent run *after* a matched subcommand, so bail when
2353
+ // a subcommand token is present (any non-flag arg; citty has already
2354
+ // rejected unknown ones with E_UNKNOWN_COMMAND before we get here).
2355
+ async run({ cmd, rawArgs }) {
2356
+ if (rawArgs.some((arg) => !arg.startsWith("-"))) {
2357
+ return;
2358
+ }
2359
+ printBanner();
2360
+ await showUsage(cmd);
2361
+ },
1647
2362
  subCommands: {
1648
2363
  comments: comments2,
1649
2364
  dashboard,
2365
+ folders: folders2,
2366
+ link: link2,
1650
2367
  list: list2,
2368
+ listen,
1651
2369
  login: login2,
1652
2370
  logout: logout2,
1653
2371
  open,
1654
2372
  protect: protect2,
1655
2373
  publish: publish2,
1656
- whoami: whoami2
2374
+ whoami: whoami2,
2375
+ workspaces: workspaces2
1657
2376
  }
1658
2377
  });
1659
2378
  runMain(main);