@nearform/fling 0.9.0 → 0.10.1
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 +70 -20
- package/dist/cli.js +1164 -196
- package/package.json +2 -2
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({
|
|
@@ -2840,7 +2912,22 @@ var KvsSiteValueSchema = z12.object({
|
|
|
2840
2912
|
/** serve /index.html for unknown extensionless paths */
|
|
2841
2913
|
spa: z12.boolean(),
|
|
2842
2914
|
/** present when the site is restricted (private with an audience): the edge wants a site pass */
|
|
2843
|
-
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()
|
|
2844
2931
|
});
|
|
2845
2932
|
|
|
2846
2933
|
// ../../packages/shared/src/listing.ts
|
|
@@ -2861,10 +2948,10 @@ function extractHtmlTitle(html) {
|
|
|
2861
2948
|
function renderHtmlListing(siteName2, entries) {
|
|
2862
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);
|
|
2863
2950
|
const groups2 = /* @__PURE__ */ new Map();
|
|
2864
|
-
for (const
|
|
2865
|
-
const slash =
|
|
2866
|
-
const folder = slash === -1 ? "" :
|
|
2867
|
-
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]);
|
|
2868
2955
|
}
|
|
2869
2956
|
const sections = [...groups2.entries()].sort(([a], [b]) => a === "" ? -1 : b === "" ? 1 : a < b ? -1 : 1).map(([folder, items]) => {
|
|
2870
2957
|
const heading = folder ? `<h2>${escapeHtml(folder)}/</h2>` : "";
|
|
@@ -3125,6 +3212,385 @@ function describeNotificationSettings(settings) {
|
|
|
3125
3212
|
return lines.join("\n");
|
|
3126
3213
|
}
|
|
3127
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
|
+
|
|
3128
3594
|
// ../../packages/shared/src/read-files.ts
|
|
3129
3595
|
var MCP_READ_MAX_BYTES = 1.5 * 1024 * 1024;
|
|
3130
3596
|
function pageOfFiles(files, opts) {
|
|
@@ -3133,7 +3599,7 @@ function pageOfFiles(files, opts) {
|
|
|
3133
3599
|
const found = new Set(selected.map((f) => f.path));
|
|
3134
3600
|
const missing = opts.paths ? opts.paths.filter((p) => !found.has(p)) : [];
|
|
3135
3601
|
const start = Math.max(0, Number.parseInt(opts.cursor ?? "0", 10) || 0);
|
|
3136
|
-
const
|
|
3602
|
+
const page2 = [];
|
|
3137
3603
|
const tooLarge = [];
|
|
3138
3604
|
let used = 0;
|
|
3139
3605
|
for (let i = start; i < selected.length; i++) {
|
|
@@ -3142,11 +3608,11 @@ function pageOfFiles(files, opts) {
|
|
|
3142
3608
|
tooLarge.push(file);
|
|
3143
3609
|
continue;
|
|
3144
3610
|
}
|
|
3145
|
-
if (used + file.size > opts.budget) return { page, tooLarge, missing, nextCursor: String(i) };
|
|
3146
|
-
|
|
3611
|
+
if (used + file.size > opts.budget) return { page: page2, tooLarge, missing, nextCursor: String(i) };
|
|
3612
|
+
page2.push(file);
|
|
3147
3613
|
used += file.size;
|
|
3148
3614
|
}
|
|
3149
|
-
return { page, tooLarge, missing };
|
|
3615
|
+
return { page: page2, tooLarge, missing };
|
|
3150
3616
|
}
|
|
3151
3617
|
function encodeForAgent(path3, bytes) {
|
|
3152
3618
|
if (isScannablePath(path3) && !looksBinary(bytes)) {
|
|
@@ -3164,8 +3630,9 @@ function encodeForAgent(path3, bytes) {
|
|
|
3164
3630
|
function describeReadFiles(name, result) {
|
|
3165
3631
|
const read = result.files.filter((f) => !f.tooLarge);
|
|
3166
3632
|
const big = result.files.filter((f) => f.tooLarge);
|
|
3633
|
+
const of = result.path === void 0 ? name : `the Path /${result.path}/ of ${name}`;
|
|
3167
3634
|
const lines = [
|
|
3168
|
-
`${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)"}.`
|
|
3169
3636
|
];
|
|
3170
3637
|
if (big.length) {
|
|
3171
3638
|
lines.push(
|
|
@@ -3179,7 +3646,7 @@ function describeReadFiles(name, result) {
|
|
|
3179
3646
|
);
|
|
3180
3647
|
}
|
|
3181
3648
|
lines.push(
|
|
3182
|
-
`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.`
|
|
3183
3650
|
);
|
|
3184
3651
|
return lines.join("\n");
|
|
3185
3652
|
}
|
|
@@ -3232,14 +3699,20 @@ var listHtml = z14.boolean().optional().describe(
|
|
|
3232
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"
|
|
3233
3700
|
);
|
|
3234
3701
|
var siteName = z14.string().min(1).max(200).describe('Site name, e.g. "q3-report"');
|
|
3235
|
-
|
|
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).`;
|
|
3236
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.";
|
|
3237
3710
|
var base = z14.string().min(1).max(100).optional().describe(
|
|
3238
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`"
|
|
3239
3712
|
);
|
|
3240
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");
|
|
3241
3714
|
var publishPath = z14.string().min(1).max(1024).optional().describe(
|
|
3242
|
-
'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'
|
|
3243
3716
|
);
|
|
3244
3717
|
var sitePath = z14.string().min(1).max(1024).describe(
|
|
3245
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")'
|
|
@@ -3285,6 +3758,53 @@ function describeResponses(exported, opts = {}) {
|
|
|
3285
3758
|
...opts.responsesPage ? [`Progress and downloads (CSV, JSONL): ${opts.responsesPage}`] : []
|
|
3286
3759
|
].join("\n");
|
|
3287
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);
|
|
3288
3808
|
var encoding = z14.enum(["utf8", "base64"]).optional().describe('How `content` is encoded: "utf8" (default) for text, "base64" for binary files');
|
|
3289
3809
|
var spa = z14.boolean().optional().describe(
|
|
3290
3810
|
"Single-page app: serve index.html for unknown paths (client-side routing). Omit to keep the current setting"
|
|
@@ -3324,8 +3844,13 @@ function mcpToolsFor(w) {
|
|
|
3324
3844
|
info: tool({
|
|
3325
3845
|
name: "fling_info",
|
|
3326
3846
|
title: "Site details",
|
|
3327
|
-
description: "Show one site: URL, visibility, share link, current version and version history (ids for fling_rollback).",
|
|
3328
|
-
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
|
+
},
|
|
3329
3854
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3330
3855
|
}),
|
|
3331
3856
|
files: tool({
|
|
@@ -3334,7 +3859,10 @@ function mcpToolsFor(w) {
|
|
|
3334
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).",
|
|
3335
3860
|
inputSchema: {
|
|
3336
3861
|
name: siteName,
|
|
3337
|
-
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
|
+
)
|
|
3338
3866
|
},
|
|
3339
3867
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3340
3868
|
}),
|
|
@@ -3345,7 +3873,7 @@ function mcpToolsFor(w) {
|
|
|
3345
3873
|
inputSchema: {
|
|
3346
3874
|
name: siteName,
|
|
3347
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'),
|
|
3348
|
-
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)'),
|
|
3349
3877
|
cursor: z14.string().max(20).optional().describe("From the previous call, for the next files"),
|
|
3350
3878
|
versionId: z14.string().min(1).max(100).optional().describe("Version id (from fling_info); omit for the version being served")
|
|
3351
3879
|
},
|
|
@@ -3383,7 +3911,7 @@ function mcpToolsFor(w) {
|
|
|
3383
3911
|
public: isPublic,
|
|
3384
3912
|
listHtml,
|
|
3385
3913
|
forms: formsConfig.nullable().optional().describe(
|
|
3386
|
-
`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`
|
|
3387
3915
|
)
|
|
3388
3916
|
},
|
|
3389
3917
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
@@ -3421,7 +3949,7 @@ function mcpToolsFor(w) {
|
|
|
3421
3949
|
uploadFinish: tool({
|
|
3422
3950
|
name: "fling_upload_finish",
|
|
3423
3951
|
title: "Finish a chunked upload",
|
|
3424
|
-
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\`.`,
|
|
3425
3953
|
inputSchema: {
|
|
3426
3954
|
uploadId: z14.string().min(1).max(100).describe("From fling_upload_begin"),
|
|
3427
3955
|
listHtml
|
|
@@ -3700,13 +4228,42 @@ function mcpToolsFor(w) {
|
|
|
3700
4228
|
},
|
|
3701
4229
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
3702
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
|
+
}),
|
|
3703
4257
|
rollback: tool({
|
|
3704
4258
|
name: "fling_rollback",
|
|
3705
4259
|
title: "Roll back",
|
|
3706
|
-
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.",
|
|
3707
4261
|
inputSchema: {
|
|
3708
4262
|
name: siteName,
|
|
3709
|
-
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
|
+
)
|
|
3710
4267
|
},
|
|
3711
4268
|
annotations: { idempotentHint: true, openWorldHint: false }
|
|
3712
4269
|
})
|
|
@@ -3772,7 +4329,7 @@ function describeComments(list, opts = {}) {
|
|
|
3772
4329
|
var READY_TIMEOUT_MS = 15e3;
|
|
3773
4330
|
var NOT_READY_NOTE = "it can take a few more seconds to appear everywhere; reload if you see a 404";
|
|
3774
4331
|
var isReachableStatus = (status) => status >= 200 && status < 400 || status === 401 || status === 403;
|
|
3775
|
-
var defaultSleep = (ms) => new Promise((
|
|
4332
|
+
var defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
3776
4333
|
async function waitUntilReachable(url, opts = {}) {
|
|
3777
4334
|
const timeoutMs = opts.timeoutMs ?? READY_TIMEOUT_MS;
|
|
3778
4335
|
const doFetch = opts.fetch ?? ((u, init) => fetch(u, init));
|
|
@@ -3805,10 +4362,10 @@ async function waitUntilReachable(url, opts = {}) {
|
|
|
3805
4362
|
interval = Math.min(interval * 1.25, 2e3);
|
|
3806
4363
|
}
|
|
3807
4364
|
}
|
|
3808
|
-
function probeUrl(siteUrl2,
|
|
4365
|
+
function probeUrl(siteUrl2, paths2) {
|
|
3809
4366
|
const base2 = siteUrl2.endsWith("/") ? siteUrl2 : `${siteUrl2}/`;
|
|
3810
|
-
if (
|
|
3811
|
-
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];
|
|
3812
4369
|
return base2 + pick.split("/").map(encodeURIComponent).join("/");
|
|
3813
4370
|
}
|
|
3814
4371
|
function readinessWaitDisabled(env) {
|
|
@@ -3922,7 +4479,7 @@ var LiveServerMessageSchema = z16.discriminatedUnion("type", [
|
|
|
3922
4479
|
]);
|
|
3923
4480
|
|
|
3924
4481
|
// src/cli.ts
|
|
3925
|
-
import
|
|
4482
|
+
import chalk16 from "chalk";
|
|
3926
4483
|
import { Command, Option } from "commander";
|
|
3927
4484
|
|
|
3928
4485
|
// src/commands/auth.ts
|
|
@@ -3972,7 +4529,7 @@ function exitCodeFor(error) {
|
|
|
3972
4529
|
// package.json
|
|
3973
4530
|
var package_default = {
|
|
3974
4531
|
name: "@nearform/fling",
|
|
3975
|
-
version: "0.
|
|
4532
|
+
version: "0.10.1",
|
|
3976
4533
|
license: "MIT",
|
|
3977
4534
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
3978
4535
|
type: "module",
|
|
@@ -4019,7 +4576,7 @@ var package_default = {
|
|
|
4019
4576
|
esbuild: "^0.25.12",
|
|
4020
4577
|
tsx: "^4.23.13",
|
|
4021
4578
|
typescript: "^5.9.3",
|
|
4022
|
-
vitest: "^
|
|
4579
|
+
vitest: "^4.1.12"
|
|
4023
4580
|
}
|
|
4024
4581
|
};
|
|
4025
4582
|
|
|
@@ -4545,8 +5102,8 @@ async function startCallbackServer(options = {}) {
|
|
|
4545
5102
|
const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
4546
5103
|
let settle;
|
|
4547
5104
|
let timer;
|
|
4548
|
-
const result = new Promise((
|
|
4549
|
-
settle = { resolve:
|
|
5105
|
+
const result = new Promise((resolve8, reject) => {
|
|
5106
|
+
settle = { resolve: resolve8, reject };
|
|
4550
5107
|
}).finally(() => clearTimeout(timer));
|
|
4551
5108
|
result.catch(() => {
|
|
4552
5109
|
});
|
|
@@ -4607,7 +5164,7 @@ ${close ? "<script>setTimeout(function(){window.close()},800)</script>" : ""}
|
|
|
4607
5164
|
res.end(html);
|
|
4608
5165
|
}
|
|
4609
5166
|
function listenOnFreePort(server) {
|
|
4610
|
-
return new Promise((
|
|
5167
|
+
return new Promise((resolve8, reject) => {
|
|
4611
5168
|
let port = CALLBACK_PORT_START;
|
|
4612
5169
|
const tryNext = () => {
|
|
4613
5170
|
if (port > CALLBACK_PORT_END) {
|
|
@@ -4629,7 +5186,7 @@ function listenOnFreePort(server) {
|
|
|
4629
5186
|
});
|
|
4630
5187
|
server.listen(port, "127.0.0.1", () => {
|
|
4631
5188
|
server.removeAllListeners("error");
|
|
4632
|
-
|
|
5189
|
+
resolve8(port);
|
|
4633
5190
|
});
|
|
4634
5191
|
};
|
|
4635
5192
|
tryNext();
|
|
@@ -4647,20 +5204,20 @@ import chalk from "chalk";
|
|
|
4647
5204
|
var skipWait = async () => ({ ready: true, waitedMs: 0, skipped: true });
|
|
4648
5205
|
function createReadyWaiter(options = {}) {
|
|
4649
5206
|
if (readinessWaitDisabled(options.env ?? process.env)) return skipWait;
|
|
4650
|
-
return (url,
|
|
5207
|
+
return (url, paths2, opts) => waitUntilReachable(probeUrl(url, paths2), {
|
|
4651
5208
|
timeoutMs: options.timeoutMs,
|
|
4652
5209
|
fetch: options.fetch,
|
|
4653
5210
|
onWaiting: opts?.onWaiting
|
|
4654
5211
|
});
|
|
4655
5212
|
}
|
|
4656
5213
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4657
|
-
async function waitWithSpinner(waiter, url,
|
|
5214
|
+
async function waitWithSpinner(waiter, url, paths2, io) {
|
|
4658
5215
|
if (!waiter) return { ready: true, waitedMs: 0, skipped: true };
|
|
4659
5216
|
let frame = 0;
|
|
4660
5217
|
let timer;
|
|
4661
5218
|
const draw = () => io.progress(`${chalk.cyan(SPINNER[frame++ % SPINNER.length])} Waiting for the edge\u2026`);
|
|
4662
5219
|
try {
|
|
4663
|
-
const result = await waiter(url,
|
|
5220
|
+
const result = await waiter(url, paths2, {
|
|
4664
5221
|
onWaiting: () => {
|
|
4665
5222
|
draw();
|
|
4666
5223
|
timer = setInterval(draw, 100);
|
|
@@ -5012,6 +5569,73 @@ async function setRoles(client, name, which, change) {
|
|
|
5012
5569
|
});
|
|
5013
5570
|
return withRoles(site);
|
|
5014
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
|
+
}
|
|
5015
5639
|
var groupPath = (slug) => `/api/groups/${encodeURIComponent(slug)}`;
|
|
5016
5640
|
async function listGroups(client, opts = {}) {
|
|
5017
5641
|
const res = await client.request("GET", "/api/groups", {
|
|
@@ -5369,7 +5993,7 @@ import { join as join2, relative, sep } from "node:path";
|
|
|
5369
5993
|
import ignore from "ignore";
|
|
5370
5994
|
var GIT_TIMEOUT_MS = 2e4;
|
|
5371
5995
|
function git(cwd, args, input) {
|
|
5372
|
-
return new Promise((
|
|
5996
|
+
return new Promise((resolve8) => {
|
|
5373
5997
|
try {
|
|
5374
5998
|
const child = execFile(
|
|
5375
5999
|
"git",
|
|
@@ -5377,14 +6001,14 @@ function git(cwd, args, input) {
|
|
|
5377
6001
|
{ timeout: GIT_TIMEOUT_MS, maxBuffer: 256 * 1024 * 1024, encoding: "utf8" },
|
|
5378
6002
|
(error, stdout) => {
|
|
5379
6003
|
const code = error?.code;
|
|
5380
|
-
if (error && !(args[0] === "check-ignore" && code === 1))
|
|
5381
|
-
else
|
|
6004
|
+
if (error && !(args[0] === "check-ignore" && code === 1)) resolve8(null);
|
|
6005
|
+
else resolve8(stdout);
|
|
5382
6006
|
}
|
|
5383
6007
|
);
|
|
5384
|
-
child.on("error", () =>
|
|
6008
|
+
child.on("error", () => resolve8(null));
|
|
5385
6009
|
if (input !== void 0) child.stdin?.end(input);
|
|
5386
6010
|
} catch {
|
|
5387
|
-
|
|
6011
|
+
resolve8(null);
|
|
5388
6012
|
}
|
|
5389
6013
|
});
|
|
5390
6014
|
}
|
|
@@ -5475,15 +6099,15 @@ var CHARSET_TYPES = /* @__PURE__ */ new Set([
|
|
|
5475
6099
|
"image/svg+xml"
|
|
5476
6100
|
]);
|
|
5477
6101
|
var SecretFilesError = class extends CliError {
|
|
5478
|
-
constructor(
|
|
5479
|
-
const
|
|
6102
|
+
constructor(paths2) {
|
|
6103
|
+
const shown3 = paths2.slice(0, 20);
|
|
5480
6104
|
super(
|
|
5481
|
-
`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}`,
|
|
5482
6106
|
{
|
|
5483
|
-
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"}.`
|
|
5484
6108
|
}
|
|
5485
6109
|
);
|
|
5486
|
-
this.paths =
|
|
6110
|
+
this.paths = paths2;
|
|
5487
6111
|
this.name = "SecretFilesError";
|
|
5488
6112
|
}
|
|
5489
6113
|
code = "secret_file";
|
|
@@ -5672,7 +6296,7 @@ function checkLimits(files, dir) {
|
|
|
5672
6296
|
);
|
|
5673
6297
|
}
|
|
5674
6298
|
}
|
|
5675
|
-
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("");
|
|
5676
6300
|
var htmlPages = (local) => local.files.filter((f) => isHtmlPath(f.path) && f.path !== "index.html");
|
|
5677
6301
|
var TITLE_BYTES = 65536;
|
|
5678
6302
|
async function readHead(file) {
|
|
@@ -5871,14 +6495,14 @@ async function deploy(deps, opts) {
|
|
|
5871
6495
|
try {
|
|
5872
6496
|
return await deployOnce(deps, opts, name, local);
|
|
5873
6497
|
} catch (error) {
|
|
5874
|
-
if (opts.path === void 0 || !baseChanged(error)) throw error;
|
|
6498
|
+
if (opts.path === void 0 || opts.base !== void 0 || !baseChanged(error)) throw error;
|
|
5875
6499
|
return deployOnce(deps, opts, name, local);
|
|
5876
6500
|
}
|
|
5877
6501
|
}
|
|
5878
6502
|
async function deployOnce(deps, opts, name, local) {
|
|
5879
6503
|
const onBaseChanged = (error) => opts.base !== void 0 && baseChanged(error) ? new BaseChangedError(name, opts.base) : error;
|
|
5880
6504
|
const folder = opts.path;
|
|
5881
|
-
const prefix = folder
|
|
6505
|
+
const prefix = folder;
|
|
5882
6506
|
const sitePath4 = (file) => prefix === void 0 ? file : `${prefix}/${file}`;
|
|
5883
6507
|
const base2 = {
|
|
5884
6508
|
name,
|
|
@@ -5896,7 +6520,7 @@ async function deployOnce(deps, opts, name, local) {
|
|
|
5896
6520
|
...folder === void 0 ? {} : { path: folder },
|
|
5897
6521
|
...opts.base === void 0 ? {} : {
|
|
5898
6522
|
base: opts.base,
|
|
5899
|
-
...opts.partial ? { partial: true, remove: opts.remove ?? [] } : {}
|
|
6523
|
+
...opts.partial ? { partial: true, remove: (opts.remove ?? []).map(sitePath4) } : {}
|
|
5900
6524
|
},
|
|
5901
6525
|
...opts.spa !== void 0 ? { spa: opts.spa } : {},
|
|
5902
6526
|
...opts.public ? { visibility: "public" } : {},
|
|
@@ -5917,7 +6541,8 @@ async function deployOnce(deps, opts, name, local) {
|
|
|
5917
6541
|
versionId: res.versionId,
|
|
5918
6542
|
site: res.site,
|
|
5919
6543
|
uploaded: 0,
|
|
5920
|
-
first: false
|
|
6544
|
+
first: false,
|
|
6545
|
+
hints: res.hints ?? []
|
|
5921
6546
|
};
|
|
5922
6547
|
}
|
|
5923
6548
|
const byPath = new Map(local.files.map((f) => [sitePath4(f.path), f]));
|
|
@@ -5948,7 +6573,8 @@ async function deployOnce(deps, opts, name, local) {
|
|
|
5948
6573
|
versionId: fin.versionId,
|
|
5949
6574
|
site: fin.site,
|
|
5950
6575
|
uploaded: uploads.length,
|
|
5951
|
-
first: fin.first === true
|
|
6576
|
+
first: fin.first === true,
|
|
6577
|
+
hints: fin.hints ?? []
|
|
5952
6578
|
};
|
|
5953
6579
|
}
|
|
5954
6580
|
async function uploadWithRetry(deps, file, url, headers) {
|
|
@@ -5996,8 +6622,8 @@ async function readFormsFile(dir) {
|
|
|
5996
6622
|
});
|
|
5997
6623
|
}
|
|
5998
6624
|
}
|
|
5999
|
-
function requireValidForms(config,
|
|
6000
|
-
const check2 = checkFormsConfig(config,
|
|
6625
|
+
function requireValidForms(config, paths2, where, bin) {
|
|
6626
|
+
const check2 = checkFormsConfig(config, paths2);
|
|
6001
6627
|
if (!check2.ok) {
|
|
6002
6628
|
throw new CliError(
|
|
6003
6629
|
`${where} is not a forms config Fling can use:
|
|
@@ -6010,6 +6636,12 @@ function requireValidForms(config, paths, where, bin) {
|
|
|
6010
6636
|
async function planForms(opts) {
|
|
6011
6637
|
const file = opts.inline === void 0 ? await readFormsFile(opts.dir) : null;
|
|
6012
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
|
+
}
|
|
6013
6645
|
if (raw === void 0) {
|
|
6014
6646
|
if (!formsEnabled(opts.instance)) return { send: void 0, check: null };
|
|
6015
6647
|
const send = !opts.layered || opts.pulledWithForms ? null : void 0;
|
|
@@ -6077,7 +6709,7 @@ async function tempFolder() {
|
|
|
6077
6709
|
const dir = await mkdtemp(join5(tmpdir(), "fling-"));
|
|
6078
6710
|
return { dir, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
6079
6711
|
}
|
|
6080
|
-
async function prepareInput(path3) {
|
|
6712
|
+
async function prepareInput(path3, opts = {}) {
|
|
6081
6713
|
let info;
|
|
6082
6714
|
try {
|
|
6083
6715
|
info = await stat3(path3);
|
|
@@ -6118,7 +6750,9 @@ async function prepareInput(path3) {
|
|
|
6118
6750
|
} else {
|
|
6119
6751
|
const name = basename2(path3);
|
|
6120
6752
|
await copyFile(path3, join5(tmp.dir, name));
|
|
6121
|
-
|
|
6753
|
+
if (!(opts.markdown && isMarkdownPath(name))) {
|
|
6754
|
+
await writeFile2(join5(tmp.dir, "index.html"), wrapperPage(name));
|
|
6755
|
+
}
|
|
6122
6756
|
}
|
|
6123
6757
|
} catch (error) {
|
|
6124
6758
|
await tmp.cleanup();
|
|
@@ -6139,8 +6773,8 @@ function zipEntryPath(name) {
|
|
|
6139
6773
|
if (JUNK_FILES.has((segments.at(-1) ?? "").toLowerCase())) return null;
|
|
6140
6774
|
return segments.join("/");
|
|
6141
6775
|
}
|
|
6142
|
-
function unwrapSingleFolder(
|
|
6143
|
-
let current =
|
|
6776
|
+
function unwrapSingleFolder(paths2) {
|
|
6777
|
+
let current = paths2;
|
|
6144
6778
|
for (; ; ) {
|
|
6145
6779
|
const first = current[0]?.split("/")[0];
|
|
6146
6780
|
if (!first || !current.every((p) => p.includes("/") && p.split("/")[0] === first)) return current;
|
|
@@ -6197,11 +6831,11 @@ async function extractZip(data, dest, skippedDotfiles = []) {
|
|
|
6197
6831
|
`Refusing to extract the zip: it has paths that escape its folder (${unsafe.slice(0, 3).join(", ")})`
|
|
6198
6832
|
);
|
|
6199
6833
|
const names = Object.keys(entries);
|
|
6200
|
-
const
|
|
6834
|
+
const paths2 = /* @__PURE__ */ new Map();
|
|
6201
6835
|
const kept = names.map((n) => zipEntryPath(n));
|
|
6202
6836
|
const unwrapped = unwrapSingleFolder(kept);
|
|
6203
6837
|
names.forEach((n, i) => {
|
|
6204
|
-
|
|
6838
|
+
paths2.set(n, unwrapped[i]);
|
|
6205
6839
|
});
|
|
6206
6840
|
if (names.length === 0) throw new CliError("The zip has no files to fling");
|
|
6207
6841
|
const root = resolve2(dest);
|
|
@@ -6212,7 +6846,7 @@ async function extractZip(data, dest, skippedDotfiles = []) {
|
|
|
6212
6846
|
total += content.length;
|
|
6213
6847
|
if (content.length > MAX_FILE_BYTES || total > MAX_DEPLOY_BYTES)
|
|
6214
6848
|
throw new CliError(`The zip expands to more than the limits allow`);
|
|
6215
|
-
const rel =
|
|
6849
|
+
const rel = paths2.get(name);
|
|
6216
6850
|
const target = resolve2(root, ...rel.split("/"));
|
|
6217
6851
|
if (!target.startsWith(root + sep4)) throw new CliError(`Unsafe path in the zip: ${name}`);
|
|
6218
6852
|
await mkdir2(join5(target, ".."), { recursive: true });
|
|
@@ -6269,7 +6903,7 @@ var SiteLinkSchema = z19.object({
|
|
|
6269
6903
|
name: z19.string(),
|
|
6270
6904
|
/** the version pulled, or the one the last deploy made */
|
|
6271
6905
|
versionId: z19.string(),
|
|
6272
|
-
/** a pull of one
|
|
6906
|
+
/** a pull of one of the site's Paths (its folder): the folder is that Path */
|
|
6273
6907
|
path: z19.string().optional(),
|
|
6274
6908
|
/** every file pulled (or deployed), by site path: its SHA-256 */
|
|
6275
6909
|
files: z19.record(z19.string(), z19.string()),
|
|
@@ -6309,7 +6943,7 @@ function onLink(link, local) {
|
|
|
6309
6943
|
for (const path3 of Object.keys(link.files)) if (!(path3 in now)) changes.removed.push(path3);
|
|
6310
6944
|
return {
|
|
6311
6945
|
base: link.versionId,
|
|
6312
|
-
...link.path === void 0 ? {} : {
|
|
6946
|
+
...link.path === void 0 ? {} : { path: link.path },
|
|
6313
6947
|
changes
|
|
6314
6948
|
};
|
|
6315
6949
|
}
|
|
@@ -6494,7 +7128,9 @@ async function runDeploy(ctx, input, opts) {
|
|
|
6494
7128
|
hint: `Leave it out, then use \`${ctx.binName} access\` or \`${ctx.binName} visibility\`.`
|
|
6495
7129
|
});
|
|
6496
7130
|
}
|
|
6497
|
-
const prepared = await prepareInput(path3
|
|
7131
|
+
const prepared = await prepareInput(path3, {
|
|
7132
|
+
markdown: instance?.capabilities.markdown === true
|
|
7133
|
+
});
|
|
6498
7134
|
try {
|
|
6499
7135
|
const { access: _asked, path: _path, ...rest } = opts;
|
|
6500
7136
|
await deployPrepared(
|
|
@@ -6535,10 +7171,10 @@ async function linkedTo(ctx, link, opts) {
|
|
|
6535
7171
|
}
|
|
6536
7172
|
function publishFolder(ctx, opts) {
|
|
6537
7173
|
if (opts.path === void 0) return void 0;
|
|
6538
|
-
const folder =
|
|
7174
|
+
const folder = normalizePathFolder(opts.path);
|
|
6539
7175
|
if (folder === null) {
|
|
6540
7176
|
throw new CliError(`Invalid --path "${opts.path}"`, {
|
|
6541
|
-
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.`
|
|
6542
7178
|
});
|
|
6543
7179
|
}
|
|
6544
7180
|
if (opts.name === void 0) {
|
|
@@ -6559,7 +7195,7 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6559
7195
|
if (!link && opts.name === void 0 && ask) {
|
|
6560
7196
|
name = await confirmSiteName(ctx, client, name, base2);
|
|
6561
7197
|
}
|
|
6562
|
-
if (!link && !hasRootIndex(local)) {
|
|
7198
|
+
if (!link && !hasRootIndex(local, instance?.capabilities.markdown === true)) {
|
|
6563
7199
|
const pages = htmlPages(local).length;
|
|
6564
7200
|
const root = base2 ? siteUrlFor(name, base2) + (folder ? `${folder}/` : "") : "the site root";
|
|
6565
7201
|
const warning = `No index.html ${folder ? `in ${folder}/` : "at the root"} \u2014 ${root} will show a 404.`;
|
|
@@ -6574,10 +7210,11 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6574
7210
|
const forms2 = await planForms({
|
|
6575
7211
|
dir: prepared.dir,
|
|
6576
7212
|
pages: local.files.map((f) => inFolder(f.path)),
|
|
6577
|
-
layered:
|
|
7213
|
+
layered: onBase !== null,
|
|
6578
7214
|
pulledWithForms: onBase !== null && link?.forms !== void 0,
|
|
6579
7215
|
instance,
|
|
6580
|
-
bin: ctx.binName
|
|
7216
|
+
bin: ctx.binName,
|
|
7217
|
+
...folder === void 0 ? {} : { path: folder }
|
|
6581
7218
|
});
|
|
6582
7219
|
let progressShown = false;
|
|
6583
7220
|
const result = await deploy(
|
|
@@ -6586,10 +7223,9 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6586
7223
|
dir: prepared.dir,
|
|
6587
7224
|
name,
|
|
6588
7225
|
manifest: local,
|
|
6589
|
-
path: opts.folder,
|
|
7226
|
+
path: opts.folder ?? onBase?.path,
|
|
6590
7227
|
...onBase ? {
|
|
6591
|
-
base: onBase.base
|
|
6592
|
-
...onBase.partial ? { partial: true, remove: onBase.remove, within: onBase.within } : {}
|
|
7228
|
+
base: onBase.base
|
|
6593
7229
|
} : {},
|
|
6594
7230
|
spa: opts.spa ? true : void 0,
|
|
6595
7231
|
public: opts.public,
|
|
@@ -6622,6 +7258,7 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6622
7258
|
);
|
|
6623
7259
|
}
|
|
6624
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);
|
|
6625
7262
|
const summary = changes ? `${describeChanges(changes)} since ${link?.versionId}` : `${result.fileCount} files, ${formatBytes(result.totalBytes)}`;
|
|
6626
7263
|
if (result.status === "unchanged") ctx.log.info(`Unchanged ${chalk7.dim(`(${summary})`)}`);
|
|
6627
7264
|
else if (result.status === "repointed")
|
|
@@ -6631,12 +7268,15 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6631
7268
|
`Deployed ${result.name}${folder ? ` to /${folder}/` : ""} ${chalk7.dim(`(${summary}; uploaded ${result.uploaded}, version ${result.versionId})`)}`
|
|
6632
7269
|
);
|
|
6633
7270
|
const url = folder ? new URL(`${folder}/`, result.url).href : result.url;
|
|
6634
|
-
const ready = result.status === "unchanged" ? true : (
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
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
|
+
);
|
|
6640
7280
|
const members2 = instance ? privateAudience(instance) : "anyone signed in to this Fling";
|
|
6641
7281
|
const level = siteAccessLevel(result.site);
|
|
6642
7282
|
const who = describeSiteAccess(result.site, members2);
|
|
@@ -6655,16 +7295,18 @@ async function deployPrepared(ctx, client, instance, base2, prepared, opts, ask)
|
|
|
6655
7295
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
6656
7296
|
const reviewNote = instance ? `Share for feedback (reviewers sign in to ${instance.name}):` : "Share for feedback:";
|
|
6657
7297
|
let formsSummary = forms2.check?.summary ?? null;
|
|
6658
|
-
if (!formsSummary && forms2.send === void 0 && formsEnabled(instance)) {
|
|
7298
|
+
if (!formsSummary && folder === void 0 && forms2.send === void 0 && formsEnabled(instance)) {
|
|
6659
7299
|
const live = await getForms(client, result.name).catch(() => null);
|
|
6660
7300
|
if (live?.config) formsSummary = checkFormsConfig(live.config).summary;
|
|
6661
7301
|
}
|
|
6662
7302
|
const responsesUrl = formsSummary && instance ? responsesPageUrl(instance.appOrigin, result.name) : void 0;
|
|
6663
|
-
const hint = formsEnabled(instance) && !formsSummary ? formsHint(ctx.binName) : void 0;
|
|
7303
|
+
const hint = formsEnabled(instance) && !formsSummary && folder === void 0 ? formsHint(ctx.binName) : void 0;
|
|
6664
7304
|
const data = {
|
|
6665
7305
|
status: result.status,
|
|
6666
7306
|
site: result.site,
|
|
6667
7307
|
...folder ? { path: folder, url } : {},
|
|
7308
|
+
versionId: result.versionId,
|
|
7309
|
+
...result.hints.length ? { hints: result.hints } : {},
|
|
6668
7310
|
...changes && link ? { base: opts.force ? null : link.versionId, changes } : {},
|
|
6669
7311
|
access: level,
|
|
6670
7312
|
...result.first ? { created: true } : {},
|
|
@@ -6771,7 +7413,7 @@ var manifest_default = {
|
|
|
6771
7413
|
manifest_version: "0.3",
|
|
6772
7414
|
name: "fling",
|
|
6773
7415
|
display_name: "Fling",
|
|
6774
|
-
version: "0.
|
|
7416
|
+
version: "0.10.1",
|
|
6775
7417
|
description: "Publish HTML pages, zips and folders from your computer to Fling and get a link back.",
|
|
6776
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.",
|
|
6777
7419
|
author: {
|
|
@@ -6885,7 +7527,7 @@ var MARKETPLACE_NAME = "fling";
|
|
|
6885
7527
|
var PLUGIN_NAME = "fling";
|
|
6886
7528
|
var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
6887
7529
|
var MCPB_PATH = "/downloads/fling.mcpb";
|
|
6888
|
-
var runClaude = (args) => new Promise((
|
|
7530
|
+
var runClaude = (args) => new Promise((resolve8) => {
|
|
6889
7531
|
execFile2(
|
|
6890
7532
|
"claude",
|
|
6891
7533
|
[...args],
|
|
@@ -6893,7 +7535,7 @@ var runClaude = (args) => new Promise((resolve7) => {
|
|
|
6893
7535
|
(error, stdout, stderr) => {
|
|
6894
7536
|
const missing = error?.code === "ENOENT";
|
|
6895
7537
|
const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
|
|
6896
|
-
|
|
7538
|
+
resolve8({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
6897
7539
|
}
|
|
6898
7540
|
);
|
|
6899
7541
|
});
|
|
@@ -7124,12 +7766,12 @@ function systemDoctorDeps(store, fetch2, runClaude2, findClaudeDesktop2) {
|
|
|
7124
7766
|
platform,
|
|
7125
7767
|
runClaude: runClaude2,
|
|
7126
7768
|
findClaudeDesktop: findClaudeDesktop2,
|
|
7127
|
-
runProgram: (file, args) => new Promise((
|
|
7769
|
+
runProgram: (file, args) => new Promise((resolve8) => {
|
|
7128
7770
|
execFile3(
|
|
7129
7771
|
file,
|
|
7130
7772
|
[...args],
|
|
7131
7773
|
{ timeout: DEFAULT_TIMEOUT_MS, shell: platform === "win32" },
|
|
7132
|
-
(error, stdout) =>
|
|
7774
|
+
(error, stdout) => resolve8({ code: error ? 1 : 0, stdout: String(stdout) })
|
|
7133
7775
|
);
|
|
7134
7776
|
}),
|
|
7135
7777
|
selfPath: process.argv[1],
|
|
@@ -7659,7 +8301,7 @@ async function runResponses(ctx, name, opts) {
|
|
|
7659
8301
|
}
|
|
7660
8302
|
const { client, instance } = await formsClient(ctx);
|
|
7661
8303
|
const exported = await getResponses(client, name, { form: opts.form, path: opts.path });
|
|
7662
|
-
const
|
|
8304
|
+
const page2 = instance ? responsesPageUrl(instance.appOrigin, name) : void 0;
|
|
7663
8305
|
if (format2 !== void 0 || opts.output !== void 0) {
|
|
7664
8306
|
const as = format2 ?? "json";
|
|
7665
8307
|
const body = as === "csv" ? responsesCsv(exported.responses, exported.forms) : as === "jsonl" ? responsesJsonl(exported.responses) : `${formatJson(exported)}
|
|
@@ -7672,7 +8314,7 @@ async function runResponses(ctx, name, opts) {
|
|
|
7672
8314
|
} else ctx.out.data(as === "json" ? exported : body, () => body.replace(/\n$/, ""));
|
|
7673
8315
|
return;
|
|
7674
8316
|
}
|
|
7675
|
-
ctx.out.data({ ...exported, ...
|
|
8317
|
+
ctx.out.data({ ...exported, ...page2 ? { responsesUrl: page2 } : {} }, () => {
|
|
7676
8318
|
if (!exported.title) return chalk9.dim(`${name} has no forms on its live version.`);
|
|
7677
8319
|
const byId = new Map(exported.forms.map((f) => [f.id, f]));
|
|
7678
8320
|
const table = renderTable(exported.responses, [
|
|
@@ -7696,7 +8338,7 @@ async function runResponses(ctx, name, opts) {
|
|
|
7696
8338
|
table,
|
|
7697
8339
|
"",
|
|
7698
8340
|
...progressLines(exported).map((l) => chalk9.dim(l)),
|
|
7699
|
-
...
|
|
8341
|
+
...page2 ? [chalk9.dim(`Progress and downloads: ${page2}`)] : [],
|
|
7700
8342
|
chalk9.dim(`Export: \`${ctx.binName} responses ${name} --format csv -o responses.csv\``)
|
|
7701
8343
|
].join("\n");
|
|
7702
8344
|
});
|
|
@@ -7859,9 +8501,145 @@ async function runNotificationsSent(ctx) {
|
|
|
7859
8501
|
);
|
|
7860
8502
|
}
|
|
7861
8503
|
|
|
7862
|
-
// src/commands/
|
|
7863
|
-
import {
|
|
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";
|
|
7864
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(", ")}.`
|
|
8588
|
+
});
|
|
8589
|
+
}
|
|
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).`
|
|
8597
|
+
});
|
|
8598
|
+
}
|
|
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
|
+
)
|
|
8607
|
+
);
|
|
8608
|
+
}
|
|
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
|
+
}
|
|
8629
|
+
ctx.log.success(
|
|
8630
|
+
`${shown2(file)} has a ${check2.block} Paths block${check2.options.length ? ` (${check2.options.join(", ")})` : ""}`
|
|
8631
|
+
);
|
|
8632
|
+
ctx.out.data(
|
|
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
|
+
)
|
|
8637
|
+
);
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
// src/commands/pull.ts
|
|
8641
|
+
import { relative as relative7, resolve as resolve6 } from "node:path";
|
|
8642
|
+
import chalk13 from "chalk";
|
|
7865
8643
|
async function runPull(ctx, name, dir, opts) {
|
|
7866
8644
|
const client = requireClient(ctx);
|
|
7867
8645
|
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "pull");
|
|
@@ -7872,15 +8650,15 @@ async function runPull(ctx, name, dir, opts) {
|
|
|
7872
8650
|
}
|
|
7873
8651
|
let path3;
|
|
7874
8652
|
if (opts.path !== void 0) {
|
|
7875
|
-
const folder =
|
|
8653
|
+
const folder = normalizePathFolder(opts.path);
|
|
7876
8654
|
if (folder === null) {
|
|
7877
8655
|
throw new CliError(`Invalid --path "${opts.path}"`, {
|
|
7878
|
-
hint: "Use
|
|
8656
|
+
hint: "Use one of the site\u2019s Paths, like PAY-412: `fling paths <site>` lists them."
|
|
7879
8657
|
});
|
|
7880
8658
|
}
|
|
7881
8659
|
path3 = folder;
|
|
7882
8660
|
}
|
|
7883
|
-
const target =
|
|
8661
|
+
const target = resolve6(dir ?? name);
|
|
7884
8662
|
let progressShown = false;
|
|
7885
8663
|
const pulled = await pullSite(
|
|
7886
8664
|
{ client, fetch: ctx.fetch },
|
|
@@ -7901,21 +8679,21 @@ async function runPull(ctx, name, dir, opts) {
|
|
|
7901
8679
|
).finally(() => {
|
|
7902
8680
|
if (progressShown) ctx.progress(null);
|
|
7903
8681
|
});
|
|
7904
|
-
const
|
|
8682
|
+
const shown3 = relative7(process.cwd(), pulled.dir) || ".";
|
|
7905
8683
|
ctx.log.success(
|
|
7906
|
-
`Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${
|
|
8684
|
+
`Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${shown3} ${chalk13.dim(`(${pulled.fileCount} files, ${formatBytes(pulled.totalBytes)}, version ${pulled.versionId})`)}`
|
|
7907
8685
|
);
|
|
7908
8686
|
ctx.out.data(
|
|
7909
8687
|
pulled,
|
|
7910
|
-
() =>
|
|
7911
|
-
`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.`
|
|
7912
8690
|
)
|
|
7913
8691
|
);
|
|
7914
8692
|
}
|
|
7915
8693
|
|
|
7916
8694
|
// src/commands/setup.ts
|
|
7917
8695
|
import { intro, outro, spinner } from "@clack/prompts";
|
|
7918
|
-
import
|
|
8696
|
+
import chalk14 from "chalk";
|
|
7919
8697
|
var SCOPES = ["user", "project", "local"];
|
|
7920
8698
|
async function runSetup(baseCtx, opts, deps = {}) {
|
|
7921
8699
|
const scope = opts.scope ?? "user";
|
|
@@ -7928,14 +8706,14 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
7928
8706
|
...baseCtx,
|
|
7929
8707
|
log: createLogger({
|
|
7930
8708
|
debug: baseCtx.config.debug,
|
|
7931
|
-
write: (line) => process.stderr.write(`${
|
|
8709
|
+
write: (line) => process.stderr.write(`${chalk14.gray("\u2502")} ${line}
|
|
7932
8710
|
`)
|
|
7933
8711
|
})
|
|
7934
8712
|
} : baseCtx;
|
|
7935
8713
|
if (fancy) {
|
|
7936
8714
|
process.stderr.write(`${logo()}
|
|
7937
8715
|
`);
|
|
7938
|
-
intro(
|
|
8716
|
+
intro(chalk14.bold(" fling setup "), { output: process.stderr });
|
|
7939
8717
|
}
|
|
7940
8718
|
const login = await stepLogin(ctx, opts.url);
|
|
7941
8719
|
const results = {
|
|
@@ -7949,7 +8727,7 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
7949
8727
|
const failed = Object.values(results).includes("failed");
|
|
7950
8728
|
if (fancy)
|
|
7951
8729
|
outro(
|
|
7952
|
-
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.`,
|
|
7953
8731
|
{ output: process.stderr }
|
|
7954
8732
|
);
|
|
7955
8733
|
ctx.out.data(results, () => "");
|
|
@@ -7972,7 +8750,7 @@ async function signIn(ctx, serverUrl) {
|
|
|
7972
8750
|
try {
|
|
7973
8751
|
const me = await whoami(requireClient(ctx));
|
|
7974
8752
|
if (ctx.config.remembered) await ctx.store.setCurrent(serverUrl);
|
|
7975
|
-
ctx.log.success(`Signed in as ${
|
|
8753
|
+
ctx.log.success(`Signed in as ${chalk14.bold(me.name)} ${chalk14.dim(`<${me.email}>`)}`);
|
|
7976
8754
|
if (ctx.config.remembered)
|
|
7977
8755
|
await fetchInstance(serverUrl, ctx.fetch).then((instance) => ctx.store.putServer(serverUrl, { instance }, { makeCurrent: false })).catch(() => {
|
|
7978
8756
|
});
|
|
@@ -7996,7 +8774,7 @@ async function signIn(ctx, serverUrl) {
|
|
|
7996
8774
|
async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
7997
8775
|
const status = await claudeCodeStatus(run);
|
|
7998
8776
|
if (status.claude === "missing") {
|
|
7999
|
-
ctx.log.info(`Claude Code not found ${
|
|
8777
|
+
ctx.log.info(`Claude Code not found ${chalk14.dim("(no `claude` on your PATH) \u2014 skipped")}`);
|
|
8000
8778
|
ctx.log.hint(
|
|
8001
8779
|
`Once it is installed, run \`${ctx.binName} setup\` again, or in Claude Code: /plugin marketplace add ${MARKETPLACE_SOURCE} then /plugin install ${PLUGIN_ID}`
|
|
8002
8780
|
);
|
|
@@ -8004,7 +8782,7 @@ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
|
8004
8782
|
}
|
|
8005
8783
|
if ("installed" in status && status.installed) return refreshPlugin(ctx, run, status, fancy);
|
|
8006
8784
|
if (ask && !await ctx.confirm("Install the Fling plugin (skill + MCP server) into Claude Code?")) {
|
|
8007
|
-
ctx.log.info(
|
|
8785
|
+
ctx.log.info(chalk14.dim("Claude Code plugin: skipped"));
|
|
8008
8786
|
return "skipped";
|
|
8009
8787
|
}
|
|
8010
8788
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
@@ -8017,7 +8795,7 @@ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
|
8017
8795
|
else ctx.log.error(message);
|
|
8018
8796
|
return "failed";
|
|
8019
8797
|
}
|
|
8020
|
-
const done = `Claude Code plugin ${PLUGIN_NAME} installed ${
|
|
8798
|
+
const done = `Claude Code plugin ${PLUGIN_NAME} installed ${chalk14.dim(`(${scope} scope)`)}`;
|
|
8021
8799
|
if (s) s.stop(done);
|
|
8022
8800
|
else ctx.log.success(done);
|
|
8023
8801
|
ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick it up.");
|
|
@@ -8030,14 +8808,14 @@ async function refreshPlugin(ctx, run, before, fancy) {
|
|
|
8030
8808
|
await updatePlugin(run, before.scope);
|
|
8031
8809
|
} catch (error) {
|
|
8032
8810
|
const message = `${error instanceof Error ? error.message : String(error)} \u2014 the installed plugin${before.version ? ` (${before.version})` : ""} is unchanged`;
|
|
8033
|
-
if (s) s.stop(
|
|
8811
|
+
if (s) s.stop(chalk14.yellow(message));
|
|
8034
8812
|
else ctx.log.warn(message);
|
|
8035
8813
|
return "already";
|
|
8036
8814
|
}
|
|
8037
8815
|
const after = await claudeCodeStatus(run).catch(() => null);
|
|
8038
8816
|
const version = after && "installed" in after && after.installed ? after.version : void 0;
|
|
8039
8817
|
const unchanged = before.version !== void 0 && version === before.version;
|
|
8040
|
-
const done = unchanged ? `Claude Code plugin ${PLUGIN_NAME} is up to date ${
|
|
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}` : ""}`;
|
|
8041
8819
|
if (s) s.stop(done);
|
|
8042
8820
|
else ctx.log.success(done);
|
|
8043
8821
|
if (unchanged) return "already";
|
|
@@ -8048,7 +8826,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
8048
8826
|
const app = await (deps.findClaudeDesktop ?? findClaudeDesktop)();
|
|
8049
8827
|
if (!app) return "missing";
|
|
8050
8828
|
if (ask && !await ctx.confirm("Add the Fling extension to Claude Desktop too?")) {
|
|
8051
|
-
ctx.log.info(
|
|
8829
|
+
ctx.log.info(chalk14.dim("Claude Desktop extension: skipped"));
|
|
8052
8830
|
return "skipped";
|
|
8053
8831
|
}
|
|
8054
8832
|
const s = fancy ? spinner({ output: process.stderr }) : null;
|
|
@@ -8082,7 +8860,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
|
|
|
8082
8860
|
}
|
|
8083
8861
|
|
|
8084
8862
|
// src/commands/sites.ts
|
|
8085
|
-
import
|
|
8863
|
+
import chalk15 from "chalk";
|
|
8086
8864
|
async function runList(ctx, opts) {
|
|
8087
8865
|
const sites = await listSites(requireClient(ctx), opts);
|
|
8088
8866
|
ctx.out.data(
|
|
@@ -8118,12 +8896,12 @@ async function runListShared(ctx, opts) {
|
|
|
8118
8896
|
);
|
|
8119
8897
|
}
|
|
8120
8898
|
var accessLabel = (site) => ACCESS_LABELS[siteAccessLevel(site)];
|
|
8121
|
-
var row = (label, value) => `${
|
|
8899
|
+
var row = (label, value) => `${chalk15.bold(`${label}:`.padEnd(14))}${value}`;
|
|
8122
8900
|
function describeSite(site) {
|
|
8123
8901
|
return [
|
|
8124
8902
|
row("Name", site.name),
|
|
8125
8903
|
row("URL", site.url),
|
|
8126
|
-
row("Access", accessLabel(site) + (site.archived ?
|
|
8904
|
+
row("Access", accessLabel(site) + (site.archived ? chalk15.yellow(" (archived)") : "")),
|
|
8127
8905
|
row("SPA", site.spa ? "yes" : "no"),
|
|
8128
8906
|
row("Share link", site.shareUrl ?? "-"),
|
|
8129
8907
|
row("Version", site.currentVersionId ?? "-"),
|
|
@@ -8152,7 +8930,7 @@ function filesRow(site) {
|
|
|
8152
8930
|
var membersOf = (ctx) => ctx.config.instance ? privateAudience(ctx.config.instance) : "anyone signed in to this Fling";
|
|
8153
8931
|
async function runInfo(ctx, name) {
|
|
8154
8932
|
const site = await getSite(requireClient(ctx), name);
|
|
8155
|
-
const access3 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${
|
|
8933
|
+
const access3 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${chalk15.bold("Who:".padEnd(14))}${describeAudience(site, membersOf(ctx))}`] : [];
|
|
8156
8934
|
ctx.out.data(
|
|
8157
8935
|
site,
|
|
8158
8936
|
() => [
|
|
@@ -8160,13 +8938,14 @@ async function runInfo(ctx, name) {
|
|
|
8160
8938
|
...access3,
|
|
8161
8939
|
...rolesRows(site),
|
|
8162
8940
|
"",
|
|
8163
|
-
|
|
8941
|
+
chalk15.dim(
|
|
8164
8942
|
`${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
|
|
8165
8943
|
)
|
|
8166
8944
|
].join("\n")
|
|
8167
8945
|
);
|
|
8168
8946
|
}
|
|
8169
|
-
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));
|
|
8170
8949
|
const site = await getSite(requireClient(ctx), name);
|
|
8171
8950
|
ctx.out.data(
|
|
8172
8951
|
{ name: site.name, currentVersionId: site.currentVersionId, versions: site.versions },
|
|
@@ -8181,14 +8960,36 @@ async function runVersions(ctx, name) {
|
|
|
8181
8960
|
])
|
|
8182
8961
|
);
|
|
8183
8962
|
}
|
|
8184
|
-
async function
|
|
8185
|
-
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);
|
|
8186
8985
|
if (!result.files.some((f) => f.path === "index.html")) {
|
|
8187
|
-
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
|
+
);
|
|
8188
8989
|
}
|
|
8189
8990
|
ctx.out.data(result, () => {
|
|
8190
8991
|
const n = result.files.length;
|
|
8191
|
-
const header =
|
|
8992
|
+
const header = chalk15.dim(
|
|
8192
8993
|
`${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
|
|
8193
8994
|
);
|
|
8194
8995
|
const table = renderTable(result.files, [
|
|
@@ -8200,7 +9001,16 @@ async function runFiles(ctx, name, versionId) {
|
|
|
8200
9001
|
${table}`;
|
|
8201
9002
|
});
|
|
8202
9003
|
}
|
|
8203
|
-
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
|
+
}
|
|
8204
9014
|
const site = await rollback(requireClient(ctx), name, versionId);
|
|
8205
9015
|
ctx.log.success(`${name} now serves version ${site.currentVersionId ?? versionId}`);
|
|
8206
9016
|
ctx.out.data(site, () => site.url);
|
|
@@ -8224,7 +9034,7 @@ async function runShare(ctx, name, opts) {
|
|
|
8224
9034
|
ctx.log.success("Share link created \u2014 anyone with it can view the site");
|
|
8225
9035
|
ctx.out.data(
|
|
8226
9036
|
ready === void 0 ? result : { ...result, ready },
|
|
8227
|
-
() => result.shareUrl ? result.shareUrl :
|
|
9037
|
+
() => result.shareUrl ? result.shareUrl : chalk15.dim("(no share link)")
|
|
8228
9038
|
);
|
|
8229
9039
|
}
|
|
8230
9040
|
async function runVisibility(ctx, name, visibility) {
|
|
@@ -8287,6 +9097,7 @@ async function runSetLevel(ctx, name, level, opts) {
|
|
|
8287
9097
|
ctx.out.data({ ...site, warnings }, () => describeAudience(site, membersOf(ctx)));
|
|
8288
9098
|
}
|
|
8289
9099
|
async function runRoles(ctx, which, name, opts) {
|
|
9100
|
+
if (opts.path !== void 0) return runPathRoles(ctx, which, name, pathFolder2(opts.path), opts);
|
|
8290
9101
|
const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "siteRoles");
|
|
8291
9102
|
if (instance && !siteRolesEnabled(instance)) {
|
|
8292
9103
|
throw new CliError(`${instance.name} has no site owners and publishers yet`, {
|
|
@@ -8312,6 +9123,23 @@ async function runRoles(ctx, which, name, opts) {
|
|
|
8312
9123
|
() => describeRoles(site)
|
|
8313
9124
|
);
|
|
8314
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
|
+
}
|
|
8315
9143
|
async function runArchive(ctx, name, unarchive = false) {
|
|
8316
9144
|
const site = await archiveSite(requireClient(ctx), name, { unarchive });
|
|
8317
9145
|
ctx.log.success(
|
|
@@ -8336,15 +9164,15 @@ async function runRemove(ctx, name, opts) {
|
|
|
8336
9164
|
}
|
|
8337
9165
|
|
|
8338
9166
|
// src/mcp.ts
|
|
8339
|
-
import { mkdir as mkdir5, mkdtemp as mkdtemp3, rm as rm3, writeFile as
|
|
9167
|
+
import { mkdir as mkdir5, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
|
|
8340
9168
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
8341
|
-
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";
|
|
8342
9170
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8343
9171
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8344
9172
|
import { z as z20 } from "zod";
|
|
8345
9173
|
|
|
8346
9174
|
// src/locate.ts
|
|
8347
|
-
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";
|
|
8348
9176
|
import { posix, win32 } from "node:path";
|
|
8349
9177
|
import { fileURLToPath } from "node:url";
|
|
8350
9178
|
var DOWNLOADS_DIR_ENV = "FLING_DOWNLOADS_DIR";
|
|
@@ -8357,7 +9185,7 @@ var SANDBOX_PATH_RE = /^\/(mnt\/user-data|mnt\/data|sessions|home\/claude|tmp\/o
|
|
|
8357
9185
|
var nodeFs = {
|
|
8358
9186
|
async stat(path3) {
|
|
8359
9187
|
try {
|
|
8360
|
-
const s = await
|
|
9188
|
+
const s = await stat9(path3);
|
|
8361
9189
|
return { isFile: s.isFile(), isDirectory: s.isDirectory(), mtimeMs: s.mtimeMs, size: s.size };
|
|
8362
9190
|
} catch {
|
|
8363
9191
|
return null;
|
|
@@ -8369,7 +9197,7 @@ var nodeFs = {
|
|
|
8369
9197
|
const out = [];
|
|
8370
9198
|
for (const e of entries) {
|
|
8371
9199
|
if (e.isSymbolicLink()) {
|
|
8372
|
-
const s = await
|
|
9200
|
+
const s = await stat9(`${path3}/${e.name}`).catch(() => null);
|
|
8373
9201
|
out.push({ name: e.name, isFile: Boolean(s?.isFile()), isDirectory: false });
|
|
8374
9202
|
} else out.push({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() });
|
|
8375
9203
|
}
|
|
@@ -8384,7 +9212,7 @@ var nodeFs = {
|
|
|
8384
9212
|
async readText(path3) {
|
|
8385
9213
|
try {
|
|
8386
9214
|
if (!(await lstat(path3)).isFile()) return null;
|
|
8387
|
-
return await
|
|
9215
|
+
return await readFile12(path3, "utf8");
|
|
8388
9216
|
} catch {
|
|
8389
9217
|
return null;
|
|
8390
9218
|
}
|
|
@@ -8800,8 +9628,8 @@ function createMcpServer(deps = {}) {
|
|
|
8800
9628
|
inputSchema: { ...def.inputSchema, server: serverArg },
|
|
8801
9629
|
annotations: def.annotations
|
|
8802
9630
|
});
|
|
8803
|
-
async function edgeReady(url,
|
|
8804
|
-
const r = await waitReady(url,
|
|
9631
|
+
async function edgeReady(url, paths2) {
|
|
9632
|
+
const r = await waitReady(url, paths2).catch(() => ({ ready: false, waitedMs: 0 }));
|
|
8805
9633
|
return {
|
|
8806
9634
|
ready: r.ready,
|
|
8807
9635
|
note: r.ready ? "" : `
|
|
@@ -8862,10 +9690,10 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
8862
9690
|
return instance;
|
|
8863
9691
|
}
|
|
8864
9692
|
async function publishFolder2(target2, raw) {
|
|
8865
|
-
const folder =
|
|
9693
|
+
const folder = normalizePathFolder(raw);
|
|
8866
9694
|
if (folder === null) {
|
|
8867
9695
|
throw new CliError(
|
|
8868
|
-
`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)`
|
|
8869
9697
|
);
|
|
8870
9698
|
}
|
|
8871
9699
|
const instance = await loadInstanceWith(store, target2.config, fetchImpl, "pathPublish");
|
|
@@ -8884,6 +9712,23 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
|
8884
9712
|
);
|
|
8885
9713
|
}
|
|
8886
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
|
+
}
|
|
8887
9732
|
async function linkedServer(path3) {
|
|
8888
9733
|
try {
|
|
8889
9734
|
const located = await resolveInput(path3, locate);
|
|
@@ -8920,9 +9765,6 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
8920
9765
|
if (opts.access) await requireAccessLevels(t2);
|
|
8921
9766
|
const link = opts.link ?? null;
|
|
8922
9767
|
if (link && !opts.force || opts.base !== void 0) await requirePull(t2);
|
|
8923
|
-
if (opts.base !== void 0 && opts.folder !== void 0) {
|
|
8924
|
-
throw new CliError("Pass `base` or `path`, not both");
|
|
8925
|
-
}
|
|
8926
9768
|
const folder = opts.folder === void 0 ? void 0 : await publishFolder2(t2, opts.folder);
|
|
8927
9769
|
const shownFolder = folder ?? link?.path;
|
|
8928
9770
|
const inFolder = (file) => shownFolder === void 0 ? file : `${shownFolder}/${file}`;
|
|
@@ -8933,17 +9775,17 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
8933
9775
|
${l}.`).join("");
|
|
8934
9776
|
let generatedIndex = false;
|
|
8935
9777
|
const asItIs = link !== null || opts.base !== void 0;
|
|
8936
|
-
if (!asItIs && !hasRootIndex(local)) {
|
|
9778
|
+
if (!asItIs && !hasRootIndex(local, (await t2.instance())?.capabilities.markdown === true)) {
|
|
8937
9779
|
const pages = htmlPages(local).map((f) => f.path);
|
|
8938
9780
|
if (pages.length > 0 && opts.listHtml === void 0) {
|
|
8939
|
-
const
|
|
9781
|
+
const shown3 = pages.slice(0, 20);
|
|
8940
9782
|
return {
|
|
8941
9783
|
kind: "needs_decision",
|
|
8942
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\`.`,
|
|
8943
9785
|
data: {
|
|
8944
9786
|
status: "needs_decision",
|
|
8945
|
-
htmlPages:
|
|
8946
|
-
...pages.length >
|
|
9787
|
+
htmlPages: shown3,
|
|
9788
|
+
...pages.length > shown3.length ? { moreHtmlPages: pages.length - shown3.length } : {},
|
|
8947
9789
|
fileCount: local.files.length
|
|
8948
9790
|
}
|
|
8949
9791
|
};
|
|
@@ -8955,7 +9797,7 @@ ${l}.`).join("");
|
|
|
8955
9797
|
}
|
|
8956
9798
|
const onBase = link && !opts.force ? onLink(link, local) : null;
|
|
8957
9799
|
const changes = link ? onLink(link, local).changes : null;
|
|
8958
|
-
const layered =
|
|
9800
|
+
const layered = onBase !== null || opts.base !== void 0;
|
|
8959
9801
|
let instance = await t2.instance();
|
|
8960
9802
|
const forms2 = await planForms({
|
|
8961
9803
|
dir,
|
|
@@ -8965,7 +9807,8 @@ ${l}.`).join("");
|
|
|
8965
9807
|
// A Fling whose stored metadata predates forms is asked again before a forms file is refused.
|
|
8966
9808
|
instance: formsEnabled(instance) ? instance : await loadInstanceWith(store, t2.config, fetchImpl, "forms"),
|
|
8967
9809
|
bin: "fling",
|
|
8968
|
-
...opts.forms === void 0 ? {} : { inline: opts.forms }
|
|
9810
|
+
...opts.forms === void 0 ? {} : { inline: opts.forms },
|
|
9811
|
+
...shownFolder === void 0 ? {} : { path: shownFolder }
|
|
8969
9812
|
});
|
|
8970
9813
|
const result = await deploy(
|
|
8971
9814
|
{ client: t2.client, fetch: fetchImpl },
|
|
@@ -8973,10 +9816,9 @@ ${l}.`).join("");
|
|
|
8973
9816
|
dir,
|
|
8974
9817
|
name: opts.name,
|
|
8975
9818
|
manifest: local,
|
|
8976
|
-
path: folder,
|
|
9819
|
+
path: folder ?? onBase?.path,
|
|
8977
9820
|
...onBase ? {
|
|
8978
|
-
base: onBase.base
|
|
8979
|
-
...onBase.partial ? { partial: true, remove: onBase.remove, within: onBase.within } : {}
|
|
9821
|
+
base: onBase.base
|
|
8980
9822
|
} : opts.base !== void 0 ? { base: opts.base, partial: true, remove: opts.remove ?? [] } : {},
|
|
8981
9823
|
spa: opts.spa,
|
|
8982
9824
|
...opts.access ? {
|
|
@@ -8999,7 +9841,7 @@ ${l}.`).join("");
|
|
|
8999
9841
|
);
|
|
9000
9842
|
});
|
|
9001
9843
|
if (link) await writeSiteLink(dir, linkAfter(link, result.versionId, local, forms2.sha256));
|
|
9002
|
-
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})`;
|
|
9003
9845
|
const warnings = result.warnings.length ? `
|
|
9004
9846
|
Warnings:
|
|
9005
9847
|
- ${result.warnings.join("\n- ")}` : "";
|
|
@@ -9010,13 +9852,14 @@ Warnings:
|
|
|
9010
9852
|
const url = shownFolder === void 0 ? result.url : new URL(`${shownFolder}/`, result.url).href;
|
|
9011
9853
|
instance = await t2.instance();
|
|
9012
9854
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
9855
|
+
const onPath2 = shownFolder !== void 0;
|
|
9013
9856
|
let formsSummary = forms2.check?.summary ?? null;
|
|
9014
|
-
if (!formsSummary && forms2.send === void 0 && formsEnabled(instance)) {
|
|
9857
|
+
if (!formsSummary && !onPath2 && forms2.send === void 0 && formsEnabled(instance)) {
|
|
9015
9858
|
const live = await getForms(t2.client, result.name).catch(() => null);
|
|
9016
9859
|
if (live?.config) formsSummary = checkFormsConfig(live.config).summary;
|
|
9017
9860
|
}
|
|
9018
9861
|
const responsesPage = formsSummary && instance ? responsesPageUrl(instance.appOrigin, result.name) : void 0;
|
|
9019
|
-
const formsHint2 = formsEnabled(instance) && !formsSummary ? FORMS_HINT : void 0;
|
|
9862
|
+
const formsHint2 = formsEnabled(instance) && !formsSummary && !onPath2 ? FORMS_HINT : void 0;
|
|
9020
9863
|
const formsText = formsSummary && responsesPage ? `
|
|
9021
9864
|
${describeFormsSummary(formsSummary, responsesPage)}` : "";
|
|
9022
9865
|
const listing2 = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
|
|
@@ -9030,7 +9873,7 @@ Who can open it: ${who} ${accessChangeHint("mcp", result.name, { invites: instan
|
|
|
9030
9873
|
Its access is unchanged (deploying never changes it for an existing site): ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : "";
|
|
9031
9874
|
return {
|
|
9032
9875
|
kind: "deployed",
|
|
9033
|
-
summary: `${head}${access4}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
9876
|
+
summary: `${head}${access4}${warnings}${describePublishHints(result.hints)}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
9034
9877
|
To check exactly which files the site has, call fling_files with name "${result.name}".` + (formsHint2 ? `
|
|
9035
9878
|
${formsHint2}` : ""),
|
|
9036
9879
|
data: {
|
|
@@ -9094,7 +9937,7 @@ ${formsHint2}` : ""),
|
|
|
9094
9937
|
"Site name (lowercase letters, digits, single hyphens; max 50). Defaults to a name generated from the file or folder"
|
|
9095
9938
|
),
|
|
9096
9939
|
sitePath: z20.string().min(1).max(1024).optional().describe(
|
|
9097
|
-
'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'
|
|
9098
9941
|
),
|
|
9099
9942
|
spa: spa2,
|
|
9100
9943
|
access: access3,
|
|
@@ -9119,7 +9962,9 @@ ${formsHint2}` : ""),
|
|
|
9119
9962
|
"`sitePath` needs `name`: the site to publish into. Ask the user which site, then call fling_deploy again"
|
|
9120
9963
|
);
|
|
9121
9964
|
}
|
|
9122
|
-
const prepared = await prepareInput(located.path
|
|
9965
|
+
const prepared = await prepareInput(located.path, {
|
|
9966
|
+
markdown: (await t2.instance())?.capabilities.markdown === true
|
|
9967
|
+
});
|
|
9123
9968
|
try {
|
|
9124
9969
|
const found = prepared.kind === "folder" ? await readSiteLink(prepared.dir) : null;
|
|
9125
9970
|
const link = found && (args.name === void 0 || args.name === found.name) && args.sitePath === void 0 && found.server === t2.url ? found : null;
|
|
@@ -9173,7 +10018,7 @@ ${formsHint2}` : ""),
|
|
|
9173
10018
|
versionId: z20.string().min(1).max(100).optional().describe(
|
|
9174
10019
|
"Another version than the live one (owners and publishers; ids from fling_info)"
|
|
9175
10020
|
),
|
|
9176
|
-
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"'),
|
|
9177
10022
|
force: z20.boolean().optional().describe("Write into the folder even if it is not empty"),
|
|
9178
10023
|
server: serverArg
|
|
9179
10024
|
},
|
|
@@ -9181,13 +10026,13 @@ ${formsHint2}` : ""),
|
|
|
9181
10026
|
},
|
|
9182
10027
|
(args) => run("fling_pull", args.server, async (t2) => {
|
|
9183
10028
|
await requirePull(t2);
|
|
9184
|
-
const path3 = args.path === void 0 ? void 0 :
|
|
10029
|
+
const path3 = args.path === void 0 ? void 0 : normalizePathFolder(args.path);
|
|
9185
10030
|
if (path3 === null) {
|
|
9186
10031
|
throw new CliError(
|
|
9187
|
-
`Invalid path "${args.path}": use
|
|
10032
|
+
`Invalid path "${args.path}": use one of the site's Paths, like "PAY-412"`
|
|
9188
10033
|
);
|
|
9189
10034
|
}
|
|
9190
|
-
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);
|
|
9191
10036
|
const pulled = await pullSite(
|
|
9192
10037
|
{ client: t2.client, fetch: fetchImpl },
|
|
9193
10038
|
{
|
|
@@ -9209,20 +10054,23 @@ ${formsHint2}` : ""),
|
|
|
9209
10054
|
serverTool(mcpTools2.readFiles),
|
|
9210
10055
|
(args) => run(mcpTools2.readFiles.name, args.server, async (t2) => {
|
|
9211
10056
|
await requirePull(t2);
|
|
9212
|
-
const path3 = args.path === void 0 ? void 0 :
|
|
9213
|
-
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`);
|
|
9214
10060
|
const pulled = await t2.client.request(
|
|
9215
10061
|
"GET",
|
|
9216
10062
|
`/api/sites/${encodeURIComponent(args.name)}/pull`,
|
|
9217
10063
|
{ schema: PullResponseSchema, query: { version: args.versionId, path: path3 } }
|
|
9218
10064
|
);
|
|
9219
|
-
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, {
|
|
9220
10068
|
paths: args.paths,
|
|
9221
10069
|
cursor: args.cursor,
|
|
9222
10070
|
budget: MCP_READ_MAX_BYTES
|
|
9223
10071
|
});
|
|
9224
10072
|
const read = await Promise.all(
|
|
9225
|
-
|
|
10073
|
+
page2.map(async ({ url, ...f }) => {
|
|
9226
10074
|
const res = await fetchImpl(url);
|
|
9227
10075
|
if (!res.ok) throw new CliError(`Could not read ${f.path}: HTTP ${res.status}`);
|
|
9228
10076
|
return { ...f, ...encodeForAgent(f.path, new Uint8Array(await res.arrayBuffer())) };
|
|
@@ -9235,6 +10083,7 @@ ${formsHint2}` : ""),
|
|
|
9235
10083
|
const result = {
|
|
9236
10084
|
name: args.name,
|
|
9237
10085
|
versionId: pulled.versionId,
|
|
10086
|
+
...path3 === void 0 ? {} : { path: path3 },
|
|
9238
10087
|
files,
|
|
9239
10088
|
missing,
|
|
9240
10089
|
...nextCursor ? { nextCursor } : {}
|
|
@@ -9293,7 +10142,7 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
9293
10142
|
}
|
|
9294
10143
|
if (args.files.length === 0 && !args.remove?.length)
|
|
9295
10144
|
throw new CliError("No files to publish");
|
|
9296
|
-
const tmp = await mkdtemp3(
|
|
10145
|
+
const tmp = await mkdtemp3(join13(tmpdir3(), "fling-publish-"));
|
|
9297
10146
|
try {
|
|
9298
10147
|
const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
|
|
9299
10148
|
return await deployFolder(t2, tmp, {
|
|
@@ -9540,6 +10389,10 @@ ${lines}` : ""}`,
|
|
|
9540
10389
|
t.info.name,
|
|
9541
10390
|
serverTool(t.info),
|
|
9542
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
|
+
}
|
|
9543
10396
|
const site = await getSite(target2.client, args.name);
|
|
9544
10397
|
const access4 = site.audience ? `
|
|
9545
10398
|
${describeAudience(site, await membersOn(target2))}` : "";
|
|
@@ -9556,8 +10409,19 @@ ${describeRoles(site)}` : "";
|
|
|
9556
10409
|
server.registerTool(
|
|
9557
10410
|
t.files.name,
|
|
9558
10411
|
serverTool(t.files),
|
|
9559
|
-
(args) => run(t.files.name, args.server, async (
|
|
9560
|
-
|
|
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);
|
|
9561
10425
|
return {
|
|
9562
10426
|
summary: describeVersionFiles(result.name, result, {
|
|
9563
10427
|
current: result.current,
|
|
@@ -9781,14 +10645,76 @@ ${describeGroups(groups3, me.email)}`,
|
|
|
9781
10645
|
server.registerTool(
|
|
9782
10646
|
t.rollback.name,
|
|
9783
10647
|
serverTool(t.rollback),
|
|
9784
|
-
(args) => run(t.rollback.name, args.server, async (
|
|
9785
|
-
|
|
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);
|
|
9786
10658
|
return {
|
|
9787
10659
|
summary: `${site.name} now serves version ${site.currentVersionId}: ${site.url}`,
|
|
9788
10660
|
data: site
|
|
9789
10661
|
};
|
|
9790
10662
|
})
|
|
9791
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
|
+
);
|
|
9792
10718
|
if (servers.some((s) => commentsEnabled(s.instance))) {
|
|
9793
10719
|
async function reviewOn(target2) {
|
|
9794
10720
|
const instance = await target2.instance();
|
|
@@ -10048,9 +10974,9 @@ async function writeInlineFiles(dir, files) {
|
|
|
10048
10974
|
total += bytes.length;
|
|
10049
10975
|
if (total > MAX_DEPLOY_BYTES)
|
|
10050
10976
|
throw new CliError(`The site is larger than ${formatBytes(MAX_DEPLOY_BYTES)}`);
|
|
10051
|
-
const target =
|
|
10977
|
+
const target = join13(dir, ...path3.split("/"));
|
|
10052
10978
|
await mkdir5(dirname4(target), { recursive: true });
|
|
10053
|
-
await
|
|
10979
|
+
await writeFile8(target, bytes);
|
|
10054
10980
|
}
|
|
10055
10981
|
if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
|
|
10056
10982
|
return { skippedDotfiles };
|
|
@@ -10094,10 +11020,10 @@ async function runMcpServer(deps = {}) {
|
|
|
10094
11020
|
}
|
|
10095
11021
|
|
|
10096
11022
|
// src/route.ts
|
|
10097
|
-
import { stat as
|
|
10098
|
-
import { resolve as
|
|
11023
|
+
import { stat as stat10 } from "node:fs/promises";
|
|
11024
|
+
import { resolve as resolve7 } from "node:path";
|
|
10099
11025
|
var PASS_THROUGH = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);
|
|
10100
|
-
var pathExists = (path3) =>
|
|
11026
|
+
var pathExists = (path3) => stat10(resolve7(path3)).then(
|
|
10101
11027
|
() => true,
|
|
10102
11028
|
() => false
|
|
10103
11029
|
);
|
|
@@ -10148,18 +11074,18 @@ program.name(BIN_NAME).description("Fling a folder of static files at Fling and
|
|
|
10148
11074
|
`server for this command: a URL, or the host of one you signed in to (default: $${ENV.url}, else the current one)`
|
|
10149
11075
|
).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
|
|
10150
11076
|
sortSubcommands: false,
|
|
10151
|
-
styleTitle: (title) =>
|
|
10152
|
-
styleCommandText: (text2) =>
|
|
11077
|
+
styleTitle: (title) => chalk16.bold(brand.primary(title)),
|
|
11078
|
+
styleCommandText: (text2) => chalk16.bold(text2),
|
|
10153
11079
|
styleSubcommandText: (text2) => brand.accent(text2),
|
|
10154
11080
|
styleOptionText: (text2) => brand.accent(text2),
|
|
10155
|
-
styleArgumentText: (text2) =>
|
|
11081
|
+
styleArgumentText: (text2) => chalk16.dim(text2)
|
|
10156
11082
|
}).addHelpText(
|
|
10157
11083
|
"before",
|
|
10158
11084
|
() => process.stdout.isTTY ? logo({ columns: process.stdout.columns }) : ""
|
|
10159
11085
|
).addHelpText(
|
|
10160
11086
|
"after",
|
|
10161
11087
|
() => `
|
|
10162
|
-
${
|
|
11088
|
+
${chalk16.bold(brand.primary("Examples:"))}
|
|
10163
11089
|
$ ${BIN_NAME} login https://fling.example.com sign in to your Fling (once; there is no default)
|
|
10164
11090
|
$ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
|
|
10165
11091
|
$ ${BIN_NAME} use list the Flings you signed in to; \`use <url>\` switches
|
|
@@ -10170,7 +11096,7 @@ ${chalk15.bold(brand.primary("Examples:"))}
|
|
|
10170
11096
|
$ ${BIN_NAME} site.zip a zip of a site
|
|
10171
11097
|
$ ${BIN_NAME} share my-app public link for people who can't sign in
|
|
10172
11098
|
|
|
10173
|
-
${
|
|
11099
|
+
${chalk16.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
|
|
10174
11100
|
Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`)}`
|
|
10175
11101
|
);
|
|
10176
11102
|
function action(handler) {
|
|
@@ -10195,7 +11121,7 @@ function report(error, ctx) {
|
|
|
10195
11121
|
if (error instanceof CliError && error.hint) log.hint(error.hint);
|
|
10196
11122
|
if (ctx?.config.debug && error instanceof Error && error.stack) log.hint(error.stack);
|
|
10197
11123
|
if (ctx?.json) {
|
|
10198
|
-
const { code, findings, paths } = error;
|
|
11124
|
+
const { code, findings, paths: paths2 } = error;
|
|
10199
11125
|
process.stdout.write(
|
|
10200
11126
|
`${formatJson({
|
|
10201
11127
|
error: {
|
|
@@ -10203,7 +11129,7 @@ function report(error, ctx) {
|
|
|
10203
11129
|
exitCode: exitCodeFor(error),
|
|
10204
11130
|
...typeof code === "string" ? { code } : {},
|
|
10205
11131
|
...Array.isArray(findings) ? { findings } : {},
|
|
10206
|
-
...Array.isArray(
|
|
11132
|
+
...Array.isArray(paths2) ? { paths: paths2 } : {}
|
|
10207
11133
|
}
|
|
10208
11134
|
})}
|
|
10209
11135
|
`
|
|
@@ -10217,7 +11143,7 @@ program.command("deploy [path]").helpGroup("Deploy:").description(
|
|
|
10217
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)"
|
|
10218
11144
|
).option(
|
|
10219
11145
|
"--path <folder>",
|
|
10220
|
-
"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`)"
|
|
10221
11147
|
).option("--spa", "single-page app: serve index.html for unknown paths").option(
|
|
10222
11148
|
"--access <level>",
|
|
10223
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`"
|
|
@@ -10240,7 +11166,7 @@ program.command("pull <name> [dir]").helpGroup("Deploy:").description(
|
|
|
10240
11166
|
).option(
|
|
10241
11167
|
"--version-id <id>",
|
|
10242
11168
|
"another version than the live one (owners and publishers; ids from `versions`)"
|
|
10243
|
-
).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(
|
|
10244
11170
|
action(
|
|
10245
11171
|
(ctx, name, dir, opts) => runPull(ctx, name, dir, opts)
|
|
10246
11172
|
)
|
|
@@ -10284,11 +11210,20 @@ program.command("ls").helpGroup("Sites:").alias("list").description("list your s
|
|
|
10284
11210
|
)
|
|
10285
11211
|
);
|
|
10286
11212
|
program.command("info <name>").helpGroup("Sites:").description("show a site").action(action((ctx, name) => runInfo(ctx, name)));
|
|
10287
|
-
program.command("versions <name>").helpGroup("Sites:").description("list a site's versions (* = live)").action(action((ctx, name) => runVersions(ctx, name)));
|
|
10288
|
-
program.command("files <name> [versionId]").helpGroup("Sites:").description("list the files in a site's live version (or in versionId)").action(
|
|
10289
|
-
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
|
+
)
|
|
10290
11226
|
);
|
|
10291
|
-
program.command("rollback <name> <versionId>").helpGroup("Sites:").description("serve an earlier version again").action(action((ctx, name, versionId) => runRollback(ctx, name, versionId)));
|
|
10292
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(
|
|
10293
11228
|
action(
|
|
10294
11229
|
(ctx, name, opts) => runShare(ctx, name, opts)
|
|
@@ -10328,7 +11263,10 @@ for (const [which, does] of [
|
|
|
10328
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(
|
|
10329
11264
|
"--group <slugs...>",
|
|
10330
11265
|
"add these groups (see `fling groups`); whoever manages a group decides who is in it"
|
|
10331
|
-
).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(
|
|
10332
11270
|
action(
|
|
10333
11271
|
(ctx, name, opts) => runRoles(ctx, which, name, opts)
|
|
10334
11272
|
)
|
|
@@ -10337,6 +11275,36 @@ for (const [which, does] of [
|
|
|
10337
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)));
|
|
10338
11276
|
program.command("unarchive <name>").helpGroup("Sites:").description("bring an archived site back").action(action((ctx, name) => runArchive(ctx, name, true)));
|
|
10339
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)));
|
|
10340
11308
|
var groups = program.command("groups").helpGroup("Groups:").description(
|
|
10341
11309
|
"named lists of people anyone can use in an audience; only the owner changes one (default: list)"
|
|
10342
11310
|
);
|