@trawlme/cli 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -5
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +52 -3
- package/dist/commands/login.js +29 -9
- package/dist/commands/scraps.js +225 -95
- package/dist/commands/telemetry.js +9 -5
- package/dist/commands/token.js +7 -21
- package/dist/index.d.ts +19 -1
- package/dist/index.js +113 -42
- package/dist/lib/api.d.ts +14 -1
- package/dist/lib/api.js +133 -19
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/errors.d.ts +41 -0
- package/dist/lib/errors.js +59 -0
- package/dist/lib/jwt.d.ts +8 -0
- package/dist/lib/jwt.js +22 -0
- package/dist/lib/posthog.d.ts +9 -0
- package/dist/lib/posthog.js +47 -3
- package/dist/lib/prompt.js +13 -2
- package/dist/lib/validate.d.ts +9 -0
- package/dist/lib/validate.js +24 -5
- package/package.json +2 -2
package/dist/lib/posthog.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PostHog } from 'posthog-node';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
5
|
import { readFileSync } from 'node:fs';
|
|
@@ -8,26 +9,66 @@ import config from './config.js';
|
|
|
8
9
|
// public-by-design — cf memory feedback_public_keys_in_config.md
|
|
9
10
|
const POSTHOG_KEY = 'phc_pGgSLivbq8rDZzXC9trWWJG7sBjNghXeTJjETzBcnEDv';
|
|
10
11
|
const POSTHOG_HOST = 'https://eu.i.posthog.com';
|
|
12
|
+
// Fail fast: a firewalled/offline endpoint must never add tens of seconds of
|
|
13
|
+
// trailing latency to a CLI command. Library defaults are requestTimeout 10s x
|
|
14
|
+
// 3 retries at a 3s delay (~40s+ worst case per call), and a 30s shutdown
|
|
15
|
+
// ceiling on top of that — cap all three low.
|
|
16
|
+
const REQUEST_TIMEOUT_MS = 3000;
|
|
17
|
+
const FETCH_RETRY_COUNT = 1;
|
|
18
|
+
const FETCH_RETRY_DELAY_MS = 250;
|
|
19
|
+
// A touch above one request + one retry, so the shutdown-time flush has room
|
|
20
|
+
// to complete normally without inheriting the library's 30s default ceiling.
|
|
21
|
+
const SHUTDOWN_TIMEOUT_MS = 8000;
|
|
11
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
23
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
|
|
13
24
|
let client = null;
|
|
14
25
|
let distinctId = null;
|
|
26
|
+
let allowedCommands = new Set();
|
|
27
|
+
/**
|
|
28
|
+
* Cross-vendor opt-out (https://consoledonottrack.com) on top of the existing
|
|
29
|
+
* config/env gates. Any one of these disables telemetry entirely.
|
|
30
|
+
*/
|
|
31
|
+
function isTelemetryDisabled() {
|
|
32
|
+
return (config.get('telemetry') === false ||
|
|
33
|
+
process.env['TRAWL_TELEMETRY'] === '0' ||
|
|
34
|
+
process.env['DO_NOT_TRACK'] === '1');
|
|
35
|
+
}
|
|
15
36
|
export const initPostHog = () => {
|
|
16
|
-
if (
|
|
37
|
+
if (isTelemetryDisabled())
|
|
17
38
|
return;
|
|
18
|
-
|
|
39
|
+
const isFirstRun = !config.get('telemetryUserId');
|
|
40
|
+
if (isFirstRun) {
|
|
19
41
|
config.set('telemetryUserId', `cli_${crypto.randomUUID()}`);
|
|
42
|
+
// One-time disclosure — telemetry is on by default and otherwise silent.
|
|
43
|
+
console.error(chalk.dim('ℹ Trawl CLI collects anonymous usage telemetry (no credentials or command arguments). ' +
|
|
44
|
+
'Opt out: trawl telemetry off, TRAWL_TELEMETRY=0, or DO_NOT_TRACK=1.'));
|
|
20
45
|
}
|
|
21
46
|
distinctId = config.get('telemetryUserId');
|
|
22
47
|
client = new PostHog(POSTHOG_KEY, {
|
|
23
48
|
host: POSTHOG_HOST,
|
|
24
49
|
flushAt: 1,
|
|
25
50
|
flushInterval: 0,
|
|
51
|
+
requestTimeout: REQUEST_TIMEOUT_MS,
|
|
52
|
+
fetchRetryCount: FETCH_RETRY_COUNT,
|
|
53
|
+
fetchRetryDelay: FETCH_RETRY_DELAY_MS,
|
|
26
54
|
});
|
|
27
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Register the complete set of valid "<parent> <name>" telemetry tokens,
|
|
58
|
+
* derived from the actual commander command tree (see resolveCommandName /
|
|
59
|
+
* collectCommandNames in src/index.ts). captureCommand refuses any command
|
|
60
|
+
* string outside this set — defense in depth so free-form user input (argv
|
|
61
|
+
* operands, flag values) can never reach PostHog as the event name, even if a
|
|
62
|
+
* future change accidentally reintroduces argv-derived naming.
|
|
63
|
+
*/
|
|
64
|
+
export const registerAllowedCommands = (names) => {
|
|
65
|
+
allowedCommands = new Set(names);
|
|
66
|
+
};
|
|
28
67
|
export const captureCommand = (command, props = {}) => {
|
|
29
68
|
if (!client || !distinctId)
|
|
30
69
|
return;
|
|
70
|
+
if (!allowedCommands.has(command))
|
|
71
|
+
return;
|
|
31
72
|
client.capture({
|
|
32
73
|
distinctId,
|
|
33
74
|
event: 'cli.command.run',
|
|
@@ -42,11 +83,14 @@ export const captureCommand = (command, props = {}) => {
|
|
|
42
83
|
});
|
|
43
84
|
};
|
|
44
85
|
export const shutdown = async () => {
|
|
86
|
+
// Explicit ceiling — client.shutdown() otherwise defaults to a 30s internal
|
|
87
|
+
// race, which would undercut the whole point of capping request/retry time.
|
|
45
88
|
if (client)
|
|
46
|
-
await client.shutdown();
|
|
89
|
+
await client.shutdown(SHUTDOWN_TIMEOUT_MS);
|
|
47
90
|
};
|
|
48
91
|
/** Reset singleton state — for testing only */
|
|
49
92
|
export const _resetForTesting = () => {
|
|
50
93
|
client = null;
|
|
51
94
|
distinctId = null;
|
|
95
|
+
allowedCommands = new Set();
|
|
52
96
|
};
|
package/dist/lib/prompt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export async function promptPassword(prompt) {
|
|
2
2
|
process.stderr.write(prompt);
|
|
3
|
-
return new Promise((resolve) => {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
4
|
let password = '';
|
|
5
5
|
if (process.stdin.isTTY) {
|
|
6
6
|
process.stdin.setRawMode(true);
|
|
@@ -31,13 +31,24 @@ export async function promptPassword(prompt) {
|
|
|
31
31
|
process.stdin.on('data', onData);
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
34
|
-
// Non-TTY fallback (e.g. piped input in CI)
|
|
34
|
+
// Non-TTY fallback (e.g. piped input in CI). If stdin closes (EOF)
|
|
35
|
+
// before an answer arrives — e.g. `trawl login < /dev/null` — the
|
|
36
|
+
// `question` callback never fires; without a `close` handler the
|
|
37
|
+
// promise hangs forever and the process exits 0 without logging in.
|
|
38
|
+
// Reject loudly instead. (#68)
|
|
35
39
|
import('readline').then(({ createInterface }) => {
|
|
36
40
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
41
|
+
let answered = false;
|
|
37
42
|
rl.question('', (answer) => {
|
|
43
|
+
answered = true;
|
|
38
44
|
rl.close();
|
|
39
45
|
resolve(answer.trim());
|
|
40
46
|
});
|
|
47
|
+
rl.on('close', () => {
|
|
48
|
+
if (!answered) {
|
|
49
|
+
reject(new Error('No input received — pass -e/-p or set TRAWL_TOKEN for non-interactive use'));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
41
52
|
});
|
|
42
53
|
}
|
|
43
54
|
});
|
package/dist/lib/validate.d.ts
CHANGED
|
@@ -2,3 +2,12 @@ export declare function validateObjectId(id: string): void;
|
|
|
2
2
|
export declare function requireString(value: unknown, name: string): string;
|
|
3
3
|
export declare function requireUrl(value: unknown, name: string): string;
|
|
4
4
|
export declare function requireJwt(value: unknown, name: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Like requireJwt, but also rejects a JWT whose `exp` claim has already
|
|
7
|
+
* passed — `trawl login --token <expired>` / `TRAWL_TOKEN=<expired>` must
|
|
8
|
+
* not print "✓ Logged in" for a token that can't actually authenticate.
|
|
9
|
+
* A JWT with an undecodable/opaque payload is let through (we can't prove
|
|
10
|
+
* it's expired), mirroring `trawl token`'s advisory-only handling of the
|
|
11
|
+
* same case. (#68)
|
|
12
|
+
*/
|
|
13
|
+
export declare function requireFreshJwt(value: unknown, name: string): string;
|
package/dist/lib/validate.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { decodeExp } from './jwt.js';
|
|
2
|
+
import { UsageError } from './errors.js';
|
|
1
3
|
export function validateObjectId(id) {
|
|
2
4
|
if (!/^[0-9a-fA-F]{24}$/.test(id)) {
|
|
3
|
-
throw new
|
|
5
|
+
throw new UsageError(`Invalid scrap ID: "${id}" — expected a 24-char hex ObjectId`);
|
|
4
6
|
}
|
|
5
7
|
}
|
|
6
8
|
export function requireString(value, name) {
|
|
7
9
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
8
|
-
throw new
|
|
10
|
+
throw new UsageError(`${name} is required and must be a non-empty string`);
|
|
9
11
|
}
|
|
10
12
|
return value.trim();
|
|
11
13
|
}
|
|
@@ -14,20 +16,37 @@ export function requireUrl(value, name) {
|
|
|
14
16
|
try {
|
|
15
17
|
const url = new URL(str);
|
|
16
18
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
17
|
-
throw new
|
|
19
|
+
throw new UsageError(`${name} must use http or https protocol`);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
catch (err) {
|
|
21
23
|
if (err instanceof Error && err.message.startsWith(name))
|
|
22
24
|
throw err;
|
|
23
|
-
throw new
|
|
25
|
+
throw new UsageError(`${name} must be a valid URL`);
|
|
24
26
|
}
|
|
25
27
|
return str;
|
|
26
28
|
}
|
|
27
29
|
export function requireJwt(value, name) {
|
|
28
30
|
const str = requireString(value, name);
|
|
29
31
|
if (str.split('.').length !== 3) {
|
|
30
|
-
throw new
|
|
32
|
+
throw new UsageError(`${name} must be a valid JWT token`);
|
|
31
33
|
}
|
|
32
34
|
return str;
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Like requireJwt, but also rejects a JWT whose `exp` claim has already
|
|
38
|
+
* passed — `trawl login --token <expired>` / `TRAWL_TOKEN=<expired>` must
|
|
39
|
+
* not print "✓ Logged in" for a token that can't actually authenticate.
|
|
40
|
+
* A JWT with an undecodable/opaque payload is let through (we can't prove
|
|
41
|
+
* it's expired), mirroring `trawl token`'s advisory-only handling of the
|
|
42
|
+
* same case. (#68)
|
|
43
|
+
*/
|
|
44
|
+
export function requireFreshJwt(value, name) {
|
|
45
|
+
const jwt = requireJwt(value, name);
|
|
46
|
+
const exp = decodeExp(jwt);
|
|
47
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
48
|
+
if (exp !== null && exp < nowSeconds) {
|
|
49
|
+
throw new UsageError(`${name} is an expired JWT (exp has already passed) — obtain a fresh token and retry`);
|
|
50
|
+
}
|
|
51
|
+
return jwt;
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trawlme/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Trawl CLI — manage scraps from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=20"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|