@nearform/fling 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -25
- package/dist/cli.js +2204 -388
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1289,6 +1289,31 @@ import { z as z4 } from "zod";
|
|
|
1289
1289
|
|
|
1290
1290
|
// ../../packages/shared/src/comments.ts
|
|
1291
1291
|
import { z as z3 } from "zod";
|
|
1292
|
+
|
|
1293
|
+
// ../../packages/shared/src/markdown-path.ts
|
|
1294
|
+
var MARKDOWN_EXTENSION = /\.(md|markdown)$/i;
|
|
1295
|
+
var isMarkdownPath = (path3) => MARKDOWN_EXTENSION.test(path3);
|
|
1296
|
+
var HOME_PAGE_NAMES = [/^index\.(md|markdown)$/i, /^readme\.(md|markdown)$/i];
|
|
1297
|
+
function markdownHomePages(files) {
|
|
1298
|
+
const folders = /* @__PURE__ */ new Map();
|
|
1299
|
+
for (const path3 of files) {
|
|
1300
|
+
const slash = path3.lastIndexOf("/");
|
|
1301
|
+
const folder = slash === -1 ? "" : path3.slice(0, slash);
|
|
1302
|
+
const names = folders.get(folder) ?? [];
|
|
1303
|
+
names.push(path3.slice(slash + 1));
|
|
1304
|
+
folders.set(folder, names);
|
|
1305
|
+
}
|
|
1306
|
+
const pages = /* @__PURE__ */ new Map();
|
|
1307
|
+
for (const [folder, names] of folders) {
|
|
1308
|
+
if (names.includes("index.html")) continue;
|
|
1309
|
+
const markdown = names.filter(isMarkdownPath).sort();
|
|
1310
|
+
const chosen = HOME_PAGE_NAMES.map((pattern) => markdown.find((name) => pattern.test(name))).find(Boolean) ?? (markdown.length === 1 ? markdown[0] : void 0);
|
|
1311
|
+
if (chosen) pages.set(folder, folder ? `${folder}/${chosen}` : chosen);
|
|
1312
|
+
}
|
|
1313
|
+
return pages;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ../../packages/shared/src/comments.ts
|
|
1292
1317
|
var COMMENT_MAX_CHARS = 4e3;
|
|
1293
1318
|
var COMMENT_PATH_MAX = 1024;
|
|
1294
1319
|
var FORM_ID_MAX = 40;
|
|
@@ -1382,18 +1407,22 @@ function normalizeCommentPath(input) {
|
|
|
1382
1407
|
if (path3 === "") return "/";
|
|
1383
1408
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path3)) {
|
|
1384
1409
|
try {
|
|
1385
|
-
|
|
1410
|
+
const url = new URL(path3);
|
|
1411
|
+
path3 = url.pathname + url.search;
|
|
1386
1412
|
} catch {
|
|
1387
1413
|
return null;
|
|
1388
1414
|
}
|
|
1389
1415
|
}
|
|
1416
|
+
const query = /^[^?#]*\?([^#]*)/.exec(path3)?.[1] ?? "";
|
|
1390
1417
|
path3 = path3.replace(/[?#].*$/, "");
|
|
1418
|
+
if (isMarkdownPath(path3) && query.split("&").includes("raw=1")) path3 += RAW_QUERY;
|
|
1391
1419
|
if (!path3.startsWith("/")) path3 = `/${path3}`;
|
|
1392
1420
|
path3 = path3.replace(/\/{2,}/g, "/").replace(/(^|\/)index\.html$/, "$1");
|
|
1393
1421
|
if (path3.length > COMMENT_PATH_MAX || /\s/.test(path3)) return null;
|
|
1394
1422
|
if (path3.split("/").some((s) => s === "." || s === "..")) return null;
|
|
1395
1423
|
return path3;
|
|
1396
1424
|
}
|
|
1425
|
+
var RAW_QUERY = "?raw=1";
|
|
1397
1426
|
function commentThreads(comments) {
|
|
1398
1427
|
const threads = /* @__PURE__ */ new Map();
|
|
1399
1428
|
for (const c of comments) {
|
|
@@ -1729,16 +1758,16 @@ function formAppliesTo(form, path3) {
|
|
|
1729
1758
|
if (include.length > 0 && !include.some((g) => globMatches(g, path3))) return false;
|
|
1730
1759
|
return !exclude.some((g) => globMatches(g, path3));
|
|
1731
1760
|
}
|
|
1732
|
-
function reviewPages(
|
|
1761
|
+
function reviewPages(paths2) {
|
|
1733
1762
|
const pages = /* @__PURE__ */ new Set();
|
|
1734
|
-
for (const p of
|
|
1763
|
+
for (const p of paths2) {
|
|
1735
1764
|
if (!/\.html?$/i.test(p) && !p.endsWith("/")) continue;
|
|
1736
|
-
const
|
|
1737
|
-
if (
|
|
1765
|
+
const page2 = normalizeCommentPath(p);
|
|
1766
|
+
if (page2 !== null) pages.add(page2);
|
|
1738
1767
|
}
|
|
1739
1768
|
return [...pages].sort();
|
|
1740
1769
|
}
|
|
1741
|
-
function checkFormsConfig(config,
|
|
1770
|
+
function checkFormsConfig(config, paths2) {
|
|
1742
1771
|
const parsed = FormsConfigSchema.safeParse(config);
|
|
1743
1772
|
if (!parsed.success) {
|
|
1744
1773
|
const errors = parsed.error.issues.map(
|
|
@@ -1746,7 +1775,7 @@ function checkFormsConfig(config, paths) {
|
|
|
1746
1775
|
);
|
|
1747
1776
|
return { ok: false, errors, summary: null };
|
|
1748
1777
|
}
|
|
1749
|
-
const pages =
|
|
1778
|
+
const pages = paths2 ? reviewPages(paths2) : null;
|
|
1750
1779
|
const warnings = [];
|
|
1751
1780
|
const forms2 = parsed.data.forms.map((form) => {
|
|
1752
1781
|
const matched = pages ? pages.filter((p) => formAppliesTo(form, p)) : null;
|
|
@@ -1863,7 +1892,7 @@ function responsesPageUrl(appOrigin, name) {
|
|
|
1863
1892
|
function progressLines(exported) {
|
|
1864
1893
|
return exported.progress.map((p) => {
|
|
1865
1894
|
const form = exported.forms.find((f) => f.id === p.form);
|
|
1866
|
-
const done = p.pages.filter((
|
|
1895
|
+
const done = p.pages.filter((page2) => (p.done[page2]?.length ?? 0) > 0).length;
|
|
1867
1896
|
const count2 = exported.responses.filter((r) => r.formId === p.form).length;
|
|
1868
1897
|
const where = form?.kind === "comment" ? `${count2} response${count2 === 1 ? "" : "s"} on ${Object.keys(p.done).length} of ${p.pages.length} pages` : `${done} / ${p.pages.length} page${p.pages.length === 1 ? "" : "s"} answered`;
|
|
1869
1898
|
return `${form?.title ?? p.form} (${p.form}): ${where}, ${p.reviewers.length} reviewer${p.reviewers.length === 1 ? "" : "s"}`;
|
|
@@ -1875,6 +1904,7 @@ var MAX_DEPLOY_BYTES = 100 * 1024 * 1024;
|
|
|
1875
1904
|
var MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
1876
1905
|
var MAX_FILES = 5e3;
|
|
1877
1906
|
var MAX_PATH_LENGTH = 1024;
|
|
1907
|
+
var MAX_PATH_FOLDER_LENGTH = 100;
|
|
1878
1908
|
|
|
1879
1909
|
// ../../packages/shared/src/manifest.ts
|
|
1880
1910
|
import { z as z5 } from "zod";
|
|
@@ -2085,12 +2115,17 @@ function normalizePublishPath(raw) {
|
|
|
2085
2115
|
const path3 = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2086
2116
|
return isValidSitePath(path3) ? path3 : null;
|
|
2087
2117
|
}
|
|
2088
|
-
|
|
2118
|
+
function normalizePathFolder(raw) {
|
|
2089
2119
|
const path3 = normalizePublishPath(raw);
|
|
2120
|
+
if (path3 === null || path3.includes("/") || path3.length > MAX_PATH_FOLDER_LENGTH) return null;
|
|
2121
|
+
return path3;
|
|
2122
|
+
}
|
|
2123
|
+
var PublishPathSchema = z6.string().transform((raw, ctx) => {
|
|
2124
|
+
const path3 = normalizePathFolder(raw);
|
|
2090
2125
|
if (path3 === null) {
|
|
2091
2126
|
ctx.addIssue({
|
|
2092
2127
|
code: "custom",
|
|
2093
|
-
message:
|
|
2128
|
+
message: `path must be one top-level folder of the site, like PAY-412 (at most ${MAX_PATH_FOLDER_LENGTH} characters, no dotfiles): publish nested folders as part of it`
|
|
2094
2129
|
});
|
|
2095
2130
|
return z6.NEVER;
|
|
2096
2131
|
}
|
|
@@ -2192,9 +2227,8 @@ var VersionSchema = z9.object({
|
|
|
2192
2227
|
/** who published it (email); absent on versions published before it was recorded */
|
|
2193
2228
|
publishedBy: z9.string().optional(),
|
|
2194
2229
|
/**
|
|
2195
|
-
* A
|
|
2196
|
-
*
|
|
2197
|
-
* site had none)
|
|
2230
|
+
* A Path's version: the Path's folder (`PAY-412`, shown as `/PAY-412/`). A publish on a base:
|
|
2231
|
+
* the version it built on, carrying its other files forward
|
|
2198
2232
|
*/
|
|
2199
2233
|
path: z9.string().optional(),
|
|
2200
2234
|
base: z9.string().optional(),
|
|
@@ -2205,6 +2239,29 @@ var SiteDetailSchema = SiteWithAudienceSchema.extend({
|
|
|
2205
2239
|
versions: z9.array(VersionSchema)
|
|
2206
2240
|
});
|
|
2207
2241
|
var SiteListSchema = z9.object({ sites: z9.array(SiteSchema) });
|
|
2242
|
+
var SitePathSchema = z9.object({
|
|
2243
|
+
folder: z9.string(),
|
|
2244
|
+
/** the `<title>` of its index.html, '' without one */
|
|
2245
|
+
title: z9.string(),
|
|
2246
|
+
url: z9.string(),
|
|
2247
|
+
currentVersionId: z9.string().nullable(),
|
|
2248
|
+
archived: z9.boolean(),
|
|
2249
|
+
owners: SiteRoleMembersSchema.optional(),
|
|
2250
|
+
publishers: SiteRoleMembersSchema.optional(),
|
|
2251
|
+
createdBy: z9.string().optional(),
|
|
2252
|
+
canPublish: z9.boolean(),
|
|
2253
|
+
/** the caller may change its owners and publishers, archive and delete it */
|
|
2254
|
+
canManage: z9.boolean().optional(),
|
|
2255
|
+
createdAt: z9.string(),
|
|
2256
|
+
updatedAt: z9.string()
|
|
2257
|
+
});
|
|
2258
|
+
var PathListSchema = z9.object({ paths: z9.array(SitePathSchema) });
|
|
2259
|
+
var PathDetailSchema = SitePathSchema.extend({ versions: z9.array(VersionSchema) });
|
|
2260
|
+
var UpdatePathRequestSchema = z9.object({
|
|
2261
|
+
owners: OwnersInputSchema.optional(),
|
|
2262
|
+
publishers: PublishersInputSchema.optional(),
|
|
2263
|
+
archived: z9.boolean().optional()
|
|
2264
|
+
});
|
|
2208
2265
|
var SharedSiteSchema = z9.object({
|
|
2209
2266
|
name: z9.string(),
|
|
2210
2267
|
url: z9.string(),
|
|
@@ -2270,7 +2327,6 @@ var DeployRequestSchema = z9.object({
|
|
|
2270
2327
|
}
|
|
2271
2328
|
if (req.partial && req.base === void 0) issue("`partial` needs `base`");
|
|
2272
2329
|
if (req.remove !== void 0 && !req.partial) issue("`remove` needs `partial: true`");
|
|
2273
|
-
if (req.base !== void 0 && req.path !== void 0) issue("Pass `base` or `path`, not both");
|
|
2274
2330
|
});
|
|
2275
2331
|
var UploadSchema = z9.object({
|
|
2276
2332
|
path: z9.string(),
|
|
@@ -2278,11 +2334,26 @@ var UploadSchema = z9.object({
|
|
|
2278
2334
|
/** headers the client must send with the PUT (they are part of the signature) */
|
|
2279
2335
|
headers: z9.record(z9.string(), z9.string())
|
|
2280
2336
|
});
|
|
2337
|
+
var PublishHintSchema = z9.object({
|
|
2338
|
+
code: z9.enum(["paths_index_missing", "paths_block_hidden"]),
|
|
2339
|
+
message: z9.string(),
|
|
2340
|
+
folders: z9.array(z9.string()).optional()
|
|
2341
|
+
});
|
|
2281
2342
|
var DeployResponseSchema = z9.discriminatedUnion("status", [
|
|
2282
2343
|
/** content identical to the current version — nothing happened */
|
|
2283
|
-
z9.object({
|
|
2344
|
+
z9.object({
|
|
2345
|
+
status: z9.literal("unchanged"),
|
|
2346
|
+
versionId: z9.string(),
|
|
2347
|
+
site: SiteSchema,
|
|
2348
|
+
hints: z9.array(PublishHintSchema).optional()
|
|
2349
|
+
}),
|
|
2284
2350
|
/** this content was deployed before — site now points at that version again */
|
|
2285
|
-
z9.object({
|
|
2351
|
+
z9.object({
|
|
2352
|
+
status: z9.literal("repointed"),
|
|
2353
|
+
versionId: z9.string(),
|
|
2354
|
+
site: SiteSchema,
|
|
2355
|
+
hints: z9.array(PublishHintSchema).optional()
|
|
2356
|
+
}),
|
|
2286
2357
|
/** new content — PUT every upload, then POST /api/sites/:name/deploys/:versionId/finalize */
|
|
2287
2358
|
z9.object({
|
|
2288
2359
|
status: z9.literal("upload"),
|
|
@@ -2295,7 +2366,8 @@ var FinalizeResponseSchema = z9.object({
|
|
|
2295
2366
|
versionId: z9.string(),
|
|
2296
2367
|
site: SiteSchema,
|
|
2297
2368
|
/** this deploy made the site live for the first time (a new site, its access just set) */
|
|
2298
|
-
first: z9.boolean().optional()
|
|
2369
|
+
first: z9.boolean().optional(),
|
|
2370
|
+
hints: z9.array(PublishHintSchema).optional()
|
|
2299
2371
|
});
|
|
2300
2372
|
var RollbackRequestSchema = z9.object({ versionId: z9.string() });
|
|
2301
2373
|
var UpdateSiteRequestSchema = z9.object({
|
|
@@ -2743,6 +2815,7 @@ function siteUrl(label, sitesDomain, protocol = "https:") {
|
|
|
2743
2815
|
// ../../packages/shared/src/instance.ts
|
|
2744
2816
|
import { z as z11 } from "zod";
|
|
2745
2817
|
var INSTANCE_METADATA_PATH = "/.well-known/fling";
|
|
2818
|
+
var RELEASE_HEADER = "Fling-Release";
|
|
2746
2819
|
var HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
2747
2820
|
var color = z11.string().regex(HEX_COLOR_RE);
|
|
2748
2821
|
var SignInProviderSchema = z11.looseObject({
|
|
@@ -2839,7 +2912,22 @@ var KvsSiteValueSchema = z12.object({
|
|
|
2839
2912
|
/** serve /index.html for unknown extensionless paths */
|
|
2840
2913
|
spa: z12.boolean(),
|
|
2841
2914
|
/** present when the site is restricted (private with an audience): the edge wants a site pass */
|
|
2842
|
-
r: z12.literal(1).optional()
|
|
2915
|
+
r: z12.literal(1).optional(),
|
|
2916
|
+
/** present when the site has live Paths: a request's first folder may be one (`path:` keys) */
|
|
2917
|
+
p: z12.literal(1).optional(),
|
|
2918
|
+
/**
|
|
2919
|
+
* the rendered home page and Paths listing (`sites/<id>/_rendered/<i>.html|json`), served for
|
|
2920
|
+
* `/`, `/index.html` and PATHS_JSON_PATH instead of the version's own index.html
|
|
2921
|
+
*/
|
|
2922
|
+
i: z12.string().optional(),
|
|
2923
|
+
/** present when the current version's Markdown files have rendered pages (markdown.ts) */
|
|
2924
|
+
m: z12.literal(1).optional()
|
|
2925
|
+
});
|
|
2926
|
+
var KvsPathValueSchema = z12.object({
|
|
2927
|
+
id: z12.string(),
|
|
2928
|
+
v: z12.string(),
|
|
2929
|
+
/** as on a site's value: its current version's Markdown has rendered pages */
|
|
2930
|
+
m: z12.literal(1).optional()
|
|
2843
2931
|
});
|
|
2844
2932
|
|
|
2845
2933
|
// ../../packages/shared/src/listing.ts
|
|
@@ -2860,10 +2948,10 @@ function extractHtmlTitle(html) {
|
|
|
2860
2948
|
function renderHtmlListing(siteName2, entries) {
|
|
2861
2949
|
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);
|
|
2862
2950
|
const groups2 = /* @__PURE__ */ new Map();
|
|
2863
|
-
for (const
|
|
2864
|
-
const slash =
|
|
2865
|
-
const folder = slash === -1 ? "" :
|
|
2866
|
-
groups2.set(folder, [...groups2.get(folder) ?? [],
|
|
2951
|
+
for (const page2 of pages) {
|
|
2952
|
+
const slash = page2.path.lastIndexOf("/");
|
|
2953
|
+
const folder = slash === -1 ? "" : page2.path.slice(0, slash);
|
|
2954
|
+
groups2.set(folder, [...groups2.get(folder) ?? [], page2]);
|
|
2867
2955
|
}
|
|
2868
2956
|
const sections = [...groups2.entries()].sort(([a], [b]) => a === "" ? -1 : b === "" ? 1 : a < b ? -1 : 1).map(([folder, items]) => {
|
|
2869
2957
|
const heading = folder ? `<h2>${escapeHtml(folder)}/</h2>` : "";
|
|
@@ -3124,6 +3212,385 @@ function describeNotificationSettings(settings) {
|
|
|
3124
3212
|
return lines.join("\n");
|
|
3125
3213
|
}
|
|
3126
3214
|
|
|
3215
|
+
// ../../packages/shared/src/paths-index.ts
|
|
3216
|
+
var PATHS_SORTS = ["updated", "oldest", "folder", "folder-desc"];
|
|
3217
|
+
var PATHS_OPTIONS = ["nocontrols", "nostyle", ...PATHS_SORTS.map((s) => `sort=${s}`)];
|
|
3218
|
+
var PATHS_PLACEHOLDERS = ["href", "folder", "title", "updated", "date", "version"];
|
|
3219
|
+
function matchesQuery(row2, query) {
|
|
3220
|
+
const marks = /[̀-ͯ]/g;
|
|
3221
|
+
const hay = `${row2.folder} ${row2.title}`.normalize("NFD").replace(marks, "").toLowerCase();
|
|
3222
|
+
const terms = String(query).normalize("NFD").replace(marks, "").toLowerCase().split(/\s+/);
|
|
3223
|
+
for (let i = 0; i < terms.length; i++) {
|
|
3224
|
+
if (terms[i] && hay.indexOf(terms[i]) < 0) return false;
|
|
3225
|
+
}
|
|
3226
|
+
return true;
|
|
3227
|
+
}
|
|
3228
|
+
function compareRows(a, b, sort) {
|
|
3229
|
+
const x = String(a.folder);
|
|
3230
|
+
const y = String(b.folder);
|
|
3231
|
+
const xs = x.match(/\d+|\D+/g) || [];
|
|
3232
|
+
const ys = y.match(/\d+|\D+/g) || [];
|
|
3233
|
+
let byFolder = 0;
|
|
3234
|
+
for (let i = 0; i < xs.length && i < ys.length && byFolder === 0; i++) {
|
|
3235
|
+
const p = xs[i];
|
|
3236
|
+
const q = ys[i];
|
|
3237
|
+
if (/^\d/.test(p) && /^\d/.test(q)) {
|
|
3238
|
+
const pn = p.replace(/^0+/, "");
|
|
3239
|
+
const qn = q.replace(/^0+/, "");
|
|
3240
|
+
byFolder = pn.length - qn.length || (pn < qn ? -1 : pn > qn ? 1 : 0);
|
|
3241
|
+
} else {
|
|
3242
|
+
const pl = p.toLowerCase();
|
|
3243
|
+
const ql = q.toLowerCase();
|
|
3244
|
+
byFolder = pl < ql ? -1 : pl > ql ? 1 : 0;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
if (byFolder === 0) byFolder = xs.length - ys.length;
|
|
3248
|
+
if (byFolder === 0) byFolder = x < y ? -1 : x > y ? 1 : 0;
|
|
3249
|
+
const byTime = (Date.parse(a.updated) || 0) - (Date.parse(b.updated) || 0);
|
|
3250
|
+
if (sort === "folder") return byFolder;
|
|
3251
|
+
if (sort === "folder-desc") return -byFolder;
|
|
3252
|
+
if (sort === "oldest") return byTime || byFolder;
|
|
3253
|
+
return -byTime || byFolder;
|
|
3254
|
+
}
|
|
3255
|
+
var OPENER = /<!--\s*fling:paths(?=\s|-->)([\s\S]*?)-->/g;
|
|
3256
|
+
var CLOSER = /<!--\s*\/fling:paths\s*-->/g;
|
|
3257
|
+
var EMPTY = /<!--\s*fling:empty\s*-->/;
|
|
3258
|
+
var CONTROLS = /<!--\s*fling:paths-controls\s*-->/;
|
|
3259
|
+
var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*\}\}/g;
|
|
3260
|
+
function findBlock(html) {
|
|
3261
|
+
OPENER.lastIndex = 0;
|
|
3262
|
+
const open = OPENER.exec(html);
|
|
3263
|
+
if (!open) return null;
|
|
3264
|
+
const afterOpen = open.index + open[0].length;
|
|
3265
|
+
const options = open[1].trim().split(/\s+/).filter(Boolean);
|
|
3266
|
+
OPENER.lastIndex = afterOpen;
|
|
3267
|
+
const next = OPENER.exec(html);
|
|
3268
|
+
CLOSER.lastIndex = afterOpen;
|
|
3269
|
+
const close = CLOSER.exec(html);
|
|
3270
|
+
const paired = close !== null && (next === null || close.index < next.index);
|
|
3271
|
+
OPENER.lastIndex = paired && close ? close.index + close[0].length : afterOpen;
|
|
3272
|
+
const more = OPENER.exec(html) !== null;
|
|
3273
|
+
OPENER.lastIndex = 0;
|
|
3274
|
+
if (!paired || !close) {
|
|
3275
|
+
return { kind: "bare", start: open.index, end: afterOpen, options, item: "", empty: null, more };
|
|
3276
|
+
}
|
|
3277
|
+
const inner = html.slice(afterOpen, close.index);
|
|
3278
|
+
const e = EMPTY.exec(inner);
|
|
3279
|
+
return {
|
|
3280
|
+
kind: "template",
|
|
3281
|
+
start: open.index,
|
|
3282
|
+
end: close.index + close[0].length,
|
|
3283
|
+
options,
|
|
3284
|
+
item: e ? inner.slice(0, e.index) : inner,
|
|
3285
|
+
empty: e ? inner.slice(e.index + e[0].length) : null,
|
|
3286
|
+
more
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
function relativeTime(iso, now) {
|
|
3290
|
+
const t = Date.parse(iso);
|
|
3291
|
+
if (!Number.isFinite(t)) return "";
|
|
3292
|
+
const minutes = Math.floor((now - t) / 6e4);
|
|
3293
|
+
if (minutes < 1) return "just now";
|
|
3294
|
+
if (minutes < 60) return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
|
|
3295
|
+
const hours = Math.floor(minutes / 60);
|
|
3296
|
+
if (hours < 24) return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
|
|
3297
|
+
const days = Math.floor(hours / 24);
|
|
3298
|
+
if (days < 7) return days === 1 ? "yesterday" : `${days} days ago`;
|
|
3299
|
+
return "";
|
|
3300
|
+
}
|
|
3301
|
+
var LOOK = [
|
|
3302
|
+
".fling-paths-controls{display:flex;flex-wrap:wrap;align-items:stretch;gap:var(--fling-paths-gap,.75rem);margin:0 0 var(--fling-paths-gap,.75rem);font:inherit;color:inherit}",
|
|
3303
|
+
".fling-paths-controls label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}",
|
|
3304
|
+
".fling-paths-controls input,.fling-paths-controls select{font:inherit;line-height:1.25;color:inherit;background-color:transparent;color-scheme:inherit;accent-color:var(--fling-paths-accent,LinkText);border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:var(--fling-paths-radius,.5rem);padding:.45rem .65rem;margin:0}",
|
|
3305
|
+
// The platform's own select keeps its height and arrow whatever the padding: draw both, the
|
|
3306
|
+
// arrow in the text's colour so it follows the page.
|
|
3307
|
+
".fling-paths-controls select{appearance:none;cursor:pointer;padding-inline-end:2rem;background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-size:.3rem .3rem;background-position:right 1rem center,right .7rem center;background-repeat:no-repeat}",
|
|
3308
|
+
".fling-paths-controls select option{color:CanvasText;background:Canvas}",
|
|
3309
|
+
".fling-paths-controls input{flex:1 1 14rem;min-inline-size:0}",
|
|
3310
|
+
".fling-paths-controls :focus-visible{outline:2px solid var(--fling-paths-accent,LinkText);outline-offset:1px}",
|
|
3311
|
+
".fling-paths-count{align-self:center;opacity:.7;font-size:.9em;font-variant-numeric:tabular-nums}",
|
|
3312
|
+
".fling-paths{list-style:none;margin:0;padding:0;display:grid;gap:calc(var(--fling-paths-gap,.75rem)/2)}",
|
|
3313
|
+
".fling-paths li{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:.25rem var(--fling-paths-gap,.75rem);padding:.6rem .8rem;border:1px solid color-mix(in srgb,currentColor 15%,transparent);border-radius:var(--fling-paths-radius,.5rem)}",
|
|
3314
|
+
".fling-paths a{color:var(--fling-paths-accent,LinkText);text-decoration:none;font-weight:600}",
|
|
3315
|
+
".fling-paths a:hover{text-decoration:underline}",
|
|
3316
|
+
".fling-paths-folder{font-weight:400;opacity:.7;font-family:ui-monospace,monospace;font-size:.85em;margin-inline-start:.5rem}",
|
|
3317
|
+
".fling-paths time{opacity:.7;font-size:.9em;font-variant-numeric:tabular-nums}"
|
|
3318
|
+
].join("\n");
|
|
3319
|
+
var PATHS_SCRIPT = `(function () {
|
|
3320
|
+
var matchesQuery = (${matchesQuery.toString()});
|
|
3321
|
+
var compareRows = (${compareRows.toString()});
|
|
3322
|
+
var relativeTime = (${relativeTime.toString()});
|
|
3323
|
+
// Dates within the last week read as how long ago they were; the rendered date stays as the
|
|
3324
|
+
// tooltip, and as the text from a week on.
|
|
3325
|
+
function ages() {
|
|
3326
|
+
var now = Date.now();
|
|
3327
|
+
document.querySelectorAll('[data-fling-path] time[datetime]').forEach(function (el) {
|
|
3328
|
+
if (!el.hasAttribute('data-fling-date')) el.setAttribute('data-fling-date', el.textContent);
|
|
3329
|
+
var fixed = el.getAttribute('data-fling-date');
|
|
3330
|
+
var ago = relativeTime(el.getAttribute('datetime'), now);
|
|
3331
|
+
el.textContent = ago || fixed;
|
|
3332
|
+
if (ago && fixed) el.title = fixed; else el.removeAttribute('title');
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
function start() {
|
|
3336
|
+
if (window.__flingPaths) return;
|
|
3337
|
+
var box = document.querySelector('[data-fling-paths-controls]');
|
|
3338
|
+
if (!box) return;
|
|
3339
|
+
window.__flingPaths = true;
|
|
3340
|
+
ages();
|
|
3341
|
+
setInterval(ages, 60000);
|
|
3342
|
+
var input = box.querySelector('input');
|
|
3343
|
+
var select = box.querySelector('select');
|
|
3344
|
+
var count = box.querySelector('[data-fling-paths-count]');
|
|
3345
|
+
var fallback = box.getAttribute('data-sort') || 'updated';
|
|
3346
|
+
var els = Array.prototype.slice.call(document.querySelectorAll('[data-fling-path]'));
|
|
3347
|
+
var rows = els.map(function (el) {
|
|
3348
|
+
return { el: el, folder: el.getAttribute('data-folder') || '', title: el.getAttribute('data-title') || '', updated: el.getAttribute('data-updated') || '' };
|
|
3349
|
+
});
|
|
3350
|
+
function rowOf(el) { return rows[els.indexOf(el)]; }
|
|
3351
|
+
function known(sort) {
|
|
3352
|
+
for (var i = 0; i < select.options.length; i++) if (select.options[i].value === sort) return true;
|
|
3353
|
+
return false;
|
|
3354
|
+
}
|
|
3355
|
+
function apply(write) {
|
|
3356
|
+
var q = input.value;
|
|
3357
|
+
var sort = select.value;
|
|
3358
|
+
var shown = 0;
|
|
3359
|
+
rows.forEach(function (r) {
|
|
3360
|
+
var ok = matchesQuery(r, q);
|
|
3361
|
+
r.el.hidden = !ok;
|
|
3362
|
+
if (ok) shown++;
|
|
3363
|
+
});
|
|
3364
|
+
var parents = [];
|
|
3365
|
+
rows.forEach(function (r) { if (r.el.parentNode && parents.indexOf(r.el.parentNode) < 0) parents.push(r.el.parentNode); });
|
|
3366
|
+
parents.forEach(function (p) {
|
|
3367
|
+
var group = Array.prototype.filter.call(p.children, function (el) { return els.indexOf(el) >= 0; });
|
|
3368
|
+
if (group.length < 2) return;
|
|
3369
|
+
var anchor = group[group.length - 1].nextSibling;
|
|
3370
|
+
group.slice().sort(function (a, b) { return compareRows(rowOf(a), rowOf(b), sort); })
|
|
3371
|
+
.forEach(function (el) { p.insertBefore(el, anchor); });
|
|
3372
|
+
});
|
|
3373
|
+
if (count) count.textContent = shown + ' of ' + rows.length;
|
|
3374
|
+
if (!write) return;
|
|
3375
|
+
try {
|
|
3376
|
+
var url = new URL(location.href);
|
|
3377
|
+
if (q) url.searchParams.set('q', q); else url.searchParams.delete('q');
|
|
3378
|
+
if (sort !== fallback) url.searchParams.set('sort', sort); else url.searchParams.delete('sort');
|
|
3379
|
+
history.replaceState(history.state, '', url.toString());
|
|
3380
|
+
} catch (e) {}
|
|
3381
|
+
}
|
|
3382
|
+
var params = new URLSearchParams(location.search);
|
|
3383
|
+
input.value = params.get('q') || '';
|
|
3384
|
+
var wanted = params.get('sort');
|
|
3385
|
+
if (wanted && known(wanted)) select.value = wanted;
|
|
3386
|
+
input.addEventListener('input', function () { apply(true); });
|
|
3387
|
+
select.addEventListener('change', function () { apply(true); });
|
|
3388
|
+
input.addEventListener('keydown', function (e) {
|
|
3389
|
+
if (e.key === 'Escape' && input.value) { e.preventDefault(); input.value = ''; apply(true); }
|
|
3390
|
+
});
|
|
3391
|
+
document.addEventListener('keydown', function (e) {
|
|
3392
|
+
if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
3393
|
+
var t = e.target;
|
|
3394
|
+
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
3395
|
+
e.preventDefault();
|
|
3396
|
+
input.focus();
|
|
3397
|
+
});
|
|
3398
|
+
apply(false);
|
|
3399
|
+
}
|
|
3400
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|
|
3401
|
+
else start();
|
|
3402
|
+
})();`;
|
|
3403
|
+
function checkPathsIndex(html) {
|
|
3404
|
+
const page2 = typeof html === "string" ? html : "";
|
|
3405
|
+
const warnings = [];
|
|
3406
|
+
const errors = [];
|
|
3407
|
+
const block = findBlock(page2);
|
|
3408
|
+
if (!block) {
|
|
3409
|
+
errors.push("No Paths block: add <!-- fling:paths --> where the listing should go.");
|
|
3410
|
+
return { ok: false, block: null, options: [], warnings, errors };
|
|
3411
|
+
}
|
|
3412
|
+
for (const o of block.options) {
|
|
3413
|
+
if (!PATHS_OPTIONS.includes(o)) {
|
|
3414
|
+
warnings.push(`Unknown option "${o}"; the options are ${PATHS_OPTIONS.join(", ")}.`);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
if (block.more) warnings.push("More than one Paths block: only the first is filled.");
|
|
3418
|
+
CLOSER.lastIndex = 0;
|
|
3419
|
+
const firstClose = CLOSER.exec(page2);
|
|
3420
|
+
CLOSER.lastIndex = 0;
|
|
3421
|
+
if (firstClose && firstClose.index < block.start) {
|
|
3422
|
+
warnings.push("A <!-- /fling:paths --> comes before the opening marker and is ignored.");
|
|
3423
|
+
}
|
|
3424
|
+
if (block.kind === "bare") {
|
|
3425
|
+
if (EMPTY.test(page2.slice(block.end)) || firstClose && firstClose.index > block.start) {
|
|
3426
|
+
warnings.push(
|
|
3427
|
+
"The template is not closed: add <!-- /fling:paths --> after it (before any other block). Until then the marker is bare and Fling uses its default list."
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
} else {
|
|
3431
|
+
if (!/<[A-Za-z]/.test(block.item) && !/\{\{/.test(block.item)) {
|
|
3432
|
+
warnings.push("The template item is empty, so each Path renders as nothing.");
|
|
3433
|
+
}
|
|
3434
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
3435
|
+
for (const m of block.item.matchAll(PLACEHOLDER)) {
|
|
3436
|
+
if (!PATHS_PLACEHOLDERS.includes(m[1])) unknown.add(m[1]);
|
|
3437
|
+
}
|
|
3438
|
+
for (const name of unknown) {
|
|
3439
|
+
warnings.push(
|
|
3440
|
+
`Unknown placeholder {{${name}}} is left as it is; the placeholders are ${PATHS_PLACEHOLDERS.map((p) => `{{${p}}}`).join(" ")}.`
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
if (!/href\s*=\s*["']?\{\{\s*href\s*\}\}/.test(block.item)) {
|
|
3444
|
+
warnings.push('The template item has no link to its Path: add <a href="{{href}}">.');
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
const panels = page2.match(new RegExp(CONTROLS.source, "g"))?.length ?? 0;
|
|
3448
|
+
if (panels > 0 && block.options.includes("nocontrols")) {
|
|
3449
|
+
warnings.push("<!-- fling:paths-controls --> is removed because the block says nocontrols.");
|
|
3450
|
+
}
|
|
3451
|
+
if (panels > 1)
|
|
3452
|
+
warnings.push("More than one <!-- fling:paths-controls -->: only the first is used.");
|
|
3453
|
+
return { ok: errors.length === 0, block: block.kind, options: block.options, warnings, errors };
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// ../../packages/shared/src/paths-templates.ts
|
|
3457
|
+
var PATHS_TEMPLATE_NAMES = ["plans", "docs", "reports", "minimal"];
|
|
3458
|
+
var PATHS_TEMPLATE_DESCRIPTIONS = {
|
|
3459
|
+
plans: "a board of plans, one card per ticket folder (e.g. PAY-412), newest first",
|
|
3460
|
+
docs: "a hub of documentation sections, A\u2013Z",
|
|
3461
|
+
reports: "an archive of reports by date, newest first, the date shown first",
|
|
3462
|
+
minimal: "a plain page with Fling's default list"
|
|
3463
|
+
};
|
|
3464
|
+
var DEFAULT_TITLES = {
|
|
3465
|
+
plans: "Plans",
|
|
3466
|
+
docs: "Docs",
|
|
3467
|
+
reports: "Reports",
|
|
3468
|
+
minimal: "Paths"
|
|
3469
|
+
};
|
|
3470
|
+
var ESCAPES = {
|
|
3471
|
+
"&": "&",
|
|
3472
|
+
"<": "<",
|
|
3473
|
+
">": ">",
|
|
3474
|
+
'"': """,
|
|
3475
|
+
"'": "'"
|
|
3476
|
+
};
|
|
3477
|
+
var esc = (value) => value.replace(/[&<>"']/g, (c) => ESCAPES[c] ?? c);
|
|
3478
|
+
var BASE_CSS = `:root{color-scheme:light dark;--bg:#f7f7f8;--fg:#1c1c21;--muted:#5d5d68;--card:#fff;--line:#e2e2e7;--fling-paths-accent:#2f5fc4}
|
|
3479
|
+
@media (prefers-color-scheme:dark){:root{--bg:#141417;--fg:#ececf0;--muted:#a0a0ab;--card:#1d1d22;--line:#2e2e35;--fling-paths-accent:#8ab0ff}}
|
|
3480
|
+
*{box-sizing:border-box}
|
|
3481
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
3482
|
+
main{max-width:var(--width,56rem);margin:0 auto;padding:clamp(1.5rem,5vw,3.5rem) 1.25rem}
|
|
3483
|
+
header{margin-bottom:1.75rem}
|
|
3484
|
+
h1{font-size:clamp(1.6rem,4vw,2.2rem);letter-spacing:-.015em;margin:0 0 .35rem}
|
|
3485
|
+
header p{margin:0;color:var(--muted)}
|
|
3486
|
+
a{color:var(--fling-paths-accent)}
|
|
3487
|
+
.empty{color:var(--muted);padding:2rem;text-align:center;border:1px dashed var(--line);border-radius:.75rem}`;
|
|
3488
|
+
function page(title, intro2, css, body) {
|
|
3489
|
+
const t = esc(title);
|
|
3490
|
+
return `<!doctype html>
|
|
3491
|
+
<html lang="en">
|
|
3492
|
+
<head>
|
|
3493
|
+
<meta charset="utf-8">
|
|
3494
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3495
|
+
<meta name="color-scheme" content="light dark">
|
|
3496
|
+
<title>${t}</title>
|
|
3497
|
+
<style>
|
|
3498
|
+
${BASE_CSS}
|
|
3499
|
+
${css}
|
|
3500
|
+
</style>
|
|
3501
|
+
</head>
|
|
3502
|
+
<body>
|
|
3503
|
+
<main>
|
|
3504
|
+
<header>
|
|
3505
|
+
<h1>${t}</h1>
|
|
3506
|
+
<p>${intro2}</p>
|
|
3507
|
+
</header>
|
|
3508
|
+
<!-- fling:paths-controls -->
|
|
3509
|
+
${body}
|
|
3510
|
+
</main>
|
|
3511
|
+
</body>
|
|
3512
|
+
</html>
|
|
3513
|
+
`;
|
|
3514
|
+
}
|
|
3515
|
+
var TEMPLATES2 = {
|
|
3516
|
+
plans: (title) => page(
|
|
3517
|
+
title,
|
|
3518
|
+
"One plan per ticket, newest first.",
|
|
3519
|
+
`.board{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,16rem),1fr));gap:1rem}
|
|
3520
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:.75rem;transition:border-color .15s}
|
|
3521
|
+
.card:hover{border-color:var(--fling-paths-accent)}
|
|
3522
|
+
.card a{display:flex;flex-direction:column;gap:.4rem;height:100%;padding:1rem 1.1rem;color:inherit;text-decoration:none}
|
|
3523
|
+
.ticket{align-self:start;font:600 .8rem/1 ui-monospace,monospace;color:var(--fling-paths-accent);background:color-mix(in srgb,var(--fling-paths-accent) 12%,transparent);padding:.3rem .5rem;border-radius:.4rem}
|
|
3524
|
+
.card h2{font-size:1.05rem;line-height:1.35;margin:0}
|
|
3525
|
+
.card time{margin-top:auto;font-size:.85rem;color:var(--muted)}`,
|
|
3526
|
+
`<div class="board">
|
|
3527
|
+
<!-- fling:paths sort=updated -->
|
|
3528
|
+
<article class="card"><a href="{{href}}"><span class="ticket">{{folder}}</span><h2>{{title}}</h2><time datetime="{{updated}}">{{date}}</time></a></article>
|
|
3529
|
+
<!-- fling:empty -->
|
|
3530
|
+
<p class="empty">No plans yet. Publish each plan to a folder named after its ticket, such as PAY-412.</p>
|
|
3531
|
+
<!-- /fling:paths -->
|
|
3532
|
+
</div>`
|
|
3533
|
+
),
|
|
3534
|
+
docs: (title) => page(
|
|
3535
|
+
title,
|
|
3536
|
+
"Every section, A\u2013Z.",
|
|
3537
|
+
`.sections{list-style:none;margin:0;padding:0;border-top:1px solid var(--line)}
|
|
3538
|
+
.sections li{border-bottom:1px solid var(--line)}
|
|
3539
|
+
.sections a{display:flex;flex-wrap:wrap;align-items:baseline;gap:.25rem 1rem;padding:.9rem .25rem;color:inherit;text-decoration:none}
|
|
3540
|
+
.sections a:hover .name{color:var(--fling-paths-accent);text-decoration:underline}
|
|
3541
|
+
.name{font-weight:600;flex:1 1 16rem}
|
|
3542
|
+
.meta{font-size:.85rem;color:var(--muted)}
|
|
3543
|
+
.meta code{font-family:ui-monospace,monospace}`,
|
|
3544
|
+
`<ul class="sections">
|
|
3545
|
+
<!-- fling:paths sort=folder -->
|
|
3546
|
+
<li><a href="{{href}}"><span class="name">{{title}}</span><span class="meta"><code>{{folder}}</code> \xB7 updated <time datetime="{{updated}}">{{date}}</time></span></a></li>
|
|
3547
|
+
<!-- fling:empty -->
|
|
3548
|
+
<li class="empty">No sections yet. Publish each section to its own folder.</li>
|
|
3549
|
+
<!-- /fling:paths -->
|
|
3550
|
+
</ul>`
|
|
3551
|
+
),
|
|
3552
|
+
reports: (title) => page(
|
|
3553
|
+
title,
|
|
3554
|
+
"Every report, newest first.",
|
|
3555
|
+
`.archive{list-style:none;margin:0;padding:0;display:grid;gap:.75rem}
|
|
3556
|
+
.report{display:grid;grid-template-columns:8.5rem 1fr;align-items:baseline;gap:.25rem 1.25rem;background:var(--card);border:1px solid var(--line);border-radius:.75rem;padding:1rem 1.1rem}
|
|
3557
|
+
@media (width < 34rem){.report{grid-template-columns:1fr}}
|
|
3558
|
+
.report time{font-size:1.15rem;font-weight:700;font-variant-numeric:tabular-nums}
|
|
3559
|
+
.report a{font-weight:600;text-decoration:none}
|
|
3560
|
+
.report a:hover{text-decoration:underline}
|
|
3561
|
+
.report small{grid-column:-2;color:var(--muted);font-family:ui-monospace,monospace}`,
|
|
3562
|
+
`<ol class="archive">
|
|
3563
|
+
<!-- fling:paths sort=updated -->
|
|
3564
|
+
<li class="report"><time datetime="{{updated}}">{{date}}</time><a href="{{href}}">{{title}}</a><small>{{folder}}</small></li>
|
|
3565
|
+
<!-- fling:empty -->
|
|
3566
|
+
<li class="empty">No reports yet. Publish each report to its own folder, such as 2026-11-05.</li>
|
|
3567
|
+
<!-- /fling:paths -->
|
|
3568
|
+
</ol>`
|
|
3569
|
+
),
|
|
3570
|
+
minimal: (title) => `<!doctype html>
|
|
3571
|
+
<html lang="en">
|
|
3572
|
+
<head>
|
|
3573
|
+
<meta charset="utf-8">
|
|
3574
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3575
|
+
<meta name="color-scheme" content="light dark">
|
|
3576
|
+
<title>${esc(title)}</title>
|
|
3577
|
+
<style>
|
|
3578
|
+
body{max-width:44rem;margin:0 auto;padding:3rem 1.25rem;font:16px/1.5 system-ui,sans-serif}
|
|
3579
|
+
</style>
|
|
3580
|
+
</head>
|
|
3581
|
+
<body>
|
|
3582
|
+
<h1>${esc(title)}</h1>
|
|
3583
|
+
<!-- fling:paths -->
|
|
3584
|
+
</body>
|
|
3585
|
+
</html>
|
|
3586
|
+
`
|
|
3587
|
+
};
|
|
3588
|
+
function pathsTemplate(name, opts = {}) {
|
|
3589
|
+
const title = opts.title?.trim() || DEFAULT_TITLES[name];
|
|
3590
|
+
return TEMPLATES2[name](title);
|
|
3591
|
+
}
|
|
3592
|
+
var isPathsTemplateName = (name) => PATHS_TEMPLATE_NAMES.includes(name);
|
|
3593
|
+
|
|
3127
3594
|
// ../../packages/shared/src/read-files.ts
|
|
3128
3595
|
var MCP_READ_MAX_BYTES = 1.5 * 1024 * 1024;
|
|
3129
3596
|
function pageOfFiles(files, opts) {
|
|
@@ -3132,7 +3599,7 @@ function pageOfFiles(files, opts) {
|
|
|
3132
3599
|
const found = new Set(selected.map((f) => f.path));
|
|
3133
3600
|
const missing = opts.paths ? opts.paths.filter((p) => !found.has(p)) : [];
|
|
3134
3601
|
const start = Math.max(0, Number.parseInt(opts.cursor ?? "0", 10) || 0);
|
|
3135
|
-
const
|
|
3602
|
+
const page2 = [];
|
|
3136
3603
|
const tooLarge = [];
|
|
3137
3604
|
let used = 0;
|
|
3138
3605
|
for (let i = start; i < selected.length; i++) {
|
|
@@ -3141,11 +3608,11 @@ function pageOfFiles(files, opts) {
|
|
|
3141
3608
|
tooLarge.push(file);
|
|
3142
3609
|
continue;
|
|
3143
3610
|
}
|
|
3144
|
-
if (used + file.size > opts.budget) return { page, tooLarge, missing, nextCursor: String(i) };
|
|
3145
|
-
|
|
3611
|
+
if (used + file.size > opts.budget) return { page: page2, tooLarge, missing, nextCursor: String(i) };
|
|
3612
|
+
page2.push(file);
|
|
3146
3613
|
used += file.size;
|
|
3147
3614
|
}
|
|
3148
|
-
return { page, tooLarge, missing };
|
|
3615
|
+
return { page: page2, tooLarge, missing };
|
|
3149
3616
|
}
|
|
3150
3617
|
function encodeForAgent(path3, bytes) {
|
|
3151
3618
|
if (isScannablePath(path3) && !looksBinary(bytes)) {
|
|
@@ -3163,8 +3630,9 @@ function encodeForAgent(path3, bytes) {
|
|
|
3163
3630
|
function describeReadFiles(name, result) {
|
|
3164
3631
|
const read = result.files.filter((f) => !f.tooLarge);
|
|
3165
3632
|
const big = result.files.filter((f) => f.tooLarge);
|
|
3633
|
+
const of = result.path === void 0 ? name : `the Path /${result.path}/ of ${name}`;
|
|
3166
3634
|
const lines = [
|
|
3167
|
-
`${read.length} file(s) of ${
|
|
3635
|
+
`${read.length} file(s) of ${of}, version ${result.versionId}${result.path === void 0 ? "" : " (paths within the Path)"}: ${read.map((f) => f.path).join(", ") || "(none)"}.`
|
|
3168
3636
|
];
|
|
3169
3637
|
if (big.length) {
|
|
3170
3638
|
lines.push(
|
|
@@ -3178,7 +3646,7 @@ function describeReadFiles(name, result) {
|
|
|
3178
3646
|
);
|
|
3179
3647
|
}
|
|
3180
3648
|
lines.push(
|
|
3181
|
-
`To change the site, call fling_publish with base "${result.versionId}" and only the files you changed or added (and \`remove\` for files to delete): every other file is kept.`
|
|
3649
|
+
result.path === void 0 ? `To change the site, call fling_publish with base "${result.versionId}" and only the files you changed or added (and \`remove\` for files to delete): every other file is kept.` : `To change the Path, call fling_publish with path "${result.path}", base "${result.versionId}" and only the files you changed or added (and \`remove\` for files to delete), named within the Path: every other file of it is kept.`
|
|
3182
3650
|
);
|
|
3183
3651
|
return lines.join("\n");
|
|
3184
3652
|
}
|
|
@@ -3231,14 +3699,20 @@ var listHtml = z14.boolean().optional().describe(
|
|
|
3231
3699
|
"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"
|
|
3232
3700
|
);
|
|
3233
3701
|
var siteName = z14.string().min(1).max(200).describe('Site name, e.g. "q3-report"');
|
|
3234
|
-
|
|
3702
|
+
function describePublishHints(hints) {
|
|
3703
|
+
if (!hints?.length) return "";
|
|
3704
|
+
return `
|
|
3705
|
+
Worth doing:
|
|
3706
|
+
- ${hints.map((h) => h.message).join("\n- ")}`;
|
|
3707
|
+
}
|
|
3708
|
+
var PATH_PUBLISH_NOTE = `With \`path\` (one top-level folder, e.g. "PAY-412"), the files are published as that folder's Path: they go to <site>/PAY-412/\u2026 (give their paths relative to the folder, its page being "index.html"), and the rest of the site is untouched. A Path has its own versions, rollback, owners and publishers, and is made on first use (you own it). Use it for one plan, spec, report or week's page on a shared team site; teammates publishing other Paths never collide. fling_paths lists a site's Paths. The site's home page lists them by default when it has no index.html, or as a custom page with a Paths block (fling_paths_template); an index.html without a block lists none: settle which with the user (see fling_paths).`;
|
|
3235
3709
|
var PULL_NOTE = "To change a site that is already published (fix a typo, update a page, add one), start from what is live, not from scratch: read its files with fling_read_files, then call fling_publish with `base` (the `versionId` it returned) and only the files you changed or added, plus `remove` for files to delete; every other file stays as it is. If someone published the site in between, it fails with base_changed: read the files again and redo the changes, rather than publishing without `base` over theirs.";
|
|
3236
3710
|
var base = z14.string().min(1).max(100).optional().describe(
|
|
3237
3711
|
"The versionId fling_read_files returned: publish only what changed on top of it. `files` then holds only the files you changed or added, and every other file of the site is kept. Fails with base_changed if the site has changed since. Not with `path`"
|
|
3238
3712
|
);
|
|
3239
3713
|
var remove = z14.array(z14.string().min(1).max(1024)).max(MAX_FILES).optional().describe("With `base`: the paths of the files to delete from the site");
|
|
3240
3714
|
var publishPath = z14.string().min(1).max(1024).optional().describe(
|
|
3241
|
-
'Publish
|
|
3715
|
+
'Publish as the Path at this top-level folder of the site, e.g. "PAY-412" (served at <site>/PAY-412/); the rest of the site is untouched. File paths are relative to it. Omit to publish the site itself'
|
|
3242
3716
|
);
|
|
3243
3717
|
var sitePath = z14.string().min(1).max(1024).describe(
|
|
3244
3718
|
'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")'
|
|
@@ -3284,6 +3758,53 @@ function describeResponses(exported, opts = {}) {
|
|
|
3284
3758
|
...opts.responsesPage ? [`Progress and downloads (CSV, JSONL): ${opts.responsesPage}`] : []
|
|
3285
3759
|
].join("\n");
|
|
3286
3760
|
}
|
|
3761
|
+
var PATHS_TEMPLATES_LIST = PATHS_TEMPLATE_NAMES.map(
|
|
3762
|
+
(n) => `\`${n}\` (${PATHS_TEMPLATE_DESCRIPTIONS[n]})`
|
|
3763
|
+
).join(", ");
|
|
3764
|
+
var PATHS_BLOCK_FORMAT = `A Paths block is either the bare marker \`<!-- fling:paths -->\` (Fling's default list, styled by the page) or a template pair \`<!-- fling:paths sort=updated --> item <!-- fling:empty --> shown when there are none <!-- /fling:paths -->\`, whose item repeats once per Path with the placeholders ${PATHS_PLACEHOLDERS.map((p) => `{{${p}}}`).join(" ")} (HTML-escaped; the \`<!-- fling:empty -->\` part is optional). Put dates in \`<time datetime="{{updated}}">{{date}}</time>\`: within a week they read as how long ago ("2 hours ago") once the page loads. Options go in the opening marker: ${PATHS_OPTIONS.join(", ")}. \`<!-- fling:paths-controls -->\` places the search and sort controls. Only the first block is filled.`;
|
|
3765
|
+
var PATHS_HOME_CHOICE = `A site with Paths has a home page (its root index.html) that can list them. Make the choice explicit with the user: (a) the default: with no root index.html, Fling serves a listing of the live Paths, and a site whose first publish is a Path gets one (the \`minimal\` template); nothing to do. (b) a custom page: the site's own index.html with a Paths block, which Fling fills with the live Paths whenever one changes. Start from fling_paths_template (${PATHS_TEMPLATE_NAMES.join(", ")}), adapt it freely while keeping the block, check it with fling_paths_check, then publish it as the site root (without \`path\`). (c) an index.html without a Paths block is served as it is and lists no Paths; publishing then says so (the paths_index_missing hint).`;
|
|
3766
|
+
function describePathsHome(home) {
|
|
3767
|
+
if (home === "block") {
|
|
3768
|
+
return "Home page: the site's index.html has a Paths block, which lists the live Paths. To change how it looks, edit that page keeping the block (fling_read_files), check it with fling_paths_check and publish it as the site root (without `path`).";
|
|
3769
|
+
}
|
|
3770
|
+
if (home === "default") {
|
|
3771
|
+
return "Home page: Fling's default listing of the live Paths (the site has no index.html of its own). For a custom page, start from fling_paths_template, check it with fling_paths_check and publish it as the site's index.html (without `path`).";
|
|
3772
|
+
}
|
|
3773
|
+
if (home === "own") {
|
|
3774
|
+
return 'Home page: the site\'s own index.html, which has no Paths block, so it lists none of them. Offer the user a custom page with a block (fling_paths_template) or the default listing (remove index.html: fling_publish with `base` and `remove: ["index.html"]`).';
|
|
3775
|
+
}
|
|
3776
|
+
return 'Home page: if the site\'s home page has no Paths block, offer a custom one (fling_paths_template) or the default (remove index.html: fling_publish with `base` and `remove: ["index.html"]`).';
|
|
3777
|
+
}
|
|
3778
|
+
function describePathLine(p) {
|
|
3779
|
+
const state = p.archived ? "archived" : p.currentVersionId ? `version ${p.currentVersionId}` : "nothing published";
|
|
3780
|
+
return `- /${p.folder}/${p.title ? ` "${p.title}"` : ""}: ${p.url} (${state}, updated ${p.updatedAt}${p.canPublish ? ", you can publish it" : ""})`;
|
|
3781
|
+
}
|
|
3782
|
+
function describePaths(name, paths2, opts = {}) {
|
|
3783
|
+
const head = paths2.length ? `${paths2.length} Path${paths2.length === 1 ? "" : "s"} on ${name}:` : `${name} has no Paths yet. Publish a folder as one with \`path\` (e.g. "PAY-412").`;
|
|
3784
|
+
return [head, ...paths2.map(describePathLine), describePathsHome(opts.home)].join("\n");
|
|
3785
|
+
}
|
|
3786
|
+
function describePathDetail(name, path3) {
|
|
3787
|
+
const roles = path3.owners ? [`Owners: ${whoHolds(path3.owners)}. Publishers: ${whoHolds(path3.publishers)}.`] : [];
|
|
3788
|
+
const versions = path3.versions.slice(0, 10).map(
|
|
3789
|
+
(v) => `- ${v.id}${v.id === path3.currentVersionId ? " (current)" : ""}: ${v.fileCount} files, ${v.createdAt}${v.publishedBy ? `, by ${v.publishedBy}` : ""}`
|
|
3790
|
+
);
|
|
3791
|
+
return [
|
|
3792
|
+
`/${path3.folder}/ of ${name}${path3.title ? ` "${path3.title}"` : ""}: ${path3.url}${path3.archived ? " (archived)" : ""}${path3.canPublish ? " \u2014 you can publish it" : ""}`,
|
|
3793
|
+
...roles,
|
|
3794
|
+
"Versions (newest first; roll back with fling_rollback and this `path`):",
|
|
3795
|
+
...versions.length ? versions : ["(none)"]
|
|
3796
|
+
].join("\n");
|
|
3797
|
+
}
|
|
3798
|
+
function describePathsCheck(check2) {
|
|
3799
|
+
const head = check2.ok ? `The page has a ${check2.block === "template" ? "template" : "bare"} Paths block${check2.options.length ? ` (options: ${check2.options.join(", ")})` : ""}: Fling fills it with the live Paths.` : "The page has no Paths block Fling can fill:";
|
|
3800
|
+
return [
|
|
3801
|
+
head,
|
|
3802
|
+
...check2.errors.map((e) => `- Error: ${e}`),
|
|
3803
|
+
...check2.warnings.map((w) => `- Warning: ${w}`),
|
|
3804
|
+
...check2.ok ? ["Publish it as the site's root index.html (without `path`)."] : []
|
|
3805
|
+
].join("\n");
|
|
3806
|
+
}
|
|
3807
|
+
var pathFolder = z14.string().min(1).max(MAX_PATH_FOLDER_LENGTH + 2);
|
|
3287
3808
|
var encoding = z14.enum(["utf8", "base64"]).optional().describe('How `content` is encoded: "utf8" (default) for text, "base64" for binary files');
|
|
3288
3809
|
var spa = z14.boolean().optional().describe(
|
|
3289
3810
|
"Single-page app: serve index.html for unknown paths (client-side routing). Omit to keep the current setting"
|
|
@@ -3294,7 +3815,7 @@ function mcpToolsFor(w) {
|
|
|
3294
3815
|
const isPublic = z14.boolean().optional().describe(
|
|
3295
3816
|
"Prefer `access` (a new site) or fling_set_access (an existing one). true \u2192 anyone can view; false \u2192 private. Omit to keep the current setting"
|
|
3296
3817
|
);
|
|
3297
|
-
const
|
|
3818
|
+
const access3 = z14.enum(ACCESS_LEVELS).optional().describe(
|
|
3298
3819
|
`Who can open a new site: only_me, members (${w.privateAudience}), groups, people or public. Ask the user before the first publish; omit for the default (${defaultAccessPhrase(w)}). Ignored when the site already exists`
|
|
3299
3820
|
);
|
|
3300
3821
|
const accessEmails = emails.optional().describe('With access "people" (or "groups"): the addresses that may open it');
|
|
@@ -3323,8 +3844,13 @@ function mcpToolsFor(w) {
|
|
|
3323
3844
|
info: tool({
|
|
3324
3845
|
name: "fling_info",
|
|
3325
3846
|
title: "Site details",
|
|
3326
|
-
description: "Show one site: URL, visibility, share link, current version and version history (ids for fling_rollback).",
|
|
3327
|
-
inputSchema: {
|
|
3847
|
+
description: "Show one site: URL, visibility, share link, current version and version history (ids for fling_rollback). With `path` (one of the site's Paths, from fling_paths), show that Path instead: its URL, owners, publishers and its own versions.",
|
|
3848
|
+
inputSchema: {
|
|
3849
|
+
name: siteName,
|
|
3850
|
+
path: pathFolder.optional().describe(
|
|
3851
|
+
`One of the site's Paths, its folder (e.g. "PAY-412"): show it instead of the site`
|
|
3852
|
+
)
|
|
3853
|
+
},
|
|
3328
3854
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3329
3855
|
}),
|
|
3330
3856
|
files: tool({
|
|
@@ -3333,7 +3859,10 @@ function mcpToolsFor(w) {
|
|
|
3333
3859
|
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).",
|
|
3334
3860
|
inputSchema: {
|
|
3335
3861
|
name: siteName,
|
|
3336
|
-
versionId: z14.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served")
|
|
3862
|
+
versionId: z14.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served"),
|
|
3863
|
+
path: pathFolder.optional().describe(
|
|
3864
|
+
"One of the site's Paths, its folder (e.g. \"PAY-412\"): list that Path's version (ids from fling_info with this `path`) instead; its files are under the folder"
|
|
3865
|
+
)
|
|
3337
3866
|
},
|
|
3338
3867
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3339
3868
|
}),
|
|
@@ -3344,7 +3873,7 @@ function mcpToolsFor(w) {
|
|
|
3344
3873
|
inputSchema: {
|
|
3345
3874
|
name: siteName,
|
|
3346
3875
|
paths: z14.array(z14.string().min(1).max(1024)).max(MAX_FILES).optional().describe('Only these files (site paths, e.g. "index.html"); omit for all of them'),
|
|
3347
|
-
path: z14.string().min(1).max(1024).optional().describe('
|
|
3876
|
+
path: z14.string().min(1).max(1024).optional().describe('The files of this Path of the site, e.g. "PAY-412" (paths relative to it)'),
|
|
3348
3877
|
cursor: z14.string().max(20).optional().describe("From the previous call, for the next files"),
|
|
3349
3878
|
versionId: z14.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served")
|
|
3350
3879
|
},
|
|
@@ -3376,13 +3905,13 @@ function mcpToolsFor(w) {
|
|
|
3376
3905
|
base,
|
|
3377
3906
|
remove,
|
|
3378
3907
|
spa,
|
|
3379
|
-
access:
|
|
3908
|
+
access: access3,
|
|
3380
3909
|
emails: accessEmails,
|
|
3381
3910
|
groups: accessGroups,
|
|
3382
3911
|
public: isPublic,
|
|
3383
3912
|
listHtml,
|
|
3384
3913
|
forms: formsConfig.nullable().optional().describe(
|
|
3385
|
-
`The site's forms config (as ${FORMS_FILE}; fling_forms_template, fling_forms_check), for structured feedback on the review page. null removes the forms. Omitted: a publish
|
|
3914
|
+
`The site's forms config (as ${FORMS_FILE}; fling_forms_template, fling_forms_check), for structured feedback on the review page. null removes the forms. Omitted: a publish on a \`base\` keeps the site's forms, a publish with \`path\` leaves them as they are (forms are the site's, never a Path's: \`forms\` with \`path\` is refused), any other publish has none`
|
|
3386
3915
|
)
|
|
3387
3916
|
},
|
|
3388
3917
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
@@ -3397,7 +3926,7 @@ function mcpToolsFor(w) {
|
|
|
3397
3926
|
base,
|
|
3398
3927
|
remove,
|
|
3399
3928
|
spa,
|
|
3400
|
-
access:
|
|
3929
|
+
access: access3,
|
|
3401
3930
|
emails: accessEmails,
|
|
3402
3931
|
groups: accessGroups,
|
|
3403
3932
|
public: isPublic
|
|
@@ -3420,7 +3949,7 @@ function mcpToolsFor(w) {
|
|
|
3420
3949
|
uploadFinish: tool({
|
|
3421
3950
|
name: "fling_upload_finish",
|
|
3422
3951
|
title: "Finish a chunked upload",
|
|
3423
|
-
description: `Publish everything sent with fling_upload_file as the new version of the site and return its URL. The site becomes exactly the uploaded files (with \`path
|
|
3952
|
+
description: `Publish everything sent with fling_upload_file as the new version of the site and return its URL. The site becomes exactly the uploaded files (with \`path\`, that Path does; with \`base\`, plus the rest of that version). ${LIST_HTML} The upload stays open, so just call fling_upload_finish again with \`listHtml\`. The result includes \`warnings\`.`,
|
|
3424
3953
|
inputSchema: {
|
|
3425
3954
|
uploadId: z14.string().min(1).max(100).describe("From fling_upload_begin"),
|
|
3426
3955
|
listHtml
|
|
@@ -3699,13 +4228,42 @@ function mcpToolsFor(w) {
|
|
|
3699
4228
|
},
|
|
3700
4229
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
3701
4230
|
}),
|
|
4231
|
+
paths: tool({
|
|
4232
|
+
name: "fling_paths",
|
|
4233
|
+
title: "List a site's Paths",
|
|
4234
|
+
description: `List a site's Paths: the top-level folders (e.g. one per ticket, PAY-412) published on their own with \`path\`, each with its title, URL, current version, when it was updated, whether it is archived and whether the user can publish to it. Use it before publishing to a shared team site, to find a Path to change or roll back (fling_info, fling_files and fling_rollback take \`path\`), or when the user asks what is on it. ${PATHS_HOME_CHOICE} ${PATHS_BLOCK_FORMAT}`,
|
|
4235
|
+
inputSchema: { name: siteName },
|
|
4236
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
4237
|
+
}),
|
|
4238
|
+
pathsTemplate: tool({
|
|
4239
|
+
name: "fling_paths_template",
|
|
4240
|
+
title: "Start a Paths home page",
|
|
4241
|
+
description: `A starter home page (index.html) for a site with Paths, with its Paths block in place: Fling fills it with the site's live Paths. Templates: ${PATHS_TEMPLATES_LIST}. Use it when the user wants a custom home page listing their Paths rather than the default one. Adapt it freely (wording, layout, styling) while keeping the block, check it with fling_paths_check, then publish it as the site root (without \`path\`). Needs no sign-in. ${PATHS_HOME_CHOICE} ${PATHS_BLOCK_FORMAT}`,
|
|
4242
|
+
inputSchema: {
|
|
4243
|
+
template: z14.enum(PATHS_TEMPLATE_NAMES).describe("Which starter"),
|
|
4244
|
+
title: z14.string().min(1).max(200).optional().describe(`The page's title and heading (e.g. "Payments plans")`)
|
|
4245
|
+
},
|
|
4246
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
4247
|
+
}),
|
|
4248
|
+
pathsCheck: tool({
|
|
4249
|
+
name: "fling_paths_check",
|
|
4250
|
+
title: "Check a Paths home page",
|
|
4251
|
+
description: `Check a site's home page (index.html) before publishing it, for the Paths block Fling fills with the site's Paths: whether there is one, bare or template, its options, and warnings (unknown options or placeholders, an unclosed template, an item without a link). Returns \`ok\`, \`errors\` and \`warnings\`. Use it on every custom home page for a site with Paths. Needs no sign-in. ${PATHS_HOME_CHOICE} ${PATHS_BLOCK_FORMAT}`,
|
|
4252
|
+
inputSchema: {
|
|
4253
|
+
html: z14.string().min(1).max(MCP_PUBLISH_MAX_BYTES).describe("The page, as HTML")
|
|
4254
|
+
},
|
|
4255
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
4256
|
+
}),
|
|
3702
4257
|
rollback: tool({
|
|
3703
4258
|
name: "fling_rollback",
|
|
3704
4259
|
title: "Roll back",
|
|
3705
|
-
description: "Point a site at an earlier version (ids from fling_info). Instant; nothing is uploaded.",
|
|
4260
|
+
description: "Point a site at an earlier version (ids from fling_info). Instant; nothing is uploaded. With `path`, roll back only that Path (ids from fling_info with the same `path`); the rest of the site and its other Paths stay as they are.",
|
|
3706
4261
|
inputSchema: {
|
|
3707
4262
|
name: siteName,
|
|
3708
|
-
versionId: z14.string().min(1).max(100).describe("Version id to serve")
|
|
4263
|
+
versionId: z14.string().min(1).max(100).describe("Version id to serve"),
|
|
4264
|
+
path: pathFolder.optional().describe(
|
|
4265
|
+
`One of the site's Paths, its folder (e.g. "PAY-412"): roll back only that Path`
|
|
4266
|
+
)
|
|
3709
4267
|
},
|
|
3710
4268
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
3711
4269
|
})
|
|
@@ -3771,7 +4329,7 @@ function describeComments(list, opts = {}) {
|
|
|
3771
4329
|
var READY_TIMEOUT_MS = 15e3;
|
|
3772
4330
|
var NOT_READY_NOTE = "it can take a few more seconds to appear everywhere; reload if you see a 404";
|
|
3773
4331
|
var isReachableStatus = (status) => status >= 200 && status < 400 || status === 401 || status === 403;
|
|
3774
|
-
var defaultSleep = (ms) => new Promise((
|
|
4332
|
+
var defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
3775
4333
|
async function waitUntilReachable(url, opts = {}) {
|
|
3776
4334
|
const timeoutMs = opts.timeoutMs ?? READY_TIMEOUT_MS;
|
|
3777
4335
|
const doFetch = opts.fetch ?? ((u, init) => fetch(u, init));
|
|
@@ -3804,10 +4362,10 @@ async function waitUntilReachable(url, opts = {}) {
|
|
|
3804
4362
|
interval = Math.min(interval * 1.25, 2e3);
|
|
3805
4363
|
}
|
|
3806
4364
|
}
|
|
3807
|
-
function probeUrl(siteUrl2,
|
|
4365
|
+
function probeUrl(siteUrl2, paths2) {
|
|
3808
4366
|
const base2 = siteUrl2.endsWith("/") ? siteUrl2 : `${siteUrl2}/`;
|
|
3809
|
-
if (
|
|
3810
|
-
const pick =
|
|
4367
|
+
if (paths2.length === 0 || paths2.includes("index.html")) return base2;
|
|
4368
|
+
const pick = paths2.find((p) => /\.html?$/i.test(p)) ?? paths2[0];
|
|
3811
4369
|
return base2 + pick.split("/").map(encodeURIComponent).join("/");
|
|
3812
4370
|
}
|
|
3813
4371
|
function readinessWaitDisabled(env) {
|
|
@@ -3921,7 +4479,7 @@ var LiveServerMessageSchema = z16.discriminatedUnion("type", [
|
|
|
3921
4479
|
]);
|
|
3922
4480
|
|
|
3923
4481
|
// src/cli.ts
|
|
3924
|
-
import
|
|
4482
|
+
import chalk16 from "chalk";
|
|
3925
4483
|
import { Command, Option } from "commander";
|
|
3926
4484
|
|
|
3927
4485
|
// src/commands/auth.ts
|
|
@@ -3971,7 +4529,7 @@ function exitCodeFor(error) {
|
|
|
3971
4529
|
// package.json
|
|
3972
4530
|
var package_default = {
|
|
3973
4531
|
name: "@nearform/fling",
|
|
3974
|
-
version: "0.
|
|
4532
|
+
version: "0.10.0",
|
|
3975
4533
|
license: "MIT",
|
|
3976
4534
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
3977
4535
|
type: "module",
|
|
@@ -4025,6 +4583,7 @@ var package_default = {
|
|
|
4025
4583
|
// src/package-info.ts
|
|
4026
4584
|
var PACKAGE_NAME = package_default.name;
|
|
4027
4585
|
var VERSION = package_default.version;
|
|
4586
|
+
var NODE_ENGINE = package_default.engines.node;
|
|
4028
4587
|
var BIN_NAME = Object.keys(package_default.bin)[0] ?? "fling";
|
|
4029
4588
|
var NPX_COMMAND = `npx ${package_default.name}`;
|
|
4030
4589
|
|
|
@@ -4066,6 +4625,14 @@ function createApiClient(options) {
|
|
|
4066
4625
|
const serverUrl = options.serverUrl.replace(/\/+$/, "");
|
|
4067
4626
|
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
4068
4627
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
4628
|
+
let knownRelease = options.release;
|
|
4629
|
+
async function noteRelease(response) {
|
|
4630
|
+
const release = response.headers?.get?.(RELEASE_HEADER)?.trim();
|
|
4631
|
+
if (!release || release.length > 64 || release === knownRelease) return;
|
|
4632
|
+
knownRelease = release;
|
|
4633
|
+
await options.onRelease?.(release).catch(() => {
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4069
4636
|
async function request(method, path3, reqOptions = {}) {
|
|
4070
4637
|
const url = buildUrl(serverUrl, path3, reqOptions.query);
|
|
4071
4638
|
const headers = {
|
|
@@ -4091,6 +4658,7 @@ function createApiClient(options) {
|
|
|
4091
4658
|
cause
|
|
4092
4659
|
});
|
|
4093
4660
|
}
|
|
4661
|
+
await noteRelease(response);
|
|
4094
4662
|
const raw = await readBody(response);
|
|
4095
4663
|
if (!response.ok) throw errorFromResponse(response.status, raw, method, path3);
|
|
4096
4664
|
if (!reqOptions.schema) return raw;
|
|
@@ -4184,7 +4752,7 @@ function describeCause(cause) {
|
|
|
4184
4752
|
// src/config.ts
|
|
4185
4753
|
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4186
4754
|
import { homedir } from "node:os";
|
|
4187
|
-
import { join } from "node:path";
|
|
4755
|
+
import { delimiter, join } from "node:path";
|
|
4188
4756
|
import { z as z17 } from "zod";
|
|
4189
4757
|
var CONFIG_DIR_NAME = ".fling";
|
|
4190
4758
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -4196,7 +4764,11 @@ var ENV = {
|
|
|
4196
4764
|
/** Where to look for files given by name (the .mcpb extension sets it from its settings). */
|
|
4197
4765
|
downloadsDir: "FLING_DOWNLOADS_DIR",
|
|
4198
4766
|
/** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
|
|
4199
|
-
addServer: "FLING_ADD_SERVER"
|
|
4767
|
+
addServer: "FLING_ADD_SERVER",
|
|
4768
|
+
/** Set (to anything) to turn off the line saying a newer Fling release is out (update-check.ts). */
|
|
4769
|
+
noUpdateCheck: "FLING_NO_UPDATE_CHECK",
|
|
4770
|
+
/** Set by CI systems; the update check stays quiet there too. */
|
|
4771
|
+
ci: "CI"
|
|
4200
4772
|
};
|
|
4201
4773
|
var serverEntrySchema = z17.object({
|
|
4202
4774
|
/** What the server said about itself at login. A copy this CLI can't read is just refetched. */
|
|
@@ -4218,6 +4790,19 @@ function fromEnv(value) {
|
|
|
4218
4790
|
function serverToAdd(env = process.env) {
|
|
4219
4791
|
return fromEnv(env[ENV.addServer]);
|
|
4220
4792
|
}
|
|
4793
|
+
function updateCheckSilenced(env = process.env) {
|
|
4794
|
+
return Boolean(fromEnv(env[ENV.noUpdateCheck]) || fromEnv(env[ENV.ci]));
|
|
4795
|
+
}
|
|
4796
|
+
function envOverrides(env = process.env) {
|
|
4797
|
+
return {
|
|
4798
|
+
configDir: Boolean(env[ENV.configDir]),
|
|
4799
|
+
url: Boolean(fromEnv(env[ENV.url])),
|
|
4800
|
+
apiKey: Boolean(fromEnv(env[ENV.apiKey]))
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function pathDirs(env = process.env) {
|
|
4804
|
+
return (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
4805
|
+
}
|
|
4221
4806
|
function resolveConfigDir(env = process.env) {
|
|
4222
4807
|
return env[ENV.configDir] ?? join(env.HOME ?? homedir(), CONFIG_DIR_NAME);
|
|
4223
4808
|
}
|
|
@@ -4435,6 +5020,19 @@ async function loadInstanceWith(store, config, fetch2, capability) {
|
|
|
4435
5020
|
return stored;
|
|
4436
5021
|
}
|
|
4437
5022
|
}
|
|
5023
|
+
async function recordRelease(store, serverUrl, release) {
|
|
5024
|
+
const entry = (await store.load()).servers[serverUrl];
|
|
5025
|
+
if (!entry?.instance || entry.instance.release === release) return;
|
|
5026
|
+
await store.putServer(
|
|
5027
|
+
serverUrl,
|
|
5028
|
+
{ instance: { ...entry.instance, release } },
|
|
5029
|
+
{ makeCurrent: false }
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
5032
|
+
var releaseTracking = (store, config) => config.remembered && config.serverUrl ? {
|
|
5033
|
+
release: config.instance?.release,
|
|
5034
|
+
onRelease: (release) => recordRelease(store, config.serverUrl, release)
|
|
5035
|
+
} : {};
|
|
4438
5036
|
|
|
4439
5037
|
// src/auth.ts
|
|
4440
5038
|
var CALLBACK_PORT_START = 8765;
|
|
@@ -4504,8 +5102,8 @@ async function startCallbackServer(options = {}) {
|
|
|
4504
5102
|
const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
4505
5103
|
let settle;
|
|
4506
5104
|
let timer;
|
|
4507
|
-
const result = new Promise((
|
|
4508
|
-
settle = { resolve:
|
|
5105
|
+
const result = new Promise((resolve8, reject) => {
|
|
5106
|
+
settle = { resolve: resolve8, reject };
|
|
4509
5107
|
}).finally(() => clearTimeout(timer));
|
|
4510
5108
|
result.catch(() => {
|
|
4511
5109
|
});
|
|
@@ -4566,7 +5164,7 @@ ${close ? "<script>setTimeout(function(){window.close()},800)</script>" : ""}
|
|
|
4566
5164
|
res.end(html);
|
|
4567
5165
|
}
|
|
4568
5166
|
function listenOnFreePort(server) {
|
|
4569
|
-
return new Promise((
|
|
5167
|
+
return new Promise((resolve8, reject) => {
|
|
4570
5168
|
let port = CALLBACK_PORT_START;
|
|
4571
5169
|
const tryNext = () => {
|
|
4572
5170
|
if (port > CALLBACK_PORT_END) {
|
|
@@ -4588,7 +5186,7 @@ function listenOnFreePort(server) {
|
|
|
4588
5186
|
});
|
|
4589
5187
|
server.listen(port, "127.0.0.1", () => {
|
|
4590
5188
|
server.removeAllListeners("error");
|
|
4591
|
-
|
|
5189
|
+
resolve8(port);
|
|
4592
5190
|
});
|
|
4593
5191
|
};
|
|
4594
5192
|
tryNext();
|
|
@@ -4606,20 +5204,20 @@ import chalk from "chalk";
|
|
|
4606
5204
|
var skipWait = async () => ({ ready: true, waitedMs: 0, skipped: true });
|
|
4607
5205
|
function createReadyWaiter(options = {}) {
|
|
4608
5206
|
if (readinessWaitDisabled(options.env ?? process.env)) return skipWait;
|
|
4609
|
-
return (url,
|
|
5207
|
+
return (url, paths2, opts) => waitUntilReachable(probeUrl(url, paths2), {
|
|
4610
5208
|
timeoutMs: options.timeoutMs,
|
|
4611
5209
|
fetch: options.fetch,
|
|
4612
5210
|
onWaiting: opts?.onWaiting
|
|
4613
5211
|
});
|
|
4614
5212
|
}
|
|
4615
5213
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4616
|
-
async function waitWithSpinner(waiter, url,
|
|
5214
|
+
async function waitWithSpinner(waiter, url, paths2, io) {
|
|
4617
5215
|
if (!waiter) return { ready: true, waitedMs: 0, skipped: true };
|
|
4618
5216
|
let frame = 0;
|
|
4619
5217
|
let timer;
|
|
4620
5218
|
const draw = () => io.progress(`${chalk.cyan(SPINNER[frame++ % SPINNER.length])} Waiting for the edge\u2026`);
|
|
4621
5219
|
try {
|
|
4622
|
-
const result = await waiter(url,
|
|
5220
|
+
const result = await waiter(url, paths2, {
|
|
4623
5221
|
onWaiting: () => {
|
|
4624
5222
|
draw();
|
|
4625
5223
|
timer = setInterval(draw, 100);
|
|
@@ -4830,7 +5428,9 @@ function requireClient(ctx) {
|
|
|
4830
5428
|
return createApiClient({
|
|
4831
5429
|
serverUrl,
|
|
4832
5430
|
apiKey: ctx.config.apiKey,
|
|
4833
|
-
fetch: ctx.fetch
|
|
5431
|
+
fetch: ctx.fetch,
|
|
5432
|
+
// A response saying the server runs another release updates the stored one.
|
|
5433
|
+
...ctx.store ? releaseTracking(ctx.store, ctx.config) : {}
|
|
4834
5434
|
});
|
|
4835
5435
|
}
|
|
4836
5436
|
|
|
@@ -4895,10 +5495,10 @@ async function setVisibility(client, name, visibility) {
|
|
|
4895
5495
|
const raw = await client.request("PATCH", sitePath2(name), { body: { visibility } });
|
|
4896
5496
|
return siteAfter(client, name, raw);
|
|
4897
5497
|
}
|
|
4898
|
-
async function setAccess(client, name,
|
|
5498
|
+
async function setAccess(client, name, access3, audience) {
|
|
4899
5499
|
return client.request("PATCH", sitePath2(name), {
|
|
4900
5500
|
schema: SiteWithAudienceSchema,
|
|
4901
|
-
body: { access:
|
|
5501
|
+
body: { access: access3, ...audience ? { audience } : {} }
|
|
4902
5502
|
});
|
|
4903
5503
|
}
|
|
4904
5504
|
function accessLevelsEnabled(instance) {
|
|
@@ -4969,6 +5569,73 @@ async function setRoles(client, name, which, change) {
|
|
|
4969
5569
|
});
|
|
4970
5570
|
return withRoles(site);
|
|
4971
5571
|
}
|
|
5572
|
+
function pathsEnabled(instance) {
|
|
5573
|
+
return instance?.capabilities.paths === true;
|
|
5574
|
+
}
|
|
5575
|
+
var pathOf = (name, folder) => `${sitePath2(name)}/paths/${encodeURIComponent(folder)}`;
|
|
5576
|
+
async function listPaths(client, name) {
|
|
5577
|
+
const res = await client.request("GET", `${sitePath2(name)}/paths`, { schema: PathListSchema });
|
|
5578
|
+
return res.paths;
|
|
5579
|
+
}
|
|
5580
|
+
function getPath(client, name, folder) {
|
|
5581
|
+
return client.request("GET", pathOf(name, folder), { schema: PathDetailSchema });
|
|
5582
|
+
}
|
|
5583
|
+
function rollbackPath(client, name, folder, versionId) {
|
|
5584
|
+
return client.request("POST", `${pathOf(name, folder)}/current`, {
|
|
5585
|
+
schema: SitePathSchema,
|
|
5586
|
+
body: { versionId }
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
function archivePath(client, name, folder, archived) {
|
|
5590
|
+
return client.request("PATCH", pathOf(name, folder), {
|
|
5591
|
+
schema: SitePathSchema,
|
|
5592
|
+
body: { archived }
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
async function deletePath(client, name, folder) {
|
|
5596
|
+
await client.request("DELETE", pathOf(name, folder));
|
|
5597
|
+
return { name, path: folder, deleted: true };
|
|
5598
|
+
}
|
|
5599
|
+
var pathWithRoles = (path3) => {
|
|
5600
|
+
if (!path3.owners) {
|
|
5601
|
+
throw new CliError(`You can't see who owns /${path3.folder}/`, {
|
|
5602
|
+
hint: "Only those on the site see its Paths' owners and publishers."
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
return {
|
|
5606
|
+
...path3,
|
|
5607
|
+
owners: path3.owners,
|
|
5608
|
+
publishers: path3.publishers ?? { emails: [], groups: [] }
|
|
5609
|
+
};
|
|
5610
|
+
};
|
|
5611
|
+
async function getPathRoles(client, name, folder) {
|
|
5612
|
+
return pathWithRoles(await getPath(client, name, folder));
|
|
5613
|
+
}
|
|
5614
|
+
async function setPathRoles(client, name, folder, which, change) {
|
|
5615
|
+
const current = await getPathRoles(client, name, folder);
|
|
5616
|
+
const path3 = await client.request("PATCH", pathOf(name, folder), {
|
|
5617
|
+
schema: SitePathSchema,
|
|
5618
|
+
body: { [which]: editRoleInput(current[which], change) }
|
|
5619
|
+
});
|
|
5620
|
+
return pathWithRoles(path3);
|
|
5621
|
+
}
|
|
5622
|
+
async function pathVersionFiles(client, name, folder, versionId) {
|
|
5623
|
+
const path3 = await getPath(client, name, folder);
|
|
5624
|
+
const id = versionId ?? path3.currentVersionId;
|
|
5625
|
+
if (!id) throw new CliError(`/${path3.folder}/ of ${name} has no published version yet`);
|
|
5626
|
+
const files = await client.request(
|
|
5627
|
+
"GET",
|
|
5628
|
+
`${sitePath2(name)}/versions/${encodeURIComponent(id)}/files`,
|
|
5629
|
+
{ schema: VersionFilesSchema, query: { path: path3.folder } }
|
|
5630
|
+
);
|
|
5631
|
+
return {
|
|
5632
|
+
name,
|
|
5633
|
+
path: path3.folder,
|
|
5634
|
+
url: path3.url,
|
|
5635
|
+
current: id === path3.currentVersionId,
|
|
5636
|
+
...files
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
4972
5639
|
var groupPath = (slug) => `/api/groups/${encodeURIComponent(slug)}`;
|
|
4973
5640
|
async function listGroups(client, opts = {}) {
|
|
4974
5641
|
const res = await client.request("GET", "/api/groups", {
|
|
@@ -5326,7 +5993,7 @@ import { join as join2, relative, sep } from "node:path";
|
|
|
5326
5993
|
import ignore from "ignore";
|
|
5327
5994
|
var GIT_TIMEOUT_MS = 2e4;
|
|
5328
5995
|
function git(cwd, args, input) {
|
|
5329
|
-
return new Promise((
|
|
5996
|
+
return new Promise((resolve8) => {
|
|
5330
5997
|
try {
|
|
5331
5998
|
const child = execFile(
|
|
5332
5999
|
"git",
|
|
@@ -5334,14 +6001,14 @@ function git(cwd, args, input) {
|
|
|
5334
6001
|
{ timeout: GIT_TIMEOUT_MS, maxBuffer: 256 * 1024 * 1024, encoding: "utf8" },
|
|
5335
6002
|
(error, stdout) => {
|
|
5336
6003
|
const code = error?.code;
|
|
5337
|
-
if (error && !(args[0] === "check-ignore" && code === 1))
|
|
5338
|
-
else
|
|
6004
|
+
if (error && !(args[0] === "check-ignore" && code === 1)) resolve8(null);
|
|
6005
|
+
else resolve8(stdout);
|
|
5339
6006
|
}
|
|
5340
6007
|
);
|
|
5341
|
-
child.on("error", () =>
|
|
6008
|
+
child.on("error", () => resolve8(null));
|
|
5342
6009
|
if (input !== void 0) child.stdin?.end(input);
|
|
5343
6010
|
} catch {
|
|
5344
|
-
|
|
6011
|
+
resolve8(null);
|
|
5345
6012
|
}
|
|
5346
6013
|
});
|
|
5347
6014
|
}
|
|
@@ -5432,15 +6099,15 @@ var CHARSET_TYPES = /* @__PURE__ */ new Set([
|
|
|
5432
6099
|
"image/svg+xml"
|
|
5433
6100
|
]);
|
|
5434
6101
|
var SecretFilesError = class extends CliError {
|
|
5435
|
-
constructor(
|
|
5436
|
-
const
|
|
6102
|
+
constructor(paths2) {
|
|
6103
|
+
const shown3 = paths2.slice(0, 20);
|
|
5437
6104
|
super(
|
|
5438
|
-
`Not flung: ${
|
|
6105
|
+
`Not flung: ${paths2.length === 1 ? "this is a secret file" : `these are ${paths2.length} secret files`} (keys, certificates, credentials): ${shown3.join(", ")}${paths2.length > shown3.length ? ` (+${paths2.length - shown3.length} more)` : ""}. ${NEVER_PUBLISHED_NOTE}`,
|
|
5439
6106
|
{
|
|
5440
|
-
hint: `Move ${
|
|
6107
|
+
hint: `Move ${paths2.length === 1 ? "it" : "them"} out of the folder, or add ${paths2.length === 1 ? "it" : "them"} to ${IGNORE_FILE} if the site does not need ${paths2.length === 1 ? "it" : "them"}.`
|
|
5441
6108
|
}
|
|
5442
6109
|
);
|
|
5443
|
-
this.paths =
|
|
6110
|
+
this.paths = paths2;
|
|
5444
6111
|
this.name = "SecretFilesError";
|
|
5445
6112
|
}
|
|
5446
6113
|
code = "secret_file";
|
|
@@ -5629,7 +6296,7 @@ function checkLimits(files, dir) {
|
|
|
5629
6296
|
);
|
|
5630
6297
|
}
|
|
5631
6298
|
}
|
|
5632
|
-
var hasRootIndex = (local) => local.files.some((f) => f.path === "index.html");
|
|
6299
|
+
var hasRootIndex = (local, markdown = false) => local.files.some((f) => f.path === "index.html") || markdown && markdownHomePages(local.files.map((f) => f.path)).has("");
|
|
5633
6300
|
var htmlPages = (local) => local.files.filter((f) => isHtmlPath(f.path) && f.path !== "index.html");
|
|
5634
6301
|
var TITLE_BYTES = 65536;
|
|
5635
6302
|
async function readHead(file) {
|
|
@@ -5828,14 +6495,14 @@ async function deploy(deps, opts) {
|
|
|
5828
6495
|
try {
|
|
5829
6496
|
return await deployOnce(deps, opts, name, local);
|
|
5830
6497
|
} catch (error) {
|
|
5831
|
-
if (opts.path === void 0 || !baseChanged(error)) throw error;
|
|
6498
|
+
if (opts.path === void 0 || opts.base !== void 0 || !baseChanged(error)) throw error;
|
|
5832
6499
|
return deployOnce(deps, opts, name, local);
|
|
5833
6500
|
}
|
|
5834
6501
|
}
|
|
5835
6502
|
async function deployOnce(deps, opts, name, local) {
|
|
5836
6503
|
const onBaseChanged = (error) => opts.base !== void 0 && baseChanged(error) ? new BaseChangedError(name, opts.base) : error;
|
|
5837
6504
|
const folder = opts.path;
|
|
5838
|
-
const prefix = folder
|
|
6505
|
+
const prefix = folder;
|
|
5839
6506
|
const sitePath4 = (file) => prefix === void 0 ? file : `${prefix}/${file}`;
|
|
5840
6507
|
const base2 = {
|
|
5841
6508
|
name,
|
|
@@ -5853,7 +6520,7 @@ async function deployOnce(deps, opts, name, local) {
|
|
|
5853
6520
|
...folder === void 0 ? {} : { path: folder },
|
|
5854
6521
|
...opts.base === void 0 ? {} : {
|
|
5855
6522
|
base: opts.base,
|
|
5856
|
-
...opts.partial ? { partial: true, remove: opts.remove ?? [] } : {}
|
|
6523
|
+
...opts.partial ? { partial: true, remove: (opts.remove ?? []).map(sitePath4) } : {}
|
|
5857
6524
|
},
|
|
5858
6525
|
...opts.spa !== void 0 ? { spa: opts.spa } : {},
|
|
5859
6526
|
...opts.public ? { visibility: "public" } : {},
|
|
@@ -5874,7 +6541,8 @@ async function deployOnce(deps, opts, name, local) {
|
|
|
5874
6541
|
versionId: res.versionId,
|
|
5875
6542
|
site: res.site,
|
|
5876
6543
|
uploaded: 0,
|
|
5877
|
-
first: false
|
|
6544
|
+
first: false,
|
|
6545
|
+
hints: res.hints ?? []
|
|
5878
6546
|
};
|
|
5879
6547
|
}
|
|
5880
6548
|
const byPath = new Map(local.files.map((f) => [sitePath4(f.path), f]));
|
|
@@ -5905,7 +6573,8 @@ async function deployOnce(deps, opts, name, local) {
|
|
|
5905
6573
|
versionId: fin.versionId,
|
|
5906
6574
|
site: fin.site,
|
|
5907
6575
|
uploaded: uploads.length,
|
|
5908
|
-
first: fin.first === true
|
|
6576
|
+
first: fin.first === true,
|
|
6577
|
+
hints: fin.hints ?? []
|
|
5909
6578
|
};
|
|
5910
6579
|
}
|
|
5911
6580
|
async function uploadWithRetry(deps, file, url, headers) {
|
|
@@ -5953,8 +6622,8 @@ async function readFormsFile(dir) {
|
|
|
5953
6622
|
});
|
|
5954
6623
|
}
|
|
5955
6624
|
}
|
|
5956
|
-
function requireValidForms(config,
|
|
5957
|
-
const check2 = checkFormsConfig(config,
|
|
6625
|
+
function requireValidForms(config, paths2, where, bin) {
|
|
6626
|
+
const check2 = checkFormsConfig(config, paths2);
|
|
5958
6627
|
if (!check2.ok) {
|
|
5959
6628
|
throw new CliError(
|
|
5960
6629
|
`${where} is not a forms config Fling can use:
|
|
@@ -5967,6 +6636,12 @@ function requireValidForms(config, paths, where, bin) {
|
|
|
5967
6636
|
async function planForms(opts) {
|
|
5968
6637
|
const file = opts.inline === void 0 ? await readFormsFile(opts.dir) : null;
|
|
5969
6638
|
const raw = opts.inline !== void 0 ? opts.inline : file?.config;
|
|
6639
|
+
if (opts.path !== void 0) {
|
|
6640
|
+
if (raw === void 0 || raw === null) return { send: void 0, check: null };
|
|
6641
|
+
throw new CliError(`Forms belong to the site, not to the Path /${opts.path}/`, {
|
|
6642
|
+
hint: `Deploy ${file ? FORMS_FILE : "the forms"} with the site's root (without a path), with \`paths\` patterns that reach /${opts.path}/ if its pages need them.`
|
|
6643
|
+
});
|
|
6644
|
+
}
|
|
5970
6645
|
if (raw === void 0) {
|
|
5971
6646
|
if (!formsEnabled(opts.instance)) return { send: void 0, check: null };
|
|
5972
6647
|
const send = !opts.layered || opts.pulledWithForms ? null : void 0;
|
|
@@ -6034,7 +6709,7 @@ async function tempFolder() {
|
|
|
6034
6709
|
const dir = await mkdtemp(join5(tmpdir(), "fling-"));
|
|
6035
6710
|
return { dir, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
6036
6711
|
}
|
|
6037
|
-
async function prepareInput(path3) {
|
|
6712
|
+
async function prepareInput(path3, opts = {}) {
|
|
6038
6713
|
let info;
|
|
6039
6714
|
try {
|
|
6040
6715
|
info = await stat3(path3);
|
|
@@ -6075,7 +6750,9 @@ async function prepareInput(path3) {
|
|
|
6075
6750
|
} else {
|
|
6076
6751
|
const name = basename2(path3);
|
|
6077
6752
|
await copyFile(path3, join5(tmp.dir, name));
|
|
6078
|
-
|
|
6753
|
+
if (!(opts.markdown && isMarkdownPath(name))) {
|
|
6754
|
+
await writeFile2(join5(tmp.dir, "index.html"), wrapperPage(name));
|
|
6755
|
+
}
|
|
6079
6756
|
}
|
|
6080
6757
|
} catch (error) {
|
|
6081
6758
|
await tmp.cleanup();
|
|
@@ -6096,8 +6773,8 @@ function zipEntryPath(name) {
|
|
|
6096
6773
|
if (JUNK_FILES.has((segments.at(-1) ?? "").toLowerCase())) return null;
|
|
6097
6774
|
return segments.join("/");
|
|
6098
6775
|
}
|
|
6099
|
-
function unwrapSingleFolder(
|
|
6100
|
-
let current =
|
|
6776
|
+
function unwrapSingleFolder(paths2) {
|
|
6777
|
+
let current = paths2;
|
|
6101
6778
|
for (; ; ) {
|
|
6102
6779
|
const first = current[0]?.split("/")[0];
|
|
6103
6780
|
if (!first || !current.every((p) => p.includes("/") && p.split("/")[0] === first)) return current;
|
|
@@ -6154,11 +6831,11 @@ async function extractZip(data, dest, skippedDotfiles = []) {
|
|
|
6154
6831
|
`Refusing to extract the zip: it has paths that escape its folder (${unsafe.slice(0, 3).join(", ")})`
|
|
6155
6832
|
);
|
|
6156
6833
|
const names = Object.keys(entries);
|
|
6157
|
-
const
|
|
6834
|
+
const paths2 = /* @__PURE__ */ new Map();
|
|
6158
6835
|
const kept = names.map((n) => zipEntryPath(n));
|
|
6159
6836
|
const unwrapped = unwrapSingleFolder(kept);
|
|
6160
6837
|
names.forEach((n, i) => {
|
|
6161
|
-
|
|
6838
|
+
paths2.set(n, unwrapped[i]);
|
|
6162
6839
|
});
|
|
6163
6840
|
if (names.length === 0) throw new CliError("The zip has no files to fling");
|
|
6164
6841
|
const root = resolve2(dest);
|
|
@@ -6169,7 +6846,7 @@ async function extractZip(data, dest, skippedDotfiles = []) {
|
|
|
6169
6846
|
total += content.length;
|
|
6170
6847
|
if (content.length > MAX_FILE_BYTES || total > MAX_DEPLOY_BYTES)
|
|
6171
6848
|
throw new CliError(`The zip expands to more than the limits allow`);
|
|
6172
|
-
const rel =
|
|
6849
|
+
const rel = paths2.get(name);
|
|
6173
6850
|
const target = resolve2(root, ...rel.split("/"));
|
|
6174
6851
|
if (!target.startsWith(root + sep4)) throw new CliError(`Unsafe path in the zip: ${name}`);
|
|
6175
6852
|
await mkdir2(join5(target, ".."), { recursive: true });
|
|
@@ -6226,7 +6903,7 @@ var SiteLinkSchema = z19.object({
|
|
|
6226
6903
|
name: z19.string(),
|
|
6227
6904
|
/** the version pulled, or the one the last deploy made */
|
|
6228
6905
|
versionId: z19.string(),
|
|
6229
|
-
/** a pull of one
|
|
6906
|
+
/** a pull of one of the site's Paths (its folder): the folder is that Path */
|
|
6230
6907
|
path: z19.string().optional(),
|
|
6231
6908
|
/** every file pulled (or deployed), by site path: its SHA-256 */
|
|
6232
6909
|
files: z19.record(z19.string(), z19.string()),
|
|
@@ -6266,7 +6943,7 @@ function onLink(link, local) {
|
|
|
6266
6943
|
for (const path3 of Object.keys(link.files)) if (!(path3 in now)) changes.removed.push(path3);
|
|
6267
6944
|
return {
|
|
6268
6945
|
base: link.versionId,
|
|
6269
|
-
...link.path === void 0 ? {} : {
|
|
6946
|
+
...link.path === void 0 ? {} : { path: link.path },
|
|
6270
6947
|
changes
|
|
6271
6948
|
};
|
|
6272
6949
|
}
|
|
@@ -6451,7 +7128,9 @@ async function runDeploy(ctx, input, opts) {
|
|
|
6451
7128
|
hint: `Leave it out, then use \`${ctx.binName} access\` or \`${ctx.binName} visibility\`.`
|
|
6452
7129
|
});
|
|
6453
7130
|
}
|
|
6454
|
-
const prepared = await prepareInput(path3
|
|
7131
|
+
const prepared = await prepareInput(path3, {
|
|
7132
|
+
markdown: instance?.capabilities.markdown === true
|
|
7133
|
+
});
|
|
6455
7134
|
try {
|
|
6456
7135
|
const { access: _asked, path: _path, ...rest } = opts;
|
|
6457
7136
|
await deployPrepared(
|
|
@@ -6492,10 +7171,10 @@ async function linkedTo(ctx, link, opts) {
|
|
|
6492
7171
|
}
|
|
6493
7172
|
function publishFolder(ctx, opts) {
|
|
6494
7173
|
if (opts.path === void 0) return void 0;
|
|
6495
|
-
const folder =
|
|
7174
|
+
const folder = normalizePathFolder(opts.path);
|
|
6496
7175
|
if (folder === null) {
|
|
6497
7176
|
throw new CliError(`Invalid --path "${opts.path}"`, {
|
|
6498
|
-
hint:
|
|
7177
|
+
hint: `Use one top-level folder of the site, like PAY-412 (no dotfiles, at most ${MAX_PATH_FOLDER_LENGTH} characters): it becomes that folder's Path. Nested folders go inside it.`
|
|
6499
7178
|
});
|
|
6500
7179
|
}
|
|
6501
7180
|
if (opts.name === void 0) {
|
|
@@ -6516,7 +7195,7 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6516
7195
|
if (!link && opts.name === void 0 && ask) {
|
|
6517
7196
|
name = await confirmSiteName(ctx, client, name, base2);
|
|
6518
7197
|
}
|
|
6519
|
-
if (!link && !hasRootIndex(local)) {
|
|
7198
|
+
if (!link && !hasRootIndex(local, instance?.capabilities.markdown === true)) {
|
|
6520
7199
|
const pages = htmlPages(local).length;
|
|
6521
7200
|
const root = base2 ? siteUrlFor(name, base2) + (folder ? `${folder}/` : "") : "the site root";
|
|
6522
7201
|
const warning = `No index.html ${folder ? `in ${folder}/` : "at the root"} \u2014 ${root} will show a 404.`;
|
|
@@ -6531,10 +7210,11 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6531
7210
|
const forms2 = await planForms({
|
|
6532
7211
|
dir: prepared.dir,
|
|
6533
7212
|
pages: local.files.map((f) => inFolder(f.path)),
|
|
6534
|
-
layered:
|
|
7213
|
+
layered: onBase !== null,
|
|
6535
7214
|
pulledWithForms: onBase !== null && link?.forms !== void 0,
|
|
6536
7215
|
instance,
|
|
6537
|
-
bin: ctx.binName
|
|
7216
|
+
bin: ctx.binName,
|
|
7217
|
+
...folder === void 0 ? {} : { path: folder }
|
|
6538
7218
|
});
|
|
6539
7219
|
let progressShown = false;
|
|
6540
7220
|
const result = await deploy(
|
|
@@ -6543,10 +7223,9 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6543
7223
|
dir: prepared.dir,
|
|
6544
7224
|
name,
|
|
6545
7225
|
manifest: local,
|
|
6546
|
-
path: opts.folder,
|
|
7226
|
+
path: opts.folder ?? onBase?.path,
|
|
6547
7227
|
...onBase ? {
|
|
6548
|
-
base: onBase.base
|
|
6549
|
-
...onBase.partial ? { partial: true, remove: onBase.remove, within: onBase.within } : {}
|
|
7228
|
+
base: onBase.base
|
|
6550
7229
|
} : {},
|
|
6551
7230
|
spa: opts.spa ? true : void 0,
|
|
6552
7231
|
public: opts.public,
|
|
@@ -6579,6 +7258,7 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6579
7258
|
);
|
|
6580
7259
|
}
|
|
6581
7260
|
for (const warning of result.warnings) if (warning !== NO_INDEX_WARNING) ctx.log.warn(warning);
|
|
7261
|
+
for (const hint2 of result.hints) ctx.log.info(hint2.message);
|
|
6582
7262
|
const summary = changes ? `${describeChanges(changes)} since ${link?.versionId}` : `${result.fileCount} files, ${formatBytes(result.totalBytes)}`;
|
|
6583
7263
|
if (result.status === "unchanged") ctx.log.info(`Unchanged ${chalk7.dim(`(${summary})`)}`);
|
|
6584
7264
|
else if (result.status === "repointed")
|
|
@@ -6588,12 +7268,15 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6588
7268
|
`Deployed ${result.name}${folder ? ` to /${folder}/` : ""} ${chalk7.dim(`(${summary}; uploaded ${result.uploaded}, version ${result.versionId})`)}`
|
|
6589
7269
|
);
|
|
6590
7270
|
const url = folder ? new URL(`${folder}/`, result.url).href : result.url;
|
|
6591
|
-
const ready = result.status === "unchanged" ? true : (
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
7271
|
+
const ready = result.status === "unchanged" ? true : (
|
|
7272
|
+
// From the site's root, as the files are named there: a Path's are under its folder.
|
|
7273
|
+
(await waitWithSpinner(
|
|
7274
|
+
ctx.waitReady,
|
|
7275
|
+
result.url,
|
|
7276
|
+
local.files.map((f) => inFolder(f.path)),
|
|
7277
|
+
{ progress: ctx.progress, warn: (m) => ctx.log.warn(m) }
|
|
7278
|
+
)).ready
|
|
7279
|
+
);
|
|
6597
7280
|
const members2 = instance ? privateAudience(instance) : "anyone signed in to this Fling";
|
|
6598
7281
|
const level = siteAccessLevel(result.site);
|
|
6599
7282
|
const who = describeSiteAccess(result.site, members2);
|
|
@@ -6612,16 +7295,18 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6612
7295
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
6613
7296
|
const reviewNote = instance ? `Share for feedback (reviewers sign in to ${instance.name}):` : "Share for feedback:";
|
|
6614
7297
|
let formsSummary = forms2.check?.summary ?? null;
|
|
6615
|
-
if (!formsSummary && forms2.send === void 0 && formsEnabled(instance)) {
|
|
7298
|
+
if (!formsSummary && folder === void 0 && forms2.send === void 0 && formsEnabled(instance)) {
|
|
6616
7299
|
const live = await getForms(client, result.name).catch(() => null);
|
|
6617
7300
|
if (live?.config) formsSummary = checkFormsConfig(live.config).summary;
|
|
6618
7301
|
}
|
|
6619
7302
|
const responsesUrl = formsSummary && instance ? responsesPageUrl(instance.appOrigin, result.name) : void 0;
|
|
6620
|
-
const hint = formsEnabled(instance) && !formsSummary ? formsHint(ctx.binName) : void 0;
|
|
7303
|
+
const hint = formsEnabled(instance) && !formsSummary && folder === void 0 ? formsHint(ctx.binName) : void 0;
|
|
6621
7304
|
const data = {
|
|
6622
7305
|
status: result.status,
|
|
6623
7306
|
site: result.site,
|
|
6624
7307
|
...folder ? { path: folder, url } : {},
|
|
7308
|
+
versionId: result.versionId,
|
|
7309
|
+
...result.hints.length ? { hints: result.hints } : {},
|
|
6625
7310
|
...changes && link ? { base: opts.force ? null : link.versionId, changes } : {},
|
|
6626
7311
|
access: level,
|
|
6627
7312
|
...result.first ? { created: true } : {},
|
|
@@ -6714,47 +7399,841 @@ async function confirmSiteName(ctx, client, generated, base2) {
|
|
|
6714
7399
|
}
|
|
6715
7400
|
}
|
|
6716
7401
|
|
|
6717
|
-
// src/commands/
|
|
6718
|
-
import { readFile as readFile8, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
|
|
6719
|
-
import { dirname as dirname3, join as join8, relative as relative5, resolve as resolve4 } from "node:path";
|
|
7402
|
+
// src/commands/doctor.ts
|
|
6720
7403
|
import chalk8 from "chalk";
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
7404
|
+
|
|
7405
|
+
// src/claude.ts
|
|
7406
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
7407
|
+
import { access, mkdtemp as mkdtemp2, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
7408
|
+
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
7409
|
+
import { join as join8 } from "node:path";
|
|
7410
|
+
|
|
7411
|
+
// mcpb/manifest.json
|
|
7412
|
+
var manifest_default = {
|
|
7413
|
+
manifest_version: "0.3",
|
|
7414
|
+
name: "fling",
|
|
7415
|
+
display_name: "Fling",
|
|
7416
|
+
version: "0.10.0",
|
|
7417
|
+
description: "Publish HTML pages, zips and folders from your computer to Fling and get a link back.",
|
|
7418
|
+
long_description: "Fling hosts static sites at https://<name>.<your sites domain> (private to people signed in to your Fling unless you make them public or share a link).\n\nThis extension runs on your computer, so Claude can publish:\n\n- content it created in the conversation (sent inline \u2014 no sandbox uploads),\n- a file you downloaded or Claude Desktop saved, by name: it looks in Downloads, Desktop, Documents/Claude and Claude Desktop's output folders,\n- a single .html file, a .zip of a site, or a folder with index.html.\n\nIt works with every Fling you sign in to: the one you downloaded it from is added on start, Claude can sign in to another with the fling_login tool, and the logins are shared with the fling CLI (`fling login <address>`). When several Flings are set up, Claude settles with you which one a conversation uses and names it on every call.",
|
|
7419
|
+
author: {
|
|
7420
|
+
name: "Fling contributors",
|
|
7421
|
+
url: "https://github.com/nearform/fling"
|
|
7422
|
+
},
|
|
7423
|
+
repository: {
|
|
7424
|
+
type: "git",
|
|
7425
|
+
url: "https://github.com/nearform/fling.git"
|
|
7426
|
+
},
|
|
7427
|
+
homepage: "https://github.com/nearform/fling",
|
|
7428
|
+
support: "https://github.com/nearform/fling/issues",
|
|
7429
|
+
icon: "icon.png",
|
|
7430
|
+
license: "MIT",
|
|
7431
|
+
keywords: ["publish", "html", "static site", "hosting"],
|
|
7432
|
+
server: {
|
|
7433
|
+
type: "node",
|
|
7434
|
+
entry_point: "server/index.mjs",
|
|
7435
|
+
mcp_config: {
|
|
7436
|
+
command: "node",
|
|
7437
|
+
args: ["${__dirname}/server/index.mjs"],
|
|
7438
|
+
env: {
|
|
7439
|
+
FLING_DOWNLOADS_DIR: "${user_config.downloads_dir}"
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
},
|
|
7443
|
+
tools: [
|
|
7444
|
+
{
|
|
7445
|
+
name: "fling_publish",
|
|
7446
|
+
description: "Publish a site from files passed inline (content Claude created)"
|
|
7447
|
+
},
|
|
7448
|
+
{
|
|
7449
|
+
name: "fling_deploy",
|
|
7450
|
+
description: "Publish a file, zip or folder from this computer, found by name in Downloads and other folders"
|
|
7451
|
+
},
|
|
7452
|
+
{
|
|
7453
|
+
name: "fling_pull",
|
|
7454
|
+
description: "Download a site's files into a folder on this computer, to change them and deploy again"
|
|
7455
|
+
},
|
|
7456
|
+
{
|
|
7457
|
+
name: "fling_read_files",
|
|
7458
|
+
description: "Read a site's files inline, to change some and publish them with fling_publish"
|
|
7459
|
+
},
|
|
7460
|
+
{
|
|
7461
|
+
name: "fling_find_files",
|
|
7462
|
+
description: "List recent HTML files, zips and site folders on this computer"
|
|
7463
|
+
},
|
|
7464
|
+
{ name: "fling_check_name", description: "Check whether a site name is free" },
|
|
7465
|
+
{ name: "fling_login", description: "Sign in to a Fling in the browser" },
|
|
7466
|
+
{
|
|
7467
|
+
name: "fling_servers",
|
|
7468
|
+
description: "List the Flings set up on this computer, or switch the current one"
|
|
7469
|
+
},
|
|
7470
|
+
{
|
|
7471
|
+
name: "fling_doctor",
|
|
7472
|
+
description: "Check the Fling setup on this computer and say how to fix what is wrong"
|
|
7473
|
+
},
|
|
7474
|
+
{ name: "fling_whoami", description: "Show the signed-in user" },
|
|
7475
|
+
{ name: "fling_list", description: "List your sites, or the sites shared with you" },
|
|
7476
|
+
{ name: "fling_info", description: "Show a site and its versions" },
|
|
7477
|
+
{ name: "fling_share", description: "Get, rotate or revoke a public share link" },
|
|
7478
|
+
{ name: "fling_set_visibility", description: "Make a site private or public" },
|
|
7479
|
+
{
|
|
7480
|
+
name: "fling_set_audience",
|
|
7481
|
+
description: "Limit a private site to listed people and groups, or open it to everyone signed in"
|
|
7482
|
+
},
|
|
7483
|
+
{ name: "fling_groups", description: "List the groups of people on a Fling" },
|
|
7484
|
+
{ name: "fling_group_show", description: "Show a group and its members" },
|
|
7485
|
+
{ name: "fling_group_create", description: "Create a group of people" },
|
|
7486
|
+
{
|
|
7487
|
+
name: "fling_group_members",
|
|
7488
|
+
description: "Add people to a group you own, or take them out"
|
|
7489
|
+
},
|
|
7490
|
+
{ name: "fling_group_rename", description: "Rename a group you own" },
|
|
7491
|
+
{ name: "fling_group_delete", description: "Delete a group you own" },
|
|
7492
|
+
{
|
|
7493
|
+
name: "fling_archive",
|
|
7494
|
+
description: "Take a site offline (or restore it), keeping its files"
|
|
7495
|
+
},
|
|
7496
|
+
{ name: "fling_rollback", description: "Serve an earlier version of a site" },
|
|
7497
|
+
{ name: "fling_delete", description: "Permanently delete a site" },
|
|
7498
|
+
{
|
|
7499
|
+
name: "fling_notifications",
|
|
7500
|
+
description: "Show what a Fling emails you about"
|
|
7501
|
+
},
|
|
7502
|
+
{
|
|
7503
|
+
name: "fling_notifications_set",
|
|
7504
|
+
description: "Turn notifications on or off, per event or all at once"
|
|
7505
|
+
}
|
|
7506
|
+
],
|
|
7507
|
+
user_config: {
|
|
7508
|
+
downloads_dir: {
|
|
7509
|
+
type: "directory",
|
|
7510
|
+
title: "Downloads folder",
|
|
7511
|
+
description: "Where to look first for files you ask Claude to fling by name (Desktop, Documents/Claude and Claude Desktop output folders are searched too).",
|
|
7512
|
+
default: "${HOME}/Downloads",
|
|
7513
|
+
required: false
|
|
7514
|
+
}
|
|
7515
|
+
},
|
|
7516
|
+
compatibility: {
|
|
7517
|
+
platforms: ["darwin", "win32"],
|
|
7518
|
+
runtimes: {
|
|
7519
|
+
node: ">=20.0.0"
|
|
7520
|
+
}
|
|
6728
7521
|
}
|
|
6729
|
-
return { client, instance };
|
|
6730
|
-
}
|
|
6731
|
-
var shown = (path3) => {
|
|
6732
|
-
const rel = relative5(process.cwd(), path3);
|
|
6733
|
-
return rel === "" ? "." : rel.startsWith("..") ? path3 : rel;
|
|
6734
7522
|
};
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
}
|
|
6757
|
-
|
|
7523
|
+
|
|
7524
|
+
// src/claude.ts
|
|
7525
|
+
var MARKETPLACE_SOURCE = REPO_SLUG;
|
|
7526
|
+
var MARKETPLACE_NAME = "fling";
|
|
7527
|
+
var PLUGIN_NAME = "fling";
|
|
7528
|
+
var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
7529
|
+
var MCPB_PATH = "/downloads/fling.mcpb";
|
|
7530
|
+
var runClaude = (args) => new Promise((resolve8) => {
|
|
7531
|
+
execFile2(
|
|
7532
|
+
"claude",
|
|
7533
|
+
[...args],
|
|
7534
|
+
{ timeout: 12e4, maxBuffer: 16 * 1024 * 1024, shell: process.platform === "win32" },
|
|
7535
|
+
(error, stdout, stderr) => {
|
|
7536
|
+
const missing = error?.code === "ENOENT";
|
|
7537
|
+
const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
|
|
7538
|
+
resolve8({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
7539
|
+
}
|
|
7540
|
+
);
|
|
7541
|
+
});
|
|
7542
|
+
async function claudeCodeStatus(run) {
|
|
7543
|
+
const version = await run(["--version"]);
|
|
7544
|
+
if (version.code !== 0) return { claude: "missing" };
|
|
7545
|
+
const claude = version.stdout.trim().split(/\s+/)[0] ?? "unknown";
|
|
7546
|
+
const list = await run(["plugin", "list", "--json"]);
|
|
7547
|
+
let plugins = [];
|
|
7548
|
+
try {
|
|
7549
|
+
plugins = JSON.parse(list.stdout);
|
|
7550
|
+
} catch {
|
|
7551
|
+
}
|
|
7552
|
+
const fling = Array.isArray(plugins) ? plugins.find((p) => p.id === PLUGIN_ID) : void 0;
|
|
7553
|
+
if (!fling) return { claude, installed: false };
|
|
7554
|
+
return {
|
|
7555
|
+
claude,
|
|
7556
|
+
installed: true,
|
|
7557
|
+
version: fling.version,
|
|
7558
|
+
scope: fling.scope,
|
|
7559
|
+
enabled: fling.enabled
|
|
7560
|
+
};
|
|
7561
|
+
}
|
|
7562
|
+
async function installPlugin(run, scope) {
|
|
7563
|
+
check(
|
|
7564
|
+
await run(["plugin", "install", PLUGIN_NAME, "--marketplace", MARKETPLACE_SOURCE, "-s", scope]),
|
|
7565
|
+
"install the Fling plugin"
|
|
7566
|
+
);
|
|
7567
|
+
}
|
|
7568
|
+
async function updatePlugin(run, scope) {
|
|
7569
|
+
check(await run(["plugin", "marketplace", "update", MARKETPLACE_NAME]), "update the marketplace");
|
|
7570
|
+
check(
|
|
7571
|
+
await run(["plugin", "update", PLUGIN_ID, ...scope ? ["-s", scope] : []]),
|
|
7572
|
+
"update the Fling plugin"
|
|
7573
|
+
);
|
|
7574
|
+
}
|
|
7575
|
+
async function marketplaceConfigured(run) {
|
|
7576
|
+
const list = await run(["plugin", "marketplace", "list", "--json"]);
|
|
7577
|
+
if (list.code !== 0) return null;
|
|
7578
|
+
try {
|
|
7579
|
+
const marketplaces = JSON.parse(list.stdout);
|
|
7580
|
+
return Array.isArray(marketplaces) && marketplaces.some((m) => m.name === MARKETPLACE_NAME);
|
|
7581
|
+
} catch {
|
|
7582
|
+
return null;
|
|
7583
|
+
}
|
|
7584
|
+
}
|
|
7585
|
+
function check(result, what) {
|
|
7586
|
+
if (result.code === 0) return;
|
|
7587
|
+
const detail = (result.stderr || result.stdout).trim().split("\n").at(-1);
|
|
7588
|
+
throw new Error(`Could not ${what}${detail ? `: ${detail}` : ""}`);
|
|
7589
|
+
}
|
|
7590
|
+
async function findClaudeDesktop(platform = process.platform, env = process.env, exists = (path3) => access(path3).then(
|
|
7591
|
+
() => true,
|
|
7592
|
+
() => false
|
|
7593
|
+
)) {
|
|
7594
|
+
const candidates = platform === "darwin" ? ["/Applications/Claude.app", join8(homedir3(), "Applications", "Claude.app")] : platform === "win32" && env.LOCALAPPDATA ? [join8(env.LOCALAPPDATA, "AnthropicClaude"), join8(env.LOCALAPPDATA, "Programs", "Claude")] : [];
|
|
7595
|
+
for (const path3 of candidates) if (await exists(path3)) return path3;
|
|
7596
|
+
return null;
|
|
7597
|
+
}
|
|
7598
|
+
var DESKTOP_EXTENSION_ID = `local.mcpb.${manifest_default.author.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.${manifest_default.name}`;
|
|
7599
|
+
function claudeDesktopDataDir(platform = process.platform, env = process.env, home = homedir3()) {
|
|
7600
|
+
if (platform === "darwin") return join8(home, "Library", "Application Support", "Claude");
|
|
7601
|
+
if (platform === "win32" && env.APPDATA) return join8(env.APPDATA, "Claude");
|
|
7602
|
+
return null;
|
|
7603
|
+
}
|
|
7604
|
+
async function desktopExtensionStatus(dataDir) {
|
|
7605
|
+
const readJson2 = async (path3) => {
|
|
7606
|
+
try {
|
|
7607
|
+
return JSON.parse(await readFile8(path3, "utf8"));
|
|
7608
|
+
} catch {
|
|
7609
|
+
return null;
|
|
7610
|
+
}
|
|
7611
|
+
};
|
|
7612
|
+
const manifest = await readJson2(
|
|
7613
|
+
join8(dataDir, "Claude Extensions", DESKTOP_EXTENSION_ID, "manifest.json")
|
|
7614
|
+
);
|
|
7615
|
+
if (!manifest) return { installed: false };
|
|
7616
|
+
const settings = await readJson2(
|
|
7617
|
+
join8(dataDir, "Claude Extensions Settings", `${DESKTOP_EXTENSION_ID}.json`)
|
|
7618
|
+
);
|
|
7619
|
+
return {
|
|
7620
|
+
installed: true,
|
|
7621
|
+
...typeof manifest.version === "string" ? { version: manifest.version } : {},
|
|
7622
|
+
...typeof settings?.isEnabled === "boolean" ? { enabled: settings.isEnabled } : {}
|
|
7623
|
+
};
|
|
7624
|
+
}
|
|
7625
|
+
var releaseExtensionUrl = (version) => `${REPO_URL}/releases/download/v${version}/fling.mcpb`;
|
|
7626
|
+
function extensionSources(serverUrl, serverRelease, version = VERSION) {
|
|
7627
|
+
const server = {
|
|
7628
|
+
url: new URL(MCPB_PATH, serverUrl).toString(),
|
|
7629
|
+
...serverRelease ? { version: serverRelease } : {}
|
|
7630
|
+
};
|
|
7631
|
+
if (serverRelease === version) return [server];
|
|
7632
|
+
return [{ url: releaseExtensionUrl(version), version }, server];
|
|
7633
|
+
}
|
|
7634
|
+
async function downloadExtension(url, fetch2) {
|
|
7635
|
+
const response = await fetch2(url);
|
|
7636
|
+
if (!response.ok) throw new Error(`Could not download ${url} (HTTP ${response.status})`);
|
|
7637
|
+
const dir = await mkdtemp2(join8(tmpdir2(), "fling-mcpb-"));
|
|
7638
|
+
const file = join8(dir, "fling.mcpb");
|
|
7639
|
+
await writeFile4(file, Buffer.from(await response.arrayBuffer()));
|
|
7640
|
+
return file;
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
// src/doctor.ts
|
|
7644
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
7645
|
+
import { access as access2, constants, realpath as realpath3, stat as stat6 } from "node:fs/promises";
|
|
7646
|
+
import { join as join10 } from "node:path";
|
|
7647
|
+
|
|
7648
|
+
// src/update-check.ts
|
|
7649
|
+
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
|
|
7650
|
+
import { join as join9 } from "node:path";
|
|
7651
|
+
var UPDATE_COMMAND = `npm i -g ${PACKAGE_NAME}@latest && ${BIN_NAME} setup`;
|
|
7652
|
+
var UPDATE_STATE_FILE = "update-check.json";
|
|
7653
|
+
var NOTIFY_EVERY_MS = 24 * 60 * 60 * 1e3;
|
|
7654
|
+
var SEMVER = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
7655
|
+
function parseSemver(input) {
|
|
7656
|
+
if (typeof input !== "string") return null;
|
|
7657
|
+
const m = SEMVER.exec(input.trim());
|
|
7658
|
+
if (!m) return null;
|
|
7659
|
+
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split(".") : [] };
|
|
7660
|
+
}
|
|
7661
|
+
function compareSemver(a, b) {
|
|
7662
|
+
for (let i = 0; i < 3; i++) {
|
|
7663
|
+
const d = (a.core[i] ?? 0) - (b.core[i] ?? 0);
|
|
7664
|
+
if (d !== 0) return d;
|
|
7665
|
+
}
|
|
7666
|
+
if (!a.pre.length || !b.pre.length) return b.pre.length - a.pre.length;
|
|
7667
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
7668
|
+
const x = a.pre[i];
|
|
7669
|
+
const y = b.pre[i];
|
|
7670
|
+
if (x === void 0) return -1;
|
|
7671
|
+
if (y === void 0) return 1;
|
|
7672
|
+
if (x === y) continue;
|
|
7673
|
+
const xn = /^\d+$/.test(x);
|
|
7674
|
+
const yn = /^\d+$/.test(y);
|
|
7675
|
+
if (xn && yn) return Number(x) - Number(y);
|
|
7676
|
+
if (xn !== yn) return xn ? -1 : 1;
|
|
7677
|
+
return x < y ? -1 : 1;
|
|
7678
|
+
}
|
|
7679
|
+
return 0;
|
|
7680
|
+
}
|
|
7681
|
+
function newerRelease(release, version = VERSION) {
|
|
7682
|
+
const theirs = parseSemver(release);
|
|
7683
|
+
const ours = parseSemver(version);
|
|
7684
|
+
if (!theirs || !ours || compareSemver(theirs, ours) <= 0) return null;
|
|
7685
|
+
return String(release).trim();
|
|
7686
|
+
}
|
|
7687
|
+
var updateLine = (release, version = VERSION) => `Fling ${release} is out (this CLI is ${version}). Update: ${UPDATE_COMMAND}`;
|
|
7688
|
+
var mcpUpdateNote = (release, version = VERSION) => `Fling ${release} is out and this Fling MCP server is ${version}. Tell the user once in this conversation, and suggest they update by running \`${UPDATE_COMMAND}\` in a terminal, then restarting Claude.`;
|
|
7689
|
+
async function updateNotice(input) {
|
|
7690
|
+
try {
|
|
7691
|
+
if (input.json || !input.stderrTTY || input.silenced || !input.serverUrl) return null;
|
|
7692
|
+
const version = input.version ?? VERSION;
|
|
7693
|
+
const release = newerRelease(input.release, version);
|
|
7694
|
+
if (!release) return null;
|
|
7695
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
7696
|
+
const file = join9(input.store.dir, UPDATE_STATE_FILE);
|
|
7697
|
+
const state = await readState(file);
|
|
7698
|
+
const last = Date.parse(state.notified[input.serverUrl] ?? "");
|
|
7699
|
+
if (Number.isFinite(last) && now.getTime() - last < NOTIFY_EVERY_MS) return null;
|
|
7700
|
+
state.notified[input.serverUrl] = now.toISOString();
|
|
7701
|
+
await mkdir4(input.store.dir, { recursive: true, mode: 448 });
|
|
7702
|
+
await writeFile5(file, `${JSON.stringify(state, null, 2)}
|
|
7703
|
+
`, { mode: 384 });
|
|
7704
|
+
return updateLine(release, version);
|
|
7705
|
+
} catch {
|
|
7706
|
+
return null;
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
async function noticeAfterCommand(ctx, server, options = {}) {
|
|
7710
|
+
try {
|
|
7711
|
+
const config = await ctx.store.resolve({ serverUrl: server }).catch(() => ctx.config);
|
|
7712
|
+
const line = await updateNotice({
|
|
7713
|
+
store: ctx.store,
|
|
7714
|
+
serverUrl: config.serverUrl,
|
|
7715
|
+
release: config.instance?.release,
|
|
7716
|
+
json: ctx.json,
|
|
7717
|
+
stderrTTY: options.stderrTTY ?? Boolean(process.stderr.isTTY),
|
|
7718
|
+
silenced: options.silenced ?? updateCheckSilenced(),
|
|
7719
|
+
now: options.now,
|
|
7720
|
+
version: options.version
|
|
7721
|
+
});
|
|
7722
|
+
if (line) ctx.log.warn(line);
|
|
7723
|
+
} catch {
|
|
7724
|
+
}
|
|
7725
|
+
}
|
|
7726
|
+
async function readState(file) {
|
|
7727
|
+
try {
|
|
7728
|
+
const json = JSON.parse(await readFile9(file, "utf8"));
|
|
7729
|
+
const notified = json?.notified;
|
|
7730
|
+
if (notified && typeof notified === "object" && !Array.isArray(notified)) {
|
|
7731
|
+
const entries = Object.entries(notified).filter(
|
|
7732
|
+
(e) => typeof e[1] === "string"
|
|
7733
|
+
);
|
|
7734
|
+
return { notified: Object.fromEntries(entries) };
|
|
7735
|
+
}
|
|
7736
|
+
} catch {
|
|
7737
|
+
}
|
|
7738
|
+
return { notified: {} };
|
|
7739
|
+
}
|
|
7740
|
+
|
|
7741
|
+
// src/doctor.ts
|
|
7742
|
+
var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME.replace("/", "%2f")}/dist-tags`;
|
|
7743
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
7744
|
+
var PLUGIN_UPDATE = `claude plugin marketplace update ${MARKETPLACE_NAME} && claude plugin update ${PLUGIN_ID}`;
|
|
7745
|
+
async function runChecks(deps) {
|
|
7746
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
7747
|
+
const fetchWithTimeout = (input, init) => deps.fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
7748
|
+
const d = { ...deps, fetch: fetchWithTimeout, timeoutMs, version: deps.version ?? VERSION };
|
|
7749
|
+
const groups2 = await Promise.all([
|
|
7750
|
+
cliChecks(d),
|
|
7751
|
+
configChecks(d),
|
|
7752
|
+
serverChecks(d),
|
|
7753
|
+
claudeCodeChecks(d),
|
|
7754
|
+
claudeDesktopChecks(d)
|
|
7755
|
+
]);
|
|
7756
|
+
return groups2.flat();
|
|
7757
|
+
}
|
|
7758
|
+
var doctorExitCode = (checks) => checks.some((c) => c.status === "problem") ? 1 : 0;
|
|
7759
|
+
function systemDoctorDeps(store, fetch2, runClaude2, findClaudeDesktop2) {
|
|
7760
|
+
const { env, home, platform } = systemLocateOptions();
|
|
7761
|
+
return {
|
|
7762
|
+
store,
|
|
7763
|
+
fetch: fetch2,
|
|
7764
|
+
env,
|
|
7765
|
+
home,
|
|
7766
|
+
platform,
|
|
7767
|
+
runClaude: runClaude2,
|
|
7768
|
+
findClaudeDesktop: findClaudeDesktop2,
|
|
7769
|
+
runProgram: (file, args) => new Promise((resolve8) => {
|
|
7770
|
+
execFile3(
|
|
7771
|
+
file,
|
|
7772
|
+
[...args],
|
|
7773
|
+
{ timeout: DEFAULT_TIMEOUT_MS, shell: platform === "win32" },
|
|
7774
|
+
(error, stdout) => resolve8({ code: error ? 1 : 0, stdout: String(stdout) })
|
|
7775
|
+
);
|
|
7776
|
+
}),
|
|
7777
|
+
selfPath: process.argv[1],
|
|
7778
|
+
nodeVersion: process.versions.node
|
|
7779
|
+
};
|
|
7780
|
+
}
|
|
7781
|
+
var SYMBOL = { ok: "\u2714", warning: "\u25B2", problem: "\u2716" };
|
|
7782
|
+
function describeChecks(checks) {
|
|
7783
|
+
return checks.map((c) => `${SYMBOL[c.status]} ${c.message}${c.fix ? ` \u2014 fix: ${c.fix}` : ""}`).join("\n");
|
|
7784
|
+
}
|
|
7785
|
+
async function cliChecks(d) {
|
|
7786
|
+
const checks = [
|
|
7787
|
+
{ area: "cli", id: "cli.version", status: "ok", message: `${BIN_NAME} CLI ${d.version}` }
|
|
7788
|
+
];
|
|
7789
|
+
const min = minimumNode(NODE_ENGINE);
|
|
7790
|
+
const node = parseSemver(d.nodeVersion);
|
|
7791
|
+
if (min && node) {
|
|
7792
|
+
checks.push(
|
|
7793
|
+
compareSemver(node, min.semver) >= 0 ? { area: "cli", id: "cli.node", status: "ok", message: `Node.js ${d.nodeVersion}` } : {
|
|
7794
|
+
area: "cli",
|
|
7795
|
+
id: "cli.node",
|
|
7796
|
+
status: "problem",
|
|
7797
|
+
message: `Node.js ${d.nodeVersion} is too old: the CLI needs ${NODE_ENGINE}`,
|
|
7798
|
+
fix: `Install Node.js ${min.text} or later (https://nodejs.org)`
|
|
7799
|
+
}
|
|
7800
|
+
);
|
|
7801
|
+
}
|
|
7802
|
+
const latest = await latestOnNpm(d);
|
|
7803
|
+
if (latest) {
|
|
7804
|
+
const newer = newerRelease(latest, d.version);
|
|
7805
|
+
checks.push(
|
|
7806
|
+
newer ? {
|
|
7807
|
+
area: "cli",
|
|
7808
|
+
id: "cli.latest",
|
|
7809
|
+
status: "warning",
|
|
7810
|
+
message: `${PACKAGE_NAME} ${newer} is on npm (this CLI is ${d.version})`,
|
|
7811
|
+
fix: UPDATE_COMMAND
|
|
7812
|
+
} : {
|
|
7813
|
+
area: "cli",
|
|
7814
|
+
id: "cli.latest",
|
|
7815
|
+
status: "ok",
|
|
7816
|
+
message: `Up to date with npm (latest: ${latest})`
|
|
7817
|
+
}
|
|
7818
|
+
);
|
|
7819
|
+
}
|
|
7820
|
+
checks.push(await onPath(d));
|
|
7821
|
+
return checks;
|
|
7822
|
+
}
|
|
7823
|
+
function minimumNode(range) {
|
|
7824
|
+
const m = /^>=\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range.trim());
|
|
7825
|
+
if (!m) return null;
|
|
7826
|
+
const text2 = [m[1], m[2], m[3]].filter((p) => p !== void 0).join(".");
|
|
7827
|
+
const semver = parseSemver(`${m[1]}.${m[2] ?? 0}.${m[3] ?? 0}`);
|
|
7828
|
+
return semver ? { text: text2, semver } : null;
|
|
7829
|
+
}
|
|
7830
|
+
async function latestOnNpm(d) {
|
|
7831
|
+
try {
|
|
7832
|
+
const response = await d.fetch(NPM_DIST_TAGS_URL, { headers: { Accept: "application/json" } });
|
|
7833
|
+
if (!response.ok) return null;
|
|
7834
|
+
const tags = await response.json();
|
|
7835
|
+
return typeof tags.latest === "string" && parseSemver(tags.latest) ? tags.latest : null;
|
|
7836
|
+
} catch {
|
|
7837
|
+
return null;
|
|
7838
|
+
}
|
|
7839
|
+
}
|
|
7840
|
+
async function onPath(d) {
|
|
7841
|
+
const names = d.platform === "win32" ? [`${BIN_NAME}.cmd`, `${BIN_NAME}.exe`, BIN_NAME] : [BIN_NAME];
|
|
7842
|
+
let found;
|
|
7843
|
+
for (const dir of pathDirs(d.env)) {
|
|
7844
|
+
for (const name of names) {
|
|
7845
|
+
const path3 = join10(dir, name);
|
|
7846
|
+
if (await isExecutable(path3, d.platform)) {
|
|
7847
|
+
found = path3;
|
|
7848
|
+
break;
|
|
7849
|
+
}
|
|
7850
|
+
}
|
|
7851
|
+
if (found) break;
|
|
7852
|
+
}
|
|
7853
|
+
const install = `npm i -g ${PACKAGE_NAME}@latest`;
|
|
7854
|
+
if (!found)
|
|
7855
|
+
return {
|
|
7856
|
+
area: "cli",
|
|
7857
|
+
id: "cli.path",
|
|
7858
|
+
status: "warning",
|
|
7859
|
+
message: `\`${BIN_NAME}\` is not on your PATH (this CLI runs through npx or another path)`,
|
|
7860
|
+
fix: install
|
|
7861
|
+
};
|
|
7862
|
+
const real = await realpath3(found).catch(() => found);
|
|
7863
|
+
const self2 = d.selfPath ? await realpath3(d.selfPath).catch(() => d.selfPath) : void 0;
|
|
7864
|
+
if (self2 && real === self2)
|
|
7865
|
+
return {
|
|
7866
|
+
area: "cli",
|
|
7867
|
+
id: "cli.path",
|
|
7868
|
+
status: "ok",
|
|
7869
|
+
message: `\`${BIN_NAME}\` on PATH is this CLI (${found})`
|
|
7870
|
+
};
|
|
7871
|
+
const other = await d.runProgram(found, ["--version"]);
|
|
7872
|
+
const version = other.code === 0 ? other.stdout.trim().split(/\s+/).at(-1) : void 0;
|
|
7873
|
+
if (version === d.version)
|
|
7874
|
+
return {
|
|
7875
|
+
area: "cli",
|
|
7876
|
+
id: "cli.path",
|
|
7877
|
+
status: "ok",
|
|
7878
|
+
message: `\`${BIN_NAME}\` on PATH (${found}) is ${version}, as this CLI`
|
|
7879
|
+
};
|
|
7880
|
+
return {
|
|
7881
|
+
area: "cli",
|
|
7882
|
+
id: "cli.path",
|
|
7883
|
+
status: "warning",
|
|
7884
|
+
message: `\`${BIN_NAME}\` on PATH (${found}) is ${version ? `version ${version}` : "another copy"}, not this CLI (${d.version})`,
|
|
7885
|
+
fix: `${install} && ${BIN_NAME} setup`
|
|
7886
|
+
};
|
|
7887
|
+
}
|
|
7888
|
+
async function isExecutable(path3, platform) {
|
|
7889
|
+
try {
|
|
7890
|
+
const s = await stat6(path3);
|
|
7891
|
+
if (!s.isFile()) return false;
|
|
7892
|
+
if (platform !== "win32") await access2(path3, constants.X_OK);
|
|
7893
|
+
return true;
|
|
7894
|
+
} catch {
|
|
7895
|
+
return false;
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
async function configChecks(d) {
|
|
7899
|
+
const overrides = envOverrides(d.env);
|
|
7900
|
+
const checks = [];
|
|
7901
|
+
const where = `${d.store.dir}${overrides.configDir ? ` (from ${ENV.configDir})` : ""}`;
|
|
7902
|
+
const config = await d.store.load().catch(() => null);
|
|
7903
|
+
const fileMode = await stat6(d.store.file).then(
|
|
7904
|
+
(s) => s.mode & 511,
|
|
7905
|
+
() => null
|
|
7906
|
+
);
|
|
7907
|
+
if (fileMode === null || !config || Object.keys(config.servers).length === 0) {
|
|
7908
|
+
checks.push({
|
|
7909
|
+
area: "config",
|
|
7910
|
+
id: "config.dir",
|
|
7911
|
+
status: "warning",
|
|
7912
|
+
message: `No Fling is set up yet (config: ${where})`,
|
|
7913
|
+
fix: `${BIN_NAME} setup <your Fling's address>`
|
|
7914
|
+
});
|
|
7915
|
+
} else {
|
|
7916
|
+
checks.push({ area: "config", id: "config.dir", status: "ok", message: `Config: ${where}` });
|
|
7917
|
+
}
|
|
7918
|
+
if (fileMode !== null && d.platform !== "win32") {
|
|
7919
|
+
const mode = fileMode.toString(8).padStart(3, "0");
|
|
7920
|
+
checks.push(
|
|
7921
|
+
fileMode & 63 ? {
|
|
7922
|
+
area: "config",
|
|
7923
|
+
id: "config.permissions",
|
|
7924
|
+
status: "problem",
|
|
7925
|
+
message: `${d.store.file} holds API keys and is readable by others (mode ${mode})`,
|
|
7926
|
+
fix: `chmod 600 "${d.store.file}"`
|
|
7927
|
+
} : {
|
|
7928
|
+
area: "config",
|
|
7929
|
+
id: "config.permissions",
|
|
7930
|
+
status: "ok",
|
|
7931
|
+
message: `The config file is private (mode ${mode})`
|
|
7932
|
+
}
|
|
7933
|
+
);
|
|
7934
|
+
}
|
|
7935
|
+
if (overrides.url)
|
|
7936
|
+
checks.push({
|
|
7937
|
+
area: "config",
|
|
7938
|
+
id: "config.env.url",
|
|
7939
|
+
status: "warning",
|
|
7940
|
+
message: `${ENV.url} is set: commands use the server it names, not the current one`,
|
|
7941
|
+
fix: `unset ${ENV.url} (if you did not mean to)`
|
|
7942
|
+
});
|
|
7943
|
+
if (overrides.apiKey)
|
|
7944
|
+
checks.push({
|
|
7945
|
+
area: "config",
|
|
7946
|
+
id: "config.env.apiKey",
|
|
7947
|
+
status: "warning",
|
|
7948
|
+
message: `${ENV.apiKey} is set: commands use that key, not the stored one`,
|
|
7949
|
+
fix: `unset ${ENV.apiKey} (if you did not mean to)`
|
|
7950
|
+
});
|
|
7951
|
+
return checks;
|
|
7952
|
+
}
|
|
7953
|
+
async function serverChecks(d) {
|
|
7954
|
+
const config = await d.store.load().catch(() => null);
|
|
7955
|
+
if (!config) return [];
|
|
7956
|
+
const resolved = await d.store.resolve().catch(() => null);
|
|
7957
|
+
const urls = Object.keys(config.servers);
|
|
7958
|
+
if (resolved?.serverUrl && !urls.includes(resolved.serverUrl)) urls.push(resolved.serverUrl);
|
|
7959
|
+
const perServer = await Promise.all(
|
|
7960
|
+
urls.map(async (url) => {
|
|
7961
|
+
const entry = config.servers[url] ?? {};
|
|
7962
|
+
const label = `${url}${url === config.current ? " (current)" : ""}`;
|
|
7963
|
+
const apiKey = resolved?.serverUrl === url && resolved.apiKeySource === "env" ? resolved.apiKey : entry.apiKey;
|
|
7964
|
+
return oneServer(d, url, label, apiKey);
|
|
7965
|
+
})
|
|
7966
|
+
);
|
|
7967
|
+
return perServer.flat();
|
|
7968
|
+
}
|
|
7969
|
+
async function oneServer(d, url, label, apiKey) {
|
|
7970
|
+
const at = (id, check2) => ({
|
|
7971
|
+
area: "server",
|
|
7972
|
+
id,
|
|
7973
|
+
server: url,
|
|
7974
|
+
...check2
|
|
7975
|
+
});
|
|
7976
|
+
let instance;
|
|
7977
|
+
try {
|
|
7978
|
+
instance = await fetchInstance(url, d.fetch);
|
|
7979
|
+
} catch (error) {
|
|
7980
|
+
return [
|
|
7981
|
+
at("server.metadata", {
|
|
7982
|
+
status: "problem",
|
|
7983
|
+
message: `${label}: ${error.message}`,
|
|
7984
|
+
fix: `Check the address and your connection; if the Fling moved: ${BIN_NAME} login <its new address>`
|
|
7985
|
+
})
|
|
7986
|
+
];
|
|
7987
|
+
}
|
|
7988
|
+
const checks = [
|
|
7989
|
+
at("server.metadata", { status: "ok", message: `${label}: ${instance.name} answers` })
|
|
7990
|
+
];
|
|
7991
|
+
const newer = newerRelease(instance.release, d.version);
|
|
7992
|
+
checks.push(
|
|
7993
|
+
newer ? at("server.release", {
|
|
7994
|
+
status: "warning",
|
|
7995
|
+
message: `${label} runs Fling ${newer}, newer than this CLI (${d.version})`,
|
|
7996
|
+
fix: UPDATE_COMMAND
|
|
7997
|
+
}) : at("server.release", {
|
|
7998
|
+
status: "ok",
|
|
7999
|
+
message: instance.release ? `${label} runs Fling ${instance.release}` : `${label} does not say which release it runs`
|
|
8000
|
+
})
|
|
8001
|
+
);
|
|
8002
|
+
const login = `${BIN_NAME} login ${url}`;
|
|
8003
|
+
if (!apiKey) {
|
|
8004
|
+
checks.push(
|
|
8005
|
+
at("server.signedIn", { status: "problem", message: `${label}: not signed in`, fix: login })
|
|
8006
|
+
);
|
|
8007
|
+
} else {
|
|
8008
|
+
try {
|
|
8009
|
+
const client = createApiClient({
|
|
8010
|
+
serverUrl: url,
|
|
8011
|
+
apiKey,
|
|
8012
|
+
fetch: d.fetch,
|
|
8013
|
+
timeoutMs: d.timeoutMs
|
|
8014
|
+
});
|
|
8015
|
+
const me = await client.request("GET", "/api/me");
|
|
8016
|
+
checks.push(
|
|
8017
|
+
at("server.signedIn", {
|
|
8018
|
+
status: "ok",
|
|
8019
|
+
message: `${label}: signed in${me?.email ? ` as ${me.email}` : ""}`
|
|
8020
|
+
})
|
|
8021
|
+
);
|
|
8022
|
+
} catch (error) {
|
|
8023
|
+
checks.push(
|
|
8024
|
+
exitCodeFor(error) === 2 ? at("server.signedIn", {
|
|
8025
|
+
status: "problem",
|
|
8026
|
+
message: `${label}: the API key is not accepted`,
|
|
8027
|
+
fix: login
|
|
8028
|
+
}) : at("server.signedIn", {
|
|
8029
|
+
status: "warning",
|
|
8030
|
+
message: `${label}: could not check the sign-in (${error.message})`,
|
|
8031
|
+
fix: `${BIN_NAME} whoami --server ${url}`
|
|
8032
|
+
})
|
|
8033
|
+
);
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
const on = Object.entries(instance.capabilities).filter(([, v]) => v === true).map(([k]) => k).sort();
|
|
8037
|
+
checks.push(
|
|
8038
|
+
at("server.capabilities", {
|
|
8039
|
+
status: "ok",
|
|
8040
|
+
message: `${label}: ${on.length ? on.join(", ") : "no optional features"}`
|
|
8041
|
+
})
|
|
8042
|
+
);
|
|
8043
|
+
return checks;
|
|
8044
|
+
}
|
|
8045
|
+
async function claudeCodeChecks(d) {
|
|
8046
|
+
const status = await claudeCodeStatus(d.runClaude).catch(() => ({ claude: "missing" }));
|
|
8047
|
+
if (status.claude === "missing")
|
|
8048
|
+
return [
|
|
8049
|
+
{
|
|
8050
|
+
area: "claudeCode",
|
|
8051
|
+
id: "claudeCode.cli",
|
|
8052
|
+
status: "warning",
|
|
8053
|
+
message: "Claude Code not found (no `claude` on your PATH)",
|
|
8054
|
+
fix: `Install Claude Code (https://claude.com/claude-code), then run \`${BIN_NAME} setup\``
|
|
8055
|
+
}
|
|
8056
|
+
];
|
|
8057
|
+
const checks = [
|
|
8058
|
+
{
|
|
8059
|
+
area: "claudeCode",
|
|
8060
|
+
id: "claudeCode.cli",
|
|
8061
|
+
status: "ok",
|
|
8062
|
+
message: `Claude Code ${status.claude}`
|
|
8063
|
+
}
|
|
8064
|
+
];
|
|
8065
|
+
const marketplace = await marketplaceConfigured(d.runClaude).catch(() => null);
|
|
8066
|
+
if (marketplace === false)
|
|
8067
|
+
checks.push({
|
|
8068
|
+
area: "claudeCode",
|
|
8069
|
+
id: "claudeCode.marketplace",
|
|
8070
|
+
status: "warning",
|
|
8071
|
+
message: `The ${MARKETPLACE_NAME} marketplace is not configured in Claude Code`,
|
|
8072
|
+
fix: `${BIN_NAME} setup, or: claude plugin marketplace add ${MARKETPLACE_SOURCE}`
|
|
8073
|
+
});
|
|
8074
|
+
else if (marketplace === true)
|
|
8075
|
+
checks.push({
|
|
8076
|
+
area: "claudeCode",
|
|
8077
|
+
id: "claudeCode.marketplace",
|
|
8078
|
+
status: "ok",
|
|
8079
|
+
message: `The ${MARKETPLACE_NAME} marketplace is configured`
|
|
8080
|
+
});
|
|
8081
|
+
if (!("installed" in status) || !status.installed) {
|
|
8082
|
+
checks.push({
|
|
8083
|
+
area: "claudeCode",
|
|
8084
|
+
id: "claudeCode.plugin",
|
|
8085
|
+
status: "warning",
|
|
8086
|
+
message: "The Fling plugin is not installed in Claude Code",
|
|
8087
|
+
fix: `${BIN_NAME} setup`
|
|
8088
|
+
});
|
|
8089
|
+
return checks;
|
|
8090
|
+
}
|
|
8091
|
+
if (status.enabled === false)
|
|
8092
|
+
checks.push({
|
|
8093
|
+
area: "claudeCode",
|
|
8094
|
+
id: "claudeCode.enabled",
|
|
8095
|
+
status: "problem",
|
|
8096
|
+
message: "The Fling plugin is installed but disabled",
|
|
8097
|
+
fix: `claude plugin enable ${PLUGIN_ID}`
|
|
8098
|
+
});
|
|
8099
|
+
const behind = status.version ? newerRelease(d.version, status.version) : null;
|
|
8100
|
+
checks.push(
|
|
8101
|
+
behind ? {
|
|
8102
|
+
area: "claudeCode",
|
|
8103
|
+
id: "claudeCode.plugin",
|
|
8104
|
+
status: "warning",
|
|
8105
|
+
message: `The Fling plugin is ${status.version}, older than this CLI (${d.version})`,
|
|
8106
|
+
fix: `${BIN_NAME} setup, or: ${PLUGIN_UPDATE}`
|
|
8107
|
+
} : {
|
|
8108
|
+
area: "claudeCode",
|
|
8109
|
+
id: "claudeCode.plugin",
|
|
8110
|
+
status: "ok",
|
|
8111
|
+
message: `The Fling plugin is installed${status.version ? ` (${status.version})` : ""}${status.enabled === false ? "" : " and enabled"}`
|
|
8112
|
+
}
|
|
8113
|
+
);
|
|
8114
|
+
return checks;
|
|
8115
|
+
}
|
|
8116
|
+
async function claudeDesktopChecks(d) {
|
|
8117
|
+
const app = await d.findClaudeDesktop().catch(() => null);
|
|
8118
|
+
if (!app) return [];
|
|
8119
|
+
const dataDir = claudeDesktopDataDir(d.platform, d.env, d.home);
|
|
8120
|
+
const ext = dataDir ? await desktopExtensionStatus(dataDir) : null;
|
|
8121
|
+
const at = (check2) => ({
|
|
8122
|
+
area: "claudeDesktop",
|
|
8123
|
+
id: "claudeDesktop.extension",
|
|
8124
|
+
...check2
|
|
8125
|
+
});
|
|
8126
|
+
if (!ext)
|
|
8127
|
+
return [
|
|
8128
|
+
at({
|
|
8129
|
+
status: "ok",
|
|
8130
|
+
message: `Claude Desktop is installed (${app}); the extension could not be looked up`
|
|
8131
|
+
})
|
|
8132
|
+
];
|
|
8133
|
+
if (!ext.installed)
|
|
8134
|
+
return [
|
|
8135
|
+
at({
|
|
8136
|
+
status: "warning",
|
|
8137
|
+
message: "The Fling extension is not installed in Claude Desktop",
|
|
8138
|
+
fix: `${BIN_NAME} setup (it opens fling.mcpb in Claude Desktop)`
|
|
8139
|
+
})
|
|
8140
|
+
];
|
|
8141
|
+
const checks = [];
|
|
8142
|
+
if (ext.enabled === false)
|
|
8143
|
+
checks.push({
|
|
8144
|
+
area: "claudeDesktop",
|
|
8145
|
+
id: "claudeDesktop.enabled",
|
|
8146
|
+
status: "problem",
|
|
8147
|
+
message: "The Fling extension is installed in Claude Desktop but disabled",
|
|
8148
|
+
fix: "Turn it on in Claude Desktop: Settings \u2192 Extensions \u2192 Fling"
|
|
8149
|
+
});
|
|
8150
|
+
const behind = ext.version ? newerRelease(d.version, ext.version) : null;
|
|
8151
|
+
checks.push(
|
|
8152
|
+
behind ? at({
|
|
8153
|
+
status: "warning",
|
|
8154
|
+
message: `The Claude Desktop extension is ${ext.version}, older than this CLI (${d.version})`,
|
|
8155
|
+
fix: `${BIN_NAME} setup`
|
|
8156
|
+
}) : at({
|
|
8157
|
+
status: "ok",
|
|
8158
|
+
message: `The Claude Desktop extension is installed${ext.version ? ` (${ext.version})` : ""} (${DESKTOP_EXTENSION_ID})`
|
|
8159
|
+
})
|
|
8160
|
+
);
|
|
8161
|
+
return checks;
|
|
8162
|
+
}
|
|
8163
|
+
|
|
8164
|
+
// src/commands/doctor.ts
|
|
8165
|
+
var AREAS = {
|
|
8166
|
+
cli: "CLI",
|
|
8167
|
+
config: "Config",
|
|
8168
|
+
server: "Flings",
|
|
8169
|
+
claudeCode: "Claude Code",
|
|
8170
|
+
claudeDesktop: "Claude Desktop"
|
|
8171
|
+
};
|
|
8172
|
+
var SYMBOLS = { ok: symbols.success, warning: symbols.warn, problem: symbols.error };
|
|
8173
|
+
async function runDoctor(ctx, deps) {
|
|
8174
|
+
const checks = await runChecks({
|
|
8175
|
+
...systemDoctorDeps(ctx.store, ctx.fetch, runClaude, () => findClaudeDesktop()),
|
|
8176
|
+
...deps
|
|
8177
|
+
});
|
|
8178
|
+
const ok = doctorExitCode(checks) === 0;
|
|
8179
|
+
ctx.out.data({ ok, checks }, () => render(checks));
|
|
8180
|
+
if (!ok) process.exitCode = 1;
|
|
8181
|
+
}
|
|
8182
|
+
function render(checks) {
|
|
8183
|
+
const lines = [];
|
|
8184
|
+
let area;
|
|
8185
|
+
for (const c of checks) {
|
|
8186
|
+
if (c.area !== area) {
|
|
8187
|
+
area = c.area;
|
|
8188
|
+
lines.push(`${lines.length ? "\n" : ""}${chalk8.bold(AREAS[c.area])}`);
|
|
8189
|
+
}
|
|
8190
|
+
const message = c.status === "problem" ? chalk8.red(c.message) : c.message;
|
|
8191
|
+
lines.push(`${SYMBOLS[c.status]} ${message}${c.fix ? chalk8.dim(` \u2014 fix: ${c.fix}`) : ""}`);
|
|
8192
|
+
}
|
|
8193
|
+
return lines.join("\n");
|
|
8194
|
+
}
|
|
8195
|
+
|
|
8196
|
+
// src/commands/forms.ts
|
|
8197
|
+
import { readFile as readFile10, stat as stat7, writeFile as writeFile6 } from "node:fs/promises";
|
|
8198
|
+
import { dirname as dirname3, join as join11, relative as relative5, resolve as resolve4 } from "node:path";
|
|
8199
|
+
import chalk9 from "chalk";
|
|
8200
|
+
async function formsClient(ctx) {
|
|
8201
|
+
const client = requireClient(ctx);
|
|
8202
|
+
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "forms");
|
|
8203
|
+
if (instance && !formsEnabled(instance)) {
|
|
8204
|
+
throw new CliError(`${instance.name} has no forms`, {
|
|
8205
|
+
hint: "Ask its operator to update it (forms come with comments)."
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
return { client, instance };
|
|
8209
|
+
}
|
|
8210
|
+
var shown = (path3) => {
|
|
8211
|
+
const rel = relative5(process.cwd(), path3);
|
|
8212
|
+
return rel === "" ? "." : rel.startsWith("..") ? path3 : rel;
|
|
8213
|
+
};
|
|
8214
|
+
async function runForms(ctx, name, opts) {
|
|
8215
|
+
const { client, instance } = await formsClient(ctx);
|
|
8216
|
+
const forms2 = await getForms(client, name, opts.path);
|
|
8217
|
+
if (!forms2.config) {
|
|
8218
|
+
ctx.log.info(`${name} has no forms on its live version`);
|
|
8219
|
+
ctx.log.hint(
|
|
8220
|
+
`\`${ctx.binName} forms init\` writes a ${FORMS_FILE} to deploy with the site's folder.`
|
|
8221
|
+
);
|
|
8222
|
+
if (ctx.json) ctx.out.data(forms2, () => "");
|
|
8223
|
+
return;
|
|
8224
|
+
}
|
|
8225
|
+
const summary = checkFormsConfig(forms2.config).summary;
|
|
8226
|
+
if (summary) {
|
|
8227
|
+
const lines = formsSummaryLines(summary);
|
|
8228
|
+
const onPath2 = opts.path ? ` (on ${forms2.path ?? opts.path}: ${forms2.forms.map((f) => f.id).join(", ") || "none"})` : "";
|
|
8229
|
+
ctx.log.info(`${summary.title} on ${name}${onPath2}`);
|
|
8230
|
+
for (const line of lines) ctx.log.hint(line);
|
|
8231
|
+
}
|
|
8232
|
+
if (instance)
|
|
8233
|
+
ctx.log.hint(`Responses and progress: ${responsesPageUrl(instance.appOrigin, name)}`);
|
|
8234
|
+
ctx.out.data(forms2, () => formatJson(forms2.config));
|
|
8235
|
+
}
|
|
8236
|
+
async function runFormsInit(ctx, dir, opts) {
|
|
6758
8237
|
const template = opts.template ?? "labels";
|
|
6759
8238
|
if (!isFormTemplateName(template)) {
|
|
6760
8239
|
throw new CliError(`Unknown template "${template}"`, {
|
|
@@ -6762,32 +8241,32 @@ async function runFormsInit(ctx, dir, opts) {
|
|
|
6762
8241
|
});
|
|
6763
8242
|
}
|
|
6764
8243
|
const folder = resolve4(dir ?? ".");
|
|
6765
|
-
const info = await
|
|
8244
|
+
const info = await stat7(folder).catch(() => null);
|
|
6766
8245
|
if (!info?.isDirectory()) throw new CliError(`No such folder: ${dir ?? "."}`);
|
|
6767
|
-
const file =
|
|
6768
|
-
if (!opts.force && await
|
|
8246
|
+
const file = join11(folder, FORMS_FILE);
|
|
8247
|
+
if (!opts.force && await stat7(file).catch(() => null)) {
|
|
6769
8248
|
throw new CliError(`${shown(file)} already exists`, {
|
|
6770
8249
|
hint: "Pass --force to replace it with the template."
|
|
6771
8250
|
});
|
|
6772
8251
|
}
|
|
6773
8252
|
const config = formsTemplate(template, opts.title);
|
|
6774
8253
|
requireValidForms(config, void 0, "The template", ctx.binName);
|
|
6775
|
-
await
|
|
8254
|
+
await writeFile6(file, `${formatJson(config)}
|
|
6776
8255
|
`);
|
|
6777
8256
|
ctx.log.success(`Wrote ${shown(file)} (${template}: ${FORM_TEMPLATE_DESCRIPTIONS[template]})`);
|
|
6778
8257
|
ctx.out.data(
|
|
6779
8258
|
{ path: file, template, forms: config },
|
|
6780
|
-
() =>
|
|
8259
|
+
() => chalk9.dim(
|
|
6781
8260
|
`Change its options, wording and paths to fit, check it with \`${ctx.binName} forms check ${shown(dirname3(file))}\`, then deploy the folder.`
|
|
6782
8261
|
)
|
|
6783
8262
|
);
|
|
6784
8263
|
}
|
|
6785
8264
|
async function runFormsCheck(ctx, path3) {
|
|
6786
8265
|
const target = resolve4(path3 ?? ".");
|
|
6787
|
-
const info = await
|
|
8266
|
+
const info = await stat7(target).catch(() => null);
|
|
6788
8267
|
if (!info) throw new CliError(`No such file or folder: ${path3 ?? "."}`);
|
|
6789
8268
|
const folder = info.isDirectory() ? target : dirname3(target);
|
|
6790
|
-
const file = info.isDirectory() ?
|
|
8269
|
+
const file = info.isDirectory() ? join11(target, FORMS_FILE) : target;
|
|
6791
8270
|
const config = info.isDirectory() ? (await readFormsFile(folder))?.config : await readJson(file);
|
|
6792
8271
|
if (config === void 0) {
|
|
6793
8272
|
throw new CliError(`No ${FORMS_FILE} in ${shown(folder)}`, {
|
|
@@ -6808,7 +8287,7 @@ async function runFormsCheck(ctx, path3) {
|
|
|
6808
8287
|
}
|
|
6809
8288
|
async function readJson(file) {
|
|
6810
8289
|
try {
|
|
6811
|
-
return JSON.parse(await
|
|
8290
|
+
return JSON.parse(await readFile10(file, "utf8"));
|
|
6812
8291
|
} catch (error) {
|
|
6813
8292
|
throw new CliError(`${shown(file)} is not valid JSON: ${error.message}`);
|
|
6814
8293
|
}
|
|
@@ -6822,21 +8301,21 @@ async function runResponses(ctx, name, opts) {
|
|
|
6822
8301
|
}
|
|
6823
8302
|
const { client, instance } = await formsClient(ctx);
|
|
6824
8303
|
const exported = await getResponses(client, name, { form: opts.form, path: opts.path });
|
|
6825
|
-
const
|
|
8304
|
+
const page2 = instance ? responsesPageUrl(instance.appOrigin, name) : void 0;
|
|
6826
8305
|
if (format2 !== void 0 || opts.output !== void 0) {
|
|
6827
8306
|
const as = format2 ?? "json";
|
|
6828
8307
|
const body = as === "csv" ? responsesCsv(exported.responses, exported.forms) : as === "jsonl" ? responsesJsonl(exported.responses) : `${formatJson(exported)}
|
|
6829
8308
|
`;
|
|
6830
8309
|
if (opts.output !== void 0) {
|
|
6831
|
-
await
|
|
8310
|
+
await writeFile6(resolve4(opts.output), body);
|
|
6832
8311
|
ctx.log.success(
|
|
6833
8312
|
`Wrote ${exported.responses.length} response${exported.responses.length === 1 ? "" : "s"} to ${shown(resolve4(opts.output))}`
|
|
6834
8313
|
);
|
|
6835
8314
|
} else ctx.out.data(as === "json" ? exported : body, () => body.replace(/\n$/, ""));
|
|
6836
8315
|
return;
|
|
6837
8316
|
}
|
|
6838
|
-
ctx.out.data({ ...exported, ...
|
|
6839
|
-
if (!exported.title) return
|
|
8317
|
+
ctx.out.data({ ...exported, ...page2 ? { responsesUrl: page2 } : {} }, () => {
|
|
8318
|
+
if (!exported.title) return chalk9.dim(`${name} has no forms on its live version.`);
|
|
6840
8319
|
const byId = new Map(exported.forms.map((f) => [f.id, f]));
|
|
6841
8320
|
const table = renderTable(exported.responses, [
|
|
6842
8321
|
{ header: "PAGE", value: (r) => r.path },
|
|
@@ -6854,19 +8333,19 @@ async function runResponses(ctx, name, opts) {
|
|
|
6854
8333
|
}
|
|
6855
8334
|
]);
|
|
6856
8335
|
return [
|
|
6857
|
-
|
|
8336
|
+
chalk9.bold(`${exported.title} on ${name}`),
|
|
6858
8337
|
"",
|
|
6859
8338
|
table,
|
|
6860
8339
|
"",
|
|
6861
|
-
...progressLines(exported).map((l) =>
|
|
6862
|
-
...
|
|
6863
|
-
|
|
8340
|
+
...progressLines(exported).map((l) => chalk9.dim(l)),
|
|
8341
|
+
...page2 ? [chalk9.dim(`Progress and downloads: ${page2}`)] : [],
|
|
8342
|
+
chalk9.dim(`Export: \`${ctx.binName} responses ${name} --format csv -o responses.csv\``)
|
|
6864
8343
|
].join("\n");
|
|
6865
8344
|
});
|
|
6866
8345
|
}
|
|
6867
8346
|
|
|
6868
8347
|
// src/commands/groups.ts
|
|
6869
|
-
import
|
|
8348
|
+
import chalk10 from "chalk";
|
|
6870
8349
|
function requireGroups(ctx) {
|
|
6871
8350
|
const instance = ctx.config.instance;
|
|
6872
8351
|
if (instance && !groupsEnabled(instance)) throw new CliError(`${instance.name} has no groups`);
|
|
@@ -6887,7 +8366,7 @@ async function runGroups(ctx, opts) {
|
|
|
6887
8366
|
{ header: "NAME", value: (g) => g.name },
|
|
6888
8367
|
{ header: "PEOPLE", value: (g) => g.size },
|
|
6889
8368
|
{ header: "OWNER", value: (g) => g.owner === me.email ? "you" : g.owner }
|
|
6890
|
-
]) :
|
|
8369
|
+
]) : chalk10.dim(members2 ? "No group has exactly those people." : describeGroups([]))
|
|
6891
8370
|
);
|
|
6892
8371
|
}
|
|
6893
8372
|
async function runGroupShow(ctx, slug) {
|
|
@@ -6933,7 +8412,7 @@ async function runGroupDelete(ctx, slug, opts) {
|
|
|
6933
8412
|
}
|
|
6934
8413
|
|
|
6935
8414
|
// src/commands/notifications.ts
|
|
6936
|
-
import
|
|
8415
|
+
import chalk11 from "chalk";
|
|
6937
8416
|
var VALUES = ["on", "off", "default"];
|
|
6938
8417
|
function notificationsClient(ctx) {
|
|
6939
8418
|
const instance = ctx.config.instance;
|
|
@@ -6956,10 +8435,10 @@ function renderSettings(settings) {
|
|
|
6956
8435
|
})),
|
|
6957
8436
|
{ header: "WHEN", value: (e) => e.title }
|
|
6958
8437
|
]);
|
|
6959
|
-
const lines = [table, "",
|
|
8438
|
+
const lines = [table, "", chalk11.dim("* your choice; the rest are the defaults")];
|
|
6960
8439
|
if (settings.muted) {
|
|
6961
8440
|
lines.unshift(
|
|
6962
|
-
|
|
8441
|
+
chalk11.yellow(
|
|
6963
8442
|
"Muted: nothing is sent, whatever the table says (`fling notifications unmute` to undo)."
|
|
6964
8443
|
),
|
|
6965
8444
|
""
|
|
@@ -6977,54 +8456,190 @@ async function runNotificationsSet(ctx, event, channel, value) {
|
|
|
6977
8456
|
if (!VALUES.includes(value)) {
|
|
6978
8457
|
throw new CliError(`"${value}" is not on, off or default`);
|
|
6979
8458
|
}
|
|
6980
|
-
const current = await getNotifications(client, name);
|
|
6981
|
-
if (!current.events.some((e) => e.key === event)) {
|
|
6982
|
-
throw new CliError(`${name} has no event "${event}"`, {
|
|
6983
|
-
hint: `Events: ${current.events.map((e) => e.key).join(", ")}.`
|
|
8459
|
+
const current = await getNotifications(client, name);
|
|
8460
|
+
if (!current.events.some((e) => e.key === event)) {
|
|
8461
|
+
throw new CliError(`${name} has no event "${event}"`, {
|
|
8462
|
+
hint: `Events: ${current.events.map((e) => e.key).join(", ")}.`
|
|
8463
|
+
});
|
|
8464
|
+
}
|
|
8465
|
+
if (!current.channels.includes(channel)) {
|
|
8466
|
+
throw new CliError(`${name} has no channel "${channel}"`, {
|
|
8467
|
+
hint: `Channels: ${current.channels.join(", ")}.`
|
|
8468
|
+
});
|
|
8469
|
+
}
|
|
8470
|
+
const settings = await setNotifications(
|
|
8471
|
+
client,
|
|
8472
|
+
{ cells: [{ event, channel, value }] },
|
|
8473
|
+
name
|
|
8474
|
+
);
|
|
8475
|
+
const now = settings.cells[event]?.[channel] ?? "off";
|
|
8476
|
+
ctx.log.success(
|
|
8477
|
+
value === "default" ? `${event} on ${channel} is back to the default (${now})` : `${event} on ${channel} is ${now}`
|
|
8478
|
+
);
|
|
8479
|
+
if (settings.muted) ctx.log.warn("Everything is muted, so nothing is sent until you unmute.");
|
|
8480
|
+
ctx.out.data(settings, () => renderSettings(settings));
|
|
8481
|
+
}
|
|
8482
|
+
async function runNotificationsMute(ctx, muted) {
|
|
8483
|
+
const { client, name } = notificationsClient(ctx);
|
|
8484
|
+
const settings = await setNotifications(client, { muted }, name);
|
|
8485
|
+
ctx.log.success(
|
|
8486
|
+
muted ? `Muted: ${name} sends you nothing` : `Unmuted: ${name} sends what the table says`
|
|
8487
|
+
);
|
|
8488
|
+
if (ctx.json) ctx.out.data(settings, () => "");
|
|
8489
|
+
}
|
|
8490
|
+
async function runNotificationsSent(ctx) {
|
|
8491
|
+
const { client, name } = notificationsClient(ctx);
|
|
8492
|
+
const sent = await listSentNotifications(client, name);
|
|
8493
|
+
ctx.out.data(
|
|
8494
|
+
{ sent },
|
|
8495
|
+
() => sent.length ? renderTable(sent, [
|
|
8496
|
+
{ header: "SENT", value: (s) => formatDate(s.sentAt) },
|
|
8497
|
+
{ header: "CHANNEL", value: (s) => s.channel },
|
|
8498
|
+
{ header: "SUBJECT", value: (s) => s.subject },
|
|
8499
|
+
{ header: "EVENTS", value: (s) => s.eventKeys.join(", ") }
|
|
8500
|
+
]) : chalk11.dim("Nothing was sent to you in the last 30 days.")
|
|
8501
|
+
);
|
|
8502
|
+
}
|
|
8503
|
+
|
|
8504
|
+
// src/commands/paths.ts
|
|
8505
|
+
import { readFile as readFile11, stat as stat8, writeFile as writeFile7 } from "node:fs/promises";
|
|
8506
|
+
import { join as join12, relative as relative6, resolve as resolve5 } from "node:path";
|
|
8507
|
+
import chalk12 from "chalk";
|
|
8508
|
+
var pathsHomeChoice = (bin) => [
|
|
8509
|
+
"A site's home page (its root index.html) can list its Paths:",
|
|
8510
|
+
` default no index.html of its own: Fling serves a listing of the live Paths (a site whose first publish is a Path gets one). Nothing to do.`,
|
|
8511
|
+
` custom your own index.html with a Paths block, filled with the live Paths: \`${bin} paths init\` writes a starter (${PATHS_TEMPLATE_NAMES.join(", ")}); adapt it, keeping the block, \`${bin} paths check\` it, then deploy it as the site root (without --path).`,
|
|
8512
|
+
" none an index.html without a Paths block lists no Paths; a deploy then says so."
|
|
8513
|
+
];
|
|
8514
|
+
async function pathsClient(ctx) {
|
|
8515
|
+
const client = requireClient(ctx);
|
|
8516
|
+
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "paths");
|
|
8517
|
+
if (instance && !pathsEnabled(instance)) {
|
|
8518
|
+
throw new CliError(`${instance.name} has no Paths`, { hint: "Ask its operator to update it." });
|
|
8519
|
+
}
|
|
8520
|
+
return client;
|
|
8521
|
+
}
|
|
8522
|
+
function pathFolder2(raw) {
|
|
8523
|
+
const folder = normalizePathFolder(raw);
|
|
8524
|
+
if (folder === null) {
|
|
8525
|
+
throw new CliError(`Invalid Path "${raw}"`, {
|
|
8526
|
+
hint: `A Path is one top-level folder of the site, like PAY-412 (no dotfiles, at most ${MAX_PATH_FOLDER_LENGTH} characters).`
|
|
8527
|
+
});
|
|
8528
|
+
}
|
|
8529
|
+
return folder;
|
|
8530
|
+
}
|
|
8531
|
+
var stateOf = (p) => p.archived ? chalk12.yellow("archived") : p.currentVersionId ? "live" : chalk12.dim("unpublished");
|
|
8532
|
+
async function runPaths(ctx, name) {
|
|
8533
|
+
const paths2 = await listPaths(await pathsClient(ctx), name);
|
|
8534
|
+
if (!ctx.json) {
|
|
8535
|
+
if (paths2.length === 0) ctx.log.info(`${name} has no Paths yet`);
|
|
8536
|
+
for (const line of pathsHomeChoice(ctx.binName)) ctx.log.hint(line);
|
|
8537
|
+
}
|
|
8538
|
+
ctx.out.data(
|
|
8539
|
+
{ name, paths: paths2 },
|
|
8540
|
+
() => paths2.length === 0 ? chalk12.dim(
|
|
8541
|
+
`Publish a folder as one: \`${ctx.binName} deploy ./plan --name ${name} --path PAY-412\`.`
|
|
8542
|
+
) : renderTable(paths2, [
|
|
8543
|
+
{ header: "PATH", value: (p) => `/${p.folder}/` },
|
|
8544
|
+
{ header: "TITLE", value: (p) => p.title || "-" },
|
|
8545
|
+
{ header: "VERSION", value: (p) => p.currentVersionId ?? "-" },
|
|
8546
|
+
{ header: "STATE", value: stateOf },
|
|
8547
|
+
{ header: "PUBLISH", value: (p) => p.canPublish ? "yes" : "no" },
|
|
8548
|
+
{ header: "UPDATED", value: (p) => formatDate(p.updatedAt) }
|
|
8549
|
+
])
|
|
8550
|
+
);
|
|
8551
|
+
}
|
|
8552
|
+
async function runPathArchive(ctx, name, raw, unarchive = false) {
|
|
8553
|
+
const folder = pathFolder2(raw);
|
|
8554
|
+
const path3 = await archivePath(await pathsClient(ctx), name, folder, !unarchive);
|
|
8555
|
+
ctx.log.success(
|
|
8556
|
+
unarchive ? `/${folder}/ of ${name} restored` : `/${folder}/ of ${name} archived: it 404s until \`${ctx.binName} paths unarchive ${name} ${folder}\``
|
|
8557
|
+
);
|
|
8558
|
+
if (ctx.json) ctx.out.data(path3, () => "");
|
|
8559
|
+
else if (unarchive) ctx.out.text(path3.url);
|
|
8560
|
+
}
|
|
8561
|
+
async function runPathDelete(ctx, name, raw, opts) {
|
|
8562
|
+
const folder = pathFolder2(raw);
|
|
8563
|
+
const client = await pathsClient(ctx);
|
|
8564
|
+
if (!opts.yes) {
|
|
8565
|
+
if (!ctx.interactive) {
|
|
8566
|
+
throw new CliError(`Refusing to delete /${folder}/ of ${name} without confirmation`, {
|
|
8567
|
+
hint: "Pass --yes to delete."
|
|
8568
|
+
});
|
|
8569
|
+
}
|
|
8570
|
+
const ok = await ctx.confirm(
|
|
8571
|
+
`Delete /${folder}/ of ${name} and every version of it? This cannot be undone.`
|
|
8572
|
+
);
|
|
8573
|
+
if (!ok) throw new CliError("Cancelled");
|
|
8574
|
+
}
|
|
8575
|
+
const result = await deletePath(client, name, folder);
|
|
8576
|
+
ctx.log.success(`/${folder}/ of ${name} deleted`);
|
|
8577
|
+
if (ctx.json) ctx.out.data(result, () => "");
|
|
8578
|
+
}
|
|
8579
|
+
var shown2 = (path3) => {
|
|
8580
|
+
const rel = relative6(process.cwd(), path3);
|
|
8581
|
+
return rel === "" ? "." : rel.startsWith("..") ? path3 : rel;
|
|
8582
|
+
};
|
|
8583
|
+
async function runPathsInit(ctx, dir, opts) {
|
|
8584
|
+
const template = opts.template ?? "minimal";
|
|
8585
|
+
if (!isPathsTemplateName(template)) {
|
|
8586
|
+
throw new CliError(`Unknown template "${template}"`, {
|
|
8587
|
+
hint: `Use one of: ${PATHS_TEMPLATE_NAMES.join(", ")}.`
|
|
6984
8588
|
});
|
|
6985
8589
|
}
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
8590
|
+
const folder = resolve5(dir ?? ".");
|
|
8591
|
+
const info = await stat8(folder).catch(() => null);
|
|
8592
|
+
if (!info?.isDirectory()) throw new CliError(`No such folder: ${dir ?? "."}`);
|
|
8593
|
+
const file = join12(folder, "index.html");
|
|
8594
|
+
if (!opts.force && await stat8(file).catch(() => null)) {
|
|
8595
|
+
throw new CliError(`${shown2(file)} already exists`, {
|
|
8596
|
+
hint: `Pass --force to replace it with the template, or add a Paths block to it (\`${ctx.binName} paths check ${shown2(file)}\` says how).`
|
|
6989
8597
|
});
|
|
6990
8598
|
}
|
|
6991
|
-
const
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
8599
|
+
const html = pathsTemplate(template, { title: opts.title });
|
|
8600
|
+
await writeFile7(file, html);
|
|
8601
|
+
ctx.log.success(`Wrote ${shown2(file)} (${template}: ${PATHS_TEMPLATE_DESCRIPTIONS[template]})`);
|
|
8602
|
+
ctx.out.data(
|
|
8603
|
+
{ path: file, template },
|
|
8604
|
+
() => chalk12.dim(
|
|
8605
|
+
`Adapt its wording and look, keeping the <!-- fling:paths --> block; check it with \`${ctx.binName} paths check ${shown2(folder)}\`, then deploy the folder as the site root (without --path).`
|
|
8606
|
+
)
|
|
6999
8607
|
);
|
|
7000
|
-
if (settings.muted) ctx.log.warn("Everything is muted, so nothing is sent until you unmute.");
|
|
7001
|
-
ctx.out.data(settings, () => renderSettings(settings));
|
|
7002
8608
|
}
|
|
7003
|
-
async function
|
|
7004
|
-
const
|
|
7005
|
-
const
|
|
8609
|
+
async function runPathsCheck(ctx, path3) {
|
|
8610
|
+
const target = resolve5(path3 ?? ".");
|
|
8611
|
+
const info = await stat8(target).catch(() => null);
|
|
8612
|
+
if (!info) throw new CliError(`No such file or folder: ${path3 ?? "."}`);
|
|
8613
|
+
const file = info.isDirectory() ? join12(target, "index.html") : target;
|
|
8614
|
+
const html = await readFile11(file, "utf8").catch(() => null);
|
|
8615
|
+
if (html === null) {
|
|
8616
|
+
throw new CliError(`No ${shown2(file)}`, {
|
|
8617
|
+
hint: `\`${ctx.binName} paths init ${shown2(info.isDirectory() ? target : resolve5(target, ".."))}\` writes a starter.`
|
|
8618
|
+
});
|
|
8619
|
+
}
|
|
8620
|
+
const check2 = checkPathsIndex(html);
|
|
8621
|
+
for (const w of check2.warnings) ctx.log.warn(w);
|
|
8622
|
+
if (!check2.ok) {
|
|
8623
|
+
if (ctx.json) ctx.out.data({ path: file, ...check2 }, () => "");
|
|
8624
|
+
throw new CliError(`${shown2(file)} has no Paths block:
|
|
8625
|
+
${check2.errors.join("\n ")}`, {
|
|
8626
|
+
hint: `Placeholders in a template: ${PATHS_PLACEHOLDERS.map((p) => `{{${p}}}`).join(" ")}. Options: ${PATHS_OPTIONS.join(", ")}.`
|
|
8627
|
+
});
|
|
8628
|
+
}
|
|
7006
8629
|
ctx.log.success(
|
|
7007
|
-
|
|
8630
|
+
`${shown2(file)} has a ${check2.block} Paths block${check2.options.length ? ` (${check2.options.join(", ")})` : ""}`
|
|
7008
8631
|
);
|
|
7009
|
-
if (ctx.json) ctx.out.data(settings, () => "");
|
|
7010
|
-
}
|
|
7011
|
-
async function runNotificationsSent(ctx) {
|
|
7012
|
-
const { client, name } = notificationsClient(ctx);
|
|
7013
|
-
const sent = await listSentNotifications(client, name);
|
|
7014
8632
|
ctx.out.data(
|
|
7015
|
-
{
|
|
7016
|
-
() =>
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
{ header: "SUBJECT", value: (s) => s.subject },
|
|
7020
|
-
{ header: "EVENTS", value: (s) => s.eventKeys.join(", ") }
|
|
7021
|
-
]) : chalk10.dim("Nothing was sent to you in the last 30 days.")
|
|
8633
|
+
{ path: file, ...check2 },
|
|
8634
|
+
() => chalk12.dim(
|
|
8635
|
+
"Deploy it as the site root (without --path): the listing fills in with the live Paths."
|
|
8636
|
+
)
|
|
7022
8637
|
);
|
|
7023
8638
|
}
|
|
7024
8639
|
|
|
7025
8640
|
// src/commands/pull.ts
|
|
7026
|
-
import { relative as
|
|
7027
|
-
import
|
|
8641
|
+
import { relative as relative7, resolve as resolve6 } from "node:path";
|
|
8642
|
+
import chalk13 from "chalk";
|
|
7028
8643
|
async function runPull(ctx, name, dir, opts) {
|
|
7029
8644
|
const client = requireClient(ctx);
|
|
7030
8645
|
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "pull");
|
|
@@ -7035,15 +8650,15 @@ async function runPull(ctx, name, dir, opts) {
|
|
|
7035
8650
|
}
|
|
7036
8651
|
let path3;
|
|
7037
8652
|
if (opts.path !== void 0) {
|
|
7038
|
-
const folder =
|
|
8653
|
+
const folder = normalizePathFolder(opts.path);
|
|
7039
8654
|
if (folder === null) {
|
|
7040
8655
|
throw new CliError(`Invalid --path "${opts.path}"`, {
|
|
7041
|
-
hint: "Use
|
|
8656
|
+
hint: "Use one of the site\u2019s Paths, like PAY-412: `fling paths <site>` lists them."
|
|
7042
8657
|
});
|
|
7043
8658
|
}
|
|
7044
8659
|
path3 = folder;
|
|
7045
8660
|
}
|
|
7046
|
-
const target =
|
|
8661
|
+
const target = resolve6(dir ?? name);
|
|
7047
8662
|
let progressShown = false;
|
|
7048
8663
|
const pulled = await pullSite(
|
|
7049
8664
|
{ client, fetch: ctx.fetch },
|
|
@@ -7064,101 +8679,21 @@ async function runPull(ctx, name, dir, opts) {
|
|
|
7064
8679
|
).finally(() => {
|
|
7065
8680
|
if (progressShown) ctx.progress(null);
|
|
7066
8681
|
});
|
|
7067
|
-
const
|
|
8682
|
+
const shown3 = relative7(process.cwd(), pulled.dir) || ".";
|
|
7068
8683
|
ctx.log.success(
|
|
7069
|
-
`Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${
|
|
8684
|
+
`Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${shown3} ${chalk13.dim(`(${pulled.fileCount} files, ${formatBytes(pulled.totalBytes)}, version ${pulled.versionId})`)}`
|
|
7070
8685
|
);
|
|
7071
8686
|
ctx.out.data(
|
|
7072
8687
|
pulled,
|
|
7073
|
-
() =>
|
|
7074
|
-
`Change what you need, then \`${ctx.binName} deploy ${
|
|
8688
|
+
() => chalk13.dim(
|
|
8689
|
+
`Change what you need, then \`${ctx.binName} deploy ${shown3}\`: it publishes on version ${pulled.versionId}, and stops if someone publishes ${name} before you do.`
|
|
7075
8690
|
)
|
|
7076
8691
|
);
|
|
7077
8692
|
}
|
|
7078
8693
|
|
|
7079
8694
|
// src/commands/setup.ts
|
|
7080
8695
|
import { intro, outro, spinner } from "@clack/prompts";
|
|
7081
|
-
import
|
|
7082
|
-
|
|
7083
|
-
// src/claude.ts
|
|
7084
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
7085
|
-
import { access, mkdtemp as mkdtemp2, writeFile as writeFile5 } from "node:fs/promises";
|
|
7086
|
-
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
7087
|
-
import { join as join9 } from "node:path";
|
|
7088
|
-
var MARKETPLACE_SOURCE = REPO_SLUG;
|
|
7089
|
-
var MARKETPLACE_NAME = "fling";
|
|
7090
|
-
var PLUGIN_NAME = "fling";
|
|
7091
|
-
var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
7092
|
-
var MCPB_PATH = "/downloads/fling.mcpb";
|
|
7093
|
-
var runClaude = (args) => new Promise((resolve7) => {
|
|
7094
|
-
execFile2(
|
|
7095
|
-
"claude",
|
|
7096
|
-
[...args],
|
|
7097
|
-
{ timeout: 12e4, maxBuffer: 16 * 1024 * 1024, shell: process.platform === "win32" },
|
|
7098
|
-
(error, stdout, stderr) => {
|
|
7099
|
-
const missing = error?.code === "ENOENT";
|
|
7100
|
-
const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
|
|
7101
|
-
resolve7({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
7102
|
-
}
|
|
7103
|
-
);
|
|
7104
|
-
});
|
|
7105
|
-
async function claudeCodeStatus(run) {
|
|
7106
|
-
const version = await run(["--version"]);
|
|
7107
|
-
if (version.code !== 0) return { claude: "missing" };
|
|
7108
|
-
const claude = version.stdout.trim().split(/\s+/)[0] ?? "unknown";
|
|
7109
|
-
const list = await run(["plugin", "list", "--json"]);
|
|
7110
|
-
let plugins = [];
|
|
7111
|
-
try {
|
|
7112
|
-
plugins = JSON.parse(list.stdout);
|
|
7113
|
-
} catch {
|
|
7114
|
-
}
|
|
7115
|
-
const fling = Array.isArray(plugins) ? plugins.find((p) => p.id === PLUGIN_ID) : void 0;
|
|
7116
|
-
if (!fling) return { claude, installed: false };
|
|
7117
|
-
return {
|
|
7118
|
-
claude,
|
|
7119
|
-
installed: true,
|
|
7120
|
-
version: fling.version,
|
|
7121
|
-
scope: fling.scope,
|
|
7122
|
-
enabled: fling.enabled
|
|
7123
|
-
};
|
|
7124
|
-
}
|
|
7125
|
-
async function installPlugin(run, scope) {
|
|
7126
|
-
check(
|
|
7127
|
-
await run(["plugin", "install", PLUGIN_NAME, "--marketplace", MARKETPLACE_SOURCE, "-s", scope]),
|
|
7128
|
-
"install the Fling plugin"
|
|
7129
|
-
);
|
|
7130
|
-
}
|
|
7131
|
-
async function updatePlugin(run, scope) {
|
|
7132
|
-
check(await run(["plugin", "marketplace", "update", MARKETPLACE_NAME]), "update the marketplace");
|
|
7133
|
-
check(
|
|
7134
|
-
await run(["plugin", "update", PLUGIN_ID, ...scope ? ["-s", scope] : []]),
|
|
7135
|
-
"update the Fling plugin"
|
|
7136
|
-
);
|
|
7137
|
-
}
|
|
7138
|
-
function check(result, what) {
|
|
7139
|
-
if (result.code === 0) return;
|
|
7140
|
-
const detail = (result.stderr || result.stdout).trim().split("\n").at(-1);
|
|
7141
|
-
throw new Error(`Could not ${what}${detail ? `: ${detail}` : ""}`);
|
|
7142
|
-
}
|
|
7143
|
-
async function findClaudeDesktop(platform = process.platform, env = process.env, exists = (path3) => access(path3).then(
|
|
7144
|
-
() => true,
|
|
7145
|
-
() => false
|
|
7146
|
-
)) {
|
|
7147
|
-
const candidates = platform === "darwin" ? ["/Applications/Claude.app", join9(homedir3(), "Applications", "Claude.app")] : platform === "win32" && env.LOCALAPPDATA ? [join9(env.LOCALAPPDATA, "AnthropicClaude"), join9(env.LOCALAPPDATA, "Programs", "Claude")] : [];
|
|
7148
|
-
for (const path3 of candidates) if (await exists(path3)) return path3;
|
|
7149
|
-
return null;
|
|
7150
|
-
}
|
|
7151
|
-
async function downloadExtension(serverUrl, fetch2) {
|
|
7152
|
-
const url = new URL(MCPB_PATH, serverUrl).toString();
|
|
7153
|
-
const response = await fetch2(url);
|
|
7154
|
-
if (!response.ok) throw new Error(`Could not download ${url} (HTTP ${response.status})`);
|
|
7155
|
-
const dir = await mkdtemp2(join9(tmpdir2(), "fling-mcpb-"));
|
|
7156
|
-
const file = join9(dir, "fling.mcpb");
|
|
7157
|
-
await writeFile5(file, Buffer.from(await response.arrayBuffer()));
|
|
7158
|
-
return file;
|
|
7159
|
-
}
|
|
7160
|
-
|
|
7161
|
-
// src/commands/setup.ts
|
|
8696
|
+
import chalk14 from "chalk";
|
|
7162
8697
|
var SCOPES = ["user", "project", "local"];
|
|
7163
8698
|
async function runSetup(baseCtx, opts, deps = {}) {
|
|
7164
8699
|
const scope = opts.scope ?? "user";
|
|
@@ -7171,14 +8706,14 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
7171
8706
|
...baseCtx,
|
|
7172
8707
|
log: createLogger({
|
|
7173
8708
|
debug: baseCtx.config.debug,
|
|
7174
|
-
write: (line) => process.stderr.write(`${
|
|
8709
|
+
write: (line) => process.stderr.write(`${chalk14.gray("\u2502")} ${line}
|
|
7175
8710
|
`)
|
|
7176
8711
|
})
|
|
7177
8712
|
} : baseCtx;
|
|
7178
8713
|
if (fancy) {
|
|
7179
8714
|
process.stderr.write(`${logo()}
|
|
7180
8715
|
`);
|
|
7181
|
-
intro(
|
|
8716
|
+
intro(chalk14.bold(" fling setup "), { output: process.stderr });
|
|
7182
8717
|
}
|
|
7183
8718
|
const login = await stepLogin(ctx, opts.url);
|
|
7184
8719
|
const results = {
|
|
@@ -7192,7 +8727,7 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
7192
8727
|
const failed = Object.values(results).includes("failed");
|
|
7193
8728
|
if (fancy)
|
|
7194
8729
|
outro(
|
|
7195
|
-
failed ?
|
|
8730
|
+
failed ? chalk14.yellow("Setup finished with problems \u2014 see above, then run `fling setup` again.") : `You're set. Try ${chalk14.bold(`${ctx.binName} deploy ./dist`)}, or ask Claude to \u201Cfling this\u201D.`,
|
|
7196
8731
|
{ output: process.stderr }
|
|
7197
8732
|
);
|
|
7198
8733
|
ctx.out.data(results, () => "");
|
|
@@ -7215,7 +8750,10 @@ async function signIn(ctx, serverUrl) {
|
|
|
7215
8750
|
try {
|
|
7216
8751
|
const me = await whoami(requireClient(ctx));
|
|
7217
8752
|
if (ctx.config.remembered) await ctx.store.setCurrent(serverUrl);
|
|
7218
|
-
ctx.log.success(`Signed in as ${
|
|
8753
|
+
ctx.log.success(`Signed in as ${chalk14.bold(me.name)} ${chalk14.dim(`<${me.email}>`)}`);
|
|
8754
|
+
if (ctx.config.remembered)
|
|
8755
|
+
await fetchInstance(serverUrl, ctx.fetch).then((instance) => ctx.store.putServer(serverUrl, { instance }, { makeCurrent: false })).catch(() => {
|
|
8756
|
+
});
|
|
7219
8757
|
return "already";
|
|
7220
8758
|
} catch (error) {
|
|
7221
8759
|
if (exitCodeFor(error) !== 2) {
|
|
@@ -7236,48 +8774,80 @@ async function signIn(ctx, serverUrl) {
|
|
|
7236
8774
|
async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
7237
8775
|
const status = await claudeCodeStatus(run);
|
|
7238
8776
|
if (status.claude === "missing") {
|
|
7239
|
-
ctx.log.info(`Claude Code not found ${
|
|
8777
|
+
ctx.log.info(`Claude Code not found ${chalk14.dim("(no `claude` on your PATH) \u2014 skipped")}`);
|
|
7240
8778
|
ctx.log.hint(
|
|
7241
8779
|
`Once it is installed, run \`${ctx.binName} setup\` again, or in Claude Code: /plugin marketplace add ${MARKETPLACE_SOURCE} then /plugin install ${PLUGIN_ID}`
|
|
7242
8780
|
);
|
|
7243
8781
|
return "missing";
|
|
7244
8782
|
}
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
return installed ? "already" : "skipped";
|
|
8783
|
+
if ("installed" in status && status.installed) return refreshPlugin(ctx, run, status, fancy);
|
|
8784
|
+
if (ask && !await ctx.confirm("Install the Fling plugin (skill + MCP server) into Claude Code?")) {
|
|
8785
|
+
ctx.log.info(chalk14.dim("Claude Code plugin: skipped"));
|
|
8786
|
+
return "skipped";
|
|
7250
8787
|
}
|
|
7251
8788
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
7252
|
-
s?.start(
|
|
8789
|
+
s?.start("Installing the Claude Code plugin");
|
|
7253
8790
|
try {
|
|
7254
|
-
|
|
7255
|
-
else await installPlugin(run, scope);
|
|
8791
|
+
await installPlugin(run, scope);
|
|
7256
8792
|
} catch (error) {
|
|
7257
8793
|
const message = error instanceof Error ? error.message : String(error);
|
|
7258
8794
|
if (s) s.error(message);
|
|
7259
8795
|
else ctx.log.error(message);
|
|
7260
8796
|
return "failed";
|
|
7261
8797
|
}
|
|
7262
|
-
const done =
|
|
8798
|
+
const done = `Claude Code plugin ${PLUGIN_NAME} installed ${chalk14.dim(`(${scope} scope)`)}`;
|
|
7263
8799
|
if (s) s.stop(done);
|
|
7264
8800
|
else ctx.log.success(done);
|
|
7265
8801
|
ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick it up.");
|
|
7266
|
-
return
|
|
8802
|
+
return "done";
|
|
8803
|
+
}
|
|
8804
|
+
async function refreshPlugin(ctx, run, before, fancy) {
|
|
8805
|
+
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
8806
|
+
s?.start("Updating the Claude Code plugin");
|
|
8807
|
+
try {
|
|
8808
|
+
await updatePlugin(run, before.scope);
|
|
8809
|
+
} catch (error) {
|
|
8810
|
+
const message = `${error instanceof Error ? error.message : String(error)} \u2014 the installed plugin${before.version ? ` (${before.version})` : ""} is unchanged`;
|
|
8811
|
+
if (s) s.stop(chalk14.yellow(message));
|
|
8812
|
+
else ctx.log.warn(message);
|
|
8813
|
+
return "already";
|
|
8814
|
+
}
|
|
8815
|
+
const after = await claudeCodeStatus(run).catch(() => null);
|
|
8816
|
+
const version = after && "installed" in after && after.installed ? after.version : void 0;
|
|
8817
|
+
const unchanged = before.version !== void 0 && version === before.version;
|
|
8818
|
+
const done = unchanged ? `Claude Code plugin ${PLUGIN_NAME} is up to date ${chalk14.dim(`(${version})`)}` : before.version && version ? `Claude Code plugin ${PLUGIN_NAME} updated from ${before.version} to ${version}` : `Claude Code plugin ${PLUGIN_NAME} updated${version ? ` to ${version}` : ""}`;
|
|
8819
|
+
if (s) s.stop(done);
|
|
8820
|
+
else ctx.log.success(done);
|
|
8821
|
+
if (unchanged) return "already";
|
|
8822
|
+
ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick up the new version.");
|
|
8823
|
+
return "updated";
|
|
7267
8824
|
}
|
|
7268
8825
|
async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
7269
8826
|
const app = await (deps.findClaudeDesktop ?? findClaudeDesktop)();
|
|
7270
8827
|
if (!app) return "missing";
|
|
7271
8828
|
if (ask && !await ctx.confirm("Add the Fling extension to Claude Desktop too?")) {
|
|
7272
|
-
ctx.log.info(
|
|
8829
|
+
ctx.log.info(chalk14.dim("Claude Desktop extension: skipped"));
|
|
7273
8830
|
return "skipped";
|
|
7274
8831
|
}
|
|
7275
8832
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
7276
8833
|
s?.start("Downloading the Claude Desktop extension");
|
|
8834
|
+
const download2 = deps.downloadExtension ?? ((url) => downloadExtension(url, ctx.fetch));
|
|
7277
8835
|
try {
|
|
7278
|
-
const
|
|
7279
|
-
|
|
7280
|
-
|
|
8836
|
+
const sources = extensionSources(requireServer(ctx), ctx.config.instance?.release);
|
|
8837
|
+
let got;
|
|
8838
|
+
let lastError;
|
|
8839
|
+
for (const source of sources) {
|
|
8840
|
+
try {
|
|
8841
|
+
got = { file: await download2(source.url), version: source.version };
|
|
8842
|
+
break;
|
|
8843
|
+
} catch (error) {
|
|
8844
|
+
lastError = error;
|
|
8845
|
+
ctx.log.debug(`Could not download ${source.url}: ${String(error)}`);
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
if (!got) throw lastError;
|
|
8849
|
+
await ctx.open(got.file);
|
|
8850
|
+
const done = `Opened fling.mcpb${got.version ? ` ${got.version}` : ""} in Claude Desktop \u2014 click Install there to finish (it replaces an older version)`;
|
|
7281
8851
|
if (s) s.stop(done);
|
|
7282
8852
|
else ctx.log.success(done);
|
|
7283
8853
|
return "done";
|
|
@@ -7290,7 +8860,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
7290
8860
|
}
|
|
7291
8861
|
|
|
7292
8862
|
// src/commands/sites.ts
|
|
7293
|
-
import
|
|
8863
|
+
import chalk15 from "chalk";
|
|
7294
8864
|
async function runList(ctx, opts) {
|
|
7295
8865
|
const sites = await listSites(requireClient(ctx), opts);
|
|
7296
8866
|
ctx.out.data(
|
|
@@ -7326,12 +8896,12 @@ async function runListShared(ctx, opts) {
|
|
|
7326
8896
|
);
|
|
7327
8897
|
}
|
|
7328
8898
|
var accessLabel = (site) => ACCESS_LABELS[siteAccessLevel(site)];
|
|
7329
|
-
var row = (label, value) => `${
|
|
8899
|
+
var row = (label, value) => `${chalk15.bold(`${label}:`.padEnd(14))}${value}`;
|
|
7330
8900
|
function describeSite(site) {
|
|
7331
8901
|
return [
|
|
7332
8902
|
row("Name", site.name),
|
|
7333
8903
|
row("URL", site.url),
|
|
7334
|
-
row("Access", accessLabel(site) + (site.archived ?
|
|
8904
|
+
row("Access", accessLabel(site) + (site.archived ? chalk15.yellow(" (archived)") : "")),
|
|
7335
8905
|
row("SPA", site.spa ? "yes" : "no"),
|
|
7336
8906
|
row("Share link", site.shareUrl ?? "-"),
|
|
7337
8907
|
row("Version", site.currentVersionId ?? "-"),
|
|
@@ -7360,21 +8930,22 @@ function filesRow(site) {
|
|
|
7360
8930
|
var membersOf = (ctx) => ctx.config.instance ? privateAudience(ctx.config.instance) : "anyone signed in to this Fling";
|
|
7361
8931
|
async function runInfo(ctx, name) {
|
|
7362
8932
|
const site = await getSite(requireClient(ctx), name);
|
|
7363
|
-
const
|
|
8933
|
+
const access3 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${chalk15.bold("Who:".padEnd(14))}${describeAudience(site, membersOf(ctx))}`] : [];
|
|
7364
8934
|
ctx.out.data(
|
|
7365
8935
|
site,
|
|
7366
8936
|
() => [
|
|
7367
8937
|
...describeSite(site),
|
|
7368
|
-
...
|
|
8938
|
+
...access3,
|
|
7369
8939
|
...rolesRows(site),
|
|
7370
8940
|
"",
|
|
7371
|
-
|
|
8941
|
+
chalk15.dim(
|
|
7372
8942
|
`${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
|
|
7373
8943
|
)
|
|
7374
8944
|
].join("\n")
|
|
7375
8945
|
);
|
|
7376
8946
|
}
|
|
7377
|
-
async function runVersions(ctx, name) {
|
|
8947
|
+
async function runVersions(ctx, name, opts = {}) {
|
|
8948
|
+
if (opts.path !== void 0) return runPathVersions(ctx, name, pathFolder2(opts.path));
|
|
7378
8949
|
const site = await getSite(requireClient(ctx), name);
|
|
7379
8950
|
ctx.out.data(
|
|
7380
8951
|
{ name: site.name, currentVersionId: site.currentVersionId, versions: site.versions },
|
|
@@ -7389,14 +8960,36 @@ async function runVersions(ctx, name) {
|
|
|
7389
8960
|
])
|
|
7390
8961
|
);
|
|
7391
8962
|
}
|
|
7392
|
-
async function
|
|
7393
|
-
const
|
|
8963
|
+
async function runPathVersions(ctx, name, folder) {
|
|
8964
|
+
const path3 = await getPath(await pathsClient(ctx), name, folder);
|
|
8965
|
+
ctx.out.data(
|
|
8966
|
+
{
|
|
8967
|
+
name,
|
|
8968
|
+
path: path3.folder,
|
|
8969
|
+
currentVersionId: path3.currentVersionId,
|
|
8970
|
+
versions: path3.versions
|
|
8971
|
+
},
|
|
8972
|
+
() => renderTable(path3.versions, [
|
|
8973
|
+
{ header: "", value: (v) => v.id === path3.currentVersionId ? "*" : " " },
|
|
8974
|
+
{ header: "VERSION", value: (v) => v.id },
|
|
8975
|
+
{ header: "FILES", value: (v) => v.fileCount },
|
|
8976
|
+
{ header: "SIZE", value: (v) => formatBytes(v.totalBytes) },
|
|
8977
|
+
{ header: "BY", value: (v) => v.publishedBy ?? "-" },
|
|
8978
|
+
{ header: "CREATED", value: (v) => formatDate(v.createdAt) }
|
|
8979
|
+
])
|
|
8980
|
+
);
|
|
8981
|
+
}
|
|
8982
|
+
async function runFiles(ctx, name, versionId, opts = {}) {
|
|
8983
|
+
const folder = opts.path === void 0 ? void 0 : pathFolder2(opts.path);
|
|
8984
|
+
const result = folder === void 0 ? await versionFiles(requireClient(ctx), name, versionId) : await pathVersionFiles(await pathsClient(ctx), name, folder, versionId);
|
|
7394
8985
|
if (!result.files.some((f) => f.path === "index.html")) {
|
|
7395
|
-
ctx.log.warn(
|
|
8986
|
+
ctx.log.warn(
|
|
8987
|
+
folder === void 0 ? "No index.html at the root \u2014 the site root URL will 404" : `No index.html in /${folder}/ \u2014 it will 404`
|
|
8988
|
+
);
|
|
7396
8989
|
}
|
|
7397
8990
|
ctx.out.data(result, () => {
|
|
7398
8991
|
const n = result.files.length;
|
|
7399
|
-
const header =
|
|
8992
|
+
const header = chalk15.dim(
|
|
7400
8993
|
`${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
|
|
7401
8994
|
);
|
|
7402
8995
|
const table = renderTable(result.files, [
|
|
@@ -7408,7 +9001,16 @@ async function runFiles(ctx, name, versionId) {
|
|
|
7408
9001
|
${table}`;
|
|
7409
9002
|
});
|
|
7410
9003
|
}
|
|
7411
|
-
async function runRollback(ctx, name, versionId) {
|
|
9004
|
+
async function runRollback(ctx, name, versionId, opts = {}) {
|
|
9005
|
+
if (opts.path !== void 0) {
|
|
9006
|
+
const folder = pathFolder2(opts.path);
|
|
9007
|
+
const path3 = await rollbackPath(await pathsClient(ctx), name, folder, versionId);
|
|
9008
|
+
ctx.log.success(
|
|
9009
|
+
`/${folder}/ of ${name} now serves version ${path3.currentVersionId ?? versionId} (the rest of the site untouched)`
|
|
9010
|
+
);
|
|
9011
|
+
ctx.out.data(path3, () => path3.url);
|
|
9012
|
+
return;
|
|
9013
|
+
}
|
|
7412
9014
|
const site = await rollback(requireClient(ctx), name, versionId);
|
|
7413
9015
|
ctx.log.success(`${name} now serves version ${site.currentVersionId ?? versionId}`);
|
|
7414
9016
|
ctx.out.data(site, () => site.url);
|
|
@@ -7432,7 +9034,7 @@ async function runShare(ctx, name, opts) {
|
|
|
7432
9034
|
ctx.log.success("Share link created \u2014 anyone with it can view the site");
|
|
7433
9035
|
ctx.out.data(
|
|
7434
9036
|
ready === void 0 ? result : { ...result, ready },
|
|
7435
|
-
() => result.shareUrl ? result.shareUrl :
|
|
9037
|
+
() => result.shareUrl ? result.shareUrl : chalk15.dim("(no share link)")
|
|
7436
9038
|
);
|
|
7437
9039
|
}
|
|
7438
9040
|
async function runVisibility(ctx, name, visibility) {
|
|
@@ -7495,6 +9097,7 @@ async function runSetLevel(ctx, name, level, opts) {
|
|
|
7495
9097
|
ctx.out.data({ ...site, warnings }, () => describeAudience(site, membersOf(ctx)));
|
|
7496
9098
|
}
|
|
7497
9099
|
async function runRoles(ctx, which, name, opts) {
|
|
9100
|
+
if (opts.path !== void 0) return runPathRoles(ctx, which, name, pathFolder2(opts.path), opts);
|
|
7498
9101
|
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "siteRoles");
|
|
7499
9102
|
if (instance && !siteRolesEnabled(instance)) {
|
|
7500
9103
|
throw new CliError(`${instance.name} has no site owners and publishers yet`, {
|
|
@@ -7520,6 +9123,23 @@ async function runRoles(ctx, which, name, opts) {
|
|
|
7520
9123
|
() => describeRoles(site)
|
|
7521
9124
|
);
|
|
7522
9125
|
}
|
|
9126
|
+
async function runPathRoles(ctx, which, name, folder, opts) {
|
|
9127
|
+
const client = await pathsClient(ctx);
|
|
9128
|
+
const change = {
|
|
9129
|
+
...opts.set ? { set: splitEmails(opts.set) } : {},
|
|
9130
|
+
add: splitEmails(opts.add ?? []),
|
|
9131
|
+
remove: splitEmails(opts.remove ?? []),
|
|
9132
|
+
addGroups: splitGroups(opts.group ?? []),
|
|
9133
|
+
removeGroups: splitGroups(opts.removeGroup ?? [])
|
|
9134
|
+
};
|
|
9135
|
+
const changed = changesAudience(change);
|
|
9136
|
+
const path3 = changed ? await setPathRoles(client, name, folder, which, change) : await getPathRoles(client, name, folder);
|
|
9137
|
+
if (changed) ctx.log.success(`The ${which} of /${folder}/ of ${name} changed`);
|
|
9138
|
+
ctx.out.data(
|
|
9139
|
+
{ name, path: path3.folder, owners: path3.owners, publishers: path3.publishers },
|
|
9140
|
+
() => `/${path3.folder}/: ${describeRoles(path3)}`
|
|
9141
|
+
);
|
|
9142
|
+
}
|
|
7523
9143
|
async function runArchive(ctx, name, unarchive = false) {
|
|
7524
9144
|
const site = await archiveSite(requireClient(ctx), name, { unarchive });
|
|
7525
9145
|
ctx.log.success(
|
|
@@ -7544,15 +9164,15 @@ async function runRemove(ctx, name, opts) {
|
|
|
7544
9164
|
}
|
|
7545
9165
|
|
|
7546
9166
|
// src/mcp.ts
|
|
7547
|
-
import { mkdir as
|
|
9167
|
+
import { mkdir as mkdir5, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
|
|
7548
9168
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
7549
|
-
import { basename as basename3, dirname as dirname4, join as
|
|
9169
|
+
import { basename as basename3, dirname as dirname4, join as join13, resolve as resolvePath } from "node:path";
|
|
7550
9170
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7551
9171
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7552
9172
|
import { z as z20 } from "zod";
|
|
7553
9173
|
|
|
7554
9174
|
// src/locate.ts
|
|
7555
|
-
import { lstat, readdir as readdir3, readFile as
|
|
9175
|
+
import { lstat, readdir as readdir3, readFile as readFile12, realpath as realpath4, stat as stat9 } from "node:fs/promises";
|
|
7556
9176
|
import { posix, win32 } from "node:path";
|
|
7557
9177
|
import { fileURLToPath } from "node:url";
|
|
7558
9178
|
var DOWNLOADS_DIR_ENV = "FLING_DOWNLOADS_DIR";
|
|
@@ -7565,7 +9185,7 @@ var SANDBOX_PATH_RE = /^\/(mnt\/user-data|mnt\/data|sessions|home\/claude|tmp\/o
|
|
|
7565
9185
|
var nodeFs = {
|
|
7566
9186
|
async stat(path3) {
|
|
7567
9187
|
try {
|
|
7568
|
-
const s = await
|
|
9188
|
+
const s = await stat9(path3);
|
|
7569
9189
|
return { isFile: s.isFile(), isDirectory: s.isDirectory(), mtimeMs: s.mtimeMs, size: s.size };
|
|
7570
9190
|
} catch {
|
|
7571
9191
|
return null;
|
|
@@ -7577,7 +9197,7 @@ var nodeFs = {
|
|
|
7577
9197
|
const out = [];
|
|
7578
9198
|
for (const e of entries) {
|
|
7579
9199
|
if (e.isSymbolicLink()) {
|
|
7580
|
-
const s = await
|
|
9200
|
+
const s = await stat9(`${path3}/${e.name}`).catch(() => null);
|
|
7581
9201
|
out.push({ name: e.name, isFile: Boolean(s?.isFile()), isDirectory: false });
|
|
7582
9202
|
} else out.push({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() });
|
|
7583
9203
|
}
|
|
@@ -7587,12 +9207,12 @@ var nodeFs = {
|
|
|
7587
9207
|
}
|
|
7588
9208
|
},
|
|
7589
9209
|
async realpath(path3) {
|
|
7590
|
-
return
|
|
9210
|
+
return realpath4(path3).catch(() => null);
|
|
7591
9211
|
},
|
|
7592
9212
|
async readText(path3) {
|
|
7593
9213
|
try {
|
|
7594
9214
|
if (!(await lstat(path3)).isFile()) return null;
|
|
7595
|
-
return await
|
|
9215
|
+
return await readFile12(path3, "utf8");
|
|
7596
9216
|
} catch {
|
|
7597
9217
|
return null;
|
|
7598
9218
|
}
|
|
@@ -7964,6 +9584,7 @@ var LOGIN_HINT = `Not logged in to Fling. Call fling_login (it opens the browser
|
|
|
7964
9584
|
var NO_SERVER_HINT = `No Fling server is set up on this computer. Ask the user for their Fling's address, then call fling_login with it as \`server\` (or ask them to run \`${NPX_COMMAND} login <url>\` in a terminal; in Claude Code: \`! ${NPX_COMMAND} login <url>\`), then try again.`;
|
|
7965
9585
|
var mcpLocalInstructions = (w, opts = {}) => [
|
|
7966
9586
|
`${w.name} publishes static sites (HTML/CSS/JS, images, documents) at ${w.sitesPattern}. This server runs on the user's computer.`,
|
|
9587
|
+
...opts.newerRelease ? [mcpUpdateNote(opts.newerRelease)] : [],
|
|
7967
9588
|
...[serversInstructions(opts.servers ?? [])].filter((p) => p !== void 0),
|
|
7968
9589
|
"Content you created in this conversation (an HTML page, a report, a small app): call fling_publish with every file inline (text as utf8, binary as base64). Never pass file paths from your sandbox (e.g. /mnt/user-data/outputs/\u2026, /sessions/\u2026) \u2014 this computer cannot see them \u2014 and never curl or upload from the sandbox.",
|
|
7969
9590
|
'A file or folder the user has on this computer (a downloaded .html, a .zip, a build folder): call fling_deploy with its path, or just its file name (e.g. "Pitch Deck.html") \u2014 it is looked up in Downloads, Desktop, Documents/Claude and Claude Desktop output folders. Use fling_find_files to see recent candidates when unsure which file the user means.',
|
|
@@ -7979,6 +9600,14 @@ var mcpLocalInstructions = (w, opts = {}) => [
|
|
|
7979
9600
|
] : [],
|
|
7980
9601
|
...opts.notifications ? [NOTIFICATIONS_NOTE(w)] : []
|
|
7981
9602
|
].join("\n\n");
|
|
9603
|
+
var PUBLISHING_TOOLS = /* @__PURE__ */ new Set(["fling_publish", "fling_deploy"]);
|
|
9604
|
+
function newestRelease(servers) {
|
|
9605
|
+
const newer = servers.map((s) => newerRelease(s.instance?.release)).filter((r) => r !== null);
|
|
9606
|
+
return newer.reduce(
|
|
9607
|
+
(best, r) => best && newerRelease(r, best) === null ? best : r,
|
|
9608
|
+
null
|
|
9609
|
+
);
|
|
9610
|
+
}
|
|
7982
9611
|
var onFling = (url, instance) => `
|
|
7983
9612
|
Fling: ${serverLabel(url)}${instance ? ` (${instance.name})` : ""}`;
|
|
7984
9613
|
function createMcpServer(deps = {}) {
|
|
@@ -7999,8 +9628,8 @@ function createMcpServer(deps = {}) {
|
|
|
7999
9628
|
inputSchema: { ...def.inputSchema, server: serverArg },
|
|
8000
9629
|
annotations: def.annotations
|
|
8001
9630
|
});
|
|
8002
|
-
async function edgeReady(url,
|
|
8003
|
-
const r = await waitReady(url,
|
|
9631
|
+
async function edgeReady(url, paths2) {
|
|
9632
|
+
const r = await waitReady(url, paths2).catch(() => ({ ready: false, waitedMs: 0 }));
|
|
8004
9633
|
return {
|
|
8005
9634
|
ready: r.ready,
|
|
8006
9635
|
note: r.ready ? "" : `
|
|
@@ -8025,7 +9654,12 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
8025
9654
|
const url = config.serverUrl;
|
|
8026
9655
|
let instance;
|
|
8027
9656
|
return {
|
|
8028
|
-
client: createApiClient({
|
|
9657
|
+
client: createApiClient({
|
|
9658
|
+
serverUrl: url,
|
|
9659
|
+
apiKey: config.apiKey,
|
|
9660
|
+
fetch: fetchImpl,
|
|
9661
|
+
...releaseTracking(store, config)
|
|
9662
|
+
}),
|
|
8029
9663
|
url,
|
|
8030
9664
|
config,
|
|
8031
9665
|
instance: () => {
|
|
@@ -8056,10 +9690,10 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
8056
9690
|
return instance;
|
|
8057
9691
|
}
|
|
8058
9692
|
async function publishFolder2(target2, raw) {
|
|
8059
|
-
const folder =
|
|
9693
|
+
const folder = normalizePathFolder(raw);
|
|
8060
9694
|
if (folder === null) {
|
|
8061
9695
|
throw new CliError(
|
|
8062
|
-
`Invalid path "${raw}": use
|
|
9696
|
+
`Invalid path "${raw}": use one top-level folder of the site, like "PAY-412" (no dotfiles, at most ${MAX_PATH_FOLDER_LENGTH} characters)`
|
|
8063
9697
|
);
|
|
8064
9698
|
}
|
|
8065
9699
|
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "pathPublish");
|
|
@@ -8078,6 +9712,23 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
8078
9712
|
);
|
|
8079
9713
|
}
|
|
8080
9714
|
}
|
|
9715
|
+
async function pathOn(target2, raw) {
|
|
9716
|
+
const folder = normalizePathFolder(raw);
|
|
9717
|
+
if (folder === null) {
|
|
9718
|
+
throw new CliError(
|
|
9719
|
+
`"${raw}" is not a Path: give one top-level folder of the site, like "PAY-412"`
|
|
9720
|
+
);
|
|
9721
|
+
}
|
|
9722
|
+
await requirePaths(target2);
|
|
9723
|
+
return folder;
|
|
9724
|
+
}
|
|
9725
|
+
async function requirePaths(target2) {
|
|
9726
|
+
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "paths");
|
|
9727
|
+
if (instance && !pathsEnabled(instance)) {
|
|
9728
|
+
throw new CliError(`${instance.name} (${serverLabel(target2.url)}) has no Paths`);
|
|
9729
|
+
}
|
|
9730
|
+
return instance;
|
|
9731
|
+
}
|
|
8081
9732
|
async function linkedServer(path3) {
|
|
8082
9733
|
try {
|
|
8083
9734
|
const located = await resolveInput(path3, locate);
|
|
@@ -8086,10 +9737,24 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
8086
9737
|
return void 0;
|
|
8087
9738
|
}
|
|
8088
9739
|
}
|
|
9740
|
+
let updateNoted = false;
|
|
9741
|
+
async function updateNoteFor(t2) {
|
|
9742
|
+
if (updateNoted) return "";
|
|
9743
|
+
const stored = await store.resolve({ serverUrl: t2.url }).catch(() => null);
|
|
9744
|
+
const release = newerRelease(
|
|
9745
|
+
(stored?.instance ?? await t2.instance().catch(() => null))?.release
|
|
9746
|
+
);
|
|
9747
|
+
if (!release) return "";
|
|
9748
|
+
updateNoted = true;
|
|
9749
|
+
return `
|
|
9750
|
+
${mcpUpdateNote(release)}`;
|
|
9751
|
+
}
|
|
8089
9752
|
async function run(tool2, server2, op) {
|
|
8090
9753
|
try {
|
|
8091
|
-
const
|
|
8092
|
-
|
|
9754
|
+
const t2 = await target(server2, tool2);
|
|
9755
|
+
const { summary, data } = await op(t2);
|
|
9756
|
+
const update = PUBLISHING_TOOLS.has(tool2) ? await updateNoteFor(t2) : "";
|
|
9757
|
+
return textResult(`${summary}${update}
|
|
8093
9758
|
|
|
8094
9759
|
${JSON.stringify(data, null, 2)}`);
|
|
8095
9760
|
} catch (error) {
|
|
@@ -8100,9 +9765,6 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
8100
9765
|
if (opts.access) await requireAccessLevels(t2);
|
|
8101
9766
|
const link = opts.link ?? null;
|
|
8102
9767
|
if (link && !opts.force || opts.base !== void 0) await requirePull(t2);
|
|
8103
|
-
if (opts.base !== void 0 && opts.folder !== void 0) {
|
|
8104
|
-
throw new CliError("Pass `base` or `path`, not both");
|
|
8105
|
-
}
|
|
8106
9768
|
const folder = opts.folder === void 0 ? void 0 : await publishFolder2(t2, opts.folder);
|
|
8107
9769
|
const shownFolder = folder ?? link?.path;
|
|
8108
9770
|
const inFolder = (file) => shownFolder === void 0 ? file : `${shownFolder}/${file}`;
|
|
@@ -8113,17 +9775,17 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
8113
9775
|
${l}.`).join("");
|
|
8114
9776
|
let generatedIndex = false;
|
|
8115
9777
|
const asItIs = link !== null || opts.base !== void 0;
|
|
8116
|
-
if (!asItIs && !hasRootIndex(local)) {
|
|
9778
|
+
if (!asItIs && !hasRootIndex(local, (await t2.instance())?.capabilities.markdown === true)) {
|
|
8117
9779
|
const pages = htmlPages(local).map((f) => f.path);
|
|
8118
9780
|
if (pages.length > 0 && opts.listHtml === void 0) {
|
|
8119
|
-
const
|
|
9781
|
+
const shown3 = pages.slice(0, 20);
|
|
8120
9782
|
return {
|
|
8121
9783
|
kind: "needs_decision",
|
|
8122
9784
|
summary: `Not published yet: there is no index.html ${top}, but there ${pages.length === 1 ? "is 1 HTML page" : `are ${pages.length} HTML pages`}. Ask the user whether to generate an index.html listing them (call again with \`listHtml: true\`) or to publish without one (\`listHtml: false\` \u2014 the root URL will 404, the pages still work at their own paths). Then call ${opts.retryWith} again with the same arguments plus \`listHtml\`.`,
|
|
8123
9785
|
data: {
|
|
8124
9786
|
status: "needs_decision",
|
|
8125
|
-
htmlPages:
|
|
8126
|
-
...pages.length >
|
|
9787
|
+
htmlPages: shown3,
|
|
9788
|
+
...pages.length > shown3.length ? { moreHtmlPages: pages.length - shown3.length } : {},
|
|
8127
9789
|
fileCount: local.files.length
|
|
8128
9790
|
}
|
|
8129
9791
|
};
|
|
@@ -8135,7 +9797,7 @@ ${l}.`).join("");
|
|
|
8135
9797
|
}
|
|
8136
9798
|
const onBase = link && !opts.force ? onLink(link, local) : null;
|
|
8137
9799
|
const changes = link ? onLink(link, local).changes : null;
|
|
8138
|
-
const layered =
|
|
9800
|
+
const layered = onBase !== null || opts.base !== void 0;
|
|
8139
9801
|
let instance = await t2.instance();
|
|
8140
9802
|
const forms2 = await planForms({
|
|
8141
9803
|
dir,
|
|
@@ -8145,7 +9807,8 @@ ${l}.`).join("");
|
|
|
8145
9807
|
// A Fling whose stored metadata predates forms is asked again before a forms file is refused.
|
|
8146
9808
|
instance: formsEnabled(instance) ? instance : await loadInstanceWith(store, t2.config, fetchImpl, "forms"),
|
|
8147
9809
|
bin: "fling",
|
|
8148
|
-
...opts.forms === void 0 ? {} : { inline: opts.forms }
|
|
9810
|
+
...opts.forms === void 0 ? {} : { inline: opts.forms },
|
|
9811
|
+
...shownFolder === void 0 ? {} : { path: shownFolder }
|
|
8149
9812
|
});
|
|
8150
9813
|
const result = await deploy(
|
|
8151
9814
|
{ client: t2.client, fetch: fetchImpl },
|
|
@@ -8153,10 +9816,9 @@ ${l}.`).join("");
|
|
|
8153
9816
|
dir,
|
|
8154
9817
|
name: opts.name,
|
|
8155
9818
|
manifest: local,
|
|
8156
|
-
path: folder,
|
|
9819
|
+
path: folder ?? onBase?.path,
|
|
8157
9820
|
...onBase ? {
|
|
8158
|
-
base: onBase.base
|
|
8159
|
-
...onBase.partial ? { partial: true, remove: onBase.remove, within: onBase.within } : {}
|
|
9821
|
+
base: onBase.base
|
|
8160
9822
|
} : opts.base !== void 0 ? { base: opts.base, partial: true, remove: opts.remove ?? [] } : {},
|
|
8161
9823
|
spa: opts.spa,
|
|
8162
9824
|
...opts.access ? {
|
|
@@ -8179,7 +9841,7 @@ ${l}.`).join("");
|
|
|
8179
9841
|
);
|
|
8180
9842
|
});
|
|
8181
9843
|
if (link) await writeSiteLink(dir, linkAfter(link, result.versionId, local, forms2.sha256));
|
|
8182
|
-
const what = result.status === "unchanged" ? "Unchanged \u2014 already live" : result.status === "repointed" ? `Restored version ${result.versionId}` : changes && link ? `Deployed ${describeChanges(changes)} since version ${link.versionId}${link.path === void 0 ? "" : ` in /${link.path}/`} (uploaded ${result.uploaded})` : opts.base !== void 0 ? `Published ${result.fileCount} changed or added file${result.fileCount === 1 ? "" : "s"} on version ${opts.base} (the rest
|
|
9844
|
+
const what = result.status === "unchanged" ? "Unchanged \u2014 already live" : result.status === "repointed" ? `Restored version ${result.versionId}` : changes && link ? `Deployed ${describeChanges(changes)} since version ${link.versionId}${link.path === void 0 ? "" : ` in /${link.path}/`} (uploaded ${result.uploaded})` : opts.base !== void 0 ? `Published ${result.fileCount} changed or added file${result.fileCount === 1 ? "" : "s"} on version ${opts.base} (the rest kept as it was)` : `Deployed ${result.fileCount} files${folder === void 0 ? "" : ` to the Path /${folder}/ (the rest of the site untouched)`} (uploaded ${result.uploaded})`;
|
|
8183
9845
|
const warnings = result.warnings.length ? `
|
|
8184
9846
|
Warnings:
|
|
8185
9847
|
- ${result.warnings.join("\n- ")}` : "";
|
|
@@ -8190,13 +9852,14 @@ Warnings:
|
|
|
8190
9852
|
const url = shownFolder === void 0 ? result.url : new URL(`${shownFolder}/`, result.url).href;
|
|
8191
9853
|
instance = await t2.instance();
|
|
8192
9854
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
9855
|
+
const onPath2 = shownFolder !== void 0;
|
|
8193
9856
|
let formsSummary = forms2.check?.summary ?? null;
|
|
8194
|
-
if (!formsSummary && forms2.send === void 0 && formsEnabled(instance)) {
|
|
9857
|
+
if (!formsSummary && !onPath2 && forms2.send === void 0 && formsEnabled(instance)) {
|
|
8195
9858
|
const live = await getForms(t2.client, result.name).catch(() => null);
|
|
8196
9859
|
if (live?.config) formsSummary = checkFormsConfig(live.config).summary;
|
|
8197
9860
|
}
|
|
8198
9861
|
const responsesPage = formsSummary && instance ? responsesPageUrl(instance.appOrigin, result.name) : void 0;
|
|
8199
|
-
const formsHint2 = formsEnabled(instance) && !formsSummary ? FORMS_HINT : void 0;
|
|
9862
|
+
const formsHint2 = formsEnabled(instance) && !formsSummary && !onPath2 ? FORMS_HINT : void 0;
|
|
8200
9863
|
const formsText = formsSummary && responsesPage ? `
|
|
8201
9864
|
${describeFormsSummary(formsSummary, responsesPage)}` : "";
|
|
8202
9865
|
const listing2 = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
|
|
@@ -8205,12 +9868,12 @@ ${describeFormsSummary(formsSummary, responsesPage)}` : "";
|
|
|
8205
9868
|
${shareForFeedback(review, instance?.name ?? "Fling", formsSummary !== null)}
|
|
8206
9869
|
Site: ${url} (${level})${formsText}` : `${what}: ${url} (${level})${listing2}${formsText}`;
|
|
8207
9870
|
const who = describeSiteAccess(result.site, await membersOn(t2));
|
|
8208
|
-
const
|
|
9871
|
+
const access4 = result.first ? `
|
|
8209
9872
|
Who can open it: ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : opts.access ? `
|
|
8210
9873
|
Its access is unchanged (deploying never changes it for an existing site): ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : "";
|
|
8211
9874
|
return {
|
|
8212
9875
|
kind: "deployed",
|
|
8213
|
-
summary: `${head}${
|
|
9876
|
+
summary: `${head}${access4}${warnings}${describePublishHints(result.hints)}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
8214
9877
|
To check exactly which files the site has, call fling_files with name "${result.name}".` + (formsHint2 ? `
|
|
8215
9878
|
${formsHint2}` : ""),
|
|
8216
9879
|
data: {
|
|
@@ -8247,7 +9910,8 @@ ${formsHint2}` : ""),
|
|
|
8247
9910
|
comments: servers.some((s) => commentsEnabled(s.instance)),
|
|
8248
9911
|
forms: servers.some((s) => formsEnabled(s.instance)),
|
|
8249
9912
|
notifications: servers.some((s) => notificationsEnabled(s.instance)),
|
|
8250
|
-
servers
|
|
9913
|
+
servers,
|
|
9914
|
+
newerRelease: newestRelease(servers)
|
|
8251
9915
|
})
|
|
8252
9916
|
}
|
|
8253
9917
|
);
|
|
@@ -8255,7 +9919,7 @@ ${formsHint2}` : ""),
|
|
|
8255
9919
|
listHtml: listHtml2,
|
|
8256
9920
|
spa: spa2,
|
|
8257
9921
|
public: isPublic,
|
|
8258
|
-
access:
|
|
9922
|
+
access: access3,
|
|
8259
9923
|
emails: emails2,
|
|
8260
9924
|
groups: groups2,
|
|
8261
9925
|
forms: formsArg
|
|
@@ -8273,10 +9937,10 @@ ${formsHint2}` : ""),
|
|
|
8273
9937
|
"Site name (lowercase letters, digits, single hyphens; max 50). Defaults to a name generated from the file or folder"
|
|
8274
9938
|
),
|
|
8275
9939
|
sitePath: z20.string().min(1).max(1024).optional().describe(
|
|
8276
|
-
'Publish
|
|
9940
|
+
'Publish as the Path at this top-level folder of the site, e.g. "PAY-412" (served at <site>/PAY-412/); the rest of the site is untouched. Needs `name`. Omit to publish the site itself'
|
|
8277
9941
|
),
|
|
8278
9942
|
spa: spa2,
|
|
8279
|
-
access:
|
|
9943
|
+
access: access3,
|
|
8280
9944
|
emails: emails2,
|
|
8281
9945
|
groups: groups2,
|
|
8282
9946
|
public: isPublic,
|
|
@@ -8298,7 +9962,9 @@ ${formsHint2}` : ""),
|
|
|
8298
9962
|
"`sitePath` needs `name`: the site to publish into. Ask the user which site, then call fling_deploy again"
|
|
8299
9963
|
);
|
|
8300
9964
|
}
|
|
8301
|
-
const prepared = await prepareInput(located.path
|
|
9965
|
+
const prepared = await prepareInput(located.path, {
|
|
9966
|
+
markdown: (await t2.instance())?.capabilities.markdown === true
|
|
9967
|
+
});
|
|
8302
9968
|
try {
|
|
8303
9969
|
const found = prepared.kind === "folder" ? await readSiteLink(prepared.dir) : null;
|
|
8304
9970
|
const link = found && (args.name === void 0 || args.name === found.name) && args.sitePath === void 0 && found.server === t2.url ? found : null;
|
|
@@ -8352,7 +10018,7 @@ ${formsHint2}` : ""),
|
|
|
8352
10018
|
versionId: z20.string().min(1).max(100).optional().describe(
|
|
8353
10019
|
"Another version than the live one (owners and publishers; ids from fling_info)"
|
|
8354
10020
|
),
|
|
8355
|
-
path: z20.string().min(1).max(1024).optional().describe('Only this
|
|
10021
|
+
path: z20.string().min(1).max(1024).optional().describe('Only this Path of the site, e.g. "PAY-412"'),
|
|
8356
10022
|
force: z20.boolean().optional().describe("Write into the folder even if it is not empty"),
|
|
8357
10023
|
server: serverArg
|
|
8358
10024
|
},
|
|
@@ -8360,13 +10026,13 @@ ${formsHint2}` : ""),
|
|
|
8360
10026
|
},
|
|
8361
10027
|
(args) => run("fling_pull", args.server, async (t2) => {
|
|
8362
10028
|
await requirePull(t2);
|
|
8363
|
-
const path3 = args.path === void 0 ? void 0 :
|
|
10029
|
+
const path3 = args.path === void 0 ? void 0 : normalizePathFolder(args.path);
|
|
8364
10030
|
if (path3 === null) {
|
|
8365
10031
|
throw new CliError(
|
|
8366
|
-
`Invalid path "${args.path}": use
|
|
10032
|
+
`Invalid path "${args.path}": use one of the site's Paths, like "PAY-412"`
|
|
8367
10033
|
);
|
|
8368
10034
|
}
|
|
8369
|
-
const dir = args.dir === void 0 ?
|
|
10035
|
+
const dir = args.dir === void 0 ? join13(locate.home, "Documents", "Claude", "fling", args.name) : resolvePath(cwd, args.dir);
|
|
8370
10036
|
const pulled = await pullSite(
|
|
8371
10037
|
{ client: t2.client, fetch: fetchImpl },
|
|
8372
10038
|
{
|
|
@@ -8388,20 +10054,23 @@ ${formsHint2}` : ""),
|
|
|
8388
10054
|
serverTool(mcpTools2.readFiles),
|
|
8389
10055
|
(args) => run(mcpTools2.readFiles.name, args.server, async (t2) => {
|
|
8390
10056
|
await requirePull(t2);
|
|
8391
|
-
const path3 = args.path === void 0 ? void 0 :
|
|
8392
|
-
if (path3 === null)
|
|
10057
|
+
const path3 = args.path === void 0 ? void 0 : normalizePathFolder(args.path);
|
|
10058
|
+
if (path3 === null)
|
|
10059
|
+
throw new CliError(`Invalid path "${args.path}": use one of the site's Paths`);
|
|
8393
10060
|
const pulled = await t2.client.request(
|
|
8394
10061
|
"GET",
|
|
8395
10062
|
`/api/sites/${encodeURIComponent(args.name)}/pull`,
|
|
8396
10063
|
{ schema: PullResponseSchema, query: { version: args.versionId, path: path3 } }
|
|
8397
10064
|
);
|
|
8398
|
-
const
|
|
10065
|
+
const prefix = path3 === void 0 ? "" : `${path3}/`;
|
|
10066
|
+
const within2 = pulled.files.map((f) => ({ ...f, path: f.path.slice(prefix.length) }));
|
|
10067
|
+
const { page: page2, tooLarge, missing, nextCursor } = pageOfFiles(within2, {
|
|
8399
10068
|
paths: args.paths,
|
|
8400
10069
|
cursor: args.cursor,
|
|
8401
10070
|
budget: MCP_READ_MAX_BYTES
|
|
8402
10071
|
});
|
|
8403
10072
|
const read = await Promise.all(
|
|
8404
|
-
|
|
10073
|
+
page2.map(async ({ url, ...f }) => {
|
|
8405
10074
|
const res = await fetchImpl(url);
|
|
8406
10075
|
if (!res.ok) throw new CliError(`Could not read ${f.path}: HTTP ${res.status}`);
|
|
8407
10076
|
return { ...f, ...encodeForAgent(f.path, new Uint8Array(await res.arrayBuffer())) };
|
|
@@ -8414,6 +10083,7 @@ ${formsHint2}` : ""),
|
|
|
8414
10083
|
const result = {
|
|
8415
10084
|
name: args.name,
|
|
8416
10085
|
versionId: pulled.versionId,
|
|
10086
|
+
...path3 === void 0 ? {} : { path: path3 },
|
|
8417
10087
|
files,
|
|
8418
10088
|
missing,
|
|
8419
10089
|
...nextCursor ? { nextCursor } : {}
|
|
@@ -8472,7 +10142,7 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
8472
10142
|
}
|
|
8473
10143
|
if (args.files.length === 0 && !args.remove?.length)
|
|
8474
10144
|
throw new CliError("No files to publish");
|
|
8475
|
-
const tmp = await mkdtemp3(
|
|
10145
|
+
const tmp = await mkdtemp3(join13(tmpdir3(), "fling-publish-"));
|
|
8476
10146
|
try {
|
|
8477
10147
|
const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
|
|
8478
10148
|
return await deployFolder(t2, tmp, {
|
|
@@ -8621,6 +10291,29 @@ ${known.join("\n")}` : ""
|
|
|
8621
10291
|
}
|
|
8622
10292
|
}
|
|
8623
10293
|
);
|
|
10294
|
+
server.registerTool(
|
|
10295
|
+
"fling_doctor",
|
|
10296
|
+
{
|
|
10297
|
+
title: "Check this computer's Fling setup",
|
|
10298
|
+
description: "Diagnose Fling on the user's computer, read-only: the CLI's version (and the latest on npm), Node.js, the config, each Fling set up here (reachable, its release, whether the sign-in still works, its features), the Claude Code plugin and the Claude Desktop extension. One line per check (\u2714 ok, \u25B2 warning, \u2716 problem), each warning and problem with its fix. Call it when something about Fling does not work (sign-in, tools missing, versions), then tell the user the fixes; never run them for the user without asking.",
|
|
10299
|
+
inputSchema: {},
|
|
10300
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
10301
|
+
},
|
|
10302
|
+
async () => {
|
|
10303
|
+
try {
|
|
10304
|
+
const checks = await runChecks({
|
|
10305
|
+
...systemDoctorDeps(store, fetchImpl, runClaude, () => findClaudeDesktop()),
|
|
10306
|
+
...deps.doctor
|
|
10307
|
+
});
|
|
10308
|
+
const ok = doctorExitCode(checks) === 0;
|
|
10309
|
+
return textResult(`${describeChecks(checks)}
|
|
10310
|
+
|
|
10311
|
+
${JSON.stringify({ ok, checks }, null, 2)}`);
|
|
10312
|
+
} catch (error) {
|
|
10313
|
+
return failure(error);
|
|
10314
|
+
}
|
|
10315
|
+
}
|
|
10316
|
+
);
|
|
8624
10317
|
server.registerTool(
|
|
8625
10318
|
"fling_servers",
|
|
8626
10319
|
{
|
|
@@ -8696,8 +10389,12 @@ ${lines}` : ""}`,
|
|
|
8696
10389
|
t.info.name,
|
|
8697
10390
|
serverTool(t.info),
|
|
8698
10391
|
(args) => run(t.info.name, args.server, async (target2) => {
|
|
10392
|
+
if (args.path !== void 0) {
|
|
10393
|
+
const path3 = await getPath(target2.client, args.name, await pathOn(target2, args.path));
|
|
10394
|
+
return { summary: describePathDetail(args.name, path3), data: { name: args.name, ...path3 } };
|
|
10395
|
+
}
|
|
8699
10396
|
const site = await getSite(target2.client, args.name);
|
|
8700
|
-
const
|
|
10397
|
+
const access4 = site.audience ? `
|
|
8701
10398
|
${describeAudience(site, await membersOn(target2))}` : "";
|
|
8702
10399
|
const current = site.versions.find((v) => v.id === site.currentVersionId);
|
|
8703
10400
|
const by = current?.publishedBy ? ` by ${current.publishedBy}` : "";
|
|
@@ -8706,14 +10403,25 @@ ${describeAudience(site, await membersOn(target2))}` : "";
|
|
|
8706
10403
|
Current version published${by}${to}` : "";
|
|
8707
10404
|
const roles = site.owners ? `
|
|
8708
10405
|
${describeRoles(site)}` : "";
|
|
8709
|
-
return { summary: `${site.name}: ${site.url}${
|
|
10406
|
+
return { summary: `${site.name}: ${site.url}${access4}${roles}${published}`, data: site };
|
|
8710
10407
|
})
|
|
8711
10408
|
);
|
|
8712
10409
|
server.registerTool(
|
|
8713
10410
|
t.files.name,
|
|
8714
10411
|
serverTool(t.files),
|
|
8715
|
-
(args) => run(t.files.name, args.server, async (
|
|
8716
|
-
|
|
10412
|
+
(args) => run(t.files.name, args.server, async (target2) => {
|
|
10413
|
+
if (args.path !== void 0) {
|
|
10414
|
+
const folder = await pathOn(target2, args.path);
|
|
10415
|
+
const result2 = await pathVersionFiles(target2.client, args.name, folder, args.versionId);
|
|
10416
|
+
return {
|
|
10417
|
+
summary: describeVersionFiles(`${args.name} /${folder}/`, result2, {
|
|
10418
|
+
current: result2.current,
|
|
10419
|
+
url: result2.url
|
|
10420
|
+
}),
|
|
10421
|
+
data: result2
|
|
10422
|
+
};
|
|
10423
|
+
}
|
|
10424
|
+
const result = await versionFiles(target2.client, args.name, args.versionId);
|
|
8717
10425
|
return {
|
|
8718
10426
|
summary: describeVersionFiles(result.name, result, {
|
|
8719
10427
|
current: result.current,
|
|
@@ -8937,14 +10645,76 @@ ${describeGroups(groups3, me.email)}`,
|
|
|
8937
10645
|
server.registerTool(
|
|
8938
10646
|
t.rollback.name,
|
|
8939
10647
|
serverTool(t.rollback),
|
|
8940
|
-
(args) => run(t.rollback.name, args.server, async (
|
|
8941
|
-
|
|
10648
|
+
(args) => run(t.rollback.name, args.server, async (target2) => {
|
|
10649
|
+
if (args.path !== void 0) {
|
|
10650
|
+
const folder = await pathOn(target2, args.path);
|
|
10651
|
+
const path3 = await rollbackPath(target2.client, args.name, folder, args.versionId);
|
|
10652
|
+
return {
|
|
10653
|
+
summary: `/${folder}/ of ${args.name} now serves version ${path3.currentVersionId}: ${path3.url} (the rest of the site untouched)`,
|
|
10654
|
+
data: { name: args.name, ...path3 }
|
|
10655
|
+
};
|
|
10656
|
+
}
|
|
10657
|
+
const site = await rollback(target2.client, args.name, args.versionId);
|
|
8942
10658
|
return {
|
|
8943
10659
|
summary: `${site.name} now serves version ${site.currentVersionId}: ${site.url}`,
|
|
8944
10660
|
data: site
|
|
8945
10661
|
};
|
|
8946
10662
|
})
|
|
8947
10663
|
);
|
|
10664
|
+
if (servers.some((s) => pathsEnabled(s.instance))) {
|
|
10665
|
+
server.registerTool(
|
|
10666
|
+
t.paths.name,
|
|
10667
|
+
serverTool(t.paths),
|
|
10668
|
+
(args) => run(t.paths.name, args.server, async (target2) => {
|
|
10669
|
+
const instance = await requirePaths(target2);
|
|
10670
|
+
const paths2 = await listPaths(target2.client, args.name);
|
|
10671
|
+
return {
|
|
10672
|
+
summary: `${describePaths(args.name, paths2)}${onFling(target2.url, instance)}`,
|
|
10673
|
+
data: { name: args.name, paths: paths2 }
|
|
10674
|
+
};
|
|
10675
|
+
})
|
|
10676
|
+
);
|
|
10677
|
+
}
|
|
10678
|
+
server.registerTool(
|
|
10679
|
+
t.pathsTemplate.name,
|
|
10680
|
+
{
|
|
10681
|
+
title: t.pathsTemplate.title,
|
|
10682
|
+
description: `${t.pathsTemplate.description} To deploy a folder with it, save it as the folder's index.html.`,
|
|
10683
|
+
inputSchema: t.pathsTemplate.inputSchema,
|
|
10684
|
+
annotations: t.pathsTemplate.annotations
|
|
10685
|
+
},
|
|
10686
|
+
async (args) => {
|
|
10687
|
+
try {
|
|
10688
|
+
const html = pathsTemplate(args.template, { title: args.title });
|
|
10689
|
+
return textResult(
|
|
10690
|
+
`A starter "${args.template}" home page with its Paths block in place. Adapt it to what the user wants while keeping the block, check it with fling_paths_check, then publish it as the site's root index.html (without \`path\`), or save it as index.html at the top of a folder to deploy.
|
|
10691
|
+
|
|
10692
|
+
` + html
|
|
10693
|
+
);
|
|
10694
|
+
} catch (error) {
|
|
10695
|
+
return failure(error);
|
|
10696
|
+
}
|
|
10697
|
+
}
|
|
10698
|
+
);
|
|
10699
|
+
server.registerTool(
|
|
10700
|
+
t.pathsCheck.name,
|
|
10701
|
+
{
|
|
10702
|
+
title: t.pathsCheck.title,
|
|
10703
|
+
description: t.pathsCheck.description,
|
|
10704
|
+
inputSchema: t.pathsCheck.inputSchema,
|
|
10705
|
+
annotations: t.pathsCheck.annotations
|
|
10706
|
+
},
|
|
10707
|
+
async (args) => {
|
|
10708
|
+
try {
|
|
10709
|
+
const check2 = checkPathsIndex(args.html);
|
|
10710
|
+
return textResult(`${describePathsCheck(check2)}
|
|
10711
|
+
|
|
10712
|
+
${JSON.stringify(check2, null, 2)}`);
|
|
10713
|
+
} catch (error) {
|
|
10714
|
+
return failure(error);
|
|
10715
|
+
}
|
|
10716
|
+
}
|
|
10717
|
+
);
|
|
8948
10718
|
if (servers.some((s) => commentsEnabled(s.instance))) {
|
|
8949
10719
|
async function reviewOn(target2) {
|
|
8950
10720
|
const instance = await target2.instance();
|
|
@@ -9204,9 +10974,9 @@ async function writeInlineFiles(dir, files) {
|
|
|
9204
10974
|
total += bytes.length;
|
|
9205
10975
|
if (total > MAX_DEPLOY_BYTES)
|
|
9206
10976
|
throw new CliError(`The site is larger than ${formatBytes(MAX_DEPLOY_BYTES)}`);
|
|
9207
|
-
const target =
|
|
9208
|
-
await
|
|
9209
|
-
await
|
|
10977
|
+
const target = join13(dir, ...path3.split("/"));
|
|
10978
|
+
await mkdir5(dirname4(target), { recursive: true });
|
|
10979
|
+
await writeFile8(target, bytes);
|
|
9210
10980
|
}
|
|
9211
10981
|
if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
|
|
9212
10982
|
return { skippedDotfiles };
|
|
@@ -9250,10 +11020,10 @@ async function runMcpServer(deps = {}) {
|
|
|
9250
11020
|
}
|
|
9251
11021
|
|
|
9252
11022
|
// src/route.ts
|
|
9253
|
-
import { stat as
|
|
9254
|
-
import { resolve as
|
|
11023
|
+
import { stat as stat10 } from "node:fs/promises";
|
|
11024
|
+
import { resolve as resolve7 } from "node:path";
|
|
9255
11025
|
var PASS_THROUGH = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);
|
|
9256
|
-
var pathExists = (path3) =>
|
|
11026
|
+
var pathExists = (path3) => stat10(resolve7(path3)).then(
|
|
9257
11027
|
() => true,
|
|
9258
11028
|
() => false
|
|
9259
11029
|
);
|
|
@@ -9304,18 +11074,18 @@ program.name(BIN_NAME).description("Fling a folder of static files at Fling and
|
|
|
9304
11074
|
`server for this command: a URL, or the host of one you signed in to (default: $${ENV.url}, else the current one)`
|
|
9305
11075
|
).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
|
|
9306
11076
|
sortSubcommands: false,
|
|
9307
|
-
styleTitle: (title) =>
|
|
9308
|
-
styleCommandText: (text2) =>
|
|
11077
|
+
styleTitle: (title) => chalk16.bold(brand.primary(title)),
|
|
11078
|
+
styleCommandText: (text2) => chalk16.bold(text2),
|
|
9309
11079
|
styleSubcommandText: (text2) => brand.accent(text2),
|
|
9310
11080
|
styleOptionText: (text2) => brand.accent(text2),
|
|
9311
|
-
styleArgumentText: (text2) =>
|
|
11081
|
+
styleArgumentText: (text2) => chalk16.dim(text2)
|
|
9312
11082
|
}).addHelpText(
|
|
9313
11083
|
"before",
|
|
9314
11084
|
() => process.stdout.isTTY ? logo({ columns: process.stdout.columns }) : ""
|
|
9315
11085
|
).addHelpText(
|
|
9316
11086
|
"after",
|
|
9317
11087
|
() => `
|
|
9318
|
-
${
|
|
11088
|
+
${chalk16.bold(brand.primary("Examples:"))}
|
|
9319
11089
|
$ ${BIN_NAME} login https://fling.example.com sign in to your Fling (once; there is no default)
|
|
9320
11090
|
$ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
|
|
9321
11091
|
$ ${BIN_NAME} use list the Flings you signed in to; \`use <url>\` switches
|
|
@@ -9326,7 +11096,7 @@ ${chalk14.bold(brand.primary("Examples:"))}
|
|
|
9326
11096
|
$ ${BIN_NAME} site.zip a zip of a site
|
|
9327
11097
|
$ ${BIN_NAME} share my-app public link for people who can't sign in
|
|
9328
11098
|
|
|
9329
|
-
${
|
|
11099
|
+
${chalk16.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
|
|
9330
11100
|
Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`)}`
|
|
9331
11101
|
);
|
|
9332
11102
|
function action(handler) {
|
|
@@ -9341,6 +11111,7 @@ function action(handler) {
|
|
|
9341
11111
|
report(error, ctx);
|
|
9342
11112
|
process.exitCode = exitCodeFor(error);
|
|
9343
11113
|
}
|
|
11114
|
+
if (ctx) await noticeAfterCommand(ctx, globals.server);
|
|
9344
11115
|
};
|
|
9345
11116
|
}
|
|
9346
11117
|
function report(error, ctx) {
|
|
@@ -9350,7 +11121,7 @@ function report(error, ctx) {
|
|
|
9350
11121
|
if (error instanceof CliError && error.hint) log.hint(error.hint);
|
|
9351
11122
|
if (ctx?.config.debug && error instanceof Error && error.stack) log.hint(error.stack);
|
|
9352
11123
|
if (ctx?.json) {
|
|
9353
|
-
const { code, findings, paths } = error;
|
|
11124
|
+
const { code, findings, paths: paths2 } = error;
|
|
9354
11125
|
process.stdout.write(
|
|
9355
11126
|
`${formatJson({
|
|
9356
11127
|
error: {
|
|
@@ -9358,7 +11129,7 @@ function report(error, ctx) {
|
|
|
9358
11129
|
exitCode: exitCodeFor(error),
|
|
9359
11130
|
...typeof code === "string" ? { code } : {},
|
|
9360
11131
|
...Array.isArray(findings) ? { findings } : {},
|
|
9361
|
-
...Array.isArray(
|
|
11132
|
+
...Array.isArray(paths2) ? { paths: paths2 } : {}
|
|
9362
11133
|
}
|
|
9363
11134
|
})}
|
|
9364
11135
|
`
|
|
@@ -9372,7 +11143,7 @@ program.command("deploy [path]").helpGroup("Deploy:").description(
|
|
|
9372
11143
|
"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)"
|
|
9373
11144
|
).option(
|
|
9374
11145
|
"--path <folder>",
|
|
9375
|
-
"publish
|
|
11146
|
+
"publish the folder as the Path at this top-level folder of the site (e.g. PAY-412, served at <site>/PAY-412/), with its own versions, owners and publishers; the rest of the site is untouched; one folder, not nested; needs --name (see `paths`)"
|
|
9376
11147
|
).option("--spa", "single-page app: serve index.html for unknown paths").option(
|
|
9377
11148
|
"--access <level>",
|
|
9378
11149
|
"who can open a new site: only_me, members (everyone signed in), groups, people or public (default: the Fling's, usually only_me). A deployed site keeps its access: see `access`"
|
|
@@ -9395,7 +11166,7 @@ program.command("pull <name> [dir]").helpGroup("Deploy:").description(
|
|
|
9395
11166
|
).option(
|
|
9396
11167
|
"--version-id <id>",
|
|
9397
11168
|
"another version than the live one (owners and publishers; ids from `versions`)"
|
|
9398
|
-
).option("--path <folder>", "only this
|
|
11169
|
+
).option("--path <folder>", "only this Path of the site (e.g. PAY-412), at the top of dir").option("--force", "write into dir even if it is not empty").action(
|
|
9399
11170
|
action(
|
|
9400
11171
|
(ctx, name, dir, opts) => runPull(ctx, name, dir, opts)
|
|
9401
11172
|
)
|
|
@@ -9407,6 +11178,9 @@ program.command("setup [url]").helpGroup("Setup:").description(
|
|
|
9407
11178
|
(ctx, url, opts) => runSetup(ctx, { ...opts, url })
|
|
9408
11179
|
)
|
|
9409
11180
|
);
|
|
11181
|
+
program.command("doctor").helpGroup("Setup:").description(
|
|
11182
|
+
"check this machine: the CLI, its config, each Fling, Claude Code and Claude Desktop (changes nothing)"
|
|
11183
|
+
).action(action((ctx) => runDoctor(ctx)));
|
|
9410
11184
|
program.command("login [url]").helpGroup("Setup:").description(
|
|
9411
11185
|
"sign in through the browser to the Fling at url and store an API key; it becomes the current server (default: the current one)"
|
|
9412
11186
|
).action(
|
|
@@ -9436,11 +11210,20 @@ program.command("ls").helpGroup("Sites:").alias("list").description("list your s
|
|
|
9436
11210
|
)
|
|
9437
11211
|
);
|
|
9438
11212
|
program.command("info <name>").helpGroup("Sites:").description("show a site").action(action((ctx, name) => runInfo(ctx, name)));
|
|
9439
|
-
program.command("versions <name>").helpGroup("Sites:").description("list a site's versions (* = live)").action(action((ctx, name) => runVersions(ctx, name)));
|
|
9440
|
-
program.command("files <name> [versionId]").helpGroup("Sites:").description("list the files in a site's live version (or in versionId)").action(
|
|
9441
|
-
action(
|
|
11213
|
+
program.command("versions <name>").helpGroup("Sites:").description("list a site's versions (* = live)").option("--path <folder>", "one of the site's Paths (e.g. PAY-412): list its own versions").action(action((ctx, name, opts) => runVersions(ctx, name, opts)));
|
|
11214
|
+
program.command("files <name> [versionId]").helpGroup("Sites:").description("list the files in a site's live version (or in versionId)").option("--path <folder>", "one of the site's Paths (e.g. PAY-412): the files of its version").action(
|
|
11215
|
+
action(
|
|
11216
|
+
(ctx, name, versionId, opts) => runFiles(ctx, name, versionId, opts)
|
|
11217
|
+
)
|
|
11218
|
+
);
|
|
11219
|
+
program.command("rollback <name> <versionId>").helpGroup("Sites:").description("serve an earlier version again").option(
|
|
11220
|
+
"--path <folder>",
|
|
11221
|
+
"one of the site's Paths (e.g. PAY-412): roll back only it (ids from `versions --path`)"
|
|
11222
|
+
).action(
|
|
11223
|
+
action(
|
|
11224
|
+
(ctx, name, versionId, opts) => runRollback(ctx, name, versionId, opts)
|
|
11225
|
+
)
|
|
9442
11226
|
);
|
|
9443
|
-
program.command("rollback <name> <versionId>").helpGroup("Sites:").description("serve an earlier version again").action(action((ctx, name, versionId) => runRollback(ctx, name, versionId)));
|
|
9444
11227
|
program.command("share <name>").helpGroup("Sites:").description("print (or create) a public share link \u2014 anyone with it can view the site").option("--refresh", "replace the link; the old one stops working").option("--revoke", "remove the link").action(
|
|
9445
11228
|
action(
|
|
9446
11229
|
(ctx, name, opts) => runShare(ctx, name, opts)
|
|
@@ -9480,7 +11263,10 @@ for (const [which, does] of [
|
|
|
9480
11263
|
).option("--add <emails...>", `add these addresses (members of this Fling only)`).option("--remove <emails...>", "take these addresses out").option("--set <emails...>", `make these addresses the whole list (groups stay)`).option(
|
|
9481
11264
|
"--group <slugs...>",
|
|
9482
11265
|
"add these groups (see `fling groups`); whoever manages a group decides who is in it"
|
|
9483
|
-
).option("--remove-group <slugs...>", "take these groups out").
|
|
11266
|
+
).option("--remove-group <slugs...>", "take these groups out").option(
|
|
11267
|
+
"--path <folder>",
|
|
11268
|
+
`one of the site's Paths (e.g. PAY-412): its own ${which}, besides the site's owners`
|
|
11269
|
+
).action(
|
|
9484
11270
|
action(
|
|
9485
11271
|
(ctx, name, opts) => runRoles(ctx, which, name, opts)
|
|
9486
11272
|
)
|
|
@@ -9489,6 +11275,36 @@ for (const [which, does] of [
|
|
|
9489
11275
|
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)));
|
|
9490
11276
|
program.command("unarchive <name>").helpGroup("Sites:").description("bring an archived site back").action(action((ctx, name) => runArchive(ctx, name, true)));
|
|
9491
11277
|
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)));
|
|
11278
|
+
var paths = program.command("paths").helpGroup("Paths:").description(
|
|
11279
|
+
"a site's top-level folders published on their own (`deploy --path`), each with its own versions and owners (default: list)"
|
|
11280
|
+
);
|
|
11281
|
+
paths.command("list <name>", { isDefault: true }).alias("ls").description(
|
|
11282
|
+
"list a site's Paths (`paths <name>`): folder, title, version, state, whether you can publish, updated"
|
|
11283
|
+
).action(action((ctx, name) => runPaths(ctx, name)));
|
|
11284
|
+
paths.command("archive <name> <folder>").description("take a Path off the site (404) but keep its versions; undo with `paths unarchive`").action(action((ctx, name, folder) => runPathArchive(ctx, name, folder)));
|
|
11285
|
+
paths.command("unarchive <name> <folder>").description("bring an archived Path back").action(action((ctx, name, folder) => runPathArchive(ctx, name, folder, true)));
|
|
11286
|
+
paths.command("delete <name> <folder>").alias("rm").description("delete a Path and all its versions (asks first unless --yes)").option("-y, --yes", "do not ask for confirmation").action(
|
|
11287
|
+
action(
|
|
11288
|
+
(ctx, name, folder, opts) => runPathDelete(ctx, name, folder, opts)
|
|
11289
|
+
)
|
|
11290
|
+
);
|
|
11291
|
+
paths.command("init [dir]").description(
|
|
11292
|
+
"write a starter home page (index.html) listing the Paths into dir (default: .), to deploy as the site root"
|
|
11293
|
+
).addOption(
|
|
11294
|
+
new Option("-t, --template <name>", "how it lists them").choices(PATHS_TEMPLATE_NAMES).default("minimal")
|
|
11295
|
+
).option("--title <title>", "the page's title and heading").option("--force", "replace an existing index.html").addHelpText(
|
|
11296
|
+
"after",
|
|
11297
|
+
() => `
|
|
11298
|
+
Templates:
|
|
11299
|
+
${PATHS_TEMPLATE_NAMES.map((n) => ` ${n.padEnd(8)} ${PATHS_TEMPLATE_DESCRIPTIONS[n]}`).join("\n")}
|
|
11300
|
+
|
|
11301
|
+
${pathsHomeChoice(BIN_NAME).join("\n")}`
|
|
11302
|
+
).action(
|
|
11303
|
+
action(
|
|
11304
|
+
(ctx, dir, opts) => runPathsInit(ctx, dir, opts)
|
|
11305
|
+
)
|
|
11306
|
+
);
|
|
11307
|
+
paths.command("check [path]").description("check a home page's Paths block: a folder's index.html (default: .) or a file").action(action((ctx, path3) => runPathsCheck(ctx, path3)));
|
|
9492
11308
|
var groups = program.command("groups").helpGroup("Groups:").description(
|
|
9493
11309
|
"named lists of people anyone can use in an audience; only the owner changes one (default: list)"
|
|
9494
11310
|
);
|