@danypops/pi-packed 0.1.7 → 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/extension/src/index.ts +12 -11
- package/extension/src/packed.ts +64 -75
- package/extension/src/tools.ts +5 -8
- package/extension/src/tui.ts +11 -12
- package/package.json +1 -1
- package/src/db.ts +55 -15
- package/src/ports.ts +1 -1
- package/src/service.ts +26 -0
package/extension/src/index.ts
CHANGED
|
@@ -11,31 +11,32 @@
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { registerTools } from "./tools.js";
|
|
13
13
|
import { showPackages } from "./tui.js";
|
|
14
|
-
import {
|
|
14
|
+
import { createNatives } from "./packed.js";
|
|
15
15
|
import { formatUpdateNotice } from "./model.js";
|
|
16
|
-
import type { UpdatesSnapshot } from "./packed.js";
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Async factory (pi awaits it): dynamic imports load the service library
|
|
18
|
+
// in-process — the native pattern, no subprocess.
|
|
19
|
+
export default async function (pi: ExtensionAPI) {
|
|
20
|
+
const natives = await createNatives();
|
|
21
|
+
registerTools(pi, natives);
|
|
20
22
|
|
|
21
23
|
pi.registerCommand("packages", {
|
|
22
24
|
description: "Browse and manage installed Pi packages (pi-packed)",
|
|
23
25
|
handler: async (_args, ctx) => {
|
|
24
|
-
await showPackages(ctx);
|
|
26
|
+
await showPackages(ctx, natives);
|
|
25
27
|
},
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
//
|
|
29
|
-
// on Pi's own lifecycle event — that's the whole event-driven story.
|
|
30
|
+
// Update check against the local mirror, on Pi's own lifecycle event.
|
|
30
31
|
pi.on("session_start", async (_event, ctx) => {
|
|
31
32
|
if (!ctx.hasUI) return;
|
|
32
33
|
try {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
ctx.ui.notify(`${formatUpdateNotice(
|
|
34
|
+
const updates = await natives.updates();
|
|
35
|
+
if (updates.length) {
|
|
36
|
+
ctx.ui.notify(`${formatUpdateNotice(updates)} — /packages to review`, "info");
|
|
36
37
|
}
|
|
37
38
|
} catch {
|
|
38
|
-
//
|
|
39
|
+
// mirror missing or unreadable — stay silent, never block startup.
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
}
|
package/extension/src/packed.ts
CHANGED
|
@@ -1,90 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* packed.ts —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* packed.ts — native library client. The seam imports the pi-packed service
|
|
3
|
+
* modules IN-PROCESS (web-spider's pattern: dynamic import() bypasses jiti's
|
|
4
|
+
* CJS interop, which can drop class constructors for "type":"module" packages).
|
|
5
|
+
*
|
|
6
|
+
* The SQLite mirror is the shared substrate (WAL: the daemon writes, we read
|
|
7
|
+
* concurrently). The daemon remains the background producer; the seam works
|
|
8
|
+
* even with the daemon down. No subprocess, no token/port files.
|
|
5
9
|
*/
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
export interface InstalledPkg {
|
|
9
|
-
name: string;
|
|
10
|
-
pinned?: string;
|
|
11
|
-
installed?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface UpdateEntry {
|
|
15
|
-
name: string;
|
|
16
|
-
installed: string;
|
|
17
|
-
latest: string;
|
|
18
|
-
detectedAt?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface UpdatesSnapshot {
|
|
22
|
-
checkedAt?: string;
|
|
23
|
-
updates: UpdateEntry[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SearchResult {
|
|
27
|
-
name: string;
|
|
28
|
-
version: string;
|
|
29
|
-
description?: string;
|
|
30
|
-
date?: string;
|
|
31
|
-
}
|
|
10
|
+
import type { Db } from "../../src/db.ts";
|
|
11
|
+
import type { InstalledPkg, Pkg, PkgInfo, UpdateEntry } from "../../src/ports.ts";
|
|
32
12
|
|
|
13
|
+
export type { InstalledPkg, UpdateEntry };
|
|
14
|
+
export type PackageInfo = PkgInfo;
|
|
33
15
|
export interface SearchResponse {
|
|
34
16
|
query: string;
|
|
35
17
|
total: number;
|
|
36
|
-
results:
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface PackageInfo {
|
|
40
|
-
name: string;
|
|
41
|
-
version: string;
|
|
42
|
-
description?: string;
|
|
43
|
-
homepage?: string;
|
|
44
|
-
repository?: string;
|
|
45
|
-
license?: string;
|
|
46
|
-
keywords?: string[];
|
|
47
|
-
pi?: Record<string, unknown>;
|
|
48
|
-
modified?: string;
|
|
49
|
-
unpackedSize?: number;
|
|
18
|
+
results: Pkg[];
|
|
50
19
|
}
|
|
51
20
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
21
|
+
export interface Natives {
|
|
22
|
+
search(query: string, limit: number): Promise<SearchResponse>;
|
|
23
|
+
searchOffline(query: string, limit: number): Promise<SearchResponse>;
|
|
24
|
+
info(name: string): Promise<PackageInfo>;
|
|
25
|
+
installed(): Promise<InstalledPkg[]>;
|
|
26
|
+
updates(): Promise<UpdateEntry[]>;
|
|
27
|
+
install(source: string): Promise<string>;
|
|
28
|
+
remove(name: string): Promise<string>;
|
|
56
29
|
}
|
|
57
30
|
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
export async function createNatives(): Promise<Natives> {
|
|
32
|
+
const [dbMod, regMod, instMod, watchMod, execMod, stateMod, portsMod] = await Promise.all([
|
|
33
|
+
import("../../src/db.ts"),
|
|
34
|
+
import("../../src/registry.ts"),
|
|
35
|
+
import("../../src/installed.ts"),
|
|
36
|
+
import("../../src/watcher.ts"),
|
|
37
|
+
import("../../src/install.ts"),
|
|
38
|
+
import("../../src/state.ts"),
|
|
39
|
+
import("../../src/ports.ts"),
|
|
40
|
+
]);
|
|
62
41
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
execFile(cmd, args, { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
66
|
-
if (err) {
|
|
67
|
-
reject(new Error(stderr?.trim() || err.message));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
resolve({ stdout, stderr });
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}
|
|
42
|
+
const reg = new regMod.HttpRegistry();
|
|
43
|
+
const inst = new execMod.ExecInstaller();
|
|
74
44
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
45
|
+
// Per-call open/close: lifecycle-clean (no held handles, safe with the
|
|
46
|
+
// daemon writing concurrently under WAL).
|
|
47
|
+
function withDb<T>(fn: (db: Db) => T): T {
|
|
48
|
+
const db = dbMod.openDb(dbMod.dbPath(stateMod.stateDir()));
|
|
49
|
+
try {
|
|
50
|
+
return fn(db);
|
|
51
|
+
} finally {
|
|
52
|
+
db.close();
|
|
53
|
+
}
|
|
82
54
|
}
|
|
83
|
-
}
|
|
84
55
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
56
|
+
return {
|
|
57
|
+
async search(query, limit) {
|
|
58
|
+
const { results, total } = await reg.search(portsMod.buildSearchQuery(query), limit);
|
|
59
|
+
return { query, total, results };
|
|
60
|
+
},
|
|
61
|
+
async searchOffline(query, limit) {
|
|
62
|
+
const results = withDb((db) => dbMod.searchLocal(db, query, limit));
|
|
63
|
+
return { query, total: results.length, results };
|
|
64
|
+
},
|
|
65
|
+
info: (name) => reg.info(name),
|
|
66
|
+
installed: () => Promise.resolve(instMod.readInstalledPackages(instMod.defaultPiHome())),
|
|
67
|
+
updates: () =>
|
|
68
|
+
Promise.resolve(
|
|
69
|
+
withDb((db) =>
|
|
70
|
+
watchMod.checkUpdates(
|
|
71
|
+
(name) => dbMod.latestVersion(db, name),
|
|
72
|
+
instMod.readInstalledPackages(instMod.defaultPiHome()),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
install: (source) => inst.install(source),
|
|
77
|
+
remove: (name) => inst.remove(`npm:${name}`),
|
|
78
|
+
};
|
|
90
79
|
}
|
package/extension/src/tools.ts
CHANGED
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
-
import {
|
|
8
|
-
import type { PackageInfo, SearchResponse } from "./packed.js";
|
|
7
|
+
import type { Natives } from "./packed.js";
|
|
9
8
|
|
|
10
9
|
function text(t: string, details: Record<string, unknown> = {}) {
|
|
11
10
|
return { content: [{ type: "text" as const, text: t }], details };
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
export function registerTools(pi: ExtensionAPI): void {
|
|
13
|
+
export function registerTools(pi: ExtensionAPI, natives: Natives): void {
|
|
15
14
|
pi.registerTool({
|
|
16
15
|
name: "pkg_search",
|
|
17
16
|
label: "Pi Package Search",
|
|
@@ -24,9 +23,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
24
23
|
}),
|
|
25
24
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
26
25
|
try {
|
|
27
|
-
const r = await
|
|
28
|
-
["search", params.query, "--limit", String(params.limit ?? 10)],
|
|
29
|
-
);
|
|
26
|
+
const r = await natives.search(params.query, params.limit ?? 10);
|
|
30
27
|
if (r.results.length === 0) return text(`No Pi packages found for "${params.query}".`);
|
|
31
28
|
const lines = r.results.map(
|
|
32
29
|
(p, i) => `${i + 1}. ${p.name}@${p.version}\n ${p.description ?? ""}`,
|
|
@@ -52,7 +49,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
52
49
|
}),
|
|
53
50
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
54
51
|
try {
|
|
55
|
-
const info = await
|
|
52
|
+
const info = await natives.info(params.name);
|
|
56
53
|
const lines = [
|
|
57
54
|
`${info.name}@${info.version}`,
|
|
58
55
|
info.description ?? "",
|
|
@@ -88,7 +85,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
88
85
|
);
|
|
89
86
|
if (!ok) return text("Install cancelled by user.");
|
|
90
87
|
try {
|
|
91
|
-
const out = await
|
|
88
|
+
const out = await natives.install(params.source);
|
|
92
89
|
return text(out || `Installed ${params.source}. Reload with /reload to activate.`);
|
|
93
90
|
} catch (e) {
|
|
94
91
|
return text(`install failed: ${e instanceof Error ? e.message : e}`);
|
package/extension/src/tui.ts
CHANGED
|
@@ -9,33 +9,32 @@ import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
|
9
9
|
import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
|
|
11
11
|
import type { Row, ViewMode } from "./model.js";
|
|
12
|
-
import {
|
|
13
|
-
import type { InstalledPkg, UpdatesSnapshot } from "./packed.js";
|
|
12
|
+
import type { Natives } from "./packed.js";
|
|
14
13
|
|
|
15
14
|
interface PanelAction {
|
|
16
15
|
type: "menu" | "refresh";
|
|
17
16
|
row?: Row;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
async function loadRows(): Promise<{ rows: Row[]; error?: string }> {
|
|
19
|
+
async function loadRows(natives: Natives): Promise<{ rows: Row[]; error?: string }> {
|
|
21
20
|
try {
|
|
22
|
-
const [installed,
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const [installed, updates] = await Promise.all([
|
|
22
|
+
natives.installed(),
|
|
23
|
+
natives.updates().catch(() => []),
|
|
25
24
|
]);
|
|
26
|
-
return { rows: mergeRows(installed,
|
|
25
|
+
return { rows: mergeRows(installed, updates) };
|
|
27
26
|
} catch (e) {
|
|
28
27
|
return { rows: [], error: e instanceof Error ? e.message : String(e) };
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
export async function showPackages(ctx: ExtensionCommandContext): Promise<void> {
|
|
31
|
+
export async function showPackages(ctx: ExtensionCommandContext, natives: Natives): Promise<void> {
|
|
33
32
|
if (!ctx.hasUI) {
|
|
34
33
|
ctx.ui.notify("/packages requires interactive mode", "warning");
|
|
35
34
|
return;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
let { rows, error } = await loadRows();
|
|
37
|
+
let { rows, error } = await loadRows(natives);
|
|
39
38
|
if (error) {
|
|
40
39
|
ctx.ui.notify(`packed unavailable: ${error}`, "error");
|
|
41
40
|
return;
|
|
@@ -47,7 +46,7 @@ export async function showPackages(ctx: ExtensionCommandContext): Promise<void>
|
|
|
47
46
|
if (!action) return; // closed
|
|
48
47
|
|
|
49
48
|
if (action.type === "refresh") {
|
|
50
|
-
({ rows, error } = await loadRows());
|
|
49
|
+
({ rows, error } = await loadRows(natives));
|
|
51
50
|
if (error) ctx.ui.notify(`refresh failed: ${error}`, "error");
|
|
52
51
|
continue;
|
|
53
52
|
}
|
|
@@ -67,7 +66,7 @@ export async function showPackages(ctx: ExtensionCommandContext): Promise<void>
|
|
|
67
66
|
if (choice?.startsWith("Update")) {
|
|
68
67
|
ctx.ui.notify(`Updating ${row.name}…`, "info");
|
|
69
68
|
try {
|
|
70
|
-
await
|
|
69
|
+
await natives.install(`npm:${row.name}@${row.latest}`);
|
|
71
70
|
ctx.ui.notify(`Updated ${row.name} to ${row.latest} (takes effect after /reload)`, "info");
|
|
72
71
|
row.version = row.latest ?? row.version;
|
|
73
72
|
row.hasUpdate = false;
|
|
@@ -78,7 +77,7 @@ export async function showPackages(ctx: ExtensionCommandContext): Promise<void>
|
|
|
78
77
|
const sure = await ctx.ui.confirm("Remove package", `pi remove npm:${row.name}?`);
|
|
79
78
|
if (sure) {
|
|
80
79
|
try {
|
|
81
|
-
await
|
|
80
|
+
await natives.remove(row.name);
|
|
82
81
|
ctx.ui.notify(`Removed ${row.name}`, "info");
|
|
83
82
|
rows = rows.filter((r) => r.name !== row.name);
|
|
84
83
|
} catch (e) {
|
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -3,8 +3,48 @@
|
|
|
3
3
|
* Master-index role: sync_meta (source, time, count, payload checksum —
|
|
4
4
|
* the APT Release / repomd.xml analog). Payload: packages + FTS5 mirror.
|
|
5
5
|
*/
|
|
6
|
-
|
|
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";
|
|
7
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
|
+
}
|
|
8
48
|
import { join } from "node:path";
|
|
9
49
|
import { createHash } from "node:crypto";
|
|
10
50
|
import type { Pkg } from "./ports.ts";
|
|
@@ -21,9 +61,9 @@ export function dbPath(dir: string): string {
|
|
|
21
61
|
return join(dir, DB_FILE);
|
|
22
62
|
}
|
|
23
63
|
|
|
24
|
-
export function openDb(path: string):
|
|
64
|
+
export function openDb(path: string): Db {
|
|
25
65
|
if (path !== ":memory:") mkdirSync(join(path, ".."), { recursive: true });
|
|
26
|
-
const db = new
|
|
66
|
+
const db = IS_BUN ? new DatabaseCtor(path, { create: true }) : new DatabaseCtor(path);
|
|
27
67
|
db.exec("PRAGMA journal_mode = WAL");
|
|
28
68
|
db.exec(`CREATE TABLE IF NOT EXISTS packages (
|
|
29
69
|
name TEXT PRIMARY KEY,
|
|
@@ -54,7 +94,7 @@ function payloadHash(pkgs: Pkg[]): string {
|
|
|
54
94
|
}
|
|
55
95
|
|
|
56
96
|
/** Atomic full-catalog replace (the mirror write side of `apt update`). */
|
|
57
|
-
export function replaceAll(db:
|
|
97
|
+
export function replaceAll(db: Db, pkgs: Pkg[], source: string): SyncMeta {
|
|
58
98
|
const meta: SyncMeta = {
|
|
59
99
|
source,
|
|
60
100
|
fetchedAt: new Date().toISOString(),
|
|
@@ -65,7 +105,7 @@ export function replaceAll(db: Database, pkgs: Pkg[], source: string): SyncMeta
|
|
|
65
105
|
// pages and a package can appear twice in one sync (rowid reuse is fine).
|
|
66
106
|
const insert = db.prepare("INSERT OR REPLACE INTO packages (name, version, description, date) VALUES (?, ?, ?, ?)");
|
|
67
107
|
const insertFts = db.prepare("INSERT INTO packages_fts (rowid, name, description) VALUES (?, ?, ?)");
|
|
68
|
-
db
|
|
108
|
+
inTransaction(db, () => {
|
|
69
109
|
db.exec("DELETE FROM packages");
|
|
70
110
|
db.exec("DELETE FROM packages_fts");
|
|
71
111
|
for (const p of pkgs) {
|
|
@@ -77,32 +117,32 @@ export function replaceAll(db: Database, pkgs: Pkg[], source: string): SyncMeta
|
|
|
77
117
|
"ON CONFLICT(id) DO UPDATE SET source=excluded.source, fetched_at=excluded.fetched_at, " +
|
|
78
118
|
"package_count=excluded.package_count, sha256=excluded.sha256",
|
|
79
119
|
).run(meta.source, meta.fetchedAt, meta.packageCount, meta.sha256);
|
|
80
|
-
})
|
|
120
|
+
});
|
|
81
121
|
return meta;
|
|
82
122
|
}
|
|
83
123
|
|
|
84
|
-
export function getSyncMeta(db:
|
|
124
|
+
export function getSyncMeta(db: Db): SyncMeta | undefined {
|
|
85
125
|
return db
|
|
86
|
-
.
|
|
126
|
+
.prepare("SELECT source, fetched_at AS fetchedAt, package_count AS packageCount, sha256 FROM sync_meta WHERE id = 1")
|
|
87
127
|
.get() as SyncMeta | undefined;
|
|
88
128
|
}
|
|
89
129
|
|
|
90
|
-
export function catalogList(db:
|
|
130
|
+
export function catalogList(db: Db, limit = 0, offset = 0): Pkg[] {
|
|
91
131
|
const sql =
|
|
92
132
|
"SELECT name, version, description, date FROM packages ORDER BY name" +
|
|
93
133
|
(limit > 0 ? ` LIMIT ${Math.floor(limit)} OFFSET ${Math.floor(offset)}` : "");
|
|
94
|
-
return db.
|
|
134
|
+
return db.prepare(sql).all() as Pkg[];
|
|
95
135
|
}
|
|
96
136
|
|
|
97
137
|
/** FTS5 over the mirror (the `apt-cache search` analog). Sanitizes user
|
|
98
138
|
* input into AND-joined quoted terms; falls back to LIKE for hostile input. */
|
|
99
|
-
export function searchLocal(db:
|
|
139
|
+
export function searchLocal(db: Db, q: string, limit = 50): Pkg[] {
|
|
100
140
|
const terms = q.trim().split(/\s+/).filter(Boolean);
|
|
101
141
|
if (terms.length === 0) return [];
|
|
102
142
|
try {
|
|
103
143
|
const match = terms.map((t) => `"${t.replaceAll('"', '""')}"*`).join(" ");
|
|
104
144
|
return db
|
|
105
|
-
.
|
|
145
|
+
.prepare(
|
|
106
146
|
`SELECT p.name, p.version, p.description, p.date
|
|
107
147
|
FROM packages_fts f JOIN packages p ON p.rowid = f.rowid
|
|
108
148
|
WHERE packages_fts MATCH ? ORDER BY rank LIMIT ?`,
|
|
@@ -112,13 +152,13 @@ export function searchLocal(db: Database, q: string, limit = 50): Pkg[] {
|
|
|
112
152
|
// FTS syntax hostility → substring fallback
|
|
113
153
|
const like = `%${q}%`;
|
|
114
154
|
return db
|
|
115
|
-
.
|
|
155
|
+
.prepare("SELECT name, version, description, date FROM packages WHERE name LIKE ? OR description LIKE ? ORDER BY name LIMIT ?")
|
|
116
156
|
.all(like, like, limit) as Pkg[];
|
|
117
157
|
}
|
|
118
158
|
}
|
|
119
159
|
|
|
120
160
|
/** Latest mirrored version of one package (watcher's lookup). */
|
|
121
|
-
export function latestVersion(db:
|
|
122
|
-
const row = db.
|
|
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;
|
|
123
163
|
return row?.version;
|
|
124
164
|
}
|
package/src/ports.ts
CHANGED
package/src/service.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { buildSearchQuery, clampLimit } from "./ports.ts";
|
|
|
7
7
|
import type { Installer, Registry } from "./ports.ts";
|
|
8
8
|
import { TTLCache } from "./cache.ts";
|
|
9
9
|
import { loadUpdates } from "./watcher.ts";
|
|
10
|
+
import { readInstalledPackages, defaultPiHome } from "./installed.ts";
|
|
10
11
|
import { openDb, searchLocal, catalogList, getSyncMeta, dbPath } from "./db.ts";
|
|
11
12
|
import { VERSION, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT } from "./constants.ts";
|
|
12
13
|
import { createLogger } from "./log.ts";
|
|
@@ -18,10 +19,12 @@ export interface Deps {
|
|
|
18
19
|
inst: Installer;
|
|
19
20
|
token: string;
|
|
20
21
|
stateDir: string;
|
|
22
|
+
piHome?: string;
|
|
21
23
|
cache?: TTLCache;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const SOURCE_RE = /^(npm:[A-Za-z0-9@._/-]+|git:[A-Za-z0-9@:._/-]+|https:\/\/[A-Za-z0-9@:._/?=&%~-]+)$/;
|
|
27
|
+
const NAME_RE = /^(@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/;
|
|
25
28
|
|
|
26
29
|
function json(v: unknown, init?: ResponseInit): Response {
|
|
27
30
|
return Response.json(v, init);
|
|
@@ -73,6 +76,29 @@ export function createApp(deps: Deps): { fetch: (req: Request) => Promise<Respon
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
if (path === "/installed" && req.method === "GET") {
|
|
80
|
+
return json(readInstalledPackages(deps.piHome ?? defaultPiHome()));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (path === "/remove" && req.method === "POST") {
|
|
84
|
+
let name = "";
|
|
85
|
+
try {
|
|
86
|
+
const body = (await req.json()) as { name?: unknown };
|
|
87
|
+
name = String(body.name ?? "");
|
|
88
|
+
} catch {
|
|
89
|
+
/* fall through to validation */
|
|
90
|
+
}
|
|
91
|
+
if (!NAME_RE.test(name)) {
|
|
92
|
+
return err(400, "invalid name; want a bare npm package name");
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const output = await deps.inst.remove(`npm:${name}`);
|
|
96
|
+
return json({ ok: true, name, output });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return json({ ok: false, name, output: e instanceof Error ? e.message : String(e) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
if (path === "/install" && req.method === "POST") {
|
|
77
103
|
let source = "";
|
|
78
104
|
try {
|