myshell-tools 2.7.0 → 2.10.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.
@@ -56,3 +56,31 @@ export declare function applyStoredCredentials(env: NodeJS.ProcessEnv, homeDir?:
56
56
  * extractClaudeToken('no token here') // → null
57
57
  */
58
58
  export declare function extractClaudeToken(text: string): string | null;
59
+ /**
60
+ * Strip surrounding whitespace and enclosing `"` or `'` quotes from a pasted
61
+ * string. Useful for normalising user-pasted tokens before extraction.
62
+ *
63
+ * Pure / never throws.
64
+ *
65
+ * @example
66
+ * stripPastedSecretWrapper('" sk-ant-oat01-abc "') // → 'sk-ant-oat01-abc'
67
+ * stripPastedSecretWrapper("'token'") // → 'token'
68
+ * stripPastedSecretWrapper(' plain ') // → 'plain'
69
+ */
70
+ export declare function stripPastedSecretWrapper(raw: string): string;
71
+ /**
72
+ * Classify a pasted secret string into one of three categories:
73
+ *
74
+ * - `'oauth-token'` — starts with `sk-ant-oat` (the expected setup-token output).
75
+ * - `'api-key'` — starts with `sk-ant-api` (a raw Anthropic API key, NOT what we want).
76
+ * - `'none'` — neither; blank or unrecognised.
77
+ *
78
+ * Input is pre-normalised (trimmed, quotes stripped) by the caller.
79
+ * Pure / never throws.
80
+ *
81
+ * @example
82
+ * classifyPastedSecret('sk-ant-oat01-abc-XYZ') // → 'oauth-token'
83
+ * classifyPastedSecret('sk-ant-api03-abc-XYZ') // → 'api-key'
84
+ * classifyPastedSecret('not-a-token') // → 'none'
85
+ */
86
+ export declare function classifyPastedSecret(s: string): 'oauth-token' | 'api-key' | 'none';
@@ -146,7 +146,7 @@ export async function applyStoredCredentials(env, homeDir) {
146
146
  }
147
147
  }
148
148
  // ---------------------------------------------------------------------------
149
- // Pure token extraction helper
149
+ // Pure token extraction and classification helpers
150
150
  // ---------------------------------------------------------------------------
151
151
  /**
152
152
  * Extract the first Claude long-lived OAuth token from `text`.
@@ -169,4 +169,55 @@ export function extractClaudeToken(text) {
169
169
  return null;
170
170
  }
171
171
  }
172
+ /**
173
+ * Strip surrounding whitespace and enclosing `"` or `'` quotes from a pasted
174
+ * string. Useful for normalising user-pasted tokens before extraction.
175
+ *
176
+ * Pure / never throws.
177
+ *
178
+ * @example
179
+ * stripPastedSecretWrapper('" sk-ant-oat01-abc "') // → 'sk-ant-oat01-abc'
180
+ * stripPastedSecretWrapper("'token'") // → 'token'
181
+ * stripPastedSecretWrapper(' plain ') // → 'plain'
182
+ */
183
+ export function stripPastedSecretWrapper(raw) {
184
+ try {
185
+ let s = raw.trim();
186
+ if ((s.startsWith('"') && s.endsWith('"')) ||
187
+ (s.startsWith("'") && s.endsWith("'"))) {
188
+ s = s.slice(1, -1).trim();
189
+ }
190
+ return s;
191
+ }
192
+ catch {
193
+ return raw;
194
+ }
195
+ }
196
+ /**
197
+ * Classify a pasted secret string into one of three categories:
198
+ *
199
+ * - `'oauth-token'` — starts with `sk-ant-oat` (the expected setup-token output).
200
+ * - `'api-key'` — starts with `sk-ant-api` (a raw Anthropic API key, NOT what we want).
201
+ * - `'none'` — neither; blank or unrecognised.
202
+ *
203
+ * Input is pre-normalised (trimmed, quotes stripped) by the caller.
204
+ * Pure / never throws.
205
+ *
206
+ * @example
207
+ * classifyPastedSecret('sk-ant-oat01-abc-XYZ') // → 'oauth-token'
208
+ * classifyPastedSecret('sk-ant-api03-abc-XYZ') // → 'api-key'
209
+ * classifyPastedSecret('not-a-token') // → 'none'
210
+ */
211
+ export function classifyPastedSecret(s) {
212
+ try {
213
+ if (s.includes('sk-ant-oat'))
214
+ return 'oauth-token';
215
+ if (s.includes('sk-ant-api'))
216
+ return 'api-key';
217
+ return 'none';
218
+ }
219
+ catch {
220
+ return 'none';
221
+ }
222
+ }
172
223
  //# sourceMappingURL=credentials.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * src/infra/update-check.ts — Self-update detection for myshell-tools.
3
+ *
4
+ * Checks the npm registry for the latest published version, caches the result
5
+ * so the check costs nothing on subsequent runs within the TTL, and reports
6
+ * whether an update is available.
7
+ *
8
+ * Architecture rules:
9
+ * - NEVER throws — all errors are caught and return a safe default.
10
+ * - NEVER hits the network in tests — `fetchLatest` is injected so tests stay hermetic.
11
+ * - Cache is stored atomically at ~/.myshell-tools/update-check.json.
12
+ * - Date.now() is allowed here (infra layer, same as config.ts / atomic.ts).
13
+ */
14
+ export interface UpdateCheckResult {
15
+ readonly current: string;
16
+ readonly latest: string | null;
17
+ readonly updateAvailable: boolean;
18
+ }
19
+ interface UpdateCache {
20
+ checkedAt: number;
21
+ latest: string;
22
+ }
23
+ /**
24
+ * Load the update cache from disk. Returns null on missing/corrupt file.
25
+ * Never throws.
26
+ */
27
+ export declare function loadUpdateCache(homeDir?: string): Promise<UpdateCache | null>;
28
+ /**
29
+ * Persist the update cache atomically. Creates the .myshell-tools directory
30
+ * if it does not exist. Never throws.
31
+ */
32
+ export declare function saveUpdateCache(latest: string, now: number, homeDir?: string): Promise<void>;
33
+ /**
34
+ * Compare two semver-ish version strings.
35
+ *
36
+ * Splits on `.`, compares numeric major/minor/patch segments in order.
37
+ * Any `-prerelease` suffix is stripped before comparison.
38
+ * Non-numeric segments are treated as 0.
39
+ * Returns true iff latest > current.
40
+ * Never throws.
41
+ */
42
+ export declare function isNewerVersion(latest: string, current: string): boolean;
43
+ /**
44
+ * Fetch the latest published version of myshell-tools from the npm registry.
45
+ *
46
+ * Uses global fetch with a 1500ms AbortSignal timeout.
47
+ * Returns the version string, or null on any error/timeout.
48
+ * Never throws.
49
+ */
50
+ export declare function fetchLatestFromNpm(): Promise<string | null>;
51
+ export interface CheckForUpdateOpts {
52
+ readonly currentVersion: string;
53
+ readonly now: number;
54
+ readonly homeDir?: string;
55
+ readonly ttlMs?: number;
56
+ readonly fetchLatest?: () => Promise<string | null>;
57
+ }
58
+ /**
59
+ * Check whether a newer version of myshell-tools is available.
60
+ *
61
+ * Uses a cache file so the npm registry is only contacted once per TTL (default 24h).
62
+ * If the cache is fresh, the registry is NOT contacted at all.
63
+ * On any error returns { current, latest: null, updateAvailable: false }.
64
+ * Never throws.
65
+ */
66
+ export declare function checkForUpdate(opts: CheckForUpdateOpts): Promise<UpdateCheckResult>;
67
+ export {};
@@ -0,0 +1,181 @@
1
+ /**
2
+ * src/infra/update-check.ts — Self-update detection for myshell-tools.
3
+ *
4
+ * Checks the npm registry for the latest published version, caches the result
5
+ * so the check costs nothing on subsequent runs within the TTL, and reports
6
+ * whether an update is available.
7
+ *
8
+ * Architecture rules:
9
+ * - NEVER throws — all errors are caught and return a safe default.
10
+ * - NEVER hits the network in tests — `fetchLatest` is injected so tests stay hermetic.
11
+ * - Cache is stored atomically at ~/.myshell-tools/update-check.json.
12
+ * - Date.now() is allowed here (infra layer, same as config.ts / atomic.ts).
13
+ */
14
+ import { mkdir, readFile } from 'node:fs/promises';
15
+ import { homedir } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { atomicWrite } from './atomic.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ const TTL_MS_DEFAULT = 24 * 60 * 60 * 1000; // 24 hours
22
+ const FETCH_TIMEOUT_MS = 1_500;
23
+ // ---------------------------------------------------------------------------
24
+ // Path helpers
25
+ // ---------------------------------------------------------------------------
26
+ function getCacheDir(homeDir) {
27
+ return join(homeDir, '.myshell-tools');
28
+ }
29
+ function getCachePath(homeDir) {
30
+ return join(getCacheDir(homeDir), 'update-check.json');
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Cache helpers
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Load the update cache from disk. Returns null on missing/corrupt file.
37
+ * Never throws.
38
+ */
39
+ export async function loadUpdateCache(homeDir) {
40
+ const home = homeDir ?? homedir();
41
+ try {
42
+ const raw = await readFile(getCachePath(home), 'utf8');
43
+ const parsed = JSON.parse(raw);
44
+ if (parsed !== null &&
45
+ typeof parsed === 'object' &&
46
+ 'checkedAt' in parsed &&
47
+ 'latest' in parsed &&
48
+ typeof parsed['checkedAt'] === 'number' &&
49
+ typeof parsed['latest'] === 'string') {
50
+ return parsed;
51
+ }
52
+ return null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /**
59
+ * Persist the update cache atomically. Creates the .myshell-tools directory
60
+ * if it does not exist. Never throws.
61
+ */
62
+ export async function saveUpdateCache(latest, now, homeDir) {
63
+ const home = homeDir ?? homedir();
64
+ try {
65
+ await mkdir(getCacheDir(home), { recursive: true });
66
+ const cache = { checkedAt: now, latest };
67
+ await atomicWrite(getCachePath(home), JSON.stringify(cache, null, 2));
68
+ }
69
+ catch {
70
+ // Silently ignore — failing to cache is not a fatal error.
71
+ }
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Version comparison
75
+ // ---------------------------------------------------------------------------
76
+ /**
77
+ * Compare two semver-ish version strings.
78
+ *
79
+ * Splits on `.`, compares numeric major/minor/patch segments in order.
80
+ * Any `-prerelease` suffix is stripped before comparison.
81
+ * Non-numeric segments are treated as 0.
82
+ * Returns true iff latest > current.
83
+ * Never throws.
84
+ */
85
+ export function isNewerVersion(latest, current) {
86
+ try {
87
+ const parse = (v) => {
88
+ // Strip any prerelease suffix (e.g. "1.2.3-beta.1" → "1.2.3")
89
+ const base = v.split('-')[0] ?? '';
90
+ return base.split('.').map((part) => {
91
+ const n = parseInt(part, 10);
92
+ return Number.isFinite(n) ? n : 0;
93
+ });
94
+ };
95
+ const lParts = parse(latest);
96
+ const cParts = parse(current);
97
+ const len = Math.max(lParts.length, cParts.length);
98
+ for (let i = 0; i < len; i++) {
99
+ const l = lParts[i] ?? 0;
100
+ const c = cParts[i] ?? 0;
101
+ if (l > c)
102
+ return true;
103
+ if (l < c)
104
+ return false;
105
+ }
106
+ return false; // equal
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // npm registry fetch (real network — injected in tests)
114
+ // ---------------------------------------------------------------------------
115
+ /**
116
+ * Fetch the latest published version of myshell-tools from the npm registry.
117
+ *
118
+ * Uses global fetch with a 1500ms AbortSignal timeout.
119
+ * Returns the version string, or null on any error/timeout.
120
+ * Never throws.
121
+ */
122
+ export async function fetchLatestFromNpm() {
123
+ const ac = new AbortController();
124
+ const timer = setTimeout(() => { ac.abort(); }, FETCH_TIMEOUT_MS);
125
+ try {
126
+ const res = await fetch('https://registry.npmjs.org/myshell-tools/latest', {
127
+ signal: ac.signal,
128
+ });
129
+ if (!res.ok)
130
+ return null;
131
+ const data = await res.json();
132
+ if (data !== null &&
133
+ typeof data === 'object' &&
134
+ 'version' in data &&
135
+ typeof data['version'] === 'string') {
136
+ return data['version'] ?? null;
137
+ }
138
+ return null;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ finally {
144
+ clearTimeout(timer);
145
+ }
146
+ }
147
+ /**
148
+ * Check whether a newer version of myshell-tools is available.
149
+ *
150
+ * Uses a cache file so the npm registry is only contacted once per TTL (default 24h).
151
+ * If the cache is fresh, the registry is NOT contacted at all.
152
+ * On any error returns { current, latest: null, updateAvailable: false }.
153
+ * Never throws.
154
+ */
155
+ export async function checkForUpdate(opts) {
156
+ const { currentVersion, now, homeDir, ttlMs = TTL_MS_DEFAULT } = opts;
157
+ const fetchFn = opts.fetchLatest ?? fetchLatestFromNpm;
158
+ try {
159
+ // Check if we have a fresh cache
160
+ const cache = await loadUpdateCache(homeDir);
161
+ if (cache !== null && now - cache.checkedAt < ttlMs) {
162
+ // Cache is fresh — use it without hitting the network
163
+ const updateAvailable = isNewerVersion(cache.latest, currentVersion);
164
+ return { current: currentVersion, latest: cache.latest, updateAvailable };
165
+ }
166
+ // Cache is stale or missing — fetch from the registry
167
+ const latest = await fetchFn();
168
+ if (latest !== null) {
169
+ await saveUpdateCache(latest, now, homeDir);
170
+ }
171
+ if (latest === null) {
172
+ return { current: currentVersion, latest: null, updateAvailable: false };
173
+ }
174
+ const updateAvailable = isNewerVersion(latest, currentVersion);
175
+ return { current: currentVersion, latest, updateAvailable };
176
+ }
177
+ catch {
178
+ return { current: currentVersion, latest: null, updateAvailable: false };
179
+ }
180
+ }
181
+ //# sourceMappingURL=update-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.js","sourceRoot":"","sources":["../../src/infra/update-check.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,7 @@ import type { EnvironmentStatus } from '../providers/detect.js';
23
23
  import type { Provider, ProviderId, SandboxLevel } from '../providers/port.js';
24
24
  import type { OutputSink } from './render.js';
25
25
  import type { LoginMethod } from '../commands/login.js';
26
+ import type { UpdateCheckResult } from '../infra/update-check.js';
26
27
  export interface MenuContext {
27
28
  readonly version: string;
28
29
  readonly clock: Clock;
@@ -67,7 +68,48 @@ export interface MenuContext {
67
68
  * spawning during tests (e.g. after first-run onboarding or [j]/[k]/[o] login).
68
69
  */
69
70
  readonly detectEnvironment?: () => Promise<EnvironmentStatus>;
71
+ /**
72
+ * Optional injected update-check for testing. When provided, `startMenu` uses
73
+ * this instead of the real `checkForUpdate` from infra/update-check.ts,
74
+ * preventing real npm registry requests from being made during tests.
75
+ *
76
+ * Returns the update check result (current, latest, updateAvailable).
77
+ */
78
+ readonly checkForUpdate?: () => Promise<UpdateCheckResult>;
79
+ /**
80
+ * Optional injected self-update function for testing. When provided, `startMenu`
81
+ * uses this instead of the real `npm install -g myshell-tools@latest` subprocess.
82
+ *
83
+ * Returns true when the update succeeded (exit code 0), false otherwise.
84
+ * Never throws.
85
+ */
86
+ readonly updateSelf?: (out: OutputSink) => Promise<boolean>;
87
+ /**
88
+ * Optional injected relaunch function for testing. When provided, `startMenu`
89
+ * uses this instead of the real `execa('myshell-tools', …)` re-exec.
90
+ *
91
+ * Returns the exit code of the relaunched process (or 1 on spawn failure).
92
+ * Used only for the opt-in auto-update path.
93
+ */
94
+ readonly relaunch?: () => Promise<number>;
70
95
  }
96
+ /**
97
+ * Parse a yes/no answer from a raw input line, with a configurable default.
98
+ *
99
+ * Accepts (case-insensitive, trimmed):
100
+ * - `"y"` or `"yes"` → true
101
+ * - `"n"` or `"no"` → false
102
+ * - empty string or `null` (EOF) → `defaultYes`
103
+ * - anything else → `defaultYes` (lenient)
104
+ *
105
+ * Never throws. Callers should display `(Y/n)` when `defaultYes` is true and
106
+ * `(y/N)` when `defaultYes` is false so the user knows which choice Enter gives.
107
+ *
108
+ * @param input - The raw line from readLine(), or null on EOF.
109
+ * @param defaultYes - True if pressing Enter (or EOF) means yes.
110
+ * @returns True for yes, false for no.
111
+ */
112
+ export declare function parseYesNo(input: string | null, defaultYes: boolean): boolean;
71
113
  /**
72
114
  * Return the shell alias hint the user can add to their shell profile to make
73
115
  * `myshell-tools` the default command-line assistant.