@tiny-fish/cli 0.10.2-next.129 → 0.10.2-next.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/connect.js +167 -62
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +12 -8
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import spawn from "cross-spawn";
|
|
4
|
-
import { CONNECT_SOURCE,
|
|
4
|
+
import { CONNECT_SOURCE, loadConfig, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, writeConfig, } from "../lib/auth.js";
|
|
5
5
|
import { CLI_VERSION } from "../lib/constants.js";
|
|
6
6
|
import { errLine } from "../lib/output.js";
|
|
7
7
|
const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
|
|
8
8
|
const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
|
|
9
9
|
const SKILL_INSTALL_TIMEOUT_MS = 120_000;
|
|
10
10
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
11
|
-
const TELEMETRY_TIMEOUT_MS =
|
|
11
|
+
const TELEMETRY_TIMEOUT_MS = 2_000;
|
|
12
12
|
const MCP_LOGIN_UNAVAILABLE_MESSAGE = "This Claude Code installation does not expose `claude mcp login`, which is required for " +
|
|
13
13
|
"one-command MCP authentication. Update Claude Code and retry, or authenticate TinyFish " +
|
|
14
14
|
"manually from /mcp.";
|
|
@@ -32,6 +32,72 @@ export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill.
|
|
|
32
32
|
"want to search and wait for my reply before using TinyFish Search. Show me the results, ask " +
|
|
33
33
|
"which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task " +
|
|
34
34
|
"I want to complete and wait before using TinyFish Agent. Never skip ahead or choose for me.";
|
|
35
|
+
/** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
|
|
36
|
+
class ConnectInterruptedError extends Error {
|
|
37
|
+
}
|
|
38
|
+
function throwIfInterrupted(result) {
|
|
39
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM") {
|
|
40
|
+
throw new ConnectInterruptedError("Setup interrupted");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Terminal events exactly once, whichever of flow or signal handler wins. */
|
|
44
|
+
function settle(state, telemetry, stage, detail) {
|
|
45
|
+
if (state.settled)
|
|
46
|
+
return;
|
|
47
|
+
state.settled = true;
|
|
48
|
+
telemetry.track(stage, detail);
|
|
49
|
+
}
|
|
50
|
+
// Without a parent handler, terminal Ctrl+C kills this process inside spawn.sync
|
|
51
|
+
// before the child result exists — no aborted event, no flush. With one, the parent
|
|
52
|
+
// survives; raw-mode children still surface via throwIfInterrupted, and cooked-mode
|
|
53
|
+
// stages (npm/npx/OAuth waits) land here.
|
|
54
|
+
// 128 + signal number
|
|
55
|
+
const SIGNAL_EXIT_CODES = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
|
|
56
|
+
function installSignalGuard(state, telemetry) {
|
|
57
|
+
const onSignal = (signal) => {
|
|
58
|
+
if (state.settled) {
|
|
59
|
+
process.exit(SIGNAL_EXIT_CODES[signal]);
|
|
60
|
+
}
|
|
61
|
+
settle(state, telemetry, "aborted", { failedStage: state.stage });
|
|
62
|
+
errLine("Setup interrupted — run the command again to finish.");
|
|
63
|
+
void telemetry.flush().finally(() => process.exit(SIGNAL_EXIT_CODES[signal]));
|
|
64
|
+
};
|
|
65
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
66
|
+
const handlers = signals.map((s) => {
|
|
67
|
+
const handler = () => onSignal(s);
|
|
68
|
+
process.on(s, handler);
|
|
69
|
+
return [s, handler];
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
for (const [s, handler] of handlers)
|
|
73
|
+
process.off(s, handler);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Shared terminal handling: interrupted → aborted (or completed post-install), else failed. */
|
|
77
|
+
async function runGuarded(state, telemetry, body) {
|
|
78
|
+
const uninstallSignalGuard = installSignalGuard(state, telemetry);
|
|
79
|
+
try {
|
|
80
|
+
body();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof ConnectInterruptedError) {
|
|
84
|
+
if (state.stage === "walkthrough_launch") {
|
|
85
|
+
settle(state, telemetry, "completed");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
settle(state, telemetry, "aborted", { failedStage: state.stage });
|
|
89
|
+
errLine("Setup interrupted — run the command again to finish.");
|
|
90
|
+
process.exitCode = 130;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
settle(state, telemetry, "failed", { failedStage: state.stage });
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
uninstallSignalGuard();
|
|
98
|
+
await telemetry.flush();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
35
101
|
const CLAUDE_CODE = {
|
|
36
102
|
command: "claude",
|
|
37
103
|
connectClient: "claude-code",
|
|
@@ -96,6 +162,7 @@ function launchHermesWalkthrough() {
|
|
|
96
162
|
timeout: HERMES_SEED_TIMEOUT_MS,
|
|
97
163
|
});
|
|
98
164
|
if (seedResult.error || seedResult.status !== 0) {
|
|
165
|
+
throwIfInterrupted(seedResult);
|
|
99
166
|
throw new Error("Could not start the TinyFish walkthrough in Hermes", {
|
|
100
167
|
cause: seedResult.error,
|
|
101
168
|
});
|
|
@@ -159,44 +226,69 @@ function isTrustedTelemetryOrigin(endpoint) {
|
|
|
159
226
|
return false;
|
|
160
227
|
return TRUSTED_TELEMETRY_HOST_SUFFIXES.some((suffix) => host === suffix.slice(1) || host.endsWith(suffix));
|
|
161
228
|
}
|
|
229
|
+
function storedApiKey() {
|
|
230
|
+
try {
|
|
231
|
+
return loadConfig().api_key;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
162
237
|
function createConnectTelemetry(mcpUrl, client, apiKey) {
|
|
163
238
|
const attemptId = randomUUID();
|
|
164
239
|
const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
|
|
165
|
-
const telemetryKey = apiKey
|
|
240
|
+
const telemetryKey = apiKey || process.env["TINYFISH_API_KEY"] || storedApiKey();
|
|
166
241
|
const headers = { "content-type": "application/json" };
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// --url pointing elsewhere gets anonymous telemetry, never the key.
|
|
242
|
+
// SHA-256 lookup key only — the raw key never rides a telemetry request
|
|
243
|
+
// (OWASP: keep credentials out of anything logging infra might capture).
|
|
170
244
|
if (telemetryKey && validateKeyFormat(telemetryKey) && isTrustedTelemetryOrigin(endpoint)) {
|
|
171
|
-
headers["x-api-key"] = telemetryKey;
|
|
245
|
+
headers["x-api-key-lookup"] = createHash("sha256").update(telemetryKey).digest("hex");
|
|
172
246
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return;
|
|
247
|
+
const pending = [];
|
|
248
|
+
async function deliver(body) {
|
|
249
|
+
// One retry: stale keep-alive sockets eat post-install POSTs. 5xx retries;
|
|
250
|
+
// 4xx never will (schema mismatch, e.g. new stages hitting an old server).
|
|
251
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
179
252
|
try {
|
|
180
253
|
const response = await fetch(endpoint, {
|
|
181
254
|
method: "POST",
|
|
182
255
|
headers,
|
|
183
|
-
body
|
|
184
|
-
|
|
185
|
-
client,
|
|
186
|
-
stage,
|
|
187
|
-
failed_stage: failedStage,
|
|
188
|
-
runtime_platform: process.platform,
|
|
189
|
-
node_version: process.version,
|
|
190
|
-
cli_version: CLI_VERSION,
|
|
191
|
-
}),
|
|
256
|
+
body,
|
|
257
|
+
redirect: "error",
|
|
192
258
|
signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS),
|
|
193
259
|
});
|
|
194
|
-
if (
|
|
195
|
-
|
|
260
|
+
if (response.ok)
|
|
261
|
+
return;
|
|
262
|
+
if (response.status < 500) {
|
|
263
|
+
if (process.env["TINYFISH_DEBUG"]) {
|
|
264
|
+
errLine(`telemetry rejected: HTTP ${response.status}`);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
196
268
|
}
|
|
197
269
|
catch {
|
|
198
|
-
|
|
270
|
+
// best-effort; never blocks setup
|
|
199
271
|
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
attemptId,
|
|
276
|
+
// Fire-and-forget; awaiting each event stalls setup when telemetry down.
|
|
277
|
+
track(stage, detail) {
|
|
278
|
+
pending.push(deliver(JSON.stringify({
|
|
279
|
+
attempt_id: attemptId,
|
|
280
|
+
client,
|
|
281
|
+
stage,
|
|
282
|
+
failed_stage: detail?.failedStage,
|
|
283
|
+
phase: detail?.phase,
|
|
284
|
+
runtime_platform: process.platform,
|
|
285
|
+
node_version: process.version,
|
|
286
|
+
cli_version: CLI_VERSION,
|
|
287
|
+
})));
|
|
288
|
+
},
|
|
289
|
+
// Awaited in finally so in-flight events land before exit.
|
|
290
|
+
async flush() {
|
|
291
|
+
await Promise.all(pending);
|
|
200
292
|
},
|
|
201
293
|
};
|
|
202
294
|
}
|
|
@@ -211,6 +303,7 @@ function requireCommandSupport(client) {
|
|
|
211
303
|
});
|
|
212
304
|
}
|
|
213
305
|
if (result.error || result.status !== 0) {
|
|
306
|
+
throwIfInterrupted(result);
|
|
214
307
|
throw new Error(client.supportCheck.unavailableMessage, { cause: result.error });
|
|
215
308
|
}
|
|
216
309
|
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
@@ -228,6 +321,7 @@ function installTinyFishCli() {
|
|
|
228
321
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
229
322
|
});
|
|
230
323
|
if (result.error || result.status !== 0) {
|
|
324
|
+
throwIfInterrupted(result);
|
|
231
325
|
throw new Error("Could not install the TinyFish CLI", { cause: result.error });
|
|
232
326
|
}
|
|
233
327
|
}
|
|
@@ -253,6 +347,7 @@ function installWebSkill(client) {
|
|
|
253
347
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
254
348
|
});
|
|
255
349
|
if (result.error || result.status !== 0) {
|
|
350
|
+
throwIfInterrupted(result);
|
|
256
351
|
throw new Error(`Could not install the TinyFish web skill in ${client.displayName}`, {
|
|
257
352
|
cause: result.error,
|
|
258
353
|
});
|
|
@@ -264,7 +359,8 @@ function ensureCliAuthenticated(source, apiKey) {
|
|
|
264
359
|
if (!validateKeyFormat(envKey)) {
|
|
265
360
|
throw new Error("TINYFISH_API_KEY has invalid format");
|
|
266
361
|
}
|
|
267
|
-
|
|
362
|
+
// Throwing variant: a write failure must reach connect's catch/flush, not exit(1) past it.
|
|
363
|
+
writeConfig(envKey);
|
|
268
364
|
persistApiKeyToEnvironment(envKey);
|
|
269
365
|
return;
|
|
270
366
|
}
|
|
@@ -285,6 +381,7 @@ function ensureCliAuthenticated(source, apiKey) {
|
|
|
285
381
|
throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
|
|
286
382
|
}
|
|
287
383
|
if (loginResult.error || loginResult.status !== 0) {
|
|
384
|
+
throwIfInterrupted(loginResult);
|
|
288
385
|
throw new Error("Could not authenticate the TinyFish CLI", { cause: loginResult.error });
|
|
289
386
|
}
|
|
290
387
|
}
|
|
@@ -295,6 +392,7 @@ function removeExistingRegistration(client, removal) {
|
|
|
295
392
|
});
|
|
296
393
|
if (result.status === 0)
|
|
297
394
|
return;
|
|
395
|
+
throwIfInterrupted(result);
|
|
298
396
|
const details = result.stderr?.trim() || result.error?.message || "unknown error";
|
|
299
397
|
if (/No MCP server named "tinyfish"/i.test(details))
|
|
300
398
|
return;
|
|
@@ -339,62 +437,66 @@ function launchOpenClawWalkthrough() {
|
|
|
339
437
|
}
|
|
340
438
|
async function connectNativeMcpClient(client, options) {
|
|
341
439
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options.apiKey);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
440
|
+
const state = { stage: "prerequisite_check", settled: false };
|
|
441
|
+
await runGuarded(state, telemetry, () => {
|
|
442
|
+
telemetry.track("started");
|
|
345
443
|
requireCommandSupport(client);
|
|
346
|
-
|
|
444
|
+
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
445
|
+
state.stage = "registration_cleanup";
|
|
347
446
|
for (const removal of client.removals)
|
|
348
447
|
removeExistingRegistration(client, removal);
|
|
349
448
|
const mcpUrl = new URL(options.mcpUrl);
|
|
350
449
|
mcpUrl.searchParams.set("source", CONNECT_SOURCE);
|
|
351
450
|
mcpUrl.searchParams.set("client", client.connectClient);
|
|
352
451
|
mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
|
|
353
|
-
|
|
452
|
+
state.stage = client.loginArgs ? "registration" : "registration_or_authentication";
|
|
354
453
|
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
355
454
|
const addResult = spawn.sync(client.command, client.addArgs(mcpUrl.toString(), options.mcpUrl), {
|
|
356
455
|
stdio: "inherit",
|
|
357
456
|
});
|
|
358
457
|
if (addResult.error || addResult.status !== 0) {
|
|
458
|
+
throwIfInterrupted(addResult);
|
|
359
459
|
throw new Error(`Could not add TinyFish to ${client.displayName}`, {
|
|
360
460
|
cause: addResult.error,
|
|
361
461
|
});
|
|
362
462
|
}
|
|
463
|
+
telemetry.track("checkpoint", { phase: "registered" });
|
|
363
464
|
if (client.loginArgs) {
|
|
364
|
-
|
|
465
|
+
state.stage = "client_oauth";
|
|
365
466
|
errLine("Signing in to TinyFish...");
|
|
366
467
|
const loginResult = spawn.sync(client.command, client.loginArgs, {
|
|
367
468
|
stdio: "inherit",
|
|
368
469
|
});
|
|
369
470
|
if (loginResult.error || loginResult.status !== 0) {
|
|
471
|
+
throwIfInterrupted(loginResult);
|
|
370
472
|
throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
|
|
371
473
|
cause: loginResult.error,
|
|
372
474
|
});
|
|
373
475
|
}
|
|
476
|
+
telemetry.track("checkpoint", { phase: "oauth_done" });
|
|
374
477
|
}
|
|
375
|
-
|
|
478
|
+
state.stage = "cli_install";
|
|
376
479
|
installTinyFishCli();
|
|
377
|
-
|
|
480
|
+
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
481
|
+
state.stage = "skill_install";
|
|
378
482
|
installWebSkill(client);
|
|
379
|
-
|
|
483
|
+
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
484
|
+
state.stage = "authentication";
|
|
380
485
|
ensureCliAuthenticated(client.connectClient, options.apiKey);
|
|
486
|
+
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
381
487
|
// Persist once here (skill installed + authenticated) so both success paths, launch
|
|
382
488
|
// or not, record the install; buildConnectHeaders reads it on subsequent requests.
|
|
383
489
|
saveConnectContext(client.connectClient, telemetry.attemptId);
|
|
384
490
|
if (!options.launch) {
|
|
385
491
|
errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
|
|
386
|
-
|
|
492
|
+
settle(state, telemetry, "completed");
|
|
387
493
|
return;
|
|
388
494
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
catch (error) {
|
|
395
|
-
await telemetry.track("failed", failureStage);
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
495
|
+
state.stage = "walkthrough_launch";
|
|
496
|
+
// Install already done; walkthrough Ctrl+C still counts completed.
|
|
497
|
+
launchNativeMcpClient(client);
|
|
498
|
+
settle(state, telemetry, "completed");
|
|
499
|
+
});
|
|
398
500
|
}
|
|
399
501
|
export async function connectClaudeCode(options) {
|
|
400
502
|
await connectNativeMcpClient(CLAUDE_CODE, options);
|
|
@@ -407,30 +509,35 @@ export async function connectHermes(options) {
|
|
|
407
509
|
}
|
|
408
510
|
export async function connectOpenClaw(options) {
|
|
409
511
|
const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw");
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
512
|
+
const state = { stage: "prerequisite_check", settled: false };
|
|
513
|
+
await runGuarded(state, telemetry, () => {
|
|
514
|
+
telemetry.track("started");
|
|
413
515
|
requireCommandSupport(OPENCLAW);
|
|
516
|
+
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
414
517
|
if (options.installCli !== false) {
|
|
415
|
-
|
|
518
|
+
state.stage = "cli_install";
|
|
416
519
|
errLine("Installing the TinyFish CLI...");
|
|
417
520
|
const cliInstallResult = spawn.sync("npm", ["install", "--global", "@tiny-fish/cli@latest"], {
|
|
418
521
|
stdio: "inherit",
|
|
419
522
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
420
523
|
});
|
|
421
524
|
if (cliInstallResult.error || cliInstallResult.status !== 0) {
|
|
525
|
+
throwIfInterrupted(cliInstallResult);
|
|
422
526
|
throw new Error("Could not install the TinyFish CLI", { cause: cliInstallResult.error });
|
|
423
527
|
}
|
|
528
|
+
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
424
529
|
}
|
|
425
|
-
|
|
530
|
+
state.stage = "skill_install";
|
|
426
531
|
errLine("Installing the TinyFish skill in OpenClaw...");
|
|
427
532
|
const skillInstallResult = spawn.sync("openclaw", ["skills", "install", OPENCLAW_SKILL, "--global", "--force", "--acknowledge-clawhub-risk"], { stdio: "inherit" });
|
|
428
533
|
if (skillInstallResult.error || skillInstallResult.status !== 0) {
|
|
534
|
+
throwIfInterrupted(skillInstallResult);
|
|
429
535
|
throw new Error("Could not install the TinyFish skill in OpenClaw", {
|
|
430
536
|
cause: skillInstallResult.error,
|
|
431
537
|
});
|
|
432
538
|
}
|
|
433
|
-
|
|
539
|
+
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
540
|
+
state.stage = "authentication";
|
|
434
541
|
const authStatus = spawn.sync("tinyfish", ["auth", "status"], {
|
|
435
542
|
stdio: "ignore",
|
|
436
543
|
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
@@ -447,26 +554,24 @@ export async function connectOpenClaw(options) {
|
|
|
447
554
|
throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
|
|
448
555
|
}
|
|
449
556
|
if (loginResult.error || loginResult.status !== 0) {
|
|
557
|
+
throwIfInterrupted(loginResult);
|
|
450
558
|
throw new Error("Could not authenticate TinyFish for OpenClaw", {
|
|
451
559
|
cause: loginResult.error,
|
|
452
560
|
});
|
|
453
561
|
}
|
|
454
562
|
}
|
|
563
|
+
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
455
564
|
saveConnectContext("openclaw", telemetry.attemptId);
|
|
456
565
|
if (!options.launch) {
|
|
457
566
|
errLine("TinyFish is connected. Open OpenClaw to start using it.");
|
|
458
|
-
|
|
567
|
+
settle(state, telemetry, "completed");
|
|
459
568
|
return;
|
|
460
569
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
await telemetry.track("failed", failureStage);
|
|
468
|
-
throw error;
|
|
469
|
-
}
|
|
570
|
+
state.stage = "walkthrough_launch";
|
|
571
|
+
// Install already done; walkthrough Ctrl+C still counts completed.
|
|
572
|
+
launchOpenClawWalkthrough();
|
|
573
|
+
settle(state, telemetry, "completed");
|
|
574
|
+
});
|
|
470
575
|
}
|
|
471
576
|
export function launchAgent(client) {
|
|
472
577
|
if (client === "claude-code") {
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface TinyfishConfig {
|
|
|
10
10
|
connect?: Record<string, ConnectEntry>;
|
|
11
11
|
}
|
|
12
12
|
export declare function loadConfig(): TinyfishConfig;
|
|
13
|
+
/** Throwing variant for callers with their own cleanup (connect's telemetry flush). */
|
|
14
|
+
export declare function writeConfig(apiKey: string): void;
|
|
13
15
|
export declare function saveConfig(apiKey: string): void;
|
|
14
16
|
export declare function saveConnectContext(client: string, attemptId: string): void;
|
|
15
17
|
interface EnvironmentOptions {
|
package/dist/lib/auth.js
CHANGED
|
@@ -44,16 +44,20 @@ function parseConnectMap(value) {
|
|
|
44
44
|
}
|
|
45
45
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/** Throwing variant for callers with their own cleanup (connect's telemetry flush). */
|
|
48
|
+
export function writeConfig(apiKey) {
|
|
48
49
|
const dir = configDir();
|
|
50
|
+
// 0o700: only owner can read/write/execute the directory
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
52
|
+
const existing = loadConfig();
|
|
53
|
+
// 0o600: only owner can read/write the config file (key is sensitive)
|
|
54
|
+
fs.writeFileSync(configFile(), JSON.stringify({ ...existing, api_key: apiKey }), {
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function saveConfig(apiKey) {
|
|
49
59
|
try {
|
|
50
|
-
|
|
51
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
52
|
-
const existing = loadConfig();
|
|
53
|
-
// 0o600: only owner can read/write the config file (key is sensitive)
|
|
54
|
-
fs.writeFileSync(configFile(), JSON.stringify({ ...existing, api_key: apiKey }), {
|
|
55
|
-
mode: 0o600,
|
|
56
|
-
});
|
|
60
|
+
writeConfig(apiKey);
|
|
57
61
|
}
|
|
58
62
|
catch (e) {
|
|
59
63
|
err({ error: `Could not write config: ${e instanceof Error ? e.message : String(e)}` });
|