@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.
Files changed (3) hide show
  1. package/README.md +70 -20
  2. package/dist/cli.js +1164 -196
  3. 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
- path3 = new URL(path3).pathname;
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(paths) {
1761
+ function reviewPages(paths2) {
1733
1762
  const pages = /* @__PURE__ */ new Set();
1734
- for (const p of paths) {
1763
+ for (const p of paths2) {
1735
1764
  if (!/\.html?$/i.test(p) && !p.endsWith("/")) continue;
1736
- const page = normalizeCommentPath(p);
1737
- if (page !== null) pages.add(page);
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, paths) {
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 = paths ? reviewPages(paths) : null;
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((page) => (p.done[page]?.length ?? 0) > 0).length;
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
- var PublishPathSchema = z6.string().transform((raw, ctx) => {
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: "path must be a folder of the site, like 30-10-26 or reports/2026 (no dotfiles)"
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 path publish (`pathPublish` capability): the folder it replaced (`30-10-26`, shown as
2196
- * `/30-10-26/`), and the version it carried the rest of the site forward from (absent when the
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({ status: z9.literal("unchanged"), versionId: z9.string(), site: SiteSchema }),
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({ status: z9.literal("repointed"), versionId: z9.string(), site: SiteSchema }),
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 page of pages) {
2865
- const slash = page.path.lastIndexOf("/");
2866
- const folder = slash === -1 ? "" : page.path.slice(0, slash);
2867
- groups2.set(folder, [...groups2.get(folder) ?? [], page]);
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
+ "&": "&amp;",
3472
+ "<": "&lt;",
3473
+ ">": "&gt;",
3474
+ '"': "&quot;",
3475
+ "'": "&#39;"
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 page = [];
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
- page.push(file);
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 ${name}, version ${result.versionId}: ${read.map((f) => f.path).join(", ") || "(none)"}.`
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
- var PATH_PUBLISH_NOTE = 'With `path` (e.g. "30-10-26"), only that folder of the site is replaced: the files go to <site>/30-10-26/\u2026 (give their paths relative to the folder, its page being "index.html") and every other file of the site stays as it is. Use it to add one report, release note or week\'s page to a shared site without republishing the rest; rolling back removes it again.';
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 into this folder of the site only, e.g. "30-10-26" or "reports/2026" (served at <site>/30-10-26/), keeping every other file. File paths are relative to it. Omit to replace the whole site'
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: { name: siteName },
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('Only the files under this folder of the site, e.g. "30-10-26"'),
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 with \`path\` or \`base\` keeps the site's forms, any other publish has none`
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\` or \`base\`, plus the rest of the site). ${LIST_HTML} The upload stays open, so just call fling_upload_finish again with \`listHtml\`. The result includes \`warnings\`.`,
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((resolve7) => setTimeout(resolve7, ms));
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, paths) {
4365
+ function probeUrl(siteUrl2, paths2) {
3809
4366
  const base2 = siteUrl2.endsWith("/") ? siteUrl2 : `${siteUrl2}/`;
3810
- if (paths.length === 0 || paths.includes("index.html")) return base2;
3811
- const pick = paths.find((p) => /\.html?$/i.test(p)) ?? paths[0];
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 chalk15 from "chalk";
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.9.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: "^3.2.7"
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((resolve7, reject) => {
4549
- settle = { resolve: resolve7, reject };
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((resolve7, reject) => {
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
- resolve7(port);
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, paths, opts) => waitUntilReachable(probeUrl(url, paths), {
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, paths, io) {
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, paths, {
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((resolve7) => {
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)) resolve7(null);
5381
- else resolve7(stdout);
6004
+ if (error && !(args[0] === "check-ignore" && code === 1)) resolve8(null);
6005
+ else resolve8(stdout);
5382
6006
  }
5383
6007
  );
5384
- child.on("error", () => resolve7(null));
6008
+ child.on("error", () => resolve8(null));
5385
6009
  if (input !== void 0) child.stdin?.end(input);
5386
6010
  } catch {
5387
- resolve7(null);
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(paths) {
5479
- const shown2 = paths.slice(0, 20);
6102
+ constructor(paths2) {
6103
+ const shown3 = paths2.slice(0, 20);
5480
6104
  super(
5481
- `Not flung: ${paths.length === 1 ? "this is a secret file" : `these are ${paths.length} secret files`} (keys, certificates, credentials): ${shown2.join(", ")}${paths.length > shown2.length ? ` (+${paths.length - shown2.length} more)` : ""}. ${NEVER_PUBLISHED_NOTE}`,
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 ${paths.length === 1 ? "it" : "them"} out of the folder, or add ${paths.length === 1 ? "it" : "them"} to ${IGNORE_FILE} if the site does not need ${paths.length === 1 ? "it" : "them"}.`
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 = 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 ?? opts.within;
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, paths, where, bin) {
6000
- const check2 = checkFormsConfig(config, paths);
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
- await writeFile2(join5(tmp.dir, "index.html"), wrapperPage(name));
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(paths) {
6143
- let current = paths;
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 paths = /* @__PURE__ */ new Map();
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
- paths.set(n, unwrapped[i]);
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 = paths.get(name);
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 folder of the site: the folder is this folder */
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 ? {} : { partial: true, within: link.path, remove: changes.removed },
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 = normalizePublishPath(opts.path);
7174
+ const folder = normalizePathFolder(opts.path);
6539
7175
  if (folder === null) {
6540
7176
  throw new CliError(`Invalid --path "${opts.path}"`, {
6541
- hint: "Use a folder of the site, like 30-10-26 or reports/2026 (no dotfiles)."
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: opts.folder !== void 0 || onBase !== null,
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 : (await waitWithSpinner(
6635
- ctx.waitReady,
6636
- url,
6637
- local.files.map((f) => inFolder(f.path)),
6638
- { progress: ctx.progress, warn: (m) => ctx.log.warn(m) }
6639
- )).ready;
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.9.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((resolve7) => {
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
- resolve7({ code, stdout: String(stdout), stderr: String(stderr) });
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((resolve7) => {
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) => resolve7({ code: error ? 1 : 0, stdout: String(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 page = instance ? responsesPageUrl(instance.appOrigin, name) : void 0;
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, ...page ? { responsesUrl: page } : {} }, () => {
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
- ...page ? [chalk9.dim(`Progress and downloads: ${page}`)] : [],
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/pull.ts
7863
- import { relative as relative6, resolve as resolve5 } from "node:path";
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 = normalizePublishPath(opts.path);
8653
+ const folder = normalizePathFolder(opts.path);
7876
8654
  if (folder === null) {
7877
8655
  throw new CliError(`Invalid --path "${opts.path}"`, {
7878
- hint: "Use a folder of the site, like 30-10-26 or reports/2026 (no dotfiles)."
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 = resolve5(dir ?? name);
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 shown2 = relative6(process.cwd(), pulled.dir) || ".";
8682
+ const shown3 = relative7(process.cwd(), pulled.dir) || ".";
7905
8683
  ctx.log.success(
7906
- `Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${shown2} ${chalk12.dim(`(${pulled.fileCount} files, ${formatBytes(pulled.totalBytes)}, version ${pulled.versionId})`)}`
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
- () => chalk12.dim(
7911
- `Change what you need, then \`${ctx.binName} deploy ${shown2}\`: it publishes on version ${pulled.versionId}, and stops if someone publishes ${name} before you do.`
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 chalk13 from "chalk";
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(`${chalk13.gray("\u2502")} ${line}
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(chalk13.bold(" fling setup "), { output: process.stderr });
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 ? chalk13.yellow("Setup finished with problems \u2014 see above, then run `fling setup` again.") : `You're set. Try ${chalk13.bold(`${ctx.binName} deploy ./dist`)}, or ask Claude to \u201Cfling this\u201D.`,
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 ${chalk13.bold(me.name)} ${chalk13.dim(`<${me.email}>`)}`);
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 ${chalk13.dim("(no `claude` on your PATH) \u2014 skipped")}`);
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(chalk13.dim("Claude Code plugin: skipped"));
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 ${chalk13.dim(`(${scope} scope)`)}`;
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(chalk13.yellow(message));
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 ${chalk13.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}` : ""}`;
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(chalk13.dim("Claude Desktop extension: skipped"));
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 chalk14 from "chalk";
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) => `${chalk14.bold(`${label}:`.padEnd(14))}${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 ? chalk14.yellow(" (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) ? [`${chalk14.bold("Who:".padEnd(14))}${describeAudience(site, membersOf(ctx))}`] : [];
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
- chalk14.dim(
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 runFiles(ctx, name, versionId) {
8185
- const result = await versionFiles(requireClient(ctx), name, versionId);
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("No index.html at the root \u2014 the site root URL will 404");
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 = chalk14.dim(
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 : chalk14.dim("(no share link)")
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 writeFile7 } from "node:fs/promises";
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 join12, resolve as resolvePath } from "node:path";
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 readFile11, realpath as realpath4, stat as stat8 } from "node:fs/promises";
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 stat8(path3);
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 stat8(`${path3}/${e.name}`).catch(() => null);
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 readFile11(path3, "utf8");
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, paths) {
8804
- const r = await waitReady(url, paths).catch(() => ({ ready: false, waitedMs: 0 }));
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 = normalizePublishPath(raw);
9693
+ const folder = normalizePathFolder(raw);
8866
9694
  if (folder === null) {
8867
9695
  throw new CliError(
8868
- `Invalid path "${raw}": use a folder of the site, like "30-10-26" or "reports/2026" (no dotfiles)`
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 shown2 = pages.slice(0, 20);
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: shown2,
8946
- ...pages.length > shown2.length ? { moreHtmlPages: pages.length - shown2.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 = folder !== void 0 || onBase !== null || opts.base !== void 0;
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 of the site kept as it was)` : `Deployed ${result.fileCount} files${folder === void 0 ? "" : ` to /${folder}/ (the rest of the site kept as it was)`} (uploaded ${result.uploaded})`;
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 into this folder of the site only, e.g. "30-10-26" (served at <site>/30-10-26/), keeping every other file of the site. Needs `name`. Omit to replace the whole site'
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 folder of the site, e.g. "30-10-26"'),
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 : normalizePublishPath(args.path);
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 a folder of the site, like "30-10-26" (no dotfiles)`
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 ? join12(locate.home, "Documents", "Claude", "fling", args.name) : resolvePath(cwd, args.dir);
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 : normalizePublishPath(args.path);
9213
- if (path3 === null) throw new CliError(`Invalid path "${args.path}"`);
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 { page, tooLarge, missing, nextCursor } = pageOfFiles(pulled.files, {
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
- page.map(async ({ url, ...f }) => {
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(join12(tmpdir3(), "fling-publish-"));
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 ({ client }) => {
9560
- const result = await versionFiles(client, args.name, args.versionId);
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 ({ client }) => {
9785
- const site = await rollback(client, args.name, args.versionId);
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 = join12(dir, ...path3.split("/"));
10977
+ const target = join13(dir, ...path3.split("/"));
10052
10978
  await mkdir5(dirname4(target), { recursive: true });
10053
- await writeFile7(target, bytes);
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 stat9 } from "node:fs/promises";
10098
- import { resolve as resolve6 } from "node:path";
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) => stat9(resolve6(path3)).then(
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) => chalk15.bold(brand.primary(title)),
10152
- styleCommandText: (text2) => chalk15.bold(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) => chalk15.dim(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
- ${chalk15.bold(brand.primary("Examples:"))}
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
- ${chalk15.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
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(paths) ? { paths } : {}
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 into this folder of the site (e.g. 30-10-26, served at <site>/30-10-26/) and keep the rest of the site; needs --name"
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 folder of the site (e.g. 30-10-26), at the top of dir").option("--force", "write into dir even if it is not empty").action(
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((ctx, name, versionId) => runFiles(ctx, name, versionId))
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").action(
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
  );