peekable 0.1.4 → 0.1.5

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.
@@ -2,32 +2,42 @@ import { Command } from "commander";
2
2
  import { requireConfig } from "../config.js";
3
3
  import { api } from "../api.js";
4
4
  import { formatQuota } from "../quota.js";
5
+ import { CommandFailure, printCommandError } from "../command-error.js";
6
+ import { runWithTelemetry } from "../telemetry.js";
5
7
  export const listCommand = new Command("list")
6
8
  .description("List active sessions")
7
9
  .option("--json", "Output JSON")
8
10
  .action(async (opts) => {
9
- const config = requireConfig();
10
- const data = await api(config, "GET", "/sessions");
11
- if (opts.json) {
12
- console.log(JSON.stringify(data));
13
- return;
14
- }
15
- if (data.sessions.length === 0) {
16
- if (data.limits) {
17
- console.log(`No active sessions. (${formatQuota(data.limits.activeSessions, data.limits.maxActiveSessions, data.limits.unlimited)})`);
11
+ await runWithTelemetry("list", async () => {
12
+ try {
13
+ const config = requireConfig();
14
+ const data = await api(config, "GET", "/sessions");
15
+ if (opts.json) {
16
+ console.log(JSON.stringify(data));
17
+ return;
18
+ }
19
+ if (data.sessions.length === 0) {
20
+ if (data.limits) {
21
+ console.log(`No active sessions. (${formatQuota(data.limits.activeSessions, data.limits.maxActiveSessions, data.limits.unlimited)})`);
22
+ }
23
+ else {
24
+ console.log("No active sessions.");
25
+ }
26
+ return;
27
+ }
28
+ if (data.limits) {
29
+ console.log(`Active sessions (${formatQuota(data.limits.activeSessions, data.limits.maxActiveSessions, data.limits.unlimited)}):`);
30
+ }
31
+ else {
32
+ console.log("Active sessions:");
33
+ }
34
+ for (const s of data.sessions) {
35
+ console.log(` ${s.id} ${s.name} (${new Date(s.created_at).toLocaleDateString()})`);
36
+ }
18
37
  }
19
- else {
20
- console.log("No active sessions.");
38
+ catch (err) {
39
+ printCommandError("List failed", err, opts.json);
40
+ throw new CommandFailure("List failed", { cause: err });
21
41
  }
22
- return;
23
- }
24
- if (data.limits) {
25
- console.log(`Active sessions (${formatQuota(data.limits.activeSessions, data.limits.maxActiveSessions, data.limits.unlimited)}):`);
26
- }
27
- else {
28
- console.log("Active sessions:");
29
- }
30
- for (const s of data.sessions) {
31
- console.log(` ${s.id} ${s.name} (${new Date(s.created_at).toLocaleDateString()})`);
32
- }
42
+ });
33
43
  });
@@ -2,7 +2,8 @@ import { Command } from "commander";
2
2
  import { watch } from "fs";
3
3
  import { requireConfig } from "../config.js";
4
4
  import { api } from "../api.js";
5
- import { printCommandError } from "../command-error.js";
5
+ import { CommandFailure, printCommandError } from "../command-error.js";
6
+ import { postStartEvent, postFireAndForgetDebugEvent } from "../telemetry.js";
6
7
  const MAX_RESPONSE_BYTES = 25 * 1024 * 1024; // 25 MB
7
8
  const TEXT_CONTENT_TYPES = [
8
9
  "text/",
@@ -28,7 +29,7 @@ export const proxyCommand = new Command("proxy")
28
29
  const port = parseInt(portArg, 10);
29
30
  if (isNaN(port) || port < 1 || port > 65535) {
30
31
  printCommandError("Proxy failed", new Error("Invalid port: must be 1-65535"), opts.json);
31
- process.exit(1);
32
+ throw new CommandFailure("Invalid port");
32
33
  }
33
34
  // Create or reuse session
34
35
  let sessionId;
@@ -49,7 +50,7 @@ export const proxyCommand = new Command("proxy")
49
50
  }
50
51
  catch (err) {
51
52
  printCommandError("Proxy session failed", err, opts.json);
52
- process.exit(1);
53
+ throw new CommandFailure("Proxy session failed", { cause: err });
53
54
  }
54
55
  sessionId = data.id;
55
56
  shareUrl = data.url;
@@ -60,9 +61,20 @@ export const proxyCommand = new Command("proxy")
60
61
  else {
61
62
  console.log(`Share URL: ${shareUrl}`);
62
63
  }
64
+ postStartEvent("cli_proxy_started", { session_id: sessionId, port });
63
65
  // Connect relay WebSocket
64
66
  const wsUrl = config.url.replace(/^http/, "ws") + "/ws/relay/" + sessionId;
65
- const ws = new WebSocket(wsUrl);
67
+ let ws;
68
+ try {
69
+ // The constructor throws synchronously on a malformed URL; fail the
70
+ // command cleanly instead of escaping the action handler.
71
+ ws = new WebSocket(wsUrl);
72
+ }
73
+ catch (err) {
74
+ printCommandError("Relay connection failed", err, opts.json);
75
+ throw new CommandFailure("Relay connection failed", { cause: err });
76
+ }
77
+ const wsStartedAt = Date.now();
66
78
  const inflight = new Map();
67
79
  const fallbackPorts = new Set();
68
80
  ws.addEventListener("open", () => {
@@ -88,7 +100,9 @@ export const proxyCommand = new Command("proxy")
88
100
  }
89
101
  if (msg.type === "error" && !msg.id) {
90
102
  console.error(`Relay error: ${msg.message ?? msg.error}`);
91
- process.exit(1);
103
+ process.exitCode = 1;
104
+ ws.close();
105
+ return;
92
106
  }
93
107
  if (msg.type === "discovered_port") {
94
108
  const p = msg.port;
@@ -108,18 +122,31 @@ export const proxyCommand = new Command("proxy")
108
122
  return;
109
123
  }
110
124
  if (msg.type === "request") {
111
- await handleRequest(ws, msg, port, inflight, fallbackPorts);
125
+ await handleRequest(ws, msg, port, inflight, fallbackPorts, opts.json);
112
126
  }
113
127
  });
114
- ws.addEventListener("close", () => {
128
+ ws.addEventListener("close", (event) => {
129
+ const closeEvent = event;
130
+ postFireAndForgetDebugEvent("cli_ws_disconnected", {
131
+ code: closeEvent?.code,
132
+ reason: typeof closeEvent?.reason === "string" ? closeEvent.reason.slice(0, 120) : undefined,
133
+ uptime_ms: Date.now() - wsStartedAt,
134
+ surface: "proxy",
135
+ }, { sessionId });
115
136
  if (!opts.json) {
116
137
  console.log("Relay disconnected.");
117
138
  }
118
- process.exit(0);
139
+ // Exit cleanly after telemetry attempts to flush
140
+ setTimeout(() => process.exit(process.exitCode ?? 0), 200);
119
141
  });
120
142
  ws.addEventListener("error", (e) => {
121
- console.error("WebSocket error:", e.message ?? e);
122
- process.exit(1);
143
+ const message = e?.message ?? String(e);
144
+ console.error("WebSocket error:", message);
145
+ postFireAndForgetDebugEvent("cli_ws_error", {
146
+ surface: "proxy",
147
+ error_message: String(message).slice(0, 280),
148
+ }, { sessionId });
149
+ process.exitCode = 1;
123
150
  });
124
151
  // File watching
125
152
  if (opts.watch) {
@@ -143,10 +170,11 @@ export const proxyCommand = new Command("proxy")
143
170
  // Shutdown
144
171
  process.on("SIGINT", () => {
145
172
  ws.close();
146
- process.exit(0);
173
+ // give telemetry a beat, then exit
174
+ setTimeout(() => process.exit(0), 200);
147
175
  });
148
176
  });
149
- async function handleRequest(ws, msg, port, inflight, fallbackPorts) {
177
+ async function handleRequest(ws, msg, port, inflight, fallbackPorts, jsonOutput) {
150
178
  const ac = new AbortController();
151
179
  inflight.set(msg.id, ac);
152
180
  try {
@@ -263,9 +291,13 @@ async function handleRequest(ws, msg, port, inflight, fallbackPorts) {
263
291
  catch (err) {
264
292
  if (ac.signal.aborted)
265
293
  return;
266
- const message = err.cause?.code === "ECONNREFUSED"
294
+ const isEconnRefused = err?.cause?.code === "ECONNREFUSED";
295
+ const message = isEconnRefused
267
296
  ? "ECONNREFUSED"
268
297
  : err.message ?? "Upstream fetch failed";
298
+ if (isEconnRefused && !jsonOutput) {
299
+ console.error(`Upstream ECONNREFUSED: no local server responding on port ${port} for ${msg.method ?? "GET"} ${msg.path}`);
300
+ }
269
301
  ws.send(JSON.stringify({
270
302
  type: "response",
271
303
  id: msg.id,
@@ -1,7 +1,8 @@
1
1
  import { Command } from "commander";
2
2
  import { requireConfig } from "../config.js";
3
3
  import { api, fetchWithTimeout } from "../api.js";
4
- import { printCommandError } from "../command-error.js";
4
+ import { CommandFailure, printCommandError } from "../command-error.js";
5
+ import { runWithTelemetry } from "../telemetry.js";
5
6
  const FETCH_TIMEOUT_MS = 30_000;
6
7
  const MAX_HTML_BYTES = 5 * 1024 * 1024;
7
8
  export const pushUrlCommand = new Command("push-url")
@@ -21,26 +22,28 @@ Note: this does not execute JavaScript, use browser cookies, or inline external
21
22
  Remote URLs require --allow-remote --yes because the fetched HTML is uploaded to the session.
22
23
  `)
23
24
  .action(async (sessionId, urlArg, opts) => {
24
- try {
25
- const config = requireConfig();
26
- const url = parseHttpUrl(urlArg);
27
- validateSnapshotTarget(url, opts);
28
- const html = await fetchHtml(url);
29
- const data = await api(config, "POST", `/sessions/${sessionId}/push`, {
30
- html,
31
- source_path: url.toString(),
32
- });
33
- if (opts.json) {
34
- console.log(JSON.stringify(data));
25
+ await runWithTelemetry("push_url", async () => {
26
+ try {
27
+ const config = requireConfig();
28
+ const url = parseHttpUrl(urlArg);
29
+ validateSnapshotTarget(url, opts);
30
+ const html = await fetchHtml(url);
31
+ const data = await api(config, "POST", `/sessions/${sessionId}/push`, {
32
+ html,
33
+ source_path: url.toString(),
34
+ });
35
+ if (opts.json) {
36
+ console.log(JSON.stringify(data));
37
+ }
38
+ else {
39
+ console.log(`Pushed v${data.version} to ${sessionId} from ${url.toString()}`);
40
+ }
35
41
  }
36
- else {
37
- console.log(`Pushed v${data.version} to ${sessionId} from ${url.toString()}`);
42
+ catch (err) {
43
+ printCommandError("Push URL failed", err, opts.json);
44
+ throw new CommandFailure("Push URL failed", { cause: err });
38
45
  }
39
- }
40
- catch (err) {
41
- printCommandError("Push URL failed", err, opts.json);
42
- process.exit(1);
43
- }
46
+ });
44
47
  });
45
48
  function parseHttpUrl(urlArg) {
46
49
  let url;
@@ -3,7 +3,8 @@ import { readFileSync } from "fs";
3
3
  import { resolve } from "path";
4
4
  import { requireConfig } from "../config.js";
5
5
  import { api } from "../api.js";
6
- import { printCommandError } from "../command-error.js";
6
+ import { CommandFailure, printCommandError } from "../command-error.js";
7
+ import { runWithTelemetry } from "../telemetry.js";
7
8
  export const pushCommand = new Command("push")
8
9
  .argument("<session-id>", "Session ID")
9
10
  .argument("<file>", "Standalone HTML file path")
@@ -15,25 +16,30 @@ For an HTML response snapshot, use: peekable push-url <session-id> <url>
15
16
  For a live dev server, use: peekable proxy <port>
16
17
  `)
17
18
  .action(async (sessionId, file, opts) => {
18
- try {
19
- const config = requireConfig();
20
- const html = readFileSync(file, "utf-8");
21
- if (!opts.json && looksLikeHtmlFragment(html)) {
22
- console.error("\x1b[33mNote:\x1b[0m this file looks like an HTML fragment. It may render unstyled when shared standalone. If it is part of a parent app, prefer `peekable push-url <session> <url>`. For live dev servers (React/Vite/Next), use `peekable proxy <port>`.");
19
+ const telemetryContext = { session_id: sessionId };
20
+ await runWithTelemetry("push", async () => {
21
+ try {
22
+ const config = requireConfig();
23
+ const html = readFileSync(file, "utf-8");
24
+ telemetryContext.file_bytes = Buffer.byteLength(html, "utf-8");
25
+ telemetryContext.looks_like_fragment = looksLikeHtmlFragment(html);
26
+ if (!opts.json && looksLikeHtmlFragment(html)) {
27
+ console.error("\x1b[33mNote:\x1b[0m this file looks like an HTML fragment. It may render unstyled when shared standalone. If it is part of a parent app, prefer `peekable push-url <session> <url>`. For live dev servers (React/Vite/Next), use `peekable proxy <port>`.");
28
+ }
29
+ const source_path = resolve(file);
30
+ const data = await api(config, "POST", `/sessions/${sessionId}/push`, { html, source_path });
31
+ if (opts.json) {
32
+ console.log(JSON.stringify(data));
33
+ }
34
+ else {
35
+ console.log(`Pushed v${data.version} to ${sessionId}`);
36
+ }
23
37
  }
24
- const source_path = resolve(file);
25
- const data = await api(config, "POST", `/sessions/${sessionId}/push`, { html, source_path });
26
- if (opts.json) {
27
- console.log(JSON.stringify(data));
38
+ catch (err) {
39
+ printCommandError("Push failed", err, opts.json);
40
+ throw new CommandFailure("Push failed", { cause: err });
28
41
  }
29
- else {
30
- console.log(`Pushed v${data.version} to ${sessionId}`);
31
- }
32
- }
33
- catch (err) {
34
- printCommandError("Push failed", err, opts.json);
35
- process.exit(1);
36
- }
42
+ }, telemetryContext);
37
43
  });
38
44
  function looksLikeHtmlFragment(html) {
39
45
  return !/<\s*(html|body)(?:\s|>)/i.test(html);
@@ -1,57 +1,62 @@
1
1
  import { Command } from "commander";
2
- import { writeConfig, readConfig } from "../config.js";
2
+ import { writeConfig, readConfig, DEFAULT_SERVER_URL } from "../config.js";
3
3
  import { apiNoAuth } from "../api.js";
4
- import { printCommandError } from "../command-error.js";
5
- const DEFAULT_URL = "https://peekable.fyi";
4
+ import { CommandFailure, printCommandError } from "../command-error.js";
5
+ import { runWithTelemetry } from "../telemetry.js";
6
6
  export const registerCommand = new Command("register")
7
7
  .description("Register for an API key")
8
8
  .requiredOption("--name <name>", "Your display name")
9
9
  .requiredOption("--email <email>", "Your email address")
10
10
  .option("--invite-code <code>", "Beta invite code")
11
- .option("--url <url>", "Server URL", DEFAULT_URL)
11
+ .option("--url <url>", "Server URL", DEFAULT_SERVER_URL)
12
12
  .option("--json", "Output JSON")
13
13
  .action(async (opts) => {
14
- const existing = readConfig();
15
- if (existing) {
16
- if (opts.json) {
17
- console.log(JSON.stringify({ error: "Already registered. Config exists at ~/.peekable/config.json" }));
14
+ await runWithTelemetry("register", async () => {
15
+ const existing = readConfig();
16
+ if (existing) {
17
+ if (opts.json) {
18
+ console.log(JSON.stringify({ error: "Already registered. Config exists at ~/.peekable/config.json" }));
19
+ }
20
+ else {
21
+ console.error("Already registered. Config exists at ~/.peekable/config.json");
22
+ console.error("Delete ~/.peekable/config.json to re-register.");
23
+ }
24
+ throw new CommandFailure("Already registered.");
18
25
  }
19
- else {
20
- console.error("Already registered. Config exists at ~/.peekable/config.json");
21
- console.error("Delete ~/.peekable/config.json to re-register.");
26
+ try {
27
+ const inviteCode = typeof opts.inviteCode === "string"
28
+ ? opts.inviteCode
29
+ : process.env.PEEKABLE_INVITE_CODE;
30
+ const body = {
31
+ name: opts.name,
32
+ email: opts.email,
33
+ };
34
+ if (inviteCode) {
35
+ body.invite_code = inviteCode;
36
+ }
37
+ const data = await apiNoAuth(opts.url, "POST", "/register", {
38
+ ...body,
39
+ });
40
+ writeConfig({
41
+ url: opts.url,
42
+ api_key: data.api_key,
43
+ name: opts.name,
44
+ });
45
+ if (opts.json) {
46
+ console.log(JSON.stringify({ api_key: data.api_key, url: opts.url, name: opts.name }));
47
+ }
48
+ else {
49
+ console.log(`Registered as ${opts.name}.`);
50
+ console.log(`API key saved to ~/.peekable/config.json`);
51
+ console.log("Peekable CLI sends anonymous usage/failure telemetry to improve reliability. Opt out: PEEKABLE_NO_TELEMETRY=1");
52
+ console.log(`\nNext step: run \`peekable init\` to complete setup.`);
53
+ }
22
54
  }
23
- process.exit(1);
24
- }
25
- try {
26
- const inviteCode = typeof opts.inviteCode === "string"
27
- ? opts.inviteCode
28
- : process.env.PEEKABLE_INVITE_CODE;
29
- const body = {
30
- name: opts.name,
31
- email: opts.email,
32
- };
33
- if (inviteCode) {
34
- body.invite_code = inviteCode;
55
+ catch (err) {
56
+ if (err instanceof CommandFailure)
57
+ throw err;
58
+ printCommandError("Registration failed", err, opts.json);
59
+ throw new CommandFailure("Registration failed", { cause: err });
35
60
  }
36
- const data = await apiNoAuth(opts.url, "POST", "/register", {
37
- ...body,
38
- });
39
- writeConfig({
40
- url: opts.url,
41
- api_key: data.api_key,
42
- name: opts.name,
43
- });
44
- if (opts.json) {
45
- console.log(JSON.stringify({ api_key: data.api_key, url: opts.url, name: opts.name }));
46
- }
47
- else {
48
- console.log(`Registered as ${opts.name}.`);
49
- console.log(`API key saved to ~/.peekable/config.json`);
50
- console.log(`\nNext step: run \`peekable init\` to complete setup.`);
51
- }
52
- }
53
- catch (err) {
54
- printCommandError("Registration failed", err, opts.json);
55
- process.exit(1);
56
- }
61
+ });
57
62
  });
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ import { type DoctorScalarSnapshot } from "./doctor.js";
3
+ export interface ReportPayload {
4
+ message: string;
5
+ diagnostics?: DoctorScalarSnapshot;
6
+ }
7
+ export declare function buildReportPayload(opts: {
8
+ message: string;
9
+ diagnostics: boolean;
10
+ }): Promise<ReportPayload>;
11
+ export declare const reportCommand: Command;
@@ -0,0 +1,107 @@
1
+ import { Command } from "commander";
2
+ import * as readline from "readline";
3
+ import { api } from "../api.js";
4
+ import { requireConfig, ConfigError } from "../config.js";
5
+ import { CommandFailure, printCommandError } from "../command-error.js";
6
+ import { runWithTelemetry } from "../telemetry.js";
7
+ import { runDoctor, toScalarSnapshot } from "./doctor.js";
8
+ const MAX_MESSAGE_LEN = 2000;
9
+ async function promptForMessage() {
10
+ // Piped/closed stdin never answers the question; without a TTY, don't prompt.
11
+ if (!process.stdin.isTTY)
12
+ return "";
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout,
16
+ });
17
+ try {
18
+ return await new Promise((resolve) => {
19
+ rl.question("What went wrong? ", (answer) => resolve(answer));
20
+ // stdin EOF closes the interface without ever invoking the question
21
+ // callback — resolve empty instead of hanging the process.
22
+ rl.once("close", () => resolve(""));
23
+ });
24
+ }
25
+ finally {
26
+ rl.close();
27
+ }
28
+ }
29
+ export async function buildReportPayload(opts) {
30
+ const message = opts.message.slice(0, MAX_MESSAGE_LEN);
31
+ if (!opts.diagnostics)
32
+ return { message };
33
+ const doctor = await runDoctor();
34
+ return { message, diagnostics: toScalarSnapshot(doctor) };
35
+ }
36
+ export const reportCommand = new Command("report")
37
+ .argument("[message...]", "Message to send")
38
+ .description("Report a problem or send feedback to the Peekable team")
39
+ .option("--no-diagnostics", "Opt out of sending diagnostics")
40
+ .option("--json", "Output JSON")
41
+ .action(async (messageParts, opts) => {
42
+ await runWithTelemetry("report", async () => {
43
+ let config;
44
+ try {
45
+ config = requireConfig();
46
+ }
47
+ catch (err) {
48
+ if (err instanceof ConfigError) {
49
+ const friendly = err.reason === "corrupt"
50
+ ? "Config file is corrupted. Run `peekable register` to reconfigure."
51
+ : "Peekable isn't configured yet. Run `peekable register` first, or file an issue at https://github.com/peekable/peekable/issues.";
52
+ if (opts.json) {
53
+ console.log(JSON.stringify({ error: friendly }));
54
+ }
55
+ else {
56
+ console.error(friendly);
57
+ }
58
+ throw new CommandFailure("Report requires registration", { cause: err });
59
+ }
60
+ throw err;
61
+ }
62
+ let message = messageParts.join(" ").trim();
63
+ if (!message) {
64
+ try {
65
+ message = (await promptForMessage()).trim();
66
+ }
67
+ catch {
68
+ message = "";
69
+ }
70
+ }
71
+ if (!message) {
72
+ const msg = "No message provided.";
73
+ if (opts.json) {
74
+ console.log(JSON.stringify({ error: msg }));
75
+ }
76
+ else {
77
+ console.error(msg);
78
+ }
79
+ throw new CommandFailure(msg);
80
+ }
81
+ // Diagnostics ON by default; commander sets opts.diagnostics = false when --no-diagnostics passed.
82
+ const includeDiagnostics = opts.diagnostics !== false;
83
+ const payload = await buildReportPayload({ message, diagnostics: includeDiagnostics });
84
+ if (!opts.json) {
85
+ console.log("Sending report:");
86
+ console.log(JSON.stringify(payload, null, 2));
87
+ }
88
+ try {
89
+ const res = await api(config, "POST", "/cli/report", payload);
90
+ const requestId = res && typeof res === "object" && typeof res.request_id === "string"
91
+ ? res.request_id
92
+ : undefined;
93
+ if (opts.json) {
94
+ console.log(JSON.stringify({ ok: true, request_id: requestId }));
95
+ }
96
+ else {
97
+ console.log("Report submitted. Thanks — we read every one.");
98
+ if (requestId)
99
+ console.log(`Request ID: ${requestId}`);
100
+ }
101
+ }
102
+ catch (err) {
103
+ printCommandError("Report failed", err, opts.json);
104
+ throw new CommandFailure("Report failed", { cause: err });
105
+ }
106
+ });
107
+ });
@@ -1,20 +1,30 @@
1
1
  import { Command } from "commander";
2
2
  import { requireConfig } from "../config.js";
3
3
  import { api } from "../api.js";
4
+ import { CommandFailure, printCommandError } from "../command-error.js";
5
+ import { runWithTelemetry } from "../telemetry.js";
4
6
  export const resolveCommand = new Command("resolve")
5
7
  .argument("<session-id>", "Session ID")
6
8
  .argument("<annotation-ids...>", "Annotation IDs to resolve")
7
9
  .description("Mark annotations as resolved")
8
10
  .option("--json", "Output JSON")
9
11
  .action(async (sessionId, annotationIds, opts) => {
10
- const config = requireConfig();
11
- const data = await api(config, "PATCH", `/sessions/${sessionId}/annotations/resolve`, {
12
- annotationIds,
12
+ await runWithTelemetry("resolve", async () => {
13
+ try {
14
+ const config = requireConfig();
15
+ const data = await api(config, "PATCH", `/sessions/${sessionId}/annotations/resolve`, {
16
+ annotationIds,
17
+ });
18
+ if (opts.json) {
19
+ console.log(JSON.stringify(data));
20
+ }
21
+ else {
22
+ console.log(`Resolved ${data.resolved} annotation(s).`);
23
+ }
24
+ }
25
+ catch (err) {
26
+ printCommandError("Resolve failed", err, opts.json);
27
+ throw new CommandFailure("Resolve failed", { cause: err });
28
+ }
13
29
  });
14
- if (opts.json) {
15
- console.log(JSON.stringify(data));
16
- }
17
- else {
18
- console.log(`Resolved ${data.resolved} annotation(s).`);
19
- }
20
30
  });
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { existsSync, rmSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { getClaudePeekableSkillDir, getCodexPeekableSkillDir, getPeekableConfigDir } from "../paths.js";
5
+ import { runWithTelemetry } from "../telemetry.js";
5
6
  function getUninstallTargets(home = homedir()) {
6
7
  return [
7
8
  {
@@ -69,17 +70,19 @@ export function createUninstallCommand() {
69
70
  return new Command("uninstall")
70
71
  .description("Remove local Peekable config and installed Claude Code skill")
71
72
  .option("--json", "Output JSON")
72
- .action((opts) => {
73
- const result = removeLocalPeekableFiles();
74
- if (opts.json) {
75
- console.log(JSON.stringify(result));
76
- }
77
- else {
78
- console.log(formatUninstallResult(result));
79
- }
80
- if (result.status !== "ok") {
81
- process.exitCode = 1;
82
- }
73
+ .action(async (opts) => {
74
+ await runWithTelemetry("uninstall", async () => {
75
+ const result = removeLocalPeekableFiles();
76
+ if (opts.json) {
77
+ console.log(JSON.stringify(result));
78
+ }
79
+ else {
80
+ console.log(formatUninstallResult(result));
81
+ }
82
+ if (result.status !== "ok") {
83
+ process.exitCode = 1;
84
+ }
85
+ });
83
86
  });
84
87
  }
85
88
  export const uninstallCommand = createUninstallCommand();