artifact-cafe 0.0.2 → 0.0.4
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.
- package/dist/index.js +869 -223
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,102 +6,149 @@ import { defineCommand, runMain } from "citty";
|
|
|
6
6
|
// src/comments.ts
|
|
7
7
|
import path2 from "path";
|
|
8
8
|
|
|
9
|
-
// ../shared/src/
|
|
9
|
+
// ../shared/src/account.ts
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
var createApiKeySchema = z.object({
|
|
12
|
+
name: z.string().trim().min(1).max(60).optional()
|
|
13
|
+
});
|
|
14
|
+
var createdApiKeySchema = z.object({
|
|
15
|
+
createdAt: z.string(),
|
|
16
|
+
id: z.string(),
|
|
17
|
+
key: z.string(),
|
|
18
|
+
keyPrefix: z.string(),
|
|
19
|
+
name: z.string()
|
|
20
|
+
});
|
|
21
|
+
var apiKeySummarySchema = z.object({
|
|
22
|
+
createdAt: z.string(),
|
|
23
|
+
id: z.string(),
|
|
24
|
+
keyPrefix: z.string(),
|
|
25
|
+
lastUsedAt: z.string().nullable(),
|
|
26
|
+
name: z.string()
|
|
27
|
+
});
|
|
28
|
+
var apiKeyListResponseSchema = z.object({
|
|
29
|
+
keys: z.array(apiKeySummarySchema)
|
|
30
|
+
});
|
|
31
|
+
var revokeApiKeyResponseSchema = z.object({
|
|
32
|
+
revoked: z.literal(true)
|
|
16
33
|
});
|
|
17
|
-
var
|
|
34
|
+
var meResponseSchema = z.object({
|
|
35
|
+
// Best-effort profile field from our own user table; null when unknown.
|
|
36
|
+
email: z.string().nullable(),
|
|
37
|
+
// Present when the request was authenticated with an API key.
|
|
38
|
+
keyName: z.string().optional(),
|
|
39
|
+
userId: z.string()
|
|
40
|
+
});
|
|
41
|
+
var artifactListItemSchema = z.object({
|
|
42
|
+
createdAt: z.string(),
|
|
43
|
+
currentVersionNumber: z.number().int().positive().nullable(),
|
|
44
|
+
id: z.string(),
|
|
45
|
+
openCommentCount: z.number().int().nonnegative(),
|
|
46
|
+
slug: z.string(),
|
|
47
|
+
title: z.string(),
|
|
48
|
+
updatedAt: z.string(),
|
|
49
|
+
url: z.string().url(),
|
|
50
|
+
versionCount: z.number().int().nonnegative()
|
|
51
|
+
});
|
|
52
|
+
var artifactListResponseSchema = z.object({
|
|
53
|
+
artifacts: z.array(artifactListItemSchema)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ../shared/src/comments.ts
|
|
57
|
+
import { z as z2 } from "zod";
|
|
58
|
+
var elementSnapshotSchema = z2.object({
|
|
59
|
+
classes: z2.string().max(256).optional(),
|
|
60
|
+
id: z2.string().max(256).optional(),
|
|
61
|
+
tag: z2.string().min(1).max(128),
|
|
62
|
+
text: z2.string().max(200).optional()
|
|
63
|
+
});
|
|
64
|
+
var pointAnchorSchema = z2.object({
|
|
18
65
|
element: elementSnapshotSchema.optional(),
|
|
19
66
|
// elementPath is the primary anchor; x/y are offsets within the
|
|
20
67
|
// element's box, normalized 0..1 (SPEC.md §8.1).
|
|
21
|
-
elementPath:
|
|
22
|
-
type:
|
|
23
|
-
viewportHeight:
|
|
24
|
-
viewportWidth:
|
|
25
|
-
x:
|
|
26
|
-
y:
|
|
27
|
-
});
|
|
28
|
-
var textAnchorSchema =
|
|
29
|
-
elementPath:
|
|
30
|
-
endOffset:
|
|
31
|
-
prefix:
|
|
32
|
-
quote:
|
|
33
|
-
startOffset:
|
|
34
|
-
suffix:
|
|
35
|
-
type:
|
|
36
|
-
});
|
|
37
|
-
var anchorSchema =
|
|
68
|
+
elementPath: z2.string().max(2048),
|
|
69
|
+
type: z2.literal("point"),
|
|
70
|
+
viewportHeight: z2.number().int().positive().max(1e5),
|
|
71
|
+
viewportWidth: z2.number().int().positive().max(1e5),
|
|
72
|
+
x: z2.number().min(0).max(1),
|
|
73
|
+
y: z2.number().min(0).max(1)
|
|
74
|
+
});
|
|
75
|
+
var textAnchorSchema = z2.object({
|
|
76
|
+
elementPath: z2.string().max(2048),
|
|
77
|
+
endOffset: z2.number().int().nonnegative().max(1e8),
|
|
78
|
+
prefix: z2.string().max(200).optional(),
|
|
79
|
+
quote: z2.string().min(1).max(1e3),
|
|
80
|
+
startOffset: z2.number().int().nonnegative().max(1e8),
|
|
81
|
+
suffix: z2.string().max(200).optional(),
|
|
82
|
+
type: z2.literal("text")
|
|
83
|
+
});
|
|
84
|
+
var anchorSchema = z2.discriminatedUnion("type", [
|
|
38
85
|
pointAnchorSchema,
|
|
39
86
|
textAnchorSchema
|
|
40
87
|
]);
|
|
41
|
-
var guestSchema =
|
|
42
|
-
displayName:
|
|
43
|
-
guestId:
|
|
88
|
+
var guestSchema = z2.object({
|
|
89
|
+
displayName: z2.string().min(1).max(64),
|
|
90
|
+
guestId: z2.string().min(16)
|
|
44
91
|
});
|
|
45
|
-
var createCommentThreadSchema =
|
|
92
|
+
var createCommentThreadSchema = z2.object({
|
|
46
93
|
anchor: anchorSchema,
|
|
47
|
-
body:
|
|
94
|
+
body: z2.string().min(1).max(1e4),
|
|
48
95
|
guest: guestSchema.optional(),
|
|
49
|
-
versionId:
|
|
96
|
+
versionId: z2.string()
|
|
50
97
|
});
|
|
51
|
-
var createCommentMessageSchema =
|
|
52
|
-
body:
|
|
98
|
+
var createCommentMessageSchema = z2.object({
|
|
99
|
+
body: z2.string().min(1).max(1e4),
|
|
53
100
|
guest: guestSchema.optional()
|
|
54
101
|
});
|
|
55
|
-
var threadActionSchema =
|
|
102
|
+
var threadActionSchema = z2.object({
|
|
56
103
|
guest: guestSchema.optional()
|
|
57
104
|
});
|
|
58
|
-
var commentAuthorSchema =
|
|
59
|
-
displayName:
|
|
60
|
-
guestId:
|
|
105
|
+
var commentAuthorSchema = z2.object({
|
|
106
|
+
displayName: z2.string(),
|
|
107
|
+
guestId: z2.string().nullable()
|
|
61
108
|
});
|
|
62
|
-
var commentMessageSchema =
|
|
109
|
+
var commentMessageSchema = z2.object({
|
|
63
110
|
author: commentAuthorSchema,
|
|
64
|
-
body:
|
|
65
|
-
createdAt:
|
|
66
|
-
id:
|
|
111
|
+
body: z2.string(),
|
|
112
|
+
createdAt: z2.string(),
|
|
113
|
+
id: z2.string()
|
|
67
114
|
});
|
|
68
|
-
var commentThreadSchema =
|
|
115
|
+
var commentThreadSchema = z2.object({
|
|
69
116
|
anchor: anchorSchema,
|
|
70
|
-
artifactId:
|
|
71
|
-
createdAt:
|
|
117
|
+
artifactId: z2.string(),
|
|
118
|
+
createdAt: z2.string(),
|
|
72
119
|
createdBy: commentAuthorSchema,
|
|
73
|
-
id:
|
|
74
|
-
messages:
|
|
75
|
-
status:
|
|
76
|
-
versionId:
|
|
120
|
+
id: z2.string(),
|
|
121
|
+
messages: z2.array(commentMessageSchema),
|
|
122
|
+
status: z2.enum(["open", "resolved"]),
|
|
123
|
+
versionId: z2.string()
|
|
77
124
|
});
|
|
78
|
-
var commentThreadListSchema =
|
|
79
|
-
threads:
|
|
125
|
+
var commentThreadListSchema = z2.object({
|
|
126
|
+
threads: z2.array(commentThreadSchema)
|
|
80
127
|
});
|
|
81
|
-
var commentExportQuerySchema =
|
|
82
|
-
status:
|
|
83
|
-
version:
|
|
128
|
+
var commentExportQuerySchema = z2.object({
|
|
129
|
+
status: z2.enum(["open", "resolved", "all"]).default("open"),
|
|
130
|
+
version: z2.enum(["current", "all"]).default("current")
|
|
84
131
|
});
|
|
85
|
-
var commentExportReplySchema =
|
|
86
|
-
author:
|
|
87
|
-
body:
|
|
88
|
-
createdAt:
|
|
132
|
+
var commentExportReplySchema = z2.object({
|
|
133
|
+
author: z2.string(),
|
|
134
|
+
body: z2.string(),
|
|
135
|
+
createdAt: z2.string()
|
|
89
136
|
});
|
|
90
|
-
var commentExportThreadSchema =
|
|
137
|
+
var commentExportThreadSchema = z2.object({
|
|
91
138
|
anchor: anchorSchema,
|
|
92
|
-
author:
|
|
93
|
-
body:
|
|
94
|
-
createdAt:
|
|
95
|
-
id:
|
|
96
|
-
replies:
|
|
97
|
-
status:
|
|
98
|
-
versionNumber:
|
|
139
|
+
author: z2.string(),
|
|
140
|
+
body: z2.string(),
|
|
141
|
+
createdAt: z2.string(),
|
|
142
|
+
id: z2.string(),
|
|
143
|
+
replies: z2.array(commentExportReplySchema),
|
|
144
|
+
status: z2.enum(["open", "resolved"]),
|
|
145
|
+
versionNumber: z2.number().int().positive()
|
|
99
146
|
});
|
|
100
|
-
var commentExportSchema =
|
|
101
|
-
artifactId:
|
|
102
|
-
comments:
|
|
147
|
+
var commentExportSchema = z2.object({
|
|
148
|
+
artifactId: z2.string(),
|
|
149
|
+
comments: z2.array(commentExportThreadSchema),
|
|
103
150
|
// The artifact's current version (null before the first finalize).
|
|
104
|
-
versionNumber:
|
|
151
|
+
versionNumber: z2.number().int().positive().nullable()
|
|
105
152
|
});
|
|
106
153
|
function elementLabel(el) {
|
|
107
154
|
if (el.id) {
|
|
@@ -112,55 +159,90 @@ function elementLabel(el) {
|
|
|
112
159
|
}
|
|
113
160
|
|
|
114
161
|
// ../shared/src/errors.ts
|
|
115
|
-
import { z as
|
|
116
|
-
var apiErrorSchema =
|
|
117
|
-
code:
|
|
118
|
-
docsUrl:
|
|
119
|
-
message:
|
|
120
|
-
retryAfter:
|
|
162
|
+
import { z as z3 } from "zod";
|
|
163
|
+
var apiErrorSchema = z3.object({
|
|
164
|
+
code: z3.string(),
|
|
165
|
+
docsUrl: z3.string().url(),
|
|
166
|
+
message: z3.string(),
|
|
167
|
+
retryAfter: z3.number().int().positive().optional()
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ../shared/src/me.ts
|
|
171
|
+
import { z as z4 } from "zod";
|
|
172
|
+
var ownArtifactSchema = z4.object({
|
|
173
|
+
createdAt: z4.string(),
|
|
174
|
+
currentVersionNumber: z4.number().nullable(),
|
|
175
|
+
id: z4.string(),
|
|
176
|
+
openThreads: z4.number(),
|
|
177
|
+
protected: z4.boolean(),
|
|
178
|
+
slug: z4.string(),
|
|
179
|
+
title: z4.string(),
|
|
180
|
+
totalViews: z4.number(),
|
|
181
|
+
updatedAt: z4.string(),
|
|
182
|
+
url: z4.string(),
|
|
183
|
+
versionCount: z4.number()
|
|
184
|
+
});
|
|
185
|
+
var ownArtifactsResponseSchema = z4.object({
|
|
186
|
+
artifacts: z4.array(ownArtifactSchema)
|
|
121
187
|
});
|
|
122
188
|
|
|
123
189
|
// ../shared/src/owner.ts
|
|
124
|
-
import { z as
|
|
190
|
+
import { z as z5 } from "zod";
|
|
125
191
|
var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
|
|
126
|
-
var claimResponseSchema =
|
|
127
|
-
artifactId:
|
|
128
|
-
slug:
|
|
129
|
-
title:
|
|
130
|
-
url:
|
|
131
|
-
});
|
|
132
|
-
var updateArtifactSchema =
|
|
133
|
-
slug:
|
|
192
|
+
var claimResponseSchema = z5.object({
|
|
193
|
+
artifactId: z5.string(),
|
|
194
|
+
slug: z5.string(),
|
|
195
|
+
title: z5.string(),
|
|
196
|
+
url: z5.string().url()
|
|
197
|
+
});
|
|
198
|
+
var updateArtifactSchema = z5.object({
|
|
199
|
+
slug: z5.string().min(3).max(63).regex(SLUG_PATTERN, {
|
|
134
200
|
message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
|
|
135
201
|
}).optional(),
|
|
136
|
-
title:
|
|
202
|
+
title: z5.string().min(1).max(200).optional()
|
|
137
203
|
}).refine((body) => body.title !== void 0 || body.slug !== void 0, {
|
|
138
204
|
message: "provide at least one of title, slug"
|
|
139
205
|
});
|
|
140
|
-
var deleteArtifactResponseSchema =
|
|
141
|
-
deleted:
|
|
206
|
+
var deleteArtifactResponseSchema = z5.object({
|
|
207
|
+
deleted: z5.literal(true)
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ../shared/src/presence.ts
|
|
211
|
+
import { z as z6 } from "zod";
|
|
212
|
+
var AGENT_PRESENCE = {
|
|
213
|
+
heartbeatSeconds: 15,
|
|
214
|
+
shellPollSeconds: 15,
|
|
215
|
+
ttlSeconds: 45
|
|
216
|
+
};
|
|
217
|
+
var agentHeartbeatResponseSchema = z6.object({
|
|
218
|
+
listening: z6.literal(true),
|
|
219
|
+
ttlSeconds: z6.number().int().positive()
|
|
220
|
+
});
|
|
221
|
+
var agentPresenceSchema = z6.object({
|
|
222
|
+
listening: z6.boolean(),
|
|
223
|
+
versionNumber: z6.number().int().positive().nullable()
|
|
142
224
|
});
|
|
143
225
|
|
|
144
226
|
// ../shared/src/protection.ts
|
|
145
|
-
import { z as
|
|
227
|
+
import { z as z7 } from "zod";
|
|
146
228
|
var PASSWORD_MIN = 6;
|
|
147
229
|
var PASSWORD_MAX = 128;
|
|
148
|
-
var gatePasswordSchema =
|
|
149
|
-
var setProtectionSchema =
|
|
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({
|
|
150
232
|
password: gatePasswordSchema.nullable()
|
|
151
233
|
});
|
|
152
|
-
var protectionResponseSchema =
|
|
153
|
-
protected:
|
|
234
|
+
var protectionResponseSchema = z7.object({
|
|
235
|
+
protected: z7.boolean()
|
|
154
236
|
});
|
|
155
|
-
var unlockSchema =
|
|
156
|
-
password:
|
|
237
|
+
var unlockSchema = z7.object({
|
|
238
|
+
password: z7.string().min(1).max(1024)
|
|
157
239
|
});
|
|
158
|
-
var unlockResponseSchema =
|
|
159
|
-
unlocked:
|
|
240
|
+
var unlockResponseSchema = z7.object({
|
|
241
|
+
unlocked: z7.literal(true)
|
|
160
242
|
});
|
|
161
243
|
|
|
162
244
|
// ../shared/src/publish.ts
|
|
163
|
-
import { z as
|
|
245
|
+
import { z as z8 } from "zod";
|
|
164
246
|
function isSafeArtifactPath(p) {
|
|
165
247
|
if (p.length === 0 || p.length > 1024) {
|
|
166
248
|
return false;
|
|
@@ -173,95 +255,128 @@ function isSafeArtifactPath(p) {
|
|
|
173
255
|
}
|
|
174
256
|
return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
|
|
175
257
|
}
|
|
176
|
-
var manifestFileSchema =
|
|
177
|
-
hash:
|
|
178
|
-
mimeType:
|
|
179
|
-
path:
|
|
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, {
|
|
180
262
|
message: "path must be a safe relative path"
|
|
181
263
|
}),
|
|
182
|
-
size:
|
|
264
|
+
size: z8.number().int().positive()
|
|
183
265
|
});
|
|
184
|
-
var publishSourceSchema =
|
|
185
|
-
agent:
|
|
186
|
-
runId:
|
|
187
|
-
type:
|
|
266
|
+
var publishSourceSchema = z8.object({
|
|
267
|
+
agent: z8.string().optional(),
|
|
268
|
+
runId: z8.string().optional(),
|
|
269
|
+
type: z8.literal("cli")
|
|
188
270
|
});
|
|
189
|
-
var createPublishSessionSchema =
|
|
190
|
-
artifactId:
|
|
191
|
-
entryPath:
|
|
192
|
-
files:
|
|
271
|
+
var createPublishSessionSchema = z8.object({
|
|
272
|
+
artifactId: z8.string().nullable(),
|
|
273
|
+
entryPath: z8.string().optional(),
|
|
274
|
+
files: z8.array(manifestFileSchema).min(1).max(1e3),
|
|
193
275
|
source: publishSourceSchema,
|
|
194
|
-
title:
|
|
276
|
+
title: z8.string().max(200).optional()
|
|
195
277
|
});
|
|
196
|
-
var publishSessionResponseSchema =
|
|
197
|
-
artifactId:
|
|
198
|
-
expiresAt:
|
|
199
|
-
filesToUpload:
|
|
200
|
-
|
|
201
|
-
method:
|
|
202
|
-
path:
|
|
203
|
-
uploadUrl:
|
|
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()
|
|
204
286
|
})
|
|
205
287
|
),
|
|
206
|
-
publishSessionId:
|
|
207
|
-
skippedFiles:
|
|
208
|
-
versionId:
|
|
288
|
+
publishSessionId: z8.string(),
|
|
289
|
+
skippedFiles: z8.array(z8.string()),
|
|
290
|
+
versionId: z8.string()
|
|
209
291
|
});
|
|
210
|
-
var finalizeResponseSchema =
|
|
211
|
-
artifactId:
|
|
292
|
+
var finalizeResponseSchema = z8.object({
|
|
293
|
+
artifactId: z8.string(),
|
|
294
|
+
// API-key publishes create the artifact already claimed (SPEC §5.4) —
|
|
295
|
+
// the CLI uses this to tell "claim URL pending" from "already yours".
|
|
296
|
+
claimed: z8.boolean(),
|
|
212
297
|
// Returned only on first publish of a new artifact (one-time).
|
|
213
|
-
claimUrl:
|
|
214
|
-
publishToken:
|
|
215
|
-
url:
|
|
216
|
-
versionId:
|
|
217
|
-
versionNumber:
|
|
218
|
-
});
|
|
219
|
-
var artifactResponseSchema =
|
|
220
|
-
claimed:
|
|
221
|
-
currentVersion:
|
|
222
|
-
entryUrl:
|
|
223
|
-
id:
|
|
224
|
-
versionNumber:
|
|
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()
|
|
225
310
|
}).nullable(),
|
|
226
|
-
id:
|
|
227
|
-
slug:
|
|
228
|
-
title:
|
|
311
|
+
id: z8.string(),
|
|
312
|
+
slug: z8.string(),
|
|
313
|
+
title: z8.string()
|
|
229
314
|
});
|
|
230
315
|
|
|
231
316
|
// ../shared/src/report.ts
|
|
232
|
-
import { z as
|
|
233
|
-
var reportRequestSchema =
|
|
234
|
-
reason:
|
|
317
|
+
import { z as z9 } from "zod";
|
|
318
|
+
var reportRequestSchema = z9.object({
|
|
319
|
+
reason: z9.string().max(1e3).optional()
|
|
320
|
+
});
|
|
321
|
+
var reportResponseSchema = z9.object({
|
|
322
|
+
reported: z9.literal(true)
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ../shared/src/stats.ts
|
|
326
|
+
import { z as z10 } from "zod";
|
|
327
|
+
var avgViewSecondsSchema = z10.number().int().nonnegative().nullable();
|
|
328
|
+
var versionViewStatsSchema = z10.object({
|
|
329
|
+
avgViewSeconds: avgViewSecondsSchema,
|
|
330
|
+
// Views from inside the review shell's iframe vs. direct visits to
|
|
331
|
+
// the render URL. views = embeddedViews + directViews.
|
|
332
|
+
directViews: z10.number().int().nonnegative(),
|
|
333
|
+
embeddedViews: z10.number().int().nonnegative(),
|
|
334
|
+
// 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()
|
|
338
|
+
});
|
|
339
|
+
var dailyViewsSchema = z10.object({
|
|
340
|
+
avgViewSeconds: avgViewSecondsSchema,
|
|
341
|
+
// UTC day, YYYY-MM-DD.
|
|
342
|
+
day: z10.string(),
|
|
343
|
+
uniqueViewers: z10.number().int().nonnegative(),
|
|
344
|
+
views: z10.number().int().nonnegative()
|
|
235
345
|
});
|
|
236
|
-
var
|
|
237
|
-
|
|
346
|
+
var artifactStatsResponseSchema = z10.object({
|
|
347
|
+
avgViewSeconds: avgViewSecondsSchema,
|
|
348
|
+
daily: z10.array(dailyViewsSchema),
|
|
349
|
+
// 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)
|
|
238
353
|
});
|
|
239
354
|
|
|
240
355
|
// ../shared/src/versions.ts
|
|
241
|
-
import { z as
|
|
242
|
-
var versionSourceSchema =
|
|
243
|
-
agentName:
|
|
244
|
-
type:
|
|
356
|
+
import { z as z11 } from "zod";
|
|
357
|
+
var versionSourceSchema = z11.object({
|
|
358
|
+
agentName: z11.string().nullable(),
|
|
359
|
+
type: z11.string()
|
|
245
360
|
});
|
|
246
|
-
var versionChangesSchema =
|
|
247
|
-
added:
|
|
248
|
-
changed:
|
|
249
|
-
removed:
|
|
361
|
+
var versionChangesSchema = z11.object({
|
|
362
|
+
added: z11.array(z11.string()),
|
|
363
|
+
changed: z11.array(z11.string()),
|
|
364
|
+
removed: z11.array(z11.string())
|
|
250
365
|
});
|
|
251
|
-
var versionSummarySchema =
|
|
366
|
+
var versionSummarySchema = z11.object({
|
|
252
367
|
// Absent for the first version — there is no previous version to diff against.
|
|
253
368
|
changes: versionChangesSchema.nullable(),
|
|
254
|
-
createdAt:
|
|
255
|
-
entryUrl:
|
|
256
|
-
fileCount:
|
|
257
|
-
id:
|
|
258
|
-
isCurrent:
|
|
369
|
+
createdAt: z11.string(),
|
|
370
|
+
entryUrl: z11.string().url(),
|
|
371
|
+
fileCount: z11.number().int().nonnegative(),
|
|
372
|
+
id: z11.string(),
|
|
373
|
+
isCurrent: z11.boolean(),
|
|
259
374
|
source: versionSourceSchema,
|
|
260
|
-
totalSizeBytes:
|
|
261
|
-
versionNumber:
|
|
375
|
+
totalSizeBytes: z11.number().int().nonnegative(),
|
|
376
|
+
versionNumber: z11.number().int().positive()
|
|
262
377
|
});
|
|
263
|
-
var versionListResponseSchema =
|
|
264
|
-
versions:
|
|
378
|
+
var versionListResponseSchema = z11.object({
|
|
379
|
+
versions: z11.array(versionSummarySchema)
|
|
265
380
|
});
|
|
266
381
|
|
|
267
382
|
// src/api.ts
|
|
@@ -306,8 +421,8 @@ async function request(url, init, parse) {
|
|
|
306
421
|
}
|
|
307
422
|
return parse(body);
|
|
308
423
|
}
|
|
309
|
-
function createApiClient(
|
|
310
|
-
const base =
|
|
424
|
+
function createApiClient(apiUrl2, publishToken) {
|
|
425
|
+
const base = apiUrl2.replace(/\/$/, "");
|
|
311
426
|
const headers = {
|
|
312
427
|
"content-type": "application/json"
|
|
313
428
|
};
|
|
@@ -315,6 +430,15 @@ function createApiClient(apiUrl, publishToken) {
|
|
|
315
430
|
headers.authorization = `Bearer ${publishToken}`;
|
|
316
431
|
}
|
|
317
432
|
return {
|
|
433
|
+
// SPEC §6.19 — mark the agent as actively waiting for feedback.
|
|
434
|
+
// Authorized by the publish token this client was created with.
|
|
435
|
+
async agentHeartbeat(artifactId) {
|
|
436
|
+
return request(
|
|
437
|
+
`${base}/api/v1/artifacts/${artifactId}/agent-presence`,
|
|
438
|
+
{ headers, method: "POST" },
|
|
439
|
+
(d) => agentHeartbeatResponseSchema.parse(d)
|
|
440
|
+
);
|
|
441
|
+
},
|
|
318
442
|
async createSession(body) {
|
|
319
443
|
return request(
|
|
320
444
|
`${base}/api/v1/publish`,
|
|
@@ -347,6 +471,24 @@ function createApiClient(apiUrl, publishToken) {
|
|
|
347
471
|
(d) => artifactResponseSchema.parse(d)
|
|
348
472
|
);
|
|
349
473
|
},
|
|
474
|
+
// SPEC §6.18 — everything owned by the account this client's API
|
|
475
|
+
// key (or session) belongs to.
|
|
476
|
+
async listArtifacts() {
|
|
477
|
+
return request(
|
|
478
|
+
`${base}/api/v1/artifacts`,
|
|
479
|
+
{ headers, method: "GET" },
|
|
480
|
+
(d) => artifactListResponseSchema.parse(d)
|
|
481
|
+
);
|
|
482
|
+
},
|
|
483
|
+
// SPEC §6.14 — who the presented credential belongs to. Used by
|
|
484
|
+
// `login` to verify a pasted key and by `whoami`.
|
|
485
|
+
async me() {
|
|
486
|
+
return request(
|
|
487
|
+
`${base}/api/v1/me`,
|
|
488
|
+
{ headers, method: "GET" },
|
|
489
|
+
(d) => meResponseSchema.parse(d)
|
|
490
|
+
);
|
|
491
|
+
},
|
|
350
492
|
// SPEC §6.16 — set a password (string) or clear it (null). Authorized
|
|
351
493
|
// by the publish token this client was created with.
|
|
352
494
|
async setProtection(artifactId, password) {
|
|
@@ -383,13 +525,13 @@ import {
|
|
|
383
525
|
writeFileSync
|
|
384
526
|
} from "fs";
|
|
385
527
|
import path from "path";
|
|
386
|
-
import { z as
|
|
387
|
-
var configSchema =
|
|
388
|
-
apiUrl:
|
|
389
|
-
artifactId:
|
|
390
|
-
publishToken:
|
|
391
|
-
slug:
|
|
392
|
-
url:
|
|
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()
|
|
393
535
|
});
|
|
394
536
|
var CONFIG_DIR = ".artifactcafe";
|
|
395
537
|
var CONFIG_FILE = "config.json";
|
|
@@ -438,6 +580,9 @@ ${CONFIG_DIR}/
|
|
|
438
580
|
// src/comments.ts
|
|
439
581
|
var STATUSES = ["open", "resolved", "all"];
|
|
440
582
|
var VERSIONS = ["current", "all"];
|
|
583
|
+
var pollSeconds = () => Number(process.env.AC_WAIT_POLL_SECONDS ?? 5);
|
|
584
|
+
var settleSeconds = () => Number(process.env.AC_WAIT_SETTLE_SECONDS ?? 20);
|
|
585
|
+
var WAIT_TIMEOUT_EXIT = 2;
|
|
441
586
|
async function comments(opts) {
|
|
442
587
|
if (!STATUSES.includes(opts.status)) {
|
|
443
588
|
throw new Error(
|
|
@@ -458,8 +603,17 @@ async function comments(opts) {
|
|
|
458
603
|
`No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
|
|
459
604
|
);
|
|
460
605
|
}
|
|
461
|
-
const
|
|
462
|
-
const client = createApiClient(
|
|
606
|
+
const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
|
|
607
|
+
const client = createApiClient(apiUrl2, config.publishToken);
|
|
608
|
+
if (opts.wait) {
|
|
609
|
+
await waitForFeedback(client, config, {
|
|
610
|
+
json: opts.json,
|
|
611
|
+
status,
|
|
612
|
+
timeoutSeconds: opts.timeout,
|
|
613
|
+
version
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
463
617
|
const data = await client.exportComments(config.artifactId, {
|
|
464
618
|
status,
|
|
465
619
|
version
|
|
@@ -470,6 +624,112 @@ async function comments(opts) {
|
|
|
470
624
|
}
|
|
471
625
|
printHuman(data, config.url, status, version);
|
|
472
626
|
}
|
|
627
|
+
var sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
|
|
628
|
+
function messageCounts(data) {
|
|
629
|
+
return new Map(data.comments.map((t) => [t.id, 1 + t.replies.length]));
|
|
630
|
+
}
|
|
631
|
+
function diffThreads(baseline, data) {
|
|
632
|
+
const changed = [];
|
|
633
|
+
for (const t of data.comments) {
|
|
634
|
+
const before = baseline.get(t.id);
|
|
635
|
+
if (before === void 0) {
|
|
636
|
+
changed.push({ ...t, change: "new" });
|
|
637
|
+
} else if (1 + t.replies.length > before) {
|
|
638
|
+
changed.push({ ...t, change: "updated" });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return changed;
|
|
642
|
+
}
|
|
643
|
+
async function waitForFeedback(client, config, opts) {
|
|
644
|
+
const query = { status: opts.status, version: opts.version };
|
|
645
|
+
const baselineExport = await client.exportComments(config.artifactId, query);
|
|
646
|
+
const baseline = messageCounts(baselineExport);
|
|
647
|
+
if (!opts.json) {
|
|
648
|
+
const mins = Math.round(opts.timeoutSeconds / 60);
|
|
649
|
+
console.log(
|
|
650
|
+
`Waiting for feedback on ${config.url} \u2014 times out after ${mins}m. Ctrl-C to stop.`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const deadline = Date.now() + opts.timeoutSeconds * 1e3;
|
|
654
|
+
let heartbeatWarned = false;
|
|
655
|
+
let lastHeartbeat = 0;
|
|
656
|
+
const heartbeat = async () => {
|
|
657
|
+
if (Date.now() - lastHeartbeat < AGENT_PRESENCE.heartbeatSeconds * 1e3) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
lastHeartbeat = Date.now();
|
|
661
|
+
try {
|
|
662
|
+
await client.agentHeartbeat(config.artifactId);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
if (!(opts.json || heartbeatWarned)) {
|
|
665
|
+
heartbeatWarned = true;
|
|
666
|
+
console.error(
|
|
667
|
+
`(presence heartbeat failed \u2014 reviewers won't see "Agent is listening": ${err instanceof Error ? err.message : err})`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
await heartbeat();
|
|
673
|
+
while (Date.now() < deadline) {
|
|
674
|
+
await sleep(Math.min(pollSeconds(), (deadline - Date.now()) / 1e3));
|
|
675
|
+
await heartbeat();
|
|
676
|
+
const data = await client.exportComments(config.artifactId, query);
|
|
677
|
+
if (diffThreads(baseline, data).length === 0) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
await sleep(settleSeconds());
|
|
681
|
+
const settled = await client.exportComments(config.artifactId, query);
|
|
682
|
+
const changed = diffThreads(baseline, settled);
|
|
683
|
+
printWaitResult(config, settled, changed, opts.json);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (opts.json) {
|
|
687
|
+
console.log(
|
|
688
|
+
JSON.stringify(
|
|
689
|
+
{
|
|
690
|
+
artifactId: config.artifactId,
|
|
691
|
+
threads: [],
|
|
692
|
+
timedOut: true,
|
|
693
|
+
versionNumber: baselineExport.versionNumber
|
|
694
|
+
},
|
|
695
|
+
null,
|
|
696
|
+
2
|
|
697
|
+
)
|
|
698
|
+
);
|
|
699
|
+
} else {
|
|
700
|
+
console.log(
|
|
701
|
+
`No new feedback after ${opts.timeoutSeconds}s. Run the command again to keep waiting.`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
process.exit(WAIT_TIMEOUT_EXIT);
|
|
705
|
+
}
|
|
706
|
+
function printWaitResult(config, data, changed, json) {
|
|
707
|
+
if (json) {
|
|
708
|
+
console.log(
|
|
709
|
+
JSON.stringify(
|
|
710
|
+
{
|
|
711
|
+
artifactId: config.artifactId,
|
|
712
|
+
threads: changed,
|
|
713
|
+
timedOut: false,
|
|
714
|
+
versionNumber: data.versionNumber
|
|
715
|
+
},
|
|
716
|
+
null,
|
|
717
|
+
2
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const plural = changed.length === 1 ? "" : "s";
|
|
723
|
+
console.log(
|
|
724
|
+
`
|
|
725
|
+
${changed.length} thread${plural} with new feedback \u2014 ${config.url}
|
|
726
|
+
`
|
|
727
|
+
);
|
|
728
|
+
for (const thread of changed) {
|
|
729
|
+
console.log(thread.change === "new" ? "NEW" : "UPDATED");
|
|
730
|
+
printThread(thread);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
473
733
|
function printHuman(data, url, status, version) {
|
|
474
734
|
const statusLabel = status === "all" ? "" : `${status} `;
|
|
475
735
|
let scope = "across all versions";
|
|
@@ -514,47 +774,75 @@ function indent(text, pad) {
|
|
|
514
774
|
return text.split("\n").map((line) => pad + line).join("\n");
|
|
515
775
|
}
|
|
516
776
|
|
|
517
|
-
// src/
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
777
|
+
// src/dashboard.ts
|
|
778
|
+
import path6 from "path";
|
|
779
|
+
|
|
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 };
|
|
537
800
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.error(`\u2717 ${message}`);
|
|
801
|
+
try {
|
|
802
|
+
return credentialsSchema.parse(JSON.parse(readFileSync2(file, "utf8")));
|
|
803
|
+
} catch {
|
|
804
|
+
return { keys: {}, version: 1 };
|
|
543
805
|
}
|
|
544
806
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|
|
548
836
|
|
|
549
837
|
// src/publish.ts
|
|
550
838
|
import { exec } from "child_process";
|
|
551
|
-
import { readFileSync as
|
|
552
|
-
import
|
|
839
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
840
|
+
import path5 from "path";
|
|
553
841
|
|
|
554
842
|
// src/manifest.ts
|
|
555
843
|
import { createHash } from "crypto";
|
|
556
|
-
import { readdirSync, readFileSync as
|
|
557
|
-
import
|
|
844
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
845
|
+
import path4 from "path";
|
|
558
846
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
|
|
559
847
|
var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
560
848
|
function isIgnoredFile(name) {
|
|
@@ -603,7 +891,7 @@ var MIME_TYPES = {
|
|
|
603
891
|
".xml": "application/xml"
|
|
604
892
|
};
|
|
605
893
|
function mimeFor(filePath) {
|
|
606
|
-
return MIME_TYPES[
|
|
894
|
+
return MIME_TYPES[path4.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
607
895
|
}
|
|
608
896
|
function scanDirectory(dir) {
|
|
609
897
|
const files = [];
|
|
@@ -611,7 +899,7 @@ function scanDirectory(dir) {
|
|
|
611
899
|
function walk(current, relPrefix) {
|
|
612
900
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
613
901
|
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
614
|
-
const abs =
|
|
902
|
+
const abs = path4.join(current, entry.name);
|
|
615
903
|
if (entry.isSymbolicLink()) {
|
|
616
904
|
skipped.push(rel);
|
|
617
905
|
continue;
|
|
@@ -636,7 +924,7 @@ function scanDirectory(dir) {
|
|
|
636
924
|
skipped.push(rel);
|
|
637
925
|
continue;
|
|
638
926
|
}
|
|
639
|
-
const content =
|
|
927
|
+
const content = readFileSync3(abs);
|
|
640
928
|
files.push({
|
|
641
929
|
absPath: abs,
|
|
642
930
|
hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
|
|
@@ -675,10 +963,9 @@ function resolveEntry(files, explicit) {
|
|
|
675
963
|
}
|
|
676
964
|
|
|
677
965
|
// src/publish.ts
|
|
678
|
-
var DEFAULT_API_URL = "https://artifact.cafe";
|
|
679
966
|
var UPLOAD_CONCURRENCY = 6;
|
|
680
967
|
async function publish(opts) {
|
|
681
|
-
const dir =
|
|
968
|
+
const dir = path5.resolve(opts.dir);
|
|
682
969
|
const config = readConfig(dir);
|
|
683
970
|
if (opts.password !== void 0) {
|
|
684
971
|
const parsed = gatePasswordSchema.safeParse(opts.password);
|
|
@@ -688,7 +975,8 @@ async function publish(opts) {
|
|
|
688
975
|
}
|
|
689
976
|
const artifactId = opts.artifact ?? config?.artifactId ?? null;
|
|
690
977
|
const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
|
|
691
|
-
const
|
|
978
|
+
const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
|
|
979
|
+
const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
|
|
692
980
|
const { files, skipped } = scanDirectory(dir);
|
|
693
981
|
if (files.length === 0) {
|
|
694
982
|
throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
|
|
@@ -702,8 +990,7 @@ async function publish(opts) {
|
|
|
702
990
|
if (skipped.length) {
|
|
703
991
|
log(` ignoring ${skipped.length} item(s): ${preview(skipped)}`);
|
|
704
992
|
}
|
|
705
|
-
const
|
|
706
|
-
const session = await client.createSession({
|
|
993
|
+
const sessionBody = {
|
|
707
994
|
artifactId,
|
|
708
995
|
entryPath,
|
|
709
996
|
files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
|
|
@@ -713,7 +1000,21 @@ async function publish(opts) {
|
|
|
713
1000
|
type: "cli"
|
|
714
1001
|
},
|
|
715
1002
|
title: opts.title
|
|
716
|
-
}
|
|
1003
|
+
};
|
|
1004
|
+
let credential = apiKey ?? token;
|
|
1005
|
+
let client = createApiClient(apiUrl2, credential);
|
|
1006
|
+
let session;
|
|
1007
|
+
try {
|
|
1008
|
+
session = await client.createSession(sessionBody);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
const keyRefused = apiKey !== void 0 && token !== void 0 && apiKey !== token && artifactId !== null && err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
|
|
1011
|
+
if (!keyRefused) {
|
|
1012
|
+
throw err;
|
|
1013
|
+
}
|
|
1014
|
+
credential = token;
|
|
1015
|
+
client = createApiClient(apiUrl2, credential);
|
|
1016
|
+
session = await client.createSession(sessionBody);
|
|
1017
|
+
}
|
|
717
1018
|
const byPath = new Map(files.map((f) => [f.path, f]));
|
|
718
1019
|
log(
|
|
719
1020
|
` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
|
|
@@ -725,14 +1026,14 @@ async function publish(opts) {
|
|
|
725
1026
|
}
|
|
726
1027
|
await uploadFile(
|
|
727
1028
|
upload.uploadUrl,
|
|
728
|
-
|
|
1029
|
+
readFileSync4(file.absPath),
|
|
729
1030
|
file.mimeType
|
|
730
1031
|
);
|
|
731
1032
|
});
|
|
732
1033
|
const result = await client.finalize(session.publishSessionId);
|
|
733
1034
|
if (result.publishToken) {
|
|
734
1035
|
writeConfig(dir, {
|
|
735
|
-
apiUrl,
|
|
1036
|
+
apiUrl: apiUrl2,
|
|
736
1037
|
artifactId: result.artifactId,
|
|
737
1038
|
publishToken: result.publishToken,
|
|
738
1039
|
slug: slugFromUrl(result.url),
|
|
@@ -740,7 +1041,10 @@ async function publish(opts) {
|
|
|
740
1041
|
});
|
|
741
1042
|
}
|
|
742
1043
|
if (opts.password !== void 0) {
|
|
743
|
-
const protectClient = createApiClient(
|
|
1044
|
+
const protectClient = createApiClient(
|
|
1045
|
+
apiUrl2,
|
|
1046
|
+
credential ?? result.publishToken
|
|
1047
|
+
);
|
|
744
1048
|
await protectClient.setProtection(result.artifactId, opts.password);
|
|
745
1049
|
}
|
|
746
1050
|
if (opts.json) {
|
|
@@ -748,6 +1052,7 @@ async function publish(opts) {
|
|
|
748
1052
|
JSON.stringify(
|
|
749
1053
|
{
|
|
750
1054
|
artifactId: result.artifactId,
|
|
1055
|
+
claimed: result.claimed,
|
|
751
1056
|
url: result.url,
|
|
752
1057
|
versionNumber: result.versionNumber,
|
|
753
1058
|
...result.claimUrl ? { claimUrl: result.claimUrl } : {},
|
|
@@ -771,6 +1076,8 @@ ${result.url}
|
|
|
771
1076
|
${result.claimUrl}
|
|
772
1077
|
`
|
|
773
1078
|
);
|
|
1079
|
+
} else if (result.claimed) {
|
|
1080
|
+
console.log("Claimed by your account \u2014 no expiry.\n");
|
|
774
1081
|
}
|
|
775
1082
|
if (opts.password !== void 0) {
|
|
776
1083
|
console.log("Password protection: on\n");
|
|
@@ -823,9 +1130,219 @@ function openInBrowser(url) {
|
|
|
823
1130
|
});
|
|
824
1131
|
}
|
|
825
1132
|
|
|
1133
|
+
// src/dashboard.ts
|
|
1134
|
+
function openDashboard(opts) {
|
|
1135
|
+
const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path6.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
|
|
1136
|
+
const url = `${base.replace(/\/$/, "")}/dashboard`;
|
|
1137
|
+
console.log(url);
|
|
1138
|
+
if (process.stdout.isTTY) {
|
|
1139
|
+
openInBrowser(url);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/errors.ts
|
|
1144
|
+
function printCliError(err, json) {
|
|
1145
|
+
if (err instanceof ApiRequestError) {
|
|
1146
|
+
if (json) {
|
|
1147
|
+
console.log(
|
|
1148
|
+
JSON.stringify({
|
|
1149
|
+
error: {
|
|
1150
|
+
code: err.code,
|
|
1151
|
+
message: err.message,
|
|
1152
|
+
retryAfter: err.retryAfter
|
|
1153
|
+
}
|
|
1154
|
+
})
|
|
1155
|
+
);
|
|
1156
|
+
} else {
|
|
1157
|
+
console.error(`\u2717 ${err.message} (${err.code})`);
|
|
1158
|
+
if (err.retryAfter) {
|
|
1159
|
+
console.error(` Retry in ${err.retryAfter}s.`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1165
|
+
if (json) {
|
|
1166
|
+
console.log(JSON.stringify({ error: { code: "cli_error", message } }));
|
|
1167
|
+
} else {
|
|
1168
|
+
console.error(`\u2717 ${message}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/list.ts
|
|
1173
|
+
async function list(opts) {
|
|
1174
|
+
const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
|
|
1175
|
+
/\/$/,
|
|
1176
|
+
""
|
|
1177
|
+
);
|
|
1178
|
+
const key = resolveApiKey(apiUrl2, opts.apiKey);
|
|
1179
|
+
if (!key) {
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
`Listing your artifacts needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
const data = await createApiClient(apiUrl2, key).listArtifacts();
|
|
1185
|
+
if (opts.json) {
|
|
1186
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (data.artifacts.length === 0) {
|
|
1190
|
+
console.log(
|
|
1191
|
+
"No artifacts in your account yet \u2014 publish while logged in, or claim one via its claim link."
|
|
1192
|
+
);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const plural = data.artifacts.length === 1 ? "" : "s";
|
|
1196
|
+
console.log(`${data.artifacts.length} artifact${plural}
|
|
1197
|
+
`);
|
|
1198
|
+
for (const artifact of data.artifacts) {
|
|
1199
|
+
printArtifact(artifact);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function printArtifact(a) {
|
|
1203
|
+
const version = a.currentVersionNumber === null ? "no version" : `v${a.currentVersionNumber}`;
|
|
1204
|
+
const open2 = a.openCommentCount === 0 ? "no open comments" : `${a.openCommentCount} open comment${a.openCommentCount === 1 ? "" : "s"}`;
|
|
1205
|
+
const marker = a.openCommentCount > 0 ? "\u25CF" : "\u25CB";
|
|
1206
|
+
console.log(`${marker} ${a.title}`);
|
|
1207
|
+
console.log(` ${version} \xB7 ${open2} \xB7 updated ${relativeTime(a.updatedAt)}`);
|
|
1208
|
+
console.log(` ${a.url}
|
|
1209
|
+
`);
|
|
1210
|
+
}
|
|
1211
|
+
function relativeTime(iso) {
|
|
1212
|
+
const seconds = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1e3);
|
|
1213
|
+
if (seconds < 60) {
|
|
1214
|
+
return "just now";
|
|
1215
|
+
}
|
|
1216
|
+
if (seconds < 3600) {
|
|
1217
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
1218
|
+
}
|
|
1219
|
+
if (seconds < 86400) {
|
|
1220
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
1221
|
+
}
|
|
1222
|
+
if (seconds < 30 * 86400) {
|
|
1223
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
1224
|
+
}
|
|
1225
|
+
return iso.slice(0, 10);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/login.ts
|
|
1229
|
+
import { createInterface } from "readline";
|
|
1230
|
+
function apiUrl() {
|
|
1231
|
+
return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
1232
|
+
}
|
|
1233
|
+
function promptSecret(question) {
|
|
1234
|
+
return new Promise((resolve) => {
|
|
1235
|
+
const rl = createInterface({
|
|
1236
|
+
input: process.stdin,
|
|
1237
|
+
output: process.stdout
|
|
1238
|
+
});
|
|
1239
|
+
let muted = false;
|
|
1240
|
+
const raw = rl;
|
|
1241
|
+
raw._writeToOutput = (s) => {
|
|
1242
|
+
if (!muted) {
|
|
1243
|
+
process.stdout.write(s);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
rl.question(question, (answer) => {
|
|
1247
|
+
rl.close();
|
|
1248
|
+
process.stdout.write("\n");
|
|
1249
|
+
resolve(answer);
|
|
1250
|
+
});
|
|
1251
|
+
muted = true;
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
async function login(opts) {
|
|
1255
|
+
const base = apiUrl();
|
|
1256
|
+
const keysUrl = `${base}/cli`;
|
|
1257
|
+
let key = opts.apiKey?.trim();
|
|
1258
|
+
if (!key) {
|
|
1259
|
+
if (!opts.json) {
|
|
1260
|
+
console.log(`
|
|
1261
|
+
Opening ${keysUrl}`);
|
|
1262
|
+
console.log("Create a CLI key there, then paste it below.\n");
|
|
1263
|
+
}
|
|
1264
|
+
if (process.stdout.isTTY) {
|
|
1265
|
+
openInBrowser(keysUrl);
|
|
1266
|
+
}
|
|
1267
|
+
key = (await promptSecret("API key: ")).trim();
|
|
1268
|
+
}
|
|
1269
|
+
if (!key) {
|
|
1270
|
+
throw new Error("No key entered \u2014 nothing was saved.");
|
|
1271
|
+
}
|
|
1272
|
+
if (!key.startsWith("ack_")) {
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
`That doesn't look like an artifact.cafe API key (they start with "ack_").`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
const me = await createApiClient(base, key).me();
|
|
1278
|
+
storeApiKey(base, key);
|
|
1279
|
+
const who = me.email ?? me.userId;
|
|
1280
|
+
if (opts.json) {
|
|
1281
|
+
console.log(
|
|
1282
|
+
JSON.stringify(
|
|
1283
|
+
{ apiUrl: base, email: me.email, loggedIn: true, userId: me.userId },
|
|
1284
|
+
null,
|
|
1285
|
+
2
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
console.log(`
|
|
1291
|
+
\u2713 Logged in to ${apiOrigin(base)} as ${who}`);
|
|
1292
|
+
console.log(
|
|
1293
|
+
"New publishes are claimed by your account automatically. Try `artifact-cafe list`.\n"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
async function whoami(opts) {
|
|
1297
|
+
const base = apiUrl();
|
|
1298
|
+
const key = resolveApiKey(base);
|
|
1299
|
+
if (!key) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`Not logged in to ${apiOrigin(base)} \u2014 run \`artifact-cafe login\` first.`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
const me = await createApiClient(base, key).me();
|
|
1305
|
+
if (opts.json) {
|
|
1306
|
+
console.log(
|
|
1307
|
+
JSON.stringify(
|
|
1308
|
+
{
|
|
1309
|
+
apiUrl: base,
|
|
1310
|
+
email: me.email,
|
|
1311
|
+
keyName: me.keyName ?? null,
|
|
1312
|
+
userId: me.userId
|
|
1313
|
+
},
|
|
1314
|
+
null,
|
|
1315
|
+
2
|
|
1316
|
+
)
|
|
1317
|
+
);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const keyLabel = me.keyName ? ` (key: ${me.keyName})` : "";
|
|
1321
|
+
console.log(
|
|
1322
|
+
`Logged in to ${apiOrigin(base)} as ${me.email ?? me.userId}${keyLabel}`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
function logout(opts) {
|
|
1326
|
+
const base = apiUrl();
|
|
1327
|
+
const removed = removeApiKey(base);
|
|
1328
|
+
if (opts.json) {
|
|
1329
|
+
console.log(JSON.stringify({ apiUrl: base, loggedOut: removed }, null, 2));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (!removed) {
|
|
1333
|
+
console.log(`No stored key for ${apiOrigin(base)} \u2014 already logged out.`);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
console.log(`\u2713 Logged out of ${apiOrigin(base)} on this machine.`);
|
|
1337
|
+
console.log(
|
|
1338
|
+
`The key itself is still active \u2014 revoke it at ${base}/cli if this machine is shared.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
826
1342
|
// src/open.ts
|
|
1343
|
+
import path7 from "path";
|
|
827
1344
|
function openArtifact(opts) {
|
|
828
|
-
const dir =
|
|
1345
|
+
const dir = path7.resolve(opts.dir);
|
|
829
1346
|
const config = readConfig(dir);
|
|
830
1347
|
if (!config) {
|
|
831
1348
|
throw new Error(
|
|
@@ -839,7 +1356,7 @@ function openArtifact(opts) {
|
|
|
839
1356
|
}
|
|
840
1357
|
|
|
841
1358
|
// src/protect.ts
|
|
842
|
-
import
|
|
1359
|
+
import path8 from "path";
|
|
843
1360
|
async function protect(opts) {
|
|
844
1361
|
if (opts.off && opts.password !== void 0) {
|
|
845
1362
|
throw new Error("Pass either --password or --off, not both.");
|
|
@@ -856,16 +1373,16 @@ async function protect(opts) {
|
|
|
856
1373
|
throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
|
|
857
1374
|
}
|
|
858
1375
|
}
|
|
859
|
-
const dir =
|
|
1376
|
+
const dir = path8.resolve(opts.dir);
|
|
860
1377
|
const config = readConfig(dir);
|
|
861
1378
|
if (!config) {
|
|
862
1379
|
throw new Error(
|
|
863
1380
|
`No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
|
|
864
1381
|
);
|
|
865
1382
|
}
|
|
866
|
-
const
|
|
1383
|
+
const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
|
|
867
1384
|
const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
|
|
868
|
-
const client = createApiClient(
|
|
1385
|
+
const client = createApiClient(apiUrl2, token);
|
|
869
1386
|
const res = await client.setProtection(config.artifactId, password);
|
|
870
1387
|
if (opts.json) {
|
|
871
1388
|
console.log(JSON.stringify({ protected: res.protected }, null, 2));
|
|
@@ -883,6 +1400,10 @@ async function protect(opts) {
|
|
|
883
1400
|
// src/index.ts
|
|
884
1401
|
var publish2 = defineCommand({
|
|
885
1402
|
args: {
|
|
1403
|
+
"api-key": {
|
|
1404
|
+
description: "Publish with an account API key (default: stored login)",
|
|
1405
|
+
type: "string"
|
|
1406
|
+
},
|
|
886
1407
|
artifact: {
|
|
887
1408
|
description: "Publish to an existing artifact ID",
|
|
888
1409
|
type: "string"
|
|
@@ -905,6 +1426,7 @@ var publish2 = defineCommand({
|
|
|
905
1426
|
async run({ args }) {
|
|
906
1427
|
try {
|
|
907
1428
|
await publish({
|
|
1429
|
+
apiKey: args["api-key"],
|
|
908
1430
|
artifact: args.artifact,
|
|
909
1431
|
dir: args.dir,
|
|
910
1432
|
entry: args.entry,
|
|
@@ -919,6 +1441,82 @@ var publish2 = defineCommand({
|
|
|
919
1441
|
}
|
|
920
1442
|
}
|
|
921
1443
|
});
|
|
1444
|
+
var login2 = defineCommand({
|
|
1445
|
+
args: {
|
|
1446
|
+
"api-key": {
|
|
1447
|
+
description: "Store this key without the browser prompt (agents, CI)",
|
|
1448
|
+
type: "string"
|
|
1449
|
+
},
|
|
1450
|
+
json: { description: "Machine-readable output", type: "boolean" }
|
|
1451
|
+
},
|
|
1452
|
+
meta: {
|
|
1453
|
+
description: "Log in \u2014 mint an API key in the browser and store it",
|
|
1454
|
+
name: "login"
|
|
1455
|
+
},
|
|
1456
|
+
async run({ args }) {
|
|
1457
|
+
try {
|
|
1458
|
+
await login({ apiKey: args["api-key"], json: Boolean(args.json) });
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
printCliError(err, Boolean(args.json));
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
var logout2 = defineCommand({
|
|
1466
|
+
args: {
|
|
1467
|
+
json: { description: "Machine-readable output", type: "boolean" }
|
|
1468
|
+
},
|
|
1469
|
+
meta: {
|
|
1470
|
+
description: "Remove the stored API key for this machine",
|
|
1471
|
+
name: "logout"
|
|
1472
|
+
},
|
|
1473
|
+
run({ args }) {
|
|
1474
|
+
try {
|
|
1475
|
+
logout({ json: Boolean(args.json) });
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
printCliError(err, Boolean(args.json));
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
var whoami2 = defineCommand({
|
|
1483
|
+
args: {
|
|
1484
|
+
json: { description: "Machine-readable output", type: "boolean" }
|
|
1485
|
+
},
|
|
1486
|
+
meta: {
|
|
1487
|
+
description: "Show which account the stored API key belongs to",
|
|
1488
|
+
name: "whoami"
|
|
1489
|
+
},
|
|
1490
|
+
async run({ args }) {
|
|
1491
|
+
try {
|
|
1492
|
+
await whoami({ json: Boolean(args.json) });
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
printCliError(err, Boolean(args.json));
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
var list2 = defineCommand({
|
|
1500
|
+
args: {
|
|
1501
|
+
"api-key": {
|
|
1502
|
+
description: "List with this API key (default: stored login)",
|
|
1503
|
+
type: "string"
|
|
1504
|
+
},
|
|
1505
|
+
json: { description: "Machine-readable output", type: "boolean" }
|
|
1506
|
+
},
|
|
1507
|
+
meta: {
|
|
1508
|
+
description: "List the artifacts your account owns",
|
|
1509
|
+
name: "list"
|
|
1510
|
+
},
|
|
1511
|
+
async run({ args }) {
|
|
1512
|
+
try {
|
|
1513
|
+
await list({ apiKey: args["api-key"], json: Boolean(args.json) });
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
printCliError(err, Boolean(args.json));
|
|
1516
|
+
process.exit(1);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
922
1520
|
var comments2 = defineCommand({
|
|
923
1521
|
args: {
|
|
924
1522
|
dir: {
|
|
@@ -932,10 +1530,19 @@ var comments2 = defineCommand({
|
|
|
932
1530
|
description: "Filter: open | resolved | all",
|
|
933
1531
|
type: "string"
|
|
934
1532
|
},
|
|
1533
|
+
timeout: {
|
|
1534
|
+
default: "900",
|
|
1535
|
+
description: "With --wait: give up after this many seconds (exit 2)",
|
|
1536
|
+
type: "string"
|
|
1537
|
+
},
|
|
935
1538
|
version: {
|
|
936
1539
|
default: "current",
|
|
937
1540
|
description: "Filter: current | all",
|
|
938
1541
|
type: "string"
|
|
1542
|
+
},
|
|
1543
|
+
wait: {
|
|
1544
|
+
description: "Block until a reviewer leaves new feedback",
|
|
1545
|
+
type: "boolean"
|
|
939
1546
|
}
|
|
940
1547
|
},
|
|
941
1548
|
meta: {
|
|
@@ -944,11 +1551,19 @@ var comments2 = defineCommand({
|
|
|
944
1551
|
},
|
|
945
1552
|
async run({ args }) {
|
|
946
1553
|
try {
|
|
1554
|
+
const timeout = Number(args.timeout);
|
|
1555
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
`--timeout must be a positive number of seconds (got "${args.timeout}").`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
947
1560
|
await comments({
|
|
948
1561
|
dir: args.dir,
|
|
949
1562
|
json: Boolean(args.json),
|
|
950
1563
|
status: args.status,
|
|
951
|
-
|
|
1564
|
+
timeout,
|
|
1565
|
+
version: args.version,
|
|
1566
|
+
wait: Boolean(args.wait)
|
|
952
1567
|
});
|
|
953
1568
|
} catch (err) {
|
|
954
1569
|
printCliError(err, Boolean(args.json));
|
|
@@ -1003,11 +1618,42 @@ var open = defineCommand({
|
|
|
1003
1618
|
}
|
|
1004
1619
|
}
|
|
1005
1620
|
});
|
|
1621
|
+
var dashboard = defineCommand({
|
|
1622
|
+
args: {
|
|
1623
|
+
dir: {
|
|
1624
|
+
default: ".",
|
|
1625
|
+
description: "Directory whose linked instance to open (optional)",
|
|
1626
|
+
type: "positional"
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
meta: {
|
|
1630
|
+
description: "Open your personal dashboard in the browser",
|
|
1631
|
+
name: "dashboard"
|
|
1632
|
+
},
|
|
1633
|
+
run({ args }) {
|
|
1634
|
+
try {
|
|
1635
|
+
openDashboard({ dir: args.dir });
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
printCliError(err, false);
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1006
1642
|
var main = defineCommand({
|
|
1007
1643
|
meta: {
|
|
1008
1644
|
description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
|
|
1009
1645
|
name: "artifact-cafe"
|
|
1010
1646
|
},
|
|
1011
|
-
subCommands: {
|
|
1647
|
+
subCommands: {
|
|
1648
|
+
comments: comments2,
|
|
1649
|
+
dashboard,
|
|
1650
|
+
list: list2,
|
|
1651
|
+
login: login2,
|
|
1652
|
+
logout: logout2,
|
|
1653
|
+
open,
|
|
1654
|
+
protect: protect2,
|
|
1655
|
+
publish: publish2,
|
|
1656
|
+
whoami: whoami2
|
|
1657
|
+
}
|
|
1012
1658
|
});
|
|
1013
1659
|
runMain(main);
|