@nearform/fling 0.3.0 → 0.4.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/CHANGELOG.md +17 -0
- package/README.md +27 -6
- package/dist/cli.js +260 -69
- package/dist/cli.js.map +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
- **Several Flings.** The CLI remembers every Fling you sign in to, each with its own API key, and
|
|
8
|
+
uses the current one. `fling login <url>` and `fling setup <url>` add a Fling (or refresh its
|
|
9
|
+
login) and make it current without signing you out of the others. `fling use` lists them, with
|
|
10
|
+
each one's name and your email; `fling use <url>` switches (a full address, its host or the start
|
|
11
|
+
of it). `fling logout <url>` signs out of another Fling and forgets it; a bare `fling logout`
|
|
12
|
+
still removes the current Fling's key and keeps the Fling. `fling whoami` shows which Fling is
|
|
13
|
+
current.
|
|
14
|
+
- `--server` and `FLING_URL` pick a remembered Fling for one command and use its stored key (an
|
|
15
|
+
address you haven't signed in to has no key; `FLING_API_KEY` still overrides). The local MCP
|
|
16
|
+
server uses the current Fling unless `FLING_URL` (the Claude Desktop extension's **Fling server**)
|
|
17
|
+
names one, and signing in from it makes that Fling current only when no other is.
|
|
18
|
+
- Upgrading: `~/.fling/config.json` from 0.3 is read as-is, as one remembered Fling that is
|
|
19
|
+
current. It is rewritten in the new shape (`current` plus `servers`, keyed by address) the next
|
|
20
|
+
time the CLI saves it, so an older CLI can no longer read it after that.
|
|
21
|
+
|
|
5
22
|
## 0.3.0
|
|
6
23
|
|
|
7
24
|
**Breaking: no default server.** The CLI works with any Fling deployment, so it no longer has one
|
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ This does everything a new machine needs:
|
|
|
35
35
|
1. **Signs you in.** The CLI reads the server's `/.well-known/fling` (its name, where its sites
|
|
36
36
|
live, how people sign in) and remembers it. Your browser opens for sign-in and an API key is
|
|
37
37
|
stored in `~/.fling/config.json` (mode 0600). This is skipped if you're already signed in.
|
|
38
|
+
The server becomes your current one; logins to other Flings are kept.
|
|
38
39
|
2. **Adds Fling to Claude Code.** If `claude` is on your PATH, setup installs the Fling plugin
|
|
39
40
|
(skill + MCP server) from the `nearform/fling` marketplace, or updates it if it's already
|
|
40
41
|
installed. `--scope project|local` installs it for this project only (default `user`).
|
|
@@ -45,8 +46,25 @@ On a terminal it asks before steps 2 and 3. With `--yes` it does them without as
|
|
|
45
46
|
|
|
46
47
|
To sign in only, use `fling login <url>` (the URL is remembered: later `fling login` needs none).
|
|
47
48
|
There is no default server: until you log in, commands say to run `fling login <url>`.
|
|
48
|
-
`fling whoami` shows who you are
|
|
49
|
-
|
|
49
|
+
`fling whoami` shows who you are and which server is current. For CI, set `FLING_URL` and
|
|
50
|
+
`FLING_API_KEY` instead.
|
|
51
|
+
|
|
52
|
+
### Several Flings
|
|
53
|
+
|
|
54
|
+
The CLI remembers every Fling you sign in to, each with its own key, and uses the current one.
|
|
55
|
+
`fling login <url>` or `fling setup <url>` adds a Fling (or signs in to it again) and makes it
|
|
56
|
+
current; the others stay signed in.
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
fling use # list them: * marks the current one, with its name and your email
|
|
60
|
+
fling use other.example.com # switch: a full address, its host, or the start of it
|
|
61
|
+
fling --server other.example.com ls # one command against another Fling, with its own key
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`FLING_URL` picks a Fling the same way for every command run with it, and `FLING_API_KEY`
|
|
65
|
+
overrides the stored key. `fling logout` removes the current Fling's key and keeps it current (a
|
|
66
|
+
bare `fling login` signs in again); `fling logout <url>` signs out of another Fling and forgets
|
|
67
|
+
it.
|
|
50
68
|
|
|
51
69
|
## Deploy
|
|
52
70
|
|
|
@@ -186,7 +204,7 @@ feedback (`reviewUrl` in `--json`); reviewers sign in to use it.
|
|
|
186
204
|
| `fling uncomment <name> <id> [--yes]` | delete a comment (a thread with its replies): your own, or any on your site |
|
|
187
205
|
|
|
188
206
|
Global options: `--json` prints machine-readable output, and `--server <url>` points one command at
|
|
189
|
-
another server. Exit codes are `0` ok, `1` error, `2` not logged in and `3` forbidden.
|
|
207
|
+
another server (a remembered one brings its key; see [Several Flings](#several-flings)). Exit codes are `0` ok, `1` error, `2` not logged in and `3` forbidden.
|
|
190
208
|
|
|
191
209
|
Environment: `FLING_API_KEY`, `FLING_URL`, `FLING_CONFIG_DIR`, `FLING_DEBUG`, `FLING_DOWNLOADS_DIR`
|
|
192
210
|
(where the MCP server looks for files given by name first), `FLING_NO_WAIT=1` (after a deploy or a new share
|
|
@@ -210,7 +228,8 @@ Or install it by hand in Claude Code:
|
|
|
210
228
|
|
|
211
229
|
Log in once with `fling login <url>` (or `npx @nearform/fling login <url>` if it is not installed).
|
|
212
230
|
The MCP server reuses that login, and its tool descriptions name your Fling and where its sites
|
|
213
|
-
live.
|
|
231
|
+
live. It re-reads the config on every call, so after `fling use <url>` it talks to the new current
|
|
232
|
+
Fling without a restart.
|
|
214
233
|
|
|
215
234
|
The MCP tools:
|
|
216
235
|
|
|
@@ -246,8 +265,10 @@ without uploading anything from Claude's sandbox.
|
|
|
246
265
|
3. Optional settings: **Fling server** (filled in with the Fling you downloaded it from; empty means
|
|
247
266
|
the one the CLI is logged in to) and **Downloads folder** (default `~/Downloads`).
|
|
248
267
|
4. Ask Claude to "fling Pitch Deck.html from my Downloads". If you're not signed in, Claude calls
|
|
249
|
-
`fling_login` and your browser opens. The extension and the CLI share the
|
|
250
|
-
`~/.fling/config.json`.
|
|
268
|
+
`fling_login` and your browser opens. The extension and the CLI share the logins in
|
|
269
|
+
`~/.fling/config.json`. With **Fling server** set, the extension always uses that Fling (and its
|
|
270
|
+
stored key), whichever one the CLI is using; signing in from the extension doesn't change the
|
|
271
|
+
CLI's current Fling.
|
|
251
272
|
|
|
252
273
|
It needs Node.js 20 or later, which Claude Desktop provides, on macOS or Windows.
|
|
253
274
|
|
package/dist/cli.js
CHANGED
|
@@ -1088,7 +1088,7 @@ function exitCodeFor(error) {
|
|
|
1088
1088
|
// package.json
|
|
1089
1089
|
var package_default = {
|
|
1090
1090
|
name: "@nearform/fling",
|
|
1091
|
-
version: "0.
|
|
1091
|
+
version: "0.4.0",
|
|
1092
1092
|
license: "MIT",
|
|
1093
1093
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
1094
1094
|
type: "module",
|
|
@@ -1310,16 +1310,15 @@ var ENV = {
|
|
|
1310
1310
|
/** Where to look for files given by name (the .mcpb extension sets it from its settings). */
|
|
1311
1311
|
downloadsDir: "FLING_DOWNLOADS_DIR"
|
|
1312
1312
|
};
|
|
1313
|
-
var
|
|
1314
|
-
|
|
1315
|
-
/** What `serverUrl` said about itself at login. A copy a newer CLI can't read is just refetched. */
|
|
1313
|
+
var serverEntrySchema = z9.object({
|
|
1314
|
+
/** What the server said about itself at login. A copy this CLI can't read is just refetched. */
|
|
1316
1315
|
instance: InstanceMetadataSchema.optional().catch(void 0),
|
|
1317
|
-
apiKey: z9.string().min(1).optional(),
|
|
1316
|
+
apiKey: z9.string().min(1).optional().catch(void 0),
|
|
1318
1317
|
user: z9.object({
|
|
1319
1318
|
email: z9.string().optional(),
|
|
1320
1319
|
name: z9.string().optional(),
|
|
1321
1320
|
userSlug: z9.string().optional()
|
|
1322
|
-
}).optional()
|
|
1321
|
+
}).optional().catch(void 0)
|
|
1323
1322
|
});
|
|
1324
1323
|
function systemLocateOptions(env = process.env) {
|
|
1325
1324
|
return { cwd: process.cwd(), home: homedir(), platform: process.platform, env };
|
|
@@ -1331,6 +1330,37 @@ function fromEnv(value) {
|
|
|
1331
1330
|
function resolveConfigDir(env = process.env) {
|
|
1332
1331
|
return env[ENV.configDir] ?? join(env.HOME ?? homedir(), CONFIG_DIR_NAME);
|
|
1333
1332
|
}
|
|
1333
|
+
function safeNormalize(input) {
|
|
1334
|
+
if (typeof input !== "string") return void 0;
|
|
1335
|
+
try {
|
|
1336
|
+
return normalizeServerUrl(input);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return void 0;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function isObject(value) {
|
|
1342
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1343
|
+
}
|
|
1344
|
+
function parseConfig(json) {
|
|
1345
|
+
if (!isObject(json)) throw new Error("expected a JSON object");
|
|
1346
|
+
const servers = {};
|
|
1347
|
+
let current;
|
|
1348
|
+
if ("servers" in json) {
|
|
1349
|
+
if (isObject(json.servers)) {
|
|
1350
|
+
for (const [key, value] of Object.entries(json.servers)) {
|
|
1351
|
+
const url = safeNormalize(key);
|
|
1352
|
+
const entry = isObject(value) ? serverEntrySchema.safeParse(value) : void 0;
|
|
1353
|
+
if (url && entry?.success) servers[url] = { ...servers[url], ...entry.data };
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
current = safeNormalize(json.current);
|
|
1357
|
+
} else {
|
|
1358
|
+
current = safeNormalize(json.serverUrl);
|
|
1359
|
+
if (current) servers[current] = serverEntrySchema.parse(json);
|
|
1360
|
+
}
|
|
1361
|
+
if (current && !servers[current]) servers[current] = {};
|
|
1362
|
+
return current ? { current, servers } : { servers };
|
|
1363
|
+
}
|
|
1334
1364
|
function createConfigStore(options = {}) {
|
|
1335
1365
|
const env = options.env ?? process.env;
|
|
1336
1366
|
const dir = options.dir ?? resolveConfigDir(env);
|
|
@@ -1340,7 +1370,7 @@ function createConfigStore(options = {}) {
|
|
|
1340
1370
|
try {
|
|
1341
1371
|
raw = await readFile(file, "utf8");
|
|
1342
1372
|
} catch (error) {
|
|
1343
|
-
if (error.code === "ENOENT") return {};
|
|
1373
|
+
if (error.code === "ENOENT") return { servers: {} };
|
|
1344
1374
|
throw error;
|
|
1345
1375
|
}
|
|
1346
1376
|
let json;
|
|
@@ -1349,57 +1379,115 @@ function createConfigStore(options = {}) {
|
|
|
1349
1379
|
} catch {
|
|
1350
1380
|
throw new Error(`Config file ${file} is not valid JSON`);
|
|
1351
1381
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1382
|
+
try {
|
|
1383
|
+
return parseConfig(json);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
throw new Error(`Config file ${file} is invalid: ${error.message}`);
|
|
1355
1386
|
}
|
|
1356
|
-
return parsed.data;
|
|
1357
1387
|
}
|
|
1358
1388
|
async function save(config) {
|
|
1359
1389
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
1360
1390
|
await chmod(dir, 448).catch(() => {
|
|
1361
1391
|
});
|
|
1362
|
-
|
|
1392
|
+
const body = { current: config.current, servers: config.servers };
|
|
1393
|
+
await writeFile(file, `${JSON.stringify(body, null, 2)}
|
|
1363
1394
|
`, { mode: 384 });
|
|
1364
1395
|
await chmod(file, 384);
|
|
1365
1396
|
}
|
|
1397
|
+
async function edit(change) {
|
|
1398
|
+
const config = await load();
|
|
1399
|
+
change(config);
|
|
1400
|
+
await save(config);
|
|
1401
|
+
return config;
|
|
1402
|
+
}
|
|
1366
1403
|
return {
|
|
1367
1404
|
dir,
|
|
1368
1405
|
file,
|
|
1369
1406
|
load,
|
|
1370
1407
|
save,
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1408
|
+
putServer(serverUrl, patch, opts = {}) {
|
|
1409
|
+
const url = normalizeServerUrl(serverUrl);
|
|
1410
|
+
return edit((config) => {
|
|
1411
|
+
config.servers[url] = { ...config.servers[url], ...patch };
|
|
1412
|
+
if (opts.makeCurrent ?? !config.current) config.current = url;
|
|
1413
|
+
});
|
|
1414
|
+
},
|
|
1415
|
+
async setCurrent(serverUrl) {
|
|
1416
|
+
const url = normalizeServerUrl(serverUrl);
|
|
1417
|
+
await edit((config) => {
|
|
1418
|
+
config.servers[url] ??= {};
|
|
1419
|
+
config.current = url;
|
|
1420
|
+
});
|
|
1375
1421
|
},
|
|
1376
|
-
async clearCredentials() {
|
|
1377
|
-
const
|
|
1378
|
-
await
|
|
1422
|
+
async clearCredentials(serverUrl) {
|
|
1423
|
+
const url = normalizeServerUrl(serverUrl);
|
|
1424
|
+
await edit((config) => {
|
|
1425
|
+
const entry = config.servers[url];
|
|
1426
|
+
if (entry) config.servers[url] = entry.instance ? { instance: entry.instance } : {};
|
|
1427
|
+
});
|
|
1428
|
+
},
|
|
1429
|
+
async removeServer(serverUrl) {
|
|
1430
|
+
const url = normalizeServerUrl(serverUrl);
|
|
1431
|
+
await edit((config) => {
|
|
1432
|
+
delete config.servers[url];
|
|
1433
|
+
if (config.current === url) config.current = void 0;
|
|
1434
|
+
});
|
|
1379
1435
|
},
|
|
1380
1436
|
async resolve(overrides = {}) {
|
|
1381
1437
|
const config = await load();
|
|
1382
1438
|
const envUrl = fromEnv(env[ENV.url]);
|
|
1383
1439
|
const envKey = fromEnv(env[ENV.apiKey]);
|
|
1384
|
-
const [rawUrl, serverUrlSource] = overrides.serverUrl ? [overrides.serverUrl, "flag"] : envUrl ? [envUrl, "env"] : config.
|
|
1385
|
-
const serverUrl = rawUrl === void 0 ? void 0 :
|
|
1386
|
-
const
|
|
1387
|
-
const
|
|
1440
|
+
const [rawUrl, serverUrlSource] = overrides.serverUrl ? [overrides.serverUrl, "flag"] : envUrl ? [envUrl, "env"] : config.current ? [config.current, "config"] : [void 0, "none"];
|
|
1441
|
+
const serverUrl = rawUrl === void 0 ? void 0 : matchServer(Object.keys(config.servers), rawUrl, { loose: true }).url;
|
|
1442
|
+
const entry = serverUrl === void 0 ? void 0 : config.servers[serverUrl];
|
|
1443
|
+
const [apiKey, apiKeySource] = envKey ? [envKey, "env"] : entry?.apiKey ? [entry.apiKey, "config"] : [void 0, "none"];
|
|
1388
1444
|
return {
|
|
1389
1445
|
serverUrl,
|
|
1390
1446
|
serverUrlSource,
|
|
1391
|
-
|
|
1447
|
+
current: config.current,
|
|
1448
|
+
remembered: entry !== void 0,
|
|
1449
|
+
instance: entry?.instance,
|
|
1392
1450
|
apiKey,
|
|
1393
1451
|
apiKeySource,
|
|
1394
|
-
user:
|
|
1452
|
+
user: entry?.user,
|
|
1395
1453
|
debug: Boolean(env[ENV.debug])
|
|
1396
1454
|
};
|
|
1397
1455
|
}
|
|
1398
1456
|
};
|
|
1399
1457
|
}
|
|
1458
|
+
var HAS_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
1459
|
+
function matchServer(remembered, input, options = {}) {
|
|
1460
|
+
const trimmed = input.trim();
|
|
1461
|
+
const normalized = safeNormalize(trimmed);
|
|
1462
|
+
if (normalized && remembered.includes(normalized)) return { url: normalized, remembered: true };
|
|
1463
|
+
if (!HAS_SCHEME.test(trimmed)) {
|
|
1464
|
+
const bare = trimmed.replace(/\/+$/, "").toLowerCase();
|
|
1465
|
+
const parts = remembered.map((url) => {
|
|
1466
|
+
const u = new URL(url);
|
|
1467
|
+
return { url, host: u.host, hostname: u.hostname, address: `${u.host}${u.pathname}` };
|
|
1468
|
+
});
|
|
1469
|
+
const tiers = [
|
|
1470
|
+
parts.filter((p) => p.host === bare || p.address === bare),
|
|
1471
|
+
options.loose && bare ? parts.filter((p) => p.address.startsWith(bare) || p.hostname.endsWith(`.${bare}`)) : []
|
|
1472
|
+
];
|
|
1473
|
+
for (const found of tiers) {
|
|
1474
|
+
const [only] = found;
|
|
1475
|
+
if (only && found.length === 1) return { url: only.url, remembered: true };
|
|
1476
|
+
if (found.length > 1)
|
|
1477
|
+
throw new CliError(`"${input}" matches more than one server you have signed in to`, {
|
|
1478
|
+
hint: `Use the full address: ${found.map((p) => p.url).join(", ")}`
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (!normalized)
|
|
1483
|
+
throw new CliError(`"${input}" is not a server address`, {
|
|
1484
|
+
hint: "Use your Fling's address, e.g. https://fling.example.com."
|
|
1485
|
+
});
|
|
1486
|
+
return { url: normalized, remembered: false };
|
|
1487
|
+
}
|
|
1400
1488
|
function normalizeServerUrl(input) {
|
|
1401
1489
|
const trimmed = input.trim();
|
|
1402
|
-
const withScheme =
|
|
1490
|
+
const withScheme = HAS_SCHEME.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
1403
1491
|
const url = new URL(withScheme);
|
|
1404
1492
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
1405
1493
|
throw new Error(`Not an http(s) URL: ${input}`);
|
|
@@ -1437,7 +1525,8 @@ async function loadInstance(store, config, fetch2) {
|
|
|
1437
1525
|
if (!config.serverUrl) return null;
|
|
1438
1526
|
try {
|
|
1439
1527
|
const instance = await fetchInstance(config.serverUrl, fetch2);
|
|
1440
|
-
if (config.
|
|
1528
|
+
if (config.remembered)
|
|
1529
|
+
await store.putServer(config.serverUrl, { instance }, { makeCurrent: false });
|
|
1441
1530
|
return instance;
|
|
1442
1531
|
} catch {
|
|
1443
1532
|
return null;
|
|
@@ -1469,12 +1558,15 @@ async function loginFlow(options) {
|
|
|
1469
1558
|
const { key } = await callback.result;
|
|
1470
1559
|
const client = createApiClient({ serverUrl, apiKey: key, fetch: options.fetch });
|
|
1471
1560
|
const user = await client.request("GET", "/api/me", { schema: MeSchema });
|
|
1472
|
-
await store.
|
|
1561
|
+
await store.putServer(
|
|
1473
1562
|
serverUrl,
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1563
|
+
{
|
|
1564
|
+
instance,
|
|
1565
|
+
apiKey: key,
|
|
1566
|
+
user: { email: user.email, name: user.name, userSlug: user.userSlug }
|
|
1567
|
+
},
|
|
1568
|
+
{ makeCurrent: options.makeCurrent ?? true }
|
|
1569
|
+
);
|
|
1478
1570
|
const keyPrefix = redactKey(key);
|
|
1479
1571
|
log.success(`Signed in as ${user.email}`);
|
|
1480
1572
|
log.hint(`Server: ${serverUrl}`);
|
|
@@ -1950,45 +2042,120 @@ function commentsEnabled(instance) {
|
|
|
1950
2042
|
}
|
|
1951
2043
|
|
|
1952
2044
|
// src/commands/auth.ts
|
|
1953
|
-
function loginServer(ctx, url) {
|
|
2045
|
+
async function loginServer(ctx, url) {
|
|
1954
2046
|
if (url === void 0) return requireServer(ctx);
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
} catch {
|
|
1958
|
-
throw new CliError(`"${url}" is not a server address`, {
|
|
1959
|
-
hint: `Run \`${ctx.binName} login <url>\` with your Fling's address, e.g. https://fling.example.com.`
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
2047
|
+
const { servers } = await ctx.store.load();
|
|
2048
|
+
return matchServer(Object.keys(servers), url).url;
|
|
1962
2049
|
}
|
|
1963
2050
|
async function runLogin(ctx, url) {
|
|
1964
2051
|
const result = await loginFlow({
|
|
1965
|
-
serverUrl: loginServer(ctx, url),
|
|
2052
|
+
serverUrl: await loginServer(ctx, url),
|
|
1966
2053
|
store: ctx.store,
|
|
1967
2054
|
log: ctx.log,
|
|
1968
2055
|
open: ctx.open,
|
|
1969
|
-
fetch: ctx.fetch
|
|
2056
|
+
fetch: ctx.fetch,
|
|
2057
|
+
makeCurrent: true
|
|
1970
2058
|
});
|
|
1971
2059
|
if (ctx.json) ctx.out.data(result, () => "");
|
|
1972
2060
|
}
|
|
1973
|
-
async function
|
|
2061
|
+
async function runUse(ctx, url) {
|
|
2062
|
+
const config = await ctx.store.load();
|
|
2063
|
+
const urls = Object.keys(config.servers);
|
|
2064
|
+
const overridden = ctx.config.serverUrlSource === "env" || ctx.config.serverUrlSource === "flag" ? ctx.config.serverUrl : void 0;
|
|
2065
|
+
const overrideHint = () => {
|
|
2066
|
+
if (overridden)
|
|
2067
|
+
ctx.log.hint(
|
|
2068
|
+
`${ctx.config.serverUrlSource === "env" ? "FLING_URL" : "--server"} is set: commands run with it use ${overridden}.`
|
|
2069
|
+
);
|
|
2070
|
+
};
|
|
2071
|
+
if (url === void 0) {
|
|
2072
|
+
const servers = urls.map((server) => {
|
|
2073
|
+
const entry2 = config.servers[server] ?? {};
|
|
2074
|
+
return {
|
|
2075
|
+
url: server,
|
|
2076
|
+
current: server === config.current,
|
|
2077
|
+
instance: entry2.instance?.name ?? null,
|
|
2078
|
+
email: entry2.apiKey ? entry2.user?.email ?? null : null,
|
|
2079
|
+
signedIn: Boolean(entry2.apiKey)
|
|
2080
|
+
};
|
|
2081
|
+
});
|
|
2082
|
+
if (servers.length === 0 && !ctx.json) {
|
|
2083
|
+
ctx.log.info("No Fling servers yet");
|
|
2084
|
+
ctx.log.hint(`Run \`${ctx.binName} login <url>\` with your Fling's address.`);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const width = Math.max(...servers.map((s) => s.url.length));
|
|
2088
|
+
ctx.out.data(
|
|
2089
|
+
{ current: config.current ?? null, servers },
|
|
2090
|
+
() => servers.map(
|
|
2091
|
+
(s) => [
|
|
2092
|
+
s.current ? chalk5.bold("*") : " ",
|
|
2093
|
+
s.current ? chalk5.bold(s.url.padEnd(width)) : s.url.padEnd(width),
|
|
2094
|
+
s.instance ?? chalk5.dim("(unknown)"),
|
|
2095
|
+
s.signedIn ? s.email ?? chalk5.dim("(signed in)") : chalk5.dim("(signed out)")
|
|
2096
|
+
].join(" ")
|
|
2097
|
+
).join("\n")
|
|
2098
|
+
);
|
|
2099
|
+
if (!ctx.json) {
|
|
2100
|
+
ctx.log.hint(`* = current. Switch with \`${ctx.binName} use <url>\`.`);
|
|
2101
|
+
overrideHint();
|
|
2102
|
+
}
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const match = matchServer(urls, url, { loose: true });
|
|
2106
|
+
if (!match.remembered)
|
|
2107
|
+
throw new CliError(`Not signed in to ${match.url} on this computer`, {
|
|
2108
|
+
hint: `Run \`${ctx.binName} login ${match.url}\` to add it, or \`${ctx.binName} use\` to list your servers.`
|
|
2109
|
+
});
|
|
2110
|
+
await ctx.store.setCurrent(match.url);
|
|
2111
|
+
const entry = config.servers[match.url] ?? {};
|
|
2112
|
+
const name = entry.instance?.name;
|
|
2113
|
+
const email = entry.apiKey ? entry.user?.email : void 0;
|
|
2114
|
+
ctx.out.data(
|
|
2115
|
+
{ current: match.url, instance: name ?? null, email: email ?? null },
|
|
2116
|
+
() => `Now using ${chalk5.bold(match.url)}${name ? chalk5.dim(` (${name})`) : ""}`
|
|
2117
|
+
);
|
|
2118
|
+
if (!ctx.json) {
|
|
2119
|
+
if (entry.apiKey) ctx.log.hint(`Signed in as ${email ?? "an unknown user"}.`);
|
|
2120
|
+
else ctx.log.hint(`Signed out there: run \`${ctx.binName} login\` to sign in.`);
|
|
2121
|
+
overrideHint();
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
async function runLogout(ctx, url) {
|
|
1974
2125
|
const config = await ctx.store.load();
|
|
1975
|
-
|
|
1976
|
-
|
|
2126
|
+
const target = url === void 0 ? ctx.config.serverUrl : matchServer(Object.keys(config.servers), url, { loose: true }).url;
|
|
2127
|
+
const entry = target === void 0 ? void 0 : config.servers[target];
|
|
2128
|
+
if (target === void 0 || entry === void 0) {
|
|
2129
|
+
ctx.log.info(`Not logged in${target ? ` to ${target}` : ""} \u2014 nothing to do`);
|
|
1977
2130
|
return;
|
|
1978
2131
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2132
|
+
if (target === config.current) {
|
|
2133
|
+
if (!entry.apiKey) {
|
|
2134
|
+
ctx.log.info("Not logged in \u2014 nothing to do");
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
await ctx.store.clearCredentials(target);
|
|
2138
|
+
ctx.log.success("Logged out; API key removed from this machine");
|
|
2139
|
+
} else {
|
|
2140
|
+
await ctx.store.removeServer(target);
|
|
2141
|
+
ctx.log.success(
|
|
2142
|
+
entry.apiKey ? `Logged out of ${target}; API key and server removed from this machine` : `Removed ${target} from this machine`
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
if (entry.apiKey) ctx.log.hint(`Revoke it on the server under Connections at ${target}`);
|
|
1983
2146
|
}
|
|
1984
2147
|
async function runWhoami(ctx) {
|
|
1985
2148
|
const client = requireClient(ctx);
|
|
1986
2149
|
const me = await whoami(client);
|
|
1987
2150
|
const instance = await loadInstance(ctx.store, ctx.config, ctx.fetch);
|
|
2151
|
+
const { serverUrl, current } = ctx.config;
|
|
2152
|
+
const others = Object.keys((await ctx.store.load()).servers).filter((s) => s !== serverUrl);
|
|
1988
2153
|
ctx.out.data(
|
|
1989
2154
|
{
|
|
1990
2155
|
...me,
|
|
1991
|
-
serverUrl
|
|
2156
|
+
serverUrl,
|
|
2157
|
+
serverUrlSource: ctx.config.serverUrlSource,
|
|
2158
|
+
current: current ?? null,
|
|
1992
2159
|
instance: instance?.name ?? null,
|
|
1993
2160
|
apiKey: redactKey(ctx.config.apiKey),
|
|
1994
2161
|
apiKeySource: ctx.config.apiKeySource
|
|
@@ -1996,8 +2163,15 @@ async function runWhoami(ctx) {
|
|
|
1996
2163
|
() => [
|
|
1997
2164
|
`${chalk5.bold("User:")} ${me.name} <${me.email}>`,
|
|
1998
2165
|
`${chalk5.bold("Sites:")} ${sitesPattern(sitesBaseOf(instance))} ${chalk5.dim("(names are first come, first served)")}`,
|
|
1999
|
-
`${chalk5.bold("Server:")} ${
|
|
2000
|
-
|
|
2166
|
+
`${chalk5.bold("Server:")} ${serverUrl}${instance ? chalk5.dim(` (${instance.name})`) : ""} ${chalk5.dim(
|
|
2167
|
+
serverUrl === current ? "(current)" : `(from ${ctx.config.serverUrlSource === "env" ? "FLING_URL" : "--server"}; current: ${current ?? "none"})`
|
|
2168
|
+
)}`,
|
|
2169
|
+
`${chalk5.bold("API key:")} ${redactKey(ctx.config.apiKey)} ${chalk5.dim(`(from ${ctx.config.apiKeySource})`)}`,
|
|
2170
|
+
...others.length > 0 ? [
|
|
2171
|
+
chalk5.dim(
|
|
2172
|
+
`${others.length} other server${others.length === 1 ? "" : "s"} on this computer: \`${ctx.binName} use\` lists them`
|
|
2173
|
+
)
|
|
2174
|
+
] : []
|
|
2001
2175
|
].join("\n")
|
|
2002
2176
|
);
|
|
2003
2177
|
}
|
|
@@ -3119,12 +3293,13 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
3119
3293
|
`);
|
|
3120
3294
|
intro(chalk8.bold(" fling setup "), { output: process.stderr });
|
|
3121
3295
|
}
|
|
3296
|
+
const login = await stepLogin(ctx, opts.url);
|
|
3122
3297
|
const results = {
|
|
3123
|
-
login:
|
|
3298
|
+
login: login.result,
|
|
3124
3299
|
claudeCode: "skipped",
|
|
3125
3300
|
claudeDesktop: "skipped"
|
|
3126
3301
|
};
|
|
3127
|
-
const after =
|
|
3302
|
+
const after = login.serverUrl ? { ...ctx, config: await ctx.store.resolve({ serverUrl: login.serverUrl }) } : ctx;
|
|
3128
3303
|
results.claudeCode = await stepClaudeCode(after, run, scope, ask, fancy);
|
|
3129
3304
|
results.claudeDesktop = await stepClaudeDesktop(after, deps, ask, fancy);
|
|
3130
3305
|
const failed = Object.values(results).includes("failed");
|
|
@@ -3137,10 +3312,22 @@ async function runSetup(baseCtx, opts, deps = {}) {
|
|
|
3137
3312
|
if (failed) process.exitCode = 1;
|
|
3138
3313
|
}
|
|
3139
3314
|
async function stepLogin(ctx, url) {
|
|
3140
|
-
|
|
3141
|
-
|
|
3315
|
+
let serverUrl;
|
|
3316
|
+
let target;
|
|
3317
|
+
try {
|
|
3318
|
+
serverUrl = await loginServer(ctx, url);
|
|
3319
|
+
target = { ...ctx, config: await ctx.store.resolve({ serverUrl }) };
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
ctx.log.error(error instanceof Error ? error.message : String(error));
|
|
3322
|
+
return { result: "failed" };
|
|
3323
|
+
}
|
|
3324
|
+
return { result: await signIn(target, serverUrl), serverUrl };
|
|
3325
|
+
}
|
|
3326
|
+
async function signIn(ctx, serverUrl) {
|
|
3327
|
+
if (ctx.config.apiKey) {
|
|
3142
3328
|
try {
|
|
3143
3329
|
const me = await whoami(requireClient(ctx));
|
|
3330
|
+
if (ctx.config.remembered) await ctx.store.setCurrent(serverUrl);
|
|
3144
3331
|
ctx.log.success(`Signed in as ${chalk8.bold(me.name)} ${chalk8.dim(`<${me.email}>`)}`);
|
|
3145
3332
|
return "already";
|
|
3146
3333
|
} catch (error) {
|
|
@@ -3152,20 +3339,13 @@ async function stepLogin(ctx, url) {
|
|
|
3152
3339
|
}
|
|
3153
3340
|
}
|
|
3154
3341
|
try {
|
|
3155
|
-
await runLogin({ ...ctx, json: false },
|
|
3342
|
+
await runLogin({ ...ctx, json: false }, serverUrl);
|
|
3156
3343
|
return "done";
|
|
3157
3344
|
} catch (error) {
|
|
3158
3345
|
ctx.log.error(error instanceof Error ? error.message : String(error));
|
|
3159
3346
|
return "failed";
|
|
3160
3347
|
}
|
|
3161
3348
|
}
|
|
3162
|
-
function safeNormalize(url) {
|
|
3163
|
-
try {
|
|
3164
|
-
return normalizeServerUrl(url);
|
|
3165
|
-
} catch {
|
|
3166
|
-
return void 0;
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
3349
|
async function stepClaudeCode(ctx, run, scope, ask, fancy) {
|
|
3170
3350
|
const status = await claudeCodeStatus(run);
|
|
3171
3351
|
if (status.claude === "missing") {
|
|
@@ -3987,6 +4167,8 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
3987
4167
|
open: deps.open ?? (async (url) => (await import("open")).default(url)),
|
|
3988
4168
|
fetch: fetchImpl,
|
|
3989
4169
|
timeoutMs: deps.loginTimeoutMs,
|
|
4170
|
+
// Signing in to the extension's server leaves the CLI's current server as it is.
|
|
4171
|
+
makeCurrent: !config.current || config.current === serverUrl,
|
|
3990
4172
|
onAuthUrl: (url) => {
|
|
3991
4173
|
login.url = url;
|
|
3992
4174
|
}
|
|
@@ -4266,7 +4448,10 @@ function routeOptionsFor(program2) {
|
|
|
4266
4448
|
|
|
4267
4449
|
// src/cli.ts
|
|
4268
4450
|
var program = new Command();
|
|
4269
|
-
program.name(BIN_NAME).description("Fling a folder of static files at Fling and get a link back").version(VERSION).option(
|
|
4451
|
+
program.name(BIN_NAME).description("Fling a folder of static files at Fling and get a link back").version(VERSION).option(
|
|
4452
|
+
"--server <url>",
|
|
4453
|
+
`server for this command: a URL, or the host of one you signed in to (default: $${ENV.url}, else the current one)`
|
|
4454
|
+
).option("--json", "print JSON instead of human output", false).showHelpAfterError().configureHelp({
|
|
4270
4455
|
sortSubcommands: false,
|
|
4271
4456
|
styleTitle: (title) => chalk10.bold(brand.primary(title)),
|
|
4272
4457
|
styleCommandText: (text2) => chalk10.bold(text2),
|
|
@@ -4282,6 +4467,7 @@ program.name(BIN_NAME).description("Fling a folder of static files at Fling and
|
|
|
4282
4467
|
${chalk10.bold(brand.primary("Examples:"))}
|
|
4283
4468
|
$ ${BIN_NAME} login https://fling.example.com sign in to your Fling (once; there is no default)
|
|
4284
4469
|
$ ${BIN_NAME} setup sign in and add Fling to Claude Code / Claude Desktop
|
|
4470
|
+
$ ${BIN_NAME} use list the Flings you signed in to; \`use <url>\` switches
|
|
4285
4471
|
$ ${BIN_NAME} deploy ./dist --name my-app \u2192 https://my-app.<your sites domain>
|
|
4286
4472
|
$ ${BIN_NAME} ./dist the same: an existing path deploys
|
|
4287
4473
|
$ ${BIN_NAME} ./dist --yes no prompts (CI): use the generated name, fail if it is taken
|
|
@@ -4342,22 +4528,27 @@ program.command("deploy [path]").helpGroup("Deploy:").description(
|
|
|
4342
4528
|
)
|
|
4343
4529
|
);
|
|
4344
4530
|
program.command("setup [url]").helpGroup("Setup:").description(
|
|
4345
|
-
"sign in (to the Fling at url,
|
|
4531
|
+
"sign in (to the Fling at url, which becomes current), then add the Fling plugin to Claude Code and the extension to Claude Desktop"
|
|
4346
4532
|
).option("-y, --yes", "do every step without asking").option("-s, --scope <scope>", "Claude Code plugin scope: user, project or local", "user").action(
|
|
4347
4533
|
action(
|
|
4348
4534
|
(ctx, url, opts) => runSetup(ctx, { ...opts, url })
|
|
4349
4535
|
)
|
|
4350
4536
|
);
|
|
4351
4537
|
program.command("login [url]").helpGroup("Setup:").description(
|
|
4352
|
-
"sign in through the browser to the Fling at url
|
|
4538
|
+
"sign in through the browser to the Fling at url and store an API key; it becomes the current server (default: the current one)"
|
|
4353
4539
|
).action(
|
|
4354
4540
|
action(async (ctx, url) => {
|
|
4355
4541
|
await runLogin(ctx, url);
|
|
4356
4542
|
ctx.log.hint(`Next: \`${BIN_NAME} setup\` adds Fling to Claude Code and Claude Desktop.`);
|
|
4357
4543
|
})
|
|
4358
4544
|
);
|
|
4359
|
-
program.command("
|
|
4360
|
-
|
|
4545
|
+
program.command("use [url]").helpGroup("Setup:").description(
|
|
4546
|
+
"switch to another Fling you signed in to (a URL or its host); without url, list them"
|
|
4547
|
+
).action(action((ctx, url) => runUse(ctx, url)));
|
|
4548
|
+
program.command("logout [url]").helpGroup("Setup:").description(
|
|
4549
|
+
"remove a server's API key (default: the current server's, which stays current); any other server is forgotten"
|
|
4550
|
+
).action(action((ctx, url) => runLogout(ctx, url)));
|
|
4551
|
+
program.command("whoami").helpGroup("Setup:").description("show the signed-in user and the current server").action(action((ctx) => runWhoami(ctx)));
|
|
4361
4552
|
program.commandsGroup("Setup:").helpCommand("help [command]", "show help for a command");
|
|
4362
4553
|
program.command("ls").helpGroup("Sites:").alias("list").description("list your sites").option("--archived", "list archived sites").action(action((ctx, opts) => runList(ctx, opts)));
|
|
4363
4554
|
program.command("info <name>").helpGroup("Sites:").description("show a site").action(action((ctx, name) => runInfo(ctx, name)));
|