@nearform/fling 0.8.0 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +64 -5
  2. package/dist/cli.js +1023 -175
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2743,6 +2743,7 @@ function siteUrl(label, sitesDomain, protocol = "https:") {
2743
2743
  // ../../packages/shared/src/instance.ts
2744
2744
  import { z as z11 } from "zod";
2745
2745
  var INSTANCE_METADATA_PATH = "/.well-known/fling";
2746
+ var RELEASE_HEADER = "Fling-Release";
2746
2747
  var HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
2747
2748
  var color = z11.string().regex(HEX_COLOR_RE);
2748
2749
  var SignInProviderSchema = z11.looseObject({
@@ -3294,7 +3295,7 @@ function mcpToolsFor(w) {
3294
3295
  const isPublic = z14.boolean().optional().describe(
3295
3296
  "Prefer `access` (a new site) or fling_set_access (an existing one). true \u2192 anyone can view; false \u2192 private. Omit to keep the current setting"
3296
3297
  );
3297
- const access2 = z14.enum(ACCESS_LEVELS).optional().describe(
3298
+ const access3 = z14.enum(ACCESS_LEVELS).optional().describe(
3298
3299
  `Who can open a new site: only_me, members (${w.privateAudience}), groups, people or public. Ask the user before the first publish; omit for the default (${defaultAccessPhrase(w)}). Ignored when the site already exists`
3299
3300
  );
3300
3301
  const accessEmails = emails.optional().describe('With access "people" (or "groups"): the addresses that may open it');
@@ -3376,7 +3377,7 @@ function mcpToolsFor(w) {
3376
3377
  base,
3377
3378
  remove,
3378
3379
  spa,
3379
- access: access2,
3380
+ access: access3,
3380
3381
  emails: accessEmails,
3381
3382
  groups: accessGroups,
3382
3383
  public: isPublic,
@@ -3397,7 +3398,7 @@ function mcpToolsFor(w) {
3397
3398
  base,
3398
3399
  remove,
3399
3400
  spa,
3400
- access: access2,
3401
+ access: access3,
3401
3402
  emails: accessEmails,
3402
3403
  groups: accessGroups,
3403
3404
  public: isPublic
@@ -3921,7 +3922,7 @@ var LiveServerMessageSchema = z16.discriminatedUnion("type", [
3921
3922
  ]);
3922
3923
 
3923
3924
  // src/cli.ts
3924
- import chalk14 from "chalk";
3925
+ import chalk15 from "chalk";
3925
3926
  import { Command, Option } from "commander";
3926
3927
 
3927
3928
  // src/commands/auth.ts
@@ -3971,7 +3972,7 @@ function exitCodeFor(error) {
3971
3972
  // package.json
3972
3973
  var package_default = {
3973
3974
  name: "@nearform/fling",
3974
- version: "0.8.0",
3975
+ version: "0.9.0",
3975
3976
  license: "MIT",
3976
3977
  description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
3977
3978
  type: "module",
@@ -4025,6 +4026,7 @@ var package_default = {
4025
4026
  // src/package-info.ts
4026
4027
  var PACKAGE_NAME = package_default.name;
4027
4028
  var VERSION = package_default.version;
4029
+ var NODE_ENGINE = package_default.engines.node;
4028
4030
  var BIN_NAME = Object.keys(package_default.bin)[0] ?? "fling";
4029
4031
  var NPX_COMMAND = `npx ${package_default.name}`;
4030
4032
 
@@ -4066,6 +4068,14 @@ function createApiClient(options) {
4066
4068
  const serverUrl = options.serverUrl.replace(/\/+$/, "");
4067
4069
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
4068
4070
  const timeoutMs = options.timeoutMs ?? 6e4;
4071
+ let knownRelease = options.release;
4072
+ async function noteRelease(response) {
4073
+ const release = response.headers?.get?.(RELEASE_HEADER)?.trim();
4074
+ if (!release || release.length > 64 || release === knownRelease) return;
4075
+ knownRelease = release;
4076
+ await options.onRelease?.(release).catch(() => {
4077
+ });
4078
+ }
4069
4079
  async function request(method, path3, reqOptions = {}) {
4070
4080
  const url = buildUrl(serverUrl, path3, reqOptions.query);
4071
4081
  const headers = {
@@ -4091,6 +4101,7 @@ function createApiClient(options) {
4091
4101
  cause
4092
4102
  });
4093
4103
  }
4104
+ await noteRelease(response);
4094
4105
  const raw = await readBody(response);
4095
4106
  if (!response.ok) throw errorFromResponse(response.status, raw, method, path3);
4096
4107
  if (!reqOptions.schema) return raw;
@@ -4184,7 +4195,7 @@ function describeCause(cause) {
4184
4195
  // src/config.ts
4185
4196
  import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
4186
4197
  import { homedir } from "node:os";
4187
- import { join } from "node:path";
4198
+ import { delimiter, join } from "node:path";
4188
4199
  import { z as z17 } from "zod";
4189
4200
  var CONFIG_DIR_NAME = ".fling";
4190
4201
  var CONFIG_FILE_NAME = "config.json";
@@ -4196,7 +4207,11 @@ var ENV = {
4196
4207
  /** Where to look for files given by name (the .mcpb extension sets it from its settings). */
4197
4208
  downloadsDir: "FLING_DOWNLOADS_DIR",
4198
4209
  /** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
4199
- addServer: "FLING_ADD_SERVER"
4210
+ addServer: "FLING_ADD_SERVER",
4211
+ /** Set (to anything) to turn off the line saying a newer Fling release is out (update-check.ts). */
4212
+ noUpdateCheck: "FLING_NO_UPDATE_CHECK",
4213
+ /** Set by CI systems; the update check stays quiet there too. */
4214
+ ci: "CI"
4200
4215
  };
4201
4216
  var serverEntrySchema = z17.object({
4202
4217
  /** What the server said about itself at login. A copy this CLI can't read is just refetched. */
@@ -4218,6 +4233,19 @@ function fromEnv(value) {
4218
4233
  function serverToAdd(env = process.env) {
4219
4234
  return fromEnv(env[ENV.addServer]);
4220
4235
  }
4236
+ function updateCheckSilenced(env = process.env) {
4237
+ return Boolean(fromEnv(env[ENV.noUpdateCheck]) || fromEnv(env[ENV.ci]));
4238
+ }
4239
+ function envOverrides(env = process.env) {
4240
+ return {
4241
+ configDir: Boolean(env[ENV.configDir]),
4242
+ url: Boolean(fromEnv(env[ENV.url])),
4243
+ apiKey: Boolean(fromEnv(env[ENV.apiKey]))
4244
+ };
4245
+ }
4246
+ function pathDirs(env = process.env) {
4247
+ return (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
4248
+ }
4221
4249
  function resolveConfigDir(env = process.env) {
4222
4250
  return env[ENV.configDir] ?? join(env.HOME ?? homedir(), CONFIG_DIR_NAME);
4223
4251
  }
@@ -4435,6 +4463,19 @@ async function loadInstanceWith(store, config, fetch2, capability) {
4435
4463
  return stored;
4436
4464
  }
4437
4465
  }
4466
+ async function recordRelease(store, serverUrl, release) {
4467
+ const entry = (await store.load()).servers[serverUrl];
4468
+ if (!entry?.instance || entry.instance.release === release) return;
4469
+ await store.putServer(
4470
+ serverUrl,
4471
+ { instance: { ...entry.instance, release } },
4472
+ { makeCurrent: false }
4473
+ );
4474
+ }
4475
+ var releaseTracking = (store, config) => config.remembered && config.serverUrl ? {
4476
+ release: config.instance?.release,
4477
+ onRelease: (release) => recordRelease(store, config.serverUrl, release)
4478
+ } : {};
4438
4479
 
4439
4480
  // src/auth.ts
4440
4481
  var CALLBACK_PORT_START = 8765;
@@ -4830,7 +4871,9 @@ function requireClient(ctx) {
4830
4871
  return createApiClient({
4831
4872
  serverUrl,
4832
4873
  apiKey: ctx.config.apiKey,
4833
- fetch: ctx.fetch
4874
+ fetch: ctx.fetch,
4875
+ // A response saying the server runs another release updates the stored one.
4876
+ ...ctx.store ? releaseTracking(ctx.store, ctx.config) : {}
4834
4877
  });
4835
4878
  }
4836
4879
 
@@ -4895,10 +4938,10 @@ async function setVisibility(client, name, visibility) {
4895
4938
  const raw = await client.request("PATCH", sitePath2(name), { body: { visibility } });
4896
4939
  return siteAfter(client, name, raw);
4897
4940
  }
4898
- async function setAccess(client, name, access2, audience) {
4941
+ async function setAccess(client, name, access3, audience) {
4899
4942
  return client.request("PATCH", sitePath2(name), {
4900
4943
  schema: SiteWithAudienceSchema,
4901
- body: { access: access2, ...audience ? { audience } : {} }
4944
+ body: { access: access3, ...audience ? { audience } : {} }
4902
4945
  });
4903
4946
  }
4904
4947
  function accessLevelsEnabled(instance) {
@@ -6714,10 +6757,804 @@ async function confirmSiteName(ctx, client, generated, base2) {
6714
6757
  }
6715
6758
  }
6716
6759
 
6717
- // src/commands/forms.ts
6718
- import { readFile as readFile8, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
6719
- import { dirname as dirname3, join as join8, relative as relative5, resolve as resolve4 } from "node:path";
6760
+ // src/commands/doctor.ts
6720
6761
  import chalk8 from "chalk";
6762
+
6763
+ // src/claude.ts
6764
+ import { execFile as execFile2 } from "node:child_process";
6765
+ import { access, mkdtemp as mkdtemp2, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
6766
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
6767
+ import { join as join8 } from "node:path";
6768
+
6769
+ // mcpb/manifest.json
6770
+ var manifest_default = {
6771
+ manifest_version: "0.3",
6772
+ name: "fling",
6773
+ display_name: "Fling",
6774
+ version: "0.9.0",
6775
+ description: "Publish HTML pages, zips and folders from your computer to Fling and get a link back.",
6776
+ 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
+ author: {
6778
+ name: "Fling contributors",
6779
+ url: "https://github.com/nearform/fling"
6780
+ },
6781
+ repository: {
6782
+ type: "git",
6783
+ url: "https://github.com/nearform/fling.git"
6784
+ },
6785
+ homepage: "https://github.com/nearform/fling",
6786
+ support: "https://github.com/nearform/fling/issues",
6787
+ icon: "icon.png",
6788
+ license: "MIT",
6789
+ keywords: ["publish", "html", "static site", "hosting"],
6790
+ server: {
6791
+ type: "node",
6792
+ entry_point: "server/index.mjs",
6793
+ mcp_config: {
6794
+ command: "node",
6795
+ args: ["${__dirname}/server/index.mjs"],
6796
+ env: {
6797
+ FLING_DOWNLOADS_DIR: "${user_config.downloads_dir}"
6798
+ }
6799
+ }
6800
+ },
6801
+ tools: [
6802
+ {
6803
+ name: "fling_publish",
6804
+ description: "Publish a site from files passed inline (content Claude created)"
6805
+ },
6806
+ {
6807
+ name: "fling_deploy",
6808
+ description: "Publish a file, zip or folder from this computer, found by name in Downloads and other folders"
6809
+ },
6810
+ {
6811
+ name: "fling_pull",
6812
+ description: "Download a site's files into a folder on this computer, to change them and deploy again"
6813
+ },
6814
+ {
6815
+ name: "fling_read_files",
6816
+ description: "Read a site's files inline, to change some and publish them with fling_publish"
6817
+ },
6818
+ {
6819
+ name: "fling_find_files",
6820
+ description: "List recent HTML files, zips and site folders on this computer"
6821
+ },
6822
+ { name: "fling_check_name", description: "Check whether a site name is free" },
6823
+ { name: "fling_login", description: "Sign in to a Fling in the browser" },
6824
+ {
6825
+ name: "fling_servers",
6826
+ description: "List the Flings set up on this computer, or switch the current one"
6827
+ },
6828
+ {
6829
+ name: "fling_doctor",
6830
+ description: "Check the Fling setup on this computer and say how to fix what is wrong"
6831
+ },
6832
+ { name: "fling_whoami", description: "Show the signed-in user" },
6833
+ { name: "fling_list", description: "List your sites, or the sites shared with you" },
6834
+ { name: "fling_info", description: "Show a site and its versions" },
6835
+ { name: "fling_share", description: "Get, rotate or revoke a public share link" },
6836
+ { name: "fling_set_visibility", description: "Make a site private or public" },
6837
+ {
6838
+ name: "fling_set_audience",
6839
+ description: "Limit a private site to listed people and groups, or open it to everyone signed in"
6840
+ },
6841
+ { name: "fling_groups", description: "List the groups of people on a Fling" },
6842
+ { name: "fling_group_show", description: "Show a group and its members" },
6843
+ { name: "fling_group_create", description: "Create a group of people" },
6844
+ {
6845
+ name: "fling_group_members",
6846
+ description: "Add people to a group you own, or take them out"
6847
+ },
6848
+ { name: "fling_group_rename", description: "Rename a group you own" },
6849
+ { name: "fling_group_delete", description: "Delete a group you own" },
6850
+ {
6851
+ name: "fling_archive",
6852
+ description: "Take a site offline (or restore it), keeping its files"
6853
+ },
6854
+ { name: "fling_rollback", description: "Serve an earlier version of a site" },
6855
+ { name: "fling_delete", description: "Permanently delete a site" },
6856
+ {
6857
+ name: "fling_notifications",
6858
+ description: "Show what a Fling emails you about"
6859
+ },
6860
+ {
6861
+ name: "fling_notifications_set",
6862
+ description: "Turn notifications on or off, per event or all at once"
6863
+ }
6864
+ ],
6865
+ user_config: {
6866
+ downloads_dir: {
6867
+ type: "directory",
6868
+ title: "Downloads folder",
6869
+ description: "Where to look first for files you ask Claude to fling by name (Desktop, Documents/Claude and Claude Desktop output folders are searched too).",
6870
+ default: "${HOME}/Downloads",
6871
+ required: false
6872
+ }
6873
+ },
6874
+ compatibility: {
6875
+ platforms: ["darwin", "win32"],
6876
+ runtimes: {
6877
+ node: ">=20.0.0"
6878
+ }
6879
+ }
6880
+ };
6881
+
6882
+ // src/claude.ts
6883
+ var MARKETPLACE_SOURCE = REPO_SLUG;
6884
+ var MARKETPLACE_NAME = "fling";
6885
+ var PLUGIN_NAME = "fling";
6886
+ var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
6887
+ var MCPB_PATH = "/downloads/fling.mcpb";
6888
+ var runClaude = (args) => new Promise((resolve7) => {
6889
+ execFile2(
6890
+ "claude",
6891
+ [...args],
6892
+ { timeout: 12e4, maxBuffer: 16 * 1024 * 1024, shell: process.platform === "win32" },
6893
+ (error, stdout, stderr) => {
6894
+ const missing = error?.code === "ENOENT";
6895
+ const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
6896
+ resolve7({ code, stdout: String(stdout), stderr: String(stderr) });
6897
+ }
6898
+ );
6899
+ });
6900
+ async function claudeCodeStatus(run) {
6901
+ const version = await run(["--version"]);
6902
+ if (version.code !== 0) return { claude: "missing" };
6903
+ const claude = version.stdout.trim().split(/\s+/)[0] ?? "unknown";
6904
+ const list = await run(["plugin", "list", "--json"]);
6905
+ let plugins = [];
6906
+ try {
6907
+ plugins = JSON.parse(list.stdout);
6908
+ } catch {
6909
+ }
6910
+ const fling = Array.isArray(plugins) ? plugins.find((p) => p.id === PLUGIN_ID) : void 0;
6911
+ if (!fling) return { claude, installed: false };
6912
+ return {
6913
+ claude,
6914
+ installed: true,
6915
+ version: fling.version,
6916
+ scope: fling.scope,
6917
+ enabled: fling.enabled
6918
+ };
6919
+ }
6920
+ async function installPlugin(run, scope) {
6921
+ check(
6922
+ await run(["plugin", "install", PLUGIN_NAME, "--marketplace", MARKETPLACE_SOURCE, "-s", scope]),
6923
+ "install the Fling plugin"
6924
+ );
6925
+ }
6926
+ async function updatePlugin(run, scope) {
6927
+ check(await run(["plugin", "marketplace", "update", MARKETPLACE_NAME]), "update the marketplace");
6928
+ check(
6929
+ await run(["plugin", "update", PLUGIN_ID, ...scope ? ["-s", scope] : []]),
6930
+ "update the Fling plugin"
6931
+ );
6932
+ }
6933
+ async function marketplaceConfigured(run) {
6934
+ const list = await run(["plugin", "marketplace", "list", "--json"]);
6935
+ if (list.code !== 0) return null;
6936
+ try {
6937
+ const marketplaces = JSON.parse(list.stdout);
6938
+ return Array.isArray(marketplaces) && marketplaces.some((m) => m.name === MARKETPLACE_NAME);
6939
+ } catch {
6940
+ return null;
6941
+ }
6942
+ }
6943
+ function check(result, what) {
6944
+ if (result.code === 0) return;
6945
+ const detail = (result.stderr || result.stdout).trim().split("\n").at(-1);
6946
+ throw new Error(`Could not ${what}${detail ? `: ${detail}` : ""}`);
6947
+ }
6948
+ async function findClaudeDesktop(platform = process.platform, env = process.env, exists = (path3) => access(path3).then(
6949
+ () => true,
6950
+ () => false
6951
+ )) {
6952
+ const candidates = platform === "darwin" ? ["/Applications/Claude.app", join8(homedir3(), "Applications", "Claude.app")] : platform === "win32" && env.LOCALAPPDATA ? [join8(env.LOCALAPPDATA, "AnthropicClaude"), join8(env.LOCALAPPDATA, "Programs", "Claude")] : [];
6953
+ for (const path3 of candidates) if (await exists(path3)) return path3;
6954
+ return null;
6955
+ }
6956
+ var DESKTOP_EXTENSION_ID = `local.mcpb.${manifest_default.author.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.${manifest_default.name}`;
6957
+ function claudeDesktopDataDir(platform = process.platform, env = process.env, home = homedir3()) {
6958
+ if (platform === "darwin") return join8(home, "Library", "Application Support", "Claude");
6959
+ if (platform === "win32" && env.APPDATA) return join8(env.APPDATA, "Claude");
6960
+ return null;
6961
+ }
6962
+ async function desktopExtensionStatus(dataDir) {
6963
+ const readJson2 = async (path3) => {
6964
+ try {
6965
+ return JSON.parse(await readFile8(path3, "utf8"));
6966
+ } catch {
6967
+ return null;
6968
+ }
6969
+ };
6970
+ const manifest = await readJson2(
6971
+ join8(dataDir, "Claude Extensions", DESKTOP_EXTENSION_ID, "manifest.json")
6972
+ );
6973
+ if (!manifest) return { installed: false };
6974
+ const settings = await readJson2(
6975
+ join8(dataDir, "Claude Extensions Settings", `${DESKTOP_EXTENSION_ID}.json`)
6976
+ );
6977
+ return {
6978
+ installed: true,
6979
+ ...typeof manifest.version === "string" ? { version: manifest.version } : {},
6980
+ ...typeof settings?.isEnabled === "boolean" ? { enabled: settings.isEnabled } : {}
6981
+ };
6982
+ }
6983
+ var releaseExtensionUrl = (version) => `${REPO_URL}/releases/download/v${version}/fling.mcpb`;
6984
+ function extensionSources(serverUrl, serverRelease, version = VERSION) {
6985
+ const server = {
6986
+ url: new URL(MCPB_PATH, serverUrl).toString(),
6987
+ ...serverRelease ? { version: serverRelease } : {}
6988
+ };
6989
+ if (serverRelease === version) return [server];
6990
+ return [{ url: releaseExtensionUrl(version), version }, server];
6991
+ }
6992
+ async function downloadExtension(url, fetch2) {
6993
+ const response = await fetch2(url);
6994
+ if (!response.ok) throw new Error(`Could not download ${url} (HTTP ${response.status})`);
6995
+ const dir = await mkdtemp2(join8(tmpdir2(), "fling-mcpb-"));
6996
+ const file = join8(dir, "fling.mcpb");
6997
+ await writeFile4(file, Buffer.from(await response.arrayBuffer()));
6998
+ return file;
6999
+ }
7000
+
7001
+ // src/doctor.ts
7002
+ import { execFile as execFile3 } from "node:child_process";
7003
+ import { access as access2, constants, realpath as realpath3, stat as stat6 } from "node:fs/promises";
7004
+ import { join as join10 } from "node:path";
7005
+
7006
+ // src/update-check.ts
7007
+ import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
7008
+ import { join as join9 } from "node:path";
7009
+ var UPDATE_COMMAND = `npm i -g ${PACKAGE_NAME}@latest && ${BIN_NAME} setup`;
7010
+ var UPDATE_STATE_FILE = "update-check.json";
7011
+ var NOTIFY_EVERY_MS = 24 * 60 * 60 * 1e3;
7012
+ var SEMVER = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/;
7013
+ function parseSemver(input) {
7014
+ if (typeof input !== "string") return null;
7015
+ const m = SEMVER.exec(input.trim());
7016
+ if (!m) return null;
7017
+ return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split(".") : [] };
7018
+ }
7019
+ function compareSemver(a, b) {
7020
+ for (let i = 0; i < 3; i++) {
7021
+ const d = (a.core[i] ?? 0) - (b.core[i] ?? 0);
7022
+ if (d !== 0) return d;
7023
+ }
7024
+ if (!a.pre.length || !b.pre.length) return b.pre.length - a.pre.length;
7025
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
7026
+ const x = a.pre[i];
7027
+ const y = b.pre[i];
7028
+ if (x === void 0) return -1;
7029
+ if (y === void 0) return 1;
7030
+ if (x === y) continue;
7031
+ const xn = /^\d+$/.test(x);
7032
+ const yn = /^\d+$/.test(y);
7033
+ if (xn && yn) return Number(x) - Number(y);
7034
+ if (xn !== yn) return xn ? -1 : 1;
7035
+ return x < y ? -1 : 1;
7036
+ }
7037
+ return 0;
7038
+ }
7039
+ function newerRelease(release, version = VERSION) {
7040
+ const theirs = parseSemver(release);
7041
+ const ours = parseSemver(version);
7042
+ if (!theirs || !ours || compareSemver(theirs, ours) <= 0) return null;
7043
+ return String(release).trim();
7044
+ }
7045
+ var updateLine = (release, version = VERSION) => `Fling ${release} is out (this CLI is ${version}). Update: ${UPDATE_COMMAND}`;
7046
+ var mcpUpdateNote = (release, version = VERSION) => `Fling ${release} is out and this Fling MCP server is ${version}. Tell the user once in this conversation, and suggest they update by running \`${UPDATE_COMMAND}\` in a terminal, then restarting Claude.`;
7047
+ async function updateNotice(input) {
7048
+ try {
7049
+ if (input.json || !input.stderrTTY || input.silenced || !input.serverUrl) return null;
7050
+ const version = input.version ?? VERSION;
7051
+ const release = newerRelease(input.release, version);
7052
+ if (!release) return null;
7053
+ const now = input.now ?? /* @__PURE__ */ new Date();
7054
+ const file = join9(input.store.dir, UPDATE_STATE_FILE);
7055
+ const state = await readState(file);
7056
+ const last = Date.parse(state.notified[input.serverUrl] ?? "");
7057
+ if (Number.isFinite(last) && now.getTime() - last < NOTIFY_EVERY_MS) return null;
7058
+ state.notified[input.serverUrl] = now.toISOString();
7059
+ await mkdir4(input.store.dir, { recursive: true, mode: 448 });
7060
+ await writeFile5(file, `${JSON.stringify(state, null, 2)}
7061
+ `, { mode: 384 });
7062
+ return updateLine(release, version);
7063
+ } catch {
7064
+ return null;
7065
+ }
7066
+ }
7067
+ async function noticeAfterCommand(ctx, server, options = {}) {
7068
+ try {
7069
+ const config = await ctx.store.resolve({ serverUrl: server }).catch(() => ctx.config);
7070
+ const line = await updateNotice({
7071
+ store: ctx.store,
7072
+ serverUrl: config.serverUrl,
7073
+ release: config.instance?.release,
7074
+ json: ctx.json,
7075
+ stderrTTY: options.stderrTTY ?? Boolean(process.stderr.isTTY),
7076
+ silenced: options.silenced ?? updateCheckSilenced(),
7077
+ now: options.now,
7078
+ version: options.version
7079
+ });
7080
+ if (line) ctx.log.warn(line);
7081
+ } catch {
7082
+ }
7083
+ }
7084
+ async function readState(file) {
7085
+ try {
7086
+ const json = JSON.parse(await readFile9(file, "utf8"));
7087
+ const notified = json?.notified;
7088
+ if (notified && typeof notified === "object" && !Array.isArray(notified)) {
7089
+ const entries = Object.entries(notified).filter(
7090
+ (e) => typeof e[1] === "string"
7091
+ );
7092
+ return { notified: Object.fromEntries(entries) };
7093
+ }
7094
+ } catch {
7095
+ }
7096
+ return { notified: {} };
7097
+ }
7098
+
7099
+ // src/doctor.ts
7100
+ var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME.replace("/", "%2f")}/dist-tags`;
7101
+ var DEFAULT_TIMEOUT_MS = 5e3;
7102
+ var PLUGIN_UPDATE = `claude plugin marketplace update ${MARKETPLACE_NAME} && claude plugin update ${PLUGIN_ID}`;
7103
+ async function runChecks(deps) {
7104
+ const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7105
+ const fetchWithTimeout = (input, init) => deps.fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
7106
+ const d = { ...deps, fetch: fetchWithTimeout, timeoutMs, version: deps.version ?? VERSION };
7107
+ const groups2 = await Promise.all([
7108
+ cliChecks(d),
7109
+ configChecks(d),
7110
+ serverChecks(d),
7111
+ claudeCodeChecks(d),
7112
+ claudeDesktopChecks(d)
7113
+ ]);
7114
+ return groups2.flat();
7115
+ }
7116
+ var doctorExitCode = (checks) => checks.some((c) => c.status === "problem") ? 1 : 0;
7117
+ function systemDoctorDeps(store, fetch2, runClaude2, findClaudeDesktop2) {
7118
+ const { env, home, platform } = systemLocateOptions();
7119
+ return {
7120
+ store,
7121
+ fetch: fetch2,
7122
+ env,
7123
+ home,
7124
+ platform,
7125
+ runClaude: runClaude2,
7126
+ findClaudeDesktop: findClaudeDesktop2,
7127
+ runProgram: (file, args) => new Promise((resolve7) => {
7128
+ execFile3(
7129
+ file,
7130
+ [...args],
7131
+ { timeout: DEFAULT_TIMEOUT_MS, shell: platform === "win32" },
7132
+ (error, stdout) => resolve7({ code: error ? 1 : 0, stdout: String(stdout) })
7133
+ );
7134
+ }),
7135
+ selfPath: process.argv[1],
7136
+ nodeVersion: process.versions.node
7137
+ };
7138
+ }
7139
+ var SYMBOL = { ok: "\u2714", warning: "\u25B2", problem: "\u2716" };
7140
+ function describeChecks(checks) {
7141
+ return checks.map((c) => `${SYMBOL[c.status]} ${c.message}${c.fix ? ` \u2014 fix: ${c.fix}` : ""}`).join("\n");
7142
+ }
7143
+ async function cliChecks(d) {
7144
+ const checks = [
7145
+ { area: "cli", id: "cli.version", status: "ok", message: `${BIN_NAME} CLI ${d.version}` }
7146
+ ];
7147
+ const min = minimumNode(NODE_ENGINE);
7148
+ const node = parseSemver(d.nodeVersion);
7149
+ if (min && node) {
7150
+ checks.push(
7151
+ compareSemver(node, min.semver) >= 0 ? { area: "cli", id: "cli.node", status: "ok", message: `Node.js ${d.nodeVersion}` } : {
7152
+ area: "cli",
7153
+ id: "cli.node",
7154
+ status: "problem",
7155
+ message: `Node.js ${d.nodeVersion} is too old: the CLI needs ${NODE_ENGINE}`,
7156
+ fix: `Install Node.js ${min.text} or later (https://nodejs.org)`
7157
+ }
7158
+ );
7159
+ }
7160
+ const latest = await latestOnNpm(d);
7161
+ if (latest) {
7162
+ const newer = newerRelease(latest, d.version);
7163
+ checks.push(
7164
+ newer ? {
7165
+ area: "cli",
7166
+ id: "cli.latest",
7167
+ status: "warning",
7168
+ message: `${PACKAGE_NAME} ${newer} is on npm (this CLI is ${d.version})`,
7169
+ fix: UPDATE_COMMAND
7170
+ } : {
7171
+ area: "cli",
7172
+ id: "cli.latest",
7173
+ status: "ok",
7174
+ message: `Up to date with npm (latest: ${latest})`
7175
+ }
7176
+ );
7177
+ }
7178
+ checks.push(await onPath(d));
7179
+ return checks;
7180
+ }
7181
+ function minimumNode(range) {
7182
+ const m = /^>=\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range.trim());
7183
+ if (!m) return null;
7184
+ const text2 = [m[1], m[2], m[3]].filter((p) => p !== void 0).join(".");
7185
+ const semver = parseSemver(`${m[1]}.${m[2] ?? 0}.${m[3] ?? 0}`);
7186
+ return semver ? { text: text2, semver } : null;
7187
+ }
7188
+ async function latestOnNpm(d) {
7189
+ try {
7190
+ const response = await d.fetch(NPM_DIST_TAGS_URL, { headers: { Accept: "application/json" } });
7191
+ if (!response.ok) return null;
7192
+ const tags = await response.json();
7193
+ return typeof tags.latest === "string" && parseSemver(tags.latest) ? tags.latest : null;
7194
+ } catch {
7195
+ return null;
7196
+ }
7197
+ }
7198
+ async function onPath(d) {
7199
+ const names = d.platform === "win32" ? [`${BIN_NAME}.cmd`, `${BIN_NAME}.exe`, BIN_NAME] : [BIN_NAME];
7200
+ let found;
7201
+ for (const dir of pathDirs(d.env)) {
7202
+ for (const name of names) {
7203
+ const path3 = join10(dir, name);
7204
+ if (await isExecutable(path3, d.platform)) {
7205
+ found = path3;
7206
+ break;
7207
+ }
7208
+ }
7209
+ if (found) break;
7210
+ }
7211
+ const install = `npm i -g ${PACKAGE_NAME}@latest`;
7212
+ if (!found)
7213
+ return {
7214
+ area: "cli",
7215
+ id: "cli.path",
7216
+ status: "warning",
7217
+ message: `\`${BIN_NAME}\` is not on your PATH (this CLI runs through npx or another path)`,
7218
+ fix: install
7219
+ };
7220
+ const real = await realpath3(found).catch(() => found);
7221
+ const self2 = d.selfPath ? await realpath3(d.selfPath).catch(() => d.selfPath) : void 0;
7222
+ if (self2 && real === self2)
7223
+ return {
7224
+ area: "cli",
7225
+ id: "cli.path",
7226
+ status: "ok",
7227
+ message: `\`${BIN_NAME}\` on PATH is this CLI (${found})`
7228
+ };
7229
+ const other = await d.runProgram(found, ["--version"]);
7230
+ const version = other.code === 0 ? other.stdout.trim().split(/\s+/).at(-1) : void 0;
7231
+ if (version === d.version)
7232
+ return {
7233
+ area: "cli",
7234
+ id: "cli.path",
7235
+ status: "ok",
7236
+ message: `\`${BIN_NAME}\` on PATH (${found}) is ${version}, as this CLI`
7237
+ };
7238
+ return {
7239
+ area: "cli",
7240
+ id: "cli.path",
7241
+ status: "warning",
7242
+ message: `\`${BIN_NAME}\` on PATH (${found}) is ${version ? `version ${version}` : "another copy"}, not this CLI (${d.version})`,
7243
+ fix: `${install} && ${BIN_NAME} setup`
7244
+ };
7245
+ }
7246
+ async function isExecutable(path3, platform) {
7247
+ try {
7248
+ const s = await stat6(path3);
7249
+ if (!s.isFile()) return false;
7250
+ if (platform !== "win32") await access2(path3, constants.X_OK);
7251
+ return true;
7252
+ } catch {
7253
+ return false;
7254
+ }
7255
+ }
7256
+ async function configChecks(d) {
7257
+ const overrides = envOverrides(d.env);
7258
+ const checks = [];
7259
+ const where = `${d.store.dir}${overrides.configDir ? ` (from ${ENV.configDir})` : ""}`;
7260
+ const config = await d.store.load().catch(() => null);
7261
+ const fileMode = await stat6(d.store.file).then(
7262
+ (s) => s.mode & 511,
7263
+ () => null
7264
+ );
7265
+ if (fileMode === null || !config || Object.keys(config.servers).length === 0) {
7266
+ checks.push({
7267
+ area: "config",
7268
+ id: "config.dir",
7269
+ status: "warning",
7270
+ message: `No Fling is set up yet (config: ${where})`,
7271
+ fix: `${BIN_NAME} setup <your Fling's address>`
7272
+ });
7273
+ } else {
7274
+ checks.push({ area: "config", id: "config.dir", status: "ok", message: `Config: ${where}` });
7275
+ }
7276
+ if (fileMode !== null && d.platform !== "win32") {
7277
+ const mode = fileMode.toString(8).padStart(3, "0");
7278
+ checks.push(
7279
+ fileMode & 63 ? {
7280
+ area: "config",
7281
+ id: "config.permissions",
7282
+ status: "problem",
7283
+ message: `${d.store.file} holds API keys and is readable by others (mode ${mode})`,
7284
+ fix: `chmod 600 "${d.store.file}"`
7285
+ } : {
7286
+ area: "config",
7287
+ id: "config.permissions",
7288
+ status: "ok",
7289
+ message: `The config file is private (mode ${mode})`
7290
+ }
7291
+ );
7292
+ }
7293
+ if (overrides.url)
7294
+ checks.push({
7295
+ area: "config",
7296
+ id: "config.env.url",
7297
+ status: "warning",
7298
+ message: `${ENV.url} is set: commands use the server it names, not the current one`,
7299
+ fix: `unset ${ENV.url} (if you did not mean to)`
7300
+ });
7301
+ if (overrides.apiKey)
7302
+ checks.push({
7303
+ area: "config",
7304
+ id: "config.env.apiKey",
7305
+ status: "warning",
7306
+ message: `${ENV.apiKey} is set: commands use that key, not the stored one`,
7307
+ fix: `unset ${ENV.apiKey} (if you did not mean to)`
7308
+ });
7309
+ return checks;
7310
+ }
7311
+ async function serverChecks(d) {
7312
+ const config = await d.store.load().catch(() => null);
7313
+ if (!config) return [];
7314
+ const resolved = await d.store.resolve().catch(() => null);
7315
+ const urls = Object.keys(config.servers);
7316
+ if (resolved?.serverUrl && !urls.includes(resolved.serverUrl)) urls.push(resolved.serverUrl);
7317
+ const perServer = await Promise.all(
7318
+ urls.map(async (url) => {
7319
+ const entry = config.servers[url] ?? {};
7320
+ const label = `${url}${url === config.current ? " (current)" : ""}`;
7321
+ const apiKey = resolved?.serverUrl === url && resolved.apiKeySource === "env" ? resolved.apiKey : entry.apiKey;
7322
+ return oneServer(d, url, label, apiKey);
7323
+ })
7324
+ );
7325
+ return perServer.flat();
7326
+ }
7327
+ async function oneServer(d, url, label, apiKey) {
7328
+ const at = (id, check2) => ({
7329
+ area: "server",
7330
+ id,
7331
+ server: url,
7332
+ ...check2
7333
+ });
7334
+ let instance;
7335
+ try {
7336
+ instance = await fetchInstance(url, d.fetch);
7337
+ } catch (error) {
7338
+ return [
7339
+ at("server.metadata", {
7340
+ status: "problem",
7341
+ message: `${label}: ${error.message}`,
7342
+ fix: `Check the address and your connection; if the Fling moved: ${BIN_NAME} login <its new address>`
7343
+ })
7344
+ ];
7345
+ }
7346
+ const checks = [
7347
+ at("server.metadata", { status: "ok", message: `${label}: ${instance.name} answers` })
7348
+ ];
7349
+ const newer = newerRelease(instance.release, d.version);
7350
+ checks.push(
7351
+ newer ? at("server.release", {
7352
+ status: "warning",
7353
+ message: `${label} runs Fling ${newer}, newer than this CLI (${d.version})`,
7354
+ fix: UPDATE_COMMAND
7355
+ }) : at("server.release", {
7356
+ status: "ok",
7357
+ message: instance.release ? `${label} runs Fling ${instance.release}` : `${label} does not say which release it runs`
7358
+ })
7359
+ );
7360
+ const login = `${BIN_NAME} login ${url}`;
7361
+ if (!apiKey) {
7362
+ checks.push(
7363
+ at("server.signedIn", { status: "problem", message: `${label}: not signed in`, fix: login })
7364
+ );
7365
+ } else {
7366
+ try {
7367
+ const client = createApiClient({
7368
+ serverUrl: url,
7369
+ apiKey,
7370
+ fetch: d.fetch,
7371
+ timeoutMs: d.timeoutMs
7372
+ });
7373
+ const me = await client.request("GET", "/api/me");
7374
+ checks.push(
7375
+ at("server.signedIn", {
7376
+ status: "ok",
7377
+ message: `${label}: signed in${me?.email ? ` as ${me.email}` : ""}`
7378
+ })
7379
+ );
7380
+ } catch (error) {
7381
+ checks.push(
7382
+ exitCodeFor(error) === 2 ? at("server.signedIn", {
7383
+ status: "problem",
7384
+ message: `${label}: the API key is not accepted`,
7385
+ fix: login
7386
+ }) : at("server.signedIn", {
7387
+ status: "warning",
7388
+ message: `${label}: could not check the sign-in (${error.message})`,
7389
+ fix: `${BIN_NAME} whoami --server ${url}`
7390
+ })
7391
+ );
7392
+ }
7393
+ }
7394
+ const on = Object.entries(instance.capabilities).filter(([, v]) => v === true).map(([k]) => k).sort();
7395
+ checks.push(
7396
+ at("server.capabilities", {
7397
+ status: "ok",
7398
+ message: `${label}: ${on.length ? on.join(", ") : "no optional features"}`
7399
+ })
7400
+ );
7401
+ return checks;
7402
+ }
7403
+ async function claudeCodeChecks(d) {
7404
+ const status = await claudeCodeStatus(d.runClaude).catch(() => ({ claude: "missing" }));
7405
+ if (status.claude === "missing")
7406
+ return [
7407
+ {
7408
+ area: "claudeCode",
7409
+ id: "claudeCode.cli",
7410
+ status: "warning",
7411
+ message: "Claude Code not found (no `claude` on your PATH)",
7412
+ fix: `Install Claude Code (https://claude.com/claude-code), then run \`${BIN_NAME} setup\``
7413
+ }
7414
+ ];
7415
+ const checks = [
7416
+ {
7417
+ area: "claudeCode",
7418
+ id: "claudeCode.cli",
7419
+ status: "ok",
7420
+ message: `Claude Code ${status.claude}`
7421
+ }
7422
+ ];
7423
+ const marketplace = await marketplaceConfigured(d.runClaude).catch(() => null);
7424
+ if (marketplace === false)
7425
+ checks.push({
7426
+ area: "claudeCode",
7427
+ id: "claudeCode.marketplace",
7428
+ status: "warning",
7429
+ message: `The ${MARKETPLACE_NAME} marketplace is not configured in Claude Code`,
7430
+ fix: `${BIN_NAME} setup, or: claude plugin marketplace add ${MARKETPLACE_SOURCE}`
7431
+ });
7432
+ else if (marketplace === true)
7433
+ checks.push({
7434
+ area: "claudeCode",
7435
+ id: "claudeCode.marketplace",
7436
+ status: "ok",
7437
+ message: `The ${MARKETPLACE_NAME} marketplace is configured`
7438
+ });
7439
+ if (!("installed" in status) || !status.installed) {
7440
+ checks.push({
7441
+ area: "claudeCode",
7442
+ id: "claudeCode.plugin",
7443
+ status: "warning",
7444
+ message: "The Fling plugin is not installed in Claude Code",
7445
+ fix: `${BIN_NAME} setup`
7446
+ });
7447
+ return checks;
7448
+ }
7449
+ if (status.enabled === false)
7450
+ checks.push({
7451
+ area: "claudeCode",
7452
+ id: "claudeCode.enabled",
7453
+ status: "problem",
7454
+ message: "The Fling plugin is installed but disabled",
7455
+ fix: `claude plugin enable ${PLUGIN_ID}`
7456
+ });
7457
+ const behind = status.version ? newerRelease(d.version, status.version) : null;
7458
+ checks.push(
7459
+ behind ? {
7460
+ area: "claudeCode",
7461
+ id: "claudeCode.plugin",
7462
+ status: "warning",
7463
+ message: `The Fling plugin is ${status.version}, older than this CLI (${d.version})`,
7464
+ fix: `${BIN_NAME} setup, or: ${PLUGIN_UPDATE}`
7465
+ } : {
7466
+ area: "claudeCode",
7467
+ id: "claudeCode.plugin",
7468
+ status: "ok",
7469
+ message: `The Fling plugin is installed${status.version ? ` (${status.version})` : ""}${status.enabled === false ? "" : " and enabled"}`
7470
+ }
7471
+ );
7472
+ return checks;
7473
+ }
7474
+ async function claudeDesktopChecks(d) {
7475
+ const app = await d.findClaudeDesktop().catch(() => null);
7476
+ if (!app) return [];
7477
+ const dataDir = claudeDesktopDataDir(d.platform, d.env, d.home);
7478
+ const ext = dataDir ? await desktopExtensionStatus(dataDir) : null;
7479
+ const at = (check2) => ({
7480
+ area: "claudeDesktop",
7481
+ id: "claudeDesktop.extension",
7482
+ ...check2
7483
+ });
7484
+ if (!ext)
7485
+ return [
7486
+ at({
7487
+ status: "ok",
7488
+ message: `Claude Desktop is installed (${app}); the extension could not be looked up`
7489
+ })
7490
+ ];
7491
+ if (!ext.installed)
7492
+ return [
7493
+ at({
7494
+ status: "warning",
7495
+ message: "The Fling extension is not installed in Claude Desktop",
7496
+ fix: `${BIN_NAME} setup (it opens fling.mcpb in Claude Desktop)`
7497
+ })
7498
+ ];
7499
+ const checks = [];
7500
+ if (ext.enabled === false)
7501
+ checks.push({
7502
+ area: "claudeDesktop",
7503
+ id: "claudeDesktop.enabled",
7504
+ status: "problem",
7505
+ message: "The Fling extension is installed in Claude Desktop but disabled",
7506
+ fix: "Turn it on in Claude Desktop: Settings \u2192 Extensions \u2192 Fling"
7507
+ });
7508
+ const behind = ext.version ? newerRelease(d.version, ext.version) : null;
7509
+ checks.push(
7510
+ behind ? at({
7511
+ status: "warning",
7512
+ message: `The Claude Desktop extension is ${ext.version}, older than this CLI (${d.version})`,
7513
+ fix: `${BIN_NAME} setup`
7514
+ }) : at({
7515
+ status: "ok",
7516
+ message: `The Claude Desktop extension is installed${ext.version ? ` (${ext.version})` : ""} (${DESKTOP_EXTENSION_ID})`
7517
+ })
7518
+ );
7519
+ return checks;
7520
+ }
7521
+
7522
+ // src/commands/doctor.ts
7523
+ var AREAS = {
7524
+ cli: "CLI",
7525
+ config: "Config",
7526
+ server: "Flings",
7527
+ claudeCode: "Claude Code",
7528
+ claudeDesktop: "Claude Desktop"
7529
+ };
7530
+ var SYMBOLS = { ok: symbols.success, warning: symbols.warn, problem: symbols.error };
7531
+ async function runDoctor(ctx, deps) {
7532
+ const checks = await runChecks({
7533
+ ...systemDoctorDeps(ctx.store, ctx.fetch, runClaude, () => findClaudeDesktop()),
7534
+ ...deps
7535
+ });
7536
+ const ok = doctorExitCode(checks) === 0;
7537
+ ctx.out.data({ ok, checks }, () => render(checks));
7538
+ if (!ok) process.exitCode = 1;
7539
+ }
7540
+ function render(checks) {
7541
+ const lines = [];
7542
+ let area;
7543
+ for (const c of checks) {
7544
+ if (c.area !== area) {
7545
+ area = c.area;
7546
+ lines.push(`${lines.length ? "\n" : ""}${chalk8.bold(AREAS[c.area])}`);
7547
+ }
7548
+ const message = c.status === "problem" ? chalk8.red(c.message) : c.message;
7549
+ lines.push(`${SYMBOLS[c.status]} ${message}${c.fix ? chalk8.dim(` \u2014 fix: ${c.fix}`) : ""}`);
7550
+ }
7551
+ return lines.join("\n");
7552
+ }
7553
+
7554
+ // src/commands/forms.ts
7555
+ import { readFile as readFile10, stat as stat7, writeFile as writeFile6 } from "node:fs/promises";
7556
+ import { dirname as dirname3, join as join11, relative as relative5, resolve as resolve4 } from "node:path";
7557
+ import chalk9 from "chalk";
6721
7558
  async function formsClient(ctx) {
6722
7559
  const client = requireClient(ctx);
6723
7560
  const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "forms");
@@ -6746,8 +7583,8 @@ async function runForms(ctx, name, opts) {
6746
7583
  const summary = checkFormsConfig(forms2.config).summary;
6747
7584
  if (summary) {
6748
7585
  const lines = formsSummaryLines(summary);
6749
- const onPath = opts.path ? ` (on ${forms2.path ?? opts.path}: ${forms2.forms.map((f) => f.id).join(", ") || "none"})` : "";
6750
- ctx.log.info(`${summary.title} on ${name}${onPath}`);
7586
+ const onPath2 = opts.path ? ` (on ${forms2.path ?? opts.path}: ${forms2.forms.map((f) => f.id).join(", ") || "none"})` : "";
7587
+ ctx.log.info(`${summary.title} on ${name}${onPath2}`);
6751
7588
  for (const line of lines) ctx.log.hint(line);
6752
7589
  }
6753
7590
  if (instance)
@@ -6762,32 +7599,32 @@ async function runFormsInit(ctx, dir, opts) {
6762
7599
  });
6763
7600
  }
6764
7601
  const folder = resolve4(dir ?? ".");
6765
- const info = await stat6(folder).catch(() => null);
7602
+ const info = await stat7(folder).catch(() => null);
6766
7603
  if (!info?.isDirectory()) throw new CliError(`No such folder: ${dir ?? "."}`);
6767
- const file = join8(folder, FORMS_FILE);
6768
- if (!opts.force && await stat6(file).catch(() => null)) {
7604
+ const file = join11(folder, FORMS_FILE);
7605
+ if (!opts.force && await stat7(file).catch(() => null)) {
6769
7606
  throw new CliError(`${shown(file)} already exists`, {
6770
7607
  hint: "Pass --force to replace it with the template."
6771
7608
  });
6772
7609
  }
6773
7610
  const config = formsTemplate(template, opts.title);
6774
7611
  requireValidForms(config, void 0, "The template", ctx.binName);
6775
- await writeFile4(file, `${formatJson(config)}
7612
+ await writeFile6(file, `${formatJson(config)}
6776
7613
  `);
6777
7614
  ctx.log.success(`Wrote ${shown(file)} (${template}: ${FORM_TEMPLATE_DESCRIPTIONS[template]})`);
6778
7615
  ctx.out.data(
6779
7616
  { path: file, template, forms: config },
6780
- () => chalk8.dim(
7617
+ () => chalk9.dim(
6781
7618
  `Change its options, wording and paths to fit, check it with \`${ctx.binName} forms check ${shown(dirname3(file))}\`, then deploy the folder.`
6782
7619
  )
6783
7620
  );
6784
7621
  }
6785
7622
  async function runFormsCheck(ctx, path3) {
6786
7623
  const target = resolve4(path3 ?? ".");
6787
- const info = await stat6(target).catch(() => null);
7624
+ const info = await stat7(target).catch(() => null);
6788
7625
  if (!info) throw new CliError(`No such file or folder: ${path3 ?? "."}`);
6789
7626
  const folder = info.isDirectory() ? target : dirname3(target);
6790
- const file = info.isDirectory() ? join8(target, FORMS_FILE) : target;
7627
+ const file = info.isDirectory() ? join11(target, FORMS_FILE) : target;
6791
7628
  const config = info.isDirectory() ? (await readFormsFile(folder))?.config : await readJson(file);
6792
7629
  if (config === void 0) {
6793
7630
  throw new CliError(`No ${FORMS_FILE} in ${shown(folder)}`, {
@@ -6808,7 +7645,7 @@ async function runFormsCheck(ctx, path3) {
6808
7645
  }
6809
7646
  async function readJson(file) {
6810
7647
  try {
6811
- return JSON.parse(await readFile8(file, "utf8"));
7648
+ return JSON.parse(await readFile10(file, "utf8"));
6812
7649
  } catch (error) {
6813
7650
  throw new CliError(`${shown(file)} is not valid JSON: ${error.message}`);
6814
7651
  }
@@ -6828,7 +7665,7 @@ async function runResponses(ctx, name, opts) {
6828
7665
  const body = as === "csv" ? responsesCsv(exported.responses, exported.forms) : as === "jsonl" ? responsesJsonl(exported.responses) : `${formatJson(exported)}
6829
7666
  `;
6830
7667
  if (opts.output !== void 0) {
6831
- await writeFile4(resolve4(opts.output), body);
7668
+ await writeFile6(resolve4(opts.output), body);
6832
7669
  ctx.log.success(
6833
7670
  `Wrote ${exported.responses.length} response${exported.responses.length === 1 ? "" : "s"} to ${shown(resolve4(opts.output))}`
6834
7671
  );
@@ -6836,7 +7673,7 @@ async function runResponses(ctx, name, opts) {
6836
7673
  return;
6837
7674
  }
6838
7675
  ctx.out.data({ ...exported, ...page ? { responsesUrl: page } : {} }, () => {
6839
- if (!exported.title) return chalk8.dim(`${name} has no forms on its live version.`);
7676
+ if (!exported.title) return chalk9.dim(`${name} has no forms on its live version.`);
6840
7677
  const byId = new Map(exported.forms.map((f) => [f.id, f]));
6841
7678
  const table = renderTable(exported.responses, [
6842
7679
  { header: "PAGE", value: (r) => r.path },
@@ -6854,19 +7691,19 @@ async function runResponses(ctx, name, opts) {
6854
7691
  }
6855
7692
  ]);
6856
7693
  return [
6857
- chalk8.bold(`${exported.title} on ${name}`),
7694
+ chalk9.bold(`${exported.title} on ${name}`),
6858
7695
  "",
6859
7696
  table,
6860
7697
  "",
6861
- ...progressLines(exported).map((l) => chalk8.dim(l)),
6862
- ...page ? [chalk8.dim(`Progress and downloads: ${page}`)] : [],
6863
- chalk8.dim(`Export: \`${ctx.binName} responses ${name} --format csv -o responses.csv\``)
7698
+ ...progressLines(exported).map((l) => chalk9.dim(l)),
7699
+ ...page ? [chalk9.dim(`Progress and downloads: ${page}`)] : [],
7700
+ chalk9.dim(`Export: \`${ctx.binName} responses ${name} --format csv -o responses.csv\``)
6864
7701
  ].join("\n");
6865
7702
  });
6866
7703
  }
6867
7704
 
6868
7705
  // src/commands/groups.ts
6869
- import chalk9 from "chalk";
7706
+ import chalk10 from "chalk";
6870
7707
  function requireGroups(ctx) {
6871
7708
  const instance = ctx.config.instance;
6872
7709
  if (instance && !groupsEnabled(instance)) throw new CliError(`${instance.name} has no groups`);
@@ -6887,7 +7724,7 @@ async function runGroups(ctx, opts) {
6887
7724
  { header: "NAME", value: (g) => g.name },
6888
7725
  { header: "PEOPLE", value: (g) => g.size },
6889
7726
  { header: "OWNER", value: (g) => g.owner === me.email ? "you" : g.owner }
6890
- ]) : chalk9.dim(members2 ? "No group has exactly those people." : describeGroups([]))
7727
+ ]) : chalk10.dim(members2 ? "No group has exactly those people." : describeGroups([]))
6891
7728
  );
6892
7729
  }
6893
7730
  async function runGroupShow(ctx, slug) {
@@ -6933,7 +7770,7 @@ async function runGroupDelete(ctx, slug, opts) {
6933
7770
  }
6934
7771
 
6935
7772
  // src/commands/notifications.ts
6936
- import chalk10 from "chalk";
7773
+ import chalk11 from "chalk";
6937
7774
  var VALUES = ["on", "off", "default"];
6938
7775
  function notificationsClient(ctx) {
6939
7776
  const instance = ctx.config.instance;
@@ -6956,10 +7793,10 @@ function renderSettings(settings) {
6956
7793
  })),
6957
7794
  { header: "WHEN", value: (e) => e.title }
6958
7795
  ]);
6959
- const lines = [table, "", chalk10.dim("* your choice; the rest are the defaults")];
7796
+ const lines = [table, "", chalk11.dim("* your choice; the rest are the defaults")];
6960
7797
  if (settings.muted) {
6961
7798
  lines.unshift(
6962
- chalk10.yellow(
7799
+ chalk11.yellow(
6963
7800
  "Muted: nothing is sent, whatever the table says (`fling notifications unmute` to undo)."
6964
7801
  ),
6965
7802
  ""
@@ -7018,13 +7855,13 @@ async function runNotificationsSent(ctx) {
7018
7855
  { header: "CHANNEL", value: (s) => s.channel },
7019
7856
  { header: "SUBJECT", value: (s) => s.subject },
7020
7857
  { header: "EVENTS", value: (s) => s.eventKeys.join(", ") }
7021
- ]) : chalk10.dim("Nothing was sent to you in the last 30 days.")
7858
+ ]) : chalk11.dim("Nothing was sent to you in the last 30 days.")
7022
7859
  );
7023
7860
  }
7024
7861
 
7025
7862
  // src/commands/pull.ts
7026
7863
  import { relative as relative6, resolve as resolve5 } from "node:path";
7027
- import chalk11 from "chalk";
7864
+ import chalk12 from "chalk";
7028
7865
  async function runPull(ctx, name, dir, opts) {
7029
7866
  const client = requireClient(ctx);
7030
7867
  const instance = await loadInstanceWith(ctx.store, ctx.config, ctx.fetch, "pull");
@@ -7066,11 +7903,11 @@ async function runPull(ctx, name, dir, opts) {
7066
7903
  });
7067
7904
  const shown2 = relative6(process.cwd(), pulled.dir) || ".";
7068
7905
  ctx.log.success(
7069
- `Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${shown2} ${chalk11.dim(`(${pulled.fileCount} files, ${formatBytes(pulled.totalBytes)}, version ${pulled.versionId})`)}`
7906
+ `Pulled ${name}${path3 ? ` /${path3}/` : ""} into ${shown2} ${chalk12.dim(`(${pulled.fileCount} files, ${formatBytes(pulled.totalBytes)}, version ${pulled.versionId})`)}`
7070
7907
  );
7071
7908
  ctx.out.data(
7072
7909
  pulled,
7073
- () => chalk11.dim(
7910
+ () => chalk12.dim(
7074
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.`
7075
7912
  )
7076
7913
  );
@@ -7078,87 +7915,7 @@ async function runPull(ctx, name, dir, opts) {
7078
7915
 
7079
7916
  // src/commands/setup.ts
7080
7917
  import { intro, outro, spinner } from "@clack/prompts";
7081
- import chalk12 from "chalk";
7082
-
7083
- // src/claude.ts
7084
- import { execFile as execFile2 } from "node:child_process";
7085
- import { access, mkdtemp as mkdtemp2, writeFile as writeFile5 } from "node:fs/promises";
7086
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
7087
- import { join as join9 } from "node:path";
7088
- var MARKETPLACE_SOURCE = REPO_SLUG;
7089
- var MARKETPLACE_NAME = "fling";
7090
- var PLUGIN_NAME = "fling";
7091
- var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
7092
- var MCPB_PATH = "/downloads/fling.mcpb";
7093
- var runClaude = (args) => new Promise((resolve7) => {
7094
- execFile2(
7095
- "claude",
7096
- [...args],
7097
- { timeout: 12e4, maxBuffer: 16 * 1024 * 1024, shell: process.platform === "win32" },
7098
- (error, stdout, stderr) => {
7099
- const missing = error?.code === "ENOENT";
7100
- const code = !error ? 0 : missing ? -1 : typeof error.code === "number" ? error.code : 1;
7101
- resolve7({ code, stdout: String(stdout), stderr: String(stderr) });
7102
- }
7103
- );
7104
- });
7105
- async function claudeCodeStatus(run) {
7106
- const version = await run(["--version"]);
7107
- if (version.code !== 0) return { claude: "missing" };
7108
- const claude = version.stdout.trim().split(/\s+/)[0] ?? "unknown";
7109
- const list = await run(["plugin", "list", "--json"]);
7110
- let plugins = [];
7111
- try {
7112
- plugins = JSON.parse(list.stdout);
7113
- } catch {
7114
- }
7115
- const fling = Array.isArray(plugins) ? plugins.find((p) => p.id === PLUGIN_ID) : void 0;
7116
- if (!fling) return { claude, installed: false };
7117
- return {
7118
- claude,
7119
- installed: true,
7120
- version: fling.version,
7121
- scope: fling.scope,
7122
- enabled: fling.enabled
7123
- };
7124
- }
7125
- async function installPlugin(run, scope) {
7126
- check(
7127
- await run(["plugin", "install", PLUGIN_NAME, "--marketplace", MARKETPLACE_SOURCE, "-s", scope]),
7128
- "install the Fling plugin"
7129
- );
7130
- }
7131
- async function updatePlugin(run, scope) {
7132
- check(await run(["plugin", "marketplace", "update", MARKETPLACE_NAME]), "update the marketplace");
7133
- check(
7134
- await run(["plugin", "update", PLUGIN_ID, ...scope ? ["-s", scope] : []]),
7135
- "update the Fling plugin"
7136
- );
7137
- }
7138
- function check(result, what) {
7139
- if (result.code === 0) return;
7140
- const detail = (result.stderr || result.stdout).trim().split("\n").at(-1);
7141
- throw new Error(`Could not ${what}${detail ? `: ${detail}` : ""}`);
7142
- }
7143
- async function findClaudeDesktop(platform = process.platform, env = process.env, exists = (path3) => access(path3).then(
7144
- () => true,
7145
- () => false
7146
- )) {
7147
- const candidates = platform === "darwin" ? ["/Applications/Claude.app", join9(homedir3(), "Applications", "Claude.app")] : platform === "win32" && env.LOCALAPPDATA ? [join9(env.LOCALAPPDATA, "AnthropicClaude"), join9(env.LOCALAPPDATA, "Programs", "Claude")] : [];
7148
- for (const path3 of candidates) if (await exists(path3)) return path3;
7149
- return null;
7150
- }
7151
- async function downloadExtension(serverUrl, fetch2) {
7152
- const url = new URL(MCPB_PATH, serverUrl).toString();
7153
- const response = await fetch2(url);
7154
- if (!response.ok) throw new Error(`Could not download ${url} (HTTP ${response.status})`);
7155
- const dir = await mkdtemp2(join9(tmpdir2(), "fling-mcpb-"));
7156
- const file = join9(dir, "fling.mcpb");
7157
- await writeFile5(file, Buffer.from(await response.arrayBuffer()));
7158
- return file;
7159
- }
7160
-
7161
- // src/commands/setup.ts
7918
+ import chalk13 from "chalk";
7162
7919
  var SCOPES = ["user", "project", "local"];
7163
7920
  async function runSetup(baseCtx, opts, deps = {}) {
7164
7921
  const scope = opts.scope ?? "user";
@@ -7171,14 +7928,14 @@ async function runSetup(baseCtx, opts, deps = {}) {
7171
7928
  ...baseCtx,
7172
7929
  log: createLogger({
7173
7930
  debug: baseCtx.config.debug,
7174
- write: (line) => process.stderr.write(`${chalk12.gray("\u2502")} ${line}
7931
+ write: (line) => process.stderr.write(`${chalk13.gray("\u2502")} ${line}
7175
7932
  `)
7176
7933
  })
7177
7934
  } : baseCtx;
7178
7935
  if (fancy) {
7179
7936
  process.stderr.write(`${logo()}
7180
7937
  `);
7181
- intro(chalk12.bold(" fling setup "), { output: process.stderr });
7938
+ intro(chalk13.bold(" fling setup "), { output: process.stderr });
7182
7939
  }
7183
7940
  const login = await stepLogin(ctx, opts.url);
7184
7941
  const results = {
@@ -7192,7 +7949,7 @@ async function runSetup(baseCtx, opts, deps = {}) {
7192
7949
  const failed = Object.values(results).includes("failed");
7193
7950
  if (fancy)
7194
7951
  outro(
7195
- failed ? chalk12.yellow("Setup finished with problems \u2014 see above, then run `fling setup` again.") : `You're set. Try ${chalk12.bold(`${ctx.binName} deploy ./dist`)}, or ask Claude to \u201Cfling this\u201D.`,
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.`,
7196
7953
  { output: process.stderr }
7197
7954
  );
7198
7955
  ctx.out.data(results, () => "");
@@ -7215,7 +7972,10 @@ async function signIn(ctx, serverUrl) {
7215
7972
  try {
7216
7973
  const me = await whoami(requireClient(ctx));
7217
7974
  if (ctx.config.remembered) await ctx.store.setCurrent(serverUrl);
7218
- ctx.log.success(`Signed in as ${chalk12.bold(me.name)} ${chalk12.dim(`<${me.email}>`)}`);
7975
+ ctx.log.success(`Signed in as ${chalk13.bold(me.name)} ${chalk13.dim(`<${me.email}>`)}`);
7976
+ if (ctx.config.remembered)
7977
+ await fetchInstance(serverUrl, ctx.fetch).then((instance) => ctx.store.putServer(serverUrl, { instance }, { makeCurrent: false })).catch(() => {
7978
+ });
7219
7979
  return "already";
7220
7980
  } catch (error) {
7221
7981
  if (exitCodeFor(error) !== 2) {
@@ -7236,48 +7996,80 @@ async function signIn(ctx, serverUrl) {
7236
7996
  async function stepClaudeCode(ctx, run, scope, ask, fancy) {
7237
7997
  const status = await claudeCodeStatus(run);
7238
7998
  if (status.claude === "missing") {
7239
- ctx.log.info(`Claude Code not found ${chalk12.dim("(no `claude` on your PATH) \u2014 skipped")}`);
7999
+ ctx.log.info(`Claude Code not found ${chalk13.dim("(no `claude` on your PATH) \u2014 skipped")}`);
7240
8000
  ctx.log.hint(
7241
8001
  `Once it is installed, run \`${ctx.binName} setup\` again, or in Claude Code: /plugin marketplace add ${MARKETPLACE_SOURCE} then /plugin install ${PLUGIN_ID}`
7242
8002
  );
7243
8003
  return "missing";
7244
8004
  }
7245
- const installed = "installed" in status && status.installed;
7246
- 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?";
7247
- if (ask && !await ctx.confirm(question)) {
7248
- ctx.log.info(chalk12.dim("Claude Code plugin: skipped"));
7249
- return installed ? "already" : "skipped";
8005
+ if ("installed" in status && status.installed) return refreshPlugin(ctx, run, status, fancy);
8006
+ 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"));
8008
+ return "skipped";
7250
8009
  }
7251
8010
  const s = fancy ? spinner({ output: process.stderr }) : null;
7252
- s?.start(installed ? "Updating the Claude Code plugin" : "Installing the Claude Code plugin");
8011
+ s?.start("Installing the Claude Code plugin");
7253
8012
  try {
7254
- if (installed) await updatePlugin(run, status.scope);
7255
- else await installPlugin(run, scope);
8013
+ await installPlugin(run, scope);
7256
8014
  } catch (error) {
7257
8015
  const message = error instanceof Error ? error.message : String(error);
7258
8016
  if (s) s.error(message);
7259
8017
  else ctx.log.error(message);
7260
8018
  return "failed";
7261
8019
  }
7262
- const done = installed ? `Claude Code plugin ${PLUGIN_NAME} is up to date` : `Claude Code plugin ${PLUGIN_NAME} installed ${chalk12.dim(`(${scope} scope)`)}`;
8020
+ const done = `Claude Code plugin ${PLUGIN_NAME} installed ${chalk13.dim(`(${scope} scope)`)}`;
7263
8021
  if (s) s.stop(done);
7264
8022
  else ctx.log.success(done);
7265
8023
  ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick it up.");
7266
- return installed ? "updated" : "done";
8024
+ return "done";
8025
+ }
8026
+ async function refreshPlugin(ctx, run, before, fancy) {
8027
+ const s = fancy ? spinner({ output: process.stderr }) : null;
8028
+ s?.start("Updating the Claude Code plugin");
8029
+ try {
8030
+ await updatePlugin(run, before.scope);
8031
+ } catch (error) {
8032
+ 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));
8034
+ else ctx.log.warn(message);
8035
+ return "already";
8036
+ }
8037
+ const after = await claudeCodeStatus(run).catch(() => null);
8038
+ const version = after && "installed" in after && after.installed ? after.version : void 0;
8039
+ 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}` : ""}`;
8041
+ if (s) s.stop(done);
8042
+ else ctx.log.success(done);
8043
+ if (unchanged) return "already";
8044
+ ctx.log.hint("Restart Claude Code (or run /reload-plugins) to pick up the new version.");
8045
+ return "updated";
7267
8046
  }
7268
8047
  async function stepClaudeDesktop(ctx, deps, ask, fancy) {
7269
8048
  const app = await (deps.findClaudeDesktop ?? findClaudeDesktop)();
7270
8049
  if (!app) return "missing";
7271
8050
  if (ask && !await ctx.confirm("Add the Fling extension to Claude Desktop too?")) {
7272
- ctx.log.info(chalk12.dim("Claude Desktop extension: skipped"));
8051
+ ctx.log.info(chalk13.dim("Claude Desktop extension: skipped"));
7273
8052
  return "skipped";
7274
8053
  }
7275
8054
  const s = fancy ? spinner({ output: process.stderr }) : null;
7276
8055
  s?.start("Downloading the Claude Desktop extension");
8056
+ const download2 = deps.downloadExtension ?? ((url) => downloadExtension(url, ctx.fetch));
7277
8057
  try {
7278
- const file = await (deps.downloadExtension ?? ((serverUrl) => downloadExtension(serverUrl, ctx.fetch)))(requireServer(ctx));
7279
- await ctx.open(file);
7280
- const done = "Opened fling.mcpb in Claude Desktop \u2014 click Install there to finish";
8058
+ const sources = extensionSources(requireServer(ctx), ctx.config.instance?.release);
8059
+ let got;
8060
+ let lastError;
8061
+ for (const source of sources) {
8062
+ try {
8063
+ got = { file: await download2(source.url), version: source.version };
8064
+ break;
8065
+ } catch (error) {
8066
+ lastError = error;
8067
+ ctx.log.debug(`Could not download ${source.url}: ${String(error)}`);
8068
+ }
8069
+ }
8070
+ if (!got) throw lastError;
8071
+ await ctx.open(got.file);
8072
+ const done = `Opened fling.mcpb${got.version ? ` ${got.version}` : ""} in Claude Desktop \u2014 click Install there to finish (it replaces an older version)`;
7281
8073
  if (s) s.stop(done);
7282
8074
  else ctx.log.success(done);
7283
8075
  return "done";
@@ -7290,7 +8082,7 @@ async function stepClaudeDesktop(ctx, deps, ask, fancy) {
7290
8082
  }
7291
8083
 
7292
8084
  // src/commands/sites.ts
7293
- import chalk13 from "chalk";
8085
+ import chalk14 from "chalk";
7294
8086
  async function runList(ctx, opts) {
7295
8087
  const sites = await listSites(requireClient(ctx), opts);
7296
8088
  ctx.out.data(
@@ -7326,12 +8118,12 @@ async function runListShared(ctx, opts) {
7326
8118
  );
7327
8119
  }
7328
8120
  var accessLabel = (site) => ACCESS_LABELS[siteAccessLevel(site)];
7329
- var row = (label, value) => `${chalk13.bold(`${label}:`.padEnd(14))}${value}`;
8121
+ var row = (label, value) => `${chalk14.bold(`${label}:`.padEnd(14))}${value}`;
7330
8122
  function describeSite(site) {
7331
8123
  return [
7332
8124
  row("Name", site.name),
7333
8125
  row("URL", site.url),
7334
- row("Access", accessLabel(site) + (site.archived ? chalk13.yellow(" (archived)") : "")),
8126
+ row("Access", accessLabel(site) + (site.archived ? chalk14.yellow(" (archived)") : "")),
7335
8127
  row("SPA", site.spa ? "yes" : "no"),
7336
8128
  row("Share link", site.shareUrl ?? "-"),
7337
8129
  row("Version", site.currentVersionId ?? "-"),
@@ -7360,15 +8152,15 @@ function filesRow(site) {
7360
8152
  var membersOf = (ctx) => ctx.config.instance ? privateAudience(ctx.config.instance) : "anyone signed in to this Fling";
7361
8153
  async function runInfo(ctx, name) {
7362
8154
  const site = await getSite(requireClient(ctx), name);
7363
- const access2 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${chalk13.bold("Who:".padEnd(14))}${describeAudience(site, membersOf(ctx))}`] : [];
8155
+ const access3 = site.audience && audiencesEnabled(ctx.config.instance) ? [`${chalk14.bold("Who:".padEnd(14))}${describeAudience(site, membersOf(ctx))}`] : [];
7364
8156
  ctx.out.data(
7365
8157
  site,
7366
8158
  () => [
7367
8159
  ...describeSite(site),
7368
- ...access2,
8160
+ ...access3,
7369
8161
  ...rolesRows(site),
7370
8162
  "",
7371
- chalk13.dim(
8163
+ chalk14.dim(
7372
8164
  `${site.versions.length} version(s) \u2014 \`fling versions ${name}\`; files \u2014 \`fling files ${name}\``
7373
8165
  )
7374
8166
  ].join("\n")
@@ -7396,7 +8188,7 @@ async function runFiles(ctx, name, versionId) {
7396
8188
  }
7397
8189
  ctx.out.data(result, () => {
7398
8190
  const n = result.files.length;
7399
- const header = chalk13.dim(
8191
+ const header = chalk14.dim(
7400
8192
  `${result.versionId}${result.current ? " (current)" : " (not served)"} \u2014 ${n} file${n === 1 ? "" : "s"}, ${formatBytes(result.totalBytes)}${result.generatedIndex ? ", generated index.html" : ""}`
7401
8193
  );
7402
8194
  const table = renderTable(result.files, [
@@ -7432,7 +8224,7 @@ async function runShare(ctx, name, opts) {
7432
8224
  ctx.log.success("Share link created \u2014 anyone with it can view the site");
7433
8225
  ctx.out.data(
7434
8226
  ready === void 0 ? result : { ...result, ready },
7435
- () => result.shareUrl ? result.shareUrl : chalk13.dim("(no share link)")
8227
+ () => result.shareUrl ? result.shareUrl : chalk14.dim("(no share link)")
7436
8228
  );
7437
8229
  }
7438
8230
  async function runVisibility(ctx, name, visibility) {
@@ -7544,15 +8336,15 @@ async function runRemove(ctx, name, opts) {
7544
8336
  }
7545
8337
 
7546
8338
  // src/mcp.ts
7547
- import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
8339
+ import { mkdir as mkdir5, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
7548
8340
  import { tmpdir as tmpdir3 } from "node:os";
7549
- import { basename as basename3, dirname as dirname4, join as join10, resolve as resolvePath } from "node:path";
8341
+ import { basename as basename3, dirname as dirname4, join as join12, resolve as resolvePath } from "node:path";
7550
8342
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7551
8343
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7552
8344
  import { z as z20 } from "zod";
7553
8345
 
7554
8346
  // src/locate.ts
7555
- import { lstat, readdir as readdir3, readFile as readFile9, realpath as realpath3, stat as stat7 } from "node:fs/promises";
8347
+ import { lstat, readdir as readdir3, readFile as readFile11, realpath as realpath4, stat as stat8 } from "node:fs/promises";
7556
8348
  import { posix, win32 } from "node:path";
7557
8349
  import { fileURLToPath } from "node:url";
7558
8350
  var DOWNLOADS_DIR_ENV = "FLING_DOWNLOADS_DIR";
@@ -7565,7 +8357,7 @@ var SANDBOX_PATH_RE = /^\/(mnt\/user-data|mnt\/data|sessions|home\/claude|tmp\/o
7565
8357
  var nodeFs = {
7566
8358
  async stat(path3) {
7567
8359
  try {
7568
- const s = await stat7(path3);
8360
+ const s = await stat8(path3);
7569
8361
  return { isFile: s.isFile(), isDirectory: s.isDirectory(), mtimeMs: s.mtimeMs, size: s.size };
7570
8362
  } catch {
7571
8363
  return null;
@@ -7577,7 +8369,7 @@ var nodeFs = {
7577
8369
  const out = [];
7578
8370
  for (const e of entries) {
7579
8371
  if (e.isSymbolicLink()) {
7580
- const s = await stat7(`${path3}/${e.name}`).catch(() => null);
8372
+ const s = await stat8(`${path3}/${e.name}`).catch(() => null);
7581
8373
  out.push({ name: e.name, isFile: Boolean(s?.isFile()), isDirectory: false });
7582
8374
  } else out.push({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() });
7583
8375
  }
@@ -7587,12 +8379,12 @@ var nodeFs = {
7587
8379
  }
7588
8380
  },
7589
8381
  async realpath(path3) {
7590
- return realpath3(path3).catch(() => null);
8382
+ return realpath4(path3).catch(() => null);
7591
8383
  },
7592
8384
  async readText(path3) {
7593
8385
  try {
7594
8386
  if (!(await lstat(path3)).isFile()) return null;
7595
- return await readFile9(path3, "utf8");
8387
+ return await readFile11(path3, "utf8");
7596
8388
  } catch {
7597
8389
  return null;
7598
8390
  }
@@ -7964,6 +8756,7 @@ var LOGIN_HINT = `Not logged in to Fling. Call fling_login (it opens the browser
7964
8756
  var NO_SERVER_HINT = `No Fling server is set up on this computer. Ask the user for their Fling's address, then call fling_login with it as \`server\` (or ask them to run \`${NPX_COMMAND} login <url>\` in a terminal; in Claude Code: \`! ${NPX_COMMAND} login <url>\`), then try again.`;
7965
8757
  var mcpLocalInstructions = (w, opts = {}) => [
7966
8758
  `${w.name} publishes static sites (HTML/CSS/JS, images, documents) at ${w.sitesPattern}. This server runs on the user's computer.`,
8759
+ ...opts.newerRelease ? [mcpUpdateNote(opts.newerRelease)] : [],
7967
8760
  ...[serversInstructions(opts.servers ?? [])].filter((p) => p !== void 0),
7968
8761
  "Content you created in this conversation (an HTML page, a report, a small app): call fling_publish with every file inline (text as utf8, binary as base64). Never pass file paths from your sandbox (e.g. /mnt/user-data/outputs/\u2026, /sessions/\u2026) \u2014 this computer cannot see them \u2014 and never curl or upload from the sandbox.",
7969
8762
  'A file or folder the user has on this computer (a downloaded .html, a .zip, a build folder): call fling_deploy with its path, or just its file name (e.g. "Pitch Deck.html") \u2014 it is looked up in Downloads, Desktop, Documents/Claude and Claude Desktop output folders. Use fling_find_files to see recent candidates when unsure which file the user means.',
@@ -7979,6 +8772,14 @@ var mcpLocalInstructions = (w, opts = {}) => [
7979
8772
  ] : [],
7980
8773
  ...opts.notifications ? [NOTIFICATIONS_NOTE(w)] : []
7981
8774
  ].join("\n\n");
8775
+ var PUBLISHING_TOOLS = /* @__PURE__ */ new Set(["fling_publish", "fling_deploy"]);
8776
+ function newestRelease(servers) {
8777
+ const newer = servers.map((s) => newerRelease(s.instance?.release)).filter((r) => r !== null);
8778
+ return newer.reduce(
8779
+ (best, r) => best && newerRelease(r, best) === null ? best : r,
8780
+ null
8781
+ );
8782
+ }
7982
8783
  var onFling = (url, instance) => `
7983
8784
  Fling: ${serverLabel(url)}${instance ? ` (${instance.name})` : ""}`;
7984
8785
  function createMcpServer(deps = {}) {
@@ -8025,7 +8826,12 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
8025
8826
  const url = config.serverUrl;
8026
8827
  let instance;
8027
8828
  return {
8028
- client: createApiClient({ serverUrl: url, apiKey: config.apiKey, fetch: fetchImpl }),
8829
+ client: createApiClient({
8830
+ serverUrl: url,
8831
+ apiKey: config.apiKey,
8832
+ fetch: fetchImpl,
8833
+ ...releaseTracking(store, config)
8834
+ }),
8029
8835
  url,
8030
8836
  config,
8031
8837
  instance: () => {
@@ -8086,10 +8892,24 @@ Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
8086
8892
  return void 0;
8087
8893
  }
8088
8894
  }
8895
+ let updateNoted = false;
8896
+ async function updateNoteFor(t2) {
8897
+ if (updateNoted) return "";
8898
+ const stored = await store.resolve({ serverUrl: t2.url }).catch(() => null);
8899
+ const release = newerRelease(
8900
+ (stored?.instance ?? await t2.instance().catch(() => null))?.release
8901
+ );
8902
+ if (!release) return "";
8903
+ updateNoted = true;
8904
+ return `
8905
+ ${mcpUpdateNote(release)}`;
8906
+ }
8089
8907
  async function run(tool2, server2, op) {
8090
8908
  try {
8091
- const { summary, data } = await op(await target(server2, tool2));
8092
- return textResult(`${summary}
8909
+ const t2 = await target(server2, tool2);
8910
+ const { summary, data } = await op(t2);
8911
+ const update = PUBLISHING_TOOLS.has(tool2) ? await updateNoteFor(t2) : "";
8912
+ return textResult(`${summary}${update}
8093
8913
 
8094
8914
  ${JSON.stringify(data, null, 2)}`);
8095
8915
  } catch (error) {
@@ -8205,12 +9025,12 @@ ${describeFormsSummary(formsSummary, responsesPage)}` : "";
8205
9025
  ${shareForFeedback(review, instance?.name ?? "Fling", formsSummary !== null)}
8206
9026
  Site: ${url} (${level})${formsText}` : `${what}: ${url} (${level})${listing2}${formsText}`;
8207
9027
  const who = describeSiteAccess(result.site, await membersOn(t2));
8208
- const access3 = result.first ? `
9028
+ const access4 = result.first ? `
8209
9029
  Who can open it: ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : opts.access ? `
8210
9030
  Its access is unchanged (deploying never changes it for an existing site): ${who} ${accessChangeHint("mcp", result.name, { invites: instance?.capabilities.invites === true })}` : "";
8211
9031
  return {
8212
9032
  kind: "deployed",
8213
- summary: `${head}${access3}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
9033
+ summary: `${head}${access4}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
8214
9034
  To check exactly which files the site has, call fling_files with name "${result.name}".` + (formsHint2 ? `
8215
9035
  ${formsHint2}` : ""),
8216
9036
  data: {
@@ -8247,7 +9067,8 @@ ${formsHint2}` : ""),
8247
9067
  comments: servers.some((s) => commentsEnabled(s.instance)),
8248
9068
  forms: servers.some((s) => formsEnabled(s.instance)),
8249
9069
  notifications: servers.some((s) => notificationsEnabled(s.instance)),
8250
- servers
9070
+ servers,
9071
+ newerRelease: newestRelease(servers)
8251
9072
  })
8252
9073
  }
8253
9074
  );
@@ -8255,7 +9076,7 @@ ${formsHint2}` : ""),
8255
9076
  listHtml: listHtml2,
8256
9077
  spa: spa2,
8257
9078
  public: isPublic,
8258
- access: access2,
9079
+ access: access3,
8259
9080
  emails: emails2,
8260
9081
  groups: groups2,
8261
9082
  forms: formsArg
@@ -8276,7 +9097,7 @@ ${formsHint2}` : ""),
8276
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'
8277
9098
  ),
8278
9099
  spa: spa2,
8279
- access: access2,
9100
+ access: access3,
8280
9101
  emails: emails2,
8281
9102
  groups: groups2,
8282
9103
  public: isPublic,
@@ -8366,7 +9187,7 @@ ${formsHint2}` : ""),
8366
9187
  `Invalid path "${args.path}": use a folder of the site, like "30-10-26" (no dotfiles)`
8367
9188
  );
8368
9189
  }
8369
- const dir = args.dir === void 0 ? join10(locate.home, "Documents", "Claude", "fling", args.name) : resolvePath(cwd, args.dir);
9190
+ const dir = args.dir === void 0 ? join12(locate.home, "Documents", "Claude", "fling", args.name) : resolvePath(cwd, args.dir);
8370
9191
  const pulled = await pullSite(
8371
9192
  { client: t2.client, fetch: fetchImpl },
8372
9193
  {
@@ -8472,7 +9293,7 @@ ${JSON.stringify(data, null, 2)}`);
8472
9293
  }
8473
9294
  if (args.files.length === 0 && !args.remove?.length)
8474
9295
  throw new CliError("No files to publish");
8475
- const tmp = await mkdtemp3(join10(tmpdir3(), "fling-publish-"));
9296
+ const tmp = await mkdtemp3(join12(tmpdir3(), "fling-publish-"));
8476
9297
  try {
8477
9298
  const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
8478
9299
  return await deployFolder(t2, tmp, {
@@ -8621,6 +9442,29 @@ ${known.join("\n")}` : ""
8621
9442
  }
8622
9443
  }
8623
9444
  );
9445
+ server.registerTool(
9446
+ "fling_doctor",
9447
+ {
9448
+ title: "Check this computer's Fling setup",
9449
+ description: "Diagnose Fling on the user's computer, read-only: the CLI's version (and the latest on npm), Node.js, the config, each Fling set up here (reachable, its release, whether the sign-in still works, its features), the Claude Code plugin and the Claude Desktop extension. One line per check (\u2714 ok, \u25B2 warning, \u2716 problem), each warning and problem with its fix. Call it when something about Fling does not work (sign-in, tools missing, versions), then tell the user the fixes; never run them for the user without asking.",
9450
+ inputSchema: {},
9451
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
9452
+ },
9453
+ async () => {
9454
+ try {
9455
+ const checks = await runChecks({
9456
+ ...systemDoctorDeps(store, fetchImpl, runClaude, () => findClaudeDesktop()),
9457
+ ...deps.doctor
9458
+ });
9459
+ const ok = doctorExitCode(checks) === 0;
9460
+ return textResult(`${describeChecks(checks)}
9461
+
9462
+ ${JSON.stringify({ ok, checks }, null, 2)}`);
9463
+ } catch (error) {
9464
+ return failure(error);
9465
+ }
9466
+ }
9467
+ );
8624
9468
  server.registerTool(
8625
9469
  "fling_servers",
8626
9470
  {
@@ -8697,7 +9541,7 @@ ${lines}` : ""}`,
8697
9541
  serverTool(t.info),
8698
9542
  (args) => run(t.info.name, args.server, async (target2) => {
8699
9543
  const site = await getSite(target2.client, args.name);
8700
- const access3 = site.audience ? `
9544
+ const access4 = site.audience ? `
8701
9545
  ${describeAudience(site, await membersOn(target2))}` : "";
8702
9546
  const current = site.versions.find((v) => v.id === site.currentVersionId);
8703
9547
  const by = current?.publishedBy ? ` by ${current.publishedBy}` : "";
@@ -8706,7 +9550,7 @@ ${describeAudience(site, await membersOn(target2))}` : "";
8706
9550
  Current version published${by}${to}` : "";
8707
9551
  const roles = site.owners ? `
8708
9552
  ${describeRoles(site)}` : "";
8709
- return { summary: `${site.name}: ${site.url}${access3}${roles}${published}`, data: site };
9553
+ return { summary: `${site.name}: ${site.url}${access4}${roles}${published}`, data: site };
8710
9554
  })
8711
9555
  );
8712
9556
  server.registerTool(
@@ -9204,9 +10048,9 @@ async function writeInlineFiles(dir, files) {
9204
10048
  total += bytes.length;
9205
10049
  if (total > MAX_DEPLOY_BYTES)
9206
10050
  throw new CliError(`The site is larger than ${formatBytes(MAX_DEPLOY_BYTES)}`);
9207
- const target = join10(dir, ...path3.split("/"));
9208
- await mkdir4(dirname4(target), { recursive: true });
9209
- await writeFile6(target, bytes);
10051
+ const target = join12(dir, ...path3.split("/"));
10052
+ await mkdir5(dirname4(target), { recursive: true });
10053
+ await writeFile7(target, bytes);
9210
10054
  }
9211
10055
  if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
9212
10056
  return { skippedDotfiles };
@@ -9250,10 +10094,10 @@ async function runMcpServer(deps = {}) {
9250
10094
  }
9251
10095
 
9252
10096
  // src/route.ts
9253
- import { stat as stat8 } from "node:fs/promises";
10097
+ import { stat as stat9 } from "node:fs/promises";
9254
10098
  import { resolve as resolve6 } from "node:path";
9255
10099
  var PASS_THROUGH = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);
9256
- var pathExists = (path3) => stat8(resolve6(path3)).then(
10100
+ var pathExists = (path3) => stat9(resolve6(path3)).then(
9257
10101
  () => true,
9258
10102
  () => false
9259
10103
  );
@@ -9304,18 +10148,18 @@ program.name(BIN_NAME).description("Fling a folder of static files at Fling and
9304
10148
  `server for this command: a URL, or the host of one you signed in to (default: $${ENV.url}, else the current one)`
9305
10149
  ).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
9306
10150
  sortSubcommands: false,
9307
- styleTitle: (title) => chalk14.bold(brand.primary(title)),
9308
- styleCommandText: (text2) => chalk14.bold(text2),
10151
+ styleTitle: (title) => chalk15.bold(brand.primary(title)),
10152
+ styleCommandText: (text2) => chalk15.bold(text2),
9309
10153
  styleSubcommandText: (text2) => brand.accent(text2),
9310
10154
  styleOptionText: (text2) => brand.accent(text2),
9311
- styleArgumentText: (text2) => chalk14.dim(text2)
10155
+ styleArgumentText: (text2) => chalk15.dim(text2)
9312
10156
  }).addHelpText(
9313
10157
  "before",
9314
10158
  () => process.stdout.isTTY ? logo({ columns: process.stdout.columns }) : ""
9315
10159
  ).addHelpText(
9316
10160
  "after",
9317
10161
  () => `
9318
- ${chalk14.bold(brand.primary("Examples:"))}
10162
+ ${chalk15.bold(brand.primary("Examples:"))}
9319
10163
  $ ${BIN_NAME} login https://fling.example.com sign in to your Fling (once; there is no default)
9320
10164
  $ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
9321
10165
  $ ${BIN_NAME} use list the Flings you signed in to; \`use <url>\` switches
@@ -9326,7 +10170,7 @@ ${chalk14.bold(brand.primary("Examples:"))}
9326
10170
  $ ${BIN_NAME} site.zip a zip of a site
9327
10171
  $ ${BIN_NAME} share my-app public link for people who can't sign in
9328
10172
 
9329
- ${chalk14.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
10173
+ ${chalk15.dim(`Exit codes: 0 ok \xB7 1 error \xB7 2 not logged in \xB7 3 forbidden
9330
10174
  Env: ${ENV.apiKey} \xB7 ${ENV.url} \xB7 ${ENV.configDir} \xB7 ${ENV.debug}`)}`
9331
10175
  );
9332
10176
  function action(handler) {
@@ -9341,6 +10185,7 @@ function action(handler) {
9341
10185
  report(error, ctx);
9342
10186
  process.exitCode = exitCodeFor(error);
9343
10187
  }
10188
+ if (ctx) await noticeAfterCommand(ctx, globals.server);
9344
10189
  };
9345
10190
  }
9346
10191
  function report(error, ctx) {
@@ -9407,6 +10252,9 @@ program.command("setup [url]").helpGroup("Setup:").description(
9407
10252
  (ctx, url, opts) => runSetup(ctx, { ...opts, url })
9408
10253
  )
9409
10254
  );
10255
+ program.command("doctor").helpGroup("Setup:").description(
10256
+ "check this machine: the CLI, its config, each Fling, Claude Code and Claude Desktop (changes nothing)"
10257
+ ).action(action((ctx) => runDoctor(ctx)));
9410
10258
  program.command("login [url]").helpGroup("Setup:").description(
9411
10259
  "sign in through the browser to the Fling at url and store an API key; it becomes the current server (default: the current one)"
9412
10260
  ).action(