@mjasnikovs/pi-task 0.13.36 → 0.13.37

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.
@@ -4,7 +4,7 @@ import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { openCache as defaultOpenCache } from './docs-cache.js';
6
6
  import { ensureIndexed as defaultEnsureIndexed } from './docs-index.js';
7
- import { resolvePackage as defaultResolvePackage, ResolveError } from './docs-resolve.js';
7
+ import { resolvePackage as defaultResolvePackage, ResolveError, detectTypesRedirect, typesPackageName, hasTypeFiles, isDtsFile } from './docs-resolve.js';
8
8
  import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
9
9
  import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
10
10
  import { getPiInvocation } from '../shared/pi-invocation.js';
@@ -44,6 +44,52 @@ export async function runAutoInstall(spawn, packageName, signal) {
44
44
  }, installDir, signal, { mode: 'text', discardStdout: true });
45
45
  return { success: result.exitCode === 0 && !result.aborted, installDir, stderr: result.stderr };
46
46
  }
47
+ /** Resolve `name` from cwd; on `not_installed`, auto-install it and resolve from
48
+ * the install dir. Returns null on any failure (caller keeps its fallback). */
49
+ async function tryResolveOrInstall(name, cwd, spawn, resolvePackage, signal) {
50
+ try {
51
+ return resolvePackage(name, cwd);
52
+ }
53
+ catch (err) {
54
+ if (!(err instanceof ResolveError) || err.kind !== 'not_installed')
55
+ return null;
56
+ const install = await runAutoInstall(spawn, extractParentPackage(name), signal);
57
+ if (!install.success)
58
+ return null;
59
+ try {
60
+ return resolvePackage(name, install.installDir);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ }
67
+ /** Follow the @types/<name> + triple-slash `<reference types>` redirect chain
68
+ * from a package that ships no usable types of its own to the one that actually
69
+ * holds the declarations (e.g. bun -> @types/bun -> bun-types). Bounded to a few
70
+ * hops; returns the original package if no better source is found. */
71
+ async function resolveTypeSource(pkg, requested, cwd, spawn, resolvePackage, signal) {
72
+ const visited = new Set([pkg.name, extractParentPackage(requested)]);
73
+ let cur = pkg;
74
+ for (let depth = 0; depth < 3; depth++) {
75
+ let next = detectTypesRedirect(cur);
76
+ if (next && visited.has(next))
77
+ next = null;
78
+ if (!next && !hasTypeFiles(cur.root)) {
79
+ const types = typesPackageName(cur.name);
80
+ if (types && !visited.has(types))
81
+ next = types;
82
+ }
83
+ if (!next)
84
+ break;
85
+ visited.add(next);
86
+ const resolved = await tryResolveOrInstall(next, cwd, spawn, resolvePackage, signal);
87
+ if (!resolved)
88
+ break;
89
+ cur = resolved;
90
+ }
91
+ return cur;
92
+ }
47
93
  export async function docsRaw(input) {
48
94
  const resolvePackage = input.resolvePackage ?? defaultResolvePackage;
49
95
  const ensureIndexed = input.ensureIndexed ?? defaultEnsureIndexed;
@@ -114,6 +160,13 @@ export async function docsRaw(input) {
114
160
  };
115
161
  }
116
162
  }
163
+ // Step 1b: if the resolved package ships no usable type declarations (e.g.
164
+ // the `bun` runtime launcher, which is just a binary + install README), or is
165
+ // a pure `@types/<name>` redirect stub, follow the conventional
166
+ // @types/<name> + triple-slash `<reference types>` chain to the package that
167
+ // actually holds the declarations (e.g. bun -> @types/bun -> bun-types).
168
+ // Best-effort: any failure leaves the original resolution untouched.
169
+ pkg = await resolveTypeSource(pkg, input.pkg, input.cwd, spawn, resolvePackage, input.signal);
117
170
  // Step 2: open cache
118
171
  let cache = null;
119
172
  let cacheError;
@@ -257,7 +310,7 @@ function walkDtsAlpha(root) {
257
310
  const full = path.join(dir, entry.name);
258
311
  if (entry.isDirectory())
259
312
  stack.push(full);
260
- else if (entry.isFile() && entry.name.endsWith('.d.ts'))
313
+ else if (entry.isFile() && isDtsFile(entry.name))
261
314
  out.push(full);
262
315
  }
263
316
  }
@@ -1,5 +1,5 @@
1
1
  import type { CacheHandle } from './docs-cache.js';
2
- import type { ResolvedPackage } from './docs-resolve.js';
2
+ import { type ResolvedPackage } from './docs-resolve.js';
3
3
  export interface IndexResult {
4
4
  hitCache: boolean;
5
5
  filesIngested: number;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ import { isDtsFile } from './docs-resolve.js';
4
5
  const MAX_CHUNK_BYTES = 8 * 1024;
5
6
  const ZERO_SEP = Buffer.from([0]);
6
7
  const DECL_SPLIT_RE = /^(?:export\s+|declare\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|namespace|module|const|let|var|enum)\s+/m;
@@ -48,13 +49,13 @@ function walkDts(root) {
48
49
  const stat = fs.statSync(realPath);
49
50
  if (stat.isDirectory())
50
51
  stack.push(realPath);
51
- else if (stat.isFile() && realPath.endsWith('.d.ts'))
52
+ else if (stat.isFile() && isDtsFile(realPath))
52
53
  out.push(realPath);
53
54
  continue;
54
55
  }
55
56
  if (entry.isDirectory())
56
57
  stack.push(full);
57
- else if (entry.isFile() && entry.name.endsWith('.d.ts'))
58
+ else if (entry.isFile() && isDtsFile(entry.name))
58
59
  out.push(full);
59
60
  }
60
61
  }
@@ -1,3 +1,5 @@
1
+ /** True for TypeScript declaration files: .d.ts, .d.mts, .d.cts. */
2
+ export declare function isDtsFile(name: string): boolean;
1
3
  export interface ResolvedPackage {
2
4
  name: string;
3
5
  version: string;
@@ -10,3 +12,15 @@ export declare class ResolveError extends Error {
10
12
  constructor(kind: 'not_installed' | 'invalid_name', message: string);
11
13
  }
12
14
  export declare function resolvePackage(moduleName: string, cwd: string): ResolvedPackage;
15
+ /** Conventional DefinitelyTyped package for a runtime package that ships no
16
+ * types of its own. `bun` -> `@types/bun`, `@scope/x` -> `@types/scope__x`.
17
+ * Returns null for packages that are already under the `@types` scope. */
18
+ export declare function typesPackageName(moduleName: string): string | null;
19
+ /** True if the package ships at least one declaration file. */
20
+ export declare function hasTypeFiles(root: string): boolean;
21
+ /** When a package is a pure pointer to another types package — a single-file
22
+ * `/// <reference types="X" />` (the `@types/bun -> bun-types` shape) or a lone
23
+ * `export * from "X"` re-export — return the target package name. Returns null
24
+ * for packages that ship their own declarations (more than one .d.ts file, or a
25
+ * local `/// <reference path=... />` aggregator entry). */
26
+ export declare function detectTypesRedirect(pkg: ResolvedPackage): string | null;
@@ -1,6 +1,11 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ const DTS_RE = /\.d\.(?:ts|mts|cts)$/;
5
+ /** True for TypeScript declaration files: .d.ts, .d.mts, .d.cts. */
6
+ export function isDtsFile(name) {
7
+ return DTS_RE.test(name);
8
+ }
4
9
  export class ResolveError extends Error {
5
10
  kind;
6
11
  constructor(kind, message) {
@@ -52,10 +57,18 @@ function findReadme(root) {
52
57
  function resolveEntryDts(moduleName, parent, root, pkg) {
53
58
  if (moduleName !== parent) {
54
59
  const subpath = moduleName.slice(parent.length + 1);
55
- const candidates = [`${subpath}.d.ts`, `${subpath}/index.d.ts`, subpath];
60
+ const candidates = [
61
+ `${subpath}.d.ts`,
62
+ `${subpath}.d.mts`,
63
+ `${subpath}.d.cts`,
64
+ `${subpath}/index.d.ts`,
65
+ `${subpath}/index.d.mts`,
66
+ `${subpath}/index.d.cts`,
67
+ subpath
68
+ ];
56
69
  for (const c of candidates) {
57
70
  const abs = path.join(root, c);
58
- if (fs.existsSync(abs) && abs.endsWith('.d.ts'))
71
+ if (fs.existsSync(abs) && isDtsFile(abs))
59
72
  return abs;
60
73
  }
61
74
  }
@@ -71,9 +84,11 @@ function resolveEntryDts(moduleName, parent, root, pkg) {
71
84
  if (fs.existsSync(abs))
72
85
  return abs;
73
86
  }
74
- const fallback = path.join(root, 'index.d.ts');
75
- if (fs.existsSync(fallback))
76
- return fallback;
87
+ for (const name of ['index.d.ts', 'index.d.mts', 'index.d.cts']) {
88
+ const fallback = path.join(root, name);
89
+ if (fs.existsSync(fallback))
90
+ return fallback;
91
+ }
77
92
  return null;
78
93
  }
79
94
  export function resolvePackage(moduleName, cwd) {
@@ -124,3 +139,95 @@ function findPackageJsonInNodeModules(parent, startDir) {
124
139
  dir = up;
125
140
  }
126
141
  }
142
+ /** Conventional DefinitelyTyped package for a runtime package that ships no
143
+ * types of its own. `bun` -> `@types/bun`, `@scope/x` -> `@types/scope__x`.
144
+ * Returns null for packages that are already under the `@types` scope. */
145
+ export function typesPackageName(moduleName) {
146
+ const parent = parentPackageName(moduleName);
147
+ if (parent.startsWith('@types/'))
148
+ return null;
149
+ if (parent.startsWith('@')) {
150
+ const [scope, name] = parent.slice(1).split('/');
151
+ if (!scope || !name)
152
+ return null;
153
+ return `@types/${scope}__${name}`;
154
+ }
155
+ return `@types/${parent}`;
156
+ }
157
+ /** Count declaration files under root (excluding nested node_modules), stopping
158
+ * once `cap` is reached. */
159
+ function countTypeFiles(root, cap) {
160
+ let n = 0;
161
+ const stack = [root];
162
+ while (stack.length) {
163
+ const dir = stack.pop();
164
+ let entries;
165
+ try {
166
+ entries = fs.readdirSync(dir, { withFileTypes: true });
167
+ }
168
+ catch {
169
+ continue;
170
+ }
171
+ for (const entry of entries) {
172
+ if (entry.name === 'node_modules')
173
+ continue;
174
+ const full = path.join(dir, entry.name);
175
+ if (entry.isDirectory())
176
+ stack.push(full);
177
+ else if (entry.isFile() && isDtsFile(entry.name)) {
178
+ if (++n >= cap)
179
+ return n;
180
+ }
181
+ }
182
+ }
183
+ return n;
184
+ }
185
+ /** True if the package ships at least one declaration file. */
186
+ export function hasTypeFiles(root) {
187
+ return countTypeFiles(root, 1) > 0;
188
+ }
189
+ function findIndexDts(root) {
190
+ for (const name of ['index.d.ts', 'index.d.mts', 'index.d.cts']) {
191
+ const abs = path.join(root, name);
192
+ if (fs.existsSync(abs))
193
+ return abs;
194
+ }
195
+ return null;
196
+ }
197
+ function isBareSpecifier(spec) {
198
+ return spec.length > 0 && !spec.startsWith('.') && !spec.startsWith('/');
199
+ }
200
+ const REFERENCE_TYPES_RE = /\/\/\/\s*<reference\s+types=["']([^"']+)["']\s*\/>/;
201
+ const REEXPORT_ALL_RE = /^\s*export\s+(?:type\s+)?\*\s+from\s+["']([^"']+)["'];?\s*$/m;
202
+ /** When a package is a pure pointer to another types package — a single-file
203
+ * `/// <reference types="X" />` (the `@types/bun -> bun-types` shape) or a lone
204
+ * `export * from "X"` re-export — return the target package name. Returns null
205
+ * for packages that ship their own declarations (more than one .d.ts file, or a
206
+ * local `/// <reference path=... />` aggregator entry). */
207
+ export function detectTypesRedirect(pkg) {
208
+ // A package that ships multiple declaration files is an aggregator, not a
209
+ // redirect stub — use its own types.
210
+ if (countTypeFiles(pkg.root, 2) > 1)
211
+ return null;
212
+ const entry = pkg.entryDts ?? findIndexDts(pkg.root);
213
+ if (!entry)
214
+ return null;
215
+ let content;
216
+ try {
217
+ content = fs.readFileSync(entry, 'utf8');
218
+ }
219
+ catch {
220
+ return null;
221
+ }
222
+ // A local `/// <reference path="..." />` means the entry aggregates sibling
223
+ // declarations (e.g. bun-types) — not a redirect to another package.
224
+ if (/\/\/\/\s*<reference\s+path=/.test(content))
225
+ return null;
226
+ const ref = REFERENCE_TYPES_RE.exec(content);
227
+ if (ref && isBareSpecifier(ref[1]))
228
+ return parentPackageName(ref[1]);
229
+ const rex = REEXPORT_ALL_RE.exec(content);
230
+ if (rex && isBareSpecifier(rex[1]))
231
+ return parentPackageName(rex[1]);
232
+ return null;
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.13.36",
3
+ "version": "0.13.37",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",