@nearform/fling 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import chalk9 from "chalk";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/commands/auth.ts
7
- import chalk4 from "chalk";
8
+ import chalk5 from "chalk";
8
9
 
9
10
  // src/auth.ts
10
11
  import { randomBytes } from "node:crypto";
@@ -24,7 +25,7 @@ var MAX_FILES = 5e3;
24
25
  var MAX_PATH_LENGTH = 1024;
25
26
 
26
27
  // ../../packages/shared/src/secrets.ts
27
- var SECRET_SCAN_MAX_FILE_BYTES = 5 * 1024 * 1024;
28
+ var SECRET_SCAN_MAX_FILE_BYTES = MAX_FILE_BYTES;
28
29
  var BINARY_SNIFF_BYTES = 8192;
29
30
  var SECRET_EXTENSIONS = [
30
31
  ".pem",
@@ -160,18 +161,18 @@ function lineAt(starts, index) {
160
161
  }
161
162
  return lo + 1;
162
163
  }
163
- function scanForSecrets(text, path) {
164
+ function scanForSecrets(text2, path) {
164
165
  let starts;
165
166
  const findings = [];
166
167
  const seen = /* @__PURE__ */ new Set();
167
168
  for (const rule of RULES) {
168
169
  rule.re.lastIndex = 0;
169
- for (let m = rule.re.exec(text); m !== null; m = rule.re.exec(text)) {
170
+ for (let m = rule.re.exec(text2); m !== null; m = rule.re.exec(text2)) {
170
171
  if (m[0].length === 0) rule.re.lastIndex++;
171
172
  if (rule.allow?.(m)) continue;
172
173
  if (!starts) {
173
174
  starts = [0];
174
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) starts.push(i + 1);
175
+ for (let i = text2.indexOf("\n"); i !== -1; i = text2.indexOf("\n", i + 1)) starts.push(i + 1);
175
176
  }
176
177
  const line = lineAt(starts, m.index);
177
178
  const key = `${rule.id}:${line}`;
@@ -655,7 +656,7 @@ function describeVersionFiles(name, files, opts) {
655
656
  var READY_TIMEOUT_MS = 15e3;
656
657
  var NOT_READY_NOTE = "it can take a few more seconds to appear everywhere; reload if you see a 404";
657
658
  var isReachableStatus = (status) => status >= 200 && status < 400 || status === 401 || status === 403;
658
- var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
659
+ var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
659
660
  async function waitUntilReachable(url, opts = {}) {
660
661
  const timeoutMs = opts.timeoutMs ?? READY_TIMEOUT_MS;
661
662
  const doFetch = opts.fetch ?? ((u, init) => fetch(u, init));
@@ -729,7 +730,7 @@ function exitCodeFor(error) {
729
730
  // package.json
730
731
  var package_default = {
731
732
  name: "@nearform/fling",
732
- version: "0.1.2",
733
+ version: "0.2.0",
733
734
  license: "MIT",
734
735
  description: "Fling a folder of static files at fling.labs.nearform.com and get a link back. CLI + MCP server.",
735
736
  type: "module",
@@ -760,6 +761,7 @@ var package_default = {
760
761
  test: "vitest run"
761
762
  },
762
763
  dependencies: {
764
+ "@clack/prompts": "^1.8.1",
763
765
  "@modelcontextprotocol/sdk": "^1.20.0",
764
766
  chalk: "^5.6.2",
765
767
  commander: "^15.0.0",
@@ -893,12 +895,12 @@ async function putUpload(fetchImpl, url, headers, body, timeoutMs = 5 * 6e4) {
893
895
  }
894
896
  async function readBody(response) {
895
897
  if (response.status === 204) return void 0;
896
- const text = await response.text();
897
- if (text === "") return void 0;
898
+ const text2 = await response.text();
899
+ if (text2 === "") return void 0;
898
900
  try {
899
- return JSON.parse(text);
901
+ return JSON.parse(text2);
900
902
  } catch {
901
- return text;
903
+ return text2;
902
904
  }
903
905
  }
904
906
  function errorFromResponse(status, body, method, path) {
@@ -1101,8 +1103,8 @@ async function startCallbackServer(options = {}) {
1101
1103
  const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS;
1102
1104
  let settle;
1103
1105
  let timer;
1104
- const result = new Promise((resolve3, reject) => {
1105
- settle = { resolve: resolve3, reject };
1106
+ const result = new Promise((resolve5, reject) => {
1107
+ settle = { resolve: resolve5, reject };
1106
1108
  }).finally(() => clearTimeout(timer));
1107
1109
  result.catch(() => {
1108
1110
  });
@@ -1163,7 +1165,7 @@ ${close ? "<script>setTimeout(function(){window.close()},800)</script>" : ""}
1163
1165
  res.end(html);
1164
1166
  }
1165
1167
  function listenOnFreePort(server) {
1166
- return new Promise((resolve3, reject) => {
1168
+ return new Promise((resolve5, reject) => {
1167
1169
  let port = CALLBACK_PORT_START;
1168
1170
  const tryNext = () => {
1169
1171
  if (port > CALLBACK_PORT_END) {
@@ -1185,18 +1187,18 @@ function listenOnFreePort(server) {
1185
1187
  });
1186
1188
  server.listen(port, "127.0.0.1", () => {
1187
1189
  server.removeAllListeners("error");
1188
- resolve3(port);
1190
+ resolve5(port);
1189
1191
  });
1190
1192
  };
1191
1193
  tryNext();
1192
1194
  });
1193
1195
  }
1194
- function escapeHtml2(text) {
1195
- return text.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
1196
+ function escapeHtml2(text2) {
1197
+ return text2.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
1196
1198
  }
1197
1199
 
1198
1200
  // src/context.ts
1199
- import { createInterface } from "node:readline/promises";
1201
+ import { confirm, isCancel, text } from "@clack/prompts";
1200
1202
 
1201
1203
  // src/ready.ts
1202
1204
  import chalk from "chalk";
@@ -1232,20 +1234,83 @@ async function waitWithSpinner(waiter, url, paths, io) {
1232
1234
  }
1233
1235
 
1234
1236
  // src/utils/logger.ts
1237
+ import chalk3 from "chalk";
1238
+
1239
+ // src/ui/theme.ts
1235
1240
  import chalk2 from "chalk";
1236
- var PREFIX = chalk2.cyan("fling");
1241
+ var STOPS = [
1242
+ [0, 229, 255],
1243
+ [124, 77, 255],
1244
+ [255, 78, 205]
1245
+ ];
1246
+ var WORDMARK = [
1247
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
1248
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D ",
1249
+ "\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557",
1250
+ "\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
1251
+ "\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
1252
+ "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D "
1253
+ ];
1254
+ var TRAILS = [" ", " \u2501\u2501\u2501 ", " \u2501\u2501\u2501\u2501 ", " \u2501\u2501 ", " ", " "];
1255
+ var LOGO_WIDTH = TRAILS[0].length + WORDMARK[0].length;
1256
+ var brand = {
1257
+ primary: chalk2.hex("#7C4DFF"),
1258
+ accent: chalk2.hex("#00E5FF"),
1259
+ url: (url) => chalk2.bold.hex("#00E5FF")(url)
1260
+ };
1261
+ var symbols = {
1262
+ info: chalk2.hex("#7C4DFF")("\u25C6"),
1263
+ success: chalk2.green("\u2714"),
1264
+ warn: chalk2.yellow("\u25B2"),
1265
+ error: chalk2.red("\u2716"),
1266
+ step: chalk2.dim("\u2502")
1267
+ };
1268
+ function colourAt(t) {
1269
+ const scaled = Math.min(Math.max(t, 0), 1) * (STOPS.length - 1);
1270
+ const i = Math.min(Math.floor(scaled), STOPS.length - 2);
1271
+ const f = scaled - i;
1272
+ const a = STOPS[i];
1273
+ const b = STOPS[i + 1];
1274
+ return [0, 1, 2].map((k) => Math.round(a[k] + (b[k] - a[k]) * f));
1275
+ }
1276
+ function gradient(text2, width = text2.length, offset = 0) {
1277
+ if (chalk2.level === 0) return text2;
1278
+ return [...text2].map((ch, i) => ch === " " ? ch : chalk2.rgb(...colourAt((offset + i) / (width - 1)))(ch)).join("");
1279
+ }
1280
+ function logo(options = {}) {
1281
+ const columns = options.columns || process.stderr.columns || 80;
1282
+ const tagline = options.tagline ?? "throw static files at the web, get a link back";
1283
+ if (columns < LOGO_WIDTH + 2) return `${gradient("\u25B6\u25B6 fling")} ${chalk2.dim(tagline)}`;
1284
+ const lines = WORDMARK.map(
1285
+ (row, i) => chalk2.dim(TRAILS[i] ?? "") + gradient(row, WORDMARK[0].length)
1286
+ );
1287
+ return ["", ...lines, `${" ".repeat(TRAILS[0].length)}${chalk2.dim(tagline)}`, ""].join("\n");
1288
+ }
1289
+ var ANSI = /\x1b\[[0-9;]*m/g;
1290
+ var visibleLength = (s) => s.replace(ANSI, "").length;
1291
+ function box(lines, title) {
1292
+ const width = Math.max(...lines.map(visibleLength), title ? visibleLength(title) + 2 : 0);
1293
+ const border = chalk2.dim;
1294
+ const top = title ? `${border("\u256D\u2500")} ${title} ${border(`${"\u2500".repeat(width - visibleLength(title) - 1)}\u256E`)}` : border(`\u256D${"\u2500".repeat(width + 2)}\u256E`);
1295
+ const body = lines.map(
1296
+ (line) => `${border("\u2502")} ${line}${" ".repeat(width - visibleLength(line))} ${border("\u2502")}`
1297
+ );
1298
+ return [top, ...body, border(`\u2570${"\u2500".repeat(width + 2)}\u256F`)].join("\n");
1299
+ }
1300
+
1301
+ // src/utils/logger.ts
1237
1302
  function createLogger(options = {}) {
1238
1303
  const write = options.write ?? ((line) => process.stderr.write(`${line}
1239
1304
  `));
1240
1305
  const debugEnabled = options.debug ?? false;
1241
1306
  return {
1242
- info: (message) => write(`${PREFIX} ${message}`),
1243
- success: (message) => write(`${PREFIX} ${chalk2.green("\u2713")} ${message}`),
1244
- warn: (message) => write(`${PREFIX} ${chalk2.yellow("!")} ${message}`),
1245
- error: (message) => write(`${PREFIX} ${chalk2.red("\u2717")} ${message}`),
1246
- hint: (message) => write(chalk2.dim(` ${message}`)),
1307
+ info: (message) => write(`${symbols.info} ${message}`),
1308
+ success: (message) => write(`${symbols.success} ${message}`),
1309
+ warn: (message) => write(`${symbols.warn} ${chalk3.yellow(message)}`),
1310
+ error: (message) => write(`${symbols.error} ${chalk3.red(message)}`),
1311
+ hint: (message) => write(chalk3.dim(` ${message}`)),
1247
1312
  debug: (message) => {
1248
- if (debugEnabled) write(chalk2.dim(`[debug] ${message}`));
1313
+ if (debugEnabled) write(chalk3.dim(`[debug] ${message}`));
1249
1314
  }
1250
1315
  };
1251
1316
  }
@@ -1265,12 +1330,13 @@ var silentLogger = {
1265
1330
  };
1266
1331
 
1267
1332
  // src/utils/output.ts
1268
- import chalk3 from "chalk";
1333
+ import chalk4 from "chalk";
1269
1334
  function createOutput(options = {}) {
1270
1335
  const json = options.json ?? false;
1271
1336
  const write = options.write ?? ((chunk) => process.stdout.write(chunk));
1272
1337
  return {
1273
1338
  json,
1339
+ tty: options.tty ?? (options.write ? false : Boolean(process.stdout.isTTY)),
1274
1340
  data: (raw, human) => write(`${json ? formatJson(raw) : human()}
1275
1341
  `),
1276
1342
  text: (line) => {
@@ -1283,15 +1349,15 @@ function formatJson(value) {
1283
1349
  return JSON.stringify(value, null, 2);
1284
1350
  }
1285
1351
  function renderTable(rows, columns) {
1286
- if (rows.length === 0) return chalk3.dim("(no results)");
1352
+ if (rows.length === 0) return chalk4.dim("(no results)");
1287
1353
  const cells = rows.map((row) => columns.map((column) => formatCell(column.value(row))));
1288
1354
  const widths = columns.map(
1289
1355
  (column, i) => Math.max(column.header.length, ...cells.map((line) => line[i]?.length ?? 0))
1290
1356
  );
1291
- const pad = (text, i) => i === columns.length - 1 ? text : text.padEnd(widths[i] ?? 0);
1357
+ const pad = (text2, i) => i === columns.length - 1 ? text2 : text2.padEnd(widths[i] ?? 0);
1292
1358
  const header = columns.map((column, i) => pad(column.header, i)).join(" ");
1293
1359
  const body = cells.map((line) => line.map(pad).join(" "));
1294
- return [chalk3.bold(header), ...body].join("\n");
1360
+ return [chalk4.bold(header), ...body].join("\n");
1295
1361
  }
1296
1362
  function formatCell(value) {
1297
1363
  if (value === null || value === void 0 || value === "") return "-";
@@ -1341,16 +1407,14 @@ async function createContext(options = {}) {
1341
1407
  };
1342
1408
  }
1343
1409
  async function ttyConfirm(question) {
1344
- const answer = await ttyPrompt(`${question} [y/N] `);
1345
- return /^y(es)?$/i.test(answer.trim());
1410
+ const answer = await confirm({ message: question, initialValue: false, output: process.stderr });
1411
+ if (isCancel(answer)) throw new CliError("Cancelled");
1412
+ return answer;
1346
1413
  }
1347
1414
  async function ttyPrompt(question) {
1348
- const rl = createInterface({ input: process.stdin, output: process.stderr });
1349
- try {
1350
- return await rl.question(question);
1351
- } finally {
1352
- rl.close();
1353
- }
1415
+ const answer = await text({ message: question.trim(), output: process.stderr });
1416
+ if (isCancel(answer)) throw new CliError("Cancelled");
1417
+ return answer ?? "";
1354
1418
  }
1355
1419
  function ttyProgress(line) {
1356
1420
  process.stderr.write(`\r\x1B[2K${line ?? ""}`);
@@ -1505,16 +1569,18 @@ async function runWhoami(ctx) {
1505
1569
  apiKeySource: ctx.config.apiKeySource
1506
1570
  },
1507
1571
  () => [
1508
- `${chalk4.bold("User:")} ${me.name} <${me.email}>`,
1509
- `${chalk4.bold("Sites:")} ${sitesPattern(base)} ${chalk4.dim("(names are first come, first served)")}`,
1510
- `${chalk4.bold("Server:")} ${ctx.config.serverUrl}`,
1511
- `${chalk4.bold("API key:")} ${redactKey(ctx.config.apiKey)} ${chalk4.dim(`(from ${ctx.config.apiKeySource})`)}`
1572
+ `${chalk5.bold("User:")} ${me.name} <${me.email}>`,
1573
+ `${chalk5.bold("Sites:")} ${sitesPattern(base)} ${chalk5.dim("(names are first come, first served)")}`,
1574
+ `${chalk5.bold("Server:")} ${ctx.config.serverUrl}`,
1575
+ `${chalk5.bold("API key:")} ${redactKey(ctx.config.apiKey)} ${chalk5.dim(`(from ${ctx.config.apiKeySource})`)}`
1512
1576
  ].join("\n")
1513
1577
  );
1514
1578
  }
1515
1579
 
1516
1580
  // src/commands/deploy.ts
1517
- import chalk5 from "chalk";
1581
+ import { stat as stat4 } from "node:fs/promises";
1582
+ import { resolve as resolve3 } from "node:path";
1583
+ import chalk6 from "chalk";
1518
1584
 
1519
1585
  // src/deploy.ts
1520
1586
  import { readFile as readFile4 } from "node:fs/promises";
@@ -1534,7 +1600,7 @@ import { join as join2, relative, sep } from "node:path";
1534
1600
  import ignore from "ignore";
1535
1601
  var GIT_TIMEOUT_MS = 2e4;
1536
1602
  function git(cwd, args, input) {
1537
- return new Promise((resolve3) => {
1603
+ return new Promise((resolve5) => {
1538
1604
  try {
1539
1605
  const child = execFile(
1540
1606
  "git",
@@ -1542,14 +1608,14 @@ function git(cwd, args, input) {
1542
1608
  { timeout: GIT_TIMEOUT_MS, maxBuffer: 256 * 1024 * 1024, encoding: "utf8" },
1543
1609
  (error, stdout) => {
1544
1610
  const code = error?.code;
1545
- if (error && !(args[0] === "check-ignore" && code === 1)) resolve3(null);
1546
- else resolve3(stdout);
1611
+ if (error && !(args[0] === "check-ignore" && code === 1)) resolve5(null);
1612
+ else resolve5(stdout);
1547
1613
  }
1548
1614
  );
1549
- child.on("error", () => resolve3(null));
1615
+ child.on("error", () => resolve5(null));
1550
1616
  if (input !== void 0) child.stdin?.end(input);
1551
1617
  } catch {
1552
- resolve3(null);
1618
+ resolve5(null);
1553
1619
  }
1554
1620
  });
1555
1621
  }
@@ -1598,13 +1664,13 @@ function nestedMode() {
1598
1664
  return false;
1599
1665
  },
1600
1666
  async enterDir(absDir, relDir) {
1601
- let text;
1667
+ let text2;
1602
1668
  try {
1603
- text = await readFile2(join2(absDir, ".gitignore"), "utf8");
1669
+ text2 = await readFile2(join2(absDir, ".gitignore"), "utf8");
1604
1670
  } catch {
1605
1671
  return;
1606
1672
  }
1607
- levels.push({ base: relDir, ig: ignore().add(text) });
1673
+ levels.push({ base: relDir, ig: ignore().add(text2) });
1608
1674
  }
1609
1675
  };
1610
1676
  }
@@ -2297,428 +2363,342 @@ function wrapperPage(file) {
2297
2363
  `;
2298
2364
  }
2299
2365
 
2300
- // src/locate.ts
2301
- import { lstat, readdir as readdir2, readFile as readFile6, realpath as realpath3, stat as stat4 } from "node:fs/promises";
2302
- import { posix, win32 } from "node:path";
2303
- import { fileURLToPath } from "node:url";
2304
- var DOWNLOADS_DIR_ENV = "FLING_DOWNLOADS_DIR";
2305
- var FOUND_AS_GIVEN = "path";
2306
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "__MACOSX", "Library", "AppData", "$RECYCLE.BIN"]);
2307
- var SESSION_SKIP_DIRS = /* @__PURE__ */ new Set(["skills-plugin", "rpm", "uploads", "memory"]);
2308
- var SESSIONS_DEPTH = 4;
2309
- var OUTPUTS_DEPTH = 4;
2310
- var SANDBOX_PATH_RE = /^\/(mnt\/user-data|mnt\/data|sessions|home\/claude|tmp\/outputs)(\/|$)/;
2311
- var nodeFs = {
2312
- async stat(path) {
2313
- try {
2314
- const s = await stat4(path);
2315
- return { isFile: s.isFile(), isDirectory: s.isDirectory(), mtimeMs: s.mtimeMs, size: s.size };
2316
- } catch {
2317
- return null;
2318
- }
2319
- },
2320
- async readdir(path) {
2321
- try {
2322
- const entries = await readdir2(path, { withFileTypes: true });
2323
- const out = [];
2324
- for (const e of entries) {
2325
- if (e.isSymbolicLink()) {
2326
- const s = await stat4(`${path}/${e.name}`).catch(() => null);
2327
- out.push({ name: e.name, isFile: Boolean(s?.isFile()), isDirectory: false });
2328
- } else out.push({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() });
2366
+ // src/commands/deploy.ts
2367
+ async function runDeploy(ctx, input, opts) {
2368
+ const client = requireClient(ctx);
2369
+ const ask = ctx.interactive && !opts.yes;
2370
+ const base = ask ? await discoverSitesBase(client) : sitesBaseFor(client.serverUrl);
2371
+ const path = resolve3(input ?? ".");
2372
+ if (!await stat4(path).catch(() => null))
2373
+ throw new CliError(`No such file or folder: ${input ?? "."}`, {
2374
+ hint: `Give the path to a folder, .html, .zip or other file, e.g. \`${ctx.binName} deploy ./dist\`.`
2375
+ });
2376
+ const prepared = await prepareInput(path);
2377
+ try {
2378
+ await deployPrepared(ctx, client, base, prepared, opts, ask);
2379
+ } finally {
2380
+ await prepared.cleanup();
2381
+ }
2382
+ }
2383
+ async function deployPrepared(ctx, client, base, prepared, opts, ask) {
2384
+ let local = await buildManifest(prepared.dir);
2385
+ local.skipped.dotfiles.unshift(...prepared.skippedDotfiles);
2386
+ for (const line of describeSkipped(local.skipped)) ctx.log.info(line);
2387
+ let name = opts.name !== void 0 ? resolveSiteName(prepared.dir, opts.name) : prepared.defaultName;
2388
+ if (opts.name === void 0 && ask) name = await confirmSiteName(ctx, client, name, base);
2389
+ if (!hasRootIndex(local)) {
2390
+ const pages = htmlPages(local).length;
2391
+ const root = base ? siteUrlFor(name, base) : "the site root";
2392
+ const warning = `No index.html at the root \u2014 ${root} will show a 404.`;
2393
+ const choice = opts.listHtml === true ? "list" : opts.listHtml === false ? "fling" : ask ? await askNoIndex(ctx, pages) : pages > 0 ? "list" : "fling";
2394
+ if (choice === "list") {
2395
+ local = await withHtmlListing(local, name);
2396
+ ctx.log.info(`Generated index.html listing ${pages} HTML page${pages === 1 ? "" : "s"}`);
2397
+ } else ctx.log.warn(warning);
2398
+ }
2399
+ let progressShown = false;
2400
+ const result = await deploy(
2401
+ { client, fetch: ctx.fetch },
2402
+ {
2403
+ dir: prepared.dir,
2404
+ name,
2405
+ manifest: local,
2406
+ spa: opts.spa ? true : void 0,
2407
+ public: opts.public,
2408
+ onProgress: (p) => {
2409
+ if (p.total === 0) return;
2410
+ progressShown = true;
2411
+ ctx.progress(
2412
+ `${progressBar(p.totalBytes ? p.bytes / p.totalBytes : p.uploaded / p.total)} uploading ${p.uploaded}/${p.total} files \xB7 ${formatBytes(p.bytes)}/${formatBytes(p.totalBytes)}`
2413
+ );
2329
2414
  }
2330
- return out;
2331
- } catch {
2332
- return [];
2333
2415
  }
2334
- },
2335
- async realpath(path) {
2336
- return realpath3(path).catch(() => null);
2337
- },
2338
- async readText(path) {
2339
- try {
2340
- if (!(await lstat(path)).isFile()) return null;
2341
- return await readFile6(path, "utf8");
2342
- } catch {
2343
- return null;
2416
+ ).finally(() => {
2417
+ if (progressShown) ctx.progress(null);
2418
+ });
2419
+ for (const warning of result.warnings) if (warning !== NO_INDEX_WARNING) ctx.log.warn(warning);
2420
+ const summary = `${result.fileCount} files, ${formatBytes(result.totalBytes)}`;
2421
+ if (result.status === "unchanged") ctx.log.info(`Unchanged ${chalk6.dim(`(${summary})`)}`);
2422
+ else if (result.status === "repointed")
2423
+ ctx.log.success(`Restored version ${result.versionId} ${chalk6.dim(`(${summary})`)}`);
2424
+ else
2425
+ ctx.log.success(
2426
+ `Deployed ${result.name} ${chalk6.dim(`(${summary}; uploaded ${result.uploaded}, version ${result.versionId})`)}`
2427
+ );
2428
+ const ready = result.status === "unchanged" ? true : (await waitWithSpinner(
2429
+ ctx.waitReady,
2430
+ result.url,
2431
+ local.files.map((f) => f.path),
2432
+ { progress: ctx.progress, warn: (m) => ctx.log.warn(m) }
2433
+ )).ready;
2434
+ const privateNote = `Private: Nearform sign-in required. Share publicly with \`${ctx.binName} share ${result.name}\`.`;
2435
+ ctx.out.data({ status: result.status, site: result.site, ready, skipped: local.skipped }, () => {
2436
+ const isPrivate = result.site.visibility === "private";
2437
+ if (ctx.out.tty)
2438
+ return box(
2439
+ [brand.url(result.url), ...isPrivate ? [chalk6.dim(privateNote)] : []],
2440
+ isPrivate ? chalk6.bold("flung \xB7 private") : chalk6.bold("flung \xB7 public")
2441
+ );
2442
+ const lines = [chalk6.bold(result.url)];
2443
+ if (isPrivate) lines.push(chalk6.dim(privateNote));
2444
+ return lines.join("\n");
2445
+ });
2446
+ }
2447
+ var BAR_WIDTH = 20;
2448
+ function progressBar(fraction) {
2449
+ const done = Math.round(Math.min(Math.max(fraction, 0), 1) * BAR_WIDTH);
2450
+ return brand.primary("\u2501".repeat(done)) + chalk6.dim("\u2500".repeat(BAR_WIDTH - done));
2451
+ }
2452
+ async function askNoIndex(ctx, pages) {
2453
+ const question = pages > 0 ? "No index.html at the root. [L]ist the HTML pages on a generated index page / [f]ling anyway (the root will 404) / [a]bort " : "No index.html at the root (and no HTML pages to list). [f]ling anyway (the root will 404) / [A]bort ";
2454
+ for (; ; ) {
2455
+ const answer = (await ctx.prompt(question)).trim().toLowerCase();
2456
+ if (pages > 0 && (answer === "" || answer === "l" || answer === "list")) return "list";
2457
+ if (answer === "f" || answer === "fling") return "fling";
2458
+ if (answer === "a" || answer === "abort" || answer === "n" || answer === "no" || answer === "")
2459
+ throw new CliError("Not flung: no index.html at the root", {
2460
+ hint: "Fling the folder that contains index.html (your build output, e.g. ./dist), or pass --list-html / --no-list-html."
2461
+ });
2462
+ }
2463
+ }
2464
+ async function confirmSiteName(ctx, client, generated, base) {
2465
+ const where = (n) => base ? ` \u2192 ${siteUrlFor(n, base)}` : "";
2466
+ const describe = (n, note) => ctx.log.info(`Site name: ${chalk6.bold(n)}${where(n)} ${chalk6.dim(`(${note})`)}`);
2467
+ let candidate = generated;
2468
+ for (; ; ) {
2469
+ const check2 = await checkName(client, candidate);
2470
+ let proposal;
2471
+ if (check2.status === "available" || check2.status === "yours") {
2472
+ proposal = candidate;
2473
+ describe(candidate, check2.status === "yours" ? "updates your existing site" : "new");
2474
+ } else {
2475
+ const why = check2.status === "taken" ? "is taken" : "is not a valid site name";
2476
+ const alternatives = check2.suggestions.length ? ` Suggestions: ${check2.suggestions.join(", ")}` : "";
2477
+ ctx.log.warn(`${candidate} ${why}.${alternatives}`);
2478
+ proposal = check2.suggestions[0];
2479
+ if (proposal) describe(proposal, "new");
2480
+ }
2481
+ for (; ; ) {
2482
+ const question = proposal ? `Use "${proposal}"? [Y/n or type another name] ` : "Type a site name (or n to abort): ";
2483
+ const answer = (await ctx.prompt(question)).trim();
2484
+ if (proposal && (answer === "" || /^y(es)?$/i.test(answer))) return proposal;
2485
+ if (/^no?$/i.test(answer))
2486
+ throw new CliError("Aborted \u2014 nothing was flung", {
2487
+ hint: "Pass --name <name> to choose the site name up front."
2488
+ });
2489
+ if (answer === "") continue;
2490
+ if (!isValidSiteName(answer)) {
2491
+ ctx.log.warn(
2492
+ `"${answer}" is not a valid site name: use lowercase letters, digits and single hyphens (max ${SITE_NAME_MAX}); some names are reserved.`
2493
+ );
2494
+ continue;
2495
+ }
2496
+ candidate = answer;
2497
+ break;
2344
2498
  }
2345
2499
  }
2346
- };
2347
- var pathFor = (platform) => platform === "win32" ? win32 : posix;
2348
- var usable = (value) => Boolean(value?.trim()) && !value?.trim().startsWith("${");
2349
- async function xdgUserDir(key, opts, fs) {
2350
- const fromEnv2 = opts.env[`XDG_${key}_DIR`];
2351
- if (usable(fromEnv2)) return fromEnv2;
2352
- const configHome = opts.env.XDG_CONFIG_HOME || posix.join(opts.home, ".config");
2353
- const text = await fs.readText(posix.join(configHome, "user-dirs.dirs"));
2354
- const match = text?.match(new RegExp(`^XDG_${key}_DIR="([^"]*)"`, "m"));
2355
- if (!match?.[1]) return void 0;
2356
- return match[1].replace(/^\$HOME(?=\/|$)/, opts.home);
2357
2500
  }
2358
- async function searchRoots(opts) {
2359
- const fs = opts.fs ?? nodeFs;
2360
- const p = pathFor(opts.platform);
2361
- const { home, env, platform } = opts;
2362
- const userHome = platform === "win32" && usable(env.USERPROFILE) ? env.USERPROFILE : home;
2363
- const roots = [];
2364
- const add = (label, path, depth, sessions = false) => {
2365
- if (!path) return;
2366
- const norm = p.resolve(path);
2367
- if (roots.some((r) => r.path === norm)) return;
2368
- roots.push({ label, path: norm, depth, ...sessions ? { sessions } : {} });
2369
- };
2370
- const cwd = p.resolve(opts.cwd);
2371
- if (cwd !== p.parse(cwd).root && cwd !== p.resolve(userHome)) add("current folder", cwd, 4);
2372
- const configured = usable(opts.downloadsDir) ? opts.downloadsDir : usable(env[DOWNLOADS_DIR_ENV]) ? env[DOWNLOADS_DIR_ENV] : void 0;
2373
- const defaultDownloads = platform === "linux" ? await xdgUserDir("DOWNLOAD", opts, fs) ?? p.join(home, "Downloads") : p.join(userHome, "Downloads");
2374
- add("Downloads", configured ? expandHome(configured, home, platform) : defaultDownloads, 3);
2375
- add("Downloads", defaultDownloads, 3);
2376
- const desktop = platform === "linux" ? await xdgUserDir("DESKTOP", opts, fs) ?? p.join(home, "Desktop") : p.join(userHome, "Desktop");
2377
- add("Desktop", desktop, 3);
2378
- if (platform === "win32" && usable(env.OneDrive))
2379
- add("Desktop", p.join(env.OneDrive, "Desktop"), 3);
2380
- const documents = platform === "linux" ? await xdgUserDir("DOCUMENTS", opts, fs) ?? p.join(home, "Documents") : p.join(userHome, "Documents");
2381
- add("Documents/Claude", p.join(documents, "Claude"), 4);
2382
- add("~/Claude", p.join(userHome, "Claude"), 4);
2383
- const claudeConfig = platform === "darwin" ? p.join(home, "Library", "Application Support", "Claude") : platform === "win32" ? p.join(
2384
- usable(env.APPDATA) ? env.APPDATA : p.join(userHome, "AppData", "Roaming"),
2385
- "Claude"
2386
- ) : p.join(env.XDG_CONFIG_HOME || p.join(home, ".config"), "Claude");
2387
- add(
2388
- "Claude Desktop outputs",
2389
- p.join(claudeConfig, "local-agent-mode-sessions"),
2390
- SESSIONS_DEPTH,
2391
- true
2501
+
2502
+ // src/commands/setup.ts
2503
+ import { intro, outro, spinner } from "@clack/prompts";
2504
+ import chalk7 from "chalk";
2505
+
2506
+ // src/claude.ts
2507
+ import { execFile as execFile2 } from "node:child_process";
2508
+ import { access, mkdtemp as mkdtemp2, writeFile as writeFile3 } from "node:fs/promises";
2509
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
2510
+ import { join as join5 } from "node:path";
2511
+ var MARKETPLACE_SOURCE = "nearform/fling";
2512
+ var MARKETPLACE_NAME = "fling";
2513
+ var PLUGIN_NAME = "fling";
2514
+ var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2515
+ var MCPB_PATH = "/downloads/fling.mcpb";
2516
+ var runClaude = (args) => new Promise((resolve5) => {
2517
+ execFile2(
2518
+ "claude",
2519
+ [...args],
2520
+ { timeout: 12e4, maxBuffer: 16 * 1024 * 1024, shell: process.platform === "win32" },
2521
+ (error, stdout, stderr) => {
2522
+ const missing = error?.code === "ENOENT";
2523
+ const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
2524
+ resolve5({ code, stdout: String(stdout), stderr: String(stderr) });
2525
+ }
2392
2526
  );
2393
- if (!fs.realpath) return roots;
2394
- const real = [];
2395
- for (const root of roots) {
2396
- const path = await fs.realpath(root.path) ?? root.path;
2397
- if (!real.some((r) => r.path === path)) real.push({ ...root, path });
2527
+ });
2528
+ async function claudeCodeStatus(run) {
2529
+ const version = await run(["--version"]);
2530
+ if (version.code !== 0) return { claude: "missing" };
2531
+ const claude = version.stdout.trim().split(/\s+/)[0] ?? "unknown";
2532
+ const list = await run(["plugin", "list", "--json"]);
2533
+ let plugins = [];
2534
+ try {
2535
+ plugins = JSON.parse(list.stdout);
2536
+ } catch {
2398
2537
  }
2399
- return real;
2538
+ const fling = Array.isArray(plugins) ? plugins.find((p) => p.id === PLUGIN_ID) : void 0;
2539
+ if (!fling) return { claude, installed: false };
2540
+ return {
2541
+ claude,
2542
+ installed: true,
2543
+ version: fling.version,
2544
+ scope: fling.scope,
2545
+ enabled: fling.enabled
2546
+ };
2400
2547
  }
2401
- function expandHome(input, home, platform) {
2402
- if (input === "~") return home;
2403
- if (input.startsWith("~/") || platform === "win32" && input.startsWith("~\\"))
2404
- return pathFor(platform).join(home, input.slice(2));
2405
- return input;
2548
+ async function installPlugin(run, scope) {
2549
+ check(
2550
+ await run(["plugin", "install", PLUGIN_NAME, "--marketplace", MARKETPLACE_SOURCE, "-s", scope]),
2551
+ "install the Fling plugin"
2552
+ );
2406
2553
  }
2407
- async function walk(roots, opts, visit) {
2408
- const fs = opts.fs ?? nodeFs;
2409
- const p = pathFor(opts.platform);
2410
- const now = opts.now ?? Date.now;
2411
- const deadline = now() + (opts.timeBudgetMs ?? 4e3);
2412
- let budget = opts.entryBudget ?? 6e4;
2413
- const skip = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
2414
- const expanded = [];
2415
- for (const root of roots) {
2416
- if (!root.sessions) {
2417
- expanded.push(root);
2418
- continue;
2554
+ async function updatePlugin(run, scope) {
2555
+ check(await run(["plugin", "marketplace", "update", MARKETPLACE_NAME]), "update the marketplace");
2556
+ check(
2557
+ await run(["plugin", "update", PLUGIN_ID, ...scope ? ["-s", scope] : []]),
2558
+ "update the Fling plugin"
2559
+ );
2560
+ }
2561
+ function check(result, what) {
2562
+ if (result.code === 0) return;
2563
+ const detail = (result.stderr || result.stdout).trim().split("\n").at(-1);
2564
+ throw new Error(`Could not ${what}${detail ? `: ${detail}` : ""}`);
2565
+ }
2566
+ async function findClaudeDesktop(platform = process.platform, env = process.env, exists = (path) => access(path).then(
2567
+ () => true,
2568
+ () => false
2569
+ )) {
2570
+ const candidates = platform === "darwin" ? ["/Applications/Claude.app", join5(homedir3(), "Applications", "Claude.app")] : platform === "win32" && env.LOCALAPPDATA ? [join5(env.LOCALAPPDATA, "AnthropicClaude"), join5(env.LOCALAPPDATA, "Programs", "Claude")] : [];
2571
+ for (const path of candidates) if (await exists(path)) return path;
2572
+ return null;
2573
+ }
2574
+ async function downloadExtension(serverUrl, fetch2) {
2575
+ const url = new URL(MCPB_PATH, serverUrl).toString();
2576
+ const response = await fetch2(url);
2577
+ if (!response.ok) throw new Error(`Could not download ${url} (HTTP ${response.status})`);
2578
+ const dir = await mkdtemp2(join5(tmpdir2(), "fling-mcpb-"));
2579
+ const file = join5(dir, "fling.mcpb");
2580
+ await writeFile3(file, Buffer.from(await response.arrayBuffer()));
2581
+ return file;
2582
+ }
2583
+
2584
+ // src/commands/setup.ts
2585
+ var SCOPES = ["user", "project", "local"];
2586
+ async function runSetup(baseCtx, opts, deps = {}) {
2587
+ const scope = opts.scope ?? "user";
2588
+ if (!SCOPES.includes(scope))
2589
+ throw new CliError(`--scope must be one of ${SCOPES.join(", ")}, not "${opts.scope}"`);
2590
+ const run = deps.runClaude ?? runClaude;
2591
+ const ask = baseCtx.interactive && !opts.yes;
2592
+ const fancy = baseCtx.interactive && !baseCtx.json;
2593
+ const ctx = fancy ? {
2594
+ ...baseCtx,
2595
+ log: createLogger({
2596
+ debug: baseCtx.config.debug,
2597
+ write: (line) => process.stderr.write(`${chalk7.gray("\u2502")} ${line}
2598
+ `)
2599
+ })
2600
+ } : baseCtx;
2601
+ if (fancy) {
2602
+ process.stderr.write(`${logo()}
2603
+ `);
2604
+ intro(chalk7.bold(" fling setup "), { output: process.stderr });
2605
+ }
2606
+ const results = {
2607
+ login: await stepLogin(ctx),
2608
+ claudeCode: "skipped",
2609
+ claudeDesktop: "skipped"
2610
+ };
2611
+ results.claudeCode = await stepClaudeCode(ctx, run, scope, ask, fancy);
2612
+ results.claudeDesktop = await stepClaudeDesktop(ctx, deps, ask, fancy);
2613
+ const failed = Object.values(results).includes("failed");
2614
+ if (fancy)
2615
+ outro(
2616
+ failed ? chalk7.yellow("Setup finished with problems \u2014 see above, then run `fling setup` again.") : `You're set. Try ${chalk7.bold(`${ctx.binName} deploy ./dist`)}, or ask Claude to \u201Cfling this\u201D.`,
2617
+ { output: process.stderr }
2618
+ );
2619
+ ctx.out.data(results, () => "");
2620
+ if (failed) process.exitCode = 1;
2621
+ }
2622
+ async function stepLogin(ctx) {
2623
+ if (ctx.config.apiKey) {
2624
+ try {
2625
+ const me = await whoami(requireClient(ctx));
2626
+ ctx.log.success(`Signed in as ${chalk7.bold(me.name)} ${chalk7.dim(`<${me.email}>`)}`);
2627
+ return "already";
2628
+ } catch (error) {
2629
+ if (exitCodeFor(error) !== 2) {
2630
+ ctx.log.error(error instanceof Error ? error.message : String(error));
2631
+ return "failed";
2632
+ }
2633
+ ctx.log.warn("The stored API key no longer works \u2014 signing in again");
2419
2634
  }
2420
- let level = [root.path];
2421
- for (let depth = 0; depth < root.depth && level.length; depth++) {
2422
- const next = [];
2423
- for (const dir of level) {
2424
- for (const e of await fs.readdir(dir)) {
2425
- if (!e.isDirectory || skip(e.name) || SESSION_SKIP_DIRS.has(e.name)) continue;
2426
- const child = p.join(dir, e.name);
2427
- if (e.name === "outputs")
2428
- expanded.push({ label: root.label, path: child, depth: OUTPUTS_DEPTH });
2429
- else next.push(child);
2430
- }
2431
- }
2432
- level = next;
2433
- }
2434
- }
2435
- for (const root of expanded) {
2436
- let level = [root.path];
2437
- for (let depth = 0; depth <= root.depth && level.length; depth++) {
2438
- const next = [];
2439
- for (const dir of level) {
2440
- if (now() > deadline || budget <= 0) return { complete: false };
2441
- const entries = await fs.readdir(dir);
2442
- budget -= entries.length;
2443
- for (const entry of entries) {
2444
- if (entry.name.startsWith(".")) continue;
2445
- const path = p.join(dir, entry.name);
2446
- const result = await visit({ root, path, entry, siblings: entries });
2447
- if (entry.isDirectory && !skip(entry.name) && result !== "skip" && depth < root.depth)
2448
- next.push(path);
2449
- }
2450
- }
2451
- level = next;
2452
- }
2453
- }
2454
- return { complete: true };
2455
- }
2456
- function withoutDuplicateSuffix(name) {
2457
- return name.replace(/ \(\d+\)(?=\.[^.]+$|$)/, "");
2458
- }
2459
- function namesMatch(candidate, wanted) {
2460
- const a = candidate.toLowerCase();
2461
- const b = wanted.toLowerCase();
2462
- return a === b || withoutDuplicateSuffix(a) === withoutDuplicateSuffix(b);
2463
- }
2464
- var splitSegments = (path) => path.split(/[\\/]+/).filter((s) => s !== "" && s !== "." && s !== "~");
2465
- function suffixScore(path, wanted) {
2466
- const have = splitSegments(path);
2467
- let score = 1;
2468
- for (let i = 2; i <= Math.min(have.length, wanted.length); i++) {
2469
- if (have[have.length - i]?.toLowerCase() !== wanted[wanted.length - i]?.toLowerCase()) break;
2470
- score++;
2471
- }
2472
- return score;
2473
- }
2474
- var looksLikeSandboxPath = (input) => SANDBOX_PATH_RE.test(input.replace(/\\/g, "/"));
2475
- function normaliseInput(raw, opts) {
2476
- let input = raw.trim().replace(/^(["'])(.*)\1$/, "$2");
2477
- if (input.startsWith("file://")) {
2478
- try {
2479
- input = fileURLToPath(input, { windows: opts.platform === "win32" });
2480
- } catch {
2481
- input = decodeURIComponent(input.slice("file://".length));
2482
- }
2483
- }
2484
- return expandHome(input, opts.home, opts.platform);
2485
- }
2486
- async function resolveInput(input, opts) {
2487
- const fs = opts.fs ?? nodeFs;
2488
- const p = pathFor(opts.platform);
2489
- const given = normaliseInput(input, opts);
2490
- if (!given) throw new CliError("No path given");
2491
- const direct = p.isAbsolute(given) ? p.normalize(given) : p.resolve(opts.cwd, given);
2492
- const directStat = await fs.stat(direct);
2493
- if (directStat && (directStat.isFile || directStat.isDirectory)) {
2494
- return {
2495
- path: direct,
2496
- foundIn: FOUND_AS_GIVEN,
2497
- isDirectory: directStat.isDirectory,
2498
- alternatives: []
2499
- };
2500
- }
2501
- const wanted = splitSegments(given);
2502
- const name = wanted.at(-1);
2503
- const roots = await searchRoots(opts);
2504
- if (!name) throw notFound(input, roots);
2505
- const found = [];
2506
- const seen = /* @__PURE__ */ new Set();
2507
- await walk(roots, opts, async ({ root, path, entry }) => {
2508
- if (!(entry.isFile || entry.isDirectory) || !namesMatch(entry.name, name)) return;
2509
- if (seen.has(path)) return;
2510
- seen.add(path);
2511
- const s = await fs.stat(path);
2512
- if (!s) return;
2513
- found.push({
2514
- path,
2515
- foundIn: root.label,
2516
- kind: s.isDirectory ? "folder" : "file",
2517
- modified: new Date(s.mtimeMs).toISOString(),
2518
- size: s.size,
2519
- score: suffixScore(path, wanted),
2520
- mtimeMs: s.mtimeMs,
2521
- isDirectory: s.isDirectory
2522
- });
2523
- return void 0;
2524
- });
2525
- if (found.length === 0) throw notFound(input, roots);
2526
- found.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs);
2527
- const strip = ({ score: _s, mtimeMs: _m, isDirectory: _d, ...c }) => c;
2528
- const [best, ...rest] = found;
2529
- return {
2530
- path: best.path,
2531
- foundIn: best.foundIn,
2532
- isDirectory: best.isDirectory,
2533
- alternatives: rest.slice(0, 5).map(strip)
2534
- };
2535
- }
2536
- function notFound(input, roots) {
2537
- const where = roots.map((r) => `${r.label} (${r.path})`).join(", ");
2538
- const sandbox = looksLikeSandboxPath(input) ? " That looks like a path inside Claude\u2019s sandbox, which this computer cannot see: publish the content inline with fling_publish, or save/download the file to this computer first." : "";
2539
- return new CliError(`Could not find "${input}". Looked for it by name in: ${where}.${sandbox}`, {
2540
- hint: "Pass the full path, or save the file to your Downloads folder and use just its name."
2541
- });
2542
- }
2543
- var LISTED_EXT = /\.(html?|zip)$/i;
2544
- async function findFiles(opts, find = {}) {
2545
- const fs = opts.fs ?? nodeFs;
2546
- const p = pathFor(opts.platform);
2547
- const now = (opts.now ?? Date.now)();
2548
- const since = now - (find.days ?? 14) * 24 * 60 * 60 * 1e3;
2549
- const query = find.query?.trim().toLowerCase();
2550
- const roots = await searchRoots(opts);
2551
- const found = /* @__PURE__ */ new Map();
2552
- const rootPaths = new Set(roots.map((r) => r.path));
2553
- const { complete } = await walk(roots, opts, async ({ root, path, entry }) => {
2554
- if (entry.isDirectory) {
2555
- if (rootPaths.has(path)) return;
2556
- const index = (await fs.readdir(path)).find((e) => e.isFile && /^index\.html?$/i.test(e.name));
2557
- if (!index) return;
2558
- const s2 = await fs.stat(p.join(path, index.name));
2559
- if (s2 && s2.mtimeMs >= since && (!query || entry.name.toLowerCase().includes(query)))
2560
- found.set(path, {
2561
- path,
2562
- foundIn: root.label,
2563
- kind: "folder",
2564
- modified: new Date(s2.mtimeMs).toISOString(),
2565
- size: s2.size,
2566
- mtimeMs: s2.mtimeMs
2567
- });
2568
- return "skip";
2569
- }
2570
- if (!entry.isFile || !LISTED_EXT.test(entry.name)) return;
2571
- if (query && !entry.name.toLowerCase().includes(query)) return;
2572
- const s = await fs.stat(path);
2573
- if (!s || s.mtimeMs < since || found.has(path)) return;
2574
- found.set(path, {
2575
- path,
2576
- foundIn: root.label,
2577
- kind: "file",
2578
- modified: new Date(s.mtimeMs).toISOString(),
2579
- size: s.size,
2580
- mtimeMs: s.mtimeMs
2581
- });
2582
- return void 0;
2583
- });
2584
- const files = [...found.values()].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, find.limit ?? 20).map(({ mtimeMs: _m, ...c }) => c);
2585
- return { files, searched: roots, complete };
2586
- }
2587
-
2588
- // src/commands/deploy.ts
2589
- async function runDeploy(ctx, input, opts) {
2590
- const client = requireClient(ctx);
2591
- const ask = ctx.interactive && !opts.yes;
2592
- const base = ask ? await discoverSitesBase(client) : sitesBaseFor(client.serverUrl);
2593
- const located = await resolveInput(input ?? ".", ctx.locate ?? systemLocateOptions());
2594
- if (located.foundIn !== FOUND_AS_GIVEN) {
2595
- ctx.log.info(`Found in ${located.foundIn}: ${located.path}`);
2596
- if (located.alternatives.length)
2597
- ctx.log.hint(
2598
- `Also found (older): ${located.alternatives.map((a) => a.path).join(", ")} \u2014 pass the full path to fling another.`
2599
- );
2600
2635
  }
2601
- const prepared = await prepareInput(located.path);
2602
2636
  try {
2603
- await deployPrepared(ctx, client, base, prepared, opts, ask);
2604
- } finally {
2605
- await prepared.cleanup();
2637
+ await runLogin({ ...ctx, json: false });
2638
+ return "done";
2639
+ } catch (error) {
2640
+ ctx.log.error(error instanceof Error ? error.message : String(error));
2641
+ return "failed";
2606
2642
  }
2607
2643
  }
2608
- async function deployPrepared(ctx, client, base, prepared, opts, ask) {
2609
- let local = await buildManifest(prepared.dir);
2610
- local.skipped.dotfiles.unshift(...prepared.skippedDotfiles);
2611
- for (const line of describeSkipped(local.skipped)) ctx.log.info(line);
2612
- let name = opts.name !== void 0 ? resolveSiteName(prepared.dir, opts.name) : prepared.defaultName;
2613
- if (opts.name === void 0 && ask) name = await confirmSiteName(ctx, client, name, base);
2614
- if (!hasRootIndex(local)) {
2615
- const pages = htmlPages(local).length;
2616
- const root = base ? siteUrlFor(name, base) : "the site root";
2617
- const warning = `No index.html at the root \u2014 ${root} will show a 404.`;
2618
- const choice = opts.listHtml === true ? "list" : opts.listHtml === false ? "fling" : ask ? await askNoIndex(ctx, pages) : pages > 0 ? "list" : "fling";
2619
- if (choice === "list") {
2620
- local = await withHtmlListing(local, name);
2621
- ctx.log.info(`Generated index.html listing ${pages} HTML page${pages === 1 ? "" : "s"}`);
2622
- } else ctx.log.warn(warning);
2623
- }
2624
- let progressShown = false;
2625
- const result = await deploy(
2626
- { client, fetch: ctx.fetch },
2627
- {
2628
- dir: prepared.dir,
2629
- name,
2630
- manifest: local,
2631
- spa: opts.spa ? true : void 0,
2632
- public: opts.public,
2633
- onProgress: (p) => {
2634
- if (p.total === 0) return;
2635
- progressShown = true;
2636
- ctx.progress(
2637
- `${chalk5.cyan("fling")} uploading ${p.uploaded}/${p.total} files \xB7 ${formatBytes(p.bytes)}/${formatBytes(p.totalBytes)}`
2638
- );
2639
- }
2640
- }
2641
- ).finally(() => {
2642
- if (progressShown) ctx.progress(null);
2643
- });
2644
- for (const warning of result.warnings) if (warning !== NO_INDEX_WARNING) ctx.log.warn(warning);
2645
- const summary = `${result.fileCount} files, ${formatBytes(result.totalBytes)}`;
2646
- if (result.status === "unchanged") ctx.log.info(`Unchanged ${chalk5.dim(`(${summary})`)}`);
2647
- else if (result.status === "repointed")
2648
- ctx.log.success(`Restored version ${result.versionId} ${chalk5.dim(`(${summary})`)}`);
2649
- else
2650
- ctx.log.success(
2651
- `Deployed ${result.name} ${chalk5.dim(`(${summary}; uploaded ${result.uploaded}, version ${result.versionId})`)}`
2644
+ async function stepClaudeCode(ctx, run, scope, ask, fancy) {
2645
+ const status = await claudeCodeStatus(run);
2646
+ if (status.claude === "missing") {
2647
+ ctx.log.info(`Claude Code not found ${chalk7.dim("(no `claude` on your PATH) \u2014 skipped")}`);
2648
+ ctx.log.hint(
2649
+ `Once it is installed, run \`${ctx.binName} setup\` again, or in Claude Code: /plugin marketplace add ${MARKETPLACE_SOURCE} then /plugin install ${PLUGIN_ID}`
2652
2650
  );
2653
- const ready = result.status === "unchanged" ? true : (await waitWithSpinner(
2654
- ctx.waitReady,
2655
- result.url,
2656
- local.files.map((f) => f.path),
2657
- { progress: ctx.progress, warn: (m) => ctx.log.warn(m) }
2658
- )).ready;
2659
- ctx.out.data({ status: result.status, site: result.site, ready, skipped: local.skipped }, () => {
2660
- const lines = [chalk5.bold(result.url)];
2661
- if (result.site.visibility === "private")
2662
- lines.push(
2663
- chalk5.dim(
2664
- `Private: Nearform sign-in required. Share publicly with \`${ctx.binName} share ${result.name}\`.`
2665
- )
2666
- );
2667
- return lines.join("\n");
2668
- });
2669
- }
2670
- async function askNoIndex(ctx, pages) {
2671
- const question = pages > 0 ? "No index.html at the root. [L]ist the HTML pages on a generated index page / [f]ling anyway (the root will 404) / [a]bort " : "No index.html at the root (and no HTML pages to list). [f]ling anyway (the root will 404) / [A]bort ";
2672
- for (; ; ) {
2673
- const answer = (await ctx.prompt(question)).trim().toLowerCase();
2674
- if (pages > 0 && (answer === "" || answer === "l" || answer === "list")) return "list";
2675
- if (answer === "f" || answer === "fling") return "fling";
2676
- if (answer === "a" || answer === "abort" || answer === "n" || answer === "no" || answer === "")
2677
- throw new CliError("Not flung: no index.html at the root", {
2678
- hint: "Fling the folder that contains index.html (your build output, e.g. ./dist), or pass --list-html / --no-list-html."
2679
- });
2651
+ return "missing";
2680
2652
  }
2681
- }
2682
- async function confirmSiteName(ctx, client, generated, base) {
2683
- const where = (n) => base ? ` \u2192 ${siteUrlFor(n, base)}` : "";
2684
- const describe = (n, note) => ctx.log.info(`Site name: ${chalk5.bold(n)}${where(n)} ${chalk5.dim(`(${note})`)}`);
2685
- let candidate = generated;
2686
- for (; ; ) {
2687
- const check = await checkName(client, candidate);
2688
- let proposal;
2689
- if (check.status === "available" || check.status === "yours") {
2690
- proposal = candidate;
2691
- describe(candidate, check.status === "yours" ? "updates your existing site" : "new");
2692
- } else {
2693
- const why = check.status === "taken" ? "is taken" : "is not a valid site name";
2694
- const alternatives = check.suggestions.length ? ` Suggestions: ${check.suggestions.join(", ")}` : "";
2695
- ctx.log.warn(`${candidate} ${why}.${alternatives}`);
2696
- proposal = check.suggestions[0];
2697
- if (proposal) describe(proposal, "new");
2698
- }
2699
- for (; ; ) {
2700
- const question = proposal ? `Use "${proposal}"? [Y/n or type another name] ` : "Type a site name (or n to abort): ";
2701
- const answer = (await ctx.prompt(question)).trim();
2702
- if (proposal && (answer === "" || /^y(es)?$/i.test(answer))) return proposal;
2703
- if (/^no?$/i.test(answer))
2704
- throw new CliError("Aborted \u2014 nothing was flung", {
2705
- hint: "Pass --name <name> to choose the site name up front."
2706
- });
2707
- if (answer === "") continue;
2708
- if (!isValidSiteName(answer)) {
2709
- ctx.log.warn(
2710
- `"${answer}" is not a valid site name: use lowercase letters, digits and single hyphens (max ${SITE_NAME_MAX}); some names are reserved.`
2711
- );
2712
- continue;
2713
- }
2714
- candidate = answer;
2715
- break;
2716
- }
2653
+ const installed = "installed" in status && status.installed;
2654
+ const question = installed ? `The Fling plugin is installed in Claude Code${status.version ? ` (${status.version})` : ""}. Update it to the latest?` : "Install the Fling plugin (skill + MCP server) into Claude Code?";
2655
+ if (ask && !await ctx.confirm(question)) {
2656
+ ctx.log.info(chalk7.dim("Claude Code plugin: skipped"));
2657
+ return installed ? "already" : "skipped";
2658
+ }
2659
+ const s = fancy ? spinner({ output: process.stderr }) : null;
2660
+ s?.start(installed ? "Updating the Claude Code plugin" : "Installing the Claude Code plugin");
2661
+ try {
2662
+ if (installed) await updatePlugin(run, status.scope);
2663
+ else await installPlugin(run, scope);
2664
+ } catch (error) {
2665
+ const message = error instanceof Error ? error.message : String(error);
2666
+ if (s) s.error(message);
2667
+ else ctx.log.error(message);
2668
+ return "failed";
2669
+ }
2670
+ const done = installed ? `Claude Code plugin ${PLUGIN_NAME} is up to date` : `Claude Code plugin ${PLUGIN_NAME} installed ${chalk7.dim(`(${scope} scope)`)}`;
2671
+ if (s) s.stop(done);
2672
+ else ctx.log.success(done);
2673
+ ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick it up.");
2674
+ return installed ? "updated" : "done";
2675
+ }
2676
+ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
2677
+ const app = await (deps.findClaudeDesktop ?? findClaudeDesktop)();
2678
+ if (!app) return "missing";
2679
+ if (ask && !await ctx.confirm("Add the Fling extension to Claude Desktop too?")) {
2680
+ ctx.log.info(chalk7.dim("Claude Desktop extension: skipped"));
2681
+ return "skipped";
2682
+ }
2683
+ const s = fancy ? spinner({ output: process.stderr }) : null;
2684
+ s?.start("Downloading the Claude Desktop extension");
2685
+ try {
2686
+ const file = await (deps.downloadExtension ?? ((serverUrl) => downloadExtension(serverUrl, ctx.fetch)))(ctx.config.serverUrl);
2687
+ await ctx.open(file);
2688
+ const done = "Opened fling.mcpb in Claude Desktop \u2014 click Install there to finish";
2689
+ if (s) s.stop(done);
2690
+ else ctx.log.success(done);
2691
+ return "done";
2692
+ } catch (error) {
2693
+ const message = error instanceof Error ? error.message : String(error);
2694
+ if (s) s.error(message);
2695
+ else ctx.log.error(message);
2696
+ return "failed";
2717
2697
  }
2718
2698
  }
2719
2699
 
2720
2700
  // src/commands/sites.ts
2721
- import chalk6 from "chalk";
2701
+ import chalk8 from "chalk";
2722
2702
  async function runList(ctx, opts) {
2723
2703
  const sites = await listSites(requireClient(ctx), opts);
2724
2704
  ctx.out.data(
@@ -2736,11 +2716,11 @@ async function runList(ctx, opts) {
2736
2716
  );
2737
2717
  }
2738
2718
  function describeSite(site) {
2739
- const row = (label, value) => `${chalk6.bold(`${label}:`.padEnd(12))}${value}`;
2719
+ const row = (label, value) => `${chalk8.bold(`${label}:`.padEnd(12))}${value}`;
2740
2720
  return [
2741
2721
  row("Name", site.name),
2742
2722
  row("URL", site.url),
2743
- row("Visibility", site.visibility + (site.archived ? chalk6.yellow(" (archived)") : "")),
2723
+ row("Visibility", site.visibility + (site.archived ? chalk8.yellow(" (archived)") : "")),
2744
2724
  row("SPA", site.spa ? "yes" : "no"),
2745
2725
  row("Share link", site.shareUrl ?? "-"),
2746
2726
  row("Version", site.currentVersionId ?? "-"),
@@ -2755,118 +2735,408 @@ function filesRow(site, row) {
2755
2735
  const n = current.fileCount;
2756
2736
  return [row("Files", `${n} file${n === 1 ? "" : "s"}, ${formatBytes(current.totalBytes)}`)];
2757
2737
  }
2758
- async function runInfo(ctx, name) {
2759
- const site = await getSite(requireClient(ctx), name);
2760
- ctx.out.data(
2761
- site,
2762
- () => [
2763
- ...describeSite(site),
2764
- "",
2765
- chalk6.dim(
2766
- `${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
2767
- )
2768
- ].join("\n")
2769
- );
2738
+ async function runInfo(ctx, name) {
2739
+ const site = await getSite(requireClient(ctx), name);
2740
+ ctx.out.data(
2741
+ site,
2742
+ () => [
2743
+ ...describeSite(site),
2744
+ "",
2745
+ chalk8.dim(
2746
+ `${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
2747
+ )
2748
+ ].join("\n")
2749
+ );
2750
+ }
2751
+ async function runVersions(ctx, name) {
2752
+ const site = await getSite(requireClient(ctx), name);
2753
+ ctx.out.data(
2754
+ { name: site.name, currentVersionId: site.currentVersionId, versions: site.versions },
2755
+ () => renderTable(site.versions, [
2756
+ { header: "", value: (v) => v.id === site.currentVersionId ? "*" : " " },
2757
+ { header: "VERSION", value: (v) => v.id },
2758
+ { header: "FILES", value: (v) => v.fileCount },
2759
+ { header: "SIZE", value: (v) => formatBytes(v.totalBytes) },
2760
+ { header: "CREATED", value: (v) => formatDate(v.createdAt) }
2761
+ ])
2762
+ );
2763
+ }
2764
+ async function runFiles(ctx, name, versionId) {
2765
+ const result = await versionFiles(requireClient(ctx), name, versionId);
2766
+ if (!result.files.some((f) => f.path === "index.html")) {
2767
+ ctx.log.warn("No index.html at the root \u2014 the site root URL will 404");
2768
+ }
2769
+ ctx.out.data(result, () => {
2770
+ const n = result.files.length;
2771
+ const header = chalk8.dim(
2772
+ `${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
2773
+ );
2774
+ const table = renderTable(result.files, [
2775
+ { header: "PATH", value: (f) => f.path },
2776
+ { header: "SIZE", value: (f) => formatBytes(f.size) },
2777
+ { header: "TYPE", value: (f) => f.contentType }
2778
+ ]);
2779
+ return `${header}
2780
+ ${table}`;
2781
+ });
2782
+ }
2783
+ async function runRollback(ctx, name, versionId) {
2784
+ const site = await rollback(requireClient(ctx), name, versionId);
2785
+ ctx.log.success(`${name} now serves version ${site.currentVersionId ?? versionId}`);
2786
+ ctx.out.data(site, () => site.url);
2787
+ }
2788
+ async function runShare(ctx, name, opts) {
2789
+ if (opts.refresh && opts.revoke) throw new CliError("Use either --refresh or --revoke, not both");
2790
+ const client = requireClient(ctx);
2791
+ const result = await share(client, name, opts);
2792
+ let ready;
2793
+ if (result.action === "created" || result.action === "refreshed") {
2794
+ const files = ctx.waitReady ? await versionFiles(client, name).catch(() => null) : null;
2795
+ ready = (await waitWithSpinner(ctx.waitReady, result.shareUrl, files?.files.map((f) => f.path) ?? [], {
2796
+ progress: ctx.progress,
2797
+ warn: (m) => ctx.log.warn(m)
2798
+ })).ready;
2799
+ }
2800
+ if (result.action === "revoked") ctx.log.success(`Share link for ${name} revoked`);
2801
+ else if (result.action === "refreshed")
2802
+ ctx.log.success("New share link \u2014 the old one no longer works");
2803
+ else if (result.action === "created")
2804
+ ctx.log.success("Share link created \u2014 anyone with it can view the site");
2805
+ ctx.out.data(
2806
+ ready === void 0 ? result : { ...result, ready },
2807
+ () => result.shareUrl ? result.shareUrl : chalk8.dim("(no share link)")
2808
+ );
2809
+ }
2810
+ async function runVisibility(ctx, name, visibility) {
2811
+ if (visibility !== "private" && visibility !== "public") {
2812
+ throw new CliError(`Visibility must be "private" or "public", not "${visibility}"`);
2813
+ }
2814
+ const site = await setVisibility(requireClient(ctx), name, visibility);
2815
+ ctx.log.success(
2816
+ site.visibility === "public" ? `${name} is public \u2014 no sign-in needed` : `${name} is private \u2014 Nearform sign-in required`
2817
+ );
2818
+ ctx.out.data(site, () => site.url);
2819
+ }
2820
+ async function runArchive(ctx, name, unarchive = false) {
2821
+ const site = await archiveSite(requireClient(ctx), name, { unarchive });
2822
+ ctx.log.success(
2823
+ unarchive ? `${name} restored` : `${name} archived \u2014 it 404s until \`fling unarchive ${name}\``
2824
+ );
2825
+ if (ctx.json) ctx.out.data(site, () => "");
2826
+ else if (unarchive) ctx.out.text(site.url);
2827
+ }
2828
+ async function runRemove(ctx, name, opts) {
2829
+ if (!opts.yes) {
2830
+ if (!ctx.interactive) {
2831
+ throw new CliError(`Refusing to delete ${name} without confirmation`, {
2832
+ hint: "Pass --yes to delete."
2833
+ });
2834
+ }
2835
+ const ok = await ctx.confirm(`Delete ${name} and every version of it? This cannot be undone.`);
2836
+ if (!ok) throw new CliError("Cancelled");
2837
+ }
2838
+ const result = await deleteSite(requireClient(ctx), name);
2839
+ ctx.log.success(`${name} deleted`);
2840
+ if (ctx.json) ctx.out.data(result, () => "");
2841
+ }
2842
+
2843
+ // src/mcp.ts
2844
+ import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
2845
+ import { tmpdir as tmpdir3 } from "node:os";
2846
+ import { basename as basename3, dirname as dirname2, join as join6 } from "node:path";
2847
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2848
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2849
+ import { z as z7 } from "zod";
2850
+
2851
+ // src/locate.ts
2852
+ import { lstat, readdir as readdir2, readFile as readFile6, realpath as realpath3, stat as stat5 } from "node:fs/promises";
2853
+ import { posix, win32 } from "node:path";
2854
+ import { fileURLToPath } from "node:url";
2855
+ var DOWNLOADS_DIR_ENV = "FLING_DOWNLOADS_DIR";
2856
+ var FOUND_AS_GIVEN = "path";
2857
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "__MACOSX", "Library", "AppData", "$RECYCLE.BIN"]);
2858
+ var SESSION_SKIP_DIRS = /* @__PURE__ */ new Set(["skills-plugin", "rpm", "uploads", "memory"]);
2859
+ var SESSIONS_DEPTH = 4;
2860
+ var OUTPUTS_DEPTH = 4;
2861
+ var SANDBOX_PATH_RE = /^\/(mnt\/user-data|mnt\/data|sessions|home\/claude|tmp\/outputs)(\/|$)/;
2862
+ var nodeFs = {
2863
+ async stat(path) {
2864
+ try {
2865
+ const s = await stat5(path);
2866
+ return { isFile: s.isFile(), isDirectory: s.isDirectory(), mtimeMs: s.mtimeMs, size: s.size };
2867
+ } catch {
2868
+ return null;
2869
+ }
2870
+ },
2871
+ async readdir(path) {
2872
+ try {
2873
+ const entries = await readdir2(path, { withFileTypes: true });
2874
+ const out = [];
2875
+ for (const e of entries) {
2876
+ if (e.isSymbolicLink()) {
2877
+ const s = await stat5(`${path}/${e.name}`).catch(() => null);
2878
+ out.push({ name: e.name, isFile: Boolean(s?.isFile()), isDirectory: false });
2879
+ } else out.push({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() });
2880
+ }
2881
+ return out;
2882
+ } catch {
2883
+ return [];
2884
+ }
2885
+ },
2886
+ async realpath(path) {
2887
+ return realpath3(path).catch(() => null);
2888
+ },
2889
+ async readText(path) {
2890
+ try {
2891
+ if (!(await lstat(path)).isFile()) return null;
2892
+ return await readFile6(path, "utf8");
2893
+ } catch {
2894
+ return null;
2895
+ }
2896
+ }
2897
+ };
2898
+ var pathFor = (platform) => platform === "win32" ? win32 : posix;
2899
+ var usable = (value) => Boolean(value?.trim()) && !value?.trim().startsWith("${");
2900
+ async function xdgUserDir(key, opts, fs) {
2901
+ const fromEnv2 = opts.env[`XDG_${key}_DIR`];
2902
+ if (usable(fromEnv2)) return fromEnv2;
2903
+ const configHome = opts.env.XDG_CONFIG_HOME || posix.join(opts.home, ".config");
2904
+ const text2 = await fs.readText(posix.join(configHome, "user-dirs.dirs"));
2905
+ const match = text2?.match(new RegExp(`^XDG_${key}_DIR="([^"]*)"`, "m"));
2906
+ if (!match?.[1]) return void 0;
2907
+ return match[1].replace(/^\$HOME(?=\/|$)/, opts.home);
2908
+ }
2909
+ async function searchRoots(opts) {
2910
+ const fs = opts.fs ?? nodeFs;
2911
+ const p = pathFor(opts.platform);
2912
+ const { home, env, platform } = opts;
2913
+ const userHome = platform === "win32" && usable(env.USERPROFILE) ? env.USERPROFILE : home;
2914
+ const roots = [];
2915
+ const add = (label, path, depth, sessions = false) => {
2916
+ if (!path) return;
2917
+ const norm = p.resolve(path);
2918
+ if (roots.some((r) => r.path === norm)) return;
2919
+ roots.push({ label, path: norm, depth, ...sessions ? { sessions } : {} });
2920
+ };
2921
+ const cwd = p.resolve(opts.cwd);
2922
+ if (cwd !== p.parse(cwd).root && cwd !== p.resolve(userHome)) add("current folder", cwd, 4);
2923
+ const configured = usable(opts.downloadsDir) ? opts.downloadsDir : usable(env[DOWNLOADS_DIR_ENV]) ? env[DOWNLOADS_DIR_ENV] : void 0;
2924
+ const defaultDownloads = platform === "linux" ? await xdgUserDir("DOWNLOAD", opts, fs) ?? p.join(home, "Downloads") : p.join(userHome, "Downloads");
2925
+ add("Downloads", configured ? expandHome(configured, home, platform) : defaultDownloads, 3);
2926
+ add("Downloads", defaultDownloads, 3);
2927
+ const desktop = platform === "linux" ? await xdgUserDir("DESKTOP", opts, fs) ?? p.join(home, "Desktop") : p.join(userHome, "Desktop");
2928
+ add("Desktop", desktop, 3);
2929
+ if (platform === "win32" && usable(env.OneDrive))
2930
+ add("Desktop", p.join(env.OneDrive, "Desktop"), 3);
2931
+ const documents = platform === "linux" ? await xdgUserDir("DOCUMENTS", opts, fs) ?? p.join(home, "Documents") : p.join(userHome, "Documents");
2932
+ add("Documents/Claude", p.join(documents, "Claude"), 4);
2933
+ add("~/Claude", p.join(userHome, "Claude"), 4);
2934
+ const claudeConfig = platform === "darwin" ? p.join(home, "Library", "Application Support", "Claude") : platform === "win32" ? p.join(
2935
+ usable(env.APPDATA) ? env.APPDATA : p.join(userHome, "AppData", "Roaming"),
2936
+ "Claude"
2937
+ ) : p.join(env.XDG_CONFIG_HOME || p.join(home, ".config"), "Claude");
2938
+ add(
2939
+ "Claude Desktop outputs",
2940
+ p.join(claudeConfig, "local-agent-mode-sessions"),
2941
+ SESSIONS_DEPTH,
2942
+ true
2943
+ );
2944
+ if (!fs.realpath) return roots;
2945
+ const real = [];
2946
+ for (const root of roots) {
2947
+ const path = await fs.realpath(root.path) ?? root.path;
2948
+ if (!real.some((r) => r.path === path)) real.push({ ...root, path });
2949
+ }
2950
+ return real;
2951
+ }
2952
+ function expandHome(input, home, platform) {
2953
+ if (input === "~") return home;
2954
+ if (input.startsWith("~/") || platform === "win32" && input.startsWith("~\\"))
2955
+ return pathFor(platform).join(home, input.slice(2));
2956
+ return input;
2957
+ }
2958
+ async function walk(roots, opts, visit) {
2959
+ const fs = opts.fs ?? nodeFs;
2960
+ const p = pathFor(opts.platform);
2961
+ const now = opts.now ?? Date.now;
2962
+ const deadline = now() + (opts.timeBudgetMs ?? 4e3);
2963
+ let budget = opts.entryBudget ?? 6e4;
2964
+ const skip = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
2965
+ const expanded = [];
2966
+ for (const root of roots) {
2967
+ if (!root.sessions) {
2968
+ expanded.push(root);
2969
+ continue;
2970
+ }
2971
+ let level = [root.path];
2972
+ for (let depth = 0; depth < root.depth && level.length; depth++) {
2973
+ const next = [];
2974
+ for (const dir of level) {
2975
+ for (const e of await fs.readdir(dir)) {
2976
+ if (!e.isDirectory || skip(e.name) || SESSION_SKIP_DIRS.has(e.name)) continue;
2977
+ const child = p.join(dir, e.name);
2978
+ if (e.name === "outputs")
2979
+ expanded.push({ label: root.label, path: child, depth: OUTPUTS_DEPTH });
2980
+ else next.push(child);
2981
+ }
2982
+ }
2983
+ level = next;
2984
+ }
2985
+ }
2986
+ for (const root of expanded) {
2987
+ let level = [root.path];
2988
+ for (let depth = 0; depth <= root.depth && level.length; depth++) {
2989
+ const next = [];
2990
+ for (const dir of level) {
2991
+ if (now() > deadline || budget <= 0) return { complete: false };
2992
+ const entries = await fs.readdir(dir);
2993
+ budget -= entries.length;
2994
+ for (const entry of entries) {
2995
+ if (entry.name.startsWith(".")) continue;
2996
+ const path = p.join(dir, entry.name);
2997
+ const result = await visit({ root, path, entry, siblings: entries });
2998
+ if (entry.isDirectory && !skip(entry.name) && result !== "skip" && depth < root.depth)
2999
+ next.push(path);
3000
+ }
3001
+ }
3002
+ level = next;
3003
+ }
3004
+ }
3005
+ return { complete: true };
3006
+ }
3007
+ function withoutDuplicateSuffix(name) {
3008
+ return name.replace(/ \(\d+\)(?=\.[^.]+$|$)/, "");
2770
3009
  }
2771
- async function runVersions(ctx, name) {
2772
- const site = await getSite(requireClient(ctx), name);
2773
- ctx.out.data(
2774
- { name: site.name, currentVersionId: site.currentVersionId, versions: site.versions },
2775
- () => renderTable(site.versions, [
2776
- { header: "", value: (v) => v.id === site.currentVersionId ? "*" : " " },
2777
- { header: "VERSION", value: (v) => v.id },
2778
- { header: "FILES", value: (v) => v.fileCount },
2779
- { header: "SIZE", value: (v) => formatBytes(v.totalBytes) },
2780
- { header: "CREATED", value: (v) => formatDate(v.createdAt) }
2781
- ])
2782
- );
3010
+ function namesMatch(candidate, wanted) {
3011
+ const a = candidate.toLowerCase();
3012
+ const b = wanted.toLowerCase();
3013
+ return a === b || withoutDuplicateSuffix(a) === withoutDuplicateSuffix(b);
2783
3014
  }
2784
- async function runFiles(ctx, name, versionId) {
2785
- const result = await versionFiles(requireClient(ctx), name, versionId);
2786
- if (!result.files.some((f) => f.path === "index.html")) {
2787
- ctx.log.warn("No index.html at the root \u2014 the site root URL will 404");
3015
+ var splitSegments = (path) => path.split(/[\\/]+/).filter((s) => s !== "" && s !== "." && s !== "~");
3016
+ function suffixScore(path, wanted) {
3017
+ const have = splitSegments(path);
3018
+ let score = 1;
3019
+ for (let i = 2; i <= Math.min(have.length, wanted.length); i++) {
3020
+ if (have[have.length - i]?.toLowerCase() !== wanted[wanted.length - i]?.toLowerCase()) break;
3021
+ score++;
2788
3022
  }
2789
- ctx.out.data(result, () => {
2790
- const n = result.files.length;
2791
- const header = chalk6.dim(
2792
- `${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
2793
- );
2794
- const table = renderTable(result.files, [
2795
- { header: "PATH", value: (f) => f.path },
2796
- { header: "SIZE", value: (f) => formatBytes(f.size) },
2797
- { header: "TYPE", value: (f) => f.contentType }
2798
- ]);
2799
- return `${header}
2800
- ${table}`;
2801
- });
2802
- }
2803
- async function runRollback(ctx, name, versionId) {
2804
- const site = await rollback(requireClient(ctx), name, versionId);
2805
- ctx.log.success(`${name} now serves version ${site.currentVersionId ?? versionId}`);
2806
- ctx.out.data(site, () => site.url);
3023
+ return score;
2807
3024
  }
2808
- async function runShare(ctx, name, opts) {
2809
- if (opts.refresh && opts.revoke) throw new CliError("Use either --refresh or --revoke, not both");
2810
- const client = requireClient(ctx);
2811
- const result = await share(client, name, opts);
2812
- let ready;
2813
- if (result.action === "created" || result.action === "refreshed") {
2814
- const files = ctx.waitReady ? await versionFiles(client, name).catch(() => null) : null;
2815
- ready = (await waitWithSpinner(ctx.waitReady, result.shareUrl, files?.files.map((f) => f.path) ?? [], {
2816
- progress: ctx.progress,
2817
- warn: (m) => ctx.log.warn(m)
2818
- })).ready;
3025
+ var looksLikeSandboxPath = (input) => SANDBOX_PATH_RE.test(input.replace(/\\/g, "/"));
3026
+ function normaliseInput(raw, opts) {
3027
+ let input = raw.trim().replace(/^(["'])(.*)\1$/, "$2");
3028
+ if (input.startsWith("file://")) {
3029
+ try {
3030
+ input = fileURLToPath(input, { windows: opts.platform === "win32" });
3031
+ } catch {
3032
+ input = decodeURIComponent(input.slice("file://".length));
3033
+ }
2819
3034
  }
2820
- if (result.action === "revoked") ctx.log.success(`Share link for ${name} revoked`);
2821
- else if (result.action === "refreshed")
2822
- ctx.log.success("New share link \u2014 the old one no longer works");
2823
- else if (result.action === "created")
2824
- ctx.log.success("Share link created \u2014 anyone with it can view the site");
2825
- ctx.out.data(
2826
- ready === void 0 ? result : { ...result, ready },
2827
- () => result.shareUrl ? result.shareUrl : chalk6.dim("(no share link)")
2828
- );
3035
+ return expandHome(input, opts.home, opts.platform);
2829
3036
  }
2830
- async function runVisibility(ctx, name, visibility) {
2831
- if (visibility !== "private" && visibility !== "public") {
2832
- throw new CliError(`Visibility must be "private" or "public", not "${visibility}"`);
3037
+ async function resolveInput(input, opts) {
3038
+ const fs = opts.fs ?? nodeFs;
3039
+ const p = pathFor(opts.platform);
3040
+ const given = normaliseInput(input, opts);
3041
+ if (!given) throw new CliError("No path given");
3042
+ const direct = p.isAbsolute(given) ? p.normalize(given) : p.resolve(opts.cwd, given);
3043
+ const directStat = await fs.stat(direct);
3044
+ if (directStat && (directStat.isFile || directStat.isDirectory)) {
3045
+ return {
3046
+ path: direct,
3047
+ foundIn: FOUND_AS_GIVEN,
3048
+ isDirectory: directStat.isDirectory,
3049
+ alternatives: []
3050
+ };
2833
3051
  }
2834
- const site = await setVisibility(requireClient(ctx), name, visibility);
2835
- ctx.log.success(
2836
- site.visibility === "public" ? `${name} is public \u2014 no sign-in needed` : `${name} is private \u2014 Nearform sign-in required`
2837
- );
2838
- ctx.out.data(site, () => site.url);
3052
+ const wanted = splitSegments(given);
3053
+ const name = wanted.at(-1);
3054
+ const roots = await searchRoots(opts);
3055
+ if (!name) throw notFound(input, roots);
3056
+ const found = [];
3057
+ const seen = /* @__PURE__ */ new Set();
3058
+ await walk(roots, opts, async ({ root, path, entry }) => {
3059
+ if (!(entry.isFile || entry.isDirectory) || !namesMatch(entry.name, name)) return;
3060
+ if (seen.has(path)) return;
3061
+ seen.add(path);
3062
+ const s = await fs.stat(path);
3063
+ if (!s) return;
3064
+ found.push({
3065
+ path,
3066
+ foundIn: root.label,
3067
+ kind: s.isDirectory ? "folder" : "file",
3068
+ modified: new Date(s.mtimeMs).toISOString(),
3069
+ size: s.size,
3070
+ score: suffixScore(path, wanted),
3071
+ mtimeMs: s.mtimeMs,
3072
+ isDirectory: s.isDirectory
3073
+ });
3074
+ return void 0;
3075
+ });
3076
+ if (found.length === 0) throw notFound(input, roots);
3077
+ found.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs);
3078
+ const strip = ({ score: _s, mtimeMs: _m, isDirectory: _d, ...c }) => c;
3079
+ const [best, ...rest] = found;
3080
+ return {
3081
+ path: best.path,
3082
+ foundIn: best.foundIn,
3083
+ isDirectory: best.isDirectory,
3084
+ alternatives: rest.slice(0, 5).map(strip)
3085
+ };
2839
3086
  }
2840
- async function runArchive(ctx, name, unarchive = false) {
2841
- const site = await archiveSite(requireClient(ctx), name, { unarchive });
2842
- ctx.log.success(
2843
- unarchive ? `${name} restored` : `${name} archived \u2014 it 404s until \`fling unarchive ${name}\``
2844
- );
2845
- if (ctx.json) ctx.out.data(site, () => "");
2846
- else if (unarchive) ctx.out.text(site.url);
3087
+ function notFound(input, roots) {
3088
+ const where = roots.map((r) => `${r.label} (${r.path})`).join(", ");
3089
+ const sandbox = looksLikeSandboxPath(input) ? " That looks like a path inside Claude\u2019s sandbox, which this computer cannot see: publish the content inline with fling_publish, or save/download the file to this computer first." : "";
3090
+ return new CliError(`Could not find "${input}". Looked for it by name in: ${where}.${sandbox}`, {
3091
+ hint: "Pass the full path, or save the file to your Downloads folder and use just its name."
3092
+ });
2847
3093
  }
2848
- async function runRemove(ctx, name, opts) {
2849
- if (!opts.yes) {
2850
- if (!ctx.interactive) {
2851
- throw new CliError(`Refusing to delete ${name} without confirmation`, {
2852
- hint: "Pass --yes to delete."
2853
- });
3094
+ var LISTED_EXT = /\.(html?|zip)$/i;
3095
+ async function findFiles(opts, find = {}) {
3096
+ const fs = opts.fs ?? nodeFs;
3097
+ const p = pathFor(opts.platform);
3098
+ const now = (opts.now ?? Date.now)();
3099
+ const since = now - (find.days ?? 14) * 24 * 60 * 60 * 1e3;
3100
+ const query = find.query?.trim().toLowerCase();
3101
+ const roots = await searchRoots(opts);
3102
+ const found = /* @__PURE__ */ new Map();
3103
+ const rootPaths = new Set(roots.map((r) => r.path));
3104
+ const { complete } = await walk(roots, opts, async ({ root, path, entry }) => {
3105
+ if (entry.isDirectory) {
3106
+ if (rootPaths.has(path)) return;
3107
+ const index = (await fs.readdir(path)).find((e) => e.isFile && /^index\.html?$/i.test(e.name));
3108
+ if (!index) return;
3109
+ const s2 = await fs.stat(p.join(path, index.name));
3110
+ if (s2 && s2.mtimeMs >= since && (!query || entry.name.toLowerCase().includes(query)))
3111
+ found.set(path, {
3112
+ path,
3113
+ foundIn: root.label,
3114
+ kind: "folder",
3115
+ modified: new Date(s2.mtimeMs).toISOString(),
3116
+ size: s2.size,
3117
+ mtimeMs: s2.mtimeMs
3118
+ });
3119
+ return "skip";
2854
3120
  }
2855
- const ok = await ctx.confirm(`Delete ${name} and every version of it? This cannot be undone.`);
2856
- if (!ok) throw new CliError("Cancelled");
2857
- }
2858
- const result = await deleteSite(requireClient(ctx), name);
2859
- ctx.log.success(`${name} deleted`);
2860
- if (ctx.json) ctx.out.data(result, () => "");
3121
+ if (!entry.isFile || !LISTED_EXT.test(entry.name)) return;
3122
+ if (query && !entry.name.toLowerCase().includes(query)) return;
3123
+ const s = await fs.stat(path);
3124
+ if (!s || s.mtimeMs < since || found.has(path)) return;
3125
+ found.set(path, {
3126
+ path,
3127
+ foundIn: root.label,
3128
+ kind: "file",
3129
+ modified: new Date(s.mtimeMs).toISOString(),
3130
+ size: s.size,
3131
+ mtimeMs: s.mtimeMs
3132
+ });
3133
+ return void 0;
3134
+ });
3135
+ const files = [...found.values()].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, find.limit ?? 20).map(({ mtimeMs: _m, ...c }) => c);
3136
+ return { files, searched: roots, complete };
2861
3137
  }
2862
3138
 
2863
3139
  // src/mcp.ts
2864
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
2865
- import { tmpdir as tmpdir2 } from "node:os";
2866
- import { basename as basename3, dirname as dirname2, join as join5 } from "node:path";
2867
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2868
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2869
- import { z as z7 } from "zod";
2870
3140
  var LOGIN_HINT = `Not logged in to Fling. Call fling_login (it opens the browser to sign in with Nearform Google), or ask the user to run \`${NPX_COMMAND} login\` in a terminal (in Claude Code: \`! ${NPX_COMMAND} login\`), then try again.`;
2871
3141
  var MCP_LOCAL_INSTRUCTIONS = [
2872
3142
  "Fling publishes static sites (HTML/CSS/JS, images, documents) for Nearform staff at https://<name>.fling.labs.nearform.com. This server runs on the user's computer.",
@@ -3080,7 +3350,7 @@ ${JSON.stringify(data, null, 2)}` }]
3080
3350
  },
3081
3351
  (args) => run(async (c) => {
3082
3352
  const name = resolveSiteName(".", args.name);
3083
- const tmp = await mkdtemp2(join5(tmpdir2(), "fling-publish-"));
3353
+ const tmp = await mkdtemp3(join6(tmpdir3(), "fling-publish-"));
3084
3354
  try {
3085
3355
  const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
3086
3356
  const outcome = await deployFolder(c, tmp, {
@@ -3117,20 +3387,20 @@ ${JSON.stringify(data, null, 2)}` }]
3117
3387
  source = await resolveInput(args.path, locate);
3118
3388
  name = defaultNameFor(source.path, source.isDirectory);
3119
3389
  }
3120
- const check = await checkName(c, name);
3121
- const base = check.status === "invalid" ? null : await discoverSitesBase(c);
3390
+ const check2 = await checkName(c, name);
3391
+ const base = check2.status === "invalid" ? null : await discoverSitesBase(c);
3122
3392
  const url = base ? siteUrlFor(name, base) : null;
3123
- const suggestions = check.suggestions.length ? ` Suggestions: ${check.suggestions.join(", ")}.` : "";
3393
+ const suggestions = check2.suggestions.length ? ` Suggestions: ${check2.suggestions.join(", ")}.` : "";
3124
3394
  const summary = {
3125
3395
  available: `"${name}" is available${url ? ` \u2192 ${url}` : ""}.`,
3126
3396
  yours: `"${name}" is the user's existing site${url ? ` (${url})` : ""} \u2014 deploying updates it.`,
3127
3397
  taken: `"${name}" is taken by someone else.${suggestions}`,
3128
3398
  invalid: `"${name}" is not a valid site name.${suggestions}`
3129
- }[check.status];
3399
+ }[check2.status];
3130
3400
  return {
3131
3401
  summary,
3132
3402
  data: {
3133
- ...check,
3403
+ ...check2,
3134
3404
  url,
3135
3405
  ...source ? { generated: true, source: { path: source.path, foundIn: source.foundIn } } : {}
3136
3406
  }
@@ -3298,7 +3568,7 @@ ${JSON.stringify(data, null, 2)}` }]
3298
3568
  );
3299
3569
  return server;
3300
3570
  }
3301
- var textResult = (text) => ({ content: [{ type: "text", text }] });
3571
+ var textResult = (text2) => ({ content: [{ type: "text", text: text2 }] });
3302
3572
  async function writeInlineFiles(dir, files) {
3303
3573
  const seen = /* @__PURE__ */ new Set();
3304
3574
  const skippedDotfiles = [];
@@ -3322,9 +3592,9 @@ async function writeInlineFiles(dir, files) {
3322
3592
  total += bytes.length;
3323
3593
  if (total > MAX_DEPLOY_BYTES)
3324
3594
  throw new CliError(`The site is larger than ${formatBytes(MAX_DEPLOY_BYTES)}`);
3325
- const target = join5(dir, ...path.split("/"));
3595
+ const target = join6(dir, ...path.split("/"));
3326
3596
  await mkdir3(dirname2(target), { recursive: true });
3327
- await writeFile3(target, bytes);
3597
+ await writeFile4(target, bytes);
3328
3598
  }
3329
3599
  if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
3330
3600
  return { skippedDotfiles };
@@ -3342,24 +3612,83 @@ async function runMcpServer(deps = {}) {
3342
3612
  await server.connect(new StdioServerTransport());
3343
3613
  }
3344
3614
 
3615
+ // src/route.ts
3616
+ import { stat as stat6 } from "node:fs/promises";
3617
+ import { resolve as resolve4 } from "node:path";
3618
+ var PASS_THROUGH = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);
3619
+ var pathExists = (path) => stat6(resolve4(path)).then(
3620
+ () => true,
3621
+ () => false
3622
+ );
3623
+ async function routeArgs(args, opts) {
3624
+ const exists = opts.exists ?? pathExists;
3625
+ let first;
3626
+ for (let i = 0; i < args.length; i++) {
3627
+ const arg = args[i];
3628
+ if (arg === "--") {
3629
+ first = args[i + 1];
3630
+ break;
3631
+ }
3632
+ if (arg.startsWith("-")) {
3633
+ if (PASS_THROUGH.has(arg)) return { kind: "run", args: [...args] };
3634
+ if (opts.valueFlags.has(arg)) i++;
3635
+ continue;
3636
+ }
3637
+ first = arg;
3638
+ break;
3639
+ }
3640
+ if (first === void 0) return { kind: "help" };
3641
+ if (await exists(first)) return { kind: "run", args: ["deploy", ...args] };
3642
+ if (opts.commands.has(first)) return { kind: "run", args: [...args] };
3643
+ return { kind: "unknown", arg: first };
3644
+ }
3645
+ function routeOptionsFor(program2) {
3646
+ const commands = /* @__PURE__ */ new Set(["help"]);
3647
+ const valueFlags = /* @__PURE__ */ new Set();
3648
+ const addFlags = (command) => {
3649
+ for (const option of command.options)
3650
+ if (option.required || option.optional) {
3651
+ for (const flag of [option.short, option.long]) if (flag) valueFlags.add(flag);
3652
+ }
3653
+ };
3654
+ addFlags(program2);
3655
+ for (const command of program2.commands) {
3656
+ commands.add(command.name());
3657
+ for (const alias of command.aliases()) commands.add(alias);
3658
+ addFlags(command);
3659
+ }
3660
+ return { commands, valueFlags };
3661
+ }
3662
+
3345
3663
  // src/cli.ts
3346
3664
  var program = new Command();
3347
3665
  program.name(BIN_NAME).description("Fling a folder of static files at Fling and get a link back").version(VERSION).option(
3348
3666
  "--server <url>",
3349
3667
  `server URL (default: $${ENV.url} or config, else ${DEFAULT_SERVER_URL})`
3350
- ).option("--json", "print JSON instead of human output", false).showHelpAfterError().addHelpText(
3668
+ ).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
3669
+ sortSubcommands: false,
3670
+ styleTitle: (title) => chalk9.bold(brand.primary(title)),
3671
+ styleCommandText: (text2) => chalk9.bold(text2),
3672
+ styleSubcommandText: (text2) => brand.accent(text2),
3673
+ styleOptionText: (text2) => brand.accent(text2),
3674
+ styleArgumentText: (text2) => chalk9.dim(text2)
3675
+ }).addHelpText(
3676
+ "before",
3677
+ () => process.stdout.isTTY ? logo({ columns: process.stdout.columns }) : ""
3678
+ ).addHelpText(
3351
3679
  "after",
3352
- `
3353
- Examples:
3354
- $ ${BIN_NAME} login
3355
- $ ${BIN_NAME} ./dist --name my-app deploy \u2192 https://my-app.fling.labs.nearform.com
3356
- $ ${BIN_NAME} ./dist --yes no prompts (CI): use the generated name, fail if it is taken
3357
- $ ${BIN_NAME} "Pitch Deck.html" a single page; found in Downloads if it is not here
3358
- $ ${BIN_NAME} site.zip a zip of a site
3359
- $ ${BIN_NAME} share my-app public link for people outside Nearform
3680
+ () => `
3681
+ ${chalk9.bold(brand.primary("Examples:"))}
3682
+ $ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
3683
+ $ ${BIN_NAME} deploy ./dist --name my-app \u2192 https://my-app.fling.labs.nearform.com
3684
+ $ ${BIN_NAME} ./dist the same: an existing path deploys
3685
+ $ ${BIN_NAME} ./dist --yes no prompts (CI): use the generated name, fail if it is taken
3686
+ $ ${BIN_NAME} "Pitch Deck.html" a single page
3687
+ $ ${BIN_NAME} site.zip a zip of a site
3688
+ $ ${BIN_NAME} share my-app public link for people outside Nearform
3360
3689
 
3361
- Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
3362
- Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`
3690
+ ${chalk9.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
3691
+ Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`)}`
3363
3692
  );
3364
3693
  function action(handler) {
3365
3694
  return async (...args) => {
@@ -3397,8 +3726,8 @@ function report(error, ctx) {
3397
3726
  );
3398
3727
  }
3399
3728
  }
3400
- program.command("deploy [path]", { isDefault: true }).description(
3401
- "deploy a folder (default: .), a single .html file, a .zip or any file \u2014 the default command, so `fling ./dist` works. A path that does not exist is looked up by name in Downloads, Desktop and Claude output folders"
3729
+ program.command("deploy [path]").helpGroup("Deploy:").description(
3730
+ "deploy a folder (default: .), a single .html file, a .zip or any file. `fling <path>` is short for it"
3402
3731
  ).option(
3403
3732
  "-n, --name <name>",
3404
3733
  "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)"
@@ -3410,28 +3739,54 @@ program.command("deploy [path]", { isDefault: true }).description(
3410
3739
  (ctx, path, opts) => runDeploy(ctx, path, opts)
3411
3740
  )
3412
3741
  );
3413
- program.command("login").description("sign in through the browser and store an API key").action(action((ctx) => runLogin(ctx)));
3414
- program.command("logout").description("remove the stored API key").action(action((ctx) => runLogout(ctx)));
3415
- program.command("whoami").description("show the signed-in user").action(action((ctx) => runWhoami(ctx)));
3416
- program.command("ls").alias("list").description("list your sites").option("--archived", "list archived sites").action(action((ctx, opts) => runList(ctx, opts)));
3417
- program.command("info <name>").description("show a site").action(action((ctx, name) => runInfo(ctx, name)));
3418
- program.command("versions <name>").description("list a site's versions (* = live)").action(action((ctx, name) => runVersions(ctx, name)));
3419
- program.command("files <name> [versionId]").description("list the files in a site's live version (or in versionId)").action(
3742
+ program.command("setup").helpGroup("Setup:").description(
3743
+ "sign in, then add the Fling plugin to Claude Code and the extension to Claude Desktop"
3744
+ ).option("-y, --yes", "do every step without asking").option("-s, --scope <scope>", "Claude Code plugin scope: user, project or local", "user").action(action((ctx, opts) => runSetup(ctx, opts)));
3745
+ program.command("login").helpGroup("Setup:").description("sign in through the browser and store an API key").action(
3746
+ action(async (ctx) => {
3747
+ await runLogin(ctx);
3748
+ ctx.log.hint(`Next: \`${BIN_NAME} setup\` adds Fling to Claude Code and Claude Desktop.`);
3749
+ })
3750
+ );
3751
+ program.command("logout").helpGroup("Setup:").description("remove the stored API key").action(action((ctx) => runLogout(ctx)));
3752
+ program.command("whoami").helpGroup("Setup:").description("show the signed-in user").action(action((ctx) => runWhoami(ctx)));
3753
+ program.commandsGroup("Setup:").helpCommand("help [command]", "show help for a command");
3754
+ program.command("ls").helpGroup("Sites:").alias("list").description("list your sites").option("--archived", "list archived sites").action(action((ctx, opts) => runList(ctx, opts)));
3755
+ program.command("info <name>").helpGroup("Sites:").description("show a site").action(action((ctx, name) => runInfo(ctx, name)));
3756
+ program.command("versions <name>").helpGroup("Sites:").description("list a site's versions (* = live)").action(action((ctx, name) => runVersions(ctx, name)));
3757
+ program.command("files <name> [versionId]").helpGroup("Sites:").description("list the files in a site's live version (or in versionId)").action(
3420
3758
  action((ctx, name, versionId) => runFiles(ctx, name, versionId))
3421
3759
  );
3422
- program.command("rollback <name> <versionId>").description("serve an earlier version again").action(action((ctx, name, versionId) => runRollback(ctx, name, versionId)));
3423
- program.command("share <name>").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(
3760
+ program.command("rollback <name> <versionId>").helpGroup("Sites:").description("serve an earlier version again").action(action((ctx, name, versionId) => runRollback(ctx, name, versionId)));
3761
+ 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(
3424
3762
  action(
3425
3763
  (ctx, name, opts) => runShare(ctx, name, opts)
3426
3764
  )
3427
3765
  );
3428
- program.command("visibility <name> <visibility>").description("private (Nearform sign-in) or public (anyone)").action(action((ctx, name, visibility) => runVisibility(ctx, name, visibility)));
3429
- program.command("archive <name>").description("take a site offline (404) but keep its files; undo with unarchive").action(action((ctx, name) => runArchive(ctx, name)));
3430
- program.command("unarchive <name>").description("bring an archived site back").action(action((ctx, name) => runArchive(ctx, name, true)));
3431
- program.command("rm <name>").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)));
3432
- program.command("mcp").description("run the MCP server on stdio (used by the Claude Code plugin)").action(async (_opts, command) => {
3766
+ program.command("visibility <name> <visibility>").helpGroup("Sites:").description("private (Nearform sign-in) or public (anyone)").action(action((ctx, name, visibility) => runVisibility(ctx, name, visibility)));
3767
+ 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)));
3768
+ program.command("unarchive <name>").helpGroup("Sites:").description("bring an archived site back").action(action((ctx, name) => runArchive(ctx, name, true)));
3769
+ 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)));
3770
+ program.command("mcp").helpGroup("Agents:").description("run the MCP server on stdio (used by the Claude Code plugin)").action(async (_opts, command) => {
3433
3771
  const globals = command.optsWithGlobals();
3434
3772
  await runMcpServer({ server: globals.server });
3435
3773
  });
3436
- program.parseAsync(process.argv);
3774
+ async function main() {
3775
+ const route = await routeArgs(process.argv.slice(2), routeOptionsFor(program));
3776
+ if (route.kind === "help") {
3777
+ program.outputHelp();
3778
+ return;
3779
+ }
3780
+ if (route.kind === "unknown") {
3781
+ const log = createLogger();
3782
+ log.error(`"${route.arg}" is not a command, file or folder`);
3783
+ log.hint(
3784
+ `Run \`${BIN_NAME} --help\` for the commands, or \`${BIN_NAME} deploy <path>\` to deploy.`
3785
+ );
3786
+ process.exitCode = 1;
3787
+ return;
3788
+ }
3789
+ await program.parseAsync(route.args, { from: "user" });
3790
+ }
3791
+ await main();
3437
3792
  //# sourceMappingURL=cli.js.map