@danypops/pi-packed 0.1.0 → 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/cache.ts +3 -1
- package/src/catalog.ts +32 -31
- package/src/cli.ts +69 -24
- package/src/client.ts +6 -5
- package/src/constants.ts +50 -0
- package/src/daemon.ts +15 -7
- package/src/db.ts +164 -0
- package/src/installed.ts +4 -2
- package/src/log.ts +42 -0
- package/src/ports.ts +4 -2
- package/src/registry.ts +56 -9
- package/src/service.ts +56 -6
- package/src/state.ts +5 -3
- package/src/watcher.ts +16 -14
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/cache.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** TTLCache — the smart-proxy concern, nothing more. */
|
|
2
|
+
import { CACHE_TTL_MS } from "./constants.ts";
|
|
3
|
+
|
|
2
4
|
export class TTLCache {
|
|
3
5
|
private m = new Map<string, { body: string; expires: number }>();
|
|
4
|
-
constructor(private ttlMs =
|
|
6
|
+
constructor(private ttlMs = CACHE_TTL_MS) {}
|
|
5
7
|
|
|
6
8
|
get(key: string): string | undefined {
|
|
7
9
|
const e = this.m.get(key);
|
package/src/catalog.ts
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* catalog.ts —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* catalog.ts — the sync pipeline (apt update / pkg update analog).
|
|
3
|
+
* Paginates the upstream registry into the local SQLite mirror and records
|
|
4
|
+
* sync metadata (the Release/repomd checksum role) in sync_meta.
|
|
5
5
|
*/
|
|
6
|
-
import { writeFile, readFile, mkdir } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
8
6
|
import type { Pkg, Registry } from "./ports.ts";
|
|
7
|
+
import { PI_PACKAGE_KEYWORD } from "./constants.ts";
|
|
8
|
+
import { openDb, replaceAll, getSyncMeta, dbPath } from "./db.ts";
|
|
9
|
+
import { createLogger } from "./log.ts";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
packages: Pkg[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function catalogPath(dir: string): string {
|
|
16
|
-
return join(dir, "catalog.json");
|
|
17
|
-
}
|
|
11
|
+
const log = createLogger("catalog");
|
|
12
|
+
import type { SyncMeta } from "./db.ts";
|
|
18
13
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
export interface CatalogStatus {
|
|
15
|
+
stale: boolean;
|
|
16
|
+
meta?: SyncMeta;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
export
|
|
19
|
+
export function catalogStatus(dir: string, ttlMs: number): CatalogStatus {
|
|
20
|
+
const db = openDb(dbPath(dir));
|
|
25
21
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
22
|
+
const meta = getSyncMeta(db);
|
|
23
|
+
if (!meta) return { stale: true };
|
|
24
|
+
return { stale: Date.now() - Date.parse(meta.fetchedAt) > ttlMs, meta };
|
|
25
|
+
} finally {
|
|
26
|
+
db.close();
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Full sync: paginate the whole keyword universe into a lean snapshot. */
|
|
38
|
-
export async function syncCatalog(reg: Registry, dir: string, query = "keywords:pi-package"): Promise<number> {
|
|
30
|
+
/** Full mirror sync: upstream pages → atomic SQLite replace. */
|
|
31
|
+
export async function syncCatalog(reg: Registry, dir: string, query: string = PI_PACKAGE_KEYWORD): Promise<number> {
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
log.info("mirror sync started", { query });
|
|
39
34
|
const packages = await reg.searchAll(query);
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const db = openDb(dbPath(dir));
|
|
36
|
+
try {
|
|
37
|
+
replaceAll(db, packages, "npm:" + query);
|
|
38
|
+
log.info("mirror sync complete", { packages: packages.length, ms: Date.now() - t0 });
|
|
39
|
+
return packages.length;
|
|
40
|
+
} finally {
|
|
41
|
+
db.close();
|
|
42
|
+
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function startCatalogSync(
|
|
@@ -49,9 +50,9 @@ export function startCatalogSync(
|
|
|
49
50
|
): () => void {
|
|
50
51
|
async function sync(): Promise<void> {
|
|
51
52
|
try {
|
|
52
|
-
if (
|
|
53
|
+
if (catalogStatus(dir, ttlMs).stale) {
|
|
53
54
|
const n = await syncCatalog(reg, dir);
|
|
54
|
-
|
|
55
|
+
log.info("scheduled sync complete", { packages: n });
|
|
55
56
|
}
|
|
56
57
|
} catch (e) {
|
|
57
58
|
onError?.(e);
|
package/src/cli.ts
CHANGED
|
@@ -7,20 +7,25 @@
|
|
|
7
7
|
import { buildSearchQuery, clampLimit } from "./ports.ts";
|
|
8
8
|
import type { Installer, Registry } from "./ports.ts";
|
|
9
9
|
import { readInstalledPackages } from "./installed.ts";
|
|
10
|
-
import { checkUpdates
|
|
11
|
-
import {
|
|
10
|
+
import { checkUpdates } from "./watcher.ts";
|
|
11
|
+
import { syncCatalog } from "./catalog.ts";
|
|
12
|
+
import { openDb, searchLocal, catalogList, getSyncMeta, latestVersion, dbPath } from "./db.ts";
|
|
12
13
|
import { NAME_RE } from "./install.ts";
|
|
14
|
+
import {
|
|
15
|
+
VERSION, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT, NPM_REGISTRY_BASE, SEARCH_PAGE_SIZE, MIRROR_PAGE_DELAY_MS,
|
|
16
|
+
} from "./constants.ts";
|
|
13
17
|
|
|
14
18
|
const SOURCE_RE = /^(npm:[A-Za-z0-9@._/-]+|git:[A-Za-z0-9@:._/-]+|https:\/\/[A-Za-z0-9@:._/?=&%~-]+)$/;
|
|
15
19
|
|
|
16
20
|
const USAGE = `packed — package service for the Pi agent
|
|
17
21
|
|
|
18
22
|
usage:
|
|
19
|
-
packed search <query> [--
|
|
23
|
+
packed search <query> [--offline] [--json] search npm (or the local mirror with --offline)
|
|
20
24
|
packed info <name> [--json] package details
|
|
21
|
-
packed updates [--
|
|
25
|
+
packed updates [--json] updates per the local mirror
|
|
26
|
+
packed mirror [--json] sync upstream into the local SQLite index
|
|
22
27
|
packed installed [--json] installed pi packages
|
|
23
|
-
packed catalog [--json]
|
|
28
|
+
packed catalog [--json] local package index (apt-cache stats)
|
|
24
29
|
packed install <source> pi install npm:|git:|https://…
|
|
25
30
|
packed remove <name> remove by bare npm name
|
|
26
31
|
packed serve run the long-running daemon
|
|
@@ -46,17 +51,19 @@ interface Flags {
|
|
|
46
51
|
json: boolean;
|
|
47
52
|
limit: number;
|
|
48
53
|
cached: boolean;
|
|
54
|
+
offline: boolean;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function parseFlags(rest: string[]): { flags: Flags; pos: string[] } {
|
|
52
|
-
const flags: Flags = { json: false, limit:
|
|
58
|
+
const flags: Flags = { json: false, limit: SEARCH_DEFAULT_LIMIT, cached: false, offline: false };
|
|
53
59
|
const pos: string[] = [];
|
|
54
60
|
for (let i = 0; i < rest.length; i++) {
|
|
55
61
|
const a = rest[i]!;
|
|
56
62
|
if (a === "--json") flags.json = true;
|
|
57
63
|
else if (a === "--cached") flags.cached = true;
|
|
58
|
-
else if (a === "--
|
|
59
|
-
else if (a
|
|
64
|
+
else if (a === "--offline") flags.offline = true;
|
|
65
|
+
else if (a === "--limit" && i + 1 < rest.length) flags.limit = Number(rest[++i]) || SEARCH_DEFAULT_LIMIT;
|
|
66
|
+
else if (a.startsWith("--limit=")) flags.limit = Number(a.slice(8)) || SEARCH_DEFAULT_LIMIT;
|
|
60
67
|
else pos.push(a);
|
|
61
68
|
}
|
|
62
69
|
return { flags, pos };
|
|
@@ -70,11 +77,26 @@ const usageErr = (out: string): CliResult => ({ code: 2, out });
|
|
|
70
77
|
|
|
71
78
|
const commands: Record<string, { usage: string; run: Command }> = {
|
|
72
79
|
search: {
|
|
73
|
-
usage: "packed search <query> [--limit N] [--json]",
|
|
80
|
+
usage: "packed search <query> [--offline] [--limit N] [--json]",
|
|
74
81
|
async run(_rest, d, flags, pos) {
|
|
75
82
|
const q = pos[0];
|
|
76
83
|
if (!q) return usageErr(`usage: ${commands["search"]!.usage}\n`);
|
|
77
|
-
const
|
|
84
|
+
const limit = clampLimit(flags.limit, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT);
|
|
85
|
+
// --offline: query the SQLite mirror only (apt-cache search analog)
|
|
86
|
+
if (flags.offline) {
|
|
87
|
+
const db = openDb(dbPath(d.stateDir));
|
|
88
|
+
try {
|
|
89
|
+
const results = searchLocal(db, q, limit);
|
|
90
|
+
if (flags.json) return ok(JSON.stringify({ query: q, total: results.length, results, offline: true }) + "\n");
|
|
91
|
+
if (results.length === 0) return ok(`no mirrored packages match "${q}" (run: packed mirror)\n`);
|
|
92
|
+
let out = `${results.length} mirrored package(s):\n\n`;
|
|
93
|
+
for (const p of results) out += ` ${p.name}@${p.version}\n ${p.description ?? ""}\n`;
|
|
94
|
+
return ok(out);
|
|
95
|
+
} finally {
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { results, total } = await d.reg.search(buildSearchQuery(q), limit);
|
|
78
100
|
if (flags.json) return ok(JSON.stringify({ query: q, total, results }) + "\n");
|
|
79
101
|
if (results.length === 0) return ok(`no pi packages found for "${q}"\n`);
|
|
80
102
|
let out = `${total} package(s) (showing ${results.length}):\n\n`;
|
|
@@ -83,6 +105,15 @@ const commands: Record<string, { usage: string; run: Command }> = {
|
|
|
83
105
|
},
|
|
84
106
|
},
|
|
85
107
|
|
|
108
|
+
mirror: {
|
|
109
|
+
usage: "packed mirror [--json] (sync the upstream registry into the local SQLite index — the apt update analog)",
|
|
110
|
+
async run(_rest, d, flags) {
|
|
111
|
+
const n = await syncCatalog(d.reg, d.stateDir);
|
|
112
|
+
if (flags.json) return ok(JSON.stringify({ synced: n }) + "\n");
|
|
113
|
+
return ok(`mirrored ${n} packages into the local index\n`);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
86
117
|
info: {
|
|
87
118
|
usage: "packed info <name> [--json]",
|
|
88
119
|
async run(_rest, d, flags, pos) {
|
|
@@ -98,18 +129,19 @@ const commands: Record<string, { usage: string; run: Command }> = {
|
|
|
98
129
|
},
|
|
99
130
|
|
|
100
131
|
updates: {
|
|
101
|
-
usage: "packed updates [--
|
|
132
|
+
usage: "packed updates [--json] (from the local mirror — run `packed mirror` first)",
|
|
102
133
|
async run(_rest, d, flags) {
|
|
134
|
+
const db = openDb(dbPath(d.stateDir));
|
|
103
135
|
let updates;
|
|
104
|
-
|
|
105
|
-
updates = (
|
|
106
|
-
}
|
|
107
|
-
|
|
136
|
+
try {
|
|
137
|
+
updates = checkUpdates((name) => latestVersion(db, name), readInstalledPackages(d.piHome));
|
|
138
|
+
} finally {
|
|
139
|
+
db.close();
|
|
108
140
|
}
|
|
109
141
|
if (flags.json) {
|
|
110
142
|
return ok(JSON.stringify({ checkedAt: new Date().toISOString(), updates }) + "\n");
|
|
111
143
|
}
|
|
112
|
-
if (updates.length === 0) return ok("all pi packages up to date\n");
|
|
144
|
+
if (updates.length === 0) return ok("all pi packages up to date (per the local mirror)\n");
|
|
113
145
|
let out = `${updates.length} update(s) available:\n\n`;
|
|
114
146
|
for (const u of updates) out += ` ${u.name} ${u.installed} → ${u.latest}\n`;
|
|
115
147
|
return ok(out + "\nrun: pi update --extensions\n");
|
|
@@ -128,12 +160,21 @@ const commands: Record<string, { usage: string; run: Command }> = {
|
|
|
128
160
|
catalog: {
|
|
129
161
|
usage: "packed catalog [--json]",
|
|
130
162
|
async run(_rest, d, flags) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
const db = openDb(dbPath(d.stateDir));
|
|
164
|
+
try {
|
|
165
|
+
const meta = getSyncMeta(db);
|
|
166
|
+
const packages = catalogList(db);
|
|
167
|
+
if (flags.json) {
|
|
168
|
+
return ok(JSON.stringify({ fetchedAt: meta?.fetchedAt, sha256: meta?.sha256, packages }) + "\n");
|
|
169
|
+
}
|
|
170
|
+
let out = `${packages.length} packages in the local index`;
|
|
171
|
+
if (meta) out += ` (synced ${meta.fetchedAt}, sha256:${meta.sha256.slice(0, 12)}…)`;
|
|
172
|
+
out += "\n\n";
|
|
173
|
+
for (const p of packages.slice(0, 50)) out += ` ${p.name}@${p.version}\n`;
|
|
174
|
+
return ok(out);
|
|
175
|
+
} finally {
|
|
176
|
+
db.close();
|
|
177
|
+
}
|
|
137
178
|
},
|
|
138
179
|
},
|
|
139
180
|
|
|
@@ -175,7 +216,7 @@ const commands: Record<string, { usage: string; run: Command }> = {
|
|
|
175
216
|
version: {
|
|
176
217
|
usage: "packed version",
|
|
177
218
|
async run() {
|
|
178
|
-
return ok("
|
|
219
|
+
return ok(VERSION + "\n");
|
|
179
220
|
},
|
|
180
221
|
},
|
|
181
222
|
};
|
|
@@ -226,7 +267,11 @@ if (import.meta.main) {
|
|
|
226
267
|
const { resolveRegistry } = await import("./client.ts");
|
|
227
268
|
const { ExecInstaller } = await import("./install.ts");
|
|
228
269
|
const dir = stateDir();
|
|
229
|
-
|
|
270
|
+
// mirror talks to UPSTREAM, not the daemon cache — apt update semantics.
|
|
271
|
+
const reg =
|
|
272
|
+
args[0] === "mirror"
|
|
273
|
+
? new (await import("./registry.ts")).HttpRegistry(NPM_REGISTRY_BASE, SEARCH_PAGE_SIZE, MIRROR_PAGE_DELAY_MS)
|
|
274
|
+
: await resolveRegistry(dir, NPM_REGISTRY_BASE);
|
|
230
275
|
const { code, out } = await cliRun(args, {
|
|
231
276
|
reg,
|
|
232
277
|
inst: new ExecInstaller(),
|
package/src/client.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { readFileSync } from "node:fs";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { PkgInfo, Registry, SearchPage } from "./ports.ts";
|
|
9
9
|
import { HttpRegistry } from "./registry.ts";
|
|
10
|
+
import { DAEMON_HOST, PROBE_TIMEOUT_MS, REGISTRY_FETCH_TIMEOUT_MS, PORT_FILE, TOKEN_FILE } from "./constants.ts";
|
|
10
11
|
|
|
11
12
|
export class DaemonRegistry implements Registry {
|
|
12
13
|
constructor(
|
|
@@ -17,7 +18,7 @@ export class DaemonRegistry implements Registry {
|
|
|
17
18
|
private async get<T>(path: string): Promise<T> {
|
|
18
19
|
const res = await fetch(`${this.base}${path}`, {
|
|
19
20
|
headers: { authorization: `Bearer ${this.token}` },
|
|
20
|
-
signal: AbortSignal.timeout(
|
|
21
|
+
signal: AbortSignal.timeout(REGISTRY_FETCH_TIMEOUT_MS),
|
|
21
22
|
});
|
|
22
23
|
if (!res.ok) throw new Error(`daemon HTTP ${res.status}`);
|
|
23
24
|
return (await res.json()) as T;
|
|
@@ -55,16 +56,16 @@ export async function probe(dir: string): Promise<DaemonHandle | undefined> {
|
|
|
55
56
|
let port: string;
|
|
56
57
|
let token: string;
|
|
57
58
|
try {
|
|
58
|
-
port = readFileSync(join(dir,
|
|
59
|
-
token = readFileSync(join(dir,
|
|
59
|
+
port = readFileSync(join(dir, PORT_FILE), "utf8").trim();
|
|
60
|
+
token = readFileSync(join(dir, TOKEN_FILE), "utf8").trim();
|
|
60
61
|
} catch {
|
|
61
62
|
return undefined;
|
|
62
63
|
}
|
|
63
|
-
const base = `http
|
|
64
|
+
const base = `http://${DAEMON_HOST}:${port}`;
|
|
64
65
|
try {
|
|
65
66
|
const res = await fetch(`${base}/health`, {
|
|
66
67
|
headers: { authorization: `Bearer ${token}` },
|
|
67
|
-
signal: AbortSignal.timeout(
|
|
68
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
68
69
|
});
|
|
69
70
|
if (res.ok) return { base, token };
|
|
70
71
|
} catch {
|