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