artifact-cafe 0.0.3 → 0.0.5

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 +773 -325
  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";
@@ -167,47 +194,82 @@ var apiErrorSchema = z3.object({
167
194
  retryAfter: z3.number().int().positive().optional()
168
195
  });
169
196
 
170
- // ../shared/src/owner.ts
197
+ // ../shared/src/me.ts
171
198
  import { z as z4 } from "zod";
172
- var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
173
- var claimResponseSchema = z4.object({
174
- artifactId: z4.string(),
199
+ var ownArtifactSchema = z4.object({
200
+ createdAt: z4.string(),
201
+ currentVersionNumber: z4.number().nullable(),
202
+ id: z4.string(),
203
+ openThreads: z4.number(),
204
+ protected: z4.boolean(),
175
205
  slug: z4.string(),
176
206
  title: z4.string(),
177
- url: z4.string().url()
207
+ totalViews: z4.number(),
208
+ updatedAt: z4.string(),
209
+ url: z4.string(),
210
+ versionCount: z4.number()
211
+ });
212
+ var ownArtifactsResponseSchema = z4.object({
213
+ artifacts: z4.array(ownArtifactSchema)
178
214
  });
179
- var updateArtifactSchema = z4.object({
180
- slug: z4.string().min(3).max(63).regex(SLUG_PATTERN, {
215
+
216
+ // ../shared/src/owner.ts
217
+ import { z as z5 } from "zod";
218
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
219
+ var claimResponseSchema = z5.object({
220
+ artifactId: z5.string(),
221
+ slug: z5.string(),
222
+ title: z5.string(),
223
+ url: z5.string().url()
224
+ });
225
+ var updateArtifactSchema = z5.object({
226
+ slug: z5.string().min(3).max(63).regex(SLUG_PATTERN, {
181
227
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
182
228
  }).optional(),
183
- title: z4.string().min(1).max(200).optional()
229
+ title: z5.string().min(1).max(200).optional()
184
230
  }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
185
231
  message: "provide at least one of title, slug"
186
232
  });
187
- var deleteArtifactResponseSchema = z4.object({
188
- deleted: z4.literal(true)
233
+ var deleteArtifactResponseSchema = z5.object({
234
+ deleted: z5.literal(true)
235
+ });
236
+
237
+ // ../shared/src/presence.ts
238
+ import { z as z6 } from "zod";
239
+ var AGENT_PRESENCE = {
240
+ heartbeatSeconds: 15,
241
+ shellPollSeconds: 15,
242
+ ttlSeconds: 45
243
+ };
244
+ var agentHeartbeatResponseSchema = z6.object({
245
+ listening: z6.literal(true),
246
+ ttlSeconds: z6.number().int().positive()
247
+ });
248
+ var agentPresenceSchema = z6.object({
249
+ listening: z6.boolean(),
250
+ versionNumber: z6.number().int().positive().nullable()
189
251
  });
190
252
 
191
253
  // ../shared/src/protection.ts
192
- import { z as z5 } from "zod";
254
+ import { z as z7 } from "zod";
193
255
  var PASSWORD_MIN = 6;
194
256
  var PASSWORD_MAX = 128;
195
- var gatePasswordSchema = z5.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
196
- var setProtectionSchema = z5.object({
257
+ 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.`);
258
+ var setProtectionSchema = z7.object({
197
259
  password: gatePasswordSchema.nullable()
198
260
  });
199
- var protectionResponseSchema = z5.object({
200
- protected: z5.boolean()
261
+ var protectionResponseSchema = z7.object({
262
+ protected: z7.boolean()
201
263
  });
202
- var unlockSchema = z5.object({
203
- password: z5.string().min(1).max(1024)
264
+ var unlockSchema = z7.object({
265
+ password: z7.string().min(1).max(1024)
204
266
  });
205
- var unlockResponseSchema = z5.object({
206
- unlocked: z5.literal(true)
267
+ var unlockResponseSchema = z7.object({
268
+ unlocked: z7.literal(true)
207
269
  });
208
270
 
209
271
  // ../shared/src/publish.ts
210
- import { z as z6 } from "zod";
272
+ import { z as z8 } from "zod";
211
273
  function isSafeArtifactPath(p) {
212
274
  if (p.length === 0 || p.length > 1024) {
213
275
  return false;
@@ -220,128 +282,128 @@ function isSafeArtifactPath(p) {
220
282
  }
221
283
  return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
222
284
  }
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, {
285
+ var manifestFileSchema = z8.object({
286
+ hash: z8.string().regex(/^sha256_[a-f0-9]{64}$/),
287
+ mimeType: z8.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
288
+ path: z8.string().min(1).max(1024).refine(isSafeArtifactPath, {
227
289
  message: "path must be a safe relative path"
228
290
  }),
229
- size: z6.number().int().positive()
291
+ size: z8.number().int().positive()
230
292
  });
231
- var publishSourceSchema = z6.object({
232
- agent: z6.string().optional(),
233
- runId: z6.string().optional(),
234
- type: z6.literal("cli")
293
+ var publishSourceSchema = z8.object({
294
+ agent: z8.string().optional(),
295
+ runId: z8.string().optional(),
296
+ type: z8.literal("cli")
235
297
  });
236
- var createPublishSessionSchema = z6.object({
237
- artifactId: z6.string().nullable(),
238
- entryPath: z6.string().optional(),
239
- files: z6.array(manifestFileSchema).min(1).max(1e3),
298
+ var createPublishSessionSchema = z8.object({
299
+ artifactId: z8.string().nullable(),
300
+ entryPath: z8.string().optional(),
301
+ files: z8.array(manifestFileSchema).min(1).max(1e3),
240
302
  source: publishSourceSchema,
241
- title: z6.string().max(200).optional()
303
+ title: z8.string().max(200).optional()
242
304
  });
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()
305
+ var publishSessionResponseSchema = z8.object({
306
+ artifactId: z8.string(),
307
+ expiresAt: z8.string(),
308
+ filesToUpload: z8.array(
309
+ z8.object({
310
+ method: z8.literal("PUT"),
311
+ path: z8.string(),
312
+ uploadUrl: z8.string().url()
251
313
  })
252
314
  ),
253
- publishSessionId: z6.string(),
254
- skippedFiles: z6.array(z6.string()),
255
- versionId: z6.string()
315
+ publishSessionId: z8.string(),
316
+ skippedFiles: z8.array(z8.string()),
317
+ versionId: z8.string()
256
318
  });
257
- var finalizeResponseSchema = z6.object({
258
- artifactId: z6.string(),
319
+ var finalizeResponseSchema = z8.object({
320
+ artifactId: z8.string(),
259
321
  // API-key publishes create the artifact already claimed (SPEC §5.4) —
260
322
  // the CLI uses this to tell "claim URL pending" from "already yours".
261
- claimed: z6.boolean(),
323
+ claimed: z8.boolean(),
262
324
  // Returned only on first publish of a new artifact (one-time).
263
- claimUrl: z6.string().url().optional(),
264
- publishToken: z6.string().optional(),
265
- url: z6.string().url(),
266
- versionId: z6.string(),
267
- versionNumber: z6.number().int().positive()
325
+ claimUrl: z8.string().url().optional(),
326
+ publishToken: z8.string().optional(),
327
+ url: z8.string().url(),
328
+ versionId: z8.string(),
329
+ versionNumber: z8.number().int().positive()
268
330
  });
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()
331
+ var artifactResponseSchema = z8.object({
332
+ claimed: z8.boolean(),
333
+ currentVersion: z8.object({
334
+ entryUrl: z8.string().url(),
335
+ id: z8.string(),
336
+ versionNumber: z8.number().int().positive()
275
337
  }).nullable(),
276
- id: z6.string(),
277
- slug: z6.string(),
278
- title: z6.string()
338
+ id: z8.string(),
339
+ slug: z8.string(),
340
+ title: z8.string()
279
341
  });
280
342
 
281
343
  // ../shared/src/report.ts
282
- import { z as z7 } from "zod";
283
- var reportRequestSchema = z7.object({
284
- reason: z7.string().max(1e3).optional()
344
+ import { z as z9 } from "zod";
345
+ var reportRequestSchema = z9.object({
346
+ reason: z9.string().max(1e3).optional()
285
347
  });
286
- var reportResponseSchema = z7.object({
287
- reported: z7.literal(true)
348
+ var reportResponseSchema = z9.object({
349
+ reported: z9.literal(true)
288
350
  });
289
351
 
290
352
  // ../shared/src/stats.ts
291
- import { z as z8 } from "zod";
292
- var avgViewSecondsSchema = z8.number().int().nonnegative().nullable();
293
- var versionViewStatsSchema = z8.object({
353
+ import { z as z10 } from "zod";
354
+ var avgViewSecondsSchema = z10.number().int().nonnegative().nullable();
355
+ var versionViewStatsSchema = z10.object({
294
356
  avgViewSeconds: avgViewSecondsSchema,
295
357
  // Views from inside the review shell's iframe vs. direct visits to
296
358
  // the render URL. views = embeddedViews + directViews.
297
- directViews: z8.number().int().nonnegative(),
298
- embeddedViews: z8.number().int().nonnegative(),
359
+ directViews: z10.number().int().nonnegative(),
360
+ embeddedViews: z10.number().int().nonnegative(),
299
361
  // 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()
362
+ uniqueViewers: z10.number().int().nonnegative(),
363
+ versionNumber: z10.number().int().positive(),
364
+ views: z10.number().int().nonnegative()
303
365
  });
304
- var dailyViewsSchema = z8.object({
366
+ var dailyViewsSchema = z10.object({
305
367
  avgViewSeconds: avgViewSecondsSchema,
306
368
  // UTC day, YYYY-MM-DD.
307
- day: z8.string(),
308
- uniqueViewers: z8.number().int().nonnegative(),
309
- views: z8.number().int().nonnegative()
369
+ day: z10.string(),
370
+ uniqueViewers: z10.number().int().nonnegative(),
371
+ views: z10.number().int().nonnegative()
310
372
  });
311
- var artifactStatsResponseSchema = z8.object({
373
+ var artifactStatsResponseSchema = z10.object({
312
374
  avgViewSeconds: avgViewSecondsSchema,
313
- daily: z8.array(dailyViewsSchema),
375
+ daily: z10.array(dailyViewsSchema),
314
376
  // 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)
377
+ totalUniqueViewers: z10.number().int().nonnegative(),
378
+ totalViews: z10.number().int().nonnegative(),
379
+ versions: z10.array(versionViewStatsSchema)
318
380
  });
319
381
 
320
382
  // ../shared/src/versions.ts
321
- import { z as z9 } from "zod";
322
- var versionSourceSchema = z9.object({
323
- agentName: z9.string().nullable(),
324
- type: z9.string()
383
+ import { z as z11 } from "zod";
384
+ var versionSourceSchema = z11.object({
385
+ agentName: z11.string().nullable(),
386
+ type: z11.string()
325
387
  });
326
- var versionChangesSchema = z9.object({
327
- added: z9.array(z9.string()),
328
- changed: z9.array(z9.string()),
329
- removed: z9.array(z9.string())
388
+ var versionChangesSchema = z11.object({
389
+ added: z11.array(z11.string()),
390
+ changed: z11.array(z11.string()),
391
+ removed: z11.array(z11.string())
330
392
  });
331
- var versionSummarySchema = z9.object({
393
+ var versionSummarySchema = z11.object({
332
394
  // Absent for the first version — there is no previous version to diff against.
333
395
  changes: versionChangesSchema.nullable(),
334
- createdAt: z9.string(),
335
- entryUrl: z9.string().url(),
336
- fileCount: z9.number().int().nonnegative(),
337
- id: z9.string(),
338
- isCurrent: z9.boolean(),
396
+ createdAt: z11.string(),
397
+ entryUrl: z11.string().url(),
398
+ fileCount: z11.number().int().nonnegative(),
399
+ id: z11.string(),
400
+ isCurrent: z11.boolean(),
339
401
  source: versionSourceSchema,
340
- totalSizeBytes: z9.number().int().nonnegative(),
341
- versionNumber: z9.number().int().positive()
402
+ totalSizeBytes: z11.number().int().nonnegative(),
403
+ versionNumber: z11.number().int().positive()
342
404
  });
343
- var versionListResponseSchema = z9.object({
344
- versions: z9.array(versionSummarySchema)
405
+ var versionListResponseSchema = z11.object({
406
+ versions: z11.array(versionSummarySchema)
345
407
  });
346
408
 
347
409
  // src/api.ts
@@ -395,6 +457,15 @@ function createApiClient(apiUrl2, publishToken) {
395
457
  headers.authorization = `Bearer ${publishToken}`;
396
458
  }
397
459
  return {
460
+ // SPEC §6.19 — mark the agent as actively waiting for feedback.
461
+ // Authorized by the publish token this client was created with.
462
+ async agentHeartbeat(artifactId) {
463
+ return request(
464
+ `${base}/api/v1/artifacts/${artifactId}/agent-presence`,
465
+ { headers, method: "POST" },
466
+ (d) => agentHeartbeatResponseSchema.parse(d)
467
+ );
468
+ },
398
469
  async createSession(body) {
399
470
  return request(
400
471
  `${base}/api/v1/publish`,
@@ -475,37 +546,99 @@ async function uploadFile(uploadUrl, content, mimeType) {
475
546
  // src/config.ts
476
547
  import {
477
548
  appendFileSync,
478
- existsSync,
479
- mkdirSync,
480
- readFileSync,
481
- writeFileSync
549
+ existsSync as existsSync2,
550
+ mkdirSync as mkdirSync2,
551
+ readFileSync as readFileSync2,
552
+ writeFileSync as writeFileSync2
482
553
  } from "fs";
554
+ import path2 from "path";
555
+ import { z as z13 } from "zod";
556
+
557
+ // src/credentials.ts
558
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
559
+ import { homedir } from "os";
483
560
  import path from "path";
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()
561
+ import { z as z12 } from "zod";
562
+ var DEFAULT_API_URL = "https://artifact.cafe";
563
+ var credentialsSchema = z12.object({
564
+ keys: z12.record(z12.string(), z12.string()),
565
+ version: z12.literal(1)
566
+ });
567
+ function credentialsPath() {
568
+ return path.join(homedir(), ".artifactcafe", "credentials");
569
+ }
570
+ function apiOrigin(apiUrl2) {
571
+ return new URL(apiUrl2).origin;
572
+ }
573
+ function readCredentials() {
574
+ const file = credentialsPath();
575
+ if (!existsSync(file)) {
576
+ return { keys: {}, version: 1 };
577
+ }
578
+ try {
579
+ return credentialsSchema.parse(JSON.parse(readFileSync(file, "utf8")));
580
+ } catch {
581
+ return { keys: {}, version: 1 };
582
+ }
583
+ }
584
+ function writeCredentials(credentials) {
585
+ const file = credentialsPath();
586
+ mkdirSync(path.dirname(file), { mode: 448, recursive: true });
587
+ writeFileSync(file, `${JSON.stringify(credentials, null, 2)}
588
+ `, {
589
+ mode: 384
590
+ });
591
+ }
592
+ function storedApiKey(apiUrl2) {
593
+ return readCredentials().keys[apiOrigin(apiUrl2)] ?? null;
594
+ }
595
+ function storeApiKey(apiUrl2, key) {
596
+ const credentials = readCredentials();
597
+ credentials.keys[apiOrigin(apiUrl2)] = key;
598
+ writeCredentials(credentials);
599
+ }
600
+ function removeApiKey(apiUrl2) {
601
+ const credentials = readCredentials();
602
+ const origin = apiOrigin(apiUrl2);
603
+ if (!(origin in credentials.keys)) {
604
+ return false;
605
+ }
606
+ delete credentials.keys[origin];
607
+ writeCredentials(credentials);
608
+ return true;
609
+ }
610
+ function resolveApiKey(apiUrl2, flag) {
611
+ return flag ?? process.env.ARTIFACT_CAFE_API_KEY ?? storedApiKey(apiUrl2) ?? void 0;
612
+ }
613
+
614
+ // src/config.ts
615
+ var configSchema = z13.object({
616
+ apiUrl: z13.string(),
617
+ artifactId: z13.string(),
618
+ // Optional: a folder linked by `link` to an artifact you own carries no
619
+ // token — owner actions authorize via your stored login instead. A first
620
+ // publish still writes the anonymous-mode token here.
621
+ publishToken: z13.string().optional(),
622
+ slug: z13.string(),
623
+ url: z13.string()
491
624
  });
492
625
  var CONFIG_DIR = ".artifactcafe";
493
626
  var CONFIG_FILE = "config.json";
494
627
  function readConfig(dir) {
495
- const file = path.join(dir, CONFIG_DIR, CONFIG_FILE);
496
- if (!existsSync(file)) {
628
+ const file = path2.join(dir, CONFIG_DIR, CONFIG_FILE);
629
+ if (!existsSync2(file)) {
497
630
  return null;
498
631
  }
499
632
  try {
500
- return configSchema.parse(JSON.parse(readFileSync(file, "utf8")));
633
+ return configSchema.parse(JSON.parse(readFileSync2(file, "utf8")));
501
634
  } catch {
502
635
  return null;
503
636
  }
504
637
  }
505
638
  function writeConfig(dir, config) {
506
- mkdirSync(path.join(dir, CONFIG_DIR), { recursive: true });
507
- writeFileSync(
508
- path.join(dir, CONFIG_DIR, CONFIG_FILE),
639
+ mkdirSync2(path2.join(dir, CONFIG_DIR), { recursive: true });
640
+ writeFileSync2(
641
+ path2.join(dir, CONFIG_DIR, CONFIG_FILE),
509
642
  `${JSON.stringify(config, null, 2)}
510
643
  `,
511
644
  { mode: 384 }
@@ -513,11 +646,11 @@ function writeConfig(dir, config) {
513
646
  ensureGitignored(dir);
514
647
  }
515
648
  function ensureGitignored(dir) {
516
- const gitignore = path.join(dir, ".gitignore");
517
- if (!existsSync(gitignore)) {
649
+ const gitignore = path2.join(dir, ".gitignore");
650
+ if (!existsSync2(gitignore)) {
518
651
  return;
519
652
  }
520
- const lines = readFileSync(gitignore, "utf8").split(/\r?\n/);
653
+ const lines = readFileSync2(gitignore, "utf8").split(/\r?\n/);
521
654
  const present = lines.some((l) => {
522
655
  const t = l.trim();
523
656
  return t === CONFIG_DIR || t === `${CONFIG_DIR}/` || t === `/${CONFIG_DIR}` || t === `/${CONFIG_DIR}/`;
@@ -532,10 +665,30 @@ ${CONFIG_DIR}/
532
665
  );
533
666
  }
534
667
  }
668
+ function reviewUrl(apiUrl2, slug) {
669
+ return `${apiUrl2.replace(/\/$/, "")}/a/${slug}`;
670
+ }
671
+ function resolveArtifactTarget(dir, opts = {}) {
672
+ const config = readConfig(dir);
673
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
674
+ const artifactId = opts.artifact ?? process.env.ARTIFACT_CAFE_ARTIFACT_ID ?? config?.artifactId ?? null;
675
+ if (!artifactId) {
676
+ throw new Error(
677
+ `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.`
678
+ );
679
+ }
680
+ const folderToken = artifactId === config?.artifactId ? config.publishToken : void 0;
681
+ const credential = folderToken ?? process.env.ARTIFACT_CAFE_TOKEN ?? resolveApiKey(apiUrl2, opts.apiKey);
682
+ const url = artifactId === config?.artifactId ? config.url : void 0;
683
+ return { apiUrl: apiUrl2, artifactId, credential, url };
684
+ }
535
685
 
536
686
  // src/comments.ts
537
687
  var STATUSES = ["open", "resolved", "all"];
538
688
  var VERSIONS = ["current", "all"];
689
+ var pollSeconds = () => Number(process.env.AC_WAIT_POLL_SECONDS ?? 5);
690
+ var settleSeconds = () => Number(process.env.AC_WAIT_SETTLE_SECONDS ?? 20);
691
+ var WAIT_TIMEOUT_EXIT = 2;
539
692
  async function comments(opts) {
540
693
  if (!STATUSES.includes(opts.status)) {
541
694
  throw new Error(
@@ -549,16 +702,24 @@ async function comments(opts) {
549
702
  }
550
703
  const status = opts.status;
551
704
  const version = opts.version;
552
- const dir = path2.resolve(opts.dir);
553
- const config = readConfig(dir);
554
- if (!config) {
555
- throw new Error(
556
- `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
557
- );
705
+ const dir = path3.resolve(opts.dir);
706
+ const target = resolveArtifactTarget(dir, {
707
+ apiKey: opts.apiKey,
708
+ artifact: opts.artifact
709
+ });
710
+ const client = createApiClient(target.apiUrl, target.credential);
711
+ const url = target.url ?? (opts.json ? "" : await deriveReviewUrl(client, target));
712
+ const ref = { artifactId: target.artifactId, url };
713
+ if (opts.wait) {
714
+ await waitForFeedback(client, ref, {
715
+ json: opts.json,
716
+ status,
717
+ timeoutSeconds: opts.timeout,
718
+ version
719
+ });
720
+ return;
558
721
  }
559
- const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
560
- const client = createApiClient(apiUrl2);
561
- const data = await client.exportComments(config.artifactId, {
722
+ const data = await client.exportComments(target.artifactId, {
562
723
  status,
563
724
  version
564
725
  });
@@ -566,7 +727,117 @@ async function comments(opts) {
566
727
  console.log(JSON.stringify(data, null, 2));
567
728
  return;
568
729
  }
569
- printHuman(data, config.url, status, version);
730
+ printHuman(data, url, status, version);
731
+ }
732
+ async function deriveReviewUrl(client, target) {
733
+ const artifact = await client.getArtifact(target.artifactId);
734
+ return reviewUrl(target.apiUrl, artifact.slug);
735
+ }
736
+ var sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
737
+ function messageCounts(data) {
738
+ return new Map(data.comments.map((t) => [t.id, 1 + t.replies.length]));
739
+ }
740
+ function diffThreads(baseline, data) {
741
+ const changed = [];
742
+ for (const t of data.comments) {
743
+ const before = baseline.get(t.id);
744
+ if (before === void 0) {
745
+ changed.push({ ...t, change: "new" });
746
+ } else if (1 + t.replies.length > before) {
747
+ changed.push({ ...t, change: "updated" });
748
+ }
749
+ }
750
+ return changed;
751
+ }
752
+ async function waitForFeedback(client, config, opts) {
753
+ const query = { status: opts.status, version: opts.version };
754
+ const baselineExport = await client.exportComments(config.artifactId, query);
755
+ const baseline = messageCounts(baselineExport);
756
+ if (!opts.json) {
757
+ const mins = Math.round(opts.timeoutSeconds / 60);
758
+ console.log(
759
+ `Waiting for feedback on ${config.url} \u2014 times out after ${mins}m. Ctrl-C to stop.`
760
+ );
761
+ }
762
+ const deadline = Date.now() + opts.timeoutSeconds * 1e3;
763
+ let heartbeatWarned = false;
764
+ let lastHeartbeat = 0;
765
+ const heartbeat = async () => {
766
+ if (Date.now() - lastHeartbeat < AGENT_PRESENCE.heartbeatSeconds * 1e3) {
767
+ return;
768
+ }
769
+ lastHeartbeat = Date.now();
770
+ try {
771
+ await client.agentHeartbeat(config.artifactId);
772
+ } catch (err) {
773
+ if (!(opts.json || heartbeatWarned)) {
774
+ heartbeatWarned = true;
775
+ console.error(
776
+ `(presence heartbeat failed \u2014 reviewers won't see "Agent is listening": ${err instanceof Error ? err.message : err})`
777
+ );
778
+ }
779
+ }
780
+ };
781
+ await heartbeat();
782
+ while (Date.now() < deadline) {
783
+ await sleep(Math.min(pollSeconds(), (deadline - Date.now()) / 1e3));
784
+ await heartbeat();
785
+ const data = await client.exportComments(config.artifactId, query);
786
+ if (diffThreads(baseline, data).length === 0) {
787
+ continue;
788
+ }
789
+ await sleep(settleSeconds());
790
+ const settled = await client.exportComments(config.artifactId, query);
791
+ const changed = diffThreads(baseline, settled);
792
+ printWaitResult(config, settled, changed, opts.json);
793
+ return;
794
+ }
795
+ if (opts.json) {
796
+ console.log(
797
+ JSON.stringify(
798
+ {
799
+ artifactId: config.artifactId,
800
+ threads: [],
801
+ timedOut: true,
802
+ versionNumber: baselineExport.versionNumber
803
+ },
804
+ null,
805
+ 2
806
+ )
807
+ );
808
+ } else {
809
+ console.log(
810
+ `No new feedback after ${opts.timeoutSeconds}s. Run the command again to keep waiting.`
811
+ );
812
+ }
813
+ process.exit(WAIT_TIMEOUT_EXIT);
814
+ }
815
+ function printWaitResult(config, data, changed, json) {
816
+ if (json) {
817
+ console.log(
818
+ JSON.stringify(
819
+ {
820
+ artifactId: config.artifactId,
821
+ threads: changed,
822
+ timedOut: false,
823
+ versionNumber: data.versionNumber
824
+ },
825
+ null,
826
+ 2
827
+ )
828
+ );
829
+ return;
830
+ }
831
+ const plural = changed.length === 1 ? "" : "s";
832
+ console.log(
833
+ `
834
+ ${changed.length} thread${plural} with new feedback \u2014 ${config.url}
835
+ `
836
+ );
837
+ for (const thread of changed) {
838
+ console.log(thread.change === "new" ? "NEW" : "UPDATED");
839
+ printThread(thread);
840
+ }
570
841
  }
571
842
  function printHuman(data, url, status, version) {
572
843
  const statusLabel = status === "all" ? "" : `${status} `;
@@ -589,173 +860,31 @@ function printHuman(data, url, status, version) {
589
860
  }
590
861
  function printThread(t) {
591
862
  const marker = t.status === "resolved" ? "\u2713" : "\u25CF";
592
- const date = t.createdAt.slice(0, 10);
593
- console.log(
594
- `${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`
595
- );
596
- if (t.anchor.type === "text") {
597
- console.log(` \u21B3 "${t.anchor.quote}"`);
598
- } else if (t.anchor.element) {
599
- const el = t.anchor.element;
600
- const text = el.text ? ` "${el.text}"` : "";
601
- console.log(` \u21B3 at ${elementLabel(el)}${text} \u2014 ${t.anchor.elementPath}`);
602
- } else {
603
- console.log(` \u21B3 at ${t.anchor.elementPath}`);
604
- }
605
- console.log(indent(t.body, " "));
606
- for (const reply of t.replies) {
607
- console.log(indent(`${reply.author}: ${reply.body}`, " "));
608
- }
609
- console.log("");
610
- }
611
- function indent(text, pad) {
612
- return text.split("\n").map((line) => pad + line).join("\n");
613
- }
614
-
615
- // src/errors.ts
616
- function printCliError(err, json) {
617
- if (err instanceof ApiRequestError) {
618
- if (json) {
619
- console.log(
620
- JSON.stringify({
621
- error: {
622
- code: err.code,
623
- message: err.message,
624
- retryAfter: err.retryAfter
625
- }
626
- })
627
- );
628
- } else {
629
- console.error(`\u2717 ${err.message} (${err.code})`);
630
- if (err.retryAfter) {
631
- console.error(` Retry in ${err.retryAfter}s.`);
632
- }
633
- }
634
- return;
635
- }
636
- const message = err instanceof Error ? err.message : String(err);
637
- if (json) {
638
- console.log(JSON.stringify({ error: { code: "cli_error", message } }));
639
- } else {
640
- console.error(`\u2717 ${message}`);
641
- }
642
- }
643
-
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`;
863
+ const date = t.createdAt.slice(0, 10);
864
+ console.log(
865
+ `${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`
866
+ );
867
+ if (t.anchor.type === "text") {
868
+ console.log(` \u21B3 "${t.anchor.quote}"`);
869
+ } else if (t.anchor.element) {
870
+ const el = t.anchor.element;
871
+ const text = el.text ? ` "${el.text}"` : "";
872
+ console.log(` \u21B3 at ${elementLabel(el)}${text} \u2014 ${t.anchor.elementPath}`);
873
+ } else {
874
+ console.log(` \u21B3 at ${t.anchor.elementPath}`);
750
875
  }
751
- if (seconds < 30 * 86400) {
752
- return `${Math.floor(seconds / 86400)}d ago`;
876
+ console.log(indent(t.body, " "));
877
+ for (const reply of t.replies) {
878
+ console.log(indent(`${reply.author}: ${reply.body}`, " "));
753
879
  }
754
- return iso.slice(0, 10);
880
+ console.log("");
881
+ }
882
+ function indent(text, pad) {
883
+ return text.split("\n").map((line) => pad + line).join("\n");
755
884
  }
756
885
 
757
- // src/login.ts
758
- import { createInterface } from "readline";
886
+ // src/dashboard.ts
887
+ import path6 from "path";
759
888
 
760
889
  // src/publish.ts
761
890
  import { exec } from "child_process";
@@ -1053,13 +1182,205 @@ function openInBrowser(url) {
1053
1182
  });
1054
1183
  }
1055
1184
 
1185
+ // src/dashboard.ts
1186
+ function openDashboard(opts) {
1187
+ const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path6.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
1188
+ const url = `${base.replace(/\/$/, "")}/dashboard`;
1189
+ console.log(url);
1190
+ if (process.stdout.isTTY) {
1191
+ openInBrowser(url);
1192
+ }
1193
+ }
1194
+
1195
+ // src/errors.ts
1196
+ function printCliError(err, json) {
1197
+ if (err instanceof ApiRequestError) {
1198
+ if (json) {
1199
+ console.log(
1200
+ JSON.stringify({
1201
+ error: {
1202
+ code: err.code,
1203
+ message: err.message,
1204
+ retryAfter: err.retryAfter
1205
+ }
1206
+ })
1207
+ );
1208
+ } else {
1209
+ console.error(`\u2717 ${err.message} (${err.code})`);
1210
+ if (err.retryAfter) {
1211
+ console.error(` Retry in ${err.retryAfter}s.`);
1212
+ }
1213
+ }
1214
+ return;
1215
+ }
1216
+ const message = err instanceof Error ? err.message : String(err);
1217
+ if (json) {
1218
+ console.log(JSON.stringify({ error: { code: "cli_error", message } }));
1219
+ } else {
1220
+ console.error(`\u2717 ${message}`);
1221
+ }
1222
+ }
1223
+
1224
+ // src/link.ts
1225
+ import path7 from "path";
1226
+ import { createInterface } from "readline";
1227
+ async function link(opts) {
1228
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1229
+ /\/$/,
1230
+ ""
1231
+ );
1232
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1233
+ if (!key) {
1234
+ throw new Error(
1235
+ `Linking needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1236
+ );
1237
+ }
1238
+ const { artifacts } = await createApiClient(apiUrl2, key).listArtifacts();
1239
+ if (artifacts.length === 0) {
1240
+ throw new Error(
1241
+ "No artifacts in your account yet \u2014 publish one first, or claim one via its claim link."
1242
+ );
1243
+ }
1244
+ const selector = opts.artifact ?? opts.slug;
1245
+ let chosen;
1246
+ if (opts.artifact) {
1247
+ chosen = artifacts.find((a) => a.id === opts.artifact);
1248
+ } else if (opts.slug) {
1249
+ chosen = artifacts.find((a) => a.slug === opts.slug);
1250
+ }
1251
+ if (selector && !chosen) {
1252
+ throw new Error(
1253
+ `No artifact "${selector}" in your account \u2014 run \`artifact-cafe list\` to see them.`
1254
+ );
1255
+ }
1256
+ if (opts.json && !chosen) {
1257
+ console.log(JSON.stringify({ artifacts }, null, 2));
1258
+ return;
1259
+ }
1260
+ if (!chosen) {
1261
+ if (!process.stdin.isTTY) {
1262
+ throw new Error(
1263
+ "Pick an artifact with --artifact <id> or --slug <slug> (or --json to list them). Interactive selection needs a terminal."
1264
+ );
1265
+ }
1266
+ chosen = await pickArtifact(artifacts);
1267
+ }
1268
+ const dir = path7.resolve(opts.dir);
1269
+ writeConfig(dir, {
1270
+ apiUrl: apiUrl2,
1271
+ artifactId: chosen.id,
1272
+ slug: chosen.slug,
1273
+ url: chosen.url
1274
+ // No publishToken — this folder authorizes via your login, not a secret.
1275
+ });
1276
+ if (opts.json) {
1277
+ console.log(
1278
+ JSON.stringify(
1279
+ {
1280
+ artifactId: chosen.id,
1281
+ linked: true,
1282
+ slug: chosen.slug,
1283
+ url: chosen.url
1284
+ },
1285
+ null,
1286
+ 2
1287
+ )
1288
+ );
1289
+ return;
1290
+ }
1291
+ console.log(`
1292
+ \u2713 Linked ${dir} \u2192 "${chosen.title}" \u2014 ${chosen.url}`);
1293
+ console.log(
1294
+ " Owner actions (publish, comments --wait, protect) use your login.\n"
1295
+ );
1296
+ }
1297
+ function pickArtifact(artifacts) {
1298
+ console.log("Your artifacts:\n");
1299
+ artifacts.forEach((a, i) => {
1300
+ const v = a.currentVersionNumber ? `v${a.currentVersionNumber}` : "\u2014";
1301
+ console.log(
1302
+ ` ${String(i + 1).padStart(2)}. ${a.title} \xB7 ${v} \xB7 ${a.url}`
1303
+ );
1304
+ });
1305
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1306
+ return new Promise((resolve, reject) => {
1307
+ rl.question(`
1308
+ Link which? [1-${artifacts.length}]: `, (answer) => {
1309
+ rl.close();
1310
+ const n = Number(answer.trim());
1311
+ if (!(Number.isInteger(n) && n >= 1 && n <= artifacts.length)) {
1312
+ reject(new Error(`Enter a number between 1 and ${artifacts.length}.`));
1313
+ return;
1314
+ }
1315
+ resolve(artifacts[n - 1]);
1316
+ });
1317
+ });
1318
+ }
1319
+
1320
+ // src/list.ts
1321
+ async function list(opts) {
1322
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1323
+ /\/$/,
1324
+ ""
1325
+ );
1326
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1327
+ if (!key) {
1328
+ throw new Error(
1329
+ `Listing your artifacts needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1330
+ );
1331
+ }
1332
+ const data = await createApiClient(apiUrl2, key).listArtifacts();
1333
+ if (opts.json) {
1334
+ console.log(JSON.stringify(data, null, 2));
1335
+ return;
1336
+ }
1337
+ if (data.artifacts.length === 0) {
1338
+ console.log(
1339
+ "No artifacts in your account yet \u2014 publish while logged in, or claim one via its claim link."
1340
+ );
1341
+ return;
1342
+ }
1343
+ const plural = data.artifacts.length === 1 ? "" : "s";
1344
+ console.log(`${data.artifacts.length} artifact${plural}
1345
+ `);
1346
+ for (const artifact of data.artifacts) {
1347
+ printArtifact(artifact);
1348
+ }
1349
+ }
1350
+ function printArtifact(a) {
1351
+ const version = a.currentVersionNumber === null ? "no version" : `v${a.currentVersionNumber}`;
1352
+ const open2 = a.openCommentCount === 0 ? "no open comments" : `${a.openCommentCount} open comment${a.openCommentCount === 1 ? "" : "s"}`;
1353
+ const marker = a.openCommentCount > 0 ? "\u25CF" : "\u25CB";
1354
+ console.log(`${marker} ${a.title}`);
1355
+ console.log(` ${version} \xB7 ${open2} \xB7 updated ${relativeTime(a.updatedAt)}`);
1356
+ console.log(` ${a.url}
1357
+ `);
1358
+ }
1359
+ function relativeTime(iso) {
1360
+ const seconds = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1e3);
1361
+ if (seconds < 60) {
1362
+ return "just now";
1363
+ }
1364
+ if (seconds < 3600) {
1365
+ return `${Math.floor(seconds / 60)}m ago`;
1366
+ }
1367
+ if (seconds < 86400) {
1368
+ return `${Math.floor(seconds / 3600)}h ago`;
1369
+ }
1370
+ if (seconds < 30 * 86400) {
1371
+ return `${Math.floor(seconds / 86400)}d ago`;
1372
+ }
1373
+ return iso.slice(0, 10);
1374
+ }
1375
+
1056
1376
  // src/login.ts
1377
+ import { createInterface as createInterface2 } from "readline";
1057
1378
  function apiUrl() {
1058
1379
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1059
1380
  }
1060
1381
  function promptSecret(question) {
1061
1382
  return new Promise((resolve) => {
1062
- const rl = createInterface({
1383
+ const rl = createInterface2({
1063
1384
  input: process.stdin,
1064
1385
  output: process.stdout
1065
1386
  });
@@ -1167,23 +1488,27 @@ function logout(opts) {
1167
1488
  }
1168
1489
 
1169
1490
  // src/open.ts
1170
- import path6 from "path";
1171
- function openArtifact(opts) {
1172
- const dir = path6.resolve(opts.dir);
1173
- const config = readConfig(dir);
1174
- if (!config) {
1175
- throw new Error(
1176
- `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
1177
- );
1178
- }
1179
- console.log(config.url);
1491
+ import path8 from "path";
1492
+ async function openArtifact(opts) {
1493
+ const dir = path8.resolve(opts.dir);
1494
+ const target = resolveArtifactTarget(dir, {
1495
+ apiKey: opts.apiKey,
1496
+ artifact: opts.artifact
1497
+ });
1498
+ const url = target.url ?? reviewUrl(
1499
+ target.apiUrl,
1500
+ (await createApiClient(target.apiUrl, target.credential).getArtifact(
1501
+ target.artifactId
1502
+ )).slug
1503
+ );
1504
+ console.log(url);
1180
1505
  if (process.stdout.isTTY) {
1181
- openInBrowser(config.url);
1506
+ openInBrowser(url);
1182
1507
  }
1183
1508
  }
1184
1509
 
1185
1510
  // src/protect.ts
1186
- import path7 from "path";
1511
+ import path9 from "path";
1187
1512
  async function protect(opts) {
1188
1513
  if (opts.off && opts.password !== void 0) {
1189
1514
  throw new Error("Pass either --password or --off, not both.");
@@ -1200,26 +1525,26 @@ async function protect(opts) {
1200
1525
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
1201
1526
  }
1202
1527
  }
1203
- const dir = path7.resolve(opts.dir);
1204
- const config = readConfig(dir);
1205
- if (!config) {
1206
- throw new Error(
1207
- `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
1208
- );
1209
- }
1210
- const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
1211
- const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
1212
- const client = createApiClient(apiUrl2, token);
1213
- const res = await client.setProtection(config.artifactId, password);
1528
+ const dir = path9.resolve(opts.dir);
1529
+ const target = resolveArtifactTarget(dir, {
1530
+ apiKey: opts.apiKey,
1531
+ artifact: opts.artifact
1532
+ });
1533
+ const client = createApiClient(target.apiUrl, target.credential);
1534
+ const res = await client.setProtection(target.artifactId, password);
1214
1535
  if (opts.json) {
1215
1536
  console.log(JSON.stringify({ protected: res.protected }, null, 2));
1216
1537
  return;
1217
1538
  }
1539
+ const url = target.url ?? reviewUrl(
1540
+ target.apiUrl,
1541
+ (await client.getArtifact(target.artifactId)).slug
1542
+ );
1218
1543
  console.log(
1219
1544
  res.protected ? `
1220
- \u2713 Password protection on \u2014 ${config.url}
1545
+ \u2713 Password protection on \u2014 ${url}
1221
1546
  ` : `
1222
- \u2713 Password protection removed \u2014 ${config.url}
1547
+ \u2713 Password protection removed \u2014 ${url}
1223
1548
  `
1224
1549
  );
1225
1550
  }
@@ -1344,8 +1669,56 @@ var list2 = defineCommand({
1344
1669
  }
1345
1670
  }
1346
1671
  });
1672
+ var link2 = defineCommand({
1673
+ args: {
1674
+ "api-key": {
1675
+ description: "Link with this API key (default: stored login)",
1676
+ type: "string"
1677
+ },
1678
+ artifact: {
1679
+ description: "Link this artifact id (skip the picker)",
1680
+ type: "string"
1681
+ },
1682
+ dir: {
1683
+ default: ".",
1684
+ description: "Directory to link",
1685
+ type: "positional"
1686
+ },
1687
+ json: { description: "Machine-readable output", type: "boolean" },
1688
+ slug: {
1689
+ description: "Link this artifact slug (skip the picker)",
1690
+ type: "string"
1691
+ }
1692
+ },
1693
+ meta: {
1694
+ description: "Link a folder to one of your artifacts (pick from your account)",
1695
+ name: "link"
1696
+ },
1697
+ async run({ args }) {
1698
+ try {
1699
+ await link({
1700
+ apiKey: args["api-key"],
1701
+ artifact: args.artifact,
1702
+ dir: args.dir,
1703
+ json: Boolean(args.json),
1704
+ slug: args.slug
1705
+ });
1706
+ } catch (err) {
1707
+ printCliError(err, Boolean(args.json));
1708
+ process.exit(1);
1709
+ }
1710
+ }
1711
+ });
1347
1712
  var comments2 = defineCommand({
1348
1713
  args: {
1714
+ "api-key": {
1715
+ description: "Authorize with an account API key (default: stored login)",
1716
+ type: "string"
1717
+ },
1718
+ artifact: {
1719
+ description: "Act on an artifact ID instead of the linked folder",
1720
+ type: "string"
1721
+ },
1349
1722
  dir: {
1350
1723
  default: ".",
1351
1724
  description: "Published directory",
@@ -1357,10 +1730,19 @@ var comments2 = defineCommand({
1357
1730
  description: "Filter: open | resolved | all",
1358
1731
  type: "string"
1359
1732
  },
1733
+ timeout: {
1734
+ default: "900",
1735
+ description: "With --wait: give up after this many seconds (exit 2)",
1736
+ type: "string"
1737
+ },
1360
1738
  version: {
1361
1739
  default: "current",
1362
1740
  description: "Filter: current | all",
1363
1741
  type: "string"
1742
+ },
1743
+ wait: {
1744
+ description: "Block until a reviewer leaves new feedback",
1745
+ type: "boolean"
1364
1746
  }
1365
1747
  },
1366
1748
  meta: {
@@ -1369,11 +1751,21 @@ var comments2 = defineCommand({
1369
1751
  },
1370
1752
  async run({ args }) {
1371
1753
  try {
1754
+ const timeout = Number(args.timeout);
1755
+ if (!Number.isFinite(timeout) || timeout <= 0) {
1756
+ throw new Error(
1757
+ `--timeout must be a positive number of seconds (got "${args.timeout}").`
1758
+ );
1759
+ }
1372
1760
  await comments({
1761
+ apiKey: args["api-key"],
1762
+ artifact: args.artifact,
1373
1763
  dir: args.dir,
1374
1764
  json: Boolean(args.json),
1375
1765
  status: args.status,
1376
- version: args.version
1766
+ timeout,
1767
+ version: args.version,
1768
+ wait: Boolean(args.wait)
1377
1769
  });
1378
1770
  } catch (err) {
1379
1771
  printCliError(err, Boolean(args.json));
@@ -1383,6 +1775,14 @@ var comments2 = defineCommand({
1383
1775
  });
1384
1776
  var protect2 = defineCommand({
1385
1777
  args: {
1778
+ "api-key": {
1779
+ description: "Authorize with an account API key (default: stored login)",
1780
+ type: "string"
1781
+ },
1782
+ artifact: {
1783
+ description: "Act on an artifact ID instead of the linked folder",
1784
+ type: "string"
1785
+ },
1386
1786
  dir: {
1387
1787
  default: ".",
1388
1788
  description: "Published directory",
@@ -1399,6 +1799,8 @@ var protect2 = defineCommand({
1399
1799
  async run({ args }) {
1400
1800
  try {
1401
1801
  await protect({
1802
+ apiKey: args["api-key"],
1803
+ artifact: args.artifact,
1402
1804
  dir: args.dir,
1403
1805
  json: Boolean(args.json),
1404
1806
  off: Boolean(args.off),
@@ -1412,6 +1814,14 @@ var protect2 = defineCommand({
1412
1814
  });
1413
1815
  var open = defineCommand({
1414
1816
  args: {
1817
+ "api-key": {
1818
+ description: "Authorize with an account API key (default: stored login)",
1819
+ type: "string"
1820
+ },
1821
+ artifact: {
1822
+ description: "Open an artifact ID instead of the linked folder",
1823
+ type: "string"
1824
+ },
1415
1825
  dir: {
1416
1826
  default: ".",
1417
1827
  description: "Published directory",
@@ -1419,9 +1829,34 @@ var open = defineCommand({
1419
1829
  }
1420
1830
  },
1421
1831
  meta: { description: "Open the linked artifact's review URL", name: "open" },
1832
+ async run({ args }) {
1833
+ try {
1834
+ await openArtifact({
1835
+ apiKey: args["api-key"],
1836
+ artifact: args.artifact,
1837
+ dir: args.dir
1838
+ });
1839
+ } catch (err) {
1840
+ printCliError(err, false);
1841
+ process.exit(1);
1842
+ }
1843
+ }
1844
+ });
1845
+ var dashboard = defineCommand({
1846
+ args: {
1847
+ dir: {
1848
+ default: ".",
1849
+ description: "Directory whose linked instance to open (optional)",
1850
+ type: "positional"
1851
+ }
1852
+ },
1853
+ meta: {
1854
+ description: "Open your personal dashboard in the browser",
1855
+ name: "dashboard"
1856
+ },
1422
1857
  run({ args }) {
1423
1858
  try {
1424
- openArtifact({ dir: args.dir });
1859
+ openDashboard({ dir: args.dir });
1425
1860
  } catch (err) {
1426
1861
  printCliError(err, false);
1427
1862
  process.exit(1);
@@ -1433,8 +1868,21 @@ var main = defineCommand({
1433
1868
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1434
1869
  name: "artifact-cafe"
1435
1870
  },
1871
+ // Bare `artifact-cafe` (no subcommand) → brand banner + usage. citty
1872
+ // still calls this parent run *after* a matched subcommand, so bail when
1873
+ // a subcommand token is present (any non-flag arg; citty has already
1874
+ // rejected unknown ones with E_UNKNOWN_COMMAND before we get here).
1875
+ async run({ cmd, rawArgs }) {
1876
+ if (rawArgs.some((arg) => !arg.startsWith("-"))) {
1877
+ return;
1878
+ }
1879
+ printBanner();
1880
+ await showUsage(cmd);
1881
+ },
1436
1882
  subCommands: {
1437
1883
  comments: comments2,
1884
+ dashboard,
1885
+ link: link2,
1438
1886
  list: list2,
1439
1887
  login: login2,
1440
1888
  logout: logout2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-cafe",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Publish AI-generated artifacts for review on artifact.cafe",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,19 +9,19 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
- "scripts": {
13
- "build": "tsup",
14
- "typecheck": "tsc --noEmit",
15
- "dev": "tsup --watch"
16
- },
17
12
  "dependencies": {
18
13
  "citty": "^0.1.6",
19
14
  "zod": "^4.1.0"
20
15
  },
21
16
  "devDependencies": {
22
- "@artifact-cafe/shared": "workspace:*",
23
17
  "@types/node": "^24.0.0",
24
18
  "tsup": "^8.5.0",
25
- "typescript": "^5.9.0"
19
+ "typescript": "^5.9.0",
20
+ "@artifact-cafe/shared": "0.1.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "typecheck": "tsc --noEmit",
25
+ "dev": "tsup --watch"
26
26
  }
27
- }
27
+ }