@nearform/fling 0.5.0 → 0.6.0
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/CHANGELOG.md +35 -1
- package/README.md +60 -2
- package/dist/cli.js +963 -327
- package/dist/cli.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import chalk10 from "chalk";
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
|
|
7
|
-
// src/commands/auth.ts
|
|
8
|
-
import chalk5 from "chalk";
|
|
9
|
-
|
|
10
|
-
// src/auth.ts
|
|
11
|
-
import { randomBytes } from "node:crypto";
|
|
12
|
-
import { createServer } from "node:http";
|
|
13
|
-
import { hostname as osHostname } from "node:os";
|
|
14
|
-
|
|
15
3
|
// ../../packages/shared/src/api.ts
|
|
16
|
-
import { z as
|
|
4
|
+
import { z as z3 } from "zod";
|
|
17
5
|
|
|
18
|
-
// ../../packages/shared/src/
|
|
6
|
+
// ../../packages/shared/src/audience.ts
|
|
19
7
|
import { z } from "zod";
|
|
8
|
+
var AUDIENCE_EMAILS_MAX = 100;
|
|
9
|
+
var AUDIENCE_GROUPS_MAX = 10;
|
|
10
|
+
var AudienceEmailSchema = z.object({ email: z.string(), known: z.boolean() });
|
|
11
|
+
var AudienceGroupSchema = z.object({
|
|
12
|
+
slug: z.string(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
size: z.number(),
|
|
15
|
+
deleted: z.boolean().optional()
|
|
16
|
+
});
|
|
17
|
+
var AudienceSchema = z.object({
|
|
18
|
+
emails: z.array(AudienceEmailSchema),
|
|
19
|
+
groups: z.array(AudienceGroupSchema).default([])
|
|
20
|
+
});
|
|
21
|
+
var AudienceInputSchema = z.object({
|
|
22
|
+
emails: z.array(z.string().trim().toLowerCase().min(3).max(254)).max(AUDIENCE_EMAILS_MAX, `An audience holds at most ${AUDIENCE_EMAILS_MAX} addresses`),
|
|
23
|
+
groups: z.array(z.string().trim().toLowerCase().min(1).max(100)).max(AUDIENCE_GROUPS_MAX, `An audience names at most ${AUDIENCE_GROUPS_MAX} groups`).default([])
|
|
24
|
+
});
|
|
25
|
+
var normaliseEmail = (email) => email.trim().toLowerCase();
|
|
26
|
+
function splitEmails(values) {
|
|
27
|
+
const all = typeof values === "string" ? [values] : values;
|
|
28
|
+
return all.flatMap((v) => v.split(/[\s,;]+/)).map(normaliseEmail).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
function editAudience(current, change) {
|
|
31
|
+
let emails2 = change.clear ? [] : change.set ? [...change.set] : [...current];
|
|
32
|
+
emails2 = [...emails2, ...change.add ?? []].map(normaliseEmail);
|
|
33
|
+
const removed = new Set((change.remove ?? []).map(normaliseEmail));
|
|
34
|
+
return [...new Set(emails2)].filter((e) => e && !removed.has(e));
|
|
35
|
+
}
|
|
36
|
+
function editAudienceGroups(current, change) {
|
|
37
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
38
|
+
let groups2 = change.clear ? [] : change.setGroups ? [...change.setGroups] : [...current];
|
|
39
|
+
groups2 = [...groups2, ...change.addGroups ?? []].map(norm);
|
|
40
|
+
const removed = new Set((change.removeGroups ?? []).map(norm));
|
|
41
|
+
return [...new Set(groups2)].filter((g) => g && !removed.has(g));
|
|
42
|
+
}
|
|
43
|
+
function editAudienceInput(current, change) {
|
|
44
|
+
return {
|
|
45
|
+
emails: editAudience(
|
|
46
|
+
current.emails.map((e) => e.email),
|
|
47
|
+
change
|
|
48
|
+
),
|
|
49
|
+
groups: editAudienceGroups(
|
|
50
|
+
current.groups.map((g) => g.slug),
|
|
51
|
+
change
|
|
52
|
+
)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var changesAudience = (change) => Boolean(
|
|
56
|
+
change.clear || change.set || change.add?.length || change.remove?.length || change.setGroups || change.addGroups?.length || change.removeGroups?.length
|
|
57
|
+
);
|
|
58
|
+
var people = (n) => `${n} ${n === 1 ? "person" : "people"}`;
|
|
59
|
+
var groupLabel = (g) => g.deleted ? `a deleted group (${g.slug})` : `${g.slug} (${g.name !== g.slug ? `${g.name}, ` : ""}${people(g.size)})`;
|
|
60
|
+
function whoIn(emails2, groups2) {
|
|
61
|
+
const live = groups2.filter((g) => !g.deleted);
|
|
62
|
+
return [
|
|
63
|
+
emails2.length ? people(emails2.length) : "",
|
|
64
|
+
live.length ? `${live.length === 1 ? "the group" : "the groups"} ${live.map(groupLabel).join(", ")}` : "",
|
|
65
|
+
...groups2.filter((g) => g.deleted).map(groupLabel)
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
var and = (parts) => parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}` : parts[0] ?? "";
|
|
69
|
+
var audienceList = (emails2) => emails2.map((e) => e.known ? e.email : `${e.email} (hasn't signed in yet)`).join(", ");
|
|
70
|
+
function describeAudience(site, privateAudience2) {
|
|
71
|
+
const emails2 = site.audience?.emails ?? [];
|
|
72
|
+
const groups2 = site.audience?.groups ?? [];
|
|
73
|
+
const empty = emails2.length + groups2.length === 0;
|
|
74
|
+
const list = emails2.length ? `: ${audienceList(emails2)}` : "";
|
|
75
|
+
if (site.visibility === "public") {
|
|
76
|
+
return empty ? "Public: anyone can open the site; reviewing it still needs sign-in." : `Public: anyone can open the site; reviewing it still needs sign-in. Its audience (${and(whoIn(emails2, groups2))}${list}) applies when it is private again.`;
|
|
77
|
+
}
|
|
78
|
+
if (empty) return `Private: ${privateAudience2} can open it.`;
|
|
79
|
+
const share2 = site.shareUrl ? ` A share link is active: anyone with it can still open the site (${site.shareUrl}).` : "";
|
|
80
|
+
if (emails2.length === 0 && groups2.every((g) => g.deleted)) {
|
|
81
|
+
const refs = groups2.map((g) => g.slug).join(", ");
|
|
82
|
+
return `Restricted: only you can open it. Its audience names only ${groups2.length === 1 ? "a deleted group" : "deleted groups"} (${refs}), which let nobody in: add people or groups, or clear it.${share2}`;
|
|
83
|
+
}
|
|
84
|
+
return `Restricted: only ${and(["you", ...whoIn(emails2, groups2)])} can open it${list}.${share2}`;
|
|
85
|
+
}
|
|
86
|
+
function sharedVia(site) {
|
|
87
|
+
const { direct, groups: groups2 } = site.via;
|
|
88
|
+
return and([
|
|
89
|
+
...direct ? ["your address"] : [],
|
|
90
|
+
...groups2.length ? [`${groups2.length === 1 ? "group" : "groups"} ${groups2.join(", ")}`] : []
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
var describeSharedSite = (site) => `- ${site.name}: ${site.url} (shared by ${site.owner}, through ${sharedVia(site)}${site.reviewUrl ? `; review: ${site.reviewUrl}` : ""})`;
|
|
94
|
+
|
|
95
|
+
// ../../packages/shared/src/manifest.ts
|
|
96
|
+
import { z as z2 } from "zod";
|
|
20
97
|
|
|
21
98
|
// ../../packages/shared/src/limits.ts
|
|
22
99
|
var MAX_DEPLOY_BYTES = 100 * 1024 * 1024;
|
|
@@ -214,220 +291,246 @@ function isValidSitePath(path3) {
|
|
|
214
291
|
if (path3.startsWith("/") || path3.includes("\\") || path3.includes("\0")) return false;
|
|
215
292
|
return path3.split("/").every((seg) => seg !== "" && !seg.startsWith("."));
|
|
216
293
|
}
|
|
217
|
-
var ManifestFileSchema =
|
|
218
|
-
path:
|
|
219
|
-
sha256:
|
|
220
|
-
size:
|
|
221
|
-
contentType:
|
|
294
|
+
var ManifestFileSchema = z2.object({
|
|
295
|
+
path: z2.string().refine(isValidSitePath, "invalid path (dotfiles are never published)").refine((p) => !isSecretPath(p), "secret file (keys, certificates, credentials) never published"),
|
|
296
|
+
sha256: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
297
|
+
size: z2.number().int().min(0).max(MAX_FILE_BYTES),
|
|
298
|
+
contentType: z2.string().min(1).max(255)
|
|
222
299
|
});
|
|
223
|
-
var ManifestSchema =
|
|
300
|
+
var ManifestSchema = z2.object({ files: z2.array(ManifestFileSchema).min(1).max(MAX_FILES) }).refine((m) => new Set(m.files.map((f) => f.path)).size === m.files.length, "duplicate paths").refine((m) => m.files.reduce((n, f) => n + f.size, 0) <= MAX_DEPLOY_BYTES, "deploy too large");
|
|
224
301
|
|
|
225
302
|
// ../../packages/shared/src/api.ts
|
|
226
|
-
var VisibilitySchema =
|
|
227
|
-
var MeSchema =
|
|
228
|
-
email:
|
|
229
|
-
name:
|
|
230
|
-
picture:
|
|
231
|
-
userSlug:
|
|
303
|
+
var VisibilitySchema = z3.enum(["private", "public"]);
|
|
304
|
+
var MeSchema = z3.object({
|
|
305
|
+
email: z3.string(),
|
|
306
|
+
name: z3.string(),
|
|
307
|
+
picture: z3.string().nullable(),
|
|
308
|
+
userSlug: z3.string(),
|
|
232
309
|
/** one of the instance's admins (servers from before admins don't say: false) */
|
|
233
|
-
admin:
|
|
310
|
+
admin: z3.boolean().default(false),
|
|
234
311
|
/** while an admin impersonates this user: the admin (browser sessions only) */
|
|
235
|
-
impersonator:
|
|
312
|
+
impersonator: z3.string().optional(),
|
|
236
313
|
/** …and when the impersonation lapses */
|
|
237
|
-
impersonationEndsAt:
|
|
314
|
+
impersonationEndsAt: z3.string().optional()
|
|
238
315
|
});
|
|
239
|
-
var SiteSchema =
|
|
240
|
-
name:
|
|
241
|
-
url:
|
|
316
|
+
var SiteSchema = z3.object({
|
|
317
|
+
name: z3.string(),
|
|
318
|
+
url: z3.string(),
|
|
242
319
|
visibility: VisibilitySchema,
|
|
243
|
-
spa:
|
|
244
|
-
archived:
|
|
245
|
-
currentVersionId:
|
|
246
|
-
shareUrl:
|
|
247
|
-
|
|
248
|
-
|
|
320
|
+
spa: z3.boolean(),
|
|
321
|
+
archived: z3.boolean(),
|
|
322
|
+
currentVersionId: z3.string().nullable(),
|
|
323
|
+
shareUrl: z3.string().nullable(),
|
|
324
|
+
/** private with an audience: only the owner and the people in it can open it */
|
|
325
|
+
restricted: z3.boolean().default(false),
|
|
326
|
+
/** how many addresses and groups the audience names (kept, and ignored, while public) */
|
|
327
|
+
audienceSize: z3.number().default(0),
|
|
328
|
+
/** how many of those are groups */
|
|
329
|
+
audienceGroups: z3.number().default(0),
|
|
330
|
+
createdAt: z3.string(),
|
|
331
|
+
updatedAt: z3.string()
|
|
332
|
+
});
|
|
333
|
+
var SiteWithAudienceSchema = SiteSchema.extend({ audience: AudienceSchema.optional() });
|
|
334
|
+
var VersionSchema = z3.object({
|
|
335
|
+
id: z3.string(),
|
|
336
|
+
fileCount: z3.number(),
|
|
337
|
+
totalBytes: z3.number(),
|
|
338
|
+
createdAt: z3.string()
|
|
249
339
|
});
|
|
250
|
-
var
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
340
|
+
var SiteDetailSchema = SiteWithAudienceSchema.extend({ versions: z3.array(VersionSchema) });
|
|
341
|
+
var SiteListSchema = z3.object({ sites: z3.array(SiteSchema) });
|
|
342
|
+
var SharedSiteSchema = z3.object({
|
|
343
|
+
name: z3.string(),
|
|
344
|
+
url: z3.string(),
|
|
345
|
+
/** the site's review page (`comments` capability) */
|
|
346
|
+
reviewUrl: z3.string().optional(),
|
|
347
|
+
owner: z3.string(),
|
|
348
|
+
/** how the caller is in the audience: by address, and through which of their groups (slugs) */
|
|
349
|
+
via: z3.object({ direct: z3.boolean(), groups: z3.array(z3.string()) }),
|
|
350
|
+
createdAt: z3.string(),
|
|
351
|
+
updatedAt: z3.string()
|
|
255
352
|
});
|
|
256
|
-
var
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
contentType: z2.string()
|
|
353
|
+
var SharedSiteListSchema = z3.object({ sites: z3.array(SharedSiteSchema) });
|
|
354
|
+
var SiteFileSchema = z3.object({
|
|
355
|
+
path: z3.string(),
|
|
356
|
+
size: z3.number(),
|
|
357
|
+
contentType: z3.string()
|
|
262
358
|
});
|
|
263
|
-
var VersionFilesSchema =
|
|
264
|
-
versionId:
|
|
265
|
-
files:
|
|
266
|
-
totalBytes:
|
|
267
|
-
generatedIndex:
|
|
359
|
+
var VersionFilesSchema = z3.object({
|
|
360
|
+
versionId: z3.string(),
|
|
361
|
+
files: z3.array(SiteFileSchema),
|
|
362
|
+
totalBytes: z3.number(),
|
|
363
|
+
generatedIndex: z3.boolean()
|
|
268
364
|
});
|
|
269
|
-
var NameCheckSchema =
|
|
270
|
-
name:
|
|
271
|
-
status:
|
|
272
|
-
suggestions:
|
|
365
|
+
var NameCheckSchema = z3.object({
|
|
366
|
+
name: z3.string(),
|
|
367
|
+
status: z3.enum(["available", "yours", "taken", "invalid"]),
|
|
368
|
+
suggestions: z3.array(z3.string())
|
|
273
369
|
});
|
|
274
|
-
var DeployRequestSchema =
|
|
370
|
+
var DeployRequestSchema = z3.object({
|
|
275
371
|
manifest: ManifestSchema,
|
|
276
|
-
spa:
|
|
372
|
+
spa: z3.boolean().optional(),
|
|
277
373
|
visibility: VisibilitySchema.optional()
|
|
278
374
|
});
|
|
279
|
-
var UploadSchema =
|
|
280
|
-
path:
|
|
281
|
-
url:
|
|
375
|
+
var UploadSchema = z3.object({
|
|
376
|
+
path: z3.string(),
|
|
377
|
+
url: z3.string(),
|
|
282
378
|
/** headers the client must send with the PUT (they are part of the signature) */
|
|
283
|
-
headers:
|
|
379
|
+
headers: z3.record(z3.string(), z3.string())
|
|
284
380
|
});
|
|
285
|
-
var DeployResponseSchema =
|
|
381
|
+
var DeployResponseSchema = z3.discriminatedUnion("status", [
|
|
286
382
|
/** content identical to the current version — nothing happened */
|
|
287
|
-
|
|
383
|
+
z3.object({ status: z3.literal("unchanged"), versionId: z3.string(), site: SiteSchema }),
|
|
288
384
|
/** this content was deployed before — site now points at that version again */
|
|
289
|
-
|
|
385
|
+
z3.object({ status: z3.literal("repointed"), versionId: z3.string(), site: SiteSchema }),
|
|
290
386
|
/** new content — PUT every upload, then POST /api/sites/:name/deploys/:versionId/finalize */
|
|
291
|
-
|
|
292
|
-
status:
|
|
293
|
-
versionId:
|
|
294
|
-
uploads:
|
|
387
|
+
z3.object({
|
|
388
|
+
status: z3.literal("upload"),
|
|
389
|
+
versionId: z3.string(),
|
|
390
|
+
uploads: z3.array(UploadSchema)
|
|
295
391
|
})
|
|
296
392
|
]);
|
|
297
|
-
var FinalizeResponseSchema =
|
|
298
|
-
status:
|
|
299
|
-
versionId:
|
|
393
|
+
var FinalizeResponseSchema = z3.object({
|
|
394
|
+
status: z3.literal("live"),
|
|
395
|
+
versionId: z3.string(),
|
|
300
396
|
site: SiteSchema
|
|
301
397
|
});
|
|
302
|
-
var RollbackRequestSchema =
|
|
303
|
-
var UpdateSiteRequestSchema =
|
|
398
|
+
var RollbackRequestSchema = z3.object({ versionId: z3.string() });
|
|
399
|
+
var UpdateSiteRequestSchema = z3.object({
|
|
304
400
|
visibility: VisibilitySchema.optional(),
|
|
305
|
-
spa:
|
|
401
|
+
spa: z3.boolean().optional(),
|
|
402
|
+
audience: AudienceInputSchema.optional()
|
|
306
403
|
});
|
|
307
|
-
var ApiKeySchema =
|
|
308
|
-
id:
|
|
309
|
-
name:
|
|
310
|
-
createdAt:
|
|
311
|
-
lastUsedAt:
|
|
404
|
+
var ApiKeySchema = z3.object({
|
|
405
|
+
id: z3.string(),
|
|
406
|
+
name: z3.string(),
|
|
407
|
+
createdAt: z3.string(),
|
|
408
|
+
lastUsedAt: z3.string().nullable()
|
|
312
409
|
});
|
|
313
|
-
var ApiKeyListSchema =
|
|
314
|
-
var ErrorEnvelopeSchema =
|
|
315
|
-
error:
|
|
410
|
+
var ApiKeyListSchema = z3.object({ keys: z3.array(ApiKeySchema) });
|
|
411
|
+
var ErrorEnvelopeSchema = z3.object({
|
|
412
|
+
error: z3.object({ code: z3.string(), message: z3.string() })
|
|
316
413
|
});
|
|
317
|
-
var ShareResponseSchema =
|
|
318
|
-
var ConnectionSchema =
|
|
319
|
-
id:
|
|
320
|
-
clientName:
|
|
414
|
+
var ShareResponseSchema = z3.object({ shareUrl: z3.string() });
|
|
415
|
+
var ConnectionSchema = z3.object({
|
|
416
|
+
id: z3.string(),
|
|
417
|
+
clientName: z3.string(),
|
|
321
418
|
/** host the client identifies as (CIMD client_id host or redirect URI host) */
|
|
322
|
-
clientHost:
|
|
323
|
-
createdAt:
|
|
324
|
-
lastUsedAt:
|
|
419
|
+
clientHost: z3.string().nullable(),
|
|
420
|
+
createdAt: z3.string(),
|
|
421
|
+
lastUsedAt: z3.string().nullable()
|
|
325
422
|
});
|
|
326
|
-
var ConnectionListSchema =
|
|
327
|
-
var OAuthRequestInfoSchema =
|
|
328
|
-
clientName:
|
|
329
|
-
clientUri:
|
|
423
|
+
var ConnectionListSchema = z3.object({ connections: z3.array(ConnectionSchema) });
|
|
424
|
+
var OAuthRequestInfoSchema = z3.object({
|
|
425
|
+
clientName: z3.string(),
|
|
426
|
+
clientUri: z3.string().nullable(),
|
|
330
427
|
/** host of the CIMD client_id URL, when the client identified itself that way */
|
|
331
|
-
clientIdHost:
|
|
332
|
-
redirectHost:
|
|
428
|
+
clientIdHost: z3.string().nullable(),
|
|
429
|
+
redirectHost: z3.string(),
|
|
333
430
|
/** true when the code goes to a loopback address (any local program could be asking) */
|
|
334
|
-
loopback:
|
|
335
|
-
scopes:
|
|
336
|
-
email:
|
|
431
|
+
loopback: z3.boolean(),
|
|
432
|
+
scopes: z3.array(z3.string()),
|
|
433
|
+
email: z3.string()
|
|
337
434
|
});
|
|
338
|
-
var OAuthDecisionSchema =
|
|
339
|
-
var AdminUserSchema =
|
|
340
|
-
email:
|
|
341
|
-
name:
|
|
342
|
-
picture:
|
|
343
|
-
userSlug:
|
|
344
|
-
createdAt:
|
|
345
|
-
lastSignInAt:
|
|
435
|
+
var OAuthDecisionSchema = z3.object({ redirectUrl: z3.string() });
|
|
436
|
+
var AdminUserSchema = z3.object({
|
|
437
|
+
email: z3.string(),
|
|
438
|
+
name: z3.string(),
|
|
439
|
+
picture: z3.string().nullable(),
|
|
440
|
+
userSlug: z3.string(),
|
|
441
|
+
createdAt: z3.string().nullable(),
|
|
442
|
+
lastSignInAt: z3.string().nullable(),
|
|
346
443
|
/** sites the user holds, archived ones included */
|
|
347
|
-
siteCount:
|
|
348
|
-
admin:
|
|
444
|
+
siteCount: z3.number(),
|
|
445
|
+
admin: z3.boolean()
|
|
349
446
|
});
|
|
350
|
-
var AdminUserListSchema =
|
|
351
|
-
users:
|
|
447
|
+
var AdminUserListSchema = z3.object({
|
|
448
|
+
users: z3.array(AdminUserSchema),
|
|
352
449
|
/** pass back as `cursor` for the next page; null: no more */
|
|
353
|
-
cursor:
|
|
450
|
+
cursor: z3.string().nullable()
|
|
354
451
|
});
|
|
355
|
-
var AuditEntrySchema =
|
|
356
|
-
id:
|
|
357
|
-
at:
|
|
358
|
-
action:
|
|
359
|
-
actor:
|
|
360
|
-
target:
|
|
361
|
-
until:
|
|
362
|
-
lapsed:
|
|
452
|
+
var AuditEntrySchema = z3.object({
|
|
453
|
+
id: z3.string(),
|
|
454
|
+
at: z3.string(),
|
|
455
|
+
action: z3.enum(["impersonation.start", "impersonation.stop"]),
|
|
456
|
+
actor: z3.string(),
|
|
457
|
+
target: z3.string(),
|
|
458
|
+
until: z3.string().optional(),
|
|
459
|
+
lapsed: z3.boolean().optional()
|
|
363
460
|
});
|
|
364
|
-
var AuditListSchema =
|
|
365
|
-
var ImpersonateRequestSchema =
|
|
461
|
+
var AuditListSchema = z3.object({ entries: z3.array(AuditEntrySchema) });
|
|
462
|
+
var ImpersonateRequestSchema = z3.object({ email: z3.string().trim().toLowerCase().min(3) });
|
|
366
463
|
|
|
367
464
|
// ../../packages/shared/src/comments.ts
|
|
368
|
-
import { z as
|
|
465
|
+
import { z as z4 } from "zod";
|
|
369
466
|
var COMMENT_MAX_CHARS = 4e3;
|
|
370
467
|
var COMMENT_PATH_MAX = 1024;
|
|
371
|
-
var CommentAuthorSchema =
|
|
372
|
-
|
|
373
|
-
|
|
468
|
+
var CommentAuthorSchema = z4.discriminatedUnion("kind", [
|
|
469
|
+
z4.object({ kind: z4.literal("user"), email: z4.string() }),
|
|
470
|
+
z4.object({ kind: z4.literal("guest"), name: z4.string() })
|
|
374
471
|
]);
|
|
375
472
|
var ANCHOR_SELECTOR_MAX = 512;
|
|
376
|
-
var
|
|
377
|
-
var
|
|
378
|
-
var
|
|
473
|
+
var ANCHOR_QUOTE_MAX = 500;
|
|
474
|
+
var px = z4.number().finite().min(-1e7).max(1e7);
|
|
475
|
+
var percent = z4.number().min(0).max(100);
|
|
476
|
+
var CommentAnchorSchema = z4.object({
|
|
379
477
|
x: percent,
|
|
380
478
|
y: percent,
|
|
381
|
-
selector:
|
|
479
|
+
selector: z4.string().min(1).max(ANCHOR_SELECTOR_MAX).optional(),
|
|
382
480
|
ex: percent.optional(),
|
|
383
481
|
ey: percent.optional(),
|
|
384
482
|
docX: px.optional(),
|
|
385
483
|
docY: px.optional(),
|
|
386
484
|
scrollX: px.optional(),
|
|
387
485
|
scrollY: px.optional(),
|
|
388
|
-
vw:
|
|
389
|
-
vh:
|
|
390
|
-
hash:
|
|
486
|
+
vw: z4.number().finite().min(0).max(1e5).optional(),
|
|
487
|
+
vh: z4.number().finite().min(0).max(1e5).optional(),
|
|
488
|
+
hash: z4.string().max(COMMENT_PATH_MAX).optional(),
|
|
489
|
+
ew: percent.optional(),
|
|
490
|
+
eh: percent.optional(),
|
|
491
|
+
docW: z4.number().finite().min(0).max(1e7).optional(),
|
|
492
|
+
docH: z4.number().finite().min(0).max(1e7).optional(),
|
|
493
|
+
quote: z4.string().min(1).max(ANCHOR_QUOTE_MAX).optional()
|
|
391
494
|
});
|
|
392
|
-
var CommentSchema =
|
|
393
|
-
id:
|
|
495
|
+
var CommentSchema = z4.object({
|
|
496
|
+
id: z4.string(),
|
|
394
497
|
/** the thread this replies to; null for the comment that starts a thread */
|
|
395
|
-
parentId:
|
|
498
|
+
parentId: z4.string().nullable(),
|
|
396
499
|
/** the page, as normalizeCommentPath() writes it ('/', '/docs/', '/about.html') */
|
|
397
|
-
path:
|
|
500
|
+
path: z4.string(),
|
|
398
501
|
/** the version the site was serving when the comment was made (null: none was) */
|
|
399
|
-
versionId:
|
|
502
|
+
versionId: z4.string().nullable(),
|
|
400
503
|
anchor: CommentAnchorSchema.nullable(),
|
|
401
|
-
body:
|
|
504
|
+
body: z4.string(),
|
|
402
505
|
author: CommentAuthorSchema,
|
|
403
|
-
createdAt:
|
|
506
|
+
createdAt: z4.string(),
|
|
404
507
|
/** threads only: when and by whom it was resolved (null: open) */
|
|
405
|
-
resolvedAt:
|
|
508
|
+
resolvedAt: z4.string().nullable(),
|
|
406
509
|
resolvedBy: CommentAuthorSchema.nullable(),
|
|
407
510
|
/** whether the caller may delete it (policy's answer, so clients don't guess) */
|
|
408
|
-
canDelete:
|
|
511
|
+
canDelete: z4.boolean()
|
|
409
512
|
});
|
|
410
|
-
var CommentListSchema =
|
|
411
|
-
site:
|
|
412
|
-
name:
|
|
413
|
-
url:
|
|
414
|
-
currentVersionId:
|
|
513
|
+
var CommentListSchema = z4.object({
|
|
514
|
+
site: z4.object({
|
|
515
|
+
name: z4.string(),
|
|
516
|
+
url: z4.string(),
|
|
517
|
+
currentVersionId: z4.string().nullable()
|
|
415
518
|
}),
|
|
416
|
-
comments:
|
|
417
|
-
can:
|
|
418
|
-
create:
|
|
419
|
-
resolve:
|
|
519
|
+
comments: z4.array(CommentSchema),
|
|
520
|
+
can: z4.object({
|
|
521
|
+
create: z4.boolean(),
|
|
522
|
+
resolve: z4.boolean(),
|
|
420
523
|
/** delete anyone's comment (everyone may delete their own) */
|
|
421
|
-
deleteAny:
|
|
524
|
+
deleteAny: z4.boolean()
|
|
422
525
|
})
|
|
423
526
|
});
|
|
424
|
-
var CreateCommentRequestSchema =
|
|
425
|
-
body:
|
|
426
|
-
path:
|
|
427
|
-
parentId:
|
|
527
|
+
var CreateCommentRequestSchema = z4.object({
|
|
528
|
+
body: z4.string().trim().min(1).max(COMMENT_MAX_CHARS),
|
|
529
|
+
path: z4.string().max(COMMENT_PATH_MAX).optional(),
|
|
530
|
+
parentId: z4.string().min(1).max(100).optional(),
|
|
428
531
|
anchor: CommentAnchorSchema.optional()
|
|
429
532
|
});
|
|
430
|
-
var UpdateCommentRequestSchema =
|
|
533
|
+
var UpdateCommentRequestSchema = z4.object({ resolved: z4.boolean() });
|
|
431
534
|
function commentThreads(comments) {
|
|
432
535
|
const threads = /* @__PURE__ */ new Map();
|
|
433
536
|
for (const c of comments) {
|
|
@@ -451,6 +554,60 @@ var CLI_PACKAGE = "@nearform/fling";
|
|
|
451
554
|
var REPO_SLUG = "nearform/fling";
|
|
452
555
|
var DEFAULT_INSTALL_COMMAND = `npm install -g ${CLI_PACKAGE}`;
|
|
453
556
|
|
|
557
|
+
// ../../packages/shared/src/groups.ts
|
|
558
|
+
import { z as z5 } from "zod";
|
|
559
|
+
var GROUP_MEMBERS_MAX = 500;
|
|
560
|
+
var GROUP_SLUG_MAX = 50;
|
|
561
|
+
var GROUP_NAME_MAX = 100;
|
|
562
|
+
var GROUP_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
563
|
+
function isValidGroupSlug(slug) {
|
|
564
|
+
return slug.length <= GROUP_SLUG_MAX && GROUP_SLUG_RE.test(slug) && !slug.includes("--");
|
|
565
|
+
}
|
|
566
|
+
var GROUP_SLUG_RULES = `1-${GROUP_SLUG_MAX} lowercase letters, digits and single hyphens`;
|
|
567
|
+
var GroupSlugSchema = z5.string().trim().toLowerCase().refine(isValidGroupSlug, `A group's slug is ${GROUP_SLUG_RULES}`);
|
|
568
|
+
var GroupNameSchema = z5.string().trim().min(1).max(GROUP_NAME_MAX);
|
|
569
|
+
var memberEmails = z5.array(z5.string().trim().toLowerCase().min(3).max(254)).max(GROUP_MEMBERS_MAX, `A group holds at most ${GROUP_MEMBERS_MAX} people`);
|
|
570
|
+
var GroupSummarySchema = z5.object({
|
|
571
|
+
slug: z5.string(),
|
|
572
|
+
name: z5.string(),
|
|
573
|
+
/** the email of the user who made it, the only one who may change it */
|
|
574
|
+
owner: z5.string(),
|
|
575
|
+
size: z5.number(),
|
|
576
|
+
createdAt: z5.string(),
|
|
577
|
+
updatedAt: z5.string()
|
|
578
|
+
});
|
|
579
|
+
var GroupListSchema = z5.object({ groups: z5.array(GroupSummarySchema) });
|
|
580
|
+
var GroupDetailSchema = GroupSummarySchema.extend({
|
|
581
|
+
members: z5.array(AudienceEmailSchema),
|
|
582
|
+
canManage: z5.boolean()
|
|
583
|
+
});
|
|
584
|
+
var CreateGroupRequestSchema = z5.object({
|
|
585
|
+
slug: GroupSlugSchema,
|
|
586
|
+
name: GroupNameSchema,
|
|
587
|
+
members: memberEmails.optional()
|
|
588
|
+
});
|
|
589
|
+
var UpdateGroupRequestSchema = z5.object({ name: GroupNameSchema });
|
|
590
|
+
var GroupMembersRequestSchema = z5.object({
|
|
591
|
+
add: memberEmails.optional(),
|
|
592
|
+
remove: memberEmails.optional()
|
|
593
|
+
});
|
|
594
|
+
function splitGroups(values) {
|
|
595
|
+
const all = typeof values === "string" ? [values] : values;
|
|
596
|
+
return all.flatMap((v) => v.split(/[\s,;]+/)).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
597
|
+
}
|
|
598
|
+
function describeGroups(groups2, me) {
|
|
599
|
+
if (groups2.length === 0) return "No groups yet.";
|
|
600
|
+
return groups2.map((g) => `- ${groupLabel(g)}, owned by ${g.owner === me ? "you" : g.owner}`).join("\n");
|
|
601
|
+
}
|
|
602
|
+
function describeGroup(group) {
|
|
603
|
+
const members2 = group.members;
|
|
604
|
+
const who = group.canManage ? "you (you can change it)" : `${group.owner} (only they can change it)`;
|
|
605
|
+
return [
|
|
606
|
+
`${groupLabel(group)}, owned by ${who}.`,
|
|
607
|
+
members2.length ? `Members: ${audienceList(members2)}.` : "No members yet."
|
|
608
|
+
].join("\n");
|
|
609
|
+
}
|
|
610
|
+
|
|
454
611
|
// ../../packages/shared/src/site-labels.ts
|
|
455
612
|
var SITE_NAME_MAX = 50;
|
|
456
613
|
var SHARE_TOKEN_LENGTH = 20;
|
|
@@ -485,47 +642,47 @@ function siteUrl(label, sitesDomain, protocol = "https:") {
|
|
|
485
642
|
}
|
|
486
643
|
|
|
487
644
|
// ../../packages/shared/src/instance.ts
|
|
488
|
-
import { z as
|
|
645
|
+
import { z as z6 } from "zod";
|
|
489
646
|
var INSTANCE_METADATA_PATH = "/.well-known/fling";
|
|
490
647
|
var HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
491
|
-
var color =
|
|
492
|
-
var InstanceAuthSchema =
|
|
493
|
-
kind:
|
|
494
|
-
displayName:
|
|
495
|
-
issuer:
|
|
496
|
-
emailDomains:
|
|
497
|
-
invitedEmails:
|
|
648
|
+
var color = z6.string().regex(HEX_COLOR_RE);
|
|
649
|
+
var InstanceAuthSchema = z6.looseObject({
|
|
650
|
+
kind: z6.string().min(1),
|
|
651
|
+
displayName: z6.string().min(1),
|
|
652
|
+
issuer: z6.string().optional(),
|
|
653
|
+
emailDomains: z6.array(z6.string()).default([]),
|
|
654
|
+
invitedEmails: z6.boolean().optional()
|
|
498
655
|
});
|
|
499
|
-
var BrandColorsSchema =
|
|
656
|
+
var BrandColorsSchema = z6.looseObject({
|
|
500
657
|
primary: color.optional(),
|
|
501
658
|
primaryContent: color.optional(),
|
|
502
659
|
accent: color.optional(),
|
|
503
660
|
ink: color.optional(),
|
|
504
661
|
highlight: color.optional()
|
|
505
662
|
});
|
|
506
|
-
var InstanceBrandSchema =
|
|
663
|
+
var InstanceBrandSchema = z6.looseObject({
|
|
507
664
|
/** https: or data: URL of the logo mark shown before the name (light theme). */
|
|
508
|
-
logoUrl:
|
|
665
|
+
logoUrl: z6.string().optional(),
|
|
509
666
|
/** The same for the dark theme; defaults to `logoUrl`. */
|
|
510
|
-
logoDarkUrl:
|
|
511
|
-
faviconUrl:
|
|
667
|
+
logoDarkUrl: z6.string().optional(),
|
|
668
|
+
faviconUrl: z6.string().optional(),
|
|
512
669
|
colors: BrandColorsSchema.default({})
|
|
513
670
|
});
|
|
514
|
-
var InstanceMetadataSchema =
|
|
515
|
-
version:
|
|
671
|
+
var InstanceMetadataSchema = z6.looseObject({
|
|
672
|
+
version: z6.number().int().min(1),
|
|
516
673
|
/** Display name, e.g. "Fling". */
|
|
517
|
-
name:
|
|
674
|
+
name: z6.string().min(1),
|
|
518
675
|
/** e.g. `https://fling.example.test` (no trailing slash) */
|
|
519
|
-
appOrigin:
|
|
676
|
+
appOrigin: z6.string().url(),
|
|
520
677
|
/** host[:port] sites hang off: a site is `<name>.<sitesDomain>` */
|
|
521
|
-
sitesDomain:
|
|
522
|
-
siteScheme:
|
|
678
|
+
sitesDomain: z6.string().min(1),
|
|
679
|
+
siteScheme: z6.enum(["http", "https"]),
|
|
523
680
|
auth: InstanceAuthSchema,
|
|
524
681
|
brand: InstanceBrandSchema.default({ colors: {} }),
|
|
525
682
|
/** Feature flags: a client hides what is missing or false. */
|
|
526
|
-
capabilities:
|
|
683
|
+
capabilities: z6.record(z6.string(), z6.boolean()).default({}),
|
|
527
684
|
/** How to install the CLI, e.g. `npm install -g <package>`. */
|
|
528
|
-
installCommand:
|
|
685
|
+
installCommand: z6.string().min(1)
|
|
529
686
|
});
|
|
530
687
|
function instanceSitesPattern(instance) {
|
|
531
688
|
return `${instance.siteScheme}://<name>.${instance.sitesDomain}`;
|
|
@@ -549,15 +706,22 @@ function instanceMetadataUrl(serverUrl) {
|
|
|
549
706
|
}
|
|
550
707
|
|
|
551
708
|
// ../../packages/shared/src/kvs.ts
|
|
552
|
-
import { z as
|
|
553
|
-
|
|
709
|
+
import { z as z7 } from "zod";
|
|
710
|
+
|
|
711
|
+
// ../../packages/shared/src/route-keys.ts
|
|
712
|
+
var SITE_PASS_TTL_SECONDS = 5 * 60;
|
|
713
|
+
|
|
714
|
+
// ../../packages/shared/src/kvs.ts
|
|
715
|
+
var KvsSiteValueSchema = z7.object({
|
|
554
716
|
/** siteId — S3 prefix is `sites/<id>/<v>/` */
|
|
555
|
-
id:
|
|
717
|
+
id: z7.string(),
|
|
556
718
|
/** current versionId */
|
|
557
|
-
v:
|
|
558
|
-
vis:
|
|
719
|
+
v: z7.string(),
|
|
720
|
+
vis: z7.enum(["private", "public"]),
|
|
559
721
|
/** serve /index.html for unknown extensionless paths */
|
|
560
|
-
spa:
|
|
722
|
+
spa: z7.boolean(),
|
|
723
|
+
/** present when the site is restricted (private with an audience): the edge wants a site pass */
|
|
724
|
+
r: z7.literal(1).optional()
|
|
561
725
|
});
|
|
562
726
|
|
|
563
727
|
// ../../packages/shared/src/listing.ts
|
|
@@ -577,13 +741,13 @@ function extractHtmlTitle(html) {
|
|
|
577
741
|
}
|
|
578
742
|
function renderHtmlListing(siteName2, entries) {
|
|
579
743
|
const pages = entries.filter((e) => isHtmlPath(e.path) && e.path.toLowerCase() !== "index.html").sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
580
|
-
const
|
|
744
|
+
const groups2 = /* @__PURE__ */ new Map();
|
|
581
745
|
for (const page of pages) {
|
|
582
746
|
const slash = page.path.lastIndexOf("/");
|
|
583
747
|
const folder = slash === -1 ? "" : page.path.slice(0, slash);
|
|
584
|
-
|
|
748
|
+
groups2.set(folder, [...groups2.get(folder) ?? [], page]);
|
|
585
749
|
}
|
|
586
|
-
const sections = [...
|
|
750
|
+
const sections = [...groups2.entries()].sort(([a], [b]) => a === "" ? -1 : b === "" ? 1 : a < b ? -1 : 1).map(([folder, items]) => {
|
|
587
751
|
const heading = folder ? `<h2>${escapeHtml(folder)}/</h2>` : "";
|
|
588
752
|
const rows = items.map((item) => {
|
|
589
753
|
const file = item.path.slice(item.path.lastIndexOf("/") + 1);
|
|
@@ -641,7 +805,19 @@ ${body}
|
|
|
641
805
|
}
|
|
642
806
|
|
|
643
807
|
// ../../packages/shared/src/mcp-tools.ts
|
|
644
|
-
import { z as
|
|
808
|
+
import { z as z8 } from "zod";
|
|
809
|
+
|
|
810
|
+
// ../../packages/shared/src/site-sort.ts
|
|
811
|
+
var SITE_SORTS = ["updated", "created", "name"];
|
|
812
|
+
var DEFAULT_SITE_SORT = "updated";
|
|
813
|
+
var byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
814
|
+
function sortSites(sites, sort = DEFAULT_SITE_SORT) {
|
|
815
|
+
const newest = (key) => (a, b) => Date.parse(b[key]) - Date.parse(a[key]) || byName(a, b);
|
|
816
|
+
const compare = sort === "name" ? byName : newest(sort === "created" ? "createdAt" : "updatedAt");
|
|
817
|
+
return [...sites].sort(compare);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ../../packages/shared/src/mcp-tools.ts
|
|
645
821
|
var MCP_PUBLISH_MAX_BYTES = 4 * 1024 * 1024;
|
|
646
822
|
var MCP_UPLOAD_CHUNK_MAX_CHARS = 4 * 1024 * 1024;
|
|
647
823
|
var MCP_UPLOAD_TTL_SECONDS = 60 * 60;
|
|
@@ -670,23 +846,28 @@ var INLINE_ONLY = "If you created the content in this conversation, pass it dire
|
|
|
670
846
|
var NEVER_PUBLISHED = "Fling never publishes dotfiles, gitignored files, secret files (keys, certs, credentials) or files that contain secrets (API keys, tokens, private keys, passwords in database URLs); it stops and tells you which file and line. Remove the secret (or leave the file out) and retry \u2014 there is no way to override this.";
|
|
671
847
|
var ENCODING = '`encoding` is "utf8" (default, for text: HTML, CSS, JS, SVG, JSON, Markdown\u2026) or "base64" (binary: images, fonts, PDFs).';
|
|
672
848
|
var LIST_HTML = "If the files have no root index.html but do include HTML pages, the call does not publish: it reports how many HTML pages there are and asks you to check with the user. Ask them whether to generate an index.html listing those pages (retry with `listHtml: true`) or to publish without one (`listHtml: false` \u2014 the site root will 404, the pages still work at their own paths), then retry.";
|
|
673
|
-
var listHtml =
|
|
849
|
+
var listHtml = z8.boolean().optional().describe(
|
|
674
850
|
"Only matters when there is no root index.html: true \u2192 generate an index.html linking every HTML page; false \u2192 publish without one (root 404s). Omit until the user has decided"
|
|
675
851
|
);
|
|
676
|
-
var siteName =
|
|
677
|
-
var sitePath =
|
|
852
|
+
var siteName = z8.string().min(1).max(200).describe('Site name, e.g. "q3-report"');
|
|
853
|
+
var sitePath = z8.string().min(1).max(1024).describe(
|
|
678
854
|
'Site-relative path with forward slashes, e.g. "index.html", "css/app.css", "img/logo.png". The root page must be "index.html". No dotfiles (".env", ".well-known/\u2026")'
|
|
679
855
|
);
|
|
680
|
-
var
|
|
681
|
-
var
|
|
682
|
-
var
|
|
683
|
-
var
|
|
856
|
+
var emails = z8.array(z8.string().min(3).max(254)).max(AUDIENCE_EMAILS_MAX);
|
|
857
|
+
var groupSlugs = z8.array(z8.string().min(1).max(100)).max(AUDIENCE_GROUPS_MAX);
|
|
858
|
+
var groupSlug = z8.string().min(1).max(100).describe(`The group's slug, e.g. "design"`);
|
|
859
|
+
var members = z8.array(z8.string().min(3).max(254)).max(GROUP_MEMBERS_MAX);
|
|
860
|
+
var groupName = z8.string().min(1).max(GROUP_NAME_MAX);
|
|
861
|
+
var commentPath = z8.string().max(1024);
|
|
862
|
+
var commentId = z8.string().min(1).max(100);
|
|
863
|
+
var encoding = z8.enum(["utf8", "base64"]).optional().describe('How `content` is encoded: "utf8" (default) for text, "base64" for binary files');
|
|
864
|
+
var spa = z8.boolean().optional().describe(
|
|
684
865
|
"Single-page app: serve index.html for unknown paths (client-side routing). Omit to keep the current setting"
|
|
685
866
|
);
|
|
686
867
|
function mcpToolsFor(w) {
|
|
687
868
|
const NAME_RULES = `Site names are 1-${SITE_NAME_MAX} chars of lowercase letters, digits and single hyphens, and are one global namespace (first come, first served): the site is served at ${w.sitesPattern}.`;
|
|
688
869
|
const VISIBILITY = `Sites are private by default (only ${w.privateAudience} can open them); pass \`public: true\` only if the user asks for a public site. Share links (fling_share) are public to anyone who has them.`;
|
|
689
|
-
const isPublic =
|
|
870
|
+
const isPublic = z8.boolean().optional().describe(
|
|
690
871
|
`true \u2192 anyone can view; false \u2192 only ${w.privateAudience}. Omit to keep the current setting (new sites are private)`
|
|
691
872
|
);
|
|
692
873
|
return {
|
|
@@ -700,9 +881,13 @@ function mcpToolsFor(w) {
|
|
|
700
881
|
list: tool({
|
|
701
882
|
name: "fling_list",
|
|
702
883
|
title: "List sites",
|
|
703
|
-
description: "List the user's Fling sites with URL, visibility and share link.",
|
|
884
|
+
description: "List the user's Fling sites with URL, visibility and share link. With `shared: true`, list instead the restricted sites other people have shared with the user (by address or through a group they're in), with URL, review link and owner.",
|
|
704
885
|
inputSchema: {
|
|
705
|
-
archived:
|
|
886
|
+
archived: z8.boolean().optional().describe("List archived sites instead of live ones"),
|
|
887
|
+
shared: z8.boolean().optional().describe("List the sites others have shared with the user instead of the user's own"),
|
|
888
|
+
sort: z8.enum(SITE_SORTS).optional().describe(
|
|
889
|
+
"Order: updated (last updated, newest first; the default), created (newest first) or name (A\u2013Z)"
|
|
890
|
+
)
|
|
706
891
|
},
|
|
707
892
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
708
893
|
}),
|
|
@@ -719,7 +904,7 @@ function mcpToolsFor(w) {
|
|
|
719
904
|
description: "List the files in a site's current version (or in `versionId`, from fling_info): path, size and content type, sorted by path. Use it to check what was actually published \u2014 e.g. when the user sees a 404, confirm the page exists at that path and that there is a root index.html (without one the site root 404s).",
|
|
720
905
|
inputSchema: {
|
|
721
906
|
name: siteName,
|
|
722
|
-
versionId:
|
|
907
|
+
versionId: z8.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served")
|
|
723
908
|
},
|
|
724
909
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
725
910
|
}),
|
|
@@ -727,7 +912,7 @@ function mcpToolsFor(w) {
|
|
|
727
912
|
name: "fling_check_name",
|
|
728
913
|
title: "Check a site name",
|
|
729
914
|
description: "Check whether a site name can be used before publishing: `available` (a new site), `yours` (publishing updates the user's existing site), `taken` (someone else's \u2014 free `suggestions` are returned) or `invalid`. " + NAME_RULES,
|
|
730
|
-
inputSchema: { name:
|
|
915
|
+
inputSchema: { name: z8.string().min(1).max(200).describe("Site name to check") },
|
|
731
916
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
732
917
|
}),
|
|
733
918
|
publish: tool({
|
|
@@ -736,10 +921,10 @@ function mcpToolsFor(w) {
|
|
|
736
921
|
description: `Publish a static site to Fling in one call and get back its URL. Pass every file of the site inline in \`files\` (whole site, up to ${MCP_PUBLISH_MAX_BYTES / 1024 / 1024} MB decoded in total; bigger sites: fling_upload_begin / fling_upload_file / fling_upload_finish). ${INLINE_ONLY} ${ENCODING} Publishing replaces the whole site with exactly these files (it is not a patch: include unchanged files too); publishing identical content again is a no-op. ${NAME_REQUIRED} ${NAME_RULES} ${VISIBILITY} ${LIST_HTML} ${NEVER_PUBLISHED} The result includes \`warnings\`. Share the returned URL with the user; fling_files shows exactly which files the site has.`,
|
|
737
922
|
inputSchema: {
|
|
738
923
|
name: siteName,
|
|
739
|
-
files:
|
|
740
|
-
|
|
924
|
+
files: z8.array(
|
|
925
|
+
z8.object({
|
|
741
926
|
path: sitePath,
|
|
742
|
-
content:
|
|
927
|
+
content: z8.string().describe("The file content, encoded as `encoding` says"),
|
|
743
928
|
encoding
|
|
744
929
|
})
|
|
745
930
|
).min(1).max(MAX_FILES).describe("Every file of the site"),
|
|
@@ -761,11 +946,11 @@ function mcpToolsFor(w) {
|
|
|
761
946
|
title: "Upload a file (chunked)",
|
|
762
947
|
description: `Add one file to a chunked upload started with fling_upload_begin. Sending the same path again replaces it. A file larger than ${MCP_UPLOAD_CHUNK_MAX_CHARS / 1024 / 1024} MB of content can span calls: send the first part normally, then the following parts in order with \`append: true\` (same path and encoding; send appends one at a time, not in parallel). ${ENCODING} ${NEVER_PUBLISHED}`,
|
|
763
948
|
inputSchema: {
|
|
764
|
-
uploadId:
|
|
949
|
+
uploadId: z8.string().min(1).max(100).describe("From fling_upload_begin"),
|
|
765
950
|
path: sitePath,
|
|
766
|
-
content:
|
|
951
|
+
content: z8.string().max(MCP_UPLOAD_CHUNK_MAX_CHARS).describe("The file content (or the next part of it, with `append`)"),
|
|
767
952
|
encoding,
|
|
768
|
-
append:
|
|
953
|
+
append: z8.boolean().optional().describe("Append this content to the file sent earlier with the same path")
|
|
769
954
|
},
|
|
770
955
|
annotations: { destructiveHint: false, openWorldHint: false }
|
|
771
956
|
}),
|
|
@@ -774,7 +959,7 @@ function mcpToolsFor(w) {
|
|
|
774
959
|
title: "Finish a chunked upload",
|
|
775
960
|
description: `Publish everything sent with fling_upload_file as the new version of the site and return its URL. The site becomes exactly the uploaded files. ${LIST_HTML} The upload stays open, so just call fling_upload_finish again with \`listHtml\`. The result includes \`warnings\`.`,
|
|
776
961
|
inputSchema: {
|
|
777
|
-
uploadId:
|
|
962
|
+
uploadId: z8.string().min(1).max(100).describe("From fling_upload_begin"),
|
|
778
963
|
listHtml
|
|
779
964
|
},
|
|
780
965
|
annotations: { destructiveHint: false, openWorldHint: true }
|
|
@@ -785,25 +970,97 @@ function mcpToolsFor(w) {
|
|
|
785
970
|
description: "Get or create a public share link for a site: anyone with the link can view it without signing in (use it to show a private site to someone who cannot sign in). `refresh` replaces the link (the old one stops working); `revoke` removes it.",
|
|
786
971
|
inputSchema: {
|
|
787
972
|
name: siteName,
|
|
788
|
-
refresh:
|
|
789
|
-
revoke:
|
|
973
|
+
refresh: z8.boolean().optional().describe("Rotate the link"),
|
|
974
|
+
revoke: z8.boolean().optional().describe("Remove the link")
|
|
790
975
|
},
|
|
791
976
|
annotations: { destructiveHint: false, openWorldHint: true }
|
|
792
977
|
}),
|
|
793
978
|
setVisibility: tool({
|
|
794
979
|
name: "fling_set_visibility",
|
|
795
980
|
title: "Set visibility",
|
|
796
|
-
description: `Make a site private (
|
|
797
|
-
inputSchema: { name: siteName, visibility:
|
|
981
|
+
description: `Make a site private (open to ${w.privateAudience}) or public (anyone can open the site; its review page and comments still need sign-in).`,
|
|
982
|
+
inputSchema: { name: siteName, visibility: z8.enum(["private", "public"]) },
|
|
798
983
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
799
984
|
}),
|
|
985
|
+
setAudience: tool({
|
|
986
|
+
name: "fling_set_audience",
|
|
987
|
+
title: "Set who can see a private site",
|
|
988
|
+
description: `Limit a private site to listed people and groups (its audience), or open it again to ${w.privateAudience}. \`add\` and \`remove\` change the addresses and \`set\` replaces them; \`addGroups\`, \`removeGroups\` and \`setGroups\` do the same for groups, by slug (fling_groups lists them); \`clear\` empties both (then every signed-in member can see it again). The owner always sees the site. Each address must be one that can sign in to ${w.name}. The audience only applies while the site is private (fling_set_visibility); share links still open it to anyone. Removing someone takes up to 5 minutes to shut them out. fling_info shows the audience.`,
|
|
989
|
+
inputSchema: {
|
|
990
|
+
name: siteName,
|
|
991
|
+
add: emails.optional().describe("Addresses to let in"),
|
|
992
|
+
remove: emails.optional().describe("Addresses to take out"),
|
|
993
|
+
set: emails.optional().describe("Replace the addresses with exactly these"),
|
|
994
|
+
addGroups: groupSlugs.optional().describe("Groups (slugs) to let in"),
|
|
995
|
+
removeGroups: groupSlugs.optional().describe("Groups (slugs) to take out"),
|
|
996
|
+
setGroups: groupSlugs.optional().describe("Replace the groups with exactly these slugs"),
|
|
997
|
+
clear: z8.boolean().optional().describe("Empty the audience (addresses and groups): every signed-in member sees it")
|
|
998
|
+
},
|
|
999
|
+
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1000
|
+
}),
|
|
1001
|
+
groups: tool({
|
|
1002
|
+
name: "fling_groups",
|
|
1003
|
+
title: "List groups",
|
|
1004
|
+
description: `List the groups on ${w.name}: named lists of people (a team) that any member can use in a site's audience (fling_set_audience \`addGroups\`). Each has a slug, a name, a size and an owner; \`mine\` lists only the user's own. \`members\` lists only the groups of exactly those people: use it before making a group, or to swap a list of addresses in an audience for a group that already holds them.`,
|
|
1005
|
+
inputSchema: {
|
|
1006
|
+
mine: z8.boolean().optional().describe("Only the groups the user owns"),
|
|
1007
|
+
members: members.optional().describe("Only the groups whose members are exactly these addresses")
|
|
1008
|
+
},
|
|
1009
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1010
|
+
}),
|
|
1011
|
+
groupShow: tool({
|
|
1012
|
+
name: "fling_group_show",
|
|
1013
|
+
title: "Show a group",
|
|
1014
|
+
description: "Show a group and its members (each marked when they have not signed in yet), and whether the user owns it (only the owner can change it).",
|
|
1015
|
+
inputSchema: { slug: groupSlug },
|
|
1016
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1017
|
+
}),
|
|
1018
|
+
groupCreate: tool({
|
|
1019
|
+
name: "fling_group_create",
|
|
1020
|
+
title: "Create a group",
|
|
1021
|
+
description: `Create a group the user owns: a named list of people that anyone signed in to ${w.name} can see and use in a site's audience, but only the user can change. The slug is ${GROUP_SLUG_RULES}, unique on ${w.name}; each member must be an address that can sign in (at most ${GROUP_MEMBERS_MAX}).`,
|
|
1022
|
+
inputSchema: {
|
|
1023
|
+
slug: groupSlug,
|
|
1024
|
+
name: groupName.describe('Its display name, e.g. "Design team"'),
|
|
1025
|
+
members: members.optional().describe("Addresses to put in it")
|
|
1026
|
+
},
|
|
1027
|
+
annotations: { destructiveHint: false, openWorldHint: false }
|
|
1028
|
+
}),
|
|
1029
|
+
groupMembers: tool({
|
|
1030
|
+
name: "fling_group_members",
|
|
1031
|
+
title: "Change who is in a group",
|
|
1032
|
+
description: "Add people to a group the user owns, or take them out. Every site whose audience names the group follows: someone taken out is shut out within 5 minutes.",
|
|
1033
|
+
inputSchema: {
|
|
1034
|
+
slug: groupSlug,
|
|
1035
|
+
add: members.optional().describe("Addresses to add"),
|
|
1036
|
+
remove: members.optional().describe("Addresses to take out")
|
|
1037
|
+
},
|
|
1038
|
+
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1039
|
+
}),
|
|
1040
|
+
groupRename: tool({
|
|
1041
|
+
name: "fling_group_rename",
|
|
1042
|
+
title: "Rename a group",
|
|
1043
|
+
description: "Change the display name of a group the user owns (the slug stays).",
|
|
1044
|
+
inputSchema: { slug: groupSlug, name: groupName.describe("The new display name") },
|
|
1045
|
+
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1046
|
+
}),
|
|
1047
|
+
groupDelete: tool({
|
|
1048
|
+
name: "fling_group_delete",
|
|
1049
|
+
title: "Delete a group",
|
|
1050
|
+
description: "Permanently delete a group the user owns. Sites whose audience names it keep their restriction, and anyone it let in loses access (a site limited to it alone is left to its owner). Cannot be undone \u2014 confirm with the user first and pass `confirm: true`.",
|
|
1051
|
+
inputSchema: {
|
|
1052
|
+
slug: groupSlug,
|
|
1053
|
+
confirm: z8.literal(true).describe("Must be true: the user has confirmed the deletion")
|
|
1054
|
+
},
|
|
1055
|
+
annotations: { destructiveHint: true, openWorldHint: false }
|
|
1056
|
+
}),
|
|
800
1057
|
archive: tool({
|
|
801
1058
|
name: "fling_archive",
|
|
802
1059
|
title: "Archive or restore a site",
|
|
803
1060
|
description: "Archive a site (it stops being served and is hidden from lists, but its name, files and versions are kept) or, with `unarchive`, bring it back. Prefer this over deleting.",
|
|
804
1061
|
inputSchema: {
|
|
805
1062
|
name: siteName,
|
|
806
|
-
unarchive:
|
|
1063
|
+
unarchive: z8.boolean().optional().describe("Restore an archived site")
|
|
807
1064
|
},
|
|
808
1065
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
809
1066
|
}),
|
|
@@ -813,7 +1070,7 @@ function mcpToolsFor(w) {
|
|
|
813
1070
|
description: "Permanently delete a site and every version of it, freeing its name. Cannot be undone \u2014 confirm with the user first and pass `confirm: true`. Consider fling_archive instead.",
|
|
814
1071
|
inputSchema: {
|
|
815
1072
|
name: siteName,
|
|
816
|
-
confirm:
|
|
1073
|
+
confirm: z8.literal(true).describe("Must be true: the user has confirmed the deletion")
|
|
817
1074
|
},
|
|
818
1075
|
annotations: { destructiveHint: true, openWorldHint: false }
|
|
819
1076
|
}),
|
|
@@ -824,7 +1081,7 @@ function mcpToolsFor(w) {
|
|
|
824
1081
|
inputSchema: {
|
|
825
1082
|
name: siteName,
|
|
826
1083
|
path: commentPath.optional().describe('Only this page, e.g. "/" or "/docs/"'),
|
|
827
|
-
includeResolved:
|
|
1084
|
+
includeResolved: z8.boolean().optional().describe("Include resolved threads")
|
|
828
1085
|
},
|
|
829
1086
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
830
1087
|
}),
|
|
@@ -834,7 +1091,7 @@ function mcpToolsFor(w) {
|
|
|
834
1091
|
description: `Comment on a page of a site (shown on its review page, ${w.reviewPattern}), or reply to a thread (\`replyTo\`: a thread id from fling_comments_list). Comments are signed with the user's name, so only write one the user asked for.`,
|
|
835
1092
|
inputSchema: {
|
|
836
1093
|
name: siteName,
|
|
837
|
-
body:
|
|
1094
|
+
body: z8.string().min(1).max(COMMENT_MAX_CHARS).describe("The comment (plain text)"),
|
|
838
1095
|
path: commentPath.optional().describe('The page it is about (default "/"); ignored for a reply'),
|
|
839
1096
|
replyTo: commentId.optional().describe("Reply to this thread instead of starting one")
|
|
840
1097
|
},
|
|
@@ -847,7 +1104,7 @@ function mcpToolsFor(w) {
|
|
|
847
1104
|
inputSchema: {
|
|
848
1105
|
name: siteName,
|
|
849
1106
|
id: commentId.describe("The thread id"),
|
|
850
|
-
reopen:
|
|
1107
|
+
reopen: z8.boolean().optional().describe("Reopen a resolved thread")
|
|
851
1108
|
},
|
|
852
1109
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
853
1110
|
}),
|
|
@@ -864,7 +1121,7 @@ function mcpToolsFor(w) {
|
|
|
864
1121
|
description: "Point a site at an earlier version (ids from fling_info). Instant; nothing is uploaded.",
|
|
865
1122
|
inputSchema: {
|
|
866
1123
|
name: siteName,
|
|
867
|
-
versionId:
|
|
1124
|
+
versionId: z8.string().min(1).max(100).describe("Version id to serve")
|
|
868
1125
|
},
|
|
869
1126
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
870
1127
|
})
|
|
@@ -961,94 +1218,109 @@ function readinessWaitDisabled(env) {
|
|
|
961
1218
|
}
|
|
962
1219
|
|
|
963
1220
|
// ../../packages/shared/src/review-bridge.ts
|
|
964
|
-
import { z as
|
|
965
|
-
var tag = { fling:
|
|
966
|
-
var path =
|
|
967
|
-
var hash =
|
|
968
|
-
var BridgePinSchema =
|
|
969
|
-
id:
|
|
970
|
-
n:
|
|
1221
|
+
import { z as z9 } from "zod";
|
|
1222
|
+
var tag = { fling: z9.literal("review") };
|
|
1223
|
+
var path = z9.string().max(COMMENT_PATH_MAX);
|
|
1224
|
+
var hash = z9.string().max(COMMENT_PATH_MAX);
|
|
1225
|
+
var BridgePinSchema = z9.object({
|
|
1226
|
+
id: z9.string(),
|
|
1227
|
+
n: z9.number().int().min(1),
|
|
971
1228
|
anchor: CommentAnchorSchema,
|
|
972
|
-
selected:
|
|
973
|
-
resolved:
|
|
1229
|
+
selected: z9.boolean(),
|
|
1230
|
+
resolved: z9.boolean()
|
|
974
1231
|
});
|
|
975
|
-
var BridgeCursorSchema =
|
|
976
|
-
id:
|
|
977
|
-
name:
|
|
978
|
-
colour:
|
|
1232
|
+
var BridgeCursorSchema = z9.object({
|
|
1233
|
+
id: z9.string(),
|
|
1234
|
+
name: z9.string().max(100),
|
|
1235
|
+
colour: z9.string().max(32),
|
|
979
1236
|
anchor: CommentAnchorSchema
|
|
980
1237
|
});
|
|
981
|
-
var ShellMessageSchema =
|
|
982
|
-
|
|
1238
|
+
var ShellMessageSchema = z9.discriminatedUnion("type", [
|
|
1239
|
+
z9.object({ ...tag, type: z9.literal("ready") }),
|
|
983
1240
|
/** the frame is on `path` (normalised, see normalizeCommentPath) with fragment `hash` ('' or '#…') */
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1241
|
+
z9.object({ ...tag, type: z9.literal("location"), path, hash, title: z9.string().max(300) }),
|
|
1242
|
+
/** a pin placed while placing: a click (a spot) or a drag (a box) */
|
|
1243
|
+
z9.object({ ...tag, type: z9.literal("pinPlaced"), anchor: CommentAnchorSchema }),
|
|
1244
|
+
/** the reviewer selected text in the page: an anchor on it, with its quote */
|
|
1245
|
+
z9.object({ ...tag, type: z9.literal("textSelected"), anchor: CommentAnchorSchema }),
|
|
1246
|
+
z9.object({ ...tag, type: z9.literal("pinCancelled") }),
|
|
1247
|
+
z9.object({ ...tag, type: z9.literal("pinClicked"), id: z9.string() }),
|
|
988
1248
|
/** this reviewer's pointer on the page; null when it left the frame */
|
|
989
|
-
|
|
1249
|
+
z9.object({ ...tag, type: z9.literal("cursor"), anchor: CommentAnchorSchema.nullable() }),
|
|
990
1250
|
/** a link or form to another origin, stopped: the frame stays on the site */
|
|
991
|
-
|
|
992
|
-
|
|
1251
|
+
z9.object({ ...tag, type: z9.literal("blocked"), href: z9.string().max(2048) }),
|
|
1252
|
+
z9.object({ ...tag, type: z9.literal("escape") })
|
|
993
1253
|
]);
|
|
994
|
-
var PageMessageSchema =
|
|
1254
|
+
var PageMessageSchema = z9.discriminatedUnion("type", [
|
|
995
1255
|
/** load `path` (site-relative, may carry `?query` and `#hash`) in the frame */
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1256
|
+
z9.object({ ...tag, type: z9.literal("navigate"), path: z9.string().max(COMMENT_PATH_MAX * 2) }),
|
|
1257
|
+
z9.object({ ...tag, type: z9.literal("reload") }),
|
|
1258
|
+
z9.object({ ...tag, type: z9.literal("placing"), on: z9.boolean() }),
|
|
999
1259
|
/** the pins on the frame's current page */
|
|
1000
|
-
|
|
1260
|
+
z9.object({ ...tag, type: z9.literal("pins"), pins: z9.array(BridgePinSchema) }),
|
|
1001
1261
|
/** scroll the pin into view and draw attention to it */
|
|
1002
|
-
|
|
1003
|
-
|
|
1262
|
+
z9.object({ ...tag, type: z9.literal("focusPin"), id: z9.string() }),
|
|
1263
|
+
z9.object({ ...tag, type: z9.literal("cursors"), cursors: z9.array(BridgeCursorSchema) })
|
|
1004
1264
|
]);
|
|
1005
1265
|
|
|
1006
1266
|
// ../../packages/shared/src/review-live.ts
|
|
1007
|
-
import { z as
|
|
1267
|
+
import { z as z10 } from "zod";
|
|
1008
1268
|
var LIVE_PICTURE_MAX = 1024;
|
|
1009
|
-
var LivePeerSchema =
|
|
1010
|
-
id:
|
|
1011
|
-
name:
|
|
1012
|
-
colour:
|
|
1269
|
+
var LivePeerSchema = z10.object({
|
|
1270
|
+
id: z10.string(),
|
|
1271
|
+
name: z10.string().max(100),
|
|
1272
|
+
colour: z10.string().max(32),
|
|
1013
1273
|
/** their profile picture (https), when sign-in gave one; else clients show initials */
|
|
1014
|
-
picture:
|
|
1274
|
+
picture: z10.string().max(LIVE_PICTURE_MAX).optional()
|
|
1015
1275
|
});
|
|
1016
|
-
var LiveTicketSchema =
|
|
1017
|
-
url:
|
|
1018
|
-
ticket:
|
|
1276
|
+
var LiveTicketSchema = z10.object({
|
|
1277
|
+
url: z10.string(),
|
|
1278
|
+
ticket: z10.string(),
|
|
1019
1279
|
/** seconds the ticket is good for: connect before then */
|
|
1020
|
-
expiresIn:
|
|
1280
|
+
expiresIn: z10.number().int()
|
|
1021
1281
|
});
|
|
1022
|
-
var path2 =
|
|
1023
|
-
var hash2 =
|
|
1024
|
-
var LiveClientMessageSchema =
|
|
1025
|
-
|
|
1282
|
+
var path2 = z10.string().max(COMMENT_PATH_MAX);
|
|
1283
|
+
var hash2 = z10.string().max(COMMENT_PATH_MAX);
|
|
1284
|
+
var LiveClientMessageSchema = z10.discriminatedUnion("type", [
|
|
1285
|
+
z10.object({ type: z10.literal("hello") }),
|
|
1026
1286
|
/** where this peer's pointer is; `anchor` null when it left the page */
|
|
1027
|
-
|
|
1028
|
-
type:
|
|
1287
|
+
z10.object({
|
|
1288
|
+
type: z10.literal("cursor"),
|
|
1029
1289
|
path: path2,
|
|
1030
1290
|
hash: hash2,
|
|
1031
1291
|
anchor: CommentAnchorSchema.nullable()
|
|
1032
1292
|
}),
|
|
1033
|
-
|
|
1293
|
+
z10.object({ type: z10.literal("ping") })
|
|
1034
1294
|
]);
|
|
1035
|
-
var LiveServerMessageSchema =
|
|
1295
|
+
var LiveServerMessageSchema = z10.discriminatedUnion("type", [
|
|
1036
1296
|
/** the answer to `hello`: who this connection is, and who else is in the room */
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
type:
|
|
1042
|
-
peerId:
|
|
1297
|
+
z10.object({ type: z10.literal("welcome"), you: LivePeerSchema, peers: z10.array(LivePeerSchema) }),
|
|
1298
|
+
z10.object({ type: z10.literal("join"), peer: LivePeerSchema }),
|
|
1299
|
+
z10.object({ type: z10.literal("leave"), peerId: z10.string() }),
|
|
1300
|
+
z10.object({
|
|
1301
|
+
type: z10.literal("cursor"),
|
|
1302
|
+
peerId: z10.string(),
|
|
1043
1303
|
path: path2,
|
|
1044
1304
|
hash: hash2,
|
|
1045
1305
|
anchor: CommentAnchorSchema.nullable()
|
|
1046
1306
|
}),
|
|
1047
1307
|
/** the site's comments changed: refetch them */
|
|
1048
|
-
|
|
1049
|
-
|
|
1308
|
+
z10.object({ type: z10.literal("comments") }),
|
|
1309
|
+
z10.object({ type: z10.literal("pong") })
|
|
1050
1310
|
]);
|
|
1051
1311
|
|
|
1312
|
+
// src/cli.ts
|
|
1313
|
+
import chalk11 from "chalk";
|
|
1314
|
+
import { Command, Option } from "commander";
|
|
1315
|
+
|
|
1316
|
+
// src/commands/auth.ts
|
|
1317
|
+
import chalk5 from "chalk";
|
|
1318
|
+
|
|
1319
|
+
// src/auth.ts
|
|
1320
|
+
import { randomBytes } from "node:crypto";
|
|
1321
|
+
import { createServer } from "node:http";
|
|
1322
|
+
import { hostname as osHostname } from "node:os";
|
|
1323
|
+
|
|
1052
1324
|
// src/errors.ts
|
|
1053
1325
|
var EXIT_ERROR = 1;
|
|
1054
1326
|
var EXIT_NOT_LOGGED_IN = 2;
|
|
@@ -1088,7 +1360,7 @@ function exitCodeFor(error) {
|
|
|
1088
1360
|
// package.json
|
|
1089
1361
|
var package_default = {
|
|
1090
1362
|
name: "@nearform/fling",
|
|
1091
|
-
version: "0.
|
|
1363
|
+
version: "0.6.0",
|
|
1092
1364
|
license: "MIT",
|
|
1093
1365
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
1094
1366
|
type: "module",
|
|
@@ -1299,7 +1571,7 @@ function describeCause(cause) {
|
|
|
1299
1571
|
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1300
1572
|
import { homedir } from "node:os";
|
|
1301
1573
|
import { join } from "node:path";
|
|
1302
|
-
import { z as
|
|
1574
|
+
import { z as z11 } from "zod";
|
|
1303
1575
|
var CONFIG_DIR_NAME = ".fling";
|
|
1304
1576
|
var CONFIG_FILE_NAME = "config.json";
|
|
1305
1577
|
var ENV = {
|
|
@@ -1312,14 +1584,14 @@ var ENV = {
|
|
|
1312
1584
|
/** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
|
|
1313
1585
|
addServer: "FLING_ADD_SERVER"
|
|
1314
1586
|
};
|
|
1315
|
-
var serverEntrySchema =
|
|
1587
|
+
var serverEntrySchema = z11.object({
|
|
1316
1588
|
/** What the server said about itself at login. A copy this CLI can't read is just refetched. */
|
|
1317
1589
|
instance: InstanceMetadataSchema.optional().catch(void 0),
|
|
1318
|
-
apiKey:
|
|
1319
|
-
user:
|
|
1320
|
-
email:
|
|
1321
|
-
name:
|
|
1322
|
-
userSlug:
|
|
1590
|
+
apiKey: z11.string().min(1).optional().catch(void 0),
|
|
1591
|
+
user: z11.object({
|
|
1592
|
+
email: z11.string().optional(),
|
|
1593
|
+
name: z11.string().optional(),
|
|
1594
|
+
userSlug: z11.string().optional()
|
|
1323
1595
|
}).optional().catch(void 0)
|
|
1324
1596
|
});
|
|
1325
1597
|
function systemLocateOptions(env = process.env) {
|
|
@@ -1937,8 +2209,8 @@ function requireClient(ctx) {
|
|
|
1937
2209
|
}
|
|
1938
2210
|
|
|
1939
2211
|
// src/sites.ts
|
|
1940
|
-
import { z as
|
|
1941
|
-
var ShareResponseSchema2 =
|
|
2212
|
+
import { z as z12 } from "zod";
|
|
2213
|
+
var ShareResponseSchema2 = z12.looseObject({ shareUrl: z12.string() });
|
|
1942
2214
|
var sitePath2 = (name) => `/api/sites/${encodeURIComponent(name)}`;
|
|
1943
2215
|
function whoami(client) {
|
|
1944
2216
|
return client.request("GET", "/api/me", { schema: MeSchema });
|
|
@@ -1962,7 +2234,11 @@ async function listSites(client, opts = {}) {
|
|
|
1962
2234
|
schema: SiteListSchema,
|
|
1963
2235
|
query: { archived: opts.archived ? 1 : void 0 }
|
|
1964
2236
|
});
|
|
1965
|
-
return res.sites;
|
|
2237
|
+
return sortSites(res.sites, opts.sort);
|
|
2238
|
+
}
|
|
2239
|
+
async function listShared(client, opts = {}) {
|
|
2240
|
+
const res = await client.request("GET", "/api/shared", { schema: SharedSiteListSchema });
|
|
2241
|
+
return sortSites(res.sites, opts.sort);
|
|
1966
2242
|
}
|
|
1967
2243
|
function getSite(client, name) {
|
|
1968
2244
|
return client.request("GET", sitePath2(name), { schema: SiteDetailSchema });
|
|
@@ -1993,6 +2269,74 @@ async function setVisibility(client, name, visibility) {
|
|
|
1993
2269
|
const raw = await client.request("PATCH", sitePath2(name), { body: { visibility } });
|
|
1994
2270
|
return siteAfter(client, name, raw);
|
|
1995
2271
|
}
|
|
2272
|
+
async function getAudience(client, name) {
|
|
2273
|
+
const site = await getSite(client, name);
|
|
2274
|
+
if (!site.audience) throw noAudiences();
|
|
2275
|
+
return site;
|
|
2276
|
+
}
|
|
2277
|
+
var noAudiences = () => new CliError("This Fling does not support limiting a site to listed people", {
|
|
2278
|
+
hint: "Ask its operator to update it."
|
|
2279
|
+
});
|
|
2280
|
+
async function setAudience(client, name, change) {
|
|
2281
|
+
const current = await getAudience(client, name);
|
|
2282
|
+
const audience = current.audience;
|
|
2283
|
+
const site = await client.request("PATCH", sitePath2(name), {
|
|
2284
|
+
schema: SiteWithAudienceSchema,
|
|
2285
|
+
body: { audience: editAudienceInput(audience, change) }
|
|
2286
|
+
});
|
|
2287
|
+
if (!site.audience) throw noAudiences();
|
|
2288
|
+
return site;
|
|
2289
|
+
}
|
|
2290
|
+
function audienceWarnings(site) {
|
|
2291
|
+
const n = site.audience ? site.audience.emails.length + site.audience.groups.length : site.audienceSize;
|
|
2292
|
+
if (site.visibility === "public" && n > 0) {
|
|
2293
|
+
return [
|
|
2294
|
+
`${site.name} is public, so its audience doesn't apply yet; it does once the site is private: fling visibility ${site.name} private`
|
|
2295
|
+
];
|
|
2296
|
+
}
|
|
2297
|
+
if (site.restricted && site.shareUrl) {
|
|
2298
|
+
return [`${site.name} has a share link: anyone with it can still open the site`];
|
|
2299
|
+
}
|
|
2300
|
+
return [];
|
|
2301
|
+
}
|
|
2302
|
+
function audiencesEnabled(instance) {
|
|
2303
|
+
if (!instance) return false;
|
|
2304
|
+
return instance.capabilities.audiences ?? true;
|
|
2305
|
+
}
|
|
2306
|
+
var groupPath = (slug) => `/api/groups/${encodeURIComponent(slug)}`;
|
|
2307
|
+
async function listGroups(client, opts = {}) {
|
|
2308
|
+
const res = await client.request("GET", "/api/groups", {
|
|
2309
|
+
schema: GroupListSchema,
|
|
2310
|
+
query: {
|
|
2311
|
+
mine: opts.mine ? 1 : void 0,
|
|
2312
|
+
members: opts.members ? splitEmails(opts.members).join(",") : void 0
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
return res.groups;
|
|
2316
|
+
}
|
|
2317
|
+
function getGroup(client, slug) {
|
|
2318
|
+
return client.request("GET", groupPath(slug), { schema: GroupDetailSchema });
|
|
2319
|
+
}
|
|
2320
|
+
function createGroup(client, input) {
|
|
2321
|
+
return client.request("POST", "/api/groups", { schema: GroupDetailSchema, body: input });
|
|
2322
|
+
}
|
|
2323
|
+
function changeGroupMembers(client, slug, change) {
|
|
2324
|
+
return client.request("POST", `${groupPath(slug)}/members`, {
|
|
2325
|
+
schema: GroupDetailSchema,
|
|
2326
|
+
body: change
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
function renameGroup(client, slug, name) {
|
|
2330
|
+
return client.request("PATCH", groupPath(slug), { schema: GroupDetailSchema, body: { name } });
|
|
2331
|
+
}
|
|
2332
|
+
async function deleteGroup(client, slug) {
|
|
2333
|
+
await client.request("DELETE", groupPath(slug));
|
|
2334
|
+
return { slug, deleted: true };
|
|
2335
|
+
}
|
|
2336
|
+
function groupsEnabled(instance) {
|
|
2337
|
+
if (!instance) return false;
|
|
2338
|
+
return instance.capabilities.groups ?? true;
|
|
2339
|
+
}
|
|
1996
2340
|
async function archiveSite(client, name, opts = {}) {
|
|
1997
2341
|
const raw = await client.request(
|
|
1998
2342
|
"POST",
|
|
@@ -2212,6 +2556,7 @@ async function runComments(ctx, name, opts) {
|
|
|
2212
2556
|
}
|
|
2213
2557
|
const state = c.resolvedAt ? chalk6.green(" \u2713 resolved") : "";
|
|
2214
2558
|
lines.push(`${chalk6.dim(c.id)}${state} ${describe(c, list.site.currentVersionId)}`);
|
|
2559
|
+
if (c.anchor?.quote) lines.push(chalk6.dim(` > ${c.anchor.quote}`));
|
|
2215
2560
|
lines.push(...c.body.split("\n").map((l) => ` ${l}`));
|
|
2216
2561
|
for (const r of replies) {
|
|
2217
2562
|
lines.push(` ${chalk6.dim("\u21B3")} ${describe(r, list.site.currentVersionId)}`);
|
|
@@ -3111,7 +3456,7 @@ async function deployPrepared(ctx, client, instance, base, prepared, opts, ask)
|
|
|
3111
3456
|
local.files.map((f) => f.path),
|
|
3112
3457
|
{ progress: ctx.progress, warn: (m) => ctx.log.warn(m) }
|
|
3113
3458
|
)).ready;
|
|
3114
|
-
const who = instance ?
|
|
3459
|
+
const who = instance ? `${privateAudience(instance)} can open it` : "sign-in required";
|
|
3115
3460
|
const privateNote = `Private: ${who}. Share publicly with \`${ctx.binName} share ${result.name}\`.`;
|
|
3116
3461
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
3117
3462
|
const reviewNote = instance ? `Share for feedback (reviewers sign in to ${instance.name}):` : "Share for feedback:";
|
|
@@ -3194,9 +3539,76 @@ async function confirmSiteName(ctx, client, generated, base) {
|
|
|
3194
3539
|
}
|
|
3195
3540
|
}
|
|
3196
3541
|
|
|
3542
|
+
// src/commands/groups.ts
|
|
3543
|
+
import chalk8 from "chalk";
|
|
3544
|
+
function requireGroups(ctx) {
|
|
3545
|
+
const instance = ctx.config.instance;
|
|
3546
|
+
if (instance && !groupsEnabled(instance)) throw new CliError(`${instance.name} has no groups`);
|
|
3547
|
+
return requireClient(ctx);
|
|
3548
|
+
}
|
|
3549
|
+
async function runGroups(ctx, opts) {
|
|
3550
|
+
const client = requireGroups(ctx);
|
|
3551
|
+
const members2 = opts.members ? splitEmails(opts.members) : void 0;
|
|
3552
|
+
if (members2 && members2.length === 0) throw new CliError("Name at least one address");
|
|
3553
|
+
const [groups2, me] = await Promise.all([
|
|
3554
|
+
listGroups(client, { mine: opts.mine, members: members2 }),
|
|
3555
|
+
whoami(client)
|
|
3556
|
+
]);
|
|
3557
|
+
ctx.out.data(
|
|
3558
|
+
{ groups: groups2 },
|
|
3559
|
+
() => groups2.length ? renderTable(groups2, [
|
|
3560
|
+
{ header: "SLUG", value: (g) => g.slug },
|
|
3561
|
+
{ header: "NAME", value: (g) => g.name },
|
|
3562
|
+
{ header: "PEOPLE", value: (g) => g.size },
|
|
3563
|
+
{ header: "OWNER", value: (g) => g.owner === me.email ? "you" : g.owner }
|
|
3564
|
+
]) : chalk8.dim(members2 ? "No group has exactly those people." : describeGroups([]))
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3567
|
+
async function runGroupShow(ctx, slug) {
|
|
3568
|
+
const group = await getGroup(requireGroups(ctx), slug);
|
|
3569
|
+
ctx.out.data(group, () => describeGroup(group));
|
|
3570
|
+
}
|
|
3571
|
+
async function runGroupCreate(ctx, slug, opts) {
|
|
3572
|
+
const group = await createGroup(requireGroups(ctx), {
|
|
3573
|
+
slug,
|
|
3574
|
+
name: opts.name ?? slug,
|
|
3575
|
+
members: splitEmails(opts.add ?? [])
|
|
3576
|
+
});
|
|
3577
|
+
ctx.log.success(`Group ${group.slug} created`);
|
|
3578
|
+
ctx.out.data(group, () => describeGroup(group));
|
|
3579
|
+
}
|
|
3580
|
+
async function runGroupMembers(ctx, slug, emails2, how) {
|
|
3581
|
+
const list = splitEmails(emails2);
|
|
3582
|
+
if (list.length === 0) throw new CliError("Name at least one address");
|
|
3583
|
+
const group = await changeGroupMembers(requireGroups(ctx), slug, { [how]: list });
|
|
3584
|
+
ctx.out.data(group, () => describeGroup(group));
|
|
3585
|
+
}
|
|
3586
|
+
async function runGroupRename(ctx, slug, name) {
|
|
3587
|
+
const group = await renameGroup(requireGroups(ctx), slug, name);
|
|
3588
|
+
ctx.log.success(`Group ${group.slug} is now called ${group.name}`);
|
|
3589
|
+
ctx.out.data(group, () => describeGroup(group));
|
|
3590
|
+
}
|
|
3591
|
+
async function runGroupDelete(ctx, slug, opts) {
|
|
3592
|
+
const client = requireGroups(ctx);
|
|
3593
|
+
if (!opts.yes) {
|
|
3594
|
+
if (!ctx.interactive) {
|
|
3595
|
+
throw new CliError(`Refusing to delete group ${slug} without confirmation`, {
|
|
3596
|
+
hint: "Pass --yes to delete."
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
const ok = await ctx.confirm(
|
|
3600
|
+
`Delete group ${slug}? Sites whose audience names it keep their restriction, and anyone it let in loses access. This cannot be undone.`
|
|
3601
|
+
);
|
|
3602
|
+
if (!ok) throw new CliError("Cancelled");
|
|
3603
|
+
}
|
|
3604
|
+
const result = await deleteGroup(client, slug);
|
|
3605
|
+
ctx.log.success(`Group ${slug} deleted`);
|
|
3606
|
+
if (ctx.json) ctx.out.data(result, () => "");
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3197
3609
|
// src/commands/setup.ts
|
|
3198
3610
|
import { intro, outro, spinner } from "@clack/prompts";
|
|
3199
|
-
import
|
|
3611
|
+
import chalk9 from "chalk";
|
|
3200
3612
|
|
|
3201
3613
|
// src/claude.ts
|
|
3202
3614
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -3289,14 +3701,14 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
3289
3701
|
...baseCtx,
|
|
3290
3702
|
log: createLogger({
|
|
3291
3703
|
debug: baseCtx.config.debug,
|
|
3292
|
-
write: (line) => process.stderr.write(`${
|
|
3704
|
+
write: (line) => process.stderr.write(`${chalk9.gray("\u2502")} ${line}
|
|
3293
3705
|
`)
|
|
3294
3706
|
})
|
|
3295
3707
|
} : baseCtx;
|
|
3296
3708
|
if (fancy) {
|
|
3297
3709
|
process.stderr.write(`${logo()}
|
|
3298
3710
|
`);
|
|
3299
|
-
intro(
|
|
3711
|
+
intro(chalk9.bold(" fling setup "), { output: process.stderr });
|
|
3300
3712
|
}
|
|
3301
3713
|
const login = await stepLogin(ctx, opts.url);
|
|
3302
3714
|
const results = {
|
|
@@ -3310,7 +3722,7 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
3310
3722
|
const failed = Object.values(results).includes("failed");
|
|
3311
3723
|
if (fancy)
|
|
3312
3724
|
outro(
|
|
3313
|
-
failed ?
|
|
3725
|
+
failed ? chalk9.yellow("Setup finished with problems \u2014 see above, then run `fling setup` again.") : `You're set. Try ${chalk9.bold(`${ctx.binName} deploy ./dist`)}, or ask Claude to \u201Cfling this\u201D.`,
|
|
3314
3726
|
{ output: process.stderr }
|
|
3315
3727
|
);
|
|
3316
3728
|
ctx.out.data(results, () => "");
|
|
@@ -3333,7 +3745,7 @@ async function signIn(ctx, serverUrl) {
|
|
|
3333
3745
|
try {
|
|
3334
3746
|
const me = await whoami(requireClient(ctx));
|
|
3335
3747
|
if (ctx.config.remembered) await ctx.store.setCurrent(serverUrl);
|
|
3336
|
-
ctx.log.success(`Signed in as ${
|
|
3748
|
+
ctx.log.success(`Signed in as ${chalk9.bold(me.name)} ${chalk9.dim(`<${me.email}>`)}`);
|
|
3337
3749
|
return "already";
|
|
3338
3750
|
} catch (error) {
|
|
3339
3751
|
if (exitCodeFor(error) !== 2) {
|
|
@@ -3354,7 +3766,7 @@ async function signIn(ctx, serverUrl) {
|
|
|
3354
3766
|
async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
3355
3767
|
const status = await claudeCodeStatus(run);
|
|
3356
3768
|
if (status.claude === "missing") {
|
|
3357
|
-
ctx.log.info(`Claude Code not found ${
|
|
3769
|
+
ctx.log.info(`Claude Code not found ${chalk9.dim("(no `claude` on your PATH) \u2014 skipped")}`);
|
|
3358
3770
|
ctx.log.hint(
|
|
3359
3771
|
`Once it is installed, run \`${ctx.binName} setup\` again, or in Claude Code: /plugin marketplace add ${MARKETPLACE_SOURCE} then /plugin install ${PLUGIN_ID}`
|
|
3360
3772
|
);
|
|
@@ -3363,7 +3775,7 @@ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
|
3363
3775
|
const installed = "installed" in status && status.installed;
|
|
3364
3776
|
const question = installed ? `The Fling plugin is installed in Claude Code${status.version ? ` (${status.version})` : ""}. Update it to the latest?` : "Install the Fling plugin (skill + MCP server) into Claude Code?";
|
|
3365
3777
|
if (ask && !await ctx.confirm(question)) {
|
|
3366
|
-
ctx.log.info(
|
|
3778
|
+
ctx.log.info(chalk9.dim("Claude Code plugin: skipped"));
|
|
3367
3779
|
return installed ? "already" : "skipped";
|
|
3368
3780
|
}
|
|
3369
3781
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
@@ -3377,7 +3789,7 @@ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
|
3377
3789
|
else ctx.log.error(message);
|
|
3378
3790
|
return "failed";
|
|
3379
3791
|
}
|
|
3380
|
-
const done = installed ? `Claude Code plugin ${PLUGIN_NAME} is up to date` : `Claude Code plugin ${PLUGIN_NAME} installed ${
|
|
3792
|
+
const done = installed ? `Claude Code plugin ${PLUGIN_NAME} is up to date` : `Claude Code plugin ${PLUGIN_NAME} installed ${chalk9.dim(`(${scope} scope)`)}`;
|
|
3381
3793
|
if (s) s.stop(done);
|
|
3382
3794
|
else ctx.log.success(done);
|
|
3383
3795
|
ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick it up.");
|
|
@@ -3387,7 +3799,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
3387
3799
|
const app = await (deps.findClaudeDesktop ?? findClaudeDesktop)();
|
|
3388
3800
|
if (!app) return "missing";
|
|
3389
3801
|
if (ask && !await ctx.confirm("Add the Fling extension to Claude Desktop too?")) {
|
|
3390
|
-
ctx.log.info(
|
|
3802
|
+
ctx.log.info(chalk9.dim("Claude Desktop extension: skipped"));
|
|
3391
3803
|
return "skipped";
|
|
3392
3804
|
}
|
|
3393
3805
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
@@ -3408,7 +3820,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
3408
3820
|
}
|
|
3409
3821
|
|
|
3410
3822
|
// src/commands/sites.ts
|
|
3411
|
-
import
|
|
3823
|
+
import chalk10 from "chalk";
|
|
3412
3824
|
async function runList(ctx, opts) {
|
|
3413
3825
|
const sites = await listSites(requireClient(ctx), opts);
|
|
3414
3826
|
ctx.out.data(
|
|
@@ -3425,12 +3837,29 @@ async function runList(ctx, opts) {
|
|
|
3425
3837
|
])
|
|
3426
3838
|
);
|
|
3427
3839
|
}
|
|
3840
|
+
async function runListShared(ctx, opts) {
|
|
3841
|
+
const instance = ctx.config.instance;
|
|
3842
|
+
if (instance && !audiencesEnabled(instance)) {
|
|
3843
|
+
throw new CliError(`${instance.name} can't limit a site to listed people, so nothing is shared`);
|
|
3844
|
+
}
|
|
3845
|
+
const sites = await listShared(requireClient(ctx), opts);
|
|
3846
|
+
ctx.out.data(
|
|
3847
|
+
{ sites },
|
|
3848
|
+
() => renderTable(sites, [
|
|
3849
|
+
{ header: "NAME", value: (s) => s.name },
|
|
3850
|
+
{ header: "URL", value: (s) => s.url },
|
|
3851
|
+
{ header: "OWNER", value: (s) => s.owner },
|
|
3852
|
+
{ header: "VIA", value: (s) => sharedVia(s) },
|
|
3853
|
+
{ header: "UPDATED", value: (s) => formatDate(s.updatedAt) }
|
|
3854
|
+
])
|
|
3855
|
+
);
|
|
3856
|
+
}
|
|
3428
3857
|
function describeSite(site) {
|
|
3429
|
-
const row = (label, value) => `${
|
|
3858
|
+
const row = (label, value) => `${chalk10.bold(`${label}:`.padEnd(12))}${value}`;
|
|
3430
3859
|
return [
|
|
3431
3860
|
row("Name", site.name),
|
|
3432
3861
|
row("URL", site.url),
|
|
3433
|
-
row("Visibility", site.visibility + (site.archived ?
|
|
3862
|
+
row("Visibility", site.visibility + (site.archived ? chalk10.yellow(" (archived)") : "")),
|
|
3434
3863
|
row("SPA", site.spa ? "yes" : "no"),
|
|
3435
3864
|
row("Share link", site.shareUrl ?? "-"),
|
|
3436
3865
|
row("Version", site.currentVersionId ?? "-"),
|
|
@@ -3445,14 +3874,17 @@ function filesRow(site, row) {
|
|
|
3445
3874
|
const n = current.fileCount;
|
|
3446
3875
|
return [row("Files", `${n} file${n === 1 ? "" : "s"}, ${formatBytes(current.totalBytes)}`)];
|
|
3447
3876
|
}
|
|
3877
|
+
var membersOf = (ctx) => ctx.config.instance ? privateAudience(ctx.config.instance) : "anyone signed in to this Fling";
|
|
3448
3878
|
async function runInfo(ctx, name) {
|
|
3449
3879
|
const site = await getSite(requireClient(ctx), name);
|
|
3880
|
+
const access2 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${chalk10.bold("Access:".padEnd(12))}${describeAudience(site, membersOf(ctx))}`] : [];
|
|
3450
3881
|
ctx.out.data(
|
|
3451
3882
|
site,
|
|
3452
3883
|
() => [
|
|
3453
3884
|
...describeSite(site),
|
|
3885
|
+
...access2,
|
|
3454
3886
|
"",
|
|
3455
|
-
|
|
3887
|
+
chalk10.dim(
|
|
3456
3888
|
`${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
|
|
3457
3889
|
)
|
|
3458
3890
|
].join("\n")
|
|
@@ -3478,7 +3910,7 @@ async function runFiles(ctx, name, versionId) {
|
|
|
3478
3910
|
}
|
|
3479
3911
|
ctx.out.data(result, () => {
|
|
3480
3912
|
const n = result.files.length;
|
|
3481
|
-
const header =
|
|
3913
|
+
const header = chalk10.dim(
|
|
3482
3914
|
`${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
|
|
3483
3915
|
);
|
|
3484
3916
|
const table = renderTable(result.files, [
|
|
@@ -3514,7 +3946,7 @@ async function runShare(ctx, name, opts) {
|
|
|
3514
3946
|
ctx.log.success("Share link created \u2014 anyone with it can view the site");
|
|
3515
3947
|
ctx.out.data(
|
|
3516
3948
|
ready === void 0 ? result : { ...result, ready },
|
|
3517
|
-
() => result.shareUrl ? result.shareUrl :
|
|
3949
|
+
() => result.shareUrl ? result.shareUrl : chalk10.dim("(no share link)")
|
|
3518
3950
|
);
|
|
3519
3951
|
}
|
|
3520
3952
|
async function runVisibility(ctx, name, visibility) {
|
|
@@ -3527,6 +3959,27 @@ async function runVisibility(ctx, name, visibility) {
|
|
|
3527
3959
|
);
|
|
3528
3960
|
ctx.out.data(site, () => site.url);
|
|
3529
3961
|
}
|
|
3962
|
+
async function runAccess(ctx, name, opts) {
|
|
3963
|
+
const instance = ctx.config.instance;
|
|
3964
|
+
if (instance && !audiencesEnabled(instance)) {
|
|
3965
|
+
throw new CliError(`${instance.name} can't limit a site to listed people`);
|
|
3966
|
+
}
|
|
3967
|
+
const change = {
|
|
3968
|
+
add: splitEmails(opts.add ?? []),
|
|
3969
|
+
remove: splitEmails(opts.remove ?? []),
|
|
3970
|
+
addGroups: splitGroups(opts.group ?? []),
|
|
3971
|
+
removeGroups: splitGroups(opts.removeGroup ?? []),
|
|
3972
|
+
clear: opts.clear
|
|
3973
|
+
};
|
|
3974
|
+
if (instance && (change.addGroups.length || change.removeGroups.length)) {
|
|
3975
|
+
if (!groupsEnabled(instance)) throw new CliError(`${instance.name} has no groups`);
|
|
3976
|
+
}
|
|
3977
|
+
const client = requireClient(ctx);
|
|
3978
|
+
const site = changesAudience(change) ? await setAudience(client, name, change) : await getAudience(client, name);
|
|
3979
|
+
const warnings = audienceWarnings(site);
|
|
3980
|
+
for (const w of warnings) ctx.log.warn(w);
|
|
3981
|
+
ctx.out.data({ ...site, warnings }, () => describeAudience(site, membersOf(ctx)));
|
|
3982
|
+
}
|
|
3530
3983
|
async function runArchive(ctx, name, unarchive = false) {
|
|
3531
3984
|
const site = await archiveSite(requireClient(ctx), name, { unarchive });
|
|
3532
3985
|
ctx.log.success(
|
|
@@ -3556,7 +4009,7 @@ import { tmpdir as tmpdir3 } from "node:os";
|
|
|
3556
4009
|
import { basename as basename3, dirname as dirname2, join as join6 } from "node:path";
|
|
3557
4010
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3558
4011
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3559
|
-
import { z as
|
|
4012
|
+
import { z as z13 } from "zod";
|
|
3560
4013
|
|
|
3561
4014
|
// src/locate.ts
|
|
3562
4015
|
import { lstat, readdir as readdir2, readFile as readFile6, realpath as realpath3, stat as stat5 } from "node:fs/promises";
|
|
@@ -3978,7 +4431,7 @@ var mcpLocalInstructions = (w, opts = {}) => [
|
|
|
3978
4431
|
"When there is no index.html but there are HTML pages, fling_deploy and fling_publish do not publish: ask the user whether to generate an index page listing the pages (`listHtml: true`) or publish without one (`listHtml: false`, the root URL 404s), then retry.",
|
|
3979
4432
|
NEVER_PUBLISHED,
|
|
3980
4433
|
"Not logged in? Call fling_login, let the user finish signing in in the browser, then retry.",
|
|
3981
|
-
`Sites are private (
|
|
4434
|
+
`Sites are private (open to ${w.privateAudience}) by default; only make one public if asked. Use fling_share for a link people who cannot sign in can open. ${opts.comments ? "Always give the user the links that come back: the review link first, as the one to share for feedback, then the site URL." : "Always give the user the URL that comes back."}`,
|
|
3982
4435
|
...opts.comments ? [COMMENTS_NOTE(w)] : []
|
|
3983
4436
|
].join("\n\n");
|
|
3984
4437
|
var onFling = (url, instance) => `
|
|
@@ -3993,7 +4446,7 @@ function createMcpServer(deps = {}) {
|
|
|
3993
4446
|
const several = servers.length > 1;
|
|
3994
4447
|
const wording = mcpWording(several ? null : servers[0]?.instance);
|
|
3995
4448
|
const mcpTools2 = mcpToolsFor(wording);
|
|
3996
|
-
const serverArg =
|
|
4449
|
+
const serverArg = z13.string().min(1).max(500).optional().describe(serverArgDescription(servers));
|
|
3997
4450
|
const note = serverToolNote(servers);
|
|
3998
4451
|
const serverTool = (def, description = def.description) => ({
|
|
3999
4452
|
title: def.title,
|
|
@@ -4130,12 +4583,12 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
4130
4583
|
"fling_deploy",
|
|
4131
4584
|
{
|
|
4132
4585
|
title: "Deploy a file or folder from this computer",
|
|
4133
|
-
description: `Fling something that is on the user's computer and return its stable URL (${wording.sitesPattern}): a folder of static files (e.g. a build output like dist/), a single .html file (served as the site's index.html), a .zip of a site (extracted; a single top-level folder is unwrapped), or any other single file (.pdf, .svg, .png\u2026, served next to a generated page that shows it). \`path\` can be absolute, relative to the working directory, or just a file name ("Pitch Deck.html"): names are looked up, newest first, in Downloads, Desktop, Documents/Claude and Claude Desktop output folders, and the result says where the file was found. Never pass paths from your own sandbox (/mnt/user-data/\u2026, /sessions/\u2026): for content you created, use fling_publish. Without \`name\`, a name is generated: from the file name for files ("Pitch Deck (1).html" \u2192 pitch-deck), from the project and folder for folders (~/work/rocketpod/docs \u2192 rocketpod-docs). Site names are one global namespace, first come first served \u2014 confirm the name with the user first (fling_check_name with \`path\` shows the generated one). If the name is taken it fails with free suggestions \u2014 ask the user to pick one, then retry with \`name\`. If there is no index.html but there are HTML pages, it does not deploy: ask the user whether to generate an index page listing them (\`listHtml: true\`) or deploy without one (\`listHtml: false\`), then retry. Re-deploying identical content is a no-op. Sites are private (
|
|
4586
|
+
description: `Fling something that is on the user's computer and return its stable URL (${wording.sitesPattern}): a folder of static files (e.g. a build output like dist/), a single .html file (served as the site's index.html), a .zip of a site (extracted; a single top-level folder is unwrapped), or any other single file (.pdf, .svg, .png\u2026, served next to a generated page that shows it). \`path\` can be absolute, relative to the working directory, or just a file name ("Pitch Deck.html"): names are looked up, newest first, in Downloads, Desktop, Documents/Claude and Claude Desktop output folders, and the result says where the file was found. Never pass paths from your own sandbox (/mnt/user-data/\u2026, /sessions/\u2026): for content you created, use fling_publish. Without \`name\`, a name is generated: from the file name for files ("Pitch Deck (1).html" \u2192 pitch-deck), from the project and folder for folders (~/work/rocketpod/docs \u2192 rocketpod-docs). Site names are one global namespace, first come first served \u2014 confirm the name with the user first (fling_check_name with \`path\` shows the generated one). If the name is taken it fails with free suggestions \u2014 ask the user to pick one, then retry with \`name\`. If there is no index.html but there are HTML pages, it does not deploy: ask the user whether to generate an index page listing them (\`listHtml: true\`) or deploy without one (\`listHtml: false\`), then retry. Re-deploying identical content is a no-op. Sites are private (open to ${wording.privateAudience}) unless \`public\` is set. ${NEVER_PUBLISHED}${note}`,
|
|
4134
4587
|
inputSchema: {
|
|
4135
|
-
path:
|
|
4588
|
+
path: z13.string().min(1).describe(
|
|
4136
4589
|
"File or folder on this computer: absolute, relative to the working directory, or just its name (looked up in Downloads, Desktop and Claude output folders)"
|
|
4137
4590
|
),
|
|
4138
|
-
name:
|
|
4591
|
+
name: z13.string().optional().describe(
|
|
4139
4592
|
"Site name (lowercase letters, digits, single hyphens; max 50). Defaults to a name generated from the file or folder"
|
|
4140
4593
|
),
|
|
4141
4594
|
spa: spa2,
|
|
@@ -4185,9 +4638,9 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
4185
4638
|
title: "Find files to fling",
|
|
4186
4639
|
description: "List recent things on this computer that could be flung, newest first: .html/.htm and .zip files, and folders containing an index.html, from the working directory, Downloads, Desktop, Documents/Claude, ~/Claude and Claude Desktop output folders. Each has its path, where it was found, size and modified time. Use it when the user refers to a file they downloaded or that Claude Desktop saved, then pass the chosen path (or name) to fling_deploy.",
|
|
4187
4640
|
inputSchema: {
|
|
4188
|
-
query:
|
|
4189
|
-
limit:
|
|
4190
|
-
days:
|
|
4641
|
+
query: z13.string().optional().describe('Only names containing this text (case-insensitive), e.g. "deck"'),
|
|
4642
|
+
limit: z13.number().int().min(1).max(100).optional().describe("Max results (default 20)"),
|
|
4643
|
+
days: z13.number().int().min(1).max(3650).optional().describe("Only things modified in the last N days (default 14)")
|
|
4191
4644
|
},
|
|
4192
4645
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
4193
4646
|
},
|
|
@@ -4248,8 +4701,8 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4248
4701
|
...serverTool(mcpTools2.checkName),
|
|
4249
4702
|
description: `${mcpTools2.checkName.description} Pass \`path\` instead of \`name\` to see the name fling_deploy would generate for a file or folder on this computer (same lookup as fling_deploy), so you can confirm it with the user.${note}`,
|
|
4250
4703
|
inputSchema: {
|
|
4251
|
-
name:
|
|
4252
|
-
path:
|
|
4704
|
+
name: z13.string().min(1).max(200).optional().describe("Site name to check"),
|
|
4705
|
+
path: z13.string().optional().describe("File or folder whose generated default name to check (path or file name)"),
|
|
4253
4706
|
server: serverArg
|
|
4254
4707
|
}
|
|
4255
4708
|
},
|
|
@@ -4290,7 +4743,7 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4290
4743
|
title: "Sign in to Fling",
|
|
4291
4744
|
description: `Sign the user in to ${several ? "a Fling" : wording.name}: opens their browser to sign in with ${wording.signIn} and stores the key on this computer (shared with the fling CLI). Waits up to ~30 seconds; if sign-in is not finished by then, it keeps waiting in the background for up to 5 minutes \u2014 tell the user to finish signing in in the browser, then retry what you were doing. Call it when another tool says the user is not logged in. Pass \`server\` (its address) to sign in to another Fling, including one not set up on this computer yet: it is remembered, and becomes the current one only when none is.` + note,
|
|
4292
4745
|
inputSchema: {
|
|
4293
|
-
server:
|
|
4746
|
+
server: z13.string().min(1).max(500).optional().describe(
|
|
4294
4747
|
`The Fling to sign in to: its address (e.g. "https://fling.example.com"), or the host of one set up on this computer. ${several ? `Required while several Flings are set up (${servers.map((s) => serverLabel(s.url)).join(", ")})` : `Omit it to use ${servers[0] ? serverLabel(servers[0].url) : "the one this computer knows"}`}`
|
|
4295
4748
|
)
|
|
4296
4749
|
},
|
|
@@ -4374,7 +4827,7 @@ ${known.join("\n")}` : ""
|
|
|
4374
4827
|
title: "List the Flings on this computer",
|
|
4375
4828
|
description: `List the Flings set up on this computer, read now: each one's address, name, who is signed in (or "signed out") and which is current, the user's usual one. Call it when unsure which Fling the user means or which exist; the list may have changed since this conversation started. Pass \`use\` to make one the current Fling, the same as running \`${NPX_COMMAND} use <server>\`: it changes the default of the fling CLI too, so only do it when the user asks. It does not replace passing \`server\` to the other tools while several Flings are set up.`,
|
|
4376
4829
|
inputSchema: {
|
|
4377
|
-
use:
|
|
4830
|
+
use: z13.string().min(1).max(500).optional().describe(
|
|
4378
4831
|
"Make this Fling current (address, host or unique part of it), like `fling use`"
|
|
4379
4832
|
)
|
|
4380
4833
|
},
|
|
@@ -4418,17 +4871,39 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4418
4871
|
server.registerTool(
|
|
4419
4872
|
t.list.name,
|
|
4420
4873
|
serverTool(t.list),
|
|
4421
|
-
(args) => run(t.list.name, args.server, async (
|
|
4422
|
-
const
|
|
4874
|
+
(args) => run(t.list.name, args.server, async (target2) => {
|
|
4875
|
+
const { client, url } = target2;
|
|
4876
|
+
if (args.shared) {
|
|
4877
|
+
const instance = await target2.instance();
|
|
4878
|
+
if (instance && !audiencesEnabled(instance)) {
|
|
4879
|
+
throw new CliError(
|
|
4880
|
+
`${instance.name} (${serverLabel(url)}) can't limit a site to listed people, so nothing is shared`
|
|
4881
|
+
);
|
|
4882
|
+
}
|
|
4883
|
+
const sites2 = await listShared(client, { sort: args.sort });
|
|
4884
|
+
const lines = sites2.map(describeSharedSite).join("\n");
|
|
4885
|
+
return {
|
|
4886
|
+
summary: `${sites2.length} site(s) shared with you on ${serverLabel(url)}${lines ? `:
|
|
4887
|
+
${lines}` : ""}`,
|
|
4888
|
+
data: { sites: sites2 }
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
const sites = await listSites(client, { archived: args.archived, sort: args.sort });
|
|
4423
4892
|
return { summary: `${sites.length} site(s) on ${serverLabel(url)}`, data: { sites } };
|
|
4424
4893
|
})
|
|
4425
4894
|
);
|
|
4895
|
+
const membersOn = async (target2) => {
|
|
4896
|
+
const instance = await target2.instance();
|
|
4897
|
+
return instance ? privateAudience(instance) : "anyone signed in to this Fling";
|
|
4898
|
+
};
|
|
4426
4899
|
server.registerTool(
|
|
4427
4900
|
t.info.name,
|
|
4428
4901
|
serverTool(t.info),
|
|
4429
|
-
(args) => run(t.info.name, args.server, async (
|
|
4430
|
-
const site = await getSite(client, args.name);
|
|
4431
|
-
|
|
4902
|
+
(args) => run(t.info.name, args.server, async (target2) => {
|
|
4903
|
+
const site = await getSite(target2.client, args.name);
|
|
4904
|
+
const access2 = site.audience ? `
|
|
4905
|
+
${describeAudience(site, await membersOn(target2))}` : "";
|
|
4906
|
+
return { summary: `${site.name}: ${site.url}${access2}`, data: site };
|
|
4432
4907
|
})
|
|
4433
4908
|
);
|
|
4434
4909
|
server.registerTool(
|
|
@@ -4472,6 +4947,126 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4472
4947
|
return { summary: `${site.name} is now ${site.visibility}: ${site.url}`, data: site };
|
|
4473
4948
|
})
|
|
4474
4949
|
);
|
|
4950
|
+
if (servers.some((s) => audiencesEnabled(s.instance))) {
|
|
4951
|
+
server.registerTool(
|
|
4952
|
+
t.setAudience.name,
|
|
4953
|
+
serverTool(t.setAudience),
|
|
4954
|
+
(args) => run(t.setAudience.name, args.server, async (target2) => {
|
|
4955
|
+
const instance = await target2.instance();
|
|
4956
|
+
if (instance && !audiencesEnabled(instance)) {
|
|
4957
|
+
throw new CliError(
|
|
4958
|
+
`${instance.name} (${serverLabel(target2.url)}) can't limit a site to listed people`
|
|
4959
|
+
);
|
|
4960
|
+
}
|
|
4961
|
+
const change = {
|
|
4962
|
+
add: args.add,
|
|
4963
|
+
remove: args.remove,
|
|
4964
|
+
set: args.set,
|
|
4965
|
+
addGroups: args.addGroups,
|
|
4966
|
+
removeGroups: args.removeGroups,
|
|
4967
|
+
setGroups: args.setGroups,
|
|
4968
|
+
clear: args.clear
|
|
4969
|
+
};
|
|
4970
|
+
if (!changesAudience(change)) {
|
|
4971
|
+
throw new CliError(
|
|
4972
|
+
"Pass `add`, `remove`, `set`, `addGroups`, `removeGroups`, `setGroups` or `clear`"
|
|
4973
|
+
);
|
|
4974
|
+
}
|
|
4975
|
+
const site = await setAudience(target2.client, args.name, change);
|
|
4976
|
+
return {
|
|
4977
|
+
summary: `${site.name}: ${describeAudience(site, await membersOn(target2))}${onFling(target2.url, instance)}`,
|
|
4978
|
+
data: site
|
|
4979
|
+
};
|
|
4980
|
+
})
|
|
4981
|
+
);
|
|
4982
|
+
}
|
|
4983
|
+
if (servers.some((s) => groupsEnabled(s.instance))) {
|
|
4984
|
+
const withGroups = async (target2) => {
|
|
4985
|
+
const instance = await target2.instance();
|
|
4986
|
+
if (instance && !groupsEnabled(instance)) {
|
|
4987
|
+
throw new CliError(`${instance.name} (${serverLabel(target2.url)}) has no groups`);
|
|
4988
|
+
}
|
|
4989
|
+
return instance;
|
|
4990
|
+
};
|
|
4991
|
+
server.registerTool(
|
|
4992
|
+
t.groups.name,
|
|
4993
|
+
serverTool(t.groups),
|
|
4994
|
+
(args) => run(t.groups.name, args.server, async (target2) => {
|
|
4995
|
+
const instance = await withGroups(target2);
|
|
4996
|
+
const [groups2, me] = await Promise.all([
|
|
4997
|
+
listGroups(target2.client, { mine: args.mine, members: args.members }),
|
|
4998
|
+
whoami(target2.client)
|
|
4999
|
+
]);
|
|
5000
|
+
const which = args.members ? " of exactly those people" : "";
|
|
5001
|
+
return {
|
|
5002
|
+
summary: args.members && groups2.length === 0 ? `No group has exactly those people${onFling(target2.url, instance)}` : `${groups2.length} ${args.mine ? "of your " : ""}group(s)${which}${onFling(target2.url, instance)}
|
|
5003
|
+
${describeGroups(groups2, me.email)}`,
|
|
5004
|
+
data: { groups: groups2 }
|
|
5005
|
+
};
|
|
5006
|
+
})
|
|
5007
|
+
);
|
|
5008
|
+
server.registerTool(
|
|
5009
|
+
t.groupShow.name,
|
|
5010
|
+
serverTool(t.groupShow),
|
|
5011
|
+
(args) => run(t.groupShow.name, args.server, async (target2) => {
|
|
5012
|
+
await withGroups(target2);
|
|
5013
|
+
const group = await getGroup(target2.client, args.slug);
|
|
5014
|
+
return { summary: describeGroup(group), data: group };
|
|
5015
|
+
})
|
|
5016
|
+
);
|
|
5017
|
+
server.registerTool(
|
|
5018
|
+
t.groupCreate.name,
|
|
5019
|
+
serverTool(t.groupCreate),
|
|
5020
|
+
(args) => run(t.groupCreate.name, args.server, async (target2) => {
|
|
5021
|
+
const instance = await withGroups(target2);
|
|
5022
|
+
const group = await createGroup(target2.client, {
|
|
5023
|
+
slug: args.slug,
|
|
5024
|
+
name: args.name,
|
|
5025
|
+
members: args.members
|
|
5026
|
+
});
|
|
5027
|
+
return {
|
|
5028
|
+
summary: `Created ${describeGroup(group)}${onFling(target2.url, instance)}`,
|
|
5029
|
+
data: group
|
|
5030
|
+
};
|
|
5031
|
+
})
|
|
5032
|
+
);
|
|
5033
|
+
server.registerTool(
|
|
5034
|
+
t.groupMembers.name,
|
|
5035
|
+
serverTool(t.groupMembers),
|
|
5036
|
+
(args) => run(t.groupMembers.name, args.server, async (target2) => {
|
|
5037
|
+
await withGroups(target2);
|
|
5038
|
+
if (!args.add?.length && !args.remove?.length) {
|
|
5039
|
+
throw new CliError("Pass `add` or `remove`");
|
|
5040
|
+
}
|
|
5041
|
+
const group = await changeGroupMembers(target2.client, args.slug, {
|
|
5042
|
+
add: args.add,
|
|
5043
|
+
remove: args.remove
|
|
5044
|
+
});
|
|
5045
|
+
return { summary: describeGroup(group), data: group };
|
|
5046
|
+
})
|
|
5047
|
+
);
|
|
5048
|
+
server.registerTool(
|
|
5049
|
+
t.groupRename.name,
|
|
5050
|
+
serverTool(t.groupRename),
|
|
5051
|
+
(args) => run(t.groupRename.name, args.server, async (target2) => {
|
|
5052
|
+
await withGroups(target2);
|
|
5053
|
+
const group = await renameGroup(target2.client, args.slug, args.name);
|
|
5054
|
+
return { summary: describeGroup(group), data: group };
|
|
5055
|
+
})
|
|
5056
|
+
);
|
|
5057
|
+
server.registerTool(
|
|
5058
|
+
t.groupDelete.name,
|
|
5059
|
+
serverTool(t.groupDelete),
|
|
5060
|
+
(args) => run(t.groupDelete.name, args.server, async (target2) => {
|
|
5061
|
+
await withGroups(target2);
|
|
5062
|
+
const result = await deleteGroup(target2.client, args.slug);
|
|
5063
|
+
return {
|
|
5064
|
+
summary: `Group ${args.slug} deleted. Sites whose audience named it keep their restriction: anyone it let in has lost access`,
|
|
5065
|
+
data: result
|
|
5066
|
+
};
|
|
5067
|
+
})
|
|
5068
|
+
);
|
|
5069
|
+
}
|
|
4475
5070
|
server.registerTool(
|
|
4476
5071
|
t.archive.name,
|
|
4477
5072
|
serverTool(t.archive),
|
|
@@ -4701,18 +5296,18 @@ program.name(BIN_NAME).description("Fling a folder of static files at Fling and
|
|
|
4701
5296
|
`server for this command: a URL, or the host of one you signed in to (default: $${ENV.url}, else the current one)`
|
|
4702
5297
|
).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
|
|
4703
5298
|
sortSubcommands: false,
|
|
4704
|
-
styleTitle: (title) =>
|
|
4705
|
-
styleCommandText: (text2) =>
|
|
5299
|
+
styleTitle: (title) => chalk11.bold(brand.primary(title)),
|
|
5300
|
+
styleCommandText: (text2) => chalk11.bold(text2),
|
|
4706
5301
|
styleSubcommandText: (text2) => brand.accent(text2),
|
|
4707
5302
|
styleOptionText: (text2) => brand.accent(text2),
|
|
4708
|
-
styleArgumentText: (text2) =>
|
|
5303
|
+
styleArgumentText: (text2) => chalk11.dim(text2)
|
|
4709
5304
|
}).addHelpText(
|
|
4710
5305
|
"before",
|
|
4711
5306
|
() => process.stdout.isTTY ? logo({ columns: process.stdout.columns }) : ""
|
|
4712
5307
|
).addHelpText(
|
|
4713
5308
|
"after",
|
|
4714
5309
|
() => `
|
|
4715
|
-
${
|
|
5310
|
+
${chalk11.bold(brand.primary("Examples:"))}
|
|
4716
5311
|
$ ${BIN_NAME} login https://fling.example.com sign in to your Fling (once; there is no default)
|
|
4717
5312
|
$ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
|
|
4718
5313
|
$ ${BIN_NAME} use list the Flings you signed in to; \`use <url>\` switches
|
|
@@ -4723,7 +5318,7 @@ ${chalk10.bold(brand.primary("Examples:"))}
|
|
|
4723
5318
|
$ ${BIN_NAME} site.zip a zip of a site
|
|
4724
5319
|
$ ${BIN_NAME} share my-app public link for people who can't sign in
|
|
4725
5320
|
|
|
4726
|
-
${
|
|
5321
|
+
${chalk11.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
|
|
4727
5322
|
Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`)}`
|
|
4728
5323
|
);
|
|
4729
5324
|
function action(handler) {
|
|
@@ -4767,7 +5362,10 @@ program.command("deploy [path]").helpGroup("Deploy:").description(
|
|
|
4767
5362
|
).option(
|
|
4768
5363
|
"-n, --name <name>",
|
|
4769
5364
|
"site name \u2014 names are global, first come first served (default: generated from the project and folder, e.g. rocketpod-docs; you confirm it on a terminal)"
|
|
4770
|
-
).option("--spa", "single-page app: serve index.html for unknown paths").option(
|
|
5365
|
+
).option("--spa", "single-page app: serve index.html for unknown paths").option(
|
|
5366
|
+
"--public",
|
|
5367
|
+
"make the site public: anyone can open it, reviewing still needs sign-in (default: sign-in required)"
|
|
5368
|
+
).option(
|
|
4771
5369
|
"-y, --yes",
|
|
4772
5370
|
"never prompt: accept the generated name; without index.html, list the HTML pages (or fling anyway if there are none)"
|
|
4773
5371
|
).option("--list-html", "no index.html at the root: generate one listing the HTML pages").option("--no-list-html", "no index.html at the root: fling anyway (the root will 404)").action(
|
|
@@ -4798,7 +5396,18 @@ program.command("logout [url]").helpGroup("Setup:").description(
|
|
|
4798
5396
|
).action(action((ctx, url) => runLogout(ctx, url)));
|
|
4799
5397
|
program.command("whoami").helpGroup("Setup:").description("show the signed-in user and the current server").action(action((ctx) => runWhoami(ctx)));
|
|
4800
5398
|
program.commandsGroup("Setup:").helpCommand("help [command]", "show help for a command");
|
|
4801
|
-
program.command("ls").helpGroup("Sites:").alias("list").description("list your sites").option("--archived", "list archived sites").
|
|
5399
|
+
program.command("ls").helpGroup("Sites:").alias("list").description("list your sites").option("--archived", "list archived sites").addOption(
|
|
5400
|
+
new Option(
|
|
5401
|
+
"--shared",
|
|
5402
|
+
"list the sites others have shared with you instead of your own"
|
|
5403
|
+
).conflicts("archived")
|
|
5404
|
+
).addOption(
|
|
5405
|
+
new Option("--sort <order>", "updated (newest first), created (newest first) or name (A\u2013Z)").choices(SITE_SORTS).default(DEFAULT_SITE_SORT)
|
|
5406
|
+
).action(
|
|
5407
|
+
action(
|
|
5408
|
+
(ctx, opts) => opts.shared ? runListShared(ctx, opts) : runList(ctx, opts)
|
|
5409
|
+
)
|
|
5410
|
+
);
|
|
4802
5411
|
program.command("info <name>").helpGroup("Sites:").description("show a site").action(action((ctx, name) => runInfo(ctx, name)));
|
|
4803
5412
|
program.command("versions <name>").helpGroup("Sites:").description("list a site's versions (* = live)").action(action((ctx, name) => runVersions(ctx, name)));
|
|
4804
5413
|
program.command("files <name> [versionId]").helpGroup("Sites:").description("list the files in a site's live version (or in versionId)").action(
|
|
@@ -4810,10 +5419,37 @@ program.command("share <name>").helpGroup("Sites:").description("print (or creat
|
|
|
4810
5419
|
(ctx, name, opts) => runShare(ctx, name, opts)
|
|
4811
5420
|
)
|
|
4812
5421
|
);
|
|
4813
|
-
program.command("visibility <name> <visibility>").helpGroup("Sites:").description(
|
|
5422
|
+
program.command("visibility <name> <visibility>").helpGroup("Sites:").description(
|
|
5423
|
+
"private (sign-in required) or public (anyone can open the site; reviewing it still needs sign-in)"
|
|
5424
|
+
).action(action((ctx, name, visibility) => runVisibility(ctx, name, visibility)));
|
|
5425
|
+
program.command("access <name>").helpGroup("Sites:").description("show who can open a private site, or limit it to listed people and groups").option("--add <emails...>", "let these addresses in (comma-separated or repeated)").option("--remove <emails...>", "take these addresses out").option("--group <slugs...>", "let these groups in (see `fling groups`)").option("--remove-group <slugs...>", "take these groups out").option("--clear", "empty the audience: everyone signed in can open it again").action(
|
|
5426
|
+
action(
|
|
5427
|
+
(ctx, name, opts) => runAccess(ctx, name, opts)
|
|
5428
|
+
)
|
|
5429
|
+
);
|
|
4814
5430
|
program.command("archive <name>").helpGroup("Sites:").description("take a site offline (404) but keep its files; undo with unarchive").action(action((ctx, name) => runArchive(ctx, name)));
|
|
4815
5431
|
program.command("unarchive <name>").helpGroup("Sites:").description("bring an archived site back").action(action((ctx, name) => runArchive(ctx, name, true)));
|
|
4816
5432
|
program.command("rm <name>").helpGroup("Sites:").alias("delete").description("delete a site and all its versions (asks first unless --yes)").option("-y, --yes", "do not ask for confirmation").action(action((ctx, name, opts) => runRemove(ctx, name, opts)));
|
|
5433
|
+
var groups = program.command("groups").helpGroup("Groups:").description(
|
|
5434
|
+
"named lists of people anyone can use in an audience; only the owner changes one (default: list)"
|
|
5435
|
+
);
|
|
5436
|
+
groups.command("list", { isDefault: true }).alias("ls").description("list the groups on this Fling").option("--mine", "only the groups you own").option("--members <emails...>", "only the groups of exactly these people (a,b or a b)").action(action((ctx, opts) => runGroups(ctx, opts)));
|
|
5437
|
+
groups.command("create <slug>").description("create a group you own (slug: lowercase letters, digits and single hyphens)").option("--name <name>", "its display name (default: the slug)").option("--add <emails...>", "put these addresses in it").action(
|
|
5438
|
+
action(
|
|
5439
|
+
(ctx, slug, opts) => runGroupCreate(ctx, slug, opts)
|
|
5440
|
+
)
|
|
5441
|
+
);
|
|
5442
|
+
groups.command("show <slug>").description("show a group and its members").action(action((ctx, slug) => runGroupShow(ctx, slug)));
|
|
5443
|
+
groups.command("add <slug> <emails...>").description("add people to a group you own").action(
|
|
5444
|
+
action((ctx, slug, emails2) => runGroupMembers(ctx, slug, emails2, "add"))
|
|
5445
|
+
);
|
|
5446
|
+
groups.command("remove <slug> <emails...>").description("take people out of a group you own").action(
|
|
5447
|
+
action((ctx, slug, emails2) => runGroupMembers(ctx, slug, emails2, "remove"))
|
|
5448
|
+
);
|
|
5449
|
+
groups.command("rename <slug> <name>").description("change the display name of a group you own").action(action((ctx, slug, name) => runGroupRename(ctx, slug, name)));
|
|
5450
|
+
groups.command("delete <slug>").alias("rm").description(
|
|
5451
|
+
"delete a group you own; anyone it let into a site loses access (asks first unless --yes)"
|
|
5452
|
+
).option("-y, --yes", "do not ask for confirmation").action(action((ctx, slug, opts) => runGroupDelete(ctx, slug, opts)));
|
|
4817
5453
|
program.command("comments <name>").helpGroup("Comments:").description("list a site's open comment threads (people leave them on its review page)").option("-p, --path <path>", "only this page, e.g. / or /docs/").option("-a, --all", "include resolved threads").action(
|
|
4818
5454
|
action(
|
|
4819
5455
|
(ctx, name, opts) => runComments(ctx, name, opts)
|