@nearform/fling 0.6.0 → 0.7.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/README.md +115 -18
- package/dist/cli.js +2256 -525
- package/package.json +3 -4
- package/CHANGELOG.md +0 -109
- package/dist/cli.js.map +0 -7
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,108 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// ../../packages/shared/src/access.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var ACCESS_LEVELS = ["only_me", "members", "groups", "people", "public"];
|
|
6
|
+
var AccessLevelSchema = z.enum(ACCESS_LEVELS);
|
|
7
|
+
var DEFAULT_ACCESS_LEVELS = ["only_me", "members"];
|
|
8
|
+
var DefaultAccessLevelSchema = z.enum(DEFAULT_ACCESS_LEVELS);
|
|
9
|
+
var LEGACY_DEFAULT_ACCESS = "members";
|
|
10
|
+
var needsAudience = (level) => level === "groups" || level === "people";
|
|
11
|
+
var ACCESS_LABELS = {
|
|
12
|
+
only_me: "Only you",
|
|
13
|
+
members: "Everyone signed in",
|
|
14
|
+
groups: "Teams",
|
|
15
|
+
people: "Specific people",
|
|
16
|
+
public: "Public"
|
|
17
|
+
};
|
|
18
|
+
function accessDescription(level, members2) {
|
|
19
|
+
switch (level) {
|
|
20
|
+
case "only_me":
|
|
21
|
+
return "Only you can open it.";
|
|
22
|
+
case "members":
|
|
23
|
+
return `${capitalise(members2)} can open it.`;
|
|
24
|
+
case "groups":
|
|
25
|
+
return "Only you and the members of the groups you name can open it.";
|
|
26
|
+
case "people":
|
|
27
|
+
return "Only you and the people you name can open it.";
|
|
28
|
+
case "public":
|
|
29
|
+
return "Anyone with the address can open it, without signing in; its review page and comments still need sign-in.";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
var capitalise = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
33
|
+
function accessLevelOf(site) {
|
|
34
|
+
if (site.visibility === "public") return "public";
|
|
35
|
+
if (!site.restricted) return "members";
|
|
36
|
+
if (site.emails > 0) return "people";
|
|
37
|
+
return site.groups > 0 ? "groups" : "only_me";
|
|
38
|
+
}
|
|
39
|
+
var siteAccessLevel = (site) => site.access ?? accessLevelOf({
|
|
40
|
+
visibility: site.visibility,
|
|
41
|
+
restricted: site.restricted ?? false,
|
|
42
|
+
emails: (site.audienceSize ?? 0) - (site.audienceGroups ?? 0),
|
|
43
|
+
groups: site.audienceGroups ?? 0
|
|
44
|
+
});
|
|
45
|
+
function parseAccessLevel(value) {
|
|
46
|
+
const v = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
47
|
+
const aliases = {
|
|
48
|
+
me: "only_me",
|
|
49
|
+
only_you: "only_me",
|
|
50
|
+
owner: "only_me",
|
|
51
|
+
private: "members",
|
|
52
|
+
member: "members",
|
|
53
|
+
everyone: "members",
|
|
54
|
+
group: "groups",
|
|
55
|
+
teams: "groups",
|
|
56
|
+
team: "groups",
|
|
57
|
+
person: "people",
|
|
58
|
+
emails: "people"
|
|
59
|
+
};
|
|
60
|
+
return ACCESS_LEVELS.includes(v) ? v : aliases[v] ?? null;
|
|
61
|
+
}
|
|
62
|
+
function accessChangeHint(client, name, opts = {}) {
|
|
63
|
+
const bin = opts.bin ?? "fling";
|
|
64
|
+
const how = client === "cli" ? `Change who can open it with \`${bin} access ${name} --only-me | --members | --add <email> | --group <slug> | --public\`.` : `To change who can open it, call fling_set_access (only_me, members, groups, people or public).`;
|
|
65
|
+
return opts.invites ? `${how} An address from outside is invited as a reviewer, who sees only what is shared with them.` : how;
|
|
66
|
+
}
|
|
67
|
+
var count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
68
|
+
function describeSiteAccess(site, members2) {
|
|
69
|
+
const level = siteAccessLevel(site);
|
|
70
|
+
const share2 = site.shareUrl && level !== "public" && level !== "members" ? " A share link is active: anyone with it can still open the site." : "";
|
|
71
|
+
if (!needsAudience(level)) return `${accessDescription(level, members2)}${share2}`;
|
|
72
|
+
const groups2 = site.audienceGroups ?? 0;
|
|
73
|
+
const people2 = (site.audienceSize ?? 0) - groups2;
|
|
74
|
+
const who = [
|
|
75
|
+
people2 ? count(people2, "person", "people") : "",
|
|
76
|
+
groups2 ? count(groups2, "group", "groups") : ""
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
return `Only you and ${who.join(" and ")} can open it.${share2}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
3
81
|
// ../../packages/shared/src/api.ts
|
|
4
|
-
import { z as
|
|
82
|
+
import { z as z7 } from "zod";
|
|
5
83
|
|
|
6
84
|
// ../../packages/shared/src/audience.ts
|
|
7
|
-
import { z } from "zod";
|
|
85
|
+
import { z as z2 } from "zod";
|
|
8
86
|
var AUDIENCE_EMAILS_MAX = 100;
|
|
9
87
|
var AUDIENCE_GROUPS_MAX = 10;
|
|
10
|
-
var AudienceEmailSchema =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
88
|
+
var AudienceEmailSchema = z2.object({
|
|
89
|
+
email: z2.string(),
|
|
90
|
+
known: z2.boolean(),
|
|
91
|
+
guest: z2.boolean().optional()
|
|
92
|
+
});
|
|
93
|
+
var AudienceGroupSchema = z2.object({
|
|
94
|
+
slug: z2.string(),
|
|
95
|
+
name: z2.string(),
|
|
96
|
+
size: z2.number(),
|
|
97
|
+
deleted: z2.boolean().optional()
|
|
16
98
|
});
|
|
17
|
-
var AudienceSchema =
|
|
18
|
-
emails:
|
|
19
|
-
groups:
|
|
99
|
+
var AudienceSchema = z2.object({
|
|
100
|
+
emails: z2.array(AudienceEmailSchema),
|
|
101
|
+
groups: z2.array(AudienceGroupSchema).default([])
|
|
20
102
|
});
|
|
21
|
-
var AudienceInputSchema =
|
|
22
|
-
emails:
|
|
23
|
-
groups:
|
|
103
|
+
var AudienceInputSchema = z2.object({
|
|
104
|
+
emails: z2.array(z2.string().trim().toLowerCase().min(3).max(254)).max(AUDIENCE_EMAILS_MAX, `An audience holds at most ${AUDIENCE_EMAILS_MAX} addresses`),
|
|
105
|
+
groups: z2.array(z2.string().trim().toLowerCase().min(1).max(100)).max(AUDIENCE_GROUPS_MAX, `An audience names at most ${AUDIENCE_GROUPS_MAX} groups`).default([])
|
|
24
106
|
});
|
|
25
107
|
var normaliseEmail = (email) => email.trim().toLowerCase();
|
|
26
108
|
function splitEmails(values) {
|
|
@@ -66,17 +148,23 @@ function whoIn(emails2, groups2) {
|
|
|
66
148
|
].filter(Boolean);
|
|
67
149
|
}
|
|
68
150
|
var and = (parts) => parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}` : parts[0] ?? "";
|
|
69
|
-
var audienceList = (emails2) => emails2.map((e) =>
|
|
151
|
+
var audienceList = (emails2) => emails2.map((e) => {
|
|
152
|
+
const notes = [e.guest ? "guest" : "", e.known ? "" : "hasn't signed in yet"].filter(Boolean);
|
|
153
|
+
return notes.length ? `${e.email} (${notes.join(", ")})` : e.email;
|
|
154
|
+
}).join(", ");
|
|
70
155
|
function describeAudience(site, privateAudience2) {
|
|
71
156
|
const emails2 = site.audience?.emails ?? [];
|
|
72
157
|
const groups2 = site.audience?.groups ?? [];
|
|
73
158
|
const empty = emails2.length + groups2.length === 0;
|
|
74
159
|
const list = emails2.length ? `: ${audienceList(emails2)}` : "";
|
|
75
160
|
if (site.visibility === "public") {
|
|
76
|
-
|
|
161
|
+
const kept = empty ? "" : ` Its audience (${and(whoIn(emails2, groups2))}${list}) applies when it is private again.`;
|
|
162
|
+
return `Public: anyone can open the site; reviewing it still needs sign-in.${kept}`;
|
|
77
163
|
}
|
|
78
|
-
if (empty) return `Private: ${privateAudience2} can open it.`;
|
|
79
164
|
const share2 = site.shareUrl ? ` A share link is active: anyone with it can still open the site (${site.shareUrl}).` : "";
|
|
165
|
+
if (empty) {
|
|
166
|
+
return site.restricted ? `Only you: nobody else can open it.${share2}` : `Private: ${privateAudience2} can open it.`;
|
|
167
|
+
}
|
|
80
168
|
if (emails2.length === 0 && groups2.every((g) => g.deleted)) {
|
|
81
169
|
const refs = groups2.map((g) => g.slug).join(", ");
|
|
82
170
|
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}`;
|
|
@@ -92,15 +180,15 @@ function sharedVia(site) {
|
|
|
92
180
|
}
|
|
93
181
|
var describeSharedSite = (site) => `- ${site.name}: ${site.url} (shared by ${site.owner}, through ${sharedVia(site)}${site.reviewUrl ? `; review: ${site.reviewUrl}` : ""})`;
|
|
94
182
|
|
|
95
|
-
// ../../packages/shared/src/manifest.ts
|
|
96
|
-
import { z as z2 } from "zod";
|
|
97
|
-
|
|
98
183
|
// ../../packages/shared/src/limits.ts
|
|
99
184
|
var MAX_DEPLOY_BYTES = 100 * 1024 * 1024;
|
|
100
185
|
var MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
101
186
|
var MAX_FILES = 5e3;
|
|
102
187
|
var MAX_PATH_LENGTH = 1024;
|
|
103
188
|
|
|
189
|
+
// ../../packages/shared/src/manifest.ts
|
|
190
|
+
import { z as z3 } from "zod";
|
|
191
|
+
|
|
104
192
|
// ../../packages/shared/src/secrets.ts
|
|
105
193
|
var SECRET_SCAN_MAX_FILE_BYTES = MAX_FILE_BYTES;
|
|
106
194
|
var BINARY_SNIFF_BYTES = 8192;
|
|
@@ -291,246 +379,464 @@ function isValidSitePath(path3) {
|
|
|
291
379
|
if (path3.startsWith("/") || path3.includes("\\") || path3.includes("\0")) return false;
|
|
292
380
|
return path3.split("/").every((seg) => seg !== "" && !seg.startsWith("."));
|
|
293
381
|
}
|
|
294
|
-
var ManifestFileSchema =
|
|
295
|
-
path:
|
|
296
|
-
sha256:
|
|
297
|
-
size:
|
|
298
|
-
contentType:
|
|
382
|
+
var ManifestFileSchema = z3.object({
|
|
383
|
+
path: z3.string().refine(isValidSitePath, "invalid path (dotfiles are never published)").refine((p) => !isSecretPath(p), "secret file (keys, certificates, credentials) never published"),
|
|
384
|
+
sha256: z3.string().regex(/^[a-f0-9]{64}$/),
|
|
385
|
+
size: z3.number().int().min(0).max(MAX_FILE_BYTES),
|
|
386
|
+
contentType: z3.string().min(1).max(255)
|
|
387
|
+
});
|
|
388
|
+
var manifestOf = (files) => z3.object({ 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");
|
|
389
|
+
var ManifestSchema = manifestOf(z3.array(ManifestFileSchema).min(1).max(MAX_FILES));
|
|
390
|
+
var DeployManifestSchema = manifestOf(z3.array(ManifestFileSchema).max(MAX_FILES));
|
|
391
|
+
|
|
392
|
+
// ../../packages/shared/src/publish-path.ts
|
|
393
|
+
import { z as z4 } from "zod";
|
|
394
|
+
function normalizePublishPath(raw) {
|
|
395
|
+
const path3 = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
396
|
+
return isValidSitePath(path3) ? path3 : null;
|
|
397
|
+
}
|
|
398
|
+
var PublishPathSchema = z4.string().transform((raw, ctx) => {
|
|
399
|
+
const path3 = normalizePublishPath(raw);
|
|
400
|
+
if (path3 === null) {
|
|
401
|
+
ctx.addIssue({
|
|
402
|
+
code: "custom",
|
|
403
|
+
message: "path must be a folder of the site, like 30-10-26 or reports/2026 (no dotfiles)"
|
|
404
|
+
});
|
|
405
|
+
return z4.NEVER;
|
|
406
|
+
}
|
|
407
|
+
return path3;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ../../packages/shared/src/site-roles.ts
|
|
411
|
+
import { z as z5 } from "zod";
|
|
412
|
+
var OWNER_EMAILS_MAX = 20;
|
|
413
|
+
var OWNER_GROUPS_MAX = 5;
|
|
414
|
+
var PUBLISHER_EMAILS_MAX = 50;
|
|
415
|
+
var PUBLISHER_GROUPS_MAX = 10;
|
|
416
|
+
var SiteRoleSchema = z5.enum(["owner", "publisher", "admin"]);
|
|
417
|
+
var SiteRoleMembersSchema = z5.object({
|
|
418
|
+
emails: z5.array(AudienceEmailSchema),
|
|
419
|
+
groups: z5.array(AudienceGroupSchema).default([])
|
|
420
|
+
});
|
|
421
|
+
var roleInput = (what, emailsMax, groupsMax) => z5.object({
|
|
422
|
+
emails: z5.array(z5.string().trim().toLowerCase().min(3).max(254)).max(emailsMax, `A site has at most ${emailsMax} ${what} addresses`),
|
|
423
|
+
groups: z5.array(z5.string().trim().toLowerCase().min(1).max(100)).max(groupsMax, `A site names at most ${groupsMax} ${what} groups`).default([])
|
|
299
424
|
});
|
|
300
|
-
var
|
|
425
|
+
var OwnersInputSchema = roleInput("owner", OWNER_EMAILS_MAX, OWNER_GROUPS_MAX);
|
|
426
|
+
var PublishersInputSchema = roleInput(
|
|
427
|
+
"publisher",
|
|
428
|
+
PUBLISHER_EMAILS_MAX,
|
|
429
|
+
PUBLISHER_GROUPS_MAX
|
|
430
|
+
);
|
|
431
|
+
var editRoleInput = (current, change) => editAudienceInput(current, change);
|
|
432
|
+
function whoHolds(members2) {
|
|
433
|
+
const parts = [
|
|
434
|
+
...(members2?.emails ?? []).map((e) => e.guest ? `${e.email} (guest)` : e.email),
|
|
435
|
+
...(members2?.groups ?? []).map((g) => `the group ${groupLabel(g)}`)
|
|
436
|
+
];
|
|
437
|
+
return parts.length ? parts.join(", ") : "nobody";
|
|
438
|
+
}
|
|
439
|
+
var roleLabel = (role) => role === "publisher" ? "a publisher" : `an ${role}`;
|
|
440
|
+
function describeRoles(site) {
|
|
441
|
+
if (!site.owners) return "";
|
|
442
|
+
const you = site.role ? ` You are ${roleLabel(site.role)}.` : "";
|
|
443
|
+
return `Owners: ${whoHolds(site.owners)}. Publishers: ${whoHolds(site.publishers)}.${you}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ../../packages/shared/src/thumbnails.ts
|
|
447
|
+
import { z as z6 } from "zod";
|
|
448
|
+
var THUMBNAIL_MAX_BYTES = 200 * 1024;
|
|
449
|
+
var ThumbnailSchema = z6.object({ versionId: z6.string(), updatedAt: z6.string() });
|
|
450
|
+
var ThumbnailStampSchema = z6.object({ updatedAt: z6.string() });
|
|
301
451
|
|
|
302
452
|
// ../../packages/shared/src/api.ts
|
|
303
|
-
var VisibilitySchema =
|
|
304
|
-
var MeSchema =
|
|
305
|
-
email:
|
|
306
|
-
name:
|
|
307
|
-
picture:
|
|
308
|
-
userSlug:
|
|
453
|
+
var VisibilitySchema = z7.enum(["private", "public"]);
|
|
454
|
+
var MeSchema = z7.object({
|
|
455
|
+
email: z7.string(),
|
|
456
|
+
name: z7.string(),
|
|
457
|
+
picture: z7.string().nullable(),
|
|
458
|
+
userSlug: z7.string(),
|
|
309
459
|
/** one of the instance's admins (servers from before admins don't say: false) */
|
|
310
|
-
admin:
|
|
460
|
+
admin: z7.boolean().default(false),
|
|
461
|
+
/** a guest: someone invited from outside who reviews what is shared with them, nothing else */
|
|
462
|
+
guest: z7.boolean().default(false),
|
|
311
463
|
/** while an admin impersonates this user: the admin (browser sessions only) */
|
|
312
|
-
impersonator:
|
|
464
|
+
impersonator: z7.string().optional(),
|
|
313
465
|
/** …and when the impersonation lapses */
|
|
314
|
-
impersonationEndsAt:
|
|
466
|
+
impersonationEndsAt: z7.string().optional()
|
|
315
467
|
});
|
|
316
|
-
var SiteSchema =
|
|
317
|
-
name:
|
|
318
|
-
url:
|
|
468
|
+
var SiteSchema = z7.object({
|
|
469
|
+
name: z7.string(),
|
|
470
|
+
url: z7.string(),
|
|
319
471
|
visibility: VisibilitySchema,
|
|
320
|
-
spa:
|
|
321
|
-
archived:
|
|
322
|
-
currentVersionId:
|
|
323
|
-
shareUrl:
|
|
324
|
-
/**
|
|
325
|
-
|
|
472
|
+
spa: z7.boolean(),
|
|
473
|
+
archived: z7.boolean(),
|
|
474
|
+
currentVersionId: z7.string().nullable(),
|
|
475
|
+
shareUrl: z7.string().nullable(),
|
|
476
|
+
/** who can open it (access.ts); servers before access levels omit it (`siteAccessLevel`) */
|
|
477
|
+
access: AccessLevelSchema.optional(),
|
|
478
|
+
/** private and limited to the owner and the audience (an empty one: the owner alone) */
|
|
479
|
+
restricted: z7.boolean().default(false),
|
|
326
480
|
/** how many addresses and groups the audience names (kept, and ignored, while public) */
|
|
327
|
-
audienceSize:
|
|
481
|
+
audienceSize: z7.number().default(0),
|
|
328
482
|
/** how many of those are groups */
|
|
329
|
-
audienceGroups:
|
|
330
|
-
|
|
331
|
-
|
|
483
|
+
audienceGroups: z7.number().default(0),
|
|
484
|
+
/** the caller's role on it (site-roles.ts); servers before site roles omit it */
|
|
485
|
+
role: SiteRoleSchema.optional(),
|
|
486
|
+
/** the caller may pull its current version (`pull` capability; servers without it omit it) */
|
|
487
|
+
canPull: z7.boolean().optional(),
|
|
488
|
+
/** its home page's thumbnail, when it has one (`thumbnails` capability; thumbnails.ts) */
|
|
489
|
+
thumbnail: ThumbnailSchema.optional(),
|
|
490
|
+
createdAt: z7.string(),
|
|
491
|
+
updatedAt: z7.string()
|
|
492
|
+
});
|
|
493
|
+
var SiteWithAudienceSchema = SiteSchema.extend({
|
|
494
|
+
audience: AudienceSchema.optional(),
|
|
495
|
+
owners: SiteRoleMembersSchema.optional(),
|
|
496
|
+
publishers: SiteRoleMembersSchema.optional()
|
|
497
|
+
});
|
|
498
|
+
var VersionSchema = z7.object({
|
|
499
|
+
id: z7.string(),
|
|
500
|
+
fileCount: z7.number(),
|
|
501
|
+
totalBytes: z7.number(),
|
|
502
|
+
/** who published it (email); absent on versions published before it was recorded */
|
|
503
|
+
publishedBy: z7.string().optional(),
|
|
504
|
+
/**
|
|
505
|
+
* A path publish (`pathPublish` capability): the folder it replaced (`30-10-26`, shown as
|
|
506
|
+
* `/30-10-26/`), and the version it carried the rest of the site forward from (absent when the
|
|
507
|
+
* site had none)
|
|
508
|
+
*/
|
|
509
|
+
path: z7.string().optional(),
|
|
510
|
+
base: z7.string().optional(),
|
|
511
|
+
createdAt: z7.string()
|
|
332
512
|
});
|
|
333
|
-
var
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
fileCount: z3.number(),
|
|
337
|
-
totalBytes: z3.number(),
|
|
338
|
-
createdAt: z3.string()
|
|
513
|
+
var SiteDetailSchema = SiteWithAudienceSchema.extend({
|
|
514
|
+
owner: z7.string().optional(),
|
|
515
|
+
versions: z7.array(VersionSchema)
|
|
339
516
|
});
|
|
340
|
-
var
|
|
341
|
-
var
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
url: z3.string(),
|
|
517
|
+
var SiteListSchema = z7.object({ sites: z7.array(SiteSchema) });
|
|
518
|
+
var SharedSiteSchema = z7.object({
|
|
519
|
+
name: z7.string(),
|
|
520
|
+
url: z7.string(),
|
|
345
521
|
/** the site's review page (`comments` capability) */
|
|
346
|
-
reviewUrl:
|
|
347
|
-
owner:
|
|
522
|
+
reviewUrl: z7.string().optional(),
|
|
523
|
+
owner: z7.string(),
|
|
348
524
|
/** how the caller is in the audience: by address, and through which of their groups (slugs) */
|
|
349
|
-
via:
|
|
350
|
-
|
|
351
|
-
|
|
525
|
+
via: z7.object({ direct: z7.boolean(), groups: z7.array(z7.string()) }),
|
|
526
|
+
/** when its thumbnail was taken, when it has one (`thumbnails` capability) */
|
|
527
|
+
thumbnail: ThumbnailStampSchema.optional(),
|
|
528
|
+
createdAt: z7.string(),
|
|
529
|
+
updatedAt: z7.string()
|
|
352
530
|
});
|
|
353
|
-
var SharedSiteListSchema =
|
|
354
|
-
var SiteFileSchema =
|
|
355
|
-
path:
|
|
356
|
-
size:
|
|
357
|
-
contentType:
|
|
531
|
+
var SharedSiteListSchema = z7.object({ sites: z7.array(SharedSiteSchema) });
|
|
532
|
+
var SiteFileSchema = z7.object({
|
|
533
|
+
path: z7.string(),
|
|
534
|
+
size: z7.number(),
|
|
535
|
+
contentType: z7.string()
|
|
358
536
|
});
|
|
359
|
-
var VersionFilesSchema =
|
|
360
|
-
versionId:
|
|
361
|
-
files:
|
|
362
|
-
totalBytes:
|
|
363
|
-
generatedIndex:
|
|
537
|
+
var VersionFilesSchema = z7.object({
|
|
538
|
+
versionId: z7.string(),
|
|
539
|
+
files: z7.array(SiteFileSchema),
|
|
540
|
+
totalBytes: z7.number(),
|
|
541
|
+
generatedIndex: z7.boolean()
|
|
364
542
|
});
|
|
365
|
-
var
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
543
|
+
var PULL_URL_TTL_SECONDS = 15 * 60;
|
|
544
|
+
var PullFileSchema = SiteFileSchema.extend({
|
|
545
|
+
/** hex SHA-256 of the bytes */
|
|
546
|
+
sha256: z7.string(),
|
|
547
|
+
/** a signed GET for the bytes, good for PULL_URL_TTL_SECONDS */
|
|
548
|
+
url: z7.string()
|
|
369
549
|
});
|
|
370
|
-
var
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
550
|
+
var PullResponseSchema = z7.object({
|
|
551
|
+
versionId: z7.string(),
|
|
552
|
+
path: z7.string().optional(),
|
|
553
|
+
files: z7.array(PullFileSchema),
|
|
554
|
+
/** when the URLs stop working */
|
|
555
|
+
expiresAt: z7.string()
|
|
374
556
|
});
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
|
|
557
|
+
var NameCheckSchema = z7.object({
|
|
558
|
+
name: z7.string(),
|
|
559
|
+
status: z7.enum(["available", "yours", "taken", "invalid"]),
|
|
560
|
+
suggestions: z7.array(z7.string())
|
|
561
|
+
});
|
|
562
|
+
var WhySiteSchema = z7.object({ url: z7.string().nullable() });
|
|
563
|
+
var DeployRequestSchema = z7.object({
|
|
564
|
+
manifest: DeployManifestSchema,
|
|
565
|
+
path: PublishPathSchema.optional(),
|
|
566
|
+
base: z7.string().min(1).max(100).optional(),
|
|
567
|
+
partial: z7.boolean().optional(),
|
|
568
|
+
remove: z7.array(z7.string().min(1).max(MAX_PATH_LENGTH)).max(MAX_FILES).optional(),
|
|
569
|
+
spa: z7.boolean().optional(),
|
|
570
|
+
visibility: VisibilitySchema.optional(),
|
|
571
|
+
access: AccessLevelSchema.optional(),
|
|
572
|
+
audience: AudienceInputSchema.optional()
|
|
573
|
+
}).superRefine((req, ctx) => {
|
|
574
|
+
const issue = (message) => ctx.addIssue({ code: "custom", message });
|
|
575
|
+
if (req.manifest.files.length === 0 && !(req.partial && req.remove?.length)) {
|
|
576
|
+
issue("manifest.files: at least one file");
|
|
577
|
+
}
|
|
578
|
+
if (req.partial && req.base === void 0) issue("`partial` needs `base`");
|
|
579
|
+
if (req.remove !== void 0 && !req.partial) issue("`remove` needs `partial: true`");
|
|
580
|
+
if (req.base !== void 0 && req.path !== void 0) issue("Pass `base` or `path`, not both");
|
|
581
|
+
});
|
|
582
|
+
var UploadSchema = z7.object({
|
|
583
|
+
path: z7.string(),
|
|
584
|
+
url: z7.string(),
|
|
378
585
|
/** headers the client must send with the PUT (they are part of the signature) */
|
|
379
|
-
headers:
|
|
586
|
+
headers: z7.record(z7.string(), z7.string())
|
|
380
587
|
});
|
|
381
|
-
var DeployResponseSchema =
|
|
588
|
+
var DeployResponseSchema = z7.discriminatedUnion("status", [
|
|
382
589
|
/** content identical to the current version — nothing happened */
|
|
383
|
-
|
|
590
|
+
z7.object({ status: z7.literal("unchanged"), versionId: z7.string(), site: SiteSchema }),
|
|
384
591
|
/** this content was deployed before — site now points at that version again */
|
|
385
|
-
|
|
592
|
+
z7.object({ status: z7.literal("repointed"), versionId: z7.string(), site: SiteSchema }),
|
|
386
593
|
/** new content — PUT every upload, then POST /api/sites/:name/deploys/:versionId/finalize */
|
|
387
|
-
|
|
388
|
-
status:
|
|
389
|
-
versionId:
|
|
390
|
-
uploads:
|
|
594
|
+
z7.object({
|
|
595
|
+
status: z7.literal("upload"),
|
|
596
|
+
versionId: z7.string(),
|
|
597
|
+
uploads: z7.array(UploadSchema)
|
|
391
598
|
})
|
|
392
599
|
]);
|
|
393
|
-
var FinalizeResponseSchema =
|
|
394
|
-
status:
|
|
395
|
-
versionId:
|
|
396
|
-
site: SiteSchema
|
|
600
|
+
var FinalizeResponseSchema = z7.object({
|
|
601
|
+
status: z7.literal("live"),
|
|
602
|
+
versionId: z7.string(),
|
|
603
|
+
site: SiteSchema,
|
|
604
|
+
/** this deploy made the site live for the first time (a new site, its access just set) */
|
|
605
|
+
first: z7.boolean().optional()
|
|
397
606
|
});
|
|
398
|
-
var RollbackRequestSchema =
|
|
399
|
-
var UpdateSiteRequestSchema =
|
|
607
|
+
var RollbackRequestSchema = z7.object({ versionId: z7.string() });
|
|
608
|
+
var UpdateSiteRequestSchema = z7.object({
|
|
609
|
+
access: AccessLevelSchema.optional(),
|
|
400
610
|
visibility: VisibilitySchema.optional(),
|
|
401
|
-
spa:
|
|
402
|
-
audience: AudienceInputSchema.optional()
|
|
611
|
+
spa: z7.boolean().optional(),
|
|
612
|
+
audience: AudienceInputSchema.optional(),
|
|
613
|
+
/** the whole new list of owners (site-roles.ts): owners only */
|
|
614
|
+
owners: OwnersInputSchema.optional(),
|
|
615
|
+
/** the whole new list of publishers (site-roles.ts): owners only */
|
|
616
|
+
publishers: PublishersInputSchema.optional()
|
|
403
617
|
});
|
|
404
|
-
var ApiKeySchema =
|
|
405
|
-
id:
|
|
406
|
-
name:
|
|
407
|
-
createdAt:
|
|
408
|
-
lastUsedAt:
|
|
618
|
+
var ApiKeySchema = z7.object({
|
|
619
|
+
id: z7.string(),
|
|
620
|
+
name: z7.string(),
|
|
621
|
+
createdAt: z7.string(),
|
|
622
|
+
lastUsedAt: z7.string().nullable()
|
|
409
623
|
});
|
|
410
|
-
var ApiKeyListSchema =
|
|
411
|
-
var ErrorEnvelopeSchema =
|
|
412
|
-
error:
|
|
624
|
+
var ApiKeyListSchema = z7.object({ keys: z7.array(ApiKeySchema) });
|
|
625
|
+
var ErrorEnvelopeSchema = z7.object({
|
|
626
|
+
error: z7.object({ code: z7.string(), message: z7.string() })
|
|
413
627
|
});
|
|
414
|
-
var ShareResponseSchema =
|
|
415
|
-
var ConnectionSchema =
|
|
416
|
-
id:
|
|
417
|
-
clientName:
|
|
628
|
+
var ShareResponseSchema = z7.object({ shareUrl: z7.string() });
|
|
629
|
+
var ConnectionSchema = z7.object({
|
|
630
|
+
id: z7.string(),
|
|
631
|
+
clientName: z7.string(),
|
|
418
632
|
/** host the client identifies as (CIMD client_id host or redirect URI host) */
|
|
419
|
-
clientHost:
|
|
420
|
-
createdAt:
|
|
421
|
-
lastUsedAt:
|
|
633
|
+
clientHost: z7.string().nullable(),
|
|
634
|
+
createdAt: z7.string(),
|
|
635
|
+
lastUsedAt: z7.string().nullable()
|
|
422
636
|
});
|
|
423
|
-
var ConnectionListSchema =
|
|
424
|
-
var OAuthRequestInfoSchema =
|
|
425
|
-
clientName:
|
|
426
|
-
clientUri:
|
|
637
|
+
var ConnectionListSchema = z7.object({ connections: z7.array(ConnectionSchema) });
|
|
638
|
+
var OAuthRequestInfoSchema = z7.object({
|
|
639
|
+
clientName: z7.string(),
|
|
640
|
+
clientUri: z7.string().nullable(),
|
|
427
641
|
/** host of the CIMD client_id URL, when the client identified itself that way */
|
|
428
|
-
clientIdHost:
|
|
429
|
-
redirectHost:
|
|
642
|
+
clientIdHost: z7.string().nullable(),
|
|
643
|
+
redirectHost: z7.string(),
|
|
430
644
|
/** true when the code goes to a loopback address (any local program could be asking) */
|
|
431
|
-
loopback:
|
|
432
|
-
scopes:
|
|
433
|
-
email:
|
|
645
|
+
loopback: z7.boolean(),
|
|
646
|
+
scopes: z7.array(z7.string()),
|
|
647
|
+
email: z7.string()
|
|
434
648
|
});
|
|
435
|
-
var OAuthDecisionSchema =
|
|
436
|
-
var AdminUserSchema =
|
|
437
|
-
email:
|
|
438
|
-
name:
|
|
439
|
-
picture:
|
|
440
|
-
userSlug:
|
|
441
|
-
createdAt:
|
|
442
|
-
lastSignInAt:
|
|
649
|
+
var OAuthDecisionSchema = z7.object({ redirectUrl: z7.string() });
|
|
650
|
+
var AdminUserSchema = z7.object({
|
|
651
|
+
email: z7.string(),
|
|
652
|
+
name: z7.string(),
|
|
653
|
+
picture: z7.string().nullable(),
|
|
654
|
+
userSlug: z7.string(),
|
|
655
|
+
createdAt: z7.string().nullable(),
|
|
656
|
+
lastSignInAt: z7.string().nullable(),
|
|
443
657
|
/** sites the user holds, archived ones included */
|
|
444
|
-
siteCount:
|
|
445
|
-
admin:
|
|
658
|
+
siteCount: z7.number(),
|
|
659
|
+
admin: z7.boolean(),
|
|
660
|
+
/** outside the email policy, on a live guest invitation (see GET /api/admin/invitations) */
|
|
661
|
+
guest: z7.boolean()
|
|
446
662
|
});
|
|
447
|
-
var AdminUserListSchema =
|
|
448
|
-
users:
|
|
663
|
+
var AdminUserListSchema = z7.object({
|
|
664
|
+
users: z7.array(AdminUserSchema),
|
|
449
665
|
/** pass back as `cursor` for the next page; null: no more */
|
|
450
|
-
cursor:
|
|
666
|
+
cursor: z7.string().nullable()
|
|
451
667
|
});
|
|
452
|
-
var
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
668
|
+
var AdminSiteSchema = z7.object({
|
|
669
|
+
name: z7.string(),
|
|
670
|
+
url: z7.string(),
|
|
671
|
+
owner: z7.string(),
|
|
672
|
+
access: AccessLevelSchema,
|
|
673
|
+
archived: z7.boolean(),
|
|
674
|
+
/** it has a current version */
|
|
675
|
+
live: z7.boolean(),
|
|
676
|
+
/** when its thumbnail was taken, when it has one (`thumbnails` capability) */
|
|
677
|
+
thumbnail: ThumbnailStampSchema.optional(),
|
|
678
|
+
createdAt: z7.string(),
|
|
679
|
+
updatedAt: z7.string()
|
|
680
|
+
});
|
|
681
|
+
var AdminSiteListSchema = z7.object({
|
|
682
|
+
sites: z7.array(AdminSiteSchema),
|
|
683
|
+
/** the list stops at the server's ceiling: there are more sites than it holds */
|
|
684
|
+
truncated: z7.boolean()
|
|
685
|
+
});
|
|
686
|
+
var SETTING_GROUPS = [
|
|
687
|
+
"sign_in",
|
|
688
|
+
"sites",
|
|
689
|
+
"branding",
|
|
690
|
+
"features",
|
|
691
|
+
"mail",
|
|
692
|
+
"platform"
|
|
693
|
+
];
|
|
694
|
+
var SettingValueSchema = z7.union([z7.string(), z7.number(), z7.boolean(), z7.array(z7.string())]).nullable();
|
|
695
|
+
var AdminSettingsSchema = z7.object({
|
|
696
|
+
settings: z7.array(
|
|
697
|
+
z7.object({
|
|
698
|
+
key: z7.string(),
|
|
699
|
+
group: z7.enum(SETTING_GROUPS),
|
|
700
|
+
value: SettingValueSchema,
|
|
701
|
+
set: z7.boolean()
|
|
702
|
+
})
|
|
703
|
+
),
|
|
704
|
+
secrets: z7.array(
|
|
705
|
+
z7.object({ name: z7.string(), group: z7.enum(SETTING_GROUPS), set: z7.boolean() }).strict()
|
|
706
|
+
)
|
|
707
|
+
});
|
|
708
|
+
var InvitationRoleSchema = z7.enum(["member", "guest"]);
|
|
709
|
+
var InvitationViaSchema = z7.enum(["admin", "share", "group"]);
|
|
710
|
+
var AdminInvitationSchema = z7.object({
|
|
711
|
+
email: z7.string(),
|
|
712
|
+
role: InvitationRoleSchema,
|
|
713
|
+
/** who invited them: the admin, or whoever shared */
|
|
714
|
+
invitedBy: z7.string(),
|
|
715
|
+
via: InvitationViaSchema,
|
|
716
|
+
createdAt: z7.string(),
|
|
717
|
+
/** set once revoked; the invitation is kept as the record */
|
|
718
|
+
revokedAt: z7.string().nullable(),
|
|
719
|
+
/** their first sign-in has happened */
|
|
720
|
+
signedIn: z7.boolean(),
|
|
721
|
+
lastSignInAt: z7.string().nullable()
|
|
722
|
+
});
|
|
723
|
+
var AdminInvitationListSchema = z7.object({
|
|
724
|
+
invitations: z7.array(AdminInvitationSchema),
|
|
725
|
+
/** pass back as `cursor` for the next page; null: no more */
|
|
726
|
+
cursor: z7.string().nullable()
|
|
727
|
+
});
|
|
728
|
+
var InviteRequestSchema = z7.object({
|
|
729
|
+
email: z7.string().trim().toLowerCase().min(3).max(254)
|
|
730
|
+
});
|
|
731
|
+
var AdminInviteResultSchema = z7.object({
|
|
732
|
+
invitation: AdminInvitationSchema,
|
|
733
|
+
/** the provider took the email */
|
|
734
|
+
sent: z7.boolean(),
|
|
735
|
+
/** why it didn't, in words (`rejected: sender not verified`) */
|
|
736
|
+
error: z7.string().nullable()
|
|
737
|
+
});
|
|
738
|
+
var AuditEntrySchema = z7.object({
|
|
739
|
+
id: z7.string(),
|
|
740
|
+
at: z7.string(),
|
|
741
|
+
action: z7.enum([
|
|
742
|
+
"impersonation.start",
|
|
743
|
+
"impersonation.stop",
|
|
744
|
+
"invitation.create",
|
|
745
|
+
"invitation.revoke",
|
|
746
|
+
"site.change",
|
|
747
|
+
"site.open"
|
|
748
|
+
]),
|
|
749
|
+
actor: z7.string(),
|
|
750
|
+
/** the person, or for site.change and site.open the site's name */
|
|
751
|
+
target: z7.string(),
|
|
752
|
+
/** site.change, site.open: the site's owner */
|
|
753
|
+
owner: z7.string().optional(),
|
|
754
|
+
/** site.change: what the admin did (a policy action, e.g. `site.archive`) */
|
|
755
|
+
change: z7.string().optional(),
|
|
756
|
+
/** invitation.create: how it came about (an admin's invite, a share, a group) */
|
|
757
|
+
via: InvitationViaSchema.optional(),
|
|
758
|
+
/** invitation.create: what it lets them be */
|
|
759
|
+
role: InvitationRoleSchema.optional(),
|
|
760
|
+
until: z7.string().optional(),
|
|
761
|
+
lapsed: z7.boolean().optional()
|
|
762
|
+
});
|
|
763
|
+
var AuditListSchema = z7.object({ entries: z7.array(AuditEntrySchema) });
|
|
764
|
+
var ImpersonateRequestSchema = z7.object({ email: z7.string().trim().toLowerCase().min(3) });
|
|
765
|
+
var SignInLinkRequestSchema = z7.object({
|
|
766
|
+
email: z7.string().trim().toLowerCase().min(3).max(254),
|
|
767
|
+
return: z7.string().max(2048).optional()
|
|
460
768
|
});
|
|
461
|
-
var AuditListSchema = z3.object({ entries: z3.array(AuditEntrySchema) });
|
|
462
|
-
var ImpersonateRequestSchema = z3.object({ email: z3.string().trim().toLowerCase().min(3) });
|
|
463
769
|
|
|
464
770
|
// ../../packages/shared/src/comments.ts
|
|
465
|
-
import { z as
|
|
771
|
+
import { z as z8 } from "zod";
|
|
466
772
|
var COMMENT_MAX_CHARS = 4e3;
|
|
467
773
|
var COMMENT_PATH_MAX = 1024;
|
|
468
|
-
var CommentAuthorSchema =
|
|
469
|
-
|
|
470
|
-
|
|
774
|
+
var CommentAuthorSchema = z8.discriminatedUnion("kind", [
|
|
775
|
+
z8.object({ kind: z8.literal("user"), email: z8.string() }),
|
|
776
|
+
z8.object({ kind: z8.literal("guest"), name: z8.string() })
|
|
471
777
|
]);
|
|
472
778
|
var ANCHOR_SELECTOR_MAX = 512;
|
|
473
779
|
var ANCHOR_QUOTE_MAX = 500;
|
|
474
|
-
var px =
|
|
475
|
-
var percent =
|
|
476
|
-
var CommentAnchorSchema =
|
|
780
|
+
var px = z8.number().finite().min(-1e7).max(1e7);
|
|
781
|
+
var percent = z8.number().min(0).max(100);
|
|
782
|
+
var CommentAnchorSchema = z8.object({
|
|
477
783
|
x: percent,
|
|
478
784
|
y: percent,
|
|
479
|
-
selector:
|
|
785
|
+
selector: z8.string().min(1).max(ANCHOR_SELECTOR_MAX).optional(),
|
|
480
786
|
ex: percent.optional(),
|
|
481
787
|
ey: percent.optional(),
|
|
482
788
|
docX: px.optional(),
|
|
483
789
|
docY: px.optional(),
|
|
484
790
|
scrollX: px.optional(),
|
|
485
791
|
scrollY: px.optional(),
|
|
486
|
-
vw:
|
|
487
|
-
vh:
|
|
488
|
-
hash:
|
|
792
|
+
vw: z8.number().finite().min(0).max(1e5).optional(),
|
|
793
|
+
vh: z8.number().finite().min(0).max(1e5).optional(),
|
|
794
|
+
hash: z8.string().max(COMMENT_PATH_MAX).optional(),
|
|
489
795
|
ew: percent.optional(),
|
|
490
796
|
eh: percent.optional(),
|
|
491
|
-
docW:
|
|
492
|
-
docH:
|
|
493
|
-
quote:
|
|
797
|
+
docW: z8.number().finite().min(0).max(1e7).optional(),
|
|
798
|
+
docH: z8.number().finite().min(0).max(1e7).optional(),
|
|
799
|
+
quote: z8.string().min(1).max(ANCHOR_QUOTE_MAX).optional()
|
|
494
800
|
});
|
|
495
|
-
var CommentSchema =
|
|
496
|
-
id:
|
|
801
|
+
var CommentSchema = z8.object({
|
|
802
|
+
id: z8.string(),
|
|
497
803
|
/** the thread this replies to; null for the comment that starts a thread */
|
|
498
|
-
parentId:
|
|
804
|
+
parentId: z8.string().nullable(),
|
|
499
805
|
/** the page, as normalizeCommentPath() writes it ('/', '/docs/', '/about.html') */
|
|
500
|
-
path:
|
|
806
|
+
path: z8.string(),
|
|
501
807
|
/** the version the site was serving when the comment was made (null: none was) */
|
|
502
|
-
versionId:
|
|
808
|
+
versionId: z8.string().nullable(),
|
|
503
809
|
anchor: CommentAnchorSchema.nullable(),
|
|
504
|
-
body:
|
|
810
|
+
body: z8.string(),
|
|
505
811
|
author: CommentAuthorSchema,
|
|
506
|
-
createdAt:
|
|
812
|
+
createdAt: z8.string(),
|
|
507
813
|
/** threads only: when and by whom it was resolved (null: open) */
|
|
508
|
-
resolvedAt:
|
|
814
|
+
resolvedAt: z8.string().nullable(),
|
|
509
815
|
resolvedBy: CommentAuthorSchema.nullable(),
|
|
510
816
|
/** whether the caller may delete it (policy's answer, so clients don't guess) */
|
|
511
|
-
canDelete:
|
|
817
|
+
canDelete: z8.boolean()
|
|
512
818
|
});
|
|
513
|
-
var CommentListSchema =
|
|
514
|
-
site:
|
|
515
|
-
name:
|
|
516
|
-
url:
|
|
517
|
-
currentVersionId:
|
|
819
|
+
var CommentListSchema = z8.object({
|
|
820
|
+
site: z8.object({
|
|
821
|
+
name: z8.string(),
|
|
822
|
+
url: z8.string(),
|
|
823
|
+
currentVersionId: z8.string().nullable()
|
|
518
824
|
}),
|
|
519
|
-
comments:
|
|
520
|
-
can:
|
|
521
|
-
create:
|
|
522
|
-
resolve:
|
|
825
|
+
comments: z8.array(CommentSchema),
|
|
826
|
+
can: z8.object({
|
|
827
|
+
create: z8.boolean(),
|
|
828
|
+
resolve: z8.boolean(),
|
|
523
829
|
/** delete anyone's comment (everyone may delete their own) */
|
|
524
|
-
deleteAny:
|
|
830
|
+
deleteAny: z8.boolean()
|
|
525
831
|
})
|
|
526
832
|
});
|
|
527
|
-
var CreateCommentRequestSchema =
|
|
528
|
-
body:
|
|
529
|
-
path:
|
|
530
|
-
parentId:
|
|
833
|
+
var CreateCommentRequestSchema = z8.object({
|
|
834
|
+
body: z8.string().trim().min(1).max(COMMENT_MAX_CHARS),
|
|
835
|
+
path: z8.string().max(COMMENT_PATH_MAX).optional(),
|
|
836
|
+
parentId: z8.string().min(1).max(100).optional(),
|
|
531
837
|
anchor: CommentAnchorSchema.optional()
|
|
532
838
|
});
|
|
533
|
-
var UpdateCommentRequestSchema =
|
|
839
|
+
var UpdateCommentRequestSchema = z8.object({ resolved: z8.boolean() });
|
|
534
840
|
function commentThreads(comments) {
|
|
535
841
|
const threads = /* @__PURE__ */ new Map();
|
|
536
842
|
for (const c of comments) {
|
|
@@ -545,17 +851,18 @@ function authorLabel(author) {
|
|
|
545
851
|
return author.kind === "user" ? author.email : `${author.name} (guest)`;
|
|
546
852
|
}
|
|
547
853
|
function reviewUrl(appOrigin, name, path3) {
|
|
548
|
-
const
|
|
549
|
-
return path3 && path3 !== "/" ? `${
|
|
854
|
+
const base2 = `${appOrigin.replace(/\/+$/, "")}/review/${name === "<name>" ? name : encodeURIComponent(name)}`;
|
|
855
|
+
return path3 && path3 !== "/" ? `${base2}?path=${encodeURIComponent(path3)}` : base2;
|
|
550
856
|
}
|
|
551
857
|
|
|
552
858
|
// ../../packages/shared/src/distribution.ts
|
|
553
859
|
var CLI_PACKAGE = "@nearform/fling";
|
|
554
860
|
var REPO_SLUG = "nearform/fling";
|
|
555
861
|
var DEFAULT_INSTALL_COMMAND = `npm install -g ${CLI_PACKAGE}`;
|
|
862
|
+
var REPO_URL = `https://github.com/${REPO_SLUG}`;
|
|
556
863
|
|
|
557
864
|
// ../../packages/shared/src/groups.ts
|
|
558
|
-
import { z as
|
|
865
|
+
import { z as z9 } from "zod";
|
|
559
866
|
var GROUP_MEMBERS_MAX = 500;
|
|
560
867
|
var GROUP_SLUG_MAX = 50;
|
|
561
868
|
var GROUP_NAME_MAX = 100;
|
|
@@ -564,30 +871,30 @@ function isValidGroupSlug(slug) {
|
|
|
564
871
|
return slug.length <= GROUP_SLUG_MAX && GROUP_SLUG_RE.test(slug) && !slug.includes("--");
|
|
565
872
|
}
|
|
566
873
|
var GROUP_SLUG_RULES = `1-${GROUP_SLUG_MAX} lowercase letters, digits and single hyphens`;
|
|
567
|
-
var GroupSlugSchema =
|
|
568
|
-
var GroupNameSchema =
|
|
569
|
-
var memberEmails =
|
|
570
|
-
var GroupSummarySchema =
|
|
571
|
-
slug:
|
|
572
|
-
name:
|
|
874
|
+
var GroupSlugSchema = z9.string().trim().toLowerCase().refine(isValidGroupSlug, `A group's slug is ${GROUP_SLUG_RULES}`);
|
|
875
|
+
var GroupNameSchema = z9.string().trim().min(1).max(GROUP_NAME_MAX);
|
|
876
|
+
var memberEmails = z9.array(z9.string().trim().toLowerCase().min(3).max(254)).max(GROUP_MEMBERS_MAX, `A group holds at most ${GROUP_MEMBERS_MAX} people`);
|
|
877
|
+
var GroupSummarySchema = z9.object({
|
|
878
|
+
slug: z9.string(),
|
|
879
|
+
name: z9.string(),
|
|
573
880
|
/** the email of the user who made it, the only one who may change it */
|
|
574
|
-
owner:
|
|
575
|
-
size:
|
|
576
|
-
createdAt:
|
|
577
|
-
updatedAt:
|
|
881
|
+
owner: z9.string(),
|
|
882
|
+
size: z9.number(),
|
|
883
|
+
createdAt: z9.string(),
|
|
884
|
+
updatedAt: z9.string()
|
|
578
885
|
});
|
|
579
|
-
var GroupListSchema =
|
|
886
|
+
var GroupListSchema = z9.object({ groups: z9.array(GroupSummarySchema) });
|
|
580
887
|
var GroupDetailSchema = GroupSummarySchema.extend({
|
|
581
|
-
members:
|
|
582
|
-
canManage:
|
|
888
|
+
members: z9.array(AudienceEmailSchema),
|
|
889
|
+
canManage: z9.boolean()
|
|
583
890
|
});
|
|
584
|
-
var CreateGroupRequestSchema =
|
|
891
|
+
var CreateGroupRequestSchema = z9.object({
|
|
585
892
|
slug: GroupSlugSchema,
|
|
586
893
|
name: GroupNameSchema,
|
|
587
894
|
members: memberEmails.optional()
|
|
588
895
|
});
|
|
589
|
-
var UpdateGroupRequestSchema =
|
|
590
|
-
var GroupMembersRequestSchema =
|
|
896
|
+
var UpdateGroupRequestSchema = z9.object({ name: GroupNameSchema });
|
|
897
|
+
var GroupMembersRequestSchema = z9.object({
|
|
591
898
|
add: memberEmails.optional(),
|
|
592
899
|
remove: memberEmails.optional()
|
|
593
900
|
});
|
|
@@ -642,48 +949,67 @@ function siteUrl(label, sitesDomain, protocol = "https:") {
|
|
|
642
949
|
}
|
|
643
950
|
|
|
644
951
|
// ../../packages/shared/src/instance.ts
|
|
645
|
-
import { z as
|
|
952
|
+
import { z as z10 } from "zod";
|
|
646
953
|
var INSTANCE_METADATA_PATH = "/.well-known/fling";
|
|
647
954
|
var HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
648
|
-
var color =
|
|
649
|
-
var
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
955
|
+
var color = z10.string().regex(HEX_COLOR_RE);
|
|
956
|
+
var SignInProviderSchema = z10.looseObject({
|
|
957
|
+
/** `google` or `oidc` today; clients pass it back as is. */
|
|
958
|
+
id: z10.string().min(1),
|
|
959
|
+
/** "Sign in with <displayName>". */
|
|
960
|
+
displayName: z10.string().min(1),
|
|
961
|
+
issuer: z10.string().optional()
|
|
962
|
+
});
|
|
963
|
+
var InstanceAuthSchema = z10.looseObject({
|
|
964
|
+
kind: z10.string().min(1),
|
|
965
|
+
displayName: z10.string().min(1),
|
|
966
|
+
issuer: z10.string().optional(),
|
|
967
|
+
providers: z10.array(SignInProviderSchema).optional(),
|
|
968
|
+
emailDomains: z10.array(z10.string()).default([]),
|
|
969
|
+
invitedEmails: z10.boolean().optional()
|
|
655
970
|
});
|
|
656
|
-
var BrandColorsSchema =
|
|
971
|
+
var BrandColorsSchema = z10.looseObject({
|
|
657
972
|
primary: color.optional(),
|
|
658
973
|
primaryContent: color.optional(),
|
|
659
974
|
accent: color.optional(),
|
|
660
975
|
ink: color.optional(),
|
|
661
976
|
highlight: color.optional()
|
|
662
977
|
});
|
|
663
|
-
var InstanceBrandSchema =
|
|
978
|
+
var InstanceBrandSchema = z10.looseObject({
|
|
664
979
|
/** https: or data: URL of the logo mark shown before the name (light theme). */
|
|
665
|
-
logoUrl:
|
|
980
|
+
logoUrl: z10.string().optional(),
|
|
666
981
|
/** The same for the dark theme; defaults to `logoUrl`. */
|
|
667
|
-
logoDarkUrl:
|
|
668
|
-
faviconUrl:
|
|
982
|
+
logoDarkUrl: z10.string().optional(),
|
|
983
|
+
faviconUrl: z10.string().optional(),
|
|
669
984
|
colors: BrandColorsSchema.default({})
|
|
670
985
|
});
|
|
671
|
-
var InstanceMetadataSchema =
|
|
672
|
-
version:
|
|
986
|
+
var InstanceMetadataSchema = z10.looseObject({
|
|
987
|
+
version: z10.number().int().min(1),
|
|
673
988
|
/** Display name, e.g. "Fling". */
|
|
674
|
-
name:
|
|
989
|
+
name: z10.string().min(1),
|
|
675
990
|
/** e.g. `https://fling.example.test` (no trailing slash) */
|
|
676
|
-
appOrigin:
|
|
991
|
+
appOrigin: z10.string().url(),
|
|
677
992
|
/** host[:port] sites hang off: a site is `<name>.<sitesDomain>` */
|
|
678
|
-
sitesDomain:
|
|
679
|
-
siteScheme:
|
|
993
|
+
sitesDomain: z10.string().min(1),
|
|
994
|
+
siteScheme: z10.enum(["http", "https"]),
|
|
680
995
|
auth: InstanceAuthSchema,
|
|
681
996
|
brand: InstanceBrandSchema.default({ colors: {} }),
|
|
682
997
|
/** Feature flags: a client hides what is missing or false. */
|
|
683
|
-
capabilities:
|
|
998
|
+
capabilities: z10.record(z10.string(), z10.boolean()).default({}),
|
|
684
999
|
/** How to install the CLI, e.g. `npm install -g <package>`. */
|
|
685
|
-
installCommand:
|
|
1000
|
+
installCommand: z10.string().min(1),
|
|
1001
|
+
/**
|
|
1002
|
+
* The access level a new site gets when its first deploy names none (`SITE_DEFAULT_ACCESS`).
|
|
1003
|
+
* Missing (older servers) or unknown: `members` (`defaultAccessOf`).
|
|
1004
|
+
*/
|
|
1005
|
+
defaultAccess: DefaultAccessLevelSchema.optional().catch(void 0),
|
|
1006
|
+
/**
|
|
1007
|
+
* The Fling release the server runs, e.g. `0.7.0` (the root package.json's version, built in).
|
|
1008
|
+
* Missing on servers built before releases were stamped.
|
|
1009
|
+
*/
|
|
1010
|
+
release: z10.string().min(1).optional().catch(void 0)
|
|
686
1011
|
});
|
|
1012
|
+
var defaultAccessOf = (instance) => instance?.defaultAccess ?? LEGACY_DEFAULT_ACCESS;
|
|
687
1013
|
function instanceSitesPattern(instance) {
|
|
688
1014
|
return `${instance.siteScheme}://<name>.${instance.sitesDomain}`;
|
|
689
1015
|
}
|
|
@@ -706,22 +1032,22 @@ function instanceMetadataUrl(serverUrl) {
|
|
|
706
1032
|
}
|
|
707
1033
|
|
|
708
1034
|
// ../../packages/shared/src/kvs.ts
|
|
709
|
-
import { z as
|
|
1035
|
+
import { z as z11 } from "zod";
|
|
710
1036
|
|
|
711
1037
|
// ../../packages/shared/src/route-keys.ts
|
|
712
1038
|
var SITE_PASS_TTL_SECONDS = 5 * 60;
|
|
713
1039
|
|
|
714
1040
|
// ../../packages/shared/src/kvs.ts
|
|
715
|
-
var KvsSiteValueSchema =
|
|
1041
|
+
var KvsSiteValueSchema = z11.object({
|
|
716
1042
|
/** siteId — S3 prefix is `sites/<id>/<v>/` */
|
|
717
|
-
id:
|
|
1043
|
+
id: z11.string(),
|
|
718
1044
|
/** current versionId */
|
|
719
|
-
v:
|
|
720
|
-
vis:
|
|
1045
|
+
v: z11.string(),
|
|
1046
|
+
vis: z11.enum(["private", "public"]),
|
|
721
1047
|
/** serve /index.html for unknown extensionless paths */
|
|
722
|
-
spa:
|
|
1048
|
+
spa: z11.boolean(),
|
|
723
1049
|
/** present when the site is restricted (private with an audience): the edge wants a site pass */
|
|
724
|
-
r:
|
|
1050
|
+
r: z11.literal(1).optional()
|
|
725
1051
|
});
|
|
726
1052
|
|
|
727
1053
|
// ../../packages/shared/src/listing.ts
|
|
@@ -760,7 +1086,7 @@ ${rows}
|
|
|
760
1086
|
</ul>`;
|
|
761
1087
|
}).join("\n");
|
|
762
1088
|
const body = pages.length ? sections : '<p class="empty">No HTML pages in this fling.</p>';
|
|
763
|
-
const
|
|
1089
|
+
const count2 = `${pages.length} page${pages.length === 1 ? "" : "s"}`;
|
|
764
1090
|
return `<!doctype html>
|
|
765
1091
|
<html lang="en">
|
|
766
1092
|
<head>
|
|
@@ -795,7 +1121,7 @@ ${rows}
|
|
|
795
1121
|
</head>
|
|
796
1122
|
<body>
|
|
797
1123
|
<main>
|
|
798
|
-
<header><h1>${escapeHtml(siteName2)}</h1><p>${
|
|
1124
|
+
<header><h1>${escapeHtml(siteName2)}</h1><p>${count2}</p></header>
|
|
799
1125
|
${body}
|
|
800
1126
|
<footer>Flung with Fling</footer>
|
|
801
1127
|
</main>
|
|
@@ -805,7 +1131,265 @@ ${body}
|
|
|
805
1131
|
}
|
|
806
1132
|
|
|
807
1133
|
// ../../packages/shared/src/mcp-tools.ts
|
|
808
|
-
import { z as
|
|
1134
|
+
import { z as z13 } from "zod";
|
|
1135
|
+
|
|
1136
|
+
// ../../packages/shared/src/notifications.ts
|
|
1137
|
+
import { z as z12 } from "zod";
|
|
1138
|
+
var CHANNEL_IDS = ["email"];
|
|
1139
|
+
var ChannelIdSchema = z12.enum(CHANNEL_IDS);
|
|
1140
|
+
var DeliverySchema = z12.enum(["on", "off"]);
|
|
1141
|
+
var EVENT_KEYS = [
|
|
1142
|
+
"site.shared",
|
|
1143
|
+
"site.shared_via_group",
|
|
1144
|
+
"site.owner_added",
|
|
1145
|
+
"site.publisher_added",
|
|
1146
|
+
"group.member_added",
|
|
1147
|
+
"comment.created",
|
|
1148
|
+
"comment.reply"
|
|
1149
|
+
];
|
|
1150
|
+
var EventKeySchema = z12.enum(EVENT_KEYS);
|
|
1151
|
+
var EVENT_CATALOGUE = [
|
|
1152
|
+
{
|
|
1153
|
+
key: "site.shared",
|
|
1154
|
+
title: "Someone shares a site with you",
|
|
1155
|
+
description: "A site owner adds you to who may see their site.",
|
|
1156
|
+
section: "sharing",
|
|
1157
|
+
defaults: { email: "on" },
|
|
1158
|
+
coolDown: false
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
key: "site.shared_via_group",
|
|
1162
|
+
title: "Someone shares a site with a group you are in",
|
|
1163
|
+
description: "A site owner adds one of your groups to who may see their site.",
|
|
1164
|
+
section: "sharing",
|
|
1165
|
+
defaults: { email: "on" },
|
|
1166
|
+
coolDown: false
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
key: "site.owner_added",
|
|
1170
|
+
title: "Someone makes you an owner of a site",
|
|
1171
|
+
description: "A site's owner adds you, or a group you are in, to its owners.",
|
|
1172
|
+
section: "sharing",
|
|
1173
|
+
defaults: { email: "on" },
|
|
1174
|
+
coolDown: false,
|
|
1175
|
+
capability: "siteRoles"
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
key: "site.publisher_added",
|
|
1179
|
+
title: "Someone makes you a publisher of a site",
|
|
1180
|
+
description: "A site's owner adds you, or a group you are in, to who may publish it.",
|
|
1181
|
+
section: "sharing",
|
|
1182
|
+
defaults: { email: "on" },
|
|
1183
|
+
coolDown: false,
|
|
1184
|
+
capability: "siteRoles"
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
key: "group.member_added",
|
|
1188
|
+
title: "Someone adds you to a group",
|
|
1189
|
+
description: "A group's owner adds you to it, opening the sites shared with it to you.",
|
|
1190
|
+
section: "groups",
|
|
1191
|
+
defaults: { email: "on" },
|
|
1192
|
+
coolDown: false
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
key: "comment.created",
|
|
1196
|
+
title: "Someone comments on your site",
|
|
1197
|
+
description: "A new thread is started on a site you own.",
|
|
1198
|
+
section: "comments",
|
|
1199
|
+
defaults: { email: "on" },
|
|
1200
|
+
coolDown: true,
|
|
1201
|
+
capability: "comments"
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
key: "comment.reply",
|
|
1205
|
+
title: "Someone replies in your thread",
|
|
1206
|
+
description: "A reply is added to a thread you started or replied in.",
|
|
1207
|
+
section: "comments",
|
|
1208
|
+
defaults: { email: "on" },
|
|
1209
|
+
coolDown: true,
|
|
1210
|
+
capability: "comments"
|
|
1211
|
+
}
|
|
1212
|
+
];
|
|
1213
|
+
var BY_KEY = new Map(EVENT_CATALOGUE.map((e) => [e.key, e]));
|
|
1214
|
+
var NotificationPreferencesUpdateSchema = z12.object({
|
|
1215
|
+
muted: z12.boolean().optional(),
|
|
1216
|
+
cells: z12.partialRecord(EventKeySchema, z12.partialRecord(ChannelIdSchema, DeliverySchema.nullable())).optional()
|
|
1217
|
+
});
|
|
1218
|
+
var EventDefinitionSchema = z12.looseObject({
|
|
1219
|
+
key: z12.string(),
|
|
1220
|
+
title: z12.string(),
|
|
1221
|
+
description: z12.string(),
|
|
1222
|
+
section: z12.string(),
|
|
1223
|
+
defaults: z12.record(z12.string(), DeliverySchema),
|
|
1224
|
+
coolDown: z12.boolean(),
|
|
1225
|
+
capability: z12.string().optional()
|
|
1226
|
+
});
|
|
1227
|
+
var NotificationSettingsSchema = z12.looseObject({
|
|
1228
|
+
channels: z12.array(z12.string()),
|
|
1229
|
+
events: z12.array(EventDefinitionSchema),
|
|
1230
|
+
muted: z12.boolean(),
|
|
1231
|
+
cells: z12.record(z12.string(), z12.record(z12.string(), DeliverySchema)),
|
|
1232
|
+
overrides: z12.record(z12.string(), z12.record(z12.string(), DeliverySchema)).default({})
|
|
1233
|
+
});
|
|
1234
|
+
var SentNotificationSchema = z12.looseObject({
|
|
1235
|
+
id: z12.string(),
|
|
1236
|
+
channel: z12.string(),
|
|
1237
|
+
/** the events it told them about (several when folded) */
|
|
1238
|
+
eventKeys: z12.array(z12.string()),
|
|
1239
|
+
subject: z12.string(),
|
|
1240
|
+
/** the provider's message id, when it gave one */
|
|
1241
|
+
providerId: z12.string().nullable(),
|
|
1242
|
+
sentAt: z12.string()
|
|
1243
|
+
});
|
|
1244
|
+
var SentNotificationListSchema = z12.object({ sent: z12.array(SentNotificationSchema) });
|
|
1245
|
+
var MailCountsSchema = z12.object({
|
|
1246
|
+
sent: z12.number(),
|
|
1247
|
+
retrying: z12.number(),
|
|
1248
|
+
givenUp: z12.number(),
|
|
1249
|
+
suppressed: z12.number(),
|
|
1250
|
+
dropped: z12.number()
|
|
1251
|
+
});
|
|
1252
|
+
var MailAdminSchema = z12.object({
|
|
1253
|
+
config: z12.object({
|
|
1254
|
+
driver: z12.string(),
|
|
1255
|
+
from: z12.string(),
|
|
1256
|
+
fromName: z12.string(),
|
|
1257
|
+
replyTo: z12.string().nullable(),
|
|
1258
|
+
configurationSet: z12.string().nullable(),
|
|
1259
|
+
/** outside invitations are on (`INVITES`) */
|
|
1260
|
+
invites: z12.boolean()
|
|
1261
|
+
}),
|
|
1262
|
+
health: z12.object({
|
|
1263
|
+
/** the first hour counted (UTC, `2026-10-28T14`) */
|
|
1264
|
+
since: z12.string(),
|
|
1265
|
+
/** the hours with any activity, oldest first */
|
|
1266
|
+
hours: z12.array(MailCountsSchema.extend({ hour: z12.string() })),
|
|
1267
|
+
totals: MailCountsSchema,
|
|
1268
|
+
/** the latest failed sends, newest first */
|
|
1269
|
+
failures: z12.array(
|
|
1270
|
+
z12.object({
|
|
1271
|
+
at: z12.string(),
|
|
1272
|
+
batchId: z12.string(),
|
|
1273
|
+
eventKeys: z12.array(z12.string()),
|
|
1274
|
+
reason: z12.string(),
|
|
1275
|
+
detail: z12.string().nullable()
|
|
1276
|
+
})
|
|
1277
|
+
)
|
|
1278
|
+
}),
|
|
1279
|
+
/** test emails this admin may still send this hour, of `limit` */
|
|
1280
|
+
tests: z12.object({ remaining: z12.number(), limit: z12.number() })
|
|
1281
|
+
});
|
|
1282
|
+
var MailTestRequestSchema = z12.object({
|
|
1283
|
+
to: z12.string().trim().toLowerCase().min(3).max(254)
|
|
1284
|
+
});
|
|
1285
|
+
var MailTestResultSchema = z12.discriminatedUnion("ok", [
|
|
1286
|
+
z12.object({ ok: z12.literal(true), providerId: z12.string().nullable(), subject: z12.string() }),
|
|
1287
|
+
z12.object({
|
|
1288
|
+
ok: z12.literal(false),
|
|
1289
|
+
reason: z12.string(),
|
|
1290
|
+
detail: z12.string().nullable(),
|
|
1291
|
+
error: z12.string()
|
|
1292
|
+
})
|
|
1293
|
+
]);
|
|
1294
|
+
var CellChangeSchema = z12.object({
|
|
1295
|
+
event: EventKeySchema,
|
|
1296
|
+
channel: ChannelIdSchema,
|
|
1297
|
+
value: z12.enum(["on", "off", "default"])
|
|
1298
|
+
});
|
|
1299
|
+
function preferencesUpdateOf(change) {
|
|
1300
|
+
const cells = {};
|
|
1301
|
+
for (const c of change.cells ?? []) {
|
|
1302
|
+
cells[c.event] = { ...cells[c.event], [c.channel]: c.value === "default" ? null : c.value };
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
...change.muted === void 0 ? {} : { muted: change.muted },
|
|
1306
|
+
...Object.keys(cells).length ? { cells } : {}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
var SECTION_TITLES = {
|
|
1310
|
+
sharing: "Sharing",
|
|
1311
|
+
groups: "Groups",
|
|
1312
|
+
comments: "Comments"
|
|
1313
|
+
};
|
|
1314
|
+
var sectionTitle = (section) => SECTION_TITLES[section] ?? section;
|
|
1315
|
+
function describeNotificationSettings(settings) {
|
|
1316
|
+
const lines = [
|
|
1317
|
+
settings.muted ? "All notifications are muted: nothing is sent, whatever the settings below say." : `Notifications are on. Channels: ${settings.channels.join(", ")}.`
|
|
1318
|
+
];
|
|
1319
|
+
let section = "";
|
|
1320
|
+
for (const e of settings.events) {
|
|
1321
|
+
if (e.section !== section) {
|
|
1322
|
+
section = e.section;
|
|
1323
|
+
lines.push("", `${sectionTitle(section)}:`);
|
|
1324
|
+
}
|
|
1325
|
+
const cells = settings.channels.map((ch) => {
|
|
1326
|
+
const value = settings.cells[e.key]?.[ch] ?? "off";
|
|
1327
|
+
const own = settings.overrides[e.key]?.[ch] !== void 0;
|
|
1328
|
+
return `${ch} ${value}${own ? " (yours)" : ""}`;
|
|
1329
|
+
});
|
|
1330
|
+
lines.push(`- ${e.key}: ${e.title} \u2014 ${cells.join(", ")}`);
|
|
1331
|
+
}
|
|
1332
|
+
return lines.join("\n");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// ../../packages/shared/src/read-files.ts
|
|
1336
|
+
var MCP_READ_MAX_BYTES = 1.5 * 1024 * 1024;
|
|
1337
|
+
function pageOfFiles(files, opts) {
|
|
1338
|
+
const wanted = opts.paths ? new Set(opts.paths) : null;
|
|
1339
|
+
const selected = wanted ? files.filter((f) => wanted.has(f.path)) : [...files];
|
|
1340
|
+
const found = new Set(selected.map((f) => f.path));
|
|
1341
|
+
const missing = opts.paths ? opts.paths.filter((p) => !found.has(p)) : [];
|
|
1342
|
+
const start = Math.max(0, Number.parseInt(opts.cursor ?? "0", 10) || 0);
|
|
1343
|
+
const page = [];
|
|
1344
|
+
const tooLarge = [];
|
|
1345
|
+
let used = 0;
|
|
1346
|
+
for (let i = start; i < selected.length; i++) {
|
|
1347
|
+
const file = selected[i];
|
|
1348
|
+
if (file.size > opts.budget) {
|
|
1349
|
+
tooLarge.push(file);
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (used + file.size > opts.budget) return { page, tooLarge, missing, nextCursor: String(i) };
|
|
1353
|
+
page.push(file);
|
|
1354
|
+
used += file.size;
|
|
1355
|
+
}
|
|
1356
|
+
return { page, tooLarge, missing };
|
|
1357
|
+
}
|
|
1358
|
+
function encodeForAgent(path3, bytes) {
|
|
1359
|
+
if (isScannablePath(path3) && !looksBinary(bytes)) {
|
|
1360
|
+
try {
|
|
1361
|
+
return { encoding: "utf8", content: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
let binary = "";
|
|
1366
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
1367
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
1368
|
+
}
|
|
1369
|
+
return { encoding: "base64", content: btoa(binary) };
|
|
1370
|
+
}
|
|
1371
|
+
function describeReadFiles(name, result) {
|
|
1372
|
+
const read = result.files.filter((f) => !f.tooLarge);
|
|
1373
|
+
const big = result.files.filter((f) => f.tooLarge);
|
|
1374
|
+
const lines = [
|
|
1375
|
+
`${read.length} file(s) of ${name}, version ${result.versionId}: ${read.map((f) => f.path).join(", ") || "(none)"}.`
|
|
1376
|
+
];
|
|
1377
|
+
if (big.length) {
|
|
1378
|
+
lines.push(
|
|
1379
|
+
`Too large to return here (left out; they stay as they are when you publish on this base): ${big.map((f) => f.path).join(", ")}.`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
if (result.missing.length) lines.push(`Not in this version: ${result.missing.join(", ")}.`);
|
|
1383
|
+
if (result.nextCursor) {
|
|
1384
|
+
lines.push(
|
|
1385
|
+
`More files: call fling_read_files again with cursor "${result.nextCursor}" (or name the files you need in \`paths\`).`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
lines.push(
|
|
1389
|
+
`To change the site, call fling_publish with base "${result.versionId}" and only the files you changed or added (and \`remove\` for files to delete): every other file is kept.`
|
|
1390
|
+
);
|
|
1391
|
+
return lines.join("\n");
|
|
1392
|
+
}
|
|
809
1393
|
|
|
810
1394
|
// ../../packages/shared/src/site-sort.ts
|
|
811
1395
|
var SITE_SORTS = ["updated", "created", "name"];
|
|
@@ -838,38 +1422,65 @@ function mcpWording(instance) {
|
|
|
838
1422
|
sitesPattern: instanceSitesPattern(instance),
|
|
839
1423
|
privateAudience: privateAudience(instance),
|
|
840
1424
|
signIn: `their ${instance.auth.displayName} account`,
|
|
841
|
-
reviewPattern: instance.appOrigin ? reviewUrl(instance.appOrigin, "<name>") : GENERIC_REVIEW
|
|
1425
|
+
reviewPattern: instance.appOrigin ? reviewUrl(instance.appOrigin, "<name>") : GENERIC_REVIEW,
|
|
1426
|
+
defaultAccess: defaultAccessOf(instance),
|
|
1427
|
+
invites: instance.capabilities?.invites === true
|
|
842
1428
|
};
|
|
843
1429
|
}
|
|
1430
|
+
var addressRule = (w) => w.invites ? `Each address must be one that can sign in to ${w.name}, or one from outside it, which is invited as a reviewer (a guest who signs in with an emailed link and sees only what is shared with them; a few a day per person, else \`too_many_invites\`).` : `Each address must be one that can sign in to ${w.name}.`;
|
|
1431
|
+
var defaultAccessPhrase = (w) => w.defaultAccess === "members" ? `open to ${w.privateAudience}` : w.defaultAccess === "only_me" ? "visible only to the user" : "given the Fling's default (only the user, unless the Fling is set up otherwise)";
|
|
1432
|
+
var ACCESS_NOTE = (w) => "Before publishing a new site, ask the user who should be able to open it, unless they already said, and pass `access`: `only_me` (only the user), `members` (" + w.privateAudience + `), \`groups\` or \`people\` (only the groups in \`groups\`, by slug, and the addresses in \`emails\`; one site may name both), or \`public\` (anyone, no sign-in; only if the user asks). Without \`access\` a new site is ${defaultAccessPhrase(w)}. Re-publishing never changes who can open an existing site: use fling_set_access for that. Share links (fling_share) open a site to anyone who has them.`;
|
|
844
1433
|
var NAME_REQUIRED = 'Publishing needs a name: if the user did not give one, propose a short descriptive one (e.g. "q3-report") and confirm it with them first (fling_check_name tells you whether it is free). If the name is taken by someone else the call fails with free suggestions \u2014 offer those to the user and retry with the one they pick.';
|
|
845
1434
|
var INLINE_ONLY = "If you created the content in this conversation, pass it directly \u2014 never pass file paths from your sandbox (this server cannot read them) and never try to curl/upload from your sandbox.";
|
|
846
1435
|
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.";
|
|
847
1436
|
var ENCODING = '`encoding` is "utf8" (default, for text: HTML, CSS, JS, SVG, JSON, Markdown\u2026) or "base64" (binary: images, fonts, PDFs).';
|
|
848
1437
|
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.";
|
|
849
|
-
var listHtml =
|
|
1438
|
+
var listHtml = z13.boolean().optional().describe(
|
|
850
1439
|
"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"
|
|
851
1440
|
);
|
|
852
|
-
var siteName =
|
|
853
|
-
var
|
|
1441
|
+
var siteName = z13.string().min(1).max(200).describe('Site name, e.g. "q3-report"');
|
|
1442
|
+
var PATH_PUBLISH_NOTE = 'With `path` (e.g. "30-10-26"), only that folder of the site is replaced: the files go to <site>/30-10-26/\u2026 (give their paths relative to the folder, its page being "index.html") and every other file of the site stays as it is. Use it to add one report, release note or week\'s page to a shared site without republishing the rest; rolling back removes it again.';
|
|
1443
|
+
var PULL_NOTE = "To change a site that is already published (fix a typo, update a page, add one), start from what is live, not from scratch: read its files with fling_read_files, then call fling_publish with `base` (the `versionId` it returned) and only the files you changed or added, plus `remove` for files to delete; every other file stays as it is. If someone published the site in between, it fails with base_changed: read the files again and redo the changes, rather than publishing without `base` over theirs.";
|
|
1444
|
+
var base = z13.string().min(1).max(100).optional().describe(
|
|
1445
|
+
"The versionId fling_read_files returned: publish only what changed on top of it. `files` then holds only the files you changed or added, and every other file of the site is kept. Fails with base_changed if the site has changed since. Not with `path`"
|
|
1446
|
+
);
|
|
1447
|
+
var remove = z13.array(z13.string().min(1).max(1024)).max(MAX_FILES).optional().describe("With `base`: the paths of the files to delete from the site");
|
|
1448
|
+
var publishPath = z13.string().min(1).max(1024).optional().describe(
|
|
1449
|
+
'Publish into this folder of the site only, e.g. "30-10-26" or "reports/2026" (served at <site>/30-10-26/), keeping every other file. File paths are relative to it. Omit to replace the whole site'
|
|
1450
|
+
);
|
|
1451
|
+
var sitePath = z13.string().min(1).max(1024).describe(
|
|
854
1452
|
'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")'
|
|
855
1453
|
);
|
|
856
|
-
var emails =
|
|
857
|
-
var groupSlugs =
|
|
858
|
-
var
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1454
|
+
var emails = z13.array(z13.string().min(3).max(254)).max(AUDIENCE_EMAILS_MAX);
|
|
1455
|
+
var groupSlugs = z13.array(z13.string().min(1).max(100)).max(AUDIENCE_GROUPS_MAX);
|
|
1456
|
+
var roleChange = {
|
|
1457
|
+
add: z13.array(z13.string().min(3).max(254)).max(PUBLISHER_EMAILS_MAX).optional(),
|
|
1458
|
+
remove: z13.array(z13.string().min(3).max(254)).max(PUBLISHER_EMAILS_MAX).optional(),
|
|
1459
|
+
set: z13.array(z13.string().min(3).max(254)).max(PUBLISHER_EMAILS_MAX).optional(),
|
|
1460
|
+
addGroups: z13.array(z13.string().min(1).max(100)).max(PUBLISHER_GROUPS_MAX).optional(),
|
|
1461
|
+
removeGroups: z13.array(z13.string().min(1).max(100)).max(PUBLISHER_GROUPS_MAX).optional(),
|
|
1462
|
+
setGroups: z13.array(z13.string().min(1).max(100)).max(PUBLISHER_GROUPS_MAX).optional()
|
|
1463
|
+
};
|
|
1464
|
+
var groupSlug = z13.string().min(1).max(100).describe(`The group's slug, e.g. "design"`);
|
|
1465
|
+
var members = z13.array(z13.string().min(3).max(254)).max(GROUP_MEMBERS_MAX);
|
|
1466
|
+
var groupName = z13.string().min(1).max(GROUP_NAME_MAX);
|
|
1467
|
+
var commentPath = z13.string().max(1024);
|
|
1468
|
+
var commentId = z13.string().min(1).max(100);
|
|
1469
|
+
var encoding = z13.enum(["utf8", "base64"]).optional().describe('How `content` is encoded: "utf8" (default) for text, "base64" for binary files');
|
|
1470
|
+
var spa = z13.boolean().optional().describe(
|
|
865
1471
|
"Single-page app: serve index.html for unknown paths (client-side routing). Omit to keep the current setting"
|
|
866
1472
|
);
|
|
867
1473
|
function mcpToolsFor(w) {
|
|
868
1474
|
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}.`;
|
|
869
|
-
const VISIBILITY =
|
|
870
|
-
const isPublic =
|
|
871
|
-
`true \u2192 anyone can view; false \u2192
|
|
1475
|
+
const VISIBILITY = ACCESS_NOTE(w);
|
|
1476
|
+
const isPublic = z13.boolean().optional().describe(
|
|
1477
|
+
"Prefer `access` (a new site) or fling_set_access (an existing one). true \u2192 anyone can view; false \u2192 private. Omit to keep the current setting"
|
|
1478
|
+
);
|
|
1479
|
+
const access2 = z13.enum(ACCESS_LEVELS).optional().describe(
|
|
1480
|
+
`Who can open a new site: only_me, members (${w.privateAudience}), groups, people or public. Ask the user before the first publish; omit for the default (${defaultAccessPhrase(w)}). Ignored when the site already exists`
|
|
872
1481
|
);
|
|
1482
|
+
const accessEmails = emails.optional().describe('With access "people" (or "groups"): the addresses that may open it');
|
|
1483
|
+
const accessGroups = groupSlugs.optional().describe('With access "groups" (or "people"): the groups, by slug, whose members may open it');
|
|
873
1484
|
return {
|
|
874
1485
|
whoami: tool({
|
|
875
1486
|
name: "fling_whoami",
|
|
@@ -883,9 +1494,9 @@ function mcpToolsFor(w) {
|
|
|
883
1494
|
title: "List sites",
|
|
884
1495
|
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.",
|
|
885
1496
|
inputSchema: {
|
|
886
|
-
archived:
|
|
887
|
-
shared:
|
|
888
|
-
sort:
|
|
1497
|
+
archived: z13.boolean().optional().describe("List archived sites instead of live ones"),
|
|
1498
|
+
shared: z13.boolean().optional().describe("List the sites others have shared with the user instead of the user's own"),
|
|
1499
|
+
sort: z13.enum(SITE_SORTS).optional().describe(
|
|
889
1500
|
"Order: updated (last updated, newest first; the default), created (newest first) or name (A\u2013Z)"
|
|
890
1501
|
)
|
|
891
1502
|
},
|
|
@@ -904,7 +1515,20 @@ function mcpToolsFor(w) {
|
|
|
904
1515
|
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).",
|
|
905
1516
|
inputSchema: {
|
|
906
1517
|
name: siteName,
|
|
907
|
-
versionId:
|
|
1518
|
+
versionId: z13.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served")
|
|
1519
|
+
},
|
|
1520
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1521
|
+
}),
|
|
1522
|
+
readFiles: tool({
|
|
1523
|
+
name: "fling_read_files",
|
|
1524
|
+
title: "Read a site's files",
|
|
1525
|
+
description: `Read the files of a site's current version (or \`versionId\`), with their content inline (text as utf8, anything else base64), to change some of them and publish again. Returns up to ${MCP_READ_MAX_BYTES / 1024 / 1024} MB a call, in path order: when there is more, pass the returned \`cursor\` for the next files, or name the files you need in \`paths\` (fling_files lists them). It returns the \`versionId\` to pass to fling_publish as \`base\`. Anyone who can open the site may read it; publishing needs an owner or publisher. ${PULL_NOTE}`,
|
|
1526
|
+
inputSchema: {
|
|
1527
|
+
name: siteName,
|
|
1528
|
+
paths: z13.array(z13.string().min(1).max(1024)).max(MAX_FILES).optional().describe('Only these files (site paths, e.g. "index.html"); omit for all of them'),
|
|
1529
|
+
path: z13.string().min(1).max(1024).optional().describe('Only the files under this folder of the site, e.g. "30-10-26"'),
|
|
1530
|
+
cursor: z13.string().max(20).optional().describe("From the previous call, for the next files"),
|
|
1531
|
+
versionId: z13.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served")
|
|
908
1532
|
},
|
|
909
1533
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
910
1534
|
}),
|
|
@@ -912,23 +1536,31 @@ function mcpToolsFor(w) {
|
|
|
912
1536
|
name: "fling_check_name",
|
|
913
1537
|
title: "Check a site name",
|
|
914
1538
|
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,
|
|
915
|
-
inputSchema: { name:
|
|
1539
|
+
inputSchema: { name: z13.string().min(1).max(200).describe("Site name to check") },
|
|
916
1540
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
917
1541
|
}),
|
|
918
1542
|
publish: tool({
|
|
919
1543
|
name: "fling_publish",
|
|
920
1544
|
title: "Publish a site",
|
|
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.`,
|
|
1545
|
+
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), unless \`path\` or \`base\` is given; publishing identical content again is a no-op. ${PATH_PUBLISH_NOTE} ${PULL_NOTE} ${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.`,
|
|
922
1546
|
inputSchema: {
|
|
923
1547
|
name: siteName,
|
|
924
|
-
files:
|
|
925
|
-
|
|
1548
|
+
files: z13.array(
|
|
1549
|
+
z13.object({
|
|
926
1550
|
path: sitePath,
|
|
927
|
-
content:
|
|
1551
|
+
content: z13.string().describe("The file content, encoded as `encoding` says"),
|
|
928
1552
|
encoding
|
|
929
1553
|
})
|
|
930
|
-
).
|
|
1554
|
+
).max(MAX_FILES).describe(
|
|
1555
|
+
"Every file of the site (with `path`: of that folder; with `base`: only those changed or added)"
|
|
1556
|
+
),
|
|
1557
|
+
path: publishPath,
|
|
1558
|
+
base,
|
|
1559
|
+
remove,
|
|
931
1560
|
spa,
|
|
1561
|
+
access: access2,
|
|
1562
|
+
emails: accessEmails,
|
|
1563
|
+
groups: accessGroups,
|
|
932
1564
|
public: isPublic,
|
|
933
1565
|
listHtml
|
|
934
1566
|
},
|
|
@@ -937,8 +1569,18 @@ function mcpToolsFor(w) {
|
|
|
937
1569
|
uploadBegin: tool({
|
|
938
1570
|
name: "fling_upload_begin",
|
|
939
1571
|
title: "Start a chunked upload",
|
|
940
|
-
description: `Start publishing a site too big for one fling_publish call (over ${MCP_PUBLISH_MAX_BYTES / 1024 / 1024} MB, or large binary files). Returns an \`uploadId\`; then send each file with fling_upload_file and finish with fling_upload_finish within ${MCP_UPLOAD_TTL_SECONDS / 60} minutes. ${INLINE_ONLY} ${NAME_REQUIRED} ${VISIBILITY}
|
|
941
|
-
inputSchema: {
|
|
1572
|
+
description: `Start publishing a site too big for one fling_publish call (over ${MCP_PUBLISH_MAX_BYTES / 1024 / 1024} MB, or large binary files). Returns an \`uploadId\`; then send each file with fling_upload_file and finish with fling_upload_finish within ${MCP_UPLOAD_TTL_SECONDS / 60} minutes. ${INLINE_ONLY} ${NAME_REQUIRED} ${VISIBILITY} ${PATH_PUBLISH_NOTE} With \`base\` (from fling_read_files), send only the files you changed or added, and \`remove\` the ones to delete: every other file of the site is kept.`,
|
|
1573
|
+
inputSchema: {
|
|
1574
|
+
name: siteName,
|
|
1575
|
+
path: publishPath,
|
|
1576
|
+
base,
|
|
1577
|
+
remove,
|
|
1578
|
+
spa,
|
|
1579
|
+
access: access2,
|
|
1580
|
+
emails: accessEmails,
|
|
1581
|
+
groups: accessGroups,
|
|
1582
|
+
public: isPublic
|
|
1583
|
+
},
|
|
942
1584
|
annotations: { destructiveHint: false, openWorldHint: false }
|
|
943
1585
|
}),
|
|
944
1586
|
uploadFile: tool({
|
|
@@ -946,20 +1588,20 @@ function mcpToolsFor(w) {
|
|
|
946
1588
|
title: "Upload a file (chunked)",
|
|
947
1589
|
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}`,
|
|
948
1590
|
inputSchema: {
|
|
949
|
-
uploadId:
|
|
1591
|
+
uploadId: z13.string().min(1).max(100).describe("From fling_upload_begin"),
|
|
950
1592
|
path: sitePath,
|
|
951
|
-
content:
|
|
1593
|
+
content: z13.string().max(MCP_UPLOAD_CHUNK_MAX_CHARS).describe("The file content (or the next part of it, with `append`)"),
|
|
952
1594
|
encoding,
|
|
953
|
-
append:
|
|
1595
|
+
append: z13.boolean().optional().describe("Append this content to the file sent earlier with the same path")
|
|
954
1596
|
},
|
|
955
1597
|
annotations: { destructiveHint: false, openWorldHint: false }
|
|
956
1598
|
}),
|
|
957
1599
|
uploadFinish: tool({
|
|
958
1600
|
name: "fling_upload_finish",
|
|
959
1601
|
title: "Finish a chunked upload",
|
|
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\`.`,
|
|
1602
|
+
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 (with \`path\` or \`base\`, plus the rest of the site). ${LIST_HTML} The upload stays open, so just call fling_upload_finish again with \`listHtml\`. The result includes \`warnings\`.`,
|
|
961
1603
|
inputSchema: {
|
|
962
|
-
uploadId:
|
|
1604
|
+
uploadId: z13.string().min(1).max(100).describe("From fling_upload_begin"),
|
|
963
1605
|
listHtml
|
|
964
1606
|
},
|
|
965
1607
|
annotations: { destructiveHint: false, openWorldHint: true }
|
|
@@ -970,22 +1612,34 @@ function mcpToolsFor(w) {
|
|
|
970
1612
|
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.",
|
|
971
1613
|
inputSchema: {
|
|
972
1614
|
name: siteName,
|
|
973
|
-
refresh:
|
|
974
|
-
revoke:
|
|
1615
|
+
refresh: z13.boolean().optional().describe("Rotate the link"),
|
|
1616
|
+
revoke: z13.boolean().optional().describe("Remove the link")
|
|
975
1617
|
},
|
|
976
1618
|
annotations: { destructiveHint: false, openWorldHint: true }
|
|
977
1619
|
}),
|
|
1620
|
+
setAccess: tool({
|
|
1621
|
+
name: "fling_set_access",
|
|
1622
|
+
title: "Set who can open a site",
|
|
1623
|
+
description: "Set a site's access level: `only_me` (only the user), `members` (" + w.privateAudience + `), \`groups\` and \`people\` (only the user, the groups in \`groups\` by slug and the addresses in \`emails\`; both levels take both, and replace the whole audience), or \`public\` (anyone can open the site; its review page and comments still need sign-in). Only change it when the user asks. ${addressRule(w)} Share links still open the site to anyone who has them. Someone who loses access is shut out within 5 minutes; people and groups added are told by email when the instance sends it.`,
|
|
1624
|
+
inputSchema: {
|
|
1625
|
+
name: siteName,
|
|
1626
|
+
access: z13.enum(ACCESS_LEVELS).describe("only_me, members, groups, people or public"),
|
|
1627
|
+
emails: emails.optional().describe("For groups or people: the addresses that may open it"),
|
|
1628
|
+
groups: groupSlugs.optional().describe("For groups or people: the groups (slugs, fling_groups) whose members may")
|
|
1629
|
+
},
|
|
1630
|
+
annotations: { idempotentHint: true, openWorldHint: true }
|
|
1631
|
+
}),
|
|
978
1632
|
setVisibility: tool({
|
|
979
1633
|
name: "fling_set_visibility",
|
|
980
1634
|
title: "Set visibility",
|
|
981
|
-
description: `Make a site private
|
|
982
|
-
inputSchema: { name: siteName, visibility:
|
|
1635
|
+
description: `Make a site private or public (anyone can open the site; its review page and comments still need sign-in). Making it private gives it back the access it had before (only the user, ${w.privateAudience}, or its audience). fling_set_access sets any level.`,
|
|
1636
|
+
inputSchema: { name: siteName, visibility: z13.enum(["private", "public"]) },
|
|
983
1637
|
annotations: { idempotentHint: true, openWorldHint: true }
|
|
984
1638
|
}),
|
|
985
1639
|
setAudience: tool({
|
|
986
1640
|
name: "fling_set_audience",
|
|
987
1641
|
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.
|
|
1642
|
+
description: `Limit a private site to listed people and groups (its audience), or open it again to ${w.privateAudience}; fling_set_access does the same for whole levels, including only the user. \`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. ${addressRule(w)} 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
1643
|
inputSchema: {
|
|
990
1644
|
name: siteName,
|
|
991
1645
|
add: emails.optional().describe("Addresses to let in"),
|
|
@@ -994,7 +1648,41 @@ function mcpToolsFor(w) {
|
|
|
994
1648
|
addGroups: groupSlugs.optional().describe("Groups (slugs) to let in"),
|
|
995
1649
|
removeGroups: groupSlugs.optional().describe("Groups (slugs) to take out"),
|
|
996
1650
|
setGroups: groupSlugs.optional().describe("Replace the groups with exactly these slugs"),
|
|
997
|
-
clear:
|
|
1651
|
+
clear: z13.boolean().optional().describe("Empty the audience (addresses and groups): every signed-in member sees it")
|
|
1652
|
+
},
|
|
1653
|
+
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1654
|
+
}),
|
|
1655
|
+
setOwners: tool({
|
|
1656
|
+
name: "fling_set_owners",
|
|
1657
|
+
title: "Set a site's owners",
|
|
1658
|
+
description: `Change who owns a site: people and groups who may do everything the user can with it (publish, roll back, change its access and settings, its owners and publishers, archive and delete it). Only owners change it, and only when the user asks. \`add\` and \`remove\` change the addresses and \`set\` replaces them; \`addGroups\`, \`removeGroups\` and \`setGroups\` do the same for groups, by slug (fling_groups). Only members of ${w.name} may be owners, never guests; a site keeps at least one person (address) as an owner, and has at most ${OWNER_EMAILS_MAX} addresses. Naming a group trusts whoever manages it. fling_info shows the owners and publishers.`,
|
|
1659
|
+
inputSchema: {
|
|
1660
|
+
name: siteName,
|
|
1661
|
+
add: roleChange.add.describe("Addresses to make owners"),
|
|
1662
|
+
remove: roleChange.remove.describe("Addresses to take out of the owners"),
|
|
1663
|
+
set: roleChange.set.describe("Replace the owner addresses with exactly these"),
|
|
1664
|
+
addGroups: roleChange.addGroups.describe("Groups (slugs) to make owners"),
|
|
1665
|
+
removeGroups: roleChange.removeGroups.describe("Groups (slugs) to take out"),
|
|
1666
|
+
setGroups: roleChange.setGroups.describe(
|
|
1667
|
+
"Replace the owner groups with exactly these slugs"
|
|
1668
|
+
)
|
|
1669
|
+
},
|
|
1670
|
+
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1671
|
+
}),
|
|
1672
|
+
setPublishers: tool({
|
|
1673
|
+
name: "fling_set_publishers",
|
|
1674
|
+
title: "Set a site's publishers",
|
|
1675
|
+
description: `Change who publishes a site without owning it: people and groups who may deploy new versions and roll back, but not change its access or settings, add owners or delete it. Only the site's owners change it, and only when the user asks. \`add\` and \`remove\` change the addresses and \`set\` replaces them; \`addGroups\`, \`removeGroups\` and \`setGroups\` do the same for groups, by slug (fling_groups). Only members of ${w.name} may publish, never guests. Someone who is also an owner stays an owner. fling_info shows the owners and publishers.`,
|
|
1676
|
+
inputSchema: {
|
|
1677
|
+
name: siteName,
|
|
1678
|
+
add: roleChange.add.describe("Addresses to make publishers"),
|
|
1679
|
+
remove: roleChange.remove.describe("Addresses to take out of the publishers"),
|
|
1680
|
+
set: roleChange.set.describe("Replace the publisher addresses with exactly these"),
|
|
1681
|
+
addGroups: roleChange.addGroups.describe("Groups (slugs) to make publishers"),
|
|
1682
|
+
removeGroups: roleChange.removeGroups.describe("Groups (slugs) to take out"),
|
|
1683
|
+
setGroups: roleChange.setGroups.describe(
|
|
1684
|
+
"Replace the publisher groups with exactly these slugs"
|
|
1685
|
+
)
|
|
998
1686
|
},
|
|
999
1687
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1000
1688
|
}),
|
|
@@ -1003,7 +1691,7 @@ function mcpToolsFor(w) {
|
|
|
1003
1691
|
title: "List groups",
|
|
1004
1692
|
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
1693
|
inputSchema: {
|
|
1006
|
-
mine:
|
|
1694
|
+
mine: z13.boolean().optional().describe("Only the groups the user owns"),
|
|
1007
1695
|
members: members.optional().describe("Only the groups whose members are exactly these addresses")
|
|
1008
1696
|
},
|
|
1009
1697
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
@@ -1011,14 +1699,14 @@ function mcpToolsFor(w) {
|
|
|
1011
1699
|
groupShow: tool({
|
|
1012
1700
|
name: "fling_group_show",
|
|
1013
1701
|
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).",
|
|
1702
|
+
description: "Show a group and its members (each marked when they have not signed in yet, and when they are a guest invited from outside), and whether the user owns it (only the owner can change it).",
|
|
1015
1703
|
inputSchema: { slug: groupSlug },
|
|
1016
1704
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1017
1705
|
}),
|
|
1018
1706
|
groupCreate: tool({
|
|
1019
1707
|
name: "fling_group_create",
|
|
1020
1708
|
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};
|
|
1709
|
+
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}; at most ${GROUP_MEMBERS_MAX} members. ${addressRule(w)}`,
|
|
1022
1710
|
inputSchema: {
|
|
1023
1711
|
slug: groupSlug,
|
|
1024
1712
|
name: groupName.describe('Its display name, e.g. "Design team"'),
|
|
@@ -1029,7 +1717,7 @@ function mcpToolsFor(w) {
|
|
|
1029
1717
|
groupMembers: tool({
|
|
1030
1718
|
name: "fling_group_members",
|
|
1031
1719
|
title: "Change who is in a group",
|
|
1032
|
-
description:
|
|
1720
|
+
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. ${addressRule(w)}`,
|
|
1033
1721
|
inputSchema: {
|
|
1034
1722
|
slug: groupSlug,
|
|
1035
1723
|
add: members.optional().describe("Addresses to add"),
|
|
@@ -1050,7 +1738,7 @@ function mcpToolsFor(w) {
|
|
|
1050
1738
|
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
1739
|
inputSchema: {
|
|
1052
1740
|
slug: groupSlug,
|
|
1053
|
-
confirm:
|
|
1741
|
+
confirm: z13.literal(true).describe("Must be true: the user has confirmed the deletion")
|
|
1054
1742
|
},
|
|
1055
1743
|
annotations: { destructiveHint: true, openWorldHint: false }
|
|
1056
1744
|
}),
|
|
@@ -1060,7 +1748,7 @@ function mcpToolsFor(w) {
|
|
|
1060
1748
|
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.",
|
|
1061
1749
|
inputSchema: {
|
|
1062
1750
|
name: siteName,
|
|
1063
|
-
unarchive:
|
|
1751
|
+
unarchive: z13.boolean().optional().describe("Restore an archived site")
|
|
1064
1752
|
},
|
|
1065
1753
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1066
1754
|
}),
|
|
@@ -1070,7 +1758,7 @@ function mcpToolsFor(w) {
|
|
|
1070
1758
|
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.",
|
|
1071
1759
|
inputSchema: {
|
|
1072
1760
|
name: siteName,
|
|
1073
|
-
confirm:
|
|
1761
|
+
confirm: z13.literal(true).describe("Must be true: the user has confirmed the deletion")
|
|
1074
1762
|
},
|
|
1075
1763
|
annotations: { destructiveHint: true, openWorldHint: false }
|
|
1076
1764
|
}),
|
|
@@ -1081,7 +1769,7 @@ function mcpToolsFor(w) {
|
|
|
1081
1769
|
inputSchema: {
|
|
1082
1770
|
name: siteName,
|
|
1083
1771
|
path: commentPath.optional().describe('Only this page, e.g. "/" or "/docs/"'),
|
|
1084
|
-
includeResolved:
|
|
1772
|
+
includeResolved: z13.boolean().optional().describe("Include resolved threads")
|
|
1085
1773
|
},
|
|
1086
1774
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1087
1775
|
}),
|
|
@@ -1091,7 +1779,7 @@ function mcpToolsFor(w) {
|
|
|
1091
1779
|
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.`,
|
|
1092
1780
|
inputSchema: {
|
|
1093
1781
|
name: siteName,
|
|
1094
|
-
body:
|
|
1782
|
+
body: z13.string().min(1).max(COMMENT_MAX_CHARS).describe("The comment (plain text)"),
|
|
1095
1783
|
path: commentPath.optional().describe('The page it is about (default "/"); ignored for a reply'),
|
|
1096
1784
|
replyTo: commentId.optional().describe("Reply to this thread instead of starting one")
|
|
1097
1785
|
},
|
|
@@ -1104,7 +1792,7 @@ function mcpToolsFor(w) {
|
|
|
1104
1792
|
inputSchema: {
|
|
1105
1793
|
name: siteName,
|
|
1106
1794
|
id: commentId.describe("The thread id"),
|
|
1107
|
-
reopen:
|
|
1795
|
+
reopen: z13.boolean().optional().describe("Reopen a resolved thread")
|
|
1108
1796
|
},
|
|
1109
1797
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1110
1798
|
}),
|
|
@@ -1115,19 +1803,39 @@ function mcpToolsFor(w) {
|
|
|
1115
1803
|
inputSchema: { name: siteName, id: commentId.describe("The comment id") },
|
|
1116
1804
|
annotations: { destructiveHint: true, openWorldHint: false }
|
|
1117
1805
|
}),
|
|
1806
|
+
notifications: tool({
|
|
1807
|
+
name: "fling_notifications",
|
|
1808
|
+
title: "Notification settings",
|
|
1809
|
+
description: `Show what ${w.name} tells the user about and how: each event (a site shared with them, added to a group, a comment on their site, a reply in their thread\u2026) with its key and, per channel (email), whether it is on or off, marking the ones the user chose themselves; and whether everything is muted. Use it before fling_notifications_set, or when the user asks why they get (or do not get) emails.`,
|
|
1810
|
+
inputSchema: {},
|
|
1811
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1812
|
+
}),
|
|
1813
|
+
notificationsSet: tool({
|
|
1814
|
+
name: "fling_notifications_set",
|
|
1815
|
+
title: "Change notification settings",
|
|
1816
|
+
description: `Change what ${w.name} tells the user about: \`muted: true\` stops everything (false turns it back on, with their choices kept), and \`cells\` turns single events on or off per channel, or back to the default (\`value: "default"\`). Event keys come from fling_notifications. Only change what the user asked for. Returns the settings after the change.`,
|
|
1817
|
+
inputSchema: {
|
|
1818
|
+
muted: z13.boolean().optional().describe("true: send nothing at all; false: unmute"),
|
|
1819
|
+
cells: z13.array(CellChangeSchema).max(50).optional().describe(
|
|
1820
|
+
'Events to change: `{ event, channel, value }`, value "on", "off" or "default"'
|
|
1821
|
+
)
|
|
1822
|
+
},
|
|
1823
|
+
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1824
|
+
}),
|
|
1118
1825
|
rollback: tool({
|
|
1119
1826
|
name: "fling_rollback",
|
|
1120
1827
|
title: "Roll back",
|
|
1121
1828
|
description: "Point a site at an earlier version (ids from fling_info). Instant; nothing is uploaded.",
|
|
1122
1829
|
inputSchema: {
|
|
1123
1830
|
name: siteName,
|
|
1124
|
-
versionId:
|
|
1831
|
+
versionId: z13.string().min(1).max(100).describe("Version id to serve")
|
|
1125
1832
|
},
|
|
1126
1833
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
1127
1834
|
})
|
|
1128
1835
|
};
|
|
1129
1836
|
}
|
|
1130
1837
|
var mcpTools = mcpToolsFor(mcpWording());
|
|
1838
|
+
var NOTIFICATIONS_NOTE = (w) => `${w.name} emails people when a site is shared with them, they are made an owner or publisher of one, they are added to a group, or someone comments on their site or replies in their thread. fling_notifications shows what the user hears about; change it with fling_notifications_set only when they ask.`;
|
|
1131
1839
|
var COMMENTS_NOTE = (w) => `People review sites at ${w.reviewPattern} and leave comments on its pages. Publishing returns this review link (\`reviewUrl\`): it is the one to share when the user wants feedback; reviewers sign in to use it. fling_comments_list shows the comments; after acting on feedback, resolve the threads with fling_comment_resolve. Only post (fling_comment_add) or delete (fling_comment_delete) comments when the user asks.`;
|
|
1132
1840
|
var shareForFeedback = (reviewPage2, instanceName) => `Share for feedback: ${reviewPage2} (reviewers sign in to ${instanceName} to see the site and comment).`;
|
|
1133
1841
|
var MCP_FILES_LISTED = 200;
|
|
@@ -1174,7 +1882,7 @@ function describeComments(list, opts = {}) {
|
|
|
1174
1882
|
var READY_TIMEOUT_MS = 15e3;
|
|
1175
1883
|
var NOT_READY_NOTE = "it can take a few more seconds to appear everywhere; reload if you see a 404";
|
|
1176
1884
|
var isReachableStatus = (status) => status >= 200 && status < 400 || status === 401 || status === 403;
|
|
1177
|
-
var defaultSleep = (ms) => new Promise((
|
|
1885
|
+
var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1178
1886
|
async function waitUntilReachable(url, opts = {}) {
|
|
1179
1887
|
const timeoutMs = opts.timeoutMs ?? READY_TIMEOUT_MS;
|
|
1180
1888
|
const doFetch = opts.fetch ?? ((u, init) => fetch(u, init));
|
|
@@ -1208,109 +1916,123 @@ async function waitUntilReachable(url, opts = {}) {
|
|
|
1208
1916
|
}
|
|
1209
1917
|
}
|
|
1210
1918
|
function probeUrl(siteUrl2, paths) {
|
|
1211
|
-
const
|
|
1212
|
-
if (paths.length === 0 || paths.includes("index.html")) return
|
|
1919
|
+
const base2 = siteUrl2.endsWith("/") ? siteUrl2 : `${siteUrl2}/`;
|
|
1920
|
+
if (paths.length === 0 || paths.includes("index.html")) return base2;
|
|
1213
1921
|
const pick = paths.find((p) => /\.html?$/i.test(p)) ?? paths[0];
|
|
1214
|
-
return
|
|
1922
|
+
return base2 + pick.split("/").map(encodeURIComponent).join("/");
|
|
1215
1923
|
}
|
|
1216
1924
|
function readinessWaitDisabled(env) {
|
|
1217
1925
|
return env.FLING_NO_WAIT === "1" || env.VITEST === "true" || env.NODE_ENV === "test";
|
|
1218
1926
|
}
|
|
1219
1927
|
|
|
1220
1928
|
// ../../packages/shared/src/review-bridge.ts
|
|
1221
|
-
import { z as
|
|
1222
|
-
var tag = { fling:
|
|
1223
|
-
var path =
|
|
1224
|
-
var hash =
|
|
1225
|
-
var BridgePinSchema =
|
|
1226
|
-
id:
|
|
1227
|
-
n:
|
|
1929
|
+
import { z as z14 } from "zod";
|
|
1930
|
+
var tag = { fling: z14.literal("review") };
|
|
1931
|
+
var path = z14.string().max(COMMENT_PATH_MAX);
|
|
1932
|
+
var hash = z14.string().max(COMMENT_PATH_MAX);
|
|
1933
|
+
var BridgePinSchema = z14.object({
|
|
1934
|
+
id: z14.string(),
|
|
1935
|
+
n: z14.number().int().min(1),
|
|
1228
1936
|
anchor: CommentAnchorSchema,
|
|
1229
|
-
selected:
|
|
1230
|
-
resolved:
|
|
1937
|
+
selected: z14.boolean(),
|
|
1938
|
+
resolved: z14.boolean()
|
|
1231
1939
|
});
|
|
1232
|
-
var BridgeCursorSchema =
|
|
1233
|
-
id:
|
|
1234
|
-
name:
|
|
1235
|
-
colour:
|
|
1940
|
+
var BridgeCursorSchema = z14.object({
|
|
1941
|
+
id: z14.string(),
|
|
1942
|
+
name: z14.string().max(100),
|
|
1943
|
+
colour: z14.string().max(32),
|
|
1236
1944
|
anchor: CommentAnchorSchema
|
|
1237
1945
|
});
|
|
1238
|
-
var ShellMessageSchema =
|
|
1239
|
-
|
|
1946
|
+
var ShellMessageSchema = z14.discriminatedUnion("type", [
|
|
1947
|
+
z14.object({ ...tag, type: z14.literal("ready") }),
|
|
1240
1948
|
/** the frame is on `path` (normalised, see normalizeCommentPath) with fragment `hash` ('' or '#…') */
|
|
1241
|
-
|
|
1949
|
+
z14.object({ ...tag, type: z14.literal("location"), path, hash, title: z14.string().max(300) }),
|
|
1242
1950
|
/** a pin placed while placing: a click (a spot) or a drag (a box) */
|
|
1243
|
-
|
|
1951
|
+
z14.object({ ...tag, type: z14.literal("pinPlaced"), anchor: CommentAnchorSchema }),
|
|
1244
1952
|
/** the reviewer selected text in the page: an anchor on it, with its quote */
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1953
|
+
z14.object({ ...tag, type: z14.literal("textSelected"), anchor: CommentAnchorSchema }),
|
|
1954
|
+
z14.object({ ...tag, type: z14.literal("pinCancelled") }),
|
|
1955
|
+
z14.object({ ...tag, type: z14.literal("pinClicked"), id: z14.string() }),
|
|
1248
1956
|
/** this reviewer's pointer on the page; null when it left the frame */
|
|
1249
|
-
|
|
1957
|
+
z14.object({ ...tag, type: z14.literal("cursor"), anchor: CommentAnchorSchema.nullable() }),
|
|
1250
1958
|
/** a link or form to another origin, stopped: the frame stays on the site */
|
|
1251
|
-
|
|
1252
|
-
|
|
1959
|
+
z14.object({ ...tag, type: z14.literal("blocked"), href: z14.string().max(2048) }),
|
|
1960
|
+
z14.object({ ...tag, type: z14.literal("escape") }),
|
|
1961
|
+
/** preview mode: the thumbnail the page asked for, a PNG of the frame's viewport of `versionId` */
|
|
1962
|
+
z14.object({
|
|
1963
|
+
...tag,
|
|
1964
|
+
type: z14.literal("captured"),
|
|
1965
|
+
png: z14.instanceof(Blob),
|
|
1966
|
+
versionId: z14.string().max(100)
|
|
1967
|
+
}),
|
|
1968
|
+
/** preview mode: no thumbnail could be drawn (CAPTURE_STALE: the host serves another version) */
|
|
1969
|
+
z14.object({ ...tag, type: z14.literal("captureFailed"), reason: z14.string().max(300) })
|
|
1253
1970
|
]);
|
|
1254
|
-
var PageMessageSchema =
|
|
1971
|
+
var PageMessageSchema = z14.discriminatedUnion("type", [
|
|
1255
1972
|
/** load `path` (site-relative, may carry `?query` and `#hash`) in the frame */
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1973
|
+
z14.object({ ...tag, type: z14.literal("navigate"), path: z14.string().max(COMMENT_PATH_MAX * 2) }),
|
|
1974
|
+
z14.object({ ...tag, type: z14.literal("reload") }),
|
|
1975
|
+
z14.object({ ...tag, type: z14.literal("placing"), on: z14.boolean() }),
|
|
1259
1976
|
/** the pins on the frame's current page */
|
|
1260
|
-
|
|
1977
|
+
z14.object({ ...tag, type: z14.literal("pins"), pins: z14.array(BridgePinSchema) }),
|
|
1261
1978
|
/** scroll the pin into view and draw attention to it */
|
|
1262
|
-
|
|
1263
|
-
|
|
1979
|
+
z14.object({ ...tag, type: z14.literal("focusPin"), id: z14.string() }),
|
|
1980
|
+
z14.object({ ...tag, type: z14.literal("cursors"), cursors: z14.array(BridgeCursorSchema) }),
|
|
1981
|
+
/**
|
|
1982
|
+
* preview mode: once the page has loaded and settled, draw it as a thumbnail (`captured`), only
|
|
1983
|
+
* if the sites host serves `versionId` (the site's current version, as the API says)
|
|
1984
|
+
*/
|
|
1985
|
+
z14.object({ ...tag, type: z14.literal("capture"), versionId: z14.string().max(100) })
|
|
1264
1986
|
]);
|
|
1265
1987
|
|
|
1266
1988
|
// ../../packages/shared/src/review-live.ts
|
|
1267
|
-
import { z as
|
|
1989
|
+
import { z as z15 } from "zod";
|
|
1268
1990
|
var LIVE_PICTURE_MAX = 1024;
|
|
1269
|
-
var LivePeerSchema =
|
|
1270
|
-
id:
|
|
1271
|
-
name:
|
|
1272
|
-
colour:
|
|
1991
|
+
var LivePeerSchema = z15.object({
|
|
1992
|
+
id: z15.string(),
|
|
1993
|
+
name: z15.string().max(100),
|
|
1994
|
+
colour: z15.string().max(32),
|
|
1273
1995
|
/** their profile picture (https), when sign-in gave one; else clients show initials */
|
|
1274
|
-
picture:
|
|
1996
|
+
picture: z15.string().max(LIVE_PICTURE_MAX).optional()
|
|
1275
1997
|
});
|
|
1276
|
-
var LiveTicketSchema =
|
|
1277
|
-
url:
|
|
1278
|
-
ticket:
|
|
1998
|
+
var LiveTicketSchema = z15.object({
|
|
1999
|
+
url: z15.string(),
|
|
2000
|
+
ticket: z15.string(),
|
|
1279
2001
|
/** seconds the ticket is good for: connect before then */
|
|
1280
|
-
expiresIn:
|
|
2002
|
+
expiresIn: z15.number().int()
|
|
1281
2003
|
});
|
|
1282
|
-
var path2 =
|
|
1283
|
-
var hash2 =
|
|
1284
|
-
var LiveClientMessageSchema =
|
|
1285
|
-
|
|
2004
|
+
var path2 = z15.string().max(COMMENT_PATH_MAX);
|
|
2005
|
+
var hash2 = z15.string().max(COMMENT_PATH_MAX);
|
|
2006
|
+
var LiveClientMessageSchema = z15.discriminatedUnion("type", [
|
|
2007
|
+
z15.object({ type: z15.literal("hello") }),
|
|
1286
2008
|
/** where this peer's pointer is; `anchor` null when it left the page */
|
|
1287
|
-
|
|
1288
|
-
type:
|
|
2009
|
+
z15.object({
|
|
2010
|
+
type: z15.literal("cursor"),
|
|
1289
2011
|
path: path2,
|
|
1290
2012
|
hash: hash2,
|
|
1291
2013
|
anchor: CommentAnchorSchema.nullable()
|
|
1292
2014
|
}),
|
|
1293
|
-
|
|
2015
|
+
z15.object({ type: z15.literal("ping") })
|
|
1294
2016
|
]);
|
|
1295
|
-
var LiveServerMessageSchema =
|
|
2017
|
+
var LiveServerMessageSchema = z15.discriminatedUnion("type", [
|
|
1296
2018
|
/** the answer to `hello`: who this connection is, and who else is in the room */
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
type:
|
|
1302
|
-
peerId:
|
|
2019
|
+
z15.object({ type: z15.literal("welcome"), you: LivePeerSchema, peers: z15.array(LivePeerSchema) }),
|
|
2020
|
+
z15.object({ type: z15.literal("join"), peer: LivePeerSchema }),
|
|
2021
|
+
z15.object({ type: z15.literal("leave"), peerId: z15.string() }),
|
|
2022
|
+
z15.object({
|
|
2023
|
+
type: z15.literal("cursor"),
|
|
2024
|
+
peerId: z15.string(),
|
|
1303
2025
|
path: path2,
|
|
1304
2026
|
hash: hash2,
|
|
1305
2027
|
anchor: CommentAnchorSchema.nullable()
|
|
1306
2028
|
}),
|
|
1307
2029
|
/** the site's comments changed: refetch them */
|
|
1308
|
-
|
|
1309
|
-
|
|
2030
|
+
z15.object({ type: z15.literal("comments") }),
|
|
2031
|
+
z15.object({ type: z15.literal("pong") })
|
|
1310
2032
|
]);
|
|
1311
2033
|
|
|
1312
2034
|
// src/cli.ts
|
|
1313
|
-
import
|
|
2035
|
+
import chalk13 from "chalk";
|
|
1314
2036
|
import { Command, Option } from "commander";
|
|
1315
2037
|
|
|
1316
2038
|
// src/commands/auth.ts
|
|
@@ -1360,7 +2082,7 @@ function exitCodeFor(error) {
|
|
|
1360
2082
|
// package.json
|
|
1361
2083
|
var package_default = {
|
|
1362
2084
|
name: "@nearform/fling",
|
|
1363
|
-
version: "0.
|
|
2085
|
+
version: "0.7.0",
|
|
1364
2086
|
license: "MIT",
|
|
1365
2087
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
1366
2088
|
type: "module",
|
|
@@ -1369,9 +2091,7 @@ var package_default = {
|
|
|
1369
2091
|
},
|
|
1370
2092
|
files: [
|
|
1371
2093
|
"dist/cli.js",
|
|
1372
|
-
"
|
|
1373
|
-
"README.md",
|
|
1374
|
-
"CHANGELOG.md"
|
|
2094
|
+
"README.md"
|
|
1375
2095
|
],
|
|
1376
2096
|
engines: {
|
|
1377
2097
|
node: ">=22"
|
|
@@ -1403,6 +2123,7 @@ var package_default = {
|
|
|
1403
2123
|
zod: "^4.6.7"
|
|
1404
2124
|
},
|
|
1405
2125
|
devDependencies: {
|
|
2126
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
1406
2127
|
"@fling/shared": "workspace:*",
|
|
1407
2128
|
"@types/node": "^24.0.0",
|
|
1408
2129
|
esbuild: "^0.25.12",
|
|
@@ -1543,7 +2264,7 @@ function errorFromResponse(status, body, method, path3) {
|
|
|
1543
2264
|
code,
|
|
1544
2265
|
body,
|
|
1545
2266
|
message: envelope.data.error.message,
|
|
1546
|
-
hint: code ?
|
|
2267
|
+
hint: code ? ERROR_HINTS[code] : void 0
|
|
1547
2268
|
});
|
|
1548
2269
|
const extra = body.error;
|
|
1549
2270
|
if (Array.isArray(extra?.findings)) Object.assign(error, { findings: extra.findings });
|
|
@@ -1557,6 +2278,10 @@ var NEVER_PUBLISHED_HINTS = {
|
|
|
1557
2278
|
secret_file: "Move the secret files out of the folder, or add them to .flingignore if the site does not need them.",
|
|
1558
2279
|
dotfile: `Dotfiles are never flung. Update the CLI (\`npm i -g ${PACKAGE_NAME}@latest\`), which skips them.`
|
|
1559
2280
|
};
|
|
2281
|
+
var ERROR_HINTS = {
|
|
2282
|
+
...NEVER_PUBLISHED_HINTS,
|
|
2283
|
+
too_many_invites: "Nothing changed. Addresses from outside are invited as reviewers, a few a day per person: add them tomorrow, or ask an admin to invite them."
|
|
2284
|
+
};
|
|
1560
2285
|
function describeCause(cause) {
|
|
1561
2286
|
if (cause instanceof Error) {
|
|
1562
2287
|
const inner = cause.cause;
|
|
@@ -1571,7 +2296,7 @@ function describeCause(cause) {
|
|
|
1571
2296
|
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1572
2297
|
import { homedir } from "node:os";
|
|
1573
2298
|
import { join } from "node:path";
|
|
1574
|
-
import { z as
|
|
2299
|
+
import { z as z16 } from "zod";
|
|
1575
2300
|
var CONFIG_DIR_NAME = ".fling";
|
|
1576
2301
|
var CONFIG_FILE_NAME = "config.json";
|
|
1577
2302
|
var ENV = {
|
|
@@ -1584,14 +2309,14 @@ var ENV = {
|
|
|
1584
2309
|
/** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
|
|
1585
2310
|
addServer: "FLING_ADD_SERVER"
|
|
1586
2311
|
};
|
|
1587
|
-
var serverEntrySchema =
|
|
2312
|
+
var serverEntrySchema = z16.object({
|
|
1588
2313
|
/** What the server said about itself at login. A copy this CLI can't read is just refetched. */
|
|
1589
2314
|
instance: InstanceMetadataSchema.optional().catch(void 0),
|
|
1590
|
-
apiKey:
|
|
1591
|
-
user:
|
|
1592
|
-
email:
|
|
1593
|
-
name:
|
|
1594
|
-
userSlug:
|
|
2315
|
+
apiKey: z16.string().min(1).optional().catch(void 0),
|
|
2316
|
+
user: z16.object({
|
|
2317
|
+
email: z16.string().optional(),
|
|
2318
|
+
name: z16.string().optional(),
|
|
2319
|
+
userSlug: z16.string().optional()
|
|
1595
2320
|
}).optional().catch(void 0)
|
|
1596
2321
|
});
|
|
1597
2322
|
function systemLocateOptions(env = process.env) {
|
|
@@ -1809,6 +2534,18 @@ async function loadInstance(store, config, fetch2) {
|
|
|
1809
2534
|
return null;
|
|
1810
2535
|
}
|
|
1811
2536
|
}
|
|
2537
|
+
async function loadInstanceWith(store, config, fetch2, capability) {
|
|
2538
|
+
const stored = await loadInstance(store, config, fetch2);
|
|
2539
|
+
if (stored?.capabilities[capability] === true || !config.serverUrl) return stored;
|
|
2540
|
+
try {
|
|
2541
|
+
const instance = await fetchInstance(config.serverUrl, fetch2);
|
|
2542
|
+
if (config.remembered)
|
|
2543
|
+
await store.putServer(config.serverUrl, { instance }, { makeCurrent: false });
|
|
2544
|
+
return instance;
|
|
2545
|
+
} catch {
|
|
2546
|
+
return stored;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
1812
2549
|
|
|
1813
2550
|
// src/auth.ts
|
|
1814
2551
|
var CALLBACK_PORT_START = 8765;
|
|
@@ -1878,8 +2615,8 @@ async function startCallbackServer(options = {}) {
|
|
|
1878
2615
|
const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
1879
2616
|
let settle;
|
|
1880
2617
|
let timer;
|
|
1881
|
-
const result = new Promise((
|
|
1882
|
-
settle = { resolve:
|
|
2618
|
+
const result = new Promise((resolve6, reject) => {
|
|
2619
|
+
settle = { resolve: resolve6, reject };
|
|
1883
2620
|
}).finally(() => clearTimeout(timer));
|
|
1884
2621
|
result.catch(() => {
|
|
1885
2622
|
});
|
|
@@ -1940,7 +2677,7 @@ ${close ? "<script>setTimeout(function(){window.close()},800)</script>" : ""}
|
|
|
1940
2677
|
res.end(html);
|
|
1941
2678
|
}
|
|
1942
2679
|
function listenOnFreePort(server) {
|
|
1943
|
-
return new Promise((
|
|
2680
|
+
return new Promise((resolve6, reject) => {
|
|
1944
2681
|
let port = CALLBACK_PORT_START;
|
|
1945
2682
|
const tryNext = () => {
|
|
1946
2683
|
if (port > CALLBACK_PORT_END) {
|
|
@@ -1962,7 +2699,7 @@ function listenOnFreePort(server) {
|
|
|
1962
2699
|
});
|
|
1963
2700
|
server.listen(port, "127.0.0.1", () => {
|
|
1964
2701
|
server.removeAllListeners("error");
|
|
1965
|
-
|
|
2702
|
+
resolve6(port);
|
|
1966
2703
|
});
|
|
1967
2704
|
};
|
|
1968
2705
|
tryNext();
|
|
@@ -2057,7 +2794,7 @@ function logo(options = {}) {
|
|
|
2057
2794
|
const tagline = options.tagline ?? "throw static files at the web, get a link back";
|
|
2058
2795
|
if (columns < LOGO_WIDTH + 2) return `${gradient("\u25B6\u25B6 fling")} ${chalk2.dim(tagline)}`;
|
|
2059
2796
|
const lines = WORDMARK.map(
|
|
2060
|
-
(
|
|
2797
|
+
(row2, i) => chalk2.dim(TRAILS[i] ?? "") + gradient(row2, WORDMARK[0].length)
|
|
2061
2798
|
);
|
|
2062
2799
|
return ["", ...lines, `${" ".repeat(TRAILS[0].length)}${chalk2.dim(tagline)}`, ""].join("\n");
|
|
2063
2800
|
}
|
|
@@ -2125,7 +2862,7 @@ function formatJson(value) {
|
|
|
2125
2862
|
}
|
|
2126
2863
|
function renderTable(rows, columns) {
|
|
2127
2864
|
if (rows.length === 0) return chalk4.dim("(no results)");
|
|
2128
|
-
const cells = rows.map((
|
|
2865
|
+
const cells = rows.map((row2) => columns.map((column) => formatCell(column.value(row2))));
|
|
2129
2866
|
const widths = columns.map(
|
|
2130
2867
|
(column, i) => Math.max(column.header.length, ...cells.map((line) => line[i]?.length ?? 0))
|
|
2131
2868
|
);
|
|
@@ -2209,8 +2946,8 @@ function requireClient(ctx) {
|
|
|
2209
2946
|
}
|
|
2210
2947
|
|
|
2211
2948
|
// src/sites.ts
|
|
2212
|
-
import { z as
|
|
2213
|
-
var ShareResponseSchema2 =
|
|
2949
|
+
import { z as z17 } from "zod";
|
|
2950
|
+
var ShareResponseSchema2 = z17.looseObject({ shareUrl: z17.string() });
|
|
2214
2951
|
var sitePath2 = (name) => `/api/sites/${encodeURIComponent(name)}`;
|
|
2215
2952
|
function whoami(client) {
|
|
2216
2953
|
return client.request("GET", "/api/me", { schema: MeSchema });
|
|
@@ -2223,11 +2960,11 @@ function checkName(client, name) {
|
|
|
2223
2960
|
function sitesBaseOf(instance) {
|
|
2224
2961
|
return instance ? { protocol: `${instance.siteScheme}:`, domain: instance.sitesDomain } : null;
|
|
2225
2962
|
}
|
|
2226
|
-
function siteUrlFor(name,
|
|
2227
|
-
return siteUrl(name,
|
|
2963
|
+
function siteUrlFor(name, base2) {
|
|
2964
|
+
return siteUrl(name, base2.domain, base2.protocol);
|
|
2228
2965
|
}
|
|
2229
|
-
function sitesPattern(
|
|
2230
|
-
return
|
|
2966
|
+
function sitesPattern(base2) {
|
|
2967
|
+
return base2 ? siteUrlFor("<name>", base2) : "<name>.<sites domain>";
|
|
2231
2968
|
}
|
|
2232
2969
|
async function listSites(client, opts = {}) {
|
|
2233
2970
|
const res = await client.request("GET", "/api/sites", {
|
|
@@ -2269,6 +3006,15 @@ async function setVisibility(client, name, visibility) {
|
|
|
2269
3006
|
const raw = await client.request("PATCH", sitePath2(name), { body: { visibility } });
|
|
2270
3007
|
return siteAfter(client, name, raw);
|
|
2271
3008
|
}
|
|
3009
|
+
async function setAccess(client, name, access2, audience) {
|
|
3010
|
+
return client.request("PATCH", sitePath2(name), {
|
|
3011
|
+
schema: SiteWithAudienceSchema,
|
|
3012
|
+
body: { access: access2, ...audience ? { audience } : {} }
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
function accessLevelsEnabled(instance) {
|
|
3016
|
+
return instance?.capabilities.accessLevels === true;
|
|
3017
|
+
}
|
|
2272
3018
|
async function getAudience(client, name) {
|
|
2273
3019
|
const site = await getSite(client, name);
|
|
2274
3020
|
if (!site.audience) throw noAudiences();
|
|
@@ -2303,6 +3049,37 @@ function audiencesEnabled(instance) {
|
|
|
2303
3049
|
if (!instance) return false;
|
|
2304
3050
|
return instance.capabilities.audiences ?? true;
|
|
2305
3051
|
}
|
|
3052
|
+
function siteRolesEnabled(instance) {
|
|
3053
|
+
return instance?.capabilities.siteRoles === true;
|
|
3054
|
+
}
|
|
3055
|
+
function pathPublishEnabled(instance) {
|
|
3056
|
+
return instance?.capabilities.pathPublish === true;
|
|
3057
|
+
}
|
|
3058
|
+
function pullEnabled(instance) {
|
|
3059
|
+
return instance?.capabilities.pull === true;
|
|
3060
|
+
}
|
|
3061
|
+
var noRoles = () => new CliError("This Fling does not support site owners and publishers", {
|
|
3062
|
+
hint: "Ask its operator to update it."
|
|
3063
|
+
});
|
|
3064
|
+
var withRoles = (site) => {
|
|
3065
|
+
if (!site.owners) throw noRoles();
|
|
3066
|
+
return {
|
|
3067
|
+
...site,
|
|
3068
|
+
owners: site.owners,
|
|
3069
|
+
publishers: site.publishers ?? { emails: [], groups: [] }
|
|
3070
|
+
};
|
|
3071
|
+
};
|
|
3072
|
+
async function getRoles(client, name) {
|
|
3073
|
+
return withRoles(await getSite(client, name));
|
|
3074
|
+
}
|
|
3075
|
+
async function setRoles(client, name, which, change) {
|
|
3076
|
+
const current = await getRoles(client, name);
|
|
3077
|
+
const site = await client.request("PATCH", sitePath2(name), {
|
|
3078
|
+
schema: SiteWithAudienceSchema,
|
|
3079
|
+
body: { [which]: editRoleInput(current[which], change) }
|
|
3080
|
+
});
|
|
3081
|
+
return withRoles(site);
|
|
3082
|
+
}
|
|
2306
3083
|
var groupPath = (slug) => `/api/groups/${encodeURIComponent(slug)}`;
|
|
2307
3084
|
async function listGroups(client, opts = {}) {
|
|
2308
3085
|
const res = await client.request("GET", "/api/groups", {
|
|
@@ -2389,6 +3166,42 @@ function commentsEnabled(instance) {
|
|
|
2389
3166
|
if (!instance) return false;
|
|
2390
3167
|
return instance.capabilities.comments ?? true;
|
|
2391
3168
|
}
|
|
3169
|
+
function notificationsEnabled(instance) {
|
|
3170
|
+
if (!instance) return false;
|
|
3171
|
+
return instance.capabilities.notifications ?? true;
|
|
3172
|
+
}
|
|
3173
|
+
async function notificationsCall(name, call) {
|
|
3174
|
+
try {
|
|
3175
|
+
return await call();
|
|
3176
|
+
} catch (err) {
|
|
3177
|
+
if (err instanceof CliApiError && err.status === 404) {
|
|
3178
|
+
throw new CliError(`${name} has no notifications`);
|
|
3179
|
+
}
|
|
3180
|
+
throw err;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function getNotifications(client, name = "This Fling") {
|
|
3184
|
+
return notificationsCall(
|
|
3185
|
+
name,
|
|
3186
|
+
() => client.request("GET", "/api/me/notifications", { schema: NotificationSettingsSchema })
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
function setNotifications(client, change, name = "This Fling") {
|
|
3190
|
+
return notificationsCall(
|
|
3191
|
+
name,
|
|
3192
|
+
() => client.request("PUT", "/api/me/notifications", {
|
|
3193
|
+
schema: NotificationSettingsSchema,
|
|
3194
|
+
body: preferencesUpdateOf(change)
|
|
3195
|
+
})
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
async function listSentNotifications(client, name = "This Fling") {
|
|
3199
|
+
const res = await notificationsCall(
|
|
3200
|
+
name,
|
|
3201
|
+
() => client.request("GET", "/api/me/notifications/sent", { schema: SentNotificationListSchema })
|
|
3202
|
+
);
|
|
3203
|
+
return res.sent;
|
|
3204
|
+
}
|
|
2392
3205
|
|
|
2393
3206
|
// src/commands/auth.ts
|
|
2394
3207
|
async function loginServer(ctx, url) {
|
|
@@ -2602,8 +3415,8 @@ async function runUncomment(ctx, name, id, opts) {
|
|
|
2602
3415
|
}
|
|
2603
3416
|
|
|
2604
3417
|
// src/commands/deploy.ts
|
|
2605
|
-
import { stat as
|
|
2606
|
-
import { resolve as resolve3 } from "node:path";
|
|
3418
|
+
import { stat as stat5 } from "node:fs/promises";
|
|
3419
|
+
import { relative as relative4, resolve as resolve3 } from "node:path";
|
|
2607
3420
|
import chalk7 from "chalk";
|
|
2608
3421
|
|
|
2609
3422
|
// src/deploy.ts
|
|
@@ -2624,7 +3437,7 @@ import { join as join2, relative, sep } from "node:path";
|
|
|
2624
3437
|
import ignore from "ignore";
|
|
2625
3438
|
var GIT_TIMEOUT_MS = 2e4;
|
|
2626
3439
|
function git(cwd, args, input) {
|
|
2627
|
-
return new Promise((
|
|
3440
|
+
return new Promise((resolve6) => {
|
|
2628
3441
|
try {
|
|
2629
3442
|
const child = execFile(
|
|
2630
3443
|
"git",
|
|
@@ -2632,14 +3445,14 @@ function git(cwd, args, input) {
|
|
|
2632
3445
|
{ timeout: GIT_TIMEOUT_MS, maxBuffer: 256 * 1024 * 1024, encoding: "utf8" },
|
|
2633
3446
|
(error, stdout) => {
|
|
2634
3447
|
const code = error?.code;
|
|
2635
|
-
if (error && !(args[0] === "check-ignore" && code === 1))
|
|
2636
|
-
else
|
|
3448
|
+
if (error && !(args[0] === "check-ignore" && code === 1)) resolve6(null);
|
|
3449
|
+
else resolve6(stdout);
|
|
2637
3450
|
}
|
|
2638
3451
|
);
|
|
2639
|
-
child.on("error", () =>
|
|
3452
|
+
child.on("error", () => resolve6(null));
|
|
2640
3453
|
if (input !== void 0) child.stdin?.end(input);
|
|
2641
3454
|
} catch {
|
|
2642
|
-
|
|
3455
|
+
resolve6(null);
|
|
2643
3456
|
}
|
|
2644
3457
|
});
|
|
2645
3458
|
}
|
|
@@ -2680,9 +3493,9 @@ function nestedMode() {
|
|
|
2680
3493
|
return {
|
|
2681
3494
|
mode: "nested",
|
|
2682
3495
|
ignores(rel, isDir) {
|
|
2683
|
-
for (const { base, ig } of levels) {
|
|
2684
|
-
if (
|
|
2685
|
-
const sub =
|
|
3496
|
+
for (const { base: base2, ig } of levels) {
|
|
3497
|
+
if (base2 && !rel.startsWith(`${base2}/`)) continue;
|
|
3498
|
+
const sub = base2 ? rel.slice(base2.length + 1) : rel;
|
|
2686
3499
|
if (sub && ig.ignores(isDir ? `${sub}/` : sub)) return true;
|
|
2687
3500
|
}
|
|
2688
3501
|
return false;
|
|
@@ -3084,6 +3897,20 @@ async function nameTakenError(client, name, body) {
|
|
|
3084
3897
|
}
|
|
3085
3898
|
return new NameTakenError(name, suggestions);
|
|
3086
3899
|
}
|
|
3900
|
+
var BaseChangedError = class extends CliError {
|
|
3901
|
+
constructor(siteName2, base2, hint) {
|
|
3902
|
+
super(
|
|
3903
|
+
`${siteName2} has changed since you pulled it (version ${base2}): someone published it since`,
|
|
3904
|
+
{
|
|
3905
|
+
hint: hint ?? "Pull it again into another folder, redo your changes there and deploy that, or deploy with --force to replace what they published."
|
|
3906
|
+
}
|
|
3907
|
+
);
|
|
3908
|
+
this.siteName = siteName2;
|
|
3909
|
+
this.base = base2;
|
|
3910
|
+
this.name = "BaseChangedError";
|
|
3911
|
+
}
|
|
3912
|
+
code = "base_changed";
|
|
3913
|
+
};
|
|
3087
3914
|
var UPLOAD_CONCURRENCY = 8;
|
|
3088
3915
|
var UPLOAD_RETRIES = 3;
|
|
3089
3916
|
function resolveSiteName(dir, name) {
|
|
@@ -3104,39 +3931,62 @@ function resolveSiteName(dir, name) {
|
|
|
3104
3931
|
return derived;
|
|
3105
3932
|
}
|
|
3106
3933
|
var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3934
|
+
var baseChanged = (error) => error instanceof CliApiError && error.status === 409 && error.code === "base_changed";
|
|
3107
3935
|
async function deploy(deps, opts) {
|
|
3108
3936
|
const name = resolveSiteName(opts.dir, opts.name);
|
|
3109
3937
|
const local = opts.manifest ?? await buildManifest(opts.dir);
|
|
3110
|
-
|
|
3938
|
+
try {
|
|
3939
|
+
return await deployOnce(deps, opts, name, local);
|
|
3940
|
+
} catch (error) {
|
|
3941
|
+
if (opts.path === void 0 || !baseChanged(error)) throw error;
|
|
3942
|
+
return deployOnce(deps, opts, name, local);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
async function deployOnce(deps, opts, name, local) {
|
|
3946
|
+
const onBaseChanged = (error) => opts.base !== void 0 && baseChanged(error) ? new BaseChangedError(name, opts.base) : error;
|
|
3947
|
+
const folder = opts.path;
|
|
3948
|
+
const prefix = folder ?? opts.within;
|
|
3949
|
+
const sitePath3 = (file) => prefix === void 0 ? file : `${prefix}/${file}`;
|
|
3950
|
+
const base2 = {
|
|
3111
3951
|
name,
|
|
3112
3952
|
fileCount: local.files.length,
|
|
3113
3953
|
totalBytes: local.totalBytes,
|
|
3114
|
-
warnings: local.warnings
|
|
3954
|
+
warnings: local.warnings,
|
|
3955
|
+
...folder === void 0 ? {} : { path: folder }
|
|
3115
3956
|
};
|
|
3116
3957
|
const path3 = `/api/sites/${encodeURIComponent(name)}/deploys`;
|
|
3958
|
+
const manifest = toManifest(local);
|
|
3117
3959
|
const res = await deps.client.request("POST", path3, {
|
|
3118
3960
|
schema: DeployResponseSchema,
|
|
3119
3961
|
body: {
|
|
3120
|
-
manifest:
|
|
3962
|
+
manifest: { files: manifest.files.map((f) => ({ ...f, path: sitePath3(f.path) })) },
|
|
3963
|
+
...folder === void 0 ? {} : { path: folder },
|
|
3964
|
+
...opts.base === void 0 ? {} : {
|
|
3965
|
+
base: opts.base,
|
|
3966
|
+
...opts.partial ? { partial: true, remove: opts.remove ?? [] } : {}
|
|
3967
|
+
},
|
|
3121
3968
|
...opts.spa !== void 0 ? { spa: opts.spa } : {},
|
|
3122
|
-
...opts.public ? { visibility: "public" } : {}
|
|
3969
|
+
...opts.public ? { visibility: "public" } : {},
|
|
3970
|
+
...opts.access ? { access: opts.access } : {},
|
|
3971
|
+
...opts.audience ? { audience: opts.audience } : {}
|
|
3123
3972
|
}
|
|
3124
3973
|
}).catch(async (error) => {
|
|
3125
3974
|
if (error instanceof CliApiError && error.status === 409 && error.code === "name_taken")
|
|
3126
3975
|
throw await nameTakenError(deps.client, name, error.body);
|
|
3127
|
-
throw error;
|
|
3976
|
+
throw onBaseChanged(error);
|
|
3128
3977
|
});
|
|
3129
3978
|
if (res.status !== "upload") {
|
|
3130
3979
|
return {
|
|
3131
|
-
...
|
|
3980
|
+
...base2,
|
|
3132
3981
|
status: res.status,
|
|
3133
3982
|
url: res.site.url,
|
|
3134
3983
|
versionId: res.versionId,
|
|
3135
3984
|
site: res.site,
|
|
3136
|
-
uploaded: 0
|
|
3985
|
+
uploaded: 0,
|
|
3986
|
+
first: false
|
|
3137
3987
|
};
|
|
3138
3988
|
}
|
|
3139
|
-
const byPath = new Map(local.files.map((f) => [f.path, f]));
|
|
3989
|
+
const byPath = new Map(local.files.map((f) => [sitePath3(f.path), f]));
|
|
3140
3990
|
const uploads = res.uploads.map((u) => {
|
|
3141
3991
|
const file = byPath.get(u.path);
|
|
3142
3992
|
if (!file) throw new CliError(`Server asked for a file that is not in the manifest: ${u.path}`);
|
|
@@ -3152,18 +4002,19 @@ async function deploy(deps, opts) {
|
|
|
3152
4002
|
bytes += u.file.size;
|
|
3153
4003
|
opts.onProgress?.({ uploaded: done, total: uploads.length, bytes, totalBytes });
|
|
3154
4004
|
});
|
|
3155
|
-
const fin = await deps.client.request(
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
);
|
|
4005
|
+
const fin = await deps.client.request("POST", `${path3}/${encodeURIComponent(res.versionId)}/finalize`, {
|
|
4006
|
+
schema: FinalizeResponseSchema
|
|
4007
|
+
}).catch((error) => {
|
|
4008
|
+
throw onBaseChanged(error);
|
|
4009
|
+
});
|
|
3160
4010
|
return {
|
|
3161
|
-
...
|
|
4011
|
+
...base2,
|
|
3162
4012
|
status: "live",
|
|
3163
4013
|
url: fin.site.url,
|
|
3164
4014
|
versionId: fin.versionId,
|
|
3165
4015
|
site: fin.site,
|
|
3166
|
-
uploaded: uploads.length
|
|
4016
|
+
uploaded: uploads.length,
|
|
4017
|
+
first: fin.first === true
|
|
3167
4018
|
};
|
|
3168
4019
|
}
|
|
3169
4020
|
async function uploadWithRetry(deps, file, url, headers) {
|
|
@@ -3291,7 +4142,7 @@ function zipDotEntry(name) {
|
|
|
3291
4142
|
async function extractZip(data, dest, skippedDotfiles = []) {
|
|
3292
4143
|
const unsafe = [];
|
|
3293
4144
|
let declared = 0;
|
|
3294
|
-
let
|
|
4145
|
+
let count2 = 0;
|
|
3295
4146
|
let entries;
|
|
3296
4147
|
try {
|
|
3297
4148
|
entries = unzipSync(data, {
|
|
@@ -3306,7 +4157,7 @@ async function extractZip(data, dest, skippedDotfiles = []) {
|
|
|
3306
4157
|
if (dot && !skippedDotfiles.includes(dot)) skippedDotfiles.push(dot);
|
|
3307
4158
|
return false;
|
|
3308
4159
|
}
|
|
3309
|
-
|
|
4160
|
+
count2++;
|
|
3310
4161
|
declared += file.originalSize;
|
|
3311
4162
|
if (file.originalSize > MAX_FILE_BYTES)
|
|
3312
4163
|
throw new CliError(
|
|
@@ -3316,7 +4167,7 @@ async function extractZip(data, dest, skippedDotfiles = []) {
|
|
|
3316
4167
|
throw new CliError(
|
|
3317
4168
|
`The zip expands to more than ${formatBytes(MAX_DEPLOY_BYTES)}, the limit per deploy`
|
|
3318
4169
|
);
|
|
3319
|
-
if (
|
|
4170
|
+
if (count2 > MAX_FILES) throw new CliError(`The zip has more than ${MAX_FILES} files`);
|
|
3320
4171
|
return true;
|
|
3321
4172
|
}
|
|
3322
4173
|
});
|
|
@@ -3387,40 +4238,307 @@ function wrapperPage(file) {
|
|
|
3387
4238
|
`;
|
|
3388
4239
|
}
|
|
3389
4240
|
|
|
3390
|
-
// src/
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
4241
|
+
// src/pull.ts
|
|
4242
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4243
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile6, rm as rm2, stat as stat4, writeFile as writeFile3 } from "node:fs/promises";
|
|
4244
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
4245
|
+
import { z as z18 } from "zod";
|
|
4246
|
+
var SITE_LINK_FILE = ".fling-site.json";
|
|
4247
|
+
var DOWNLOAD_CONCURRENCY = 8;
|
|
4248
|
+
var DOWNLOAD_RETRIES = 3;
|
|
4249
|
+
var SiteLinkSchema = z18.object({
|
|
4250
|
+
/** the Fling it was pulled from */
|
|
4251
|
+
server: z18.string(),
|
|
4252
|
+
name: z18.string(),
|
|
4253
|
+
/** the version pulled, or the one the last deploy made */
|
|
4254
|
+
versionId: z18.string(),
|
|
4255
|
+
/** a pull of one folder of the site: the folder is this folder */
|
|
4256
|
+
path: z18.string().optional(),
|
|
4257
|
+
/** every file pulled (or deployed), by site path: its SHA-256 */
|
|
4258
|
+
files: z18.record(z18.string(), z18.string())
|
|
4259
|
+
});
|
|
4260
|
+
async function readSiteLink(dir) {
|
|
4261
|
+
let raw;
|
|
3402
4262
|
try {
|
|
3403
|
-
await
|
|
4263
|
+
raw = await readFile6(join5(dir, SITE_LINK_FILE), "utf8");
|
|
4264
|
+
} catch {
|
|
4265
|
+
return null;
|
|
4266
|
+
}
|
|
4267
|
+
try {
|
|
4268
|
+
return SiteLinkSchema.parse(JSON.parse(raw));
|
|
4269
|
+
} catch {
|
|
4270
|
+
throw new CliError(`${join5(dir, SITE_LINK_FILE)} is not a site link Fling can read`, {
|
|
4271
|
+
hint: "Delete it to deploy the folder as a plain folder, or pull the site again."
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
async function writeSiteLink(dir, link) {
|
|
4276
|
+
await writeFile3(join5(dir, SITE_LINK_FILE), `${JSON.stringify(link, null, 2)}
|
|
4277
|
+
`);
|
|
4278
|
+
}
|
|
4279
|
+
var siteFiles = (link, local) => Object.fromEntries(
|
|
4280
|
+
local.files.map((f) => [link.path === void 0 ? f.path : `${link.path}/${f.path}`, f.sha256])
|
|
4281
|
+
);
|
|
4282
|
+
function onLink(link, local) {
|
|
4283
|
+
const now = siteFiles(link, local);
|
|
4284
|
+
const changes = { changed: [], added: [], removed: [] };
|
|
4285
|
+
for (const [path3, sha] of Object.entries(now)) {
|
|
4286
|
+
const was = link.files[path3];
|
|
4287
|
+
if (was === void 0) changes.added.push(path3);
|
|
4288
|
+
else if (was !== sha) changes.changed.push(path3);
|
|
4289
|
+
}
|
|
4290
|
+
for (const path3 of Object.keys(link.files)) if (!(path3 in now)) changes.removed.push(path3);
|
|
4291
|
+
return {
|
|
4292
|
+
base: link.versionId,
|
|
4293
|
+
...link.path === void 0 ? {} : { partial: true, within: link.path, remove: changes.removed },
|
|
4294
|
+
changes
|
|
4295
|
+
};
|
|
4296
|
+
}
|
|
4297
|
+
var linkAfter = (link, versionId, local) => ({
|
|
4298
|
+
...link,
|
|
4299
|
+
versionId,
|
|
4300
|
+
files: siteFiles(link, local)
|
|
4301
|
+
});
|
|
4302
|
+
function describeChanges(c) {
|
|
4303
|
+
const parts = [
|
|
4304
|
+
c.changed.length ? `${c.changed.length} changed` : "",
|
|
4305
|
+
c.added.length ? `${c.added.length} added` : "",
|
|
4306
|
+
c.removed.length ? `${c.removed.length} removed` : ""
|
|
4307
|
+
].filter(Boolean);
|
|
4308
|
+
return parts.length ? parts.join(", ") : "no changes";
|
|
4309
|
+
}
|
|
4310
|
+
var defaultSleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4311
|
+
async function pullSite(deps, opts) {
|
|
4312
|
+
const existing = await stat4(opts.dir).catch(() => null);
|
|
4313
|
+
if (existing && !existing.isDirectory()) throw new CliError(`${opts.dir} is not a folder`);
|
|
4314
|
+
const entries = existing ? await readdir2(opts.dir) : [];
|
|
4315
|
+
if (entries.length > 0 && !opts.force) {
|
|
4316
|
+
throw new CliError(`${opts.dir} is not empty`, {
|
|
4317
|
+
hint: "Pull into a new folder, or pass --force to write the files over what is there."
|
|
4318
|
+
});
|
|
4319
|
+
}
|
|
4320
|
+
const before = entries.includes(SITE_LINK_FILE) ? await readSiteLink(opts.dir) : null;
|
|
4321
|
+
const pulled = await deps.client.request(
|
|
4322
|
+
"GET",
|
|
4323
|
+
`/api/sites/${encodeURIComponent(opts.name)}/pull`,
|
|
4324
|
+
{
|
|
4325
|
+
schema: PullResponseSchema,
|
|
4326
|
+
query: { version: opts.versionId, path: opts.path }
|
|
4327
|
+
}
|
|
4328
|
+
);
|
|
4329
|
+
const prefix = opts.path === void 0 ? "" : `${opts.path}/`;
|
|
4330
|
+
const local = (f) => {
|
|
4331
|
+
const rel = f.path.slice(prefix.length);
|
|
4332
|
+
if (!f.path.startsWith(prefix) || !isValidSitePath(rel)) {
|
|
4333
|
+
throw new CliError(`The server sent a file outside the site: ${f.path}`);
|
|
4334
|
+
}
|
|
4335
|
+
return rel;
|
|
4336
|
+
};
|
|
4337
|
+
const totalBytes = pulled.files.reduce((n, f) => n + f.size, 0);
|
|
4338
|
+
let done = 0;
|
|
4339
|
+
let bytes = 0;
|
|
4340
|
+
await mkdir3(opts.dir, { recursive: true });
|
|
4341
|
+
opts.onProgress?.({ done, total: pulled.files.length, bytes, totalBytes });
|
|
4342
|
+
await mapLimit(pulled.files, DOWNLOAD_CONCURRENCY, async (f) => {
|
|
4343
|
+
const target = join5(opts.dir, local(f));
|
|
4344
|
+
const body = await download(deps, f);
|
|
4345
|
+
await mkdir3(dirname2(target), { recursive: true });
|
|
4346
|
+
await writeFile3(target, body);
|
|
4347
|
+
done++;
|
|
4348
|
+
bytes += f.size;
|
|
4349
|
+
opts.onProgress?.({ done, total: pulled.files.length, bytes, totalBytes });
|
|
4350
|
+
});
|
|
4351
|
+
if (before && before.server === deps.client.serverUrl && before.name === opts.name) {
|
|
4352
|
+
const now = new Set(pulled.files.map((f) => f.path));
|
|
4353
|
+
const beforePrefix = before.path === void 0 ? "" : `${before.path}/`;
|
|
4354
|
+
for (const path3 of Object.keys(before.files)) {
|
|
4355
|
+
if (now.has(path3) || !path3.startsWith(beforePrefix)) continue;
|
|
4356
|
+
const rel = path3.slice(beforePrefix.length);
|
|
4357
|
+
if (isValidSitePath(rel)) await rm2(join5(opts.dir, rel), { force: true });
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
await writeSiteLink(opts.dir, {
|
|
4361
|
+
server: deps.client.serverUrl,
|
|
4362
|
+
name: opts.name,
|
|
4363
|
+
versionId: pulled.versionId,
|
|
4364
|
+
...pulled.path === void 0 ? {} : { path: pulled.path },
|
|
4365
|
+
files: Object.fromEntries(pulled.files.map((f) => [f.path, f.sha256]))
|
|
4366
|
+
});
|
|
4367
|
+
return {
|
|
4368
|
+
server: deps.client.serverUrl,
|
|
4369
|
+
name: opts.name,
|
|
4370
|
+
dir: opts.dir,
|
|
4371
|
+
versionId: pulled.versionId,
|
|
4372
|
+
...pulled.path === void 0 ? {} : { path: pulled.path },
|
|
4373
|
+
fileCount: pulled.files.length,
|
|
4374
|
+
totalBytes
|
|
4375
|
+
};
|
|
4376
|
+
}
|
|
4377
|
+
async function download(deps, file) {
|
|
4378
|
+
const sleep = deps.sleep ?? defaultSleep3;
|
|
4379
|
+
for (let attempt = 0; ; attempt++) {
|
|
4380
|
+
let retryable = true;
|
|
4381
|
+
let why;
|
|
4382
|
+
try {
|
|
4383
|
+
const res = await deps.fetch(file.url, { signal: AbortSignal.timeout(5 * 6e4) });
|
|
4384
|
+
if (res.ok) {
|
|
4385
|
+
const body = new Uint8Array(await res.arrayBuffer());
|
|
4386
|
+
if (createHash2("sha256").update(body).digest("hex") === file.sha256) return body;
|
|
4387
|
+
why = "the bytes do not match their hash";
|
|
4388
|
+
} else {
|
|
4389
|
+
await res.arrayBuffer().catch(() => {
|
|
4390
|
+
});
|
|
4391
|
+
retryable = res.status >= 500 || res.status === 408 || res.status === 429;
|
|
4392
|
+
why = `HTTP ${res.status}`;
|
|
4393
|
+
}
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
why = `network error (${error.message})`;
|
|
4396
|
+
}
|
|
4397
|
+
if (!retryable || attempt >= DOWNLOAD_RETRIES) {
|
|
4398
|
+
throw new CliError(`Download of ${file.path} failed: ${why}`, {
|
|
4399
|
+
hint: retryable ? "Check your connection and pull again." : `Download links last ${PULL_URL_TTL_SECONDS / 60} minutes: pull again.`
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
await sleep(500 * 2 ** attempt);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// src/commands/deploy.ts
|
|
4407
|
+
function accessChoice(opts, bin = "fling") {
|
|
4408
|
+
const emails2 = splitEmails(opts.add ?? []);
|
|
4409
|
+
const groups2 = splitGroups(opts.group ?? []);
|
|
4410
|
+
const named = emails2.length + groups2.length > 0;
|
|
4411
|
+
let level;
|
|
4412
|
+
if (opts.access !== void 0) {
|
|
4413
|
+
const parsed = parseAccessLevel(opts.access);
|
|
4414
|
+
if (!parsed) {
|
|
4415
|
+
throw new CliError(`Unknown access "${opts.access}"`, {
|
|
4416
|
+
hint: "Use only_me, members, groups, people or public."
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
level = parsed;
|
|
4420
|
+
} else if (named) level = emails2.length ? "people" : "groups";
|
|
4421
|
+
if (!level) return {};
|
|
4422
|
+
if (opts.public) throw new CliError("Use --access or --public, not both");
|
|
4423
|
+
if (needsAudience(level) && !named) {
|
|
4424
|
+
throw new CliError(`--access ${level} needs someone to let in`, {
|
|
4425
|
+
hint: `Add --add <emails> or --group <slugs> (\`${bin} groups\` lists them).`
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
if (!needsAudience(level) && named) {
|
|
4429
|
+
throw new CliError(`--access ${level} lets no one else in: leave out --add and --group`);
|
|
4430
|
+
}
|
|
4431
|
+
return { access: level, ...needsAudience(level) ? { audience: { emails: emails2, groups: groups2 } } : {} };
|
|
4432
|
+
}
|
|
4433
|
+
async function runDeploy(ctx, input, opts) {
|
|
4434
|
+
const path3 = resolve3(input ?? ".");
|
|
4435
|
+
const found = await stat5(path3).catch(() => null);
|
|
4436
|
+
if (!found)
|
|
4437
|
+
throw new CliError(`No such file or folder: ${input ?? "."}`, {
|
|
4438
|
+
hint: `Give the path to a folder, .html, .zip or other file, e.g. \`${ctx.binName} deploy ./dist\`.`
|
|
4439
|
+
});
|
|
4440
|
+
const linked = found.isDirectory() ? await linkedTo(ctx, await readSiteLink(path3), opts) : null;
|
|
4441
|
+
if (linked) ctx = linked.ctx;
|
|
4442
|
+
const link = linked?.link ?? null;
|
|
4443
|
+
const client = requireClient(ctx);
|
|
4444
|
+
const ask = ctx.interactive && !opts.yes;
|
|
4445
|
+
const choice = accessChoice(opts, ctx.binName);
|
|
4446
|
+
const folder = publishFolder(ctx, opts);
|
|
4447
|
+
const needs = folder !== void 0 ? "pathPublish" : link && !opts.force ? "pull" : choice.access ? "accessLevels" : null;
|
|
4448
|
+
const instance = needs ? await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, needs) : await loadInstance(ctx.store, ctx.config, ctx.fetch);
|
|
4449
|
+
const base2 = sitesBaseOf(instance);
|
|
4450
|
+
if (folder !== void 0 && !pathPublishEnabled(instance)) {
|
|
4451
|
+
throw new CliError(`${instance?.name ?? "This Fling"} can't publish to a path yet`, {
|
|
4452
|
+
hint: "Leave out --path to publish the whole site, or ask its operator to update it."
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
if (link && !opts.force && !pullEnabled(instance)) {
|
|
4456
|
+
throw new CliError(`${instance?.name ?? "This Fling"} can't publish on a pulled version yet`, {
|
|
4457
|
+
hint: "Deploy with --force to replace the whole site with the folder, or ask its operator to update it."
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
4460
|
+
if (choice.access && instance && !accessLevelsEnabled(instance)) {
|
|
4461
|
+
throw new CliError(`${instance.name} can't take --access yet`, {
|
|
4462
|
+
hint: `Leave it out, then use \`${ctx.binName} access\` or \`${ctx.binName} visibility\`.`
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
const prepared = await prepareInput(path3);
|
|
4466
|
+
try {
|
|
4467
|
+
const { access: _asked, path: _path, ...rest } = opts;
|
|
4468
|
+
await deployPrepared(
|
|
4469
|
+
ctx,
|
|
4470
|
+
client,
|
|
4471
|
+
instance,
|
|
4472
|
+
base2,
|
|
4473
|
+
prepared,
|
|
4474
|
+
{ ...rest, ...choice, folder, link },
|
|
4475
|
+
ask
|
|
4476
|
+
);
|
|
3404
4477
|
} finally {
|
|
3405
4478
|
await prepared.cleanup();
|
|
3406
4479
|
}
|
|
3407
4480
|
}
|
|
3408
|
-
async function
|
|
4481
|
+
async function linkedTo(ctx, link, opts) {
|
|
4482
|
+
if (!link) return null;
|
|
4483
|
+
const plain = (why) => {
|
|
4484
|
+
ctx.log.info(`The folder was pulled from ${link.name} on ${link.server}; ${why}`);
|
|
4485
|
+
return null;
|
|
4486
|
+
};
|
|
4487
|
+
if (opts.name !== void 0 && opts.name !== link.name) {
|
|
4488
|
+
return plain(`deploying it to ${opts.name} as a plain folder.`);
|
|
4489
|
+
}
|
|
4490
|
+
if (opts.path !== void 0) return plain("--path deploys it as a plain folder.");
|
|
4491
|
+
if (ctx.config.serverUrl === link.server) return { link, ctx };
|
|
4492
|
+
if (ctx.config.serverUrlSource === "flag" || ctx.config.serverUrlSource === "env") {
|
|
4493
|
+
return plain(`deploying it to ${ctx.config.serverUrl} as a plain folder.`);
|
|
4494
|
+
}
|
|
4495
|
+
const config = await ctx.store.resolve({ serverUrl: link.server });
|
|
4496
|
+
if (!config.apiKey) {
|
|
4497
|
+
throw new CliError(`Not signed in to ${link.server}, where the folder was pulled from`, {
|
|
4498
|
+
exitCode: EXIT_NOT_LOGGED_IN,
|
|
4499
|
+
hint: `Run \`${ctx.binName} login ${link.server}\` first.`
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
return { link, ctx: { ...ctx, config } };
|
|
4503
|
+
}
|
|
4504
|
+
function publishFolder(ctx, opts) {
|
|
4505
|
+
if (opts.path === void 0) return void 0;
|
|
4506
|
+
const folder = normalizePublishPath(opts.path);
|
|
4507
|
+
if (folder === null) {
|
|
4508
|
+
throw new CliError(`Invalid --path "${opts.path}"`, {
|
|
4509
|
+
hint: "Use a folder of the site, like 30-10-26 or reports/2026 (no dotfiles)."
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
if (opts.name === void 0) {
|
|
4513
|
+
throw new CliError("--path needs --name: the site to publish into", {
|
|
4514
|
+
hint: `e.g. \`${ctx.binName} deploy ./report --name changelog --path ${folder}\`.`
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
return folder;
|
|
4518
|
+
}
|
|
4519
|
+
async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask) {
|
|
4520
|
+
const { link } = opts;
|
|
4521
|
+
const folder = opts.folder ?? link?.path;
|
|
4522
|
+
const inFolder = (file) => folder === void 0 ? file : `${folder}/${file}`;
|
|
3409
4523
|
let local = await buildManifest(prepared.dir);
|
|
3410
4524
|
local.skipped.dotfiles.unshift(...prepared.skippedDotfiles);
|
|
3411
4525
|
for (const line of describeSkipped(local.skipped)) ctx.log.info(line);
|
|
3412
|
-
let name = opts.name !== void 0 ? resolveSiteName(prepared.dir, opts.name) : prepared.defaultName;
|
|
3413
|
-
if (opts.name === void 0 && ask)
|
|
3414
|
-
|
|
4526
|
+
let name = link ? link.name : opts.name !== void 0 ? resolveSiteName(prepared.dir, opts.name) : prepared.defaultName;
|
|
4527
|
+
if (!link && opts.name === void 0 && ask) {
|
|
4528
|
+
name = await confirmSiteName(ctx, client, name, base2);
|
|
4529
|
+
}
|
|
4530
|
+
if (!link && !hasRootIndex(local)) {
|
|
3415
4531
|
const pages = htmlPages(local).length;
|
|
3416
|
-
const root =
|
|
3417
|
-
const warning = `No index.html at the root \u2014 ${root} will show a 404.`;
|
|
4532
|
+
const root = base2 ? siteUrlFor(name, base2) + (folder ? `${folder}/` : "") : "the site root";
|
|
4533
|
+
const warning = `No index.html ${folder ? `in ${folder}/` : "at the root"} \u2014 ${root} will show a 404.`;
|
|
3418
4534
|
const choice = opts.listHtml === true ? "list" : opts.listHtml === false ? "fling" : ask ? await askNoIndex(ctx, pages) : pages > 0 ? "list" : "fling";
|
|
3419
4535
|
if (choice === "list") {
|
|
3420
4536
|
local = await withHtmlListing(local, name);
|
|
3421
4537
|
ctx.log.info(`Generated index.html listing ${pages} HTML page${pages === 1 ? "" : "s"}`);
|
|
3422
4538
|
} else ctx.log.warn(warning);
|
|
3423
4539
|
}
|
|
4540
|
+
const onBase = link && !opts.force ? onLink(link, local) : null;
|
|
4541
|
+
const changes = link ? onLink(link, local).changes : null;
|
|
3424
4542
|
let progressShown = false;
|
|
3425
4543
|
const result = await deploy(
|
|
3426
4544
|
{ client, fetch: ctx.fetch },
|
|
@@ -3428,8 +4546,15 @@ async function deployPrepared(ctx, client, instance, base, prepared, opts, ask)
|
|
|
3428
4546
|
dir: prepared.dir,
|
|
3429
4547
|
name,
|
|
3430
4548
|
manifest: local,
|
|
4549
|
+
path: opts.folder,
|
|
4550
|
+
...onBase ? {
|
|
4551
|
+
base: onBase.base,
|
|
4552
|
+
...onBase.partial ? { partial: true, remove: onBase.remove, within: onBase.within } : {}
|
|
4553
|
+
} : {},
|
|
3431
4554
|
spa: opts.spa ? true : void 0,
|
|
3432
4555
|
public: opts.public,
|
|
4556
|
+
access: opts.access,
|
|
4557
|
+
audience: opts.audience,
|
|
3433
4558
|
onProgress: (p) => {
|
|
3434
4559
|
if (p.total === 0) return;
|
|
3435
4560
|
progressShown = true;
|
|
@@ -3438,49 +4563,80 @@ async function deployPrepared(ctx, client, instance, base, prepared, opts, ask)
|
|
|
3438
4563
|
);
|
|
3439
4564
|
}
|
|
3440
4565
|
}
|
|
3441
|
-
).
|
|
4566
|
+
).catch((error) => {
|
|
4567
|
+
if (!(error instanceof BaseChangedError)) throw error;
|
|
4568
|
+
const dir = relative4(process.cwd(), prepared.dir) || ".";
|
|
4569
|
+
throw new BaseChangedError(
|
|
4570
|
+
error.siteName,
|
|
4571
|
+
error.base,
|
|
4572
|
+
`Pull it again into another folder (\`${ctx.binName} pull ${name} ${dir}-latest\`), redo your changes there and deploy that; or \`${ctx.binName} deploy ${dir} --force\` to replace what they published with this folder.`
|
|
4573
|
+
);
|
|
4574
|
+
}).finally(() => {
|
|
3442
4575
|
if (progressShown) ctx.progress(null);
|
|
3443
4576
|
});
|
|
4577
|
+
if (link) {
|
|
4578
|
+
await writeSiteLink(
|
|
4579
|
+
prepared.dir,
|
|
4580
|
+
linkAfter({ ...link, server: client.serverUrl }, result.versionId, local)
|
|
4581
|
+
);
|
|
4582
|
+
}
|
|
3444
4583
|
for (const warning of result.warnings) if (warning !== NO_INDEX_WARNING) ctx.log.warn(warning);
|
|
3445
|
-
const summary = `${result.fileCount} files, ${formatBytes(result.totalBytes)}`;
|
|
4584
|
+
const summary = changes ? `${describeChanges(changes)} since ${link?.versionId}` : `${result.fileCount} files, ${formatBytes(result.totalBytes)}`;
|
|
3446
4585
|
if (result.status === "unchanged") ctx.log.info(`Unchanged ${chalk7.dim(`(${summary})`)}`);
|
|
3447
4586
|
else if (result.status === "repointed")
|
|
3448
4587
|
ctx.log.success(`Restored version ${result.versionId} ${chalk7.dim(`(${summary})`)}`);
|
|
3449
4588
|
else
|
|
3450
4589
|
ctx.log.success(
|
|
3451
|
-
`Deployed ${result.name} ${chalk7.dim(`(${summary}; uploaded ${result.uploaded}, version ${result.versionId})`)}`
|
|
4590
|
+
`Deployed ${result.name}${folder ? ` to /${folder}/` : ""} ${chalk7.dim(`(${summary}; uploaded ${result.uploaded}, version ${result.versionId})`)}`
|
|
3452
4591
|
);
|
|
4592
|
+
const url = folder ? new URL(`${folder}/`, result.url).href : result.url;
|
|
3453
4593
|
const ready = result.status === "unchanged" ? true : (await waitWithSpinner(
|
|
3454
4594
|
ctx.waitReady,
|
|
3455
|
-
|
|
3456
|
-
local.files.map((f) => f.path),
|
|
4595
|
+
url,
|
|
4596
|
+
local.files.map((f) => inFolder(f.path)),
|
|
3457
4597
|
{ progress: ctx.progress, warn: (m) => ctx.log.warn(m) }
|
|
3458
4598
|
)).ready;
|
|
3459
|
-
const
|
|
3460
|
-
const
|
|
4599
|
+
const members2 = instance ? privateAudience(instance) : "anyone signed in to this Fling";
|
|
4600
|
+
const level = siteAccessLevel(result.site);
|
|
4601
|
+
const who = describeSiteAccess(result.site, members2);
|
|
4602
|
+
if (!result.first && opts.access) {
|
|
4603
|
+
ctx.log.warn(
|
|
4604
|
+
`--access only applies to a new site: ${result.name} keeps its access. Change it with \`${ctx.binName} access ${result.name}\`.`
|
|
4605
|
+
);
|
|
4606
|
+
}
|
|
4607
|
+
const accessNotes = result.first ? [
|
|
4608
|
+
`Who can open it: ${who}`,
|
|
4609
|
+
accessChangeHint("cli", result.name, {
|
|
4610
|
+
bin: ctx.binName,
|
|
4611
|
+
invites: instance?.capabilities.invites === true
|
|
4612
|
+
})
|
|
4613
|
+
] : level === "public" ? [] : [`${who} Share it with a link: \`${ctx.binName} share ${result.name}\`.`];
|
|
3461
4614
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
3462
4615
|
const reviewNote = instance ? `Share for feedback (reviewers sign in to ${instance.name}):` : "Share for feedback:";
|
|
3463
4616
|
const data = {
|
|
3464
4617
|
status: result.status,
|
|
3465
4618
|
site: result.site,
|
|
4619
|
+
...folder ? { path: folder, url } : {},
|
|
4620
|
+
...changes && link ? { base: opts.force ? null : link.versionId, changes } : {},
|
|
4621
|
+
access: level,
|
|
4622
|
+
...result.first ? { created: true } : {},
|
|
3466
4623
|
...review ? { reviewUrl: review } : {},
|
|
3467
4624
|
ready,
|
|
3468
4625
|
skipped: local.skipped
|
|
3469
4626
|
};
|
|
3470
4627
|
ctx.out.data(data, () => {
|
|
3471
|
-
const isPrivate = result.site.visibility === "private";
|
|
3472
4628
|
if (ctx.out.tty)
|
|
3473
4629
|
return box(
|
|
3474
4630
|
[
|
|
3475
4631
|
...review ? [chalk7.dim(reviewNote), brand.url(review), "", chalk7.dim("Site:")] : [],
|
|
3476
|
-
brand.url(
|
|
3477
|
-
...
|
|
4632
|
+
brand.url(url),
|
|
4633
|
+
...accessNotes.map((n) => chalk7.dim(n))
|
|
3478
4634
|
],
|
|
3479
|
-
|
|
4635
|
+
chalk7.bold(`flung \xB7 ${ACCESS_LABELS[level].toLowerCase()}`)
|
|
3480
4636
|
);
|
|
3481
|
-
const lines = [chalk7.bold(
|
|
4637
|
+
const lines = [chalk7.bold(url)];
|
|
3482
4638
|
if (review) lines.push(chalk7.dim(`${reviewNote} ${review}`));
|
|
3483
|
-
|
|
4639
|
+
for (const n of accessNotes) lines.push(chalk7.dim(n));
|
|
3484
4640
|
return lines.join("\n");
|
|
3485
4641
|
});
|
|
3486
4642
|
}
|
|
@@ -3501,8 +4657,8 @@ async function askNoIndex(ctx, pages) {
|
|
|
3501
4657
|
});
|
|
3502
4658
|
}
|
|
3503
4659
|
}
|
|
3504
|
-
async function confirmSiteName(ctx, client, generated,
|
|
3505
|
-
const where = (n) =>
|
|
4660
|
+
async function confirmSiteName(ctx, client, generated, base2) {
|
|
4661
|
+
const where = (n) => base2 ? ` \u2192 ${siteUrlFor(n, base2)}` : "";
|
|
3506
4662
|
const describe2 = (n, note) => ctx.log.info(`Site name: ${chalk7.bold(n)}${where(n)} ${chalk7.dim(`(${note})`)}`);
|
|
3507
4663
|
let candidate = generated;
|
|
3508
4664
|
for (; ; ) {
|
|
@@ -3606,21 +4762,165 @@ async function runGroupDelete(ctx, slug, opts) {
|
|
|
3606
4762
|
if (ctx.json) ctx.out.data(result, () => "");
|
|
3607
4763
|
}
|
|
3608
4764
|
|
|
4765
|
+
// src/commands/notifications.ts
|
|
4766
|
+
import chalk9 from "chalk";
|
|
4767
|
+
var VALUES = ["on", "off", "default"];
|
|
4768
|
+
function notificationsClient(ctx) {
|
|
4769
|
+
const instance = ctx.config.instance;
|
|
4770
|
+
if (instance && !notificationsEnabled(instance)) {
|
|
4771
|
+
throw new CliError(`${instance.name} has no notifications`);
|
|
4772
|
+
}
|
|
4773
|
+
return { client: requireClient(ctx), name: instance?.name ?? "This Fling" };
|
|
4774
|
+
}
|
|
4775
|
+
function renderSettings(settings) {
|
|
4776
|
+
const marked = (key, channel) => {
|
|
4777
|
+
const value = settings.cells[key]?.[channel] ?? "off";
|
|
4778
|
+
return settings.overrides[key]?.[channel] !== void 0 ? `${value}*` : value;
|
|
4779
|
+
};
|
|
4780
|
+
const table = renderTable(settings.events, [
|
|
4781
|
+
{ header: "SECTION", value: (e) => sectionTitle(e.section) },
|
|
4782
|
+
{ header: "EVENT", value: (e) => e.key },
|
|
4783
|
+
...settings.channels.map((channel) => ({
|
|
4784
|
+
header: channel.toUpperCase(),
|
|
4785
|
+
value: (e) => marked(e.key, channel)
|
|
4786
|
+
})),
|
|
4787
|
+
{ header: "WHEN", value: (e) => e.title }
|
|
4788
|
+
]);
|
|
4789
|
+
const lines = [table, "", chalk9.dim("* your choice; the rest are the defaults")];
|
|
4790
|
+
if (settings.muted) {
|
|
4791
|
+
lines.unshift(
|
|
4792
|
+
chalk9.yellow(
|
|
4793
|
+
"Muted: nothing is sent, whatever the table says (`fling notifications unmute` to undo)."
|
|
4794
|
+
),
|
|
4795
|
+
""
|
|
4796
|
+
);
|
|
4797
|
+
}
|
|
4798
|
+
return lines.join("\n");
|
|
4799
|
+
}
|
|
4800
|
+
async function runNotifications(ctx) {
|
|
4801
|
+
const { client, name } = notificationsClient(ctx);
|
|
4802
|
+
const settings = await getNotifications(client, name);
|
|
4803
|
+
ctx.out.data(settings, () => renderSettings(settings));
|
|
4804
|
+
}
|
|
4805
|
+
async function runNotificationsSet(ctx, event, channel, value) {
|
|
4806
|
+
const { client, name } = notificationsClient(ctx);
|
|
4807
|
+
if (!VALUES.includes(value)) {
|
|
4808
|
+
throw new CliError(`"${value}" is not on, off or default`);
|
|
4809
|
+
}
|
|
4810
|
+
const current = await getNotifications(client, name);
|
|
4811
|
+
if (!current.events.some((e) => e.key === event)) {
|
|
4812
|
+
throw new CliError(`${name} has no event "${event}"`, {
|
|
4813
|
+
hint: `Events: ${current.events.map((e) => e.key).join(", ")}.`
|
|
4814
|
+
});
|
|
4815
|
+
}
|
|
4816
|
+
if (!current.channels.includes(channel)) {
|
|
4817
|
+
throw new CliError(`${name} has no channel "${channel}"`, {
|
|
4818
|
+
hint: `Channels: ${current.channels.join(", ")}.`
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
const settings = await setNotifications(
|
|
4822
|
+
client,
|
|
4823
|
+
{ cells: [{ event, channel, value }] },
|
|
4824
|
+
name
|
|
4825
|
+
);
|
|
4826
|
+
const now = settings.cells[event]?.[channel] ?? "off";
|
|
4827
|
+
ctx.log.success(
|
|
4828
|
+
value === "default" ? `${event} on ${channel} is back to the default (${now})` : `${event} on ${channel} is ${now}`
|
|
4829
|
+
);
|
|
4830
|
+
if (settings.muted) ctx.log.warn("Everything is muted, so nothing is sent until you unmute.");
|
|
4831
|
+
ctx.out.data(settings, () => renderSettings(settings));
|
|
4832
|
+
}
|
|
4833
|
+
async function runNotificationsMute(ctx, muted) {
|
|
4834
|
+
const { client, name } = notificationsClient(ctx);
|
|
4835
|
+
const settings = await setNotifications(client, { muted }, name);
|
|
4836
|
+
ctx.log.success(
|
|
4837
|
+
muted ? `Muted: ${name} sends you nothing` : `Unmuted: ${name} sends what the table says`
|
|
4838
|
+
);
|
|
4839
|
+
if (ctx.json) ctx.out.data(settings, () => "");
|
|
4840
|
+
}
|
|
4841
|
+
async function runNotificationsSent(ctx) {
|
|
4842
|
+
const { client, name } = notificationsClient(ctx);
|
|
4843
|
+
const sent = await listSentNotifications(client, name);
|
|
4844
|
+
ctx.out.data(
|
|
4845
|
+
{ sent },
|
|
4846
|
+
() => sent.length ? renderTable(sent, [
|
|
4847
|
+
{ header: "SENT", value: (s) => formatDate(s.sentAt) },
|
|
4848
|
+
{ header: "CHANNEL", value: (s) => s.channel },
|
|
4849
|
+
{ header: "SUBJECT", value: (s) => s.subject },
|
|
4850
|
+
{ header: "EVENTS", value: (s) => s.eventKeys.join(", ") }
|
|
4851
|
+
]) : chalk9.dim("Nothing was sent to you in the last 30 days.")
|
|
4852
|
+
);
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
// src/commands/pull.ts
|
|
4856
|
+
import { relative as relative5, resolve as resolve4 } from "node:path";
|
|
4857
|
+
import chalk10 from "chalk";
|
|
4858
|
+
async function runPull(ctx, name, dir, opts) {
|
|
4859
|
+
const client = requireClient(ctx);
|
|
4860
|
+
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "pull");
|
|
4861
|
+
if (instance && !pullEnabled(instance)) {
|
|
4862
|
+
throw new CliError(`${instance.name} can't pull sites yet`, {
|
|
4863
|
+
hint: "Ask its operator to update it."
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
let path3;
|
|
4867
|
+
if (opts.path !== void 0) {
|
|
4868
|
+
const folder = normalizePublishPath(opts.path);
|
|
4869
|
+
if (folder === null) {
|
|
4870
|
+
throw new CliError(`Invalid --path "${opts.path}"`, {
|
|
4871
|
+
hint: "Use a folder of the site, like 30-10-26 or reports/2026 (no dotfiles)."
|
|
4872
|
+
});
|
|
4873
|
+
}
|
|
4874
|
+
path3 = folder;
|
|
4875
|
+
}
|
|
4876
|
+
const target = resolve4(dir ?? name);
|
|
4877
|
+
let progressShown = false;
|
|
4878
|
+
const pulled = await pullSite(
|
|
4879
|
+
{ client, fetch: ctx.fetch },
|
|
4880
|
+
{
|
|
4881
|
+
name,
|
|
4882
|
+
dir: target,
|
|
4883
|
+
...opts.versionId ? { versionId: opts.versionId } : {},
|
|
4884
|
+
...path3 === void 0 ? {} : { path: path3 },
|
|
4885
|
+
force: opts.force,
|
|
4886
|
+
onProgress: (p) => {
|
|
4887
|
+
if (p.total === 0) return;
|
|
4888
|
+
progressShown = true;
|
|
4889
|
+
ctx.progress(
|
|
4890
|
+
`downloading ${p.done}/${p.total} files \xB7 ${formatBytes(p.bytes)}/${formatBytes(p.totalBytes)}`
|
|
4891
|
+
);
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
).finally(() => {
|
|
4895
|
+
if (progressShown) ctx.progress(null);
|
|
4896
|
+
});
|
|
4897
|
+
const shown = relative5(process.cwd(), pulled.dir) || ".";
|
|
4898
|
+
ctx.log.success(
|
|
4899
|
+
`Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${shown} ${chalk10.dim(`(${pulled.fileCount} files, ${formatBytes(pulled.totalBytes)}, version ${pulled.versionId})`)}`
|
|
4900
|
+
);
|
|
4901
|
+
ctx.out.data(
|
|
4902
|
+
pulled,
|
|
4903
|
+
() => chalk10.dim(
|
|
4904
|
+
`Change what you need, then \`${ctx.binName} deploy ${shown}\`: it publishes on version ${pulled.versionId}, and stops if someone publishes ${name} before you do.`
|
|
4905
|
+
)
|
|
4906
|
+
);
|
|
4907
|
+
}
|
|
4908
|
+
|
|
3609
4909
|
// src/commands/setup.ts
|
|
3610
4910
|
import { intro, outro, spinner } from "@clack/prompts";
|
|
3611
|
-
import
|
|
4911
|
+
import chalk11 from "chalk";
|
|
3612
4912
|
|
|
3613
4913
|
// src/claude.ts
|
|
3614
4914
|
import { execFile as execFile2 } from "node:child_process";
|
|
3615
|
-
import { access, mkdtemp as mkdtemp2, writeFile as
|
|
4915
|
+
import { access, mkdtemp as mkdtemp2, writeFile as writeFile4 } from "node:fs/promises";
|
|
3616
4916
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3617
|
-
import { join as
|
|
4917
|
+
import { join as join6 } from "node:path";
|
|
3618
4918
|
var MARKETPLACE_SOURCE = REPO_SLUG;
|
|
3619
4919
|
var MARKETPLACE_NAME = "fling";
|
|
3620
4920
|
var PLUGIN_NAME = "fling";
|
|
3621
4921
|
var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
3622
4922
|
var MCPB_PATH = "/downloads/fling.mcpb";
|
|
3623
|
-
var runClaude = (args) => new Promise((
|
|
4923
|
+
var runClaude = (args) => new Promise((resolve6) => {
|
|
3624
4924
|
execFile2(
|
|
3625
4925
|
"claude",
|
|
3626
4926
|
[...args],
|
|
@@ -3628,7 +4928,7 @@ var runClaude = (args) => new Promise((resolve5) => {
|
|
|
3628
4928
|
(error, stdout, stderr) => {
|
|
3629
4929
|
const missing = error?.code === "ENOENT";
|
|
3630
4930
|
const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
|
|
3631
|
-
|
|
4931
|
+
resolve6({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
3632
4932
|
}
|
|
3633
4933
|
);
|
|
3634
4934
|
});
|
|
@@ -3674,7 +4974,7 @@ async function findClaudeDesktop(platform = process.platform, env = process.env,
|
|
|
3674
4974
|
() => true,
|
|
3675
4975
|
() => false
|
|
3676
4976
|
)) {
|
|
3677
|
-
const candidates = platform === "darwin" ? ["/Applications/Claude.app",
|
|
4977
|
+
const candidates = platform === "darwin" ? ["/Applications/Claude.app", join6(homedir3(), "Applications", "Claude.app")] : platform === "win32" && env.LOCALAPPDATA ? [join6(env.LOCALAPPDATA, "AnthropicClaude"), join6(env.LOCALAPPDATA, "Programs", "Claude")] : [];
|
|
3678
4978
|
for (const path3 of candidates) if (await exists(path3)) return path3;
|
|
3679
4979
|
return null;
|
|
3680
4980
|
}
|
|
@@ -3682,9 +4982,9 @@ async function downloadExtension(serverUrl, fetch2) {
|
|
|
3682
4982
|
const url = new URL(MCPB_PATH, serverUrl).toString();
|
|
3683
4983
|
const response = await fetch2(url);
|
|
3684
4984
|
if (!response.ok) throw new Error(`Could not download ${url} (HTTP ${response.status})`);
|
|
3685
|
-
const dir = await mkdtemp2(
|
|
3686
|
-
const file =
|
|
3687
|
-
await
|
|
4985
|
+
const dir = await mkdtemp2(join6(tmpdir2(), "fling-mcpb-"));
|
|
4986
|
+
const file = join6(dir, "fling.mcpb");
|
|
4987
|
+
await writeFile4(file, Buffer.from(await response.arrayBuffer()));
|
|
3688
4988
|
return file;
|
|
3689
4989
|
}
|
|
3690
4990
|
|
|
@@ -3701,14 +5001,14 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
3701
5001
|
...baseCtx,
|
|
3702
5002
|
log: createLogger({
|
|
3703
5003
|
debug: baseCtx.config.debug,
|
|
3704
|
-
write: (line) => process.stderr.write(`${
|
|
5004
|
+
write: (line) => process.stderr.write(`${chalk11.gray("\u2502")} ${line}
|
|
3705
5005
|
`)
|
|
3706
5006
|
})
|
|
3707
5007
|
} : baseCtx;
|
|
3708
5008
|
if (fancy) {
|
|
3709
5009
|
process.stderr.write(`${logo()}
|
|
3710
5010
|
`);
|
|
3711
|
-
intro(
|
|
5011
|
+
intro(chalk11.bold(" fling setup "), { output: process.stderr });
|
|
3712
5012
|
}
|
|
3713
5013
|
const login = await stepLogin(ctx, opts.url);
|
|
3714
5014
|
const results = {
|
|
@@ -3722,7 +5022,7 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
3722
5022
|
const failed = Object.values(results).includes("failed");
|
|
3723
5023
|
if (fancy)
|
|
3724
5024
|
outro(
|
|
3725
|
-
failed ?
|
|
5025
|
+
failed ? chalk11.yellow("Setup finished with problems \u2014 see above, then run `fling setup` again.") : `You're set. Try ${chalk11.bold(`${ctx.binName} deploy ./dist`)}, or ask Claude to \u201Cfling this\u201D.`,
|
|
3726
5026
|
{ output: process.stderr }
|
|
3727
5027
|
);
|
|
3728
5028
|
ctx.out.data(results, () => "");
|
|
@@ -3745,7 +5045,7 @@ async function signIn(ctx, serverUrl) {
|
|
|
3745
5045
|
try {
|
|
3746
5046
|
const me = await whoami(requireClient(ctx));
|
|
3747
5047
|
if (ctx.config.remembered) await ctx.store.setCurrent(serverUrl);
|
|
3748
|
-
ctx.log.success(`Signed in as ${
|
|
5048
|
+
ctx.log.success(`Signed in as ${chalk11.bold(me.name)} ${chalk11.dim(`<${me.email}>`)}`);
|
|
3749
5049
|
return "already";
|
|
3750
5050
|
} catch (error) {
|
|
3751
5051
|
if (exitCodeFor(error) !== 2) {
|
|
@@ -3766,7 +5066,7 @@ async function signIn(ctx, serverUrl) {
|
|
|
3766
5066
|
async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
3767
5067
|
const status = await claudeCodeStatus(run);
|
|
3768
5068
|
if (status.claude === "missing") {
|
|
3769
|
-
ctx.log.info(`Claude Code not found ${
|
|
5069
|
+
ctx.log.info(`Claude Code not found ${chalk11.dim("(no `claude` on your PATH) \u2014 skipped")}`);
|
|
3770
5070
|
ctx.log.hint(
|
|
3771
5071
|
`Once it is installed, run \`${ctx.binName} setup\` again, or in Claude Code: /plugin marketplace add ${MARKETPLACE_SOURCE} then /plugin install ${PLUGIN_ID}`
|
|
3772
5072
|
);
|
|
@@ -3775,7 +5075,7 @@ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
|
3775
5075
|
const installed = "installed" in status && status.installed;
|
|
3776
5076
|
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?";
|
|
3777
5077
|
if (ask && !await ctx.confirm(question)) {
|
|
3778
|
-
ctx.log.info(
|
|
5078
|
+
ctx.log.info(chalk11.dim("Claude Code plugin: skipped"));
|
|
3779
5079
|
return installed ? "already" : "skipped";
|
|
3780
5080
|
}
|
|
3781
5081
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
@@ -3789,7 +5089,7 @@ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
|
3789
5089
|
else ctx.log.error(message);
|
|
3790
5090
|
return "failed";
|
|
3791
5091
|
}
|
|
3792
|
-
const done = installed ? `Claude Code plugin ${PLUGIN_NAME} is up to date` : `Claude Code plugin ${PLUGIN_NAME} installed ${
|
|
5092
|
+
const done = installed ? `Claude Code plugin ${PLUGIN_NAME} is up to date` : `Claude Code plugin ${PLUGIN_NAME} installed ${chalk11.dim(`(${scope} scope)`)}`;
|
|
3793
5093
|
if (s) s.stop(done);
|
|
3794
5094
|
else ctx.log.success(done);
|
|
3795
5095
|
ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick it up.");
|
|
@@ -3799,7 +5099,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
3799
5099
|
const app = await (deps.findClaudeDesktop ?? findClaudeDesktop)();
|
|
3800
5100
|
if (!app) return "missing";
|
|
3801
5101
|
if (ask && !await ctx.confirm("Add the Fling extension to Claude Desktop too?")) {
|
|
3802
|
-
ctx.log.info(
|
|
5102
|
+
ctx.log.info(chalk11.dim("Claude Desktop extension: skipped"));
|
|
3803
5103
|
return "skipped";
|
|
3804
5104
|
}
|
|
3805
5105
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
@@ -3820,7 +5120,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
3820
5120
|
}
|
|
3821
5121
|
|
|
3822
5122
|
// src/commands/sites.ts
|
|
3823
|
-
import
|
|
5123
|
+
import chalk12 from "chalk";
|
|
3824
5124
|
async function runList(ctx, opts) {
|
|
3825
5125
|
const sites = await listSites(requireClient(ctx), opts);
|
|
3826
5126
|
ctx.out.data(
|
|
@@ -3829,9 +5129,10 @@ async function runList(ctx, opts) {
|
|
|
3829
5129
|
{ header: "NAME", value: (s) => s.name },
|
|
3830
5130
|
{ header: "URL", value: (s) => s.url },
|
|
3831
5131
|
{
|
|
3832
|
-
header: "
|
|
3833
|
-
value: (s) => s.archived ?
|
|
5132
|
+
header: "ACCESS",
|
|
5133
|
+
value: (s) => accessLabel(s) + (s.archived ? " (archived)" : "")
|
|
3834
5134
|
},
|
|
5135
|
+
...sites.some((s) => s.role) ? [{ header: "ROLE", value: (s) => s.role ?? "-" }] : [],
|
|
3835
5136
|
{ header: "SHARE", value: (s) => s.shareUrl },
|
|
3836
5137
|
{ header: "UPDATED", value: (s) => formatDate(s.updatedAt) }
|
|
3837
5138
|
])
|
|
@@ -3854,37 +5155,50 @@ async function runListShared(ctx, opts) {
|
|
|
3854
5155
|
])
|
|
3855
5156
|
);
|
|
3856
5157
|
}
|
|
5158
|
+
var accessLabel = (site) => ACCESS_LABELS[siteAccessLevel(site)];
|
|
5159
|
+
var row = (label, value) => `${chalk12.bold(`${label}:`.padEnd(14))}${value}`;
|
|
3857
5160
|
function describeSite(site) {
|
|
3858
|
-
const row = (label, value) => `${chalk10.bold(`${label}:`.padEnd(12))}${value}`;
|
|
3859
5161
|
return [
|
|
3860
5162
|
row("Name", site.name),
|
|
3861
5163
|
row("URL", site.url),
|
|
3862
|
-
row("
|
|
5164
|
+
row("Access", accessLabel(site) + (site.archived ? chalk12.yellow(" (archived)") : "")),
|
|
3863
5165
|
row("SPA", site.spa ? "yes" : "no"),
|
|
3864
5166
|
row("Share link", site.shareUrl ?? "-"),
|
|
3865
5167
|
row("Version", site.currentVersionId ?? "-"),
|
|
3866
|
-
...filesRow(site
|
|
5168
|
+
...filesRow(site),
|
|
3867
5169
|
row("Created", formatDate(site.createdAt)),
|
|
3868
5170
|
row("Updated", formatDate(site.updatedAt))
|
|
3869
5171
|
];
|
|
3870
5172
|
}
|
|
3871
|
-
function
|
|
5173
|
+
function rolesRows(site) {
|
|
5174
|
+
return [
|
|
5175
|
+
...site.owners ? [row("Owners", whoHolds(site.owners))] : [],
|
|
5176
|
+
...site.owners ? [row("Publishers", whoHolds(site.publishers))] : [],
|
|
5177
|
+
...site.role ? [row("You", site.role)] : []
|
|
5178
|
+
];
|
|
5179
|
+
}
|
|
5180
|
+
function filesRow(site) {
|
|
3872
5181
|
const current = site.versions?.find((v) => v.id === site.currentVersionId);
|
|
3873
5182
|
if (!current) return [];
|
|
3874
5183
|
const n = current.fileCount;
|
|
3875
|
-
return [
|
|
5184
|
+
return [
|
|
5185
|
+
row("Files", `${n} file${n === 1 ? "" : "s"}, ${formatBytes(current.totalBytes)}`),
|
|
5186
|
+
...current.publishedBy ? [row("Published by", current.publishedBy)] : [],
|
|
5187
|
+
...current.path ? [row("Published to", `/${current.path}/`)] : []
|
|
5188
|
+
];
|
|
3876
5189
|
}
|
|
3877
5190
|
var membersOf = (ctx) => ctx.config.instance ? privateAudience(ctx.config.instance) : "anyone signed in to this Fling";
|
|
3878
5191
|
async function runInfo(ctx, name) {
|
|
3879
5192
|
const site = await getSite(requireClient(ctx), name);
|
|
3880
|
-
const access2 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${
|
|
5193
|
+
const access2 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${chalk12.bold("Who:".padEnd(14))}${describeAudience(site, membersOf(ctx))}`] : [];
|
|
3881
5194
|
ctx.out.data(
|
|
3882
5195
|
site,
|
|
3883
5196
|
() => [
|
|
3884
5197
|
...describeSite(site),
|
|
3885
5198
|
...access2,
|
|
5199
|
+
...rolesRows(site),
|
|
3886
5200
|
"",
|
|
3887
|
-
|
|
5201
|
+
chalk12.dim(
|
|
3888
5202
|
`${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
|
|
3889
5203
|
)
|
|
3890
5204
|
].join("\n")
|
|
@@ -3899,6 +5213,8 @@ async function runVersions(ctx, name) {
|
|
|
3899
5213
|
{ header: "VERSION", value: (v) => v.id },
|
|
3900
5214
|
{ header: "FILES", value: (v) => v.fileCount },
|
|
3901
5215
|
{ header: "SIZE", value: (v) => formatBytes(v.totalBytes) },
|
|
5216
|
+
{ header: "BY", value: (v) => v.publishedBy ?? "-" },
|
|
5217
|
+
{ header: "PATH", value: (v) => v.path ? `/${v.path}/` : "-" },
|
|
3902
5218
|
{ header: "CREATED", value: (v) => formatDate(v.createdAt) }
|
|
3903
5219
|
])
|
|
3904
5220
|
);
|
|
@@ -3910,7 +5226,7 @@ async function runFiles(ctx, name, versionId) {
|
|
|
3910
5226
|
}
|
|
3911
5227
|
ctx.out.data(result, () => {
|
|
3912
5228
|
const n = result.files.length;
|
|
3913
|
-
const header =
|
|
5229
|
+
const header = chalk12.dim(
|
|
3914
5230
|
`${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
|
|
3915
5231
|
);
|
|
3916
5232
|
const table = renderTable(result.files, [
|
|
@@ -3946,7 +5262,7 @@ async function runShare(ctx, name, opts) {
|
|
|
3946
5262
|
ctx.log.success("Share link created \u2014 anyone with it can view the site");
|
|
3947
5263
|
ctx.out.data(
|
|
3948
5264
|
ready === void 0 ? result : { ...result, ready },
|
|
3949
|
-
() => result.shareUrl ? result.shareUrl :
|
|
5265
|
+
() => result.shareUrl ? result.shareUrl : chalk12.dim("(no share link)")
|
|
3950
5266
|
);
|
|
3951
5267
|
}
|
|
3952
5268
|
async function runVisibility(ctx, name, visibility) {
|
|
@@ -3959,7 +5275,18 @@ async function runVisibility(ctx, name, visibility) {
|
|
|
3959
5275
|
);
|
|
3960
5276
|
ctx.out.data(site, () => site.url);
|
|
3961
5277
|
}
|
|
5278
|
+
function levelOf(opts) {
|
|
5279
|
+
const levels = [
|
|
5280
|
+
...opts.onlyMe ? ["only_me"] : [],
|
|
5281
|
+
...opts.members ? ["members"] : [],
|
|
5282
|
+
...opts.public ? ["public"] : []
|
|
5283
|
+
];
|
|
5284
|
+
if (levels.length > 1) throw new CliError("Use one of --only-me, --members and --public");
|
|
5285
|
+
return levels[0];
|
|
5286
|
+
}
|
|
3962
5287
|
async function runAccess(ctx, name, opts) {
|
|
5288
|
+
const level = levelOf(opts);
|
|
5289
|
+
if (level) return runSetLevel(ctx, name, level, opts);
|
|
3963
5290
|
const instance = ctx.config.instance;
|
|
3964
5291
|
if (instance && !audiencesEnabled(instance)) {
|
|
3965
5292
|
throw new CliError(`${instance.name} can't limit a site to listed people`);
|
|
@@ -3980,6 +5307,49 @@ async function runAccess(ctx, name, opts) {
|
|
|
3980
5307
|
for (const w of warnings) ctx.log.warn(w);
|
|
3981
5308
|
ctx.out.data({ ...site, warnings }, () => describeAudience(site, membersOf(ctx)));
|
|
3982
5309
|
}
|
|
5310
|
+
async function runSetLevel(ctx, name, level, opts) {
|
|
5311
|
+
if (opts.add?.length || opts.remove?.length || opts.group?.length || opts.removeGroup?.length || opts.clear) {
|
|
5312
|
+
throw new CliError(`--${level === "only_me" ? "only-me" : level} lets no one else in`, {
|
|
5313
|
+
hint: "Leave out --add, --remove, --group, --remove-group and --clear."
|
|
5314
|
+
});
|
|
5315
|
+
}
|
|
5316
|
+
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "accessLevels");
|
|
5317
|
+
if (instance && !accessLevelsEnabled(instance)) {
|
|
5318
|
+
throw new CliError(`${instance.name} has no access levels yet`, {
|
|
5319
|
+
hint: `Use \`${ctx.binName} visibility ${name} public|private\` and \`${ctx.binName} access ${name} --clear\`.`
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
const site = await setAccess(requireClient(ctx), name, level);
|
|
5323
|
+
const warnings = audienceWarnings(site);
|
|
5324
|
+
for (const w of warnings) ctx.log.warn(w);
|
|
5325
|
+
ctx.out.data({ ...site, warnings }, () => describeAudience(site, membersOf(ctx)));
|
|
5326
|
+
}
|
|
5327
|
+
async function runRoles(ctx, which, name, opts) {
|
|
5328
|
+
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "siteRoles");
|
|
5329
|
+
if (instance && !siteRolesEnabled(instance)) {
|
|
5330
|
+
throw new CliError(`${instance.name} has no site owners and publishers yet`, {
|
|
5331
|
+
hint: "Ask its operator to update it."
|
|
5332
|
+
});
|
|
5333
|
+
}
|
|
5334
|
+
const change = {
|
|
5335
|
+
...opts.set ? { set: splitEmails(opts.set) } : {},
|
|
5336
|
+
add: splitEmails(opts.add ?? []),
|
|
5337
|
+
remove: splitEmails(opts.remove ?? []),
|
|
5338
|
+
addGroups: splitGroups(opts.group ?? []),
|
|
5339
|
+
removeGroups: splitGroups(opts.removeGroup ?? [])
|
|
5340
|
+
};
|
|
5341
|
+
if (instance && (change.addGroups.length || change.removeGroups.length)) {
|
|
5342
|
+
if (!groupsEnabled(instance)) throw new CliError(`${instance.name} has no groups`);
|
|
5343
|
+
}
|
|
5344
|
+
const client = requireClient(ctx);
|
|
5345
|
+
const changed = changesAudience(change);
|
|
5346
|
+
const site = changed ? await setRoles(client, name, which, change) : await getRoles(client, name);
|
|
5347
|
+
if (changed) ctx.log.success(`${name}'s ${which} changed`);
|
|
5348
|
+
ctx.out.data(
|
|
5349
|
+
{ name: site.name, role: site.role, owners: site.owners, publishers: site.publishers },
|
|
5350
|
+
() => describeRoles(site)
|
|
5351
|
+
);
|
|
5352
|
+
}
|
|
3983
5353
|
async function runArchive(ctx, name, unarchive = false) {
|
|
3984
5354
|
const site = await archiveSite(requireClient(ctx), name, { unarchive });
|
|
3985
5355
|
ctx.log.success(
|
|
@@ -4004,15 +5374,15 @@ async function runRemove(ctx, name, opts) {
|
|
|
4004
5374
|
}
|
|
4005
5375
|
|
|
4006
5376
|
// src/mcp.ts
|
|
4007
|
-
import { mkdir as
|
|
5377
|
+
import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
4008
5378
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
4009
|
-
import { basename as basename3, dirname as
|
|
5379
|
+
import { basename as basename3, dirname as dirname3, join as join7, resolve as resolvePath } from "node:path";
|
|
4010
5380
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4011
5381
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4012
|
-
import { z as
|
|
5382
|
+
import { z as z19 } from "zod";
|
|
4013
5383
|
|
|
4014
5384
|
// src/locate.ts
|
|
4015
|
-
import { lstat, readdir as
|
|
5385
|
+
import { lstat, readdir as readdir3, readFile as readFile7, realpath as realpath3, stat as stat6 } from "node:fs/promises";
|
|
4016
5386
|
import { posix, win32 } from "node:path";
|
|
4017
5387
|
import { fileURLToPath } from "node:url";
|
|
4018
5388
|
var DOWNLOADS_DIR_ENV = "FLING_DOWNLOADS_DIR";
|
|
@@ -4025,7 +5395,7 @@ var SANDBOX_PATH_RE = /^\/(mnt\/user-data|mnt\/data|sessions|home\/claude|tmp\/o
|
|
|
4025
5395
|
var nodeFs = {
|
|
4026
5396
|
async stat(path3) {
|
|
4027
5397
|
try {
|
|
4028
|
-
const s = await
|
|
5398
|
+
const s = await stat6(path3);
|
|
4029
5399
|
return { isFile: s.isFile(), isDirectory: s.isDirectory(), mtimeMs: s.mtimeMs, size: s.size };
|
|
4030
5400
|
} catch {
|
|
4031
5401
|
return null;
|
|
@@ -4033,11 +5403,11 @@ var nodeFs = {
|
|
|
4033
5403
|
},
|
|
4034
5404
|
async readdir(path3) {
|
|
4035
5405
|
try {
|
|
4036
|
-
const entries = await
|
|
5406
|
+
const entries = await readdir3(path3, { withFileTypes: true });
|
|
4037
5407
|
const out = [];
|
|
4038
5408
|
for (const e of entries) {
|
|
4039
5409
|
if (e.isSymbolicLink()) {
|
|
4040
|
-
const s = await
|
|
5410
|
+
const s = await stat6(`${path3}/${e.name}`).catch(() => null);
|
|
4041
5411
|
out.push({ name: e.name, isFile: Boolean(s?.isFile()), isDirectory: false });
|
|
4042
5412
|
} else out.push({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() });
|
|
4043
5413
|
}
|
|
@@ -4052,7 +5422,7 @@ var nodeFs = {
|
|
|
4052
5422
|
async readText(path3) {
|
|
4053
5423
|
try {
|
|
4054
5424
|
if (!(await lstat(path3)).isFile()) return null;
|
|
4055
|
-
return await
|
|
5425
|
+
return await readFile7(path3, "utf8");
|
|
4056
5426
|
} catch {
|
|
4057
5427
|
return null;
|
|
4058
5428
|
}
|
|
@@ -4427,12 +5797,14 @@ var mcpLocalInstructions = (w, opts = {}) => [
|
|
|
4427
5797
|
...[serversInstructions(opts.servers ?? [])].filter((p) => p !== void 0),
|
|
4428
5798
|
"Content you created in this conversation (an HTML page, a report, a small app): call fling_publish with every file inline (text as utf8, binary as base64). Never pass file paths from your sandbox (e.g. /mnt/user-data/outputs/\u2026, /sessions/\u2026) \u2014 this computer cannot see them \u2014 and never curl or upload from the sandbox.",
|
|
4429
5799
|
'A file or folder the user has on this computer (a downloaded .html, a .zip, a build folder): call fling_deploy with its path, or just its file name (e.g. "Pitch Deck.html") \u2014 it is looked up in Downloads, Desktop, Documents/Claude and Claude Desktop output folders. Use fling_find_files to see recent candidates when unsure which file the user means.',
|
|
5800
|
+
"To change a site that is already published, start from what is live: fling_pull downloads its files into a folder on this computer; edit them there, then fling_deploy that folder publishes on the version pulled, sending only what changed. If it says base_changed, someone published in between: pull again into another folder and redo the changes, rather than force over their work. (fling_read_files and fling_publish with `base` do the same inline, for content you edit in this conversation.)",
|
|
4430
5801
|
"Confirm the site name with the user before publishing (fling_check_name shows whether it is free and, for a path, the generated name). Names are global, first come first served; when one is taken, offer the suggestions.",
|
|
4431
5802
|
"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.",
|
|
4432
5803
|
NEVER_PUBLISHED,
|
|
4433
5804
|
"Not logged in? Call fling_login, let the user finish signing in in the browser, then retry.",
|
|
4434
|
-
|
|
4435
|
-
...opts.comments ? [COMMENTS_NOTE(w)] : []
|
|
5805
|
+
`${ACCESS_NOTE(w)} 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."}`,
|
|
5806
|
+
...opts.comments ? [COMMENTS_NOTE(w)] : [],
|
|
5807
|
+
...opts.notifications ? [NOTIFICATIONS_NOTE(w)] : []
|
|
4436
5808
|
].join("\n\n");
|
|
4437
5809
|
var onFling = (url, instance) => `
|
|
4438
5810
|
Fling: ${serverLabel(url)}${instance ? ` (${instance.name})` : ""}`;
|
|
@@ -4446,7 +5818,7 @@ function createMcpServer(deps = {}) {
|
|
|
4446
5818
|
const several = servers.length > 1;
|
|
4447
5819
|
const wording = mcpWording(several ? null : servers[0]?.instance);
|
|
4448
5820
|
const mcpTools2 = mcpToolsFor(wording);
|
|
4449
|
-
const serverArg =
|
|
5821
|
+
const serverArg = z19.string().min(1).max(500).optional().describe(serverArgDescription(servers));
|
|
4450
5822
|
const note = serverToolNote(servers);
|
|
4451
5823
|
const serverTool = (def, description = def.description) => ({
|
|
4452
5824
|
title: def.title,
|
|
@@ -4489,6 +5861,58 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
4489
5861
|
}
|
|
4490
5862
|
};
|
|
4491
5863
|
}
|
|
5864
|
+
async function membersOn(target2) {
|
|
5865
|
+
const instance = await target2.instance();
|
|
5866
|
+
return instance ? privateAudience(instance) : "anyone signed in to this Fling";
|
|
5867
|
+
}
|
|
5868
|
+
async function requireAccessLevels(target2) {
|
|
5869
|
+
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "accessLevels");
|
|
5870
|
+
if (instance && !accessLevelsEnabled(instance)) {
|
|
5871
|
+
throw new CliError(
|
|
5872
|
+
`${instance.name} (${serverLabel(target2.url)}) has no access levels yet: leave out \`access\`, then use fling_set_visibility or fling_set_audience`
|
|
5873
|
+
);
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
async function requireSiteRoles(target2) {
|
|
5877
|
+
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "siteRoles");
|
|
5878
|
+
if (instance && !siteRolesEnabled(instance)) {
|
|
5879
|
+
throw new CliError(
|
|
5880
|
+
`${instance.name} (${serverLabel(target2.url)}) has no site owners and publishers yet`
|
|
5881
|
+
);
|
|
5882
|
+
}
|
|
5883
|
+
return instance;
|
|
5884
|
+
}
|
|
5885
|
+
async function publishFolder2(target2, raw) {
|
|
5886
|
+
const folder = normalizePublishPath(raw);
|
|
5887
|
+
if (folder === null) {
|
|
5888
|
+
throw new CliError(
|
|
5889
|
+
`Invalid path "${raw}": use a folder of the site, like "30-10-26" or "reports/2026" (no dotfiles)`
|
|
5890
|
+
);
|
|
5891
|
+
}
|
|
5892
|
+
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "pathPublish");
|
|
5893
|
+
if (!pathPublishEnabled(instance)) {
|
|
5894
|
+
throw new CliError(
|
|
5895
|
+
`${instance?.name ?? "This Fling"} (${serverLabel(target2.url)}) can't publish to a path yet: publish the whole site instead`
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5898
|
+
return folder;
|
|
5899
|
+
}
|
|
5900
|
+
async function requirePull(target2) {
|
|
5901
|
+
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "pull");
|
|
5902
|
+
if (!pullEnabled(instance)) {
|
|
5903
|
+
throw new CliError(
|
|
5904
|
+
`${instance?.name ?? "This Fling"} (${serverLabel(target2.url)}) can't pull sites or publish on a pulled version yet`
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
async function linkedServer(path3) {
|
|
5909
|
+
try {
|
|
5910
|
+
const located = await resolveInput(path3, locate);
|
|
5911
|
+
return located.isDirectory ? (await readSiteLink(located.path))?.server : void 0;
|
|
5912
|
+
} catch {
|
|
5913
|
+
return void 0;
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
4492
5916
|
async function run(tool2, server2, op) {
|
|
4493
5917
|
try {
|
|
4494
5918
|
const { summary, data } = await op(await target(server2, tool2));
|
|
@@ -4500,18 +5924,29 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4500
5924
|
}
|
|
4501
5925
|
}
|
|
4502
5926
|
async function deployFolder(t2, dir, opts) {
|
|
5927
|
+
if (opts.access) await requireAccessLevels(t2);
|
|
5928
|
+
const link = opts.link ?? null;
|
|
5929
|
+
if (link && !opts.force || opts.base !== void 0) await requirePull(t2);
|
|
5930
|
+
if (opts.base !== void 0 && opts.folder !== void 0) {
|
|
5931
|
+
throw new CliError("Pass `base` or `path`, not both");
|
|
5932
|
+
}
|
|
5933
|
+
const folder = opts.folder === void 0 ? void 0 : await publishFolder2(t2, opts.folder);
|
|
5934
|
+
const shownFolder = folder ?? link?.path;
|
|
5935
|
+
const inFolder = (file) => shownFolder === void 0 ? file : `${shownFolder}/${file}`;
|
|
5936
|
+
const top = folder === void 0 ? "at the root" : `in /${folder}/`;
|
|
4503
5937
|
let local = await buildManifest(dir);
|
|
4504
5938
|
local.skipped.dotfiles.unshift(...opts.skippedDotfiles ?? []);
|
|
4505
5939
|
const skippedNote = describeSkipped(local.skipped).map((l) => `
|
|
4506
5940
|
${l}.`).join("");
|
|
4507
5941
|
let generatedIndex = false;
|
|
4508
|
-
|
|
5942
|
+
const asItIs = link !== null || opts.base !== void 0;
|
|
5943
|
+
if (!asItIs && !hasRootIndex(local)) {
|
|
4509
5944
|
const pages = htmlPages(local).map((f) => f.path);
|
|
4510
5945
|
if (pages.length > 0 && opts.listHtml === void 0) {
|
|
4511
5946
|
const shown = pages.slice(0, 20);
|
|
4512
5947
|
return {
|
|
4513
5948
|
kind: "needs_decision",
|
|
4514
|
-
summary: `Not published yet: there is no index.html
|
|
5949
|
+
summary: `Not published yet: there is no index.html ${top}, but there ${pages.length === 1 ? "is 1 HTML page" : `are ${pages.length} HTML pages`}. Ask the user whether to generate an index.html listing them (call again with \`listHtml: true\`) or to publish without one (\`listHtml: false\` \u2014 the root URL will 404, the pages still work at their own paths). Then call ${opts.retryWith} again with the same arguments plus \`listHtml\`.`,
|
|
4515
5950
|
data: {
|
|
4516
5951
|
status: "needs_decision",
|
|
4517
5952
|
htmlPages: shown,
|
|
@@ -4525,10 +5960,31 @@ ${l}.`).join("");
|
|
|
4525
5960
|
generatedIndex = true;
|
|
4526
5961
|
}
|
|
4527
5962
|
}
|
|
5963
|
+
const onBase = link && !opts.force ? onLink(link, local) : null;
|
|
5964
|
+
const changes = link ? onLink(link, local).changes : null;
|
|
4528
5965
|
const result = await deploy(
|
|
4529
5966
|
{ client: t2.client, fetch: fetchImpl },
|
|
4530
|
-
{
|
|
5967
|
+
{
|
|
5968
|
+
dir,
|
|
5969
|
+
name: opts.name,
|
|
5970
|
+
manifest: local,
|
|
5971
|
+
path: folder,
|
|
5972
|
+
...onBase ? {
|
|
5973
|
+
base: onBase.base,
|
|
5974
|
+
...onBase.partial ? { partial: true, remove: onBase.remove, within: onBase.within } : {}
|
|
5975
|
+
} : opts.base !== void 0 ? { base: opts.base, partial: true, remove: opts.remove ?? [] } : {},
|
|
5976
|
+
spa: opts.spa,
|
|
5977
|
+
...opts.access ? {
|
|
5978
|
+
access: opts.access,
|
|
5979
|
+
...needsAudience(opts.access) ? { audience: { emails: opts.emails ?? [], groups: opts.groups ?? [] } } : {}
|
|
5980
|
+
} : { public: opts.public }
|
|
5981
|
+
}
|
|
4531
5982
|
).catch((error) => {
|
|
5983
|
+
if (error instanceof BaseChangedError) {
|
|
5984
|
+
throw new CliError(
|
|
5985
|
+
`Not published: ${error.message}. ` + (link ? `Pull it again with fling_pull into another folder, redo the changes there and deploy that; or call ${opts.retryWith} again with \`force: true\` to replace what they published, only if the user says so.` : "Read the files again with fling_read_files and redo the changes on the new version.")
|
|
5986
|
+
);
|
|
5987
|
+
}
|
|
4532
5988
|
if (!(error instanceof NameTakenError)) throw error;
|
|
4533
5989
|
const how = opts.nameGiven ? "" : ` (generated from ${opts.nameFrom ?? "the folder"})`;
|
|
4534
5990
|
const pick = error.suggestions.length ? ` Free alternatives: ${error.suggestions.join(", ")}.` : "";
|
|
@@ -4536,23 +5992,30 @@ ${l}.`).join("");
|
|
|
4536
5992
|
`Not deployed: the site name "${error.siteName}"${how} is taken by someone else on ${serverLabel(t2.url)}.${pick} Ask the user which name to use (one of these or their own), then call ${opts.retryWith} again with \`name\`.`
|
|
4537
5993
|
);
|
|
4538
5994
|
});
|
|
4539
|
-
|
|
5995
|
+
if (link) await writeSiteLink(dir, linkAfter(link, result.versionId, local));
|
|
5996
|
+
const what = result.status === "unchanged" ? "Unchanged \u2014 already live" : result.status === "repointed" ? `Restored version ${result.versionId}` : changes && link ? `Deployed ${describeChanges(changes)} since version ${link.versionId}${link.path === void 0 ? "" : ` in /${link.path}/`} (uploaded ${result.uploaded})` : opts.base !== void 0 ? `Published ${result.fileCount} changed or added file${result.fileCount === 1 ? "" : "s"} on version ${opts.base} (the rest of the site kept as it was)` : `Deployed ${result.fileCount} files${folder === void 0 ? "" : ` to /${folder}/ (the rest of the site kept as it was)`} (uploaded ${result.uploaded})`;
|
|
4540
5997
|
const warnings = result.warnings.length ? `
|
|
4541
5998
|
Warnings:
|
|
4542
5999
|
- ${result.warnings.join("\n- ")}` : "";
|
|
4543
6000
|
const edge = result.status === "unchanged" ? { ready: true, note: "" } : await edgeReady(
|
|
4544
6001
|
result.url,
|
|
4545
|
-
local.files.map((f) => f.path)
|
|
6002
|
+
local.files.map((f) => inFolder(f.path))
|
|
4546
6003
|
);
|
|
6004
|
+
const url = shownFolder === void 0 ? result.url : new URL(`${shownFolder}/`, result.url).href;
|
|
4547
6005
|
const instance = await t2.instance();
|
|
4548
6006
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
4549
6007
|
const listing2 = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
|
|
6008
|
+
const level = result.site.access ?? result.site.visibility;
|
|
4550
6009
|
const head = review ? `${what}${listing2}.
|
|
4551
6010
|
${shareForFeedback(review, instance?.name ?? "Fling")}
|
|
4552
|
-
Site: ${
|
|
6011
|
+
Site: ${url} (${level})` : `${what}: ${url} (${level})${listing2}`;
|
|
6012
|
+
const who = describeSiteAccess(result.site, await membersOn(t2));
|
|
6013
|
+
const access3 = result.first ? `
|
|
6014
|
+
Who can open it: ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : opts.access ? `
|
|
6015
|
+
Its access is unchanged (deploying never changes it for an existing site): ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : "";
|
|
4553
6016
|
return {
|
|
4554
6017
|
kind: "deployed",
|
|
4555
|
-
summary: `${head}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
6018
|
+
summary: `${head}${access3}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
4556
6019
|
To check exactly which files the site has, call fling_files with name "${result.name}".`,
|
|
4557
6020
|
data: {
|
|
4558
6021
|
status: result.status,
|
|
@@ -4562,6 +6025,9 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
4562
6025
|
nameGenerated: !opts.nameGiven,
|
|
4563
6026
|
versionId: result.versionId,
|
|
4564
6027
|
site: result.site,
|
|
6028
|
+
...shownFolder === void 0 ? {} : { path: shownFolder, url },
|
|
6029
|
+
...changes && link ? { base: opts.force ? null : link.versionId, changes } : {},
|
|
6030
|
+
...result.first ? { created: true } : {},
|
|
4565
6031
|
...review ? { reviewUrl: review } : {},
|
|
4566
6032
|
generatedIndex,
|
|
4567
6033
|
warnings: result.warnings,
|
|
@@ -4574,42 +6040,65 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
4574
6040
|
{
|
|
4575
6041
|
instructions: mcpLocalInstructions(wording, {
|
|
4576
6042
|
comments: servers.some((s) => commentsEnabled(s.instance)),
|
|
6043
|
+
notifications: servers.some((s) => notificationsEnabled(s.instance)),
|
|
4577
6044
|
servers
|
|
4578
6045
|
})
|
|
4579
6046
|
}
|
|
4580
6047
|
);
|
|
4581
|
-
const { listHtml: listHtml2, spa: spa2, public: isPublic } = mcpTools2.publish.inputSchema;
|
|
6048
|
+
const { listHtml: listHtml2, spa: spa2, public: isPublic, access: access2, emails: emails2, groups: groups2 } = mcpTools2.publish.inputSchema;
|
|
4582
6049
|
server.registerTool(
|
|
4583
6050
|
"fling_deploy",
|
|
4584
6051
|
{
|
|
4585
6052
|
title: "Deploy a file or folder from this computer",
|
|
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.
|
|
6053
|
+
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. ${ACCESS_NOTE(wording)} ${PATH_PUBLISH_NOTE.replace("With `path`", "With `sitePath`")} A folder made by fling_pull publishes to the site and Fling it was pulled from, on the version pulled: only what changed is sent, and if someone published the site since, it stops (base_changed) rather than replace their work; \`force: true\` replaces it anyway (only when the user says so). ${NEVER_PUBLISHED}${note}`,
|
|
4587
6054
|
inputSchema: {
|
|
4588
|
-
path:
|
|
6055
|
+
path: z19.string().min(1).describe(
|
|
4589
6056
|
"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)"
|
|
4590
6057
|
),
|
|
4591
|
-
name:
|
|
6058
|
+
name: z19.string().optional().describe(
|
|
4592
6059
|
"Site name (lowercase letters, digits, single hyphens; max 50). Defaults to a name generated from the file or folder"
|
|
4593
6060
|
),
|
|
6061
|
+
sitePath: z19.string().min(1).max(1024).optional().describe(
|
|
6062
|
+
'Publish into this folder of the site only, e.g. "30-10-26" (served at <site>/30-10-26/), keeping every other file of the site. Needs `name`. Omit to replace the whole site'
|
|
6063
|
+
),
|
|
4594
6064
|
spa: spa2,
|
|
6065
|
+
access: access2,
|
|
6066
|
+
emails: emails2,
|
|
6067
|
+
groups: groups2,
|
|
4595
6068
|
public: isPublic,
|
|
4596
6069
|
listHtml: listHtml2,
|
|
6070
|
+
force: z19.boolean().optional().describe(
|
|
6071
|
+
"A folder made by fling_pull: publish it without a base, replacing whatever is live even if someone published since. Only when the user says so"
|
|
6072
|
+
),
|
|
4597
6073
|
server: serverArg
|
|
4598
6074
|
},
|
|
4599
6075
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
4600
6076
|
},
|
|
4601
|
-
(args) => run("fling_deploy", args.server, async (t2) => {
|
|
6077
|
+
async (args) => run("fling_deploy", args.server ?? await linkedServer(args.path), async (t2) => {
|
|
4602
6078
|
const located = await resolveInput(args.path, locate);
|
|
6079
|
+
if (args.sitePath !== void 0 && args.name === void 0) {
|
|
6080
|
+
throw new CliError(
|
|
6081
|
+
"`sitePath` needs `name`: the site to publish into. Ask the user which site, then call fling_deploy again"
|
|
6082
|
+
);
|
|
6083
|
+
}
|
|
4603
6084
|
const prepared = await prepareInput(located.path);
|
|
4604
6085
|
try {
|
|
4605
|
-
const
|
|
6086
|
+
const found = prepared.kind === "folder" ? await readSiteLink(prepared.dir) : null;
|
|
6087
|
+
const link = found && (args.name === void 0 || args.name === found.name) && args.sitePath === void 0 && found.server === t2.url ? found : null;
|
|
6088
|
+
const name = link ? link.name : args.name !== void 0 ? resolveSiteName(prepared.dir, args.name) : prepared.defaultName;
|
|
4606
6089
|
const outcome = await deployFolder(t2, prepared.dir, {
|
|
6090
|
+
link,
|
|
6091
|
+
force: args.force,
|
|
4607
6092
|
name,
|
|
4608
|
-
nameGiven: args.name !== void 0,
|
|
6093
|
+
nameGiven: args.name !== void 0 || link !== null,
|
|
4609
6094
|
nameFrom: prepared.kind === "folder" ? "the folder" : "the file name",
|
|
4610
6095
|
spa: args.spa,
|
|
4611
6096
|
public: args.public,
|
|
6097
|
+
access: args.access,
|
|
6098
|
+
emails: args.emails,
|
|
6099
|
+
groups: args.groups,
|
|
4612
6100
|
listHtml: args.listHtml,
|
|
6101
|
+
folder: args.sitePath,
|
|
4613
6102
|
retryWith: "fling_deploy",
|
|
4614
6103
|
skippedDotfiles: prepared.skippedDotfiles
|
|
4615
6104
|
});
|
|
@@ -4632,15 +6121,97 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
4632
6121
|
}
|
|
4633
6122
|
})
|
|
4634
6123
|
);
|
|
6124
|
+
server.registerTool(
|
|
6125
|
+
"fling_pull",
|
|
6126
|
+
{
|
|
6127
|
+
title: "Download a site's files to change them",
|
|
6128
|
+
description: `Download a site's files into a folder on this computer, to change some or all of them and publish again: by default ~/Documents/Claude/fling/<name>. Then edit the files there and call fling_deploy with the folder: it publishes on the version pulled, sending only what changed. Anyone who can open the site may pull it; publishing needs an owner or publisher. \`path\` pulls one folder of the site (its files at the top of the folder); deploying it keeps the rest of the site. If the folder is not empty it stops, unless \`force\` (it then writes over what is there). When fling_deploy says base_changed, someone published in between: pull again into another folder and redo the changes there.` + note,
|
|
6129
|
+
inputSchema: {
|
|
6130
|
+
name: z19.string().min(1).max(200).describe("Site name"),
|
|
6131
|
+
dir: z19.string().min(1).optional().describe(
|
|
6132
|
+
"Folder to write the files to (absolute, or relative to the working directory). Default ~/Documents/Claude/fling/<name>"
|
|
6133
|
+
),
|
|
6134
|
+
versionId: z19.string().min(1).max(100).optional().describe(
|
|
6135
|
+
"Another version than the live one (owners and publishers; ids from fling_info)"
|
|
6136
|
+
),
|
|
6137
|
+
path: z19.string().min(1).max(1024).optional().describe('Only this folder of the site, e.g. "30-10-26"'),
|
|
6138
|
+
force: z19.boolean().optional().describe("Write into the folder even if it is not empty"),
|
|
6139
|
+
server: serverArg
|
|
6140
|
+
},
|
|
6141
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
6142
|
+
},
|
|
6143
|
+
(args) => run("fling_pull", args.server, async (t2) => {
|
|
6144
|
+
await requirePull(t2);
|
|
6145
|
+
const path3 = args.path === void 0 ? void 0 : normalizePublishPath(args.path);
|
|
6146
|
+
if (path3 === null) {
|
|
6147
|
+
throw new CliError(
|
|
6148
|
+
`Invalid path "${args.path}": use a folder of the site, like "30-10-26" (no dotfiles)`
|
|
6149
|
+
);
|
|
6150
|
+
}
|
|
6151
|
+
const dir = args.dir === void 0 ? join7(locate.home, "Documents", "Claude", "fling", args.name) : resolvePath(cwd, args.dir);
|
|
6152
|
+
const pulled = await pullSite(
|
|
6153
|
+
{ client: t2.client, fetch: fetchImpl },
|
|
6154
|
+
{
|
|
6155
|
+
name: args.name,
|
|
6156
|
+
dir,
|
|
6157
|
+
...args.versionId ? { versionId: args.versionId } : {},
|
|
6158
|
+
...path3 === void 0 ? {} : { path: path3 },
|
|
6159
|
+
force: args.force
|
|
6160
|
+
}
|
|
6161
|
+
);
|
|
6162
|
+
return {
|
|
6163
|
+
summary: `Pulled ${pulled.fileCount} file(s) of ${pulled.name}${path3 ? ` /${path3}/` : ""} (version ${pulled.versionId}, ${formatBytes(pulled.totalBytes)}) into ${pulled.dir}. Edit the files there, then call fling_deploy with path "${pulled.dir}": it publishes on version ${pulled.versionId}.`,
|
|
6164
|
+
data: pulled
|
|
6165
|
+
};
|
|
6166
|
+
})
|
|
6167
|
+
);
|
|
6168
|
+
server.registerTool(
|
|
6169
|
+
mcpTools2.readFiles.name,
|
|
6170
|
+
serverTool(mcpTools2.readFiles),
|
|
6171
|
+
(args) => run(mcpTools2.readFiles.name, args.server, async (t2) => {
|
|
6172
|
+
await requirePull(t2);
|
|
6173
|
+
const path3 = args.path === void 0 ? void 0 : normalizePublishPath(args.path);
|
|
6174
|
+
if (path3 === null) throw new CliError(`Invalid path "${args.path}"`);
|
|
6175
|
+
const pulled = await t2.client.request(
|
|
6176
|
+
"GET",
|
|
6177
|
+
`/api/sites/${encodeURIComponent(args.name)}/pull`,
|
|
6178
|
+
{ schema: PullResponseSchema, query: { version: args.versionId, path: path3 } }
|
|
6179
|
+
);
|
|
6180
|
+
const { page, tooLarge, missing, nextCursor } = pageOfFiles(pulled.files, {
|
|
6181
|
+
paths: args.paths,
|
|
6182
|
+
cursor: args.cursor,
|
|
6183
|
+
budget: MCP_READ_MAX_BYTES
|
|
6184
|
+
});
|
|
6185
|
+
const read = await Promise.all(
|
|
6186
|
+
page.map(async ({ url, ...f }) => {
|
|
6187
|
+
const res = await fetchImpl(url);
|
|
6188
|
+
if (!res.ok) throw new CliError(`Could not read ${f.path}: HTTP ${res.status}`);
|
|
6189
|
+
return { ...f, ...encodeForAgent(f.path, new Uint8Array(await res.arrayBuffer())) };
|
|
6190
|
+
})
|
|
6191
|
+
);
|
|
6192
|
+
const files = [
|
|
6193
|
+
...read,
|
|
6194
|
+
...tooLarge.map(({ url: _url, ...f }) => ({ ...f, tooLarge: true }))
|
|
6195
|
+
].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
6196
|
+
const result = {
|
|
6197
|
+
name: args.name,
|
|
6198
|
+
versionId: pulled.versionId,
|
|
6199
|
+
files,
|
|
6200
|
+
missing,
|
|
6201
|
+
...nextCursor ? { nextCursor } : {}
|
|
6202
|
+
};
|
|
6203
|
+
return { summary: describeReadFiles(args.name, result), data: result };
|
|
6204
|
+
})
|
|
6205
|
+
);
|
|
4635
6206
|
server.registerTool(
|
|
4636
6207
|
"fling_find_files",
|
|
4637
6208
|
{
|
|
4638
6209
|
title: "Find files to fling",
|
|
4639
6210
|
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.",
|
|
4640
6211
|
inputSchema: {
|
|
4641
|
-
query:
|
|
4642
|
-
limit:
|
|
4643
|
-
days:
|
|
6212
|
+
query: z19.string().optional().describe('Only names containing this text (case-insensitive), e.g. "deck"'),
|
|
6213
|
+
limit: z19.number().int().min(1).max(100).optional().describe("Max results (default 20)"),
|
|
6214
|
+
days: z19.number().int().min(1).max(3650).optional().describe("Only things modified in the last N days (default 14)")
|
|
4644
6215
|
},
|
|
4645
6216
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
4646
6217
|
},
|
|
@@ -4678,7 +6249,12 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4678
6249
|
),
|
|
4679
6250
|
(args) => run(publishDef.name, args.server, async (t2) => {
|
|
4680
6251
|
const name = resolveSiteName(".", args.name);
|
|
4681
|
-
|
|
6252
|
+
if (args.remove?.length && args.base === void 0) {
|
|
6253
|
+
throw new CliError("`remove` needs `base`: the versionId fling_read_files returned");
|
|
6254
|
+
}
|
|
6255
|
+
if (args.files.length === 0 && !args.remove?.length)
|
|
6256
|
+
throw new CliError("No files to publish");
|
|
6257
|
+
const tmp = await mkdtemp3(join7(tmpdir3(), "fling-publish-"));
|
|
4682
6258
|
try {
|
|
4683
6259
|
const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
|
|
4684
6260
|
return await deployFolder(t2, tmp, {
|
|
@@ -4687,11 +6263,16 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4687
6263
|
nameGiven: true,
|
|
4688
6264
|
spa: args.spa,
|
|
4689
6265
|
public: args.public,
|
|
6266
|
+
access: args.access,
|
|
6267
|
+
emails: args.emails,
|
|
6268
|
+
groups: args.groups,
|
|
4690
6269
|
listHtml: args.listHtml,
|
|
6270
|
+
folder: args.path,
|
|
6271
|
+
...args.base === void 0 ? {} : { base: args.base, remove: args.remove },
|
|
4691
6272
|
retryWith: "fling_publish"
|
|
4692
6273
|
});
|
|
4693
6274
|
} finally {
|
|
4694
|
-
await
|
|
6275
|
+
await rm3(tmp, { recursive: true, force: true });
|
|
4695
6276
|
}
|
|
4696
6277
|
})
|
|
4697
6278
|
);
|
|
@@ -4701,8 +6282,8 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4701
6282
|
...serverTool(mcpTools2.checkName),
|
|
4702
6283
|
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}`,
|
|
4703
6284
|
inputSchema: {
|
|
4704
|
-
name:
|
|
4705
|
-
path:
|
|
6285
|
+
name: z19.string().min(1).max(200).optional().describe("Site name to check"),
|
|
6286
|
+
path: z19.string().optional().describe("File or folder whose generated default name to check (path or file name)"),
|
|
4706
6287
|
server: serverArg
|
|
4707
6288
|
}
|
|
4708
6289
|
},
|
|
@@ -4716,8 +6297,8 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4716
6297
|
name = defaultNameFor(source.path, source.isDirectory);
|
|
4717
6298
|
}
|
|
4718
6299
|
const check2 = await checkName(t2.client, name);
|
|
4719
|
-
const
|
|
4720
|
-
const url =
|
|
6300
|
+
const base2 = check2.status === "invalid" ? null : sitesBaseOf(await t2.instance());
|
|
6301
|
+
const url = base2 ? siteUrlFor(name, base2) : null;
|
|
4721
6302
|
const suggestions = check2.suggestions.length ? ` Suggestions: ${check2.suggestions.join(", ")}.` : "";
|
|
4722
6303
|
const summary = {
|
|
4723
6304
|
available: `"${name}" is available${url ? ` \u2192 ${url}` : ""}.`,
|
|
@@ -4743,7 +6324,7 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
4743
6324
|
title: "Sign in to Fling",
|
|
4744
6325
|
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,
|
|
4745
6326
|
inputSchema: {
|
|
4746
|
-
server:
|
|
6327
|
+
server: z19.string().min(1).max(500).optional().describe(
|
|
4747
6328
|
`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"}`}`
|
|
4748
6329
|
)
|
|
4749
6330
|
},
|
|
@@ -4827,7 +6408,7 @@ ${known.join("\n")}` : ""
|
|
|
4827
6408
|
title: "List the Flings on this computer",
|
|
4828
6409
|
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.`,
|
|
4829
6410
|
inputSchema: {
|
|
4830
|
-
use:
|
|
6411
|
+
use: z19.string().min(1).max(500).optional().describe(
|
|
4831
6412
|
"Make this Fling current (address, host or unique part of it), like `fling use`"
|
|
4832
6413
|
)
|
|
4833
6414
|
},
|
|
@@ -4892,18 +6473,21 @@ ${lines}` : ""}`,
|
|
|
4892
6473
|
return { summary: `${sites.length} site(s) on ${serverLabel(url)}`, data: { sites } };
|
|
4893
6474
|
})
|
|
4894
6475
|
);
|
|
4895
|
-
const membersOn = async (target2) => {
|
|
4896
|
-
const instance = await target2.instance();
|
|
4897
|
-
return instance ? privateAudience(instance) : "anyone signed in to this Fling";
|
|
4898
|
-
};
|
|
4899
6476
|
server.registerTool(
|
|
4900
6477
|
t.info.name,
|
|
4901
6478
|
serverTool(t.info),
|
|
4902
6479
|
(args) => run(t.info.name, args.server, async (target2) => {
|
|
4903
6480
|
const site = await getSite(target2.client, args.name);
|
|
4904
|
-
const
|
|
6481
|
+
const access3 = site.audience ? `
|
|
4905
6482
|
${describeAudience(site, await membersOn(target2))}` : "";
|
|
4906
|
-
|
|
6483
|
+
const current = site.versions.find((v) => v.id === site.currentVersionId);
|
|
6484
|
+
const by = current?.publishedBy ? ` by ${current.publishedBy}` : "";
|
|
6485
|
+
const to = current?.path ? ` to /${current.path}/` : "";
|
|
6486
|
+
const published = by || to ? `
|
|
6487
|
+
Current version published${by}${to}` : "";
|
|
6488
|
+
const roles = site.owners ? `
|
|
6489
|
+
${describeRoles(site)}` : "";
|
|
6490
|
+
return { summary: `${site.name}: ${site.url}${access3}${roles}${published}`, data: site };
|
|
4907
6491
|
})
|
|
4908
6492
|
);
|
|
4909
6493
|
server.registerTool(
|
|
@@ -4939,6 +6523,24 @@ ${describeAudience(site, await membersOn(target2))}` : "";
|
|
|
4939
6523
|
};
|
|
4940
6524
|
})
|
|
4941
6525
|
);
|
|
6526
|
+
server.registerTool(
|
|
6527
|
+
t.setAccess.name,
|
|
6528
|
+
serverTool(t.setAccess),
|
|
6529
|
+
(args) => run(t.setAccess.name, args.server, async (target2) => {
|
|
6530
|
+
await requireAccessLevels(target2);
|
|
6531
|
+
const site = await setAccess(
|
|
6532
|
+
target2.client,
|
|
6533
|
+
args.name,
|
|
6534
|
+
args.access,
|
|
6535
|
+
needsAudience(args.access) ? { emails: args.emails ?? [], groups: args.groups ?? [] } : void 0
|
|
6536
|
+
);
|
|
6537
|
+
const instance = await target2.instance();
|
|
6538
|
+
return {
|
|
6539
|
+
summary: `${site.name}: ${describeAudience(site, await membersOn(target2))}${onFling(target2.url, instance)}`,
|
|
6540
|
+
data: site
|
|
6541
|
+
};
|
|
6542
|
+
})
|
|
6543
|
+
);
|
|
4942
6544
|
server.registerTool(
|
|
4943
6545
|
t.setVisibility.name,
|
|
4944
6546
|
serverTool(t.setVisibility),
|
|
@@ -4980,6 +6582,36 @@ ${describeAudience(site, await membersOn(target2))}` : "";
|
|
|
4980
6582
|
})
|
|
4981
6583
|
);
|
|
4982
6584
|
}
|
|
6585
|
+
for (const [def, which] of [
|
|
6586
|
+
[t.setOwners, "owners"],
|
|
6587
|
+
[t.setPublishers, "publishers"]
|
|
6588
|
+
]) {
|
|
6589
|
+
server.registerTool(
|
|
6590
|
+
def.name,
|
|
6591
|
+
serverTool(def),
|
|
6592
|
+
(args) => run(def.name, args.server, async (target2) => {
|
|
6593
|
+
const instance = await requireSiteRoles(target2);
|
|
6594
|
+
const change = {
|
|
6595
|
+
add: args.add,
|
|
6596
|
+
remove: args.remove,
|
|
6597
|
+
set: args.set,
|
|
6598
|
+
addGroups: args.addGroups,
|
|
6599
|
+
removeGroups: args.removeGroups,
|
|
6600
|
+
setGroups: args.setGroups
|
|
6601
|
+
};
|
|
6602
|
+
if (!changesAudience(change)) {
|
|
6603
|
+
throw new CliError(
|
|
6604
|
+
"Pass `add`, `remove`, `set`, `addGroups`, `removeGroups` or `setGroups`"
|
|
6605
|
+
);
|
|
6606
|
+
}
|
|
6607
|
+
const site = await setRoles(target2.client, args.name, which, change);
|
|
6608
|
+
return {
|
|
6609
|
+
summary: `${site.name}: ${describeRoles(site)}${onFling(target2.url, instance)}`,
|
|
6610
|
+
data: site
|
|
6611
|
+
};
|
|
6612
|
+
})
|
|
6613
|
+
);
|
|
6614
|
+
}
|
|
4983
6615
|
if (servers.some((s) => groupsEnabled(s.instance))) {
|
|
4984
6616
|
const withGroups = async (target2) => {
|
|
4985
6617
|
const instance = await target2.instance();
|
|
@@ -4993,15 +6625,15 @@ ${describeAudience(site, await membersOn(target2))}` : "";
|
|
|
4993
6625
|
serverTool(t.groups),
|
|
4994
6626
|
(args) => run(t.groups.name, args.server, async (target2) => {
|
|
4995
6627
|
const instance = await withGroups(target2);
|
|
4996
|
-
const [
|
|
6628
|
+
const [groups3, me] = await Promise.all([
|
|
4997
6629
|
listGroups(target2.client, { mine: args.mine, members: args.members }),
|
|
4998
6630
|
whoami(target2.client)
|
|
4999
6631
|
]);
|
|
5000
6632
|
const which = args.members ? " of exactly those people" : "";
|
|
5001
6633
|
return {
|
|
5002
|
-
summary: args.members &&
|
|
5003
|
-
${describeGroups(
|
|
5004
|
-
data: { groups:
|
|
6634
|
+
summary: args.members && groups3.length === 0 ? `No group has exactly those people${onFling(target2.url, instance)}` : `${groups3.length} ${args.mine ? "of your " : ""}group(s)${which}${onFling(target2.url, instance)}
|
|
6635
|
+
${describeGroups(groups3, me.email)}`,
|
|
6636
|
+
data: { groups: groups3 }
|
|
5005
6637
|
};
|
|
5006
6638
|
})
|
|
5007
6639
|
);
|
|
@@ -5157,15 +6789,54 @@ ${describeGroups(groups2, me.email)}`,
|
|
|
5157
6789
|
})
|
|
5158
6790
|
);
|
|
5159
6791
|
}
|
|
6792
|
+
if (servers.some((s) => notificationsEnabled(s.instance))) {
|
|
6793
|
+
const withNotifications = async (target2) => {
|
|
6794
|
+
const instance = await target2.instance();
|
|
6795
|
+
const name = `${instance?.name ?? "This Fling"} (${serverLabel(target2.url)})`;
|
|
6796
|
+
if (instance && !notificationsEnabled(instance)) {
|
|
6797
|
+
throw new CliError(`${name} has no notifications`);
|
|
6798
|
+
}
|
|
6799
|
+
return { name, fling: onFling(target2.url, instance) };
|
|
6800
|
+
};
|
|
6801
|
+
server.registerTool(
|
|
6802
|
+
t.notifications.name,
|
|
6803
|
+
serverTool(t.notifications),
|
|
6804
|
+
(args) => run(t.notifications.name, args.server, async (target2) => {
|
|
6805
|
+
const { name, fling } = await withNotifications(target2);
|
|
6806
|
+
const settings = await getNotifications(target2.client, name);
|
|
6807
|
+
return { summary: `${describeNotificationSettings(settings)}${fling}`, data: settings };
|
|
6808
|
+
})
|
|
6809
|
+
);
|
|
6810
|
+
server.registerTool(
|
|
6811
|
+
t.notificationsSet.name,
|
|
6812
|
+
serverTool(t.notificationsSet),
|
|
6813
|
+
(args) => run(t.notificationsSet.name, args.server, async (target2) => {
|
|
6814
|
+
const { name, fling } = await withNotifications(target2);
|
|
6815
|
+
if (args.muted === void 0 && !args.cells?.length) {
|
|
6816
|
+
throw new CliError("Pass `muted` or `cells`");
|
|
6817
|
+
}
|
|
6818
|
+
const settings = await setNotifications(
|
|
6819
|
+
target2.client,
|
|
6820
|
+
{ muted: args.muted, cells: args.cells },
|
|
6821
|
+
name
|
|
6822
|
+
);
|
|
6823
|
+
return {
|
|
6824
|
+
summary: `Saved.
|
|
6825
|
+
${describeNotificationSettings(settings)}${fling}`,
|
|
6826
|
+
data: settings
|
|
6827
|
+
};
|
|
6828
|
+
})
|
|
6829
|
+
);
|
|
6830
|
+
}
|
|
5160
6831
|
server.registerTool(
|
|
5161
6832
|
t.whoami.name,
|
|
5162
6833
|
serverTool(t.whoami),
|
|
5163
6834
|
(args) => run(t.whoami.name, args.server, async (target2) => {
|
|
5164
6835
|
const me = await whoami(target2.client);
|
|
5165
6836
|
const instance = await target2.instance();
|
|
5166
|
-
const
|
|
6837
|
+
const base2 = sitesBaseOf(instance);
|
|
5167
6838
|
return {
|
|
5168
|
-
summary: `Signed in to ${serverLabel(target2.url)}${instance ? ` (${instance.name})` : ""} as ${me.email} (sites are served at ${sitesPattern(
|
|
6839
|
+
summary: `Signed in to ${serverLabel(target2.url)}${instance ? ` (${instance.name})` : ""} as ${me.email} (sites are served at ${sitesPattern(base2)})`,
|
|
5169
6840
|
data: { ...me, server: target2.url }
|
|
5170
6841
|
};
|
|
5171
6842
|
})
|
|
@@ -5196,9 +6867,9 @@ async function writeInlineFiles(dir, files) {
|
|
|
5196
6867
|
total += bytes.length;
|
|
5197
6868
|
if (total > MAX_DEPLOY_BYTES)
|
|
5198
6869
|
throw new CliError(`The site is larger than ${formatBytes(MAX_DEPLOY_BYTES)}`);
|
|
5199
|
-
const target =
|
|
5200
|
-
await
|
|
5201
|
-
await
|
|
6870
|
+
const target = join7(dir, ...path3.split("/"));
|
|
6871
|
+
await mkdir4(dirname3(target), { recursive: true });
|
|
6872
|
+
await writeFile5(target, bytes);
|
|
5202
6873
|
}
|
|
5203
6874
|
if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
|
|
5204
6875
|
return { skippedDotfiles };
|
|
@@ -5242,10 +6913,10 @@ async function runMcpServer(deps = {}) {
|
|
|
5242
6913
|
}
|
|
5243
6914
|
|
|
5244
6915
|
// src/route.ts
|
|
5245
|
-
import { stat as
|
|
5246
|
-
import { resolve as
|
|
6916
|
+
import { stat as stat7 } from "node:fs/promises";
|
|
6917
|
+
import { resolve as resolve5 } from "node:path";
|
|
5247
6918
|
var PASS_THROUGH = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);
|
|
5248
|
-
var pathExists = (path3) =>
|
|
6919
|
+
var pathExists = (path3) => stat7(resolve5(path3)).then(
|
|
5249
6920
|
() => true,
|
|
5250
6921
|
() => false
|
|
5251
6922
|
);
|
|
@@ -5296,18 +6967,18 @@ program.name(BIN_NAME).description("Fling a folder of static files at Fling and
|
|
|
5296
6967
|
`server for this command: a URL, or the host of one you signed in to (default: $${ENV.url}, else the current one)`
|
|
5297
6968
|
).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
|
|
5298
6969
|
sortSubcommands: false,
|
|
5299
|
-
styleTitle: (title) =>
|
|
5300
|
-
styleCommandText: (text2) =>
|
|
6970
|
+
styleTitle: (title) => chalk13.bold(brand.primary(title)),
|
|
6971
|
+
styleCommandText: (text2) => chalk13.bold(text2),
|
|
5301
6972
|
styleSubcommandText: (text2) => brand.accent(text2),
|
|
5302
6973
|
styleOptionText: (text2) => brand.accent(text2),
|
|
5303
|
-
styleArgumentText: (text2) =>
|
|
6974
|
+
styleArgumentText: (text2) => chalk13.dim(text2)
|
|
5304
6975
|
}).addHelpText(
|
|
5305
6976
|
"before",
|
|
5306
6977
|
() => process.stdout.isTTY ? logo({ columns: process.stdout.columns }) : ""
|
|
5307
6978
|
).addHelpText(
|
|
5308
6979
|
"after",
|
|
5309
6980
|
() => `
|
|
5310
|
-
${
|
|
6981
|
+
${chalk13.bold(brand.primary("Examples:"))}
|
|
5311
6982
|
$ ${BIN_NAME} login https://fling.example.com sign in to your Fling (once; there is no default)
|
|
5312
6983
|
$ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
|
|
5313
6984
|
$ ${BIN_NAME} use list the Flings you signed in to; \`use <url>\` switches
|
|
@@ -5318,7 +6989,7 @@ ${chalk11.bold(brand.primary("Examples:"))}
|
|
|
5318
6989
|
$ ${BIN_NAME} site.zip a zip of a site
|
|
5319
6990
|
$ ${BIN_NAME} share my-app public link for people who can't sign in
|
|
5320
6991
|
|
|
5321
|
-
${
|
|
6992
|
+
${chalk13.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
|
|
5322
6993
|
Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`)}`
|
|
5323
6994
|
);
|
|
5324
6995
|
function action(handler) {
|
|
@@ -5362,17 +7033,36 @@ program.command("deploy [path]").helpGroup("Deploy:").description(
|
|
|
5362
7033
|
).option(
|
|
5363
7034
|
"-n, --name <name>",
|
|
5364
7035
|
"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)"
|
|
7036
|
+
).option(
|
|
7037
|
+
"--path <folder>",
|
|
7038
|
+
"publish into this folder of the site (e.g. 30-10-26, served at <site>/30-10-26/) and keep the rest of the site; needs --name"
|
|
5365
7039
|
).option("--spa", "single-page app: serve index.html for unknown paths").option(
|
|
7040
|
+
"--access <level>",
|
|
7041
|
+
"who can open a new site: only_me, members (everyone signed in), groups, people or public (default: the Fling's, usually only_me). A deployed site keeps its access: see `access`"
|
|
7042
|
+
).option("--add <emails...>", "with --access people: the addresses that may open it").option("--group <slugs...>", "with --access groups: the groups whose members may open it").option(
|
|
5366
7043
|
"--public",
|
|
5367
|
-
"make the site public: anyone can open it, reviewing still needs sign-in (
|
|
7044
|
+
"make the site public: anyone can open it, reviewing still needs sign-in (on every deploy; for a new site prefer --access public)"
|
|
5368
7045
|
).option(
|
|
5369
7046
|
"-y, --yes",
|
|
5370
7047
|
"never prompt: accept the generated name; without index.html, list the HTML pages (or fling anyway if there are none)"
|
|
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)").
|
|
7048
|
+
).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)").option(
|
|
7049
|
+
"--force",
|
|
7050
|
+
"a pulled folder: publish it without a base, replacing whatever is live (even if someone published since you pulled)"
|
|
7051
|
+
).action(
|
|
5372
7052
|
action(
|
|
5373
7053
|
(ctx, path3, opts) => runDeploy(ctx, path3, opts)
|
|
5374
7054
|
)
|
|
5375
7055
|
);
|
|
7056
|
+
program.command("pull <name> [dir]").helpGroup("Deploy:").description(
|
|
7057
|
+
"download a site's files into dir (default: ./<name>) to change them; `deploy` in that folder publishes on the version pulled"
|
|
7058
|
+
).option(
|
|
7059
|
+
"--version-id <id>",
|
|
7060
|
+
"another version than the live one (owners and publishers; ids from `versions`)"
|
|
7061
|
+
).option("--path <folder>", "only this folder of the site (e.g. 30-10-26), at the top of dir").option("--force", "write into dir even if it is not empty").action(
|
|
7062
|
+
action(
|
|
7063
|
+
(ctx, name, dir, opts) => runPull(ctx, name, dir, opts)
|
|
7064
|
+
)
|
|
7065
|
+
);
|
|
5376
7066
|
program.command("setup [url]").helpGroup("Setup:").description(
|
|
5377
7067
|
"sign in (to the Fling at url, which becomes current), then add the Fling plugin to Claude Code and the extension to Claude Desktop"
|
|
5378
7068
|
).option("-y, --yes", "do every step without asking").option("-s, --scope <scope>", "Claude Code plugin scope: user, project or local", "user").action(
|
|
@@ -5422,11 +7112,43 @@ program.command("share <name>").helpGroup("Sites:").description("print (or creat
|
|
|
5422
7112
|
program.command("visibility <name> <visibility>").helpGroup("Sites:").description(
|
|
5423
7113
|
"private (sign-in required) or public (anyone can open the site; reviewing it still needs sign-in)"
|
|
5424
7114
|
).action(action((ctx, name, visibility) => runVisibility(ctx, name, visibility)));
|
|
5425
|
-
program.command("access <name>").helpGroup("Sites:").description(
|
|
7115
|
+
program.command("access <name>").helpGroup("Sites:").description(
|
|
7116
|
+
"show who can open a site, or change it: only you, everyone signed in, listed people and groups, or anyone"
|
|
7117
|
+
).addOption(new Option("--only-me", "only you can open it").conflicts(["members", "public"])).addOption(new Option("--members", "everyone signed in can open it").conflicts("public")).addOption(
|
|
7118
|
+
new Option("--public", "anyone can open it; reviewing still needs sign-in").conflicts([
|
|
7119
|
+
"add",
|
|
7120
|
+
"group",
|
|
7121
|
+
"clear"
|
|
7122
|
+
])
|
|
7123
|
+
).option(
|
|
7124
|
+
"--add <emails...>",
|
|
7125
|
+
"let these addresses in (comma-separated or repeated); where the Fling invites by sharing, an address from outside is invited as a reviewer"
|
|
7126
|
+
).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
7127
|
action(
|
|
5427
7128
|
(ctx, name, opts) => runAccess(ctx, name, opts)
|
|
5428
7129
|
)
|
|
5429
7130
|
);
|
|
7131
|
+
for (const [which, does] of [
|
|
7132
|
+
[
|
|
7133
|
+
"owners",
|
|
7134
|
+
"own it: publish, change its access and settings, its owners and publishers, delete it"
|
|
7135
|
+
],
|
|
7136
|
+
[
|
|
7137
|
+
"publishers",
|
|
7138
|
+
"publish it and roll it back, but can't change its access or settings or delete it"
|
|
7139
|
+
]
|
|
7140
|
+
]) {
|
|
7141
|
+
program.command(`${which} <name>`).helpGroup("Sites:").description(
|
|
7142
|
+
`show who ${which === "owners" ? "owns" : "publishes"} a site, or change it (${which} ${does})`
|
|
7143
|
+
).option("--add <emails...>", `add these addresses (members of this Fling only)`).option("--remove <emails...>", "take these addresses out").option("--set <emails...>", `make these addresses the whole list (groups stay)`).option(
|
|
7144
|
+
"--group <slugs...>",
|
|
7145
|
+
"add these groups (see `fling groups`); whoever manages a group decides who is in it"
|
|
7146
|
+
).option("--remove-group <slugs...>", "take these groups out").action(
|
|
7147
|
+
action(
|
|
7148
|
+
(ctx, name, opts) => runRoles(ctx, which, name, opts)
|
|
7149
|
+
)
|
|
7150
|
+
);
|
|
7151
|
+
}
|
|
5430
7152
|
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)));
|
|
5431
7153
|
program.command("unarchive <name>").helpGroup("Sites:").description("bring an archived site back").action(action((ctx, name) => runArchive(ctx, name, true)));
|
|
5432
7154
|
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)));
|
|
@@ -5470,6 +7192,16 @@ program.command("uncomment <name> <id>").helpGroup("Comments:").alias("delete-co
|
|
|
5470
7192
|
(ctx, name, id, opts) => runUncomment(ctx, name, id, opts)
|
|
5471
7193
|
)
|
|
5472
7194
|
);
|
|
7195
|
+
var notifications = program.command("notifications").helpGroup("Notifications:").description("what this Fling tells you about, and how (default: show the table)");
|
|
7196
|
+
notifications.command("show", { isDefault: true }).description("show each event and channel: on or off, * where you chose it").action(action((ctx) => runNotifications(ctx)));
|
|
7197
|
+
notifications.command("set <event> <channel> <value>").description("turn one event on or off on a channel, or back to its default (on|off|default)").action(
|
|
7198
|
+
action(
|
|
7199
|
+
(ctx, event, channel, value) => runNotificationsSet(ctx, event, channel, value)
|
|
7200
|
+
)
|
|
7201
|
+
);
|
|
7202
|
+
notifications.command("mute").description("stop all notifications (your choices are kept)").action(action((ctx) => runNotificationsMute(ctx, true)));
|
|
7203
|
+
notifications.command("unmute").description("turn notifications back on").action(action((ctx) => runNotificationsMute(ctx, false)));
|
|
7204
|
+
notifications.command("sent").description("list what was sent to you in the last 30 days").action(action((ctx) => runNotificationsSent(ctx)));
|
|
5473
7205
|
program.command("mcp").helpGroup("Agents:").description("run the MCP server on stdio (used by the Claude Code plugin)").action(async (_opts, command) => {
|
|
5474
7206
|
const globals = command.optsWithGlobals();
|
|
5475
7207
|
await runMcpServer({ server: globals.server });
|
|
@@ -5492,4 +7224,3 @@ async function main() {
|
|
|
5492
7224
|
await program.parseAsync(route.args, { from: "user" });
|
|
5493
7225
|
}
|
|
5494
7226
|
await main();
|
|
5495
|
-
//# sourceMappingURL=cli.js.map
|