@danypops/pi-packed 0.5.2 → 0.6.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/src/constants.ts DELETED
@@ -1,60 +0,0 @@
1
- /**
2
- * constants.ts — every magic value in one place, named.
3
- * (lexicon: replace-magic-number-with-symbolic-constant)
4
- */
5
-
6
- // --- Upstream ---
7
- export const NPM_REGISTRY_BASE = "https://registry.npmjs.org";
8
-
9
- // --- Search / pagination ---
10
- export const SEARCH_DEFAULT_LIMIT = 10;
11
- export const SEARCH_MAX_LIMIT = 50;
12
- export const SEARCH_PAGE_SIZE = 250; // npm registry max page size
13
- export const PI_PACKAGE_KEYWORD = "keywords:pi-package";
14
-
15
- // --- Native tool presentation bounds ---
16
- export const TOOL_MODEL_CONTENT_MAX_CHARACTERS = 2_000;
17
- export const TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS = 32_000;
18
- export const TOOL_DETAILS_MAX_PACKAGES = 50;
19
- export const TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS = 240;
20
- export const TOOL_DETAILS_MAX_OUTPUT_CHARACTERS = 1_000;
21
- export const TOOL_DETAILS_MAX_KEYWORDS = 20;
22
- export const TOOL_DETAILS_MAX_CAPABILITIES = 12;
23
- export const TOOL_COLLAPSED_PACKAGE_PREVIEW = 3;
24
-
25
- // --- Upstream etiquette (429s) ---
26
- export const RETRY_MAX_ATTEMPTS = 6;
27
- export const RETRY_BASE_DELAY_MS = 2_000; // 2+4+8+16+32s spans npm's ~60s search window
28
- export const PAGE_DELAY_MS = 100; // politeness pause between catalog pages
29
- export const MIRROR_PAGE_DELAY_MS = 400; // manual full-sync: extra polite (burst limits)
30
-
31
- // --- Cache / fetch ---
32
- export const CACHE_TTL_MS = 5 * 60_000;
33
- export const PROBE_TIMEOUT_MS = 800;
34
- export const REGISTRY_FETCH_TIMEOUT_MS = 15_000;
35
-
36
- // --- Daemon ---
37
- export const DAEMON_HOST = "127.0.0.1";
38
- export const WATCH_INTERVAL_DEFAULT_MS = 30 * 60_000; // updates diff cadence
39
- export const CATALOG_INTERVAL_DEFAULT_MS = 6 * 3_600_000; // full mirror TTL
40
- export const IDLE_BUDGET_DEFAULT_MS = 10 * 60_000; // on-demand self-exit
41
- export const WATCHDOG_TICK_MS = 15_000;
42
-
43
- // --- Identity ---
44
-
45
- // --- State-dir file names ---
46
- export const TOKEN_FILE = "token";
47
- export const PORT_FILE = "port";
48
- export const UPDATES_FILE = "updates.json";
49
- export const DB_FILE = "packed.db";
50
- export const SETTINGS_FILE = "settings.json";
51
- export const SECURITY_FILE = "security.json";
52
-
53
- // --- Environment knobs ---
54
- export const ENV = {
55
- HOME: "PI_PACKED_HOME",
56
- PI_HOME: "PI_PACKED_PI_HOME",
57
- WATCH_SECS: "PI_PACKED_WATCH_SECS",
58
- CATALOG_SECS: "PI_PACKED_CATALOG_SECS",
59
- IDLE_SECS: "PI_PACKED_IDLE_SECS",
60
- } as const;
package/src/daemon.ts DELETED
@@ -1,71 +0,0 @@
1
- /**
2
- * daemon.ts — serve mode: thin wiring around tested units. Bun.serve wraps
3
- * the hexagon's fetch handler; watcher + catalogSync produce snapshots;
4
- * idle watchdog self-terminates (the daemon is spawned on demand).
5
- */
6
- import { createApp } from "./service.ts";
7
- import { HttpRegistry } from "./registry.ts";
8
- import { ExecInstaller } from "./install.ts";
9
- import { loadOrCreateToken, writePort, idleExpired, envMs, stateDir } from "./state.ts";
10
- import { startWatcher } from "./watcher.ts";
11
- import { startCatalogSync } from "./catalog.ts";
12
- import { openDb, latestVersion, dbPath } from "./db.ts";
13
- import {
14
- DAEMON_HOST, ENV, WATCH_INTERVAL_DEFAULT_MS, CATALOG_INTERVAL_DEFAULT_MS,
15
- IDLE_BUDGET_DEFAULT_MS, WATCHDOG_TICK_MS,
16
- } from "./constants.ts";
17
- import { readInstalledPackages, defaultPiHome } from "./installed.ts";
18
-
19
- export function serveMain(): void {
20
- const dir = stateDir();
21
- const token = loadOrCreateToken(dir);
22
- const reg = new HttpRegistry();
23
- const app = createApp({ reg, inst: new ExecInstaller(), token, stateDir: dir });
24
-
25
- let lastActive = Date.now();
26
- const server = Bun.serve({
27
- port: 0,
28
- hostname: DAEMON_HOST,
29
- fetch: (req) => {
30
- lastActive = Date.now();
31
- return app.fetch(req);
32
- },
33
- });
34
- if (!server.port) throw new Error("failed to bind listener");
35
- writePort(dir, server.port);
36
-
37
- // Event producer #1: drift detection from the local mirror (default 30min).
38
- const watcherDb = openDb(dbPath(dir));
39
- const stopWatcher = startWatcher((name) => latestVersion(watcherDb, name), dir, () => readInstalledPackages(defaultPiHome()), {
40
- intervalMs: envMs(ENV.WATCH_SECS, WATCH_INTERVAL_DEFAULT_MS),
41
- });
42
- const stopCatalog = startCatalogSync(reg, dir, envMs(ENV.CATALOG_SECS, CATALOG_INTERVAL_DEFAULT_MS));
43
-
44
- // Idle watchdog: for on-demand spawns. PI_PACKED_IDLE_SECS=0 disables it
45
- // (systemd or another supervisor owns the lifecycle then).
46
- const idleBudget = envMs(ENV.IDLE_SECS, IDLE_BUDGET_DEFAULT_MS);
47
- let watchdog: ReturnType<typeof setInterval> | undefined;
48
- if (idleBudget > 0) {
49
- watchdog = setInterval(() => {
50
- if (idleExpired(lastActive, Date.now(), idleBudget)) {
51
- console.error(`[packed] idle for ${idleBudget / 1000}s, exiting`);
52
- shutdown();
53
- }
54
- }, WATCHDOG_TICK_MS);
55
- }
56
-
57
- function shutdown(): void {
58
- if (watchdog) clearInterval(watchdog);
59
- stopWatcher();
60
- stopCatalog();
61
- watcherDb.close();
62
- server.stop(true);
63
- process.exit(0);
64
- }
65
- process.on("SIGTERM", shutdown);
66
- process.on("SIGINT", shutdown);
67
-
68
- console.error(
69
- `[packed] listening on ${DAEMON_HOST}:${server.port} (state ${dir}, watch ${envMs(ENV.WATCH_SECS, WATCH_INTERVAL_DEFAULT_MS) / 1000}s, idle ${idleBudget / 1000}s)`,
70
- );
71
- }
package/src/db.ts DELETED
@@ -1,164 +0,0 @@
1
- /**
2
- * db.ts — the local registry index (SQLite), the pkgng/YUM model.
3
- * Master-index role: sync_meta (source, time, count, payload checksum —
4
- * the APT Release / repomd.xml analog). Payload: packages + FTS5 mirror.
5
- */
6
- // Runtime-detected SQLite backend: bun:sqlite under Bun (CLI, daemon),
7
- // node:sqlite under Node ≥22.5 (pi's extension host — jiti runs on Node).
8
- // Both share the better-sqlite3 API shape; transactions are done manually
9
- // because node:sqlite has no .transaction() helper.
10
- import { createRequire } from "node:module";
11
- import { mkdirSync } from "node:fs";
12
-
13
- const require_ = createRequire(import.meta.url);
14
- const IS_BUN = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
15
- const backend = IS_BUN
16
- ? (require_("bun:sqlite") as typeof import("bun:sqlite"))
17
- : (require_("node:sqlite") as unknown as typeof import("bun:sqlite"));
18
-
19
- // Constructor + options differ: bun:sqlite exports Database({create}),
20
- // node:sqlite exports DatabaseSync (creates by default).
21
- const DatabaseCtor = (
22
- "DatabaseSync" in backend ? (backend as { DatabaseSync: unknown }).DatabaseSync : backend.Database
23
- ) as new (path: string, opts?: { create?: boolean }) => Db;
24
-
25
- export interface DbStatement {
26
- run(...params: unknown[]): { lastInsertRowid: number | bigint };
27
- get(...params: unknown[]): unknown;
28
- all(...params: unknown[]): unknown[];
29
- }
30
-
31
- export interface Db {
32
- exec(sql: string): unknown;
33
- prepare(sql: string): DbStatement;
34
- close(): void;
35
- }
36
-
37
- function inTransaction<T>(db: Db, fn: () => T): T {
38
- db.exec("BEGIN");
39
- try {
40
- const result = fn();
41
- db.exec("COMMIT");
42
- return result;
43
- } catch (e) {
44
- db.exec("ROLLBACK");
45
- throw e;
46
- }
47
- }
48
- import { join } from "node:path";
49
- import { createHash } from "node:crypto";
50
- import type { Pkg } from "./ports.ts";
51
- import { DB_FILE } from "./constants.ts";
52
-
53
- export interface SyncMeta {
54
- source: string;
55
- fetchedAt: string;
56
- packageCount: number;
57
- sha256: string;
58
- }
59
-
60
- export function dbPath(dir: string): string {
61
- return join(dir, DB_FILE);
62
- }
63
-
64
- export function openDb(path: string): Db {
65
- if (path !== ":memory:") mkdirSync(join(path, ".."), { recursive: true });
66
- const db = IS_BUN ? new DatabaseCtor(path, { create: true }) : new DatabaseCtor(path);
67
- db.exec("PRAGMA journal_mode = WAL");
68
- db.exec(`CREATE TABLE IF NOT EXISTS packages (
69
- name TEXT PRIMARY KEY,
70
- version TEXT NOT NULL DEFAULT '',
71
- description TEXT,
72
- date TEXT
73
- )`);
74
- db.exec(`CREATE TABLE IF NOT EXISTS sync_meta (
75
- id INTEGER PRIMARY KEY CHECK (id = 1),
76
- source TEXT NOT NULL,
77
- fetched_at TEXT NOT NULL,
78
- package_count INTEGER NOT NULL,
79
- sha256 TEXT NOT NULL
80
- )`);
81
- // FTS5 mirror; rebuilt wholesale on each sync.
82
- db.exec(
83
- `CREATE VIRTUAL TABLE IF NOT EXISTS packages_fts USING fts5(name, description)`,
84
- );
85
- return db;
86
- }
87
-
88
- /** Canonical checksum over the payload — the Release-file analog: cheap
89
- * change detection between syncs and integrity for the local mirror. */
90
- function payloadHash(pkgs: Pkg[]): string {
91
- const h = createHash("sha256");
92
- for (const p of pkgs) h.update(`${p.name}@${p.version}\n`);
93
- return h.digest("hex");
94
- }
95
-
96
- /** Atomic full-catalog replace (the mirror write side of `apt update`). */
97
- export function replaceAll(db: Db, pkgs: Pkg[], source: string): SyncMeta {
98
- const meta: SyncMeta = {
99
- source,
100
- fetchedAt: new Date().toISOString(),
101
- packageCount: pkgs.length,
102
- sha256: payloadHash(pkgs),
103
- };
104
- // INSERT OR REPLACE: npm pagination is unstable — rankings shift between
105
- // pages and a package can appear twice in one sync (rowid reuse is fine).
106
- const insert = db.prepare("INSERT OR REPLACE INTO packages (name, version, description, date) VALUES (?, ?, ?, ?)");
107
- const insertFts = db.prepare("INSERT INTO packages_fts (rowid, name, description) VALUES (?, ?, ?)");
108
- inTransaction(db, () => {
109
- db.exec("DELETE FROM packages");
110
- db.exec("DELETE FROM packages_fts");
111
- for (const p of pkgs) {
112
- const { lastInsertRowid } = insert.run(p.name, p.version, p.description ?? null, p.date ?? null);
113
- insertFts.run(Number(lastInsertRowid), p.name, p.description ?? "");
114
- }
115
- db.prepare(
116
- "INSERT INTO sync_meta (id, source, fetched_at, package_count, sha256) VALUES (1, ?, ?, ?, ?) " +
117
- "ON CONFLICT(id) DO UPDATE SET source=excluded.source, fetched_at=excluded.fetched_at, " +
118
- "package_count=excluded.package_count, sha256=excluded.sha256",
119
- ).run(meta.source, meta.fetchedAt, meta.packageCount, meta.sha256);
120
- });
121
- return meta;
122
- }
123
-
124
- export function getSyncMeta(db: Db): SyncMeta | undefined {
125
- return db
126
- .prepare("SELECT source, fetched_at AS fetchedAt, package_count AS packageCount, sha256 FROM sync_meta WHERE id = 1")
127
- .get() as SyncMeta | undefined;
128
- }
129
-
130
- export function catalogList(db: Db, limit = 0, offset = 0): Pkg[] {
131
- const sql =
132
- "SELECT name, version, description, date FROM packages ORDER BY name" +
133
- (limit > 0 ? ` LIMIT ${Math.floor(limit)} OFFSET ${Math.floor(offset)}` : "");
134
- return db.prepare(sql).all() as Pkg[];
135
- }
136
-
137
- /** FTS5 over the mirror (the `apt-cache search` analog). Sanitizes user
138
- * input into AND-joined quoted terms; falls back to LIKE for hostile input. */
139
- export function searchLocal(db: Db, q: string, limit = 50): Pkg[] {
140
- const terms = q.trim().split(/\s+/).filter(Boolean);
141
- if (terms.length === 0) return [];
142
- try {
143
- const match = terms.map((t) => `"${t.replaceAll('"', '""')}"*`).join(" ");
144
- return db
145
- .prepare(
146
- `SELECT p.name, p.version, p.description, p.date
147
- FROM packages_fts f JOIN packages p ON p.rowid = f.rowid
148
- WHERE packages_fts MATCH ? ORDER BY rank LIMIT ?`,
149
- )
150
- .all(match, limit) as Pkg[];
151
- } catch {
152
- // FTS syntax hostility → substring fallback
153
- const like = `%${q}%`;
154
- return db
155
- .prepare("SELECT name, version, description, date FROM packages WHERE name LIKE ? OR description LIKE ? ORDER BY name LIMIT ?")
156
- .all(like, like, limit) as Pkg[];
157
- }
158
- }
159
-
160
- /** Latest mirrored version of one package (watcher's lookup). */
161
- export function latestVersion(db: Db, name: string): string | undefined {
162
- const row = db.prepare("SELECT version FROM packages WHERE name = ?").get(name) as { version: string } | null;
163
- return row?.version;
164
- }
package/src/install.ts DELETED
@@ -1,52 +0,0 @@
1
- /** install.ts — driven adapter: pi CLI mutations via Bun.spawn. */
2
- import { defaultPiHome, isPinnedNpmSource, readResolvedVersion } from "./installed.ts";
3
- import type { Installer, UpdateOutcome } from "./ports.ts";
4
-
5
- /** Bare npm package name (for `packed remove`). */
6
- export const NAME_RE = /^(@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/;
7
-
8
- export function defaultPiBin(): string {
9
- return process.env["PI_PACKED_PI_BIN"] ?? process.env["PI_BIN"] ?? "pi";
10
- }
11
-
12
- export class ExecInstaller implements Installer {
13
- constructor(
14
- private bin = defaultPiBin(),
15
- private piHome = defaultPiHome(),
16
- ) {}
17
-
18
- private async run(args: string[]): Promise<string> {
19
- const proc = Bun.spawn([this.bin, ...args], { stdout: "pipe", stderr: "pipe" });
20
- const [stdout, stderr] = await Promise.all([
21
- new Response(proc.stdout).text(),
22
- new Response(proc.stderr).text(),
23
- ]);
24
- const out = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
25
- const code = await proc.exited;
26
- if (code !== 0) throw new Error(out || `exit ${code}`);
27
- return out;
28
- }
29
-
30
- install(source: string, _options?: { approved?: boolean }): Promise<string> {
31
- return this.run(["install", source]);
32
- }
33
-
34
- remove(source: string, _options?: { approved?: boolean }): Promise<string> {
35
- return this.run(["remove", source]);
36
- }
37
-
38
- async update(source: string, _options?: { approved?: boolean }): Promise<UpdateOutcome> {
39
- const pinned = isPinnedNpmSource(source);
40
- const previousVersion = readResolvedVersion(this.piHome, source);
41
- const output = await this.run(["update", "--extension", source]);
42
- const currentVersion = readResolvedVersion(this.piHome, source);
43
- // Only trust a "nothing changed" conclusion when we actually read a
44
- // real version both before and after (npm source, resolvable in
45
- // node_modules). Otherwise (git:/https: sources, or an unreadable
46
- // node_modules entry) fall back to the traditional "assume it may
47
- // have changed" signal instead of falsely claiming it didn't.
48
- const knowsBoth = previousVersion !== undefined && currentVersion !== undefined;
49
- const changed = !knowsBoth || previousVersion !== currentVersion;
50
- return { output, reloadRequired: changed, alreadyUpToDate: !changed, pinned, previousVersion, currentVersion };
51
- }
52
- }
package/src/installed.ts DELETED
@@ -1,88 +0,0 @@
1
- /** installed.ts — pi's settings.json is the source of truth for what is
2
- * installed; node_modules supplies versions for unpinned sources. */
3
- import { readFileSync } from "node:fs";
4
- import { homedir } from "node:os";
5
- import { join } from "node:path";
6
- import { ENV, SETTINGS_FILE } from "./constants.ts";
7
- import type { InstalledPkg } from "./ports.ts";
8
-
9
- export function splitNpmSource(spec: string): [name: string, version: string] {
10
- const i = spec.lastIndexOf("@");
11
- if (i <= 0) return [spec, ""];
12
- return [spec.slice(0, i), spec.slice(i + 1)];
13
- }
14
-
15
- function extractSource(entry: unknown): string {
16
- if (typeof entry === "string") return entry;
17
- if (entry && typeof entry === "object") {
18
- const s = (entry as Record<string, unknown>)["source"];
19
- if (typeof s === "string") return s;
20
- }
21
- return "";
22
- }
23
-
24
- function nodeModulesVersion(piHome: string, name: string): string | undefined {
25
- try {
26
- const pkg = JSON.parse(readFileSync(join(piHome, "npm", "node_modules", name, "package.json"), "utf8"));
27
- return typeof pkg.version === "string" ? pkg.version : undefined;
28
- } catch {
29
- return undefined;
30
- }
31
- }
32
-
33
- /**
34
- * True when a configured npm: source pins an exact version, e.g.
35
- * "npm:@scope/pkg@1.2.3" vs. the floating "npm:@scope/pkg". `pi update`
36
- * intentionally leaves pinned sources unchanged (see readInstalledPackages)
37
- * but still exits 0 and prints "Updated <source>" either way -- callers
38
- * must not treat that text as proof anything changed.
39
- */
40
- export function isPinnedNpmSource(source: string): boolean {
41
- if (!source.startsWith("npm:")) return false;
42
- const [, pinned] = splitNpmSource(source.slice(4));
43
- return pinned !== "";
44
- }
45
-
46
- /** The bare npm package name for a configured npm: source, pinned or not.
47
- * undefined for git:/https: sources -- there is no npm-registry name to read. */
48
- export function npmPackageName(source: string): string | undefined {
49
- if (!source.startsWith("npm:")) return undefined;
50
- const [name] = splitNpmSource(source.slice(4));
51
- return name;
52
- }
53
-
54
- /** Reads a single npm package's real on-disk resolved version, regardless of
55
- * whether its configured source is pinned -- ground truth for detecting
56
- * whether an update actually changed anything. undefined for non-npm
57
- * sources or when node_modules has no matching package.json to read. */
58
- export function readResolvedVersion(piHome: string, source: string): string | undefined {
59
- const name = npmPackageName(source);
60
- return name ? nodeModulesVersion(piHome, name) : undefined;
61
- }
62
-
63
- export function readInstalledPackages(piHome: string): InstalledPkg[] {
64
- let settings: { packages?: unknown[] };
65
- try {
66
- settings = JSON.parse(readFileSync(join(piHome, SETTINGS_FILE), "utf8"));
67
- } catch {
68
- return [];
69
- }
70
- const out: InstalledPkg[] = [];
71
- for (const raw of settings.packages ?? []) {
72
- const source = extractSource(raw);
73
- if (!source.startsWith("npm:")) continue;
74
- const [name, pinned] = splitNpmSource(source.slice(4));
75
- out.push({
76
- name,
77
- pinned: pinned || undefined,
78
- installed: pinned ? undefined : nodeModulesVersion(piHome, name),
79
- });
80
- }
81
- return out;
82
- }
83
-
84
- export function defaultPiHome(): string {
85
- const envHome = process.env[ENV.PI_HOME];
86
- if (envHome) return envHome;
87
- return join(homedir(), ".pi", "agent");
88
- }
package/src/log.ts DELETED
@@ -1,42 +0,0 @@
1
- /**
2
- * log.ts — structured logging for the service. JSON lines to stderr
3
- * (stdout belongs to CLI output; the TUI owns the terminal UI).
4
- * Level via PI_PACKED_LOG_LEVEL (debug|info|warn|error, default info).
5
- * Sinks are injectable so tests can capture without a terminal.
6
- */
7
-
8
- export type LogLevel = "debug" | "info" | "warn" | "error";
9
- export type LogSink = (line: string) => void;
10
-
11
- const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
12
- const LEVELS = new Set(Object.keys(ORDER));
13
-
14
- export interface LogFields {
15
- [key: string]: unknown;
16
- }
17
-
18
- export interface Logger {
19
- debug(msg: string, fields?: LogFields): void;
20
- info(msg: string, fields?: LogFields): void;
21
- warn(msg: string, fields?: LogFields): void;
22
- error(msg: string, fields?: LogFields): void;
23
- }
24
-
25
- function envLevel(): LogLevel {
26
- const raw = process.env["PI_PACKED_LOG_LEVEL"] ?? "info";
27
- return (LEVELS.has(raw) ? raw : "info") as LogLevel;
28
- }
29
-
30
- export function createLogger(module: string, sink: LogSink = (l) => console.error(l), minLevel?: LogLevel): Logger {
31
- const threshold = ORDER[minLevel ?? envLevel()];
32
- function emit(level: LogLevel, msg: string, fields?: LogFields): void {
33
- if (ORDER[level] < threshold) return;
34
- sink(JSON.stringify({ ts: new Date().toISOString(), level, module, msg, ...fields }));
35
- }
36
- return {
37
- debug: (m, f) => emit("debug", m, f),
38
- info: (m, f) => emit("info", m, f),
39
- warn: (m, f) => emit("warn", m, f),
40
- error: (m, f) => emit("error", m, f),
41
- };
42
- }
package/src/ports.ts DELETED
@@ -1,96 +0,0 @@
1
- /**
2
- * ports.ts — the hexagon's driven ports and lean domain types.
3
- * Driving adapters: HTTP service (service.ts), CLI (cli.ts), watcher,
4
- * tests. Driven adapters: registry.ts (npm), install.ts (pi exec).
5
- */
6
-
7
- export interface Pkg {
8
- name: string;
9
- version: string;
10
- description?: string;
11
- date?: string;
12
- }
13
-
14
- export interface PkgInfo {
15
- name: string;
16
- version: string;
17
- description?: string;
18
- homepage?: string;
19
- repository?: string;
20
- license?: string;
21
- keywords?: string[];
22
- pi?: Record<string, unknown>;
23
- modified?: string;
24
- unpackedSize?: number;
25
- }
26
-
27
- export interface SearchPage {
28
- results: Pkg[];
29
- total: number;
30
- }
31
-
32
- /** Driven port: package metadata source (npm registry, or the daemon proxy). */
33
- export interface Registry {
34
- search(query: string, limit: number): Promise<SearchPage>;
35
- searchPage(query: string, from: number, size: number): Promise<SearchPage>;
36
- searchAll(query: string): Promise<Pkg[]>;
37
- info(name: string): Promise<PkgInfo>;
38
- }
39
-
40
- /**
41
- * `pi update --extension <source>` exits 0 and prints "Updated <source>"
42
- * whether or not anything actually changed -- verified empirically against
43
- * both a pinned, already-current source and an unpinned, already-latest
44
- * one. reloadRequired/alreadyUpToDate are ground truth (on-disk resolved
45
- * version, before vs. after), not that text. previousVersion/currentVersion
46
- * are omitted when unknown (git:/https: sources, or no node_modules entry
47
- * to read) -- reloadRequired then conservatively stays true rather than
48
- * guessing.
49
- */
50
- export interface UpdateOutcome {
51
- output: string;
52
- reloadRequired: boolean;
53
- alreadyUpToDate: boolean;
54
- pinned: boolean;
55
- previousVersion?: string;
56
- currentVersion?: string;
57
- }
58
-
59
- /** Driven port: pi CLI mutations. */
60
- export interface Installer {
61
- install(source: string, options?: { approved?: boolean }): Promise<string>;
62
- remove(source: string, options?: { approved?: boolean }): Promise<string>;
63
- update(source: string, options?: { approved?: boolean }): Promise<UpdateOutcome>;
64
- }
65
-
66
- export interface InstalledPkg {
67
- name: string;
68
- pinned?: string;
69
- installed?: string;
70
- }
71
-
72
- export interface UpdateEntry {
73
- name: string;
74
- installed: string;
75
- latest: string;
76
- detectedAt?: string;
77
- }
78
-
79
- export interface UpdatesSnapshot {
80
- checkedAt: string;
81
- updates: UpdateEntry[];
82
- }
83
-
84
- /** Scope every query to pi packages unless the caller already qualified it. */
85
- import { PI_PACKAGE_KEYWORD } from "./constants.ts";
86
-
87
- export function buildSearchQuery(q: string): string {
88
- const t = q.trim();
89
- if (t.includes("keywords:")) return t;
90
- return t === "" ? PI_PACKAGE_KEYWORD : `${PI_PACKAGE_KEYWORD} ${t}`;
91
- }
92
-
93
- export function clampLimit(v: number, def: number, max: number): number {
94
- if (!Number.isFinite(v) || v <= 0) return def;
95
- return Math.min(v, max);
96
- }