peekable 0.1.4 → 0.2.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/dist/api.d.ts +1 -0
- package/dist/api.js +19 -1
- package/dist/command-error.d.ts +8 -0
- package/dist/command-error.js +33 -2
- package/dist/commands/close.js +18 -8
- package/dist/commands/create.js +17 -14
- package/dist/commands/doctor.d.ts +30 -0
- package/dist/commands/doctor.js +131 -10
- package/dist/commands/feedback.js +53 -43
- package/dist/commands/init.js +48 -42
- package/dist/commands/list.js +32 -22
- package/dist/commands/proxy.js +45 -13
- package/dist/commands/push-url.js +22 -19
- package/dist/commands/push.js +24 -18
- package/dist/commands/register.js +62 -44
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.js +107 -0
- package/dist/commands/resolve.js +19 -9
- package/dist/commands/uninstall.js +14 -11
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +63 -0
- package/dist/commands/watch.js +37 -3
- package/dist/config.d.ts +16 -0
- package/dist/config.js +64 -15
- package/dist/index.js +36 -1
- package/dist/telemetry.d.ts +26 -0
- package/dist/telemetry.js +297 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
package/dist/api.d.ts
CHANGED
package/dist/api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { CLI_VERSION } from "./version.js";
|
|
1
2
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
3
|
+
export const USER_AGENT = `peekable-cli/${CLI_VERSION} (${process.platform}; ${process.arch}; node ${process.version})`;
|
|
2
4
|
export class ApiError extends Error {
|
|
3
5
|
status;
|
|
4
6
|
details;
|
|
@@ -15,6 +17,7 @@ export async function api(config, method, path, body, opts = {}) {
|
|
|
15
17
|
headers: {
|
|
16
18
|
Authorization: `Bearer ${config.api_key}`,
|
|
17
19
|
"Content-Type": "application/json",
|
|
20
|
+
"User-Agent": USER_AGENT,
|
|
18
21
|
},
|
|
19
22
|
body: body ? JSON.stringify(body) : undefined,
|
|
20
23
|
}, opts.timeoutMs);
|
|
@@ -29,7 +32,10 @@ export async function postDebugEvent(config, body) {
|
|
|
29
32
|
export async function apiNoAuth(url, method, path, body, opts = {}) {
|
|
30
33
|
const res = await fetchWithTimeout(`${url}/api${path}`, {
|
|
31
34
|
method,
|
|
32
|
-
headers: {
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"User-Agent": USER_AGENT,
|
|
38
|
+
},
|
|
33
39
|
body: body ? JSON.stringify(body) : undefined,
|
|
34
40
|
}, opts.timeoutMs);
|
|
35
41
|
if (!res.ok) {
|
|
@@ -40,9 +46,14 @@ export async function apiNoAuth(url, method, path, body, opts = {}) {
|
|
|
40
46
|
export async function fetchWithTimeout(input, init = {}, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
41
47
|
const controller = new AbortController();
|
|
42
48
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
49
|
+
const headers = new Headers(init.headers);
|
|
50
|
+
if (!headers.has("User-Agent")) {
|
|
51
|
+
headers.set("User-Agent", USER_AGENT);
|
|
52
|
+
}
|
|
43
53
|
try {
|
|
44
54
|
return await fetch(input, {
|
|
45
55
|
...init,
|
|
56
|
+
headers,
|
|
46
57
|
signal: init.signal ?? controller.signal,
|
|
47
58
|
});
|
|
48
59
|
}
|
|
@@ -61,5 +72,12 @@ async function buildApiError(res) {
|
|
|
61
72
|
const body = details && typeof details === "object" && !Array.isArray(details)
|
|
62
73
|
? details
|
|
63
74
|
: { error: res.statusText };
|
|
75
|
+
// Every response carries X-Request-Id, but only 500 bodies embed it —
|
|
76
|
+
// lift it from the header so 4xx failures stay correlatable too.
|
|
77
|
+
if (typeof body.request_id !== "string") {
|
|
78
|
+
const headerRequestId = res.headers.get("x-request-id");
|
|
79
|
+
if (headerRequestId)
|
|
80
|
+
body.request_id = headerRequestId;
|
|
81
|
+
}
|
|
64
82
|
return new ApiError(res.status, body, res.statusText);
|
|
65
83
|
}
|
package/dist/command-error.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export declare class CommandFailure extends Error {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
cause?: unknown;
|
|
4
|
+
constructor(message: string, opts?: {
|
|
5
|
+
exitCode?: number;
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
1
9
|
export declare function errorPayload(err: unknown): Record<string, unknown>;
|
|
2
10
|
export declare function getErrorMessage(err: unknown): string;
|
|
3
11
|
export declare function printCommandError(prefix: string, err: unknown, json?: boolean): void;
|
package/dist/command-error.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { ApiError } from "./api.js";
|
|
2
|
+
export class CommandFailure extends Error {
|
|
3
|
+
exitCode;
|
|
4
|
+
cause;
|
|
5
|
+
constructor(message, opts = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CommandFailure";
|
|
8
|
+
this.exitCode = opts.exitCode ?? 1;
|
|
9
|
+
this.cause = opts.cause;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
2
12
|
export function errorPayload(err) {
|
|
3
13
|
if (err instanceof ApiError) {
|
|
4
14
|
return {
|
|
@@ -10,14 +20,35 @@ export function errorPayload(err) {
|
|
|
10
20
|
return { error: getErrorMessage(err) };
|
|
11
21
|
}
|
|
12
22
|
export function getErrorMessage(err) {
|
|
23
|
+
if (err instanceof CommandFailure) {
|
|
24
|
+
if (err.cause !== undefined)
|
|
25
|
+
return getErrorMessage(err.cause);
|
|
26
|
+
return err.message;
|
|
27
|
+
}
|
|
28
|
+
if (err instanceof ApiError && err.details.code === "trial_expired") {
|
|
29
|
+
const upgradeUrl = typeof err.details.upgrade_url === "string" ? err.details.upgrade_url : null;
|
|
30
|
+
return `Your Peekable trial has ended. Run \`peekable upgrade\` to keep pushing.${upgradeUrl ? ` Upgrade: ${upgradeUrl}` : ""}`;
|
|
31
|
+
}
|
|
13
32
|
if (err instanceof Error)
|
|
14
33
|
return err.message;
|
|
15
34
|
return String(err);
|
|
16
35
|
}
|
|
36
|
+
function getRequestId(err) {
|
|
37
|
+
const source = err instanceof CommandFailure ? err.cause : err;
|
|
38
|
+
if (source instanceof ApiError) {
|
|
39
|
+
const rid = source.details.request_id;
|
|
40
|
+
if (typeof rid === "string" && rid)
|
|
41
|
+
return rid;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
17
45
|
export function printCommandError(prefix, err, json) {
|
|
46
|
+
const cause = err instanceof CommandFailure && err.cause !== undefined ? err.cause : err;
|
|
18
47
|
if (json) {
|
|
19
|
-
console.log(JSON.stringify(errorPayload(
|
|
48
|
+
console.log(JSON.stringify(errorPayload(cause)));
|
|
20
49
|
return;
|
|
21
50
|
}
|
|
22
|
-
|
|
51
|
+
const requestId = getRequestId(err);
|
|
52
|
+
const suffix = requestId ? ` (request_id: ${requestId})` : "";
|
|
53
|
+
console.error(`${prefix}: ${getErrorMessage(err)}${suffix}`);
|
|
23
54
|
}
|
package/dist/commands/close.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
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 closeCommand = new Command("close")
|
|
5
7
|
.argument("<session-id>", "Session ID")
|
|
6
8
|
.description("Close a session")
|
|
7
9
|
.option("--json", "Output JSON")
|
|
8
10
|
.action(async (sessionId, opts) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
await runWithTelemetry("close", async () => {
|
|
12
|
+
try {
|
|
13
|
+
const config = requireConfig();
|
|
14
|
+
await api(config, "DELETE", `/sessions/${sessionId}`);
|
|
15
|
+
if (opts.json) {
|
|
16
|
+
console.log(JSON.stringify({ ok: true }));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log(`Closed session ${sessionId}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
printCommandError("Close failed", err, opts.json);
|
|
24
|
+
throw new CommandFailure("Close failed", { cause: err });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
17
27
|
});
|
package/dist/commands/create.js
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { requireConfig } from "../config.js";
|
|
3
3
|
import { api } 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
|
export const createCommand = new Command("create")
|
|
6
7
|
.argument("<name>", "Session name")
|
|
7
8
|
.description("Create a new sharing session")
|
|
8
9
|
.option("--json", "Output JSON")
|
|
9
10
|
.action(async (name, opts) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
await runWithTelemetry("create", async () => {
|
|
12
|
+
try {
|
|
13
|
+
const config = requireConfig();
|
|
14
|
+
const data = await api(config, "POST", "/sessions", { name });
|
|
15
|
+
if (opts.json) {
|
|
16
|
+
console.log(JSON.stringify(data));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log(`Created session: ${data.id}`);
|
|
20
|
+
console.log(`URL: ${data.url}`);
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
catch (err) {
|
|
24
|
+
printCommandError("Create failed", err, opts.json);
|
|
25
|
+
throw new CommandFailure("Create failed", { cause: err });
|
|
19
26
|
}
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
printCommandError("Create failed", err, opts.json);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
27
|
+
});
|
|
25
28
|
});
|
|
@@ -3,19 +3,49 @@ export type DoctorStatus = "ok" | "failed" | "skipped";
|
|
|
3
3
|
export interface DoctorReport {
|
|
4
4
|
cliVersion: string;
|
|
5
5
|
nodeVersion: string;
|
|
6
|
+
os: string;
|
|
7
|
+
osRelease: string;
|
|
8
|
+
arch: string;
|
|
9
|
+
isTty: boolean;
|
|
6
10
|
configPath: string;
|
|
7
11
|
configured: boolean;
|
|
8
12
|
serverUrl: string | null;
|
|
9
13
|
health: DoctorStatus;
|
|
10
14
|
auth: DoctorStatus;
|
|
15
|
+
identity: DoctorStatus;
|
|
16
|
+
identityName?: string | null;
|
|
17
|
+
identityEmail?: string | null;
|
|
18
|
+
planState?: string | null;
|
|
19
|
+
daysRemaining?: number | null;
|
|
11
20
|
activeSessions: number | null;
|
|
12
21
|
maxActiveSessions: number | null;
|
|
13
22
|
activeSessionUnlimited?: boolean;
|
|
14
23
|
testPush: DoctorStatus;
|
|
15
24
|
errors?: string[];
|
|
16
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Scalar-only snapshot suitable for authenticated telemetry. Excludes raw
|
|
28
|
+
* error strings, identity fields, and any path or free-form text.
|
|
29
|
+
*/
|
|
30
|
+
export interface DoctorScalarSnapshot {
|
|
31
|
+
cli_version: string;
|
|
32
|
+
node_version: string;
|
|
33
|
+
os: string;
|
|
34
|
+
os_release: string;
|
|
35
|
+
arch: string;
|
|
36
|
+
is_tty: boolean;
|
|
37
|
+
configured: boolean;
|
|
38
|
+
health: DoctorStatus;
|
|
39
|
+
auth: DoctorStatus;
|
|
40
|
+
identity: DoctorStatus;
|
|
41
|
+
test_push: DoctorStatus;
|
|
42
|
+
active_sessions: number | null;
|
|
43
|
+
max_active_sessions: number | null;
|
|
44
|
+
active_session_unlimited: boolean;
|
|
45
|
+
}
|
|
17
46
|
export declare const doctorCommand: Command;
|
|
18
47
|
export declare function runDoctor(opts?: {
|
|
19
48
|
testPush?: boolean;
|
|
20
49
|
}): Promise<DoctorReport>;
|
|
50
|
+
export declare function toScalarSnapshot(report: DoctorReport): DoctorScalarSnapshot;
|
|
21
51
|
export declare function formatDoctorReport(report: DoctorReport): string;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,35 +1,90 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { release } from "os";
|
|
3
|
+
import { api, fetchWithTimeout, postDebugEvent } from "../api.js";
|
|
4
|
+
import { ConfigError, getConfigPath, readConfig, tryReadConfig } from "../config.js";
|
|
5
|
+
import { CommandFailure, getErrorMessage } from "../command-error.js";
|
|
5
6
|
import { formatQuota } from "../quota.js";
|
|
6
7
|
import { CLI_VERSION } from "../version.js";
|
|
8
|
+
import { runWithTelemetry } from "../telemetry.js";
|
|
7
9
|
export const doctorCommand = new Command("doctor")
|
|
8
10
|
.description("Print a support snapshot for debugging Peekable setup")
|
|
9
11
|
.option("--json", "Output JSON")
|
|
10
12
|
.option("--test-push", "Create, push, and close a temporary test session")
|
|
13
|
+
.option("--report", "Send a scalar snapshot to Peekable as authenticated telemetry")
|
|
11
14
|
.action(async (opts) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
await runWithTelemetry("doctor", async () => {
|
|
16
|
+
const report = await runDoctor({ testPush: Boolean(opts.testPush) });
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(JSON.stringify(report));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log(formatDoctorReport(report));
|
|
22
|
+
}
|
|
23
|
+
if (opts.report) {
|
|
24
|
+
const configResult = tryReadConfig();
|
|
25
|
+
if (configResult.state !== "ok") {
|
|
26
|
+
// Distinguish corrupt from missing so telemetry classifies the
|
|
27
|
+
// beacon as cli_config_corrupt rather than the generic catch-all.
|
|
28
|
+
if (configResult.state === "corrupt") {
|
|
29
|
+
const msg = "Cannot send doctor report: config file is corrupted. Run `peekable register` to reconfigure.";
|
|
30
|
+
console.error(msg);
|
|
31
|
+
throw new CommandFailure(msg, {
|
|
32
|
+
cause: new ConfigError("corrupt", configResult.parseError),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
console.error("Cannot send doctor report: not configured. Run `peekable register` first.");
|
|
36
|
+
throw new CommandFailure("Not configured");
|
|
37
|
+
}
|
|
38
|
+
const config = configResult.config;
|
|
39
|
+
const snapshot = toScalarSnapshot(report);
|
|
40
|
+
if (!opts.json) {
|
|
41
|
+
console.log("\nSending doctor report:");
|
|
42
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await postDebugEvent(config, {
|
|
46
|
+
event_name: "cli_doctor_ran",
|
|
47
|
+
source: "cli",
|
|
48
|
+
command: "doctor",
|
|
49
|
+
cli_version: CLI_VERSION,
|
|
50
|
+
status: "ok",
|
|
51
|
+
metadata: {
|
|
52
|
+
...snapshot,
|
|
53
|
+
diagnostics_json: JSON.stringify(snapshot),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (!opts.json)
|
|
57
|
+
console.log("Report sent.");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (!opts.json)
|
|
61
|
+
console.error(`Report send failed: ${getErrorMessage(err)}`);
|
|
62
|
+
throw new CommandFailure("Report send failed", { cause: err });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
18
66
|
});
|
|
19
67
|
export async function runDoctor(opts = {}) {
|
|
20
68
|
const config = readConfig();
|
|
21
69
|
const report = {
|
|
22
70
|
cliVersion: CLI_VERSION,
|
|
23
71
|
nodeVersion: process.version,
|
|
72
|
+
os: process.platform,
|
|
73
|
+
osRelease: release(),
|
|
74
|
+
arch: process.arch,
|
|
75
|
+
isTty: Boolean(process.stdout.isTTY),
|
|
24
76
|
configPath: redactHomePath(getConfigPath()),
|
|
25
77
|
configured: Boolean(config),
|
|
26
78
|
serverUrl: config?.url ?? null,
|
|
27
79
|
health: "skipped",
|
|
28
80
|
auth: "skipped",
|
|
81
|
+
identity: "skipped",
|
|
29
82
|
activeSessions: null,
|
|
30
83
|
maxActiveSessions: null,
|
|
31
84
|
activeSessionUnlimited: false,
|
|
32
85
|
testPush: "skipped",
|
|
86
|
+
planState: null,
|
|
87
|
+
daysRemaining: null,
|
|
33
88
|
errors: [],
|
|
34
89
|
};
|
|
35
90
|
if (!config) {
|
|
@@ -57,9 +112,27 @@ export async function runDoctor(opts = {}) {
|
|
|
57
112
|
report.auth = "failed";
|
|
58
113
|
report.errors.push(`Auth check failed: ${getErrorMessage(err)}`);
|
|
59
114
|
}
|
|
115
|
+
try {
|
|
116
|
+
const me = await api(config, "GET", "/me");
|
|
117
|
+
report.identity = "ok";
|
|
118
|
+
if (me && typeof me === "object") {
|
|
119
|
+
const anyMe = me;
|
|
120
|
+
report.identityName = typeof anyMe.name === "string" ? anyMe.name : null;
|
|
121
|
+
report.identityEmail = typeof anyMe.email === "string" ? anyMe.email : null;
|
|
122
|
+
report.planState = typeof anyMe.plan_state === "string" ? anyMe.plan_state : null;
|
|
123
|
+
report.daysRemaining = typeof anyMe.days_remaining === "number" ? anyMe.days_remaining : null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
report.identity = "failed";
|
|
128
|
+
report.errors.push(`Identity check failed: ${getErrorMessage(err)}`);
|
|
129
|
+
}
|
|
60
130
|
if (opts.testPush && report.auth === "ok") {
|
|
61
131
|
try {
|
|
62
|
-
const session = await api(config, "POST", "/sessions", {
|
|
132
|
+
const session = await api(config, "POST", "/sessions", {
|
|
133
|
+
name: "__peekable_doctor__",
|
|
134
|
+
diagnostic: true,
|
|
135
|
+
});
|
|
63
136
|
try {
|
|
64
137
|
await api(config, "POST", `/sessions/${session.id}/push`, {
|
|
65
138
|
html: "<html><body><p>Peekable doctor test</p></body></html>",
|
|
@@ -79,13 +152,35 @@ export async function runDoctor(opts = {}) {
|
|
|
79
152
|
delete report.errors;
|
|
80
153
|
return report;
|
|
81
154
|
}
|
|
155
|
+
export function toScalarSnapshot(report) {
|
|
156
|
+
return {
|
|
157
|
+
cli_version: report.cliVersion,
|
|
158
|
+
node_version: report.nodeVersion,
|
|
159
|
+
os: report.os,
|
|
160
|
+
os_release: report.osRelease,
|
|
161
|
+
arch: report.arch,
|
|
162
|
+
is_tty: report.isTty,
|
|
163
|
+
configured: report.configured,
|
|
164
|
+
health: report.health,
|
|
165
|
+
auth: report.auth,
|
|
166
|
+
identity: report.identity,
|
|
167
|
+
test_push: report.testPush,
|
|
168
|
+
active_sessions: report.activeSessions,
|
|
169
|
+
max_active_sessions: report.maxActiveSessions,
|
|
170
|
+
active_session_unlimited: Boolean(report.activeSessionUnlimited),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
82
173
|
export function formatDoctorReport(report) {
|
|
83
174
|
const lines = [
|
|
84
175
|
`Peekable CLI: ${report.cliVersion}`,
|
|
85
176
|
`Node: ${report.nodeVersion}`,
|
|
177
|
+
`OS: ${report.os} ${report.osRelease} (${report.arch})`,
|
|
178
|
+
`TTY: ${report.isTty ? "yes" : "no"}`,
|
|
86
179
|
`Config: ${report.configured ? report.configPath : "missing"}`,
|
|
87
180
|
`Server: ${formatServer(report)}`,
|
|
88
181
|
`Auth: ${formatAuth(report.auth)}`,
|
|
182
|
+
`Identity: ${formatIdentity(report)}`,
|
|
183
|
+
`Plan: ${formatPlan(report)}`,
|
|
89
184
|
`Active sessions: ${formatQuota(report.activeSessions, report.maxActiveSessions, report.activeSessionUnlimited)}`,
|
|
90
185
|
`Test push: ${report.testPush}`,
|
|
91
186
|
];
|
|
@@ -102,6 +197,19 @@ export function formatDoctorReport(report) {
|
|
|
102
197
|
}
|
|
103
198
|
return lines.join("\n");
|
|
104
199
|
}
|
|
200
|
+
function formatPlan(report) {
|
|
201
|
+
if (report.planState === "trial_registered")
|
|
202
|
+
return "trial — not activated";
|
|
203
|
+
if (report.planState === "trial_active") {
|
|
204
|
+
const days = report.daysRemaining ?? 0;
|
|
205
|
+
return `trial (${days} ${days === 1 ? "day" : "days"} left)`;
|
|
206
|
+
}
|
|
207
|
+
if (report.planState === "trial_expired")
|
|
208
|
+
return "trial expired — run `peekable upgrade`";
|
|
209
|
+
if (report.planState === "paid")
|
|
210
|
+
return "paid";
|
|
211
|
+
return "free";
|
|
212
|
+
}
|
|
105
213
|
function formatServer(report) {
|
|
106
214
|
if (!report.serverUrl)
|
|
107
215
|
return "not configured";
|
|
@@ -118,6 +226,19 @@ function formatAuth(status) {
|
|
|
118
226
|
return "failed";
|
|
119
227
|
return "not checked";
|
|
120
228
|
}
|
|
229
|
+
function formatIdentity(report) {
|
|
230
|
+
if (report.identity === "ok") {
|
|
231
|
+
const parts = [];
|
|
232
|
+
if (report.identityName)
|
|
233
|
+
parts.push(report.identityName);
|
|
234
|
+
if (report.identityEmail)
|
|
235
|
+
parts.push(`<${report.identityEmail}>`);
|
|
236
|
+
return parts.length ? parts.join(" ") : "ok";
|
|
237
|
+
}
|
|
238
|
+
if (report.identity === "failed")
|
|
239
|
+
return "failed";
|
|
240
|
+
return "not checked";
|
|
241
|
+
}
|
|
121
242
|
async function getUsageSnapshot(config) {
|
|
122
243
|
try {
|
|
123
244
|
return await api(config, "GET", "/me/usage");
|
|
@@ -1,56 +1,66 @@
|
|
|
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 feedbackCommand = new Command("feedback")
|
|
5
7
|
.argument("<session-id>", "Session ID")
|
|
6
8
|
.description("Fetch feedback for a session")
|
|
7
9
|
.option("--json", "Output JSON")
|
|
8
10
|
.action(async (sessionId, opts) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const hasAnnotations = annotationsData.annotations && annotationsData.annotations.length > 0;
|
|
20
|
-
if (!hasFeedback && !hasAnnotations) {
|
|
21
|
-
console.log("No feedback yet.");
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
// Display choice feedback
|
|
25
|
-
if (hasFeedback) {
|
|
26
|
-
console.log("\nChoice Feedback:");
|
|
27
|
-
for (const group of eventsData.feedback) {
|
|
28
|
-
console.log(`\n--- Version ${group.version} ---`);
|
|
29
|
-
for (const event of group.events) {
|
|
30
|
-
const who = event.viewer ?? "Anonymous";
|
|
31
|
-
console.log(` ${who} chose "${event.choice}" — ${event.label}`);
|
|
11
|
+
await runWithTelemetry("feedback", async () => {
|
|
12
|
+
try {
|
|
13
|
+
const config = requireConfig();
|
|
14
|
+
const [eventsData, annotationsData] = await Promise.all([
|
|
15
|
+
api(config, "GET", `/sessions/${sessionId}/feedback`),
|
|
16
|
+
api(config, "GET", `/sessions/${sessionId}/annotations`).catch(() => ({ annotations: [] })),
|
|
17
|
+
]);
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({ ...eventsData, ...annotationsData }));
|
|
20
|
+
return;
|
|
32
21
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.map(([key, val]) => `${key}: ${val}`)
|
|
49
|
-
.join(", ");
|
|
50
|
-
if (styles)
|
|
51
|
-
console.log(` Styles: ${styles}`);
|
|
22
|
+
const hasFeedback = eventsData.feedback.length > 0;
|
|
23
|
+
const hasAnnotations = annotationsData.annotations && annotationsData.annotations.length > 0;
|
|
24
|
+
if (!hasFeedback && !hasAnnotations) {
|
|
25
|
+
console.log("No feedback yet.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Display choice feedback
|
|
29
|
+
if (hasFeedback) {
|
|
30
|
+
console.log("\nChoice Feedback:");
|
|
31
|
+
for (const group of eventsData.feedback) {
|
|
32
|
+
console.log(`\n--- Version ${group.version} ---`);
|
|
33
|
+
for (const event of group.events) {
|
|
34
|
+
const who = event.viewer ?? "Anonymous";
|
|
35
|
+
console.log(` ${who} chose "${event.choice}" — ${event.label}`);
|
|
36
|
+
}
|
|
52
37
|
}
|
|
53
38
|
}
|
|
39
|
+
// Display annotations
|
|
40
|
+
if (hasAnnotations) {
|
|
41
|
+
console.log("\nAnnotations:");
|
|
42
|
+
for (const group of annotationsData.annotations) {
|
|
43
|
+
console.log(`\n--- Version ${group.version} ---`);
|
|
44
|
+
for (const ann of group.items) {
|
|
45
|
+
const who = ann.viewer ?? "Anonymous";
|
|
46
|
+
const elementName = ann.element_name || "Unknown";
|
|
47
|
+
const selector = ann.selector || "";
|
|
48
|
+
console.log(` [${who}] ${elementName} (${selector})`);
|
|
49
|
+
console.log(` "${ann.note}"`);
|
|
50
|
+
if (ann.element_context?.computedStyles) {
|
|
51
|
+
const styles = Object.entries(ann.element_context.computedStyles)
|
|
52
|
+
.map(([key, val]) => `${key}: ${val}`)
|
|
53
|
+
.join(", ");
|
|
54
|
+
if (styles)
|
|
55
|
+
console.log(` Styles: ${styles}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
printCommandError("Feedback failed", err, opts.json);
|
|
63
|
+
throw new CommandFailure("Feedback failed", { cause: err });
|
|
54
64
|
}
|
|
55
|
-
}
|
|
65
|
+
});
|
|
56
66
|
});
|