@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.
- package/README.md +64 -5
- package/dist/cli.js +1023 -175
- 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
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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,
|
|
4941
|
+
async function setAccess(client, name, access3, audience) {
|
|
4899
4942
|
return client.request("PATCH", sitePath2(name), {
|
|
4900
4943
|
schema: SiteWithAudienceSchema,
|
|
4901
|
-
body: { access:
|
|
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/
|
|
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
|
|
6750
|
-
ctx.log.info(`${summary.title} on ${name}${
|
|
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
|
|
7602
|
+
const info = await stat7(folder).catch(() => null);
|
|
6766
7603
|
if (!info?.isDirectory()) throw new CliError(`No such folder: ${dir ?? "."}`);
|
|
6767
|
-
const file =
|
|
6768
|
-
if (!opts.force && await
|
|
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
|
|
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
|
-
() =>
|
|
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
|
|
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() ?
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
7694
|
+
chalk9.bold(`${exported.title} on ${name}`),
|
|
6858
7695
|
"",
|
|
6859
7696
|
table,
|
|
6860
7697
|
"",
|
|
6861
|
-
...progressLines(exported).map((l) =>
|
|
6862
|
-
...page ? [
|
|
6863
|
-
|
|
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
|
|
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
|
-
]) :
|
|
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
|
|
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, "",
|
|
7796
|
+
const lines = [table, "", chalk11.dim("* your choice; the rest are the defaults")];
|
|
6960
7797
|
if (settings.muted) {
|
|
6961
7798
|
lines.unshift(
|
|
6962
|
-
|
|
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
|
-
]) :
|
|
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
|
|
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} ${
|
|
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
|
-
() =>
|
|
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
|
|
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(`${
|
|
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(
|
|
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 ?
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
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(
|
|
8011
|
+
s?.start("Installing the Claude Code plugin");
|
|
7253
8012
|
try {
|
|
7254
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
7279
|
-
|
|
7280
|
-
|
|
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
|
|
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) => `${
|
|
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 ?
|
|
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
|
|
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
|
-
...
|
|
8160
|
+
...access3,
|
|
7369
8161
|
...rolesRows(site),
|
|
7370
8162
|
"",
|
|
7371
|
-
|
|
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 =
|
|
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 :
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
8092
|
-
|
|
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
|
|
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}${
|
|
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:
|
|
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:
|
|
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 ?
|
|
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(
|
|
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
|
|
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}${
|
|
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 =
|
|
9208
|
-
await
|
|
9209
|
-
await
|
|
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
|
|
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) =>
|
|
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) =>
|
|
9308
|
-
styleCommandText: (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) =>
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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(
|