@danypops/pi-packed 0.19.7 → 0.19.10

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.
Files changed (94) hide show
  1. package/dist/client.d.ts +109 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +1 -0
  4. package/dist/protocol.d.ts +221 -0
  5. package/dist/protocol.d.ts.map +1 -0
  6. package/dist/protocol.js +1 -0
  7. package/extension/src/{permission.ts → approval/permission.ts} +1 -1
  8. package/extension/src/index.ts +1 -1
  9. package/extension/src/packed.ts +2 -2
  10. package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
  11. package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
  12. package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
  13. package/extension/src/tool-output.ts +1 -1
  14. package/extension/src/tools.ts +2 -2
  15. package/extension/src/tui.ts +4 -4
  16. package/package.json +31 -8
  17. package/service/schema/pi-setup-v1.schema.json +70 -0
  18. package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
  19. package/service/src/adoption/advisories.ts +268 -0
  20. package/service/src/adoption/check.ts +872 -0
  21. package/service/src/adoption/commit-freshness.ts +167 -0
  22. package/service/src/adoption/doctor.ts +135 -0
  23. package/service/src/adoption/install-validation.ts +187 -0
  24. package/service/src/adoption/pack.ts +291 -0
  25. package/service/src/adoption/score.ts +466 -0
  26. package/service/src/adoption/smoke-child.ts +113 -0
  27. package/service/src/adoption/smoke.ts +282 -0
  28. package/service/src/cli/cli.ts +926 -0
  29. package/service/src/daemon/cleanup.ts +76 -0
  30. package/service/src/daemon/client.ts +412 -0
  31. package/service/src/daemon/daemon-service.ts +249 -0
  32. package/service/src/daemon/daemon.ts +110 -0
  33. package/service/src/daemon/service.ts +664 -0
  34. package/service/src/daemon/watcher.ts +92 -0
  35. package/service/src/index/build-index.ts +256 -0
  36. package/service/src/packages/catalog.ts +61 -0
  37. package/service/src/packages/db.ts +224 -0
  38. package/service/src/packages/install.ts +60 -0
  39. package/service/src/packages/installed.ts +123 -0
  40. package/service/src/packages/package.ts +141 -0
  41. package/service/src/packages/resources.ts +203 -0
  42. package/service/src/pi/pi-version.ts +171 -0
  43. package/service/src/public/atomic-json.ts +32 -0
  44. package/service/src/public/client.ts +277 -0
  45. package/service/src/public/protocol.ts +169 -0
  46. package/service/src/publish/publish.ts +855 -0
  47. package/service/src/registry/registry.ts +246 -0
  48. package/service/src/security/security.ts +128 -0
  49. package/service/src/self-update/self-update.ts +148 -0
  50. package/service/src/setup/setup.ts +761 -0
  51. package/service/src/shared/atomic-json.ts +33 -0
  52. package/service/src/shared/cache.ts +21 -0
  53. package/service/src/shared/constants.ts +73 -0
  54. package/service/src/shared/log.ts +21 -0
  55. package/service/src/shared/paths.ts +88 -0
  56. package/service/src/shared/state.ts +15 -0
  57. package/service/src/shared/version.ts +46 -0
  58. package/service/test/advisories.test.ts +287 -0
  59. package/service/test/check.test.ts +368 -0
  60. package/service/test/cleanup.test.ts +220 -0
  61. package/service/test/cli.test.ts +1303 -0
  62. package/service/test/core.test.ts +181 -0
  63. package/service/test/daemon-kit-migration.test.ts +181 -0
  64. package/service/test/daemon-service.test.ts +238 -0
  65. package/service/test/db.test.ts +178 -0
  66. package/service/test/doctor.test.ts +234 -0
  67. package/service/test/domain.test.ts +291 -0
  68. package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
  69. package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
  70. package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
  71. package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
  72. package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
  73. package/service/test/index.test.ts +353 -0
  74. package/service/test/install-validation.test.ts +114 -0
  75. package/service/test/install.test.ts +113 -0
  76. package/service/test/log.test.ts +42 -0
  77. package/service/test/pack-score.test.ts +513 -0
  78. package/service/test/pi-version.test.ts +318 -0
  79. package/service/test/public-boundary.test.ts +54 -0
  80. package/service/test/public-client.test.ts +127 -0
  81. package/service/test/public-consumer.ts +8 -0
  82. package/service/test/publish.test.ts +333 -0
  83. package/service/test/registry-contract.test.ts +148 -0
  84. package/service/test/resources.test.ts +255 -0
  85. package/service/test/security.test.ts +89 -0
  86. package/service/test/self-update.test.ts +257 -0
  87. package/service/test/service.test.ts +555 -0
  88. package/service/test/setup.test.ts +375 -0
  89. package/service/test/smoke.test.ts +118 -0
  90. package/service/test/version.test.ts +37 -0
  91. package/service/tsconfig.consumer.json +13 -0
  92. package/service/tsconfig.public.json +12 -0
  93. /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
  94. /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * daemon-service.ts — resolves an installed npm package's own persistent-
3
+ * daemon declaration into a real vehicle-server ServiceSpec, and installs it
4
+ * through the exact same systemd/launchd/Registry mechanism that
5
+ * package's own `<bin> service install` command already uses (daemon-
6
+ * kit's installUserService()), so the two are fully interchangeable for
7
+ * the same package.
8
+ *
9
+ * Answers the gap every daemon-backed pi package shared: `pi install
10
+ * npm:@danypops/web-spider-daemon` (or any equivalent) already gets you a
11
+ * daemon that auto-spawns lazily on first use (see each package's own
12
+ * connectWithPolicy/ensure*Client wiring) -- but surviving a reboot always
13
+ * required separately discovering and running that package's own `service
14
+ * install` command by hand. This lets Packed do it as one more explicitly
15
+ * approved mutation, the same way it already gates install/update/remove.
16
+ *
17
+ * Resolution order: an explicit `packed.daemonService` manifest always
18
+ * wins (covers a non-standard bin name or args), then falls back to
19
+ * detectVehicleDaemonService() -- reading conventions every real
20
+ * daemon-backed package already follows (a `bin` entry, a `serve`
21
+ * subcommand, a real dependency on @danypops/vehicle-server or the
22
+ * legacy @danypops/daemon-kit) instead of requiring every package author
23
+ * to redeclare them. Confirmed live: as of this module's introduction,
24
+ * zero installed packages had adopted the manifest -- an unused opt-in
25
+ * step isn't a real convention, so detection reads what's already true
26
+ * on disk instead of asking for one more declaration.
27
+ */
28
+ import { readFileSync } from "node:fs";
29
+ import { basename, join } from "node:path";
30
+ import { resolveDaemonPaths } from "@danypops/vehicle-server/paths";
31
+ import {
32
+ createNodeServiceInstallDeps,
33
+ installUserService,
34
+ isServiceInstalled,
35
+ type ServiceInstallDeps,
36
+ type ServiceInstallResult,
37
+ type ServiceSpec,
38
+ } from "@danypops/vehicle-server/service";
39
+ import { npmPackageName } from "../packages/installed.ts";
40
+
41
+ export interface DaemonServiceManifest {
42
+ /** Relative to the installed package's own root directory. */
43
+ binPath: string;
44
+ args?: string[];
45
+ /** Defaults to the bare npm package name (scope stripped). Used for the state-directory/unit name, matching each package's own service-install convention (e.g. web-spider.service). */
46
+ name?: string;
47
+ displayName?: string;
48
+ }
49
+
50
+ export type ResolveDaemonServiceResult =
51
+ | { ok: true; spec: ServiceSpec }
52
+ /** notADaemon: this package (and nothing one level into its own dependencies) is Vehicle-shaped at all -- the overwhelmingly common case for a non-daemon Pi package, distinct from a real failure (systemctl unavailable, spec resolved but install itself failed). A caller composing install + install-service (see cli.ts's `install` command) uses this to stay silent instead of surfacing a false alarm on every ordinary package install. */
53
+ | { ok: false; reason: string; notADaemon?: boolean };
54
+
55
+ function unscopedName(packageName: string): string {
56
+ const slash = packageName.indexOf("/");
57
+ return slash === -1 ? packageName : packageName.slice(slash + 1);
58
+ }
59
+
60
+ /** A resolved daemon entrypoint, already an absolute binPath -- the common shape both an explicit manifest and detection converge to before a ServiceSpec is built. */
61
+ interface ResolvedDaemonEntrypoint {
62
+ binPath: string;
63
+ args?: string[];
64
+ name: string;
65
+ displayName?: string;
66
+ }
67
+
68
+ interface InstalledPackageJson {
69
+ name?: string;
70
+ bin?: string | Record<string, string>;
71
+ dependencies?: Record<string, string>;
72
+ packed?: { daemonService?: DaemonServiceManifest };
73
+ }
74
+
75
+ function readPackageJson(dir: string): InstalledPackageJson | undefined {
76
+ try {
77
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as InstalledPackageJson;
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function firstBinPath(pkg: InstalledPackageJson): string | undefined {
84
+ if (typeof pkg.bin === "string") return pkg.bin;
85
+ if (pkg.bin && typeof pkg.bin === "object") return Object.values(pkg.bin)[0];
86
+ return undefined;
87
+ }
88
+
89
+ /** @danypops/vehicle-server is the current substrate; @danypops/daemon-kit is the pre-absorption package name -- a real, live signal for any not-yet-migrated ecosystem package (or an older installed version of an already-migrated one), not just a historical artifact. */
90
+ const VEHICLE_DEPENDENCY_NAMES = ["@danypops/vehicle-server", "@danypops/daemon-kit"];
91
+
92
+ function dependsOnVehicle(pkg: InstalledPackageJson): boolean {
93
+ const deps = pkg.dependencies ?? {};
94
+ return VEHICLE_DEPENDENCY_NAMES.some((depName) => depName in deps);
95
+ }
96
+
97
+ /**
98
+ * Detects a Vehicle-shaped (or legacy daemon-kit-shaped) daemon without
99
+ * requiring an explicit packed.daemonService manifest. Checks the
100
+ * installed package itself first (the "I am the daemon" case, e.g.
101
+ * @danypops/papyrus installed directly), then one level into its own
102
+ * `dependencies` (the common case: a Pi extension like @danypops/
103
+ * pi-papyrus has no daemon of its own, but npm already resolved its real
104
+ * daemon dependency during `pi install`). That dependency can land in
105
+ * either of two real, both-observed-live layouts: nested directly under
106
+ * the installing package's own node_modules, or hoisted by npm to the
107
+ * single flat node_modules root every Packed-managed install shares --
108
+ * confirmed live for @danypops/pi-papyrus + @danypops/papyrus, which npm
109
+ * hoists to siblings rather than nesting. `hoistedNodeModulesDir` (when
110
+ * given) is checked as a fallback whenever the nested path misses.
111
+ * `args` defaults to `["serve"]` -- confirmed as a real, universal
112
+ * convention across every daemon-backed package checked (papyrus,
113
+ * web-spider, packed, enigma all expose a `serve` subcommand on the same
114
+ * bin their `service install` command already points at).
115
+ */
116
+ export function detectVehicleDaemonService(
117
+ packageDir: string,
118
+ fallbackName: string,
119
+ hoistedNodeModulesDir?: string,
120
+ ): ResolvedDaemonEntrypoint | undefined {
121
+ const own = readPackageJson(packageDir);
122
+ if (!own) return undefined;
123
+
124
+ const ownBin = firstBinPath(own);
125
+ if (ownBin && dependsOnVehicle(own)) {
126
+ return { binPath: join(packageDir, ownBin), args: ["serve"], name: unscopedName(own.name ?? fallbackName) };
127
+ }
128
+
129
+ for (const depName of Object.keys(own.dependencies ?? {})) {
130
+ const candidateDirs = [
131
+ join(packageDir, "node_modules", depName),
132
+ ...(hoistedNodeModulesDir ? [join(hoistedNodeModulesDir, depName)] : []),
133
+ ];
134
+ for (const depDir of candidateDirs) {
135
+ const dep = readPackageJson(depDir);
136
+ if (!dep) continue;
137
+ const depBin = firstBinPath(dep);
138
+ if (depBin && dependsOnVehicle(dep)) {
139
+ return { binPath: join(depDir, depBin), args: ["serve"], name: unscopedName(dep.name ?? depName) };
140
+ }
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+
146
+ function buildSpec(entry: ResolvedDaemonEntrypoint): ServiceSpec {
147
+ const paths = resolveDaemonPaths({
148
+ stateDirectoryName: entry.name,
149
+ // Only serviceDescriptor is used below; these three exist purely to
150
+ // satisfy resolveDaemonPaths()'s shared shape for a package this
151
+ // module never opens the db/token/handle of.
152
+ databaseFilename: "unused.db",
153
+ tokenFilename: "unused-token",
154
+ handleFilename: "unused-handle.json",
155
+ systemdUnitName: `${entry.name}.service`,
156
+ });
157
+ return {
158
+ name: entry.name,
159
+ displayName: entry.displayName,
160
+ binPath: entry.binPath,
161
+ args: entry.args,
162
+ descriptorPath: paths.serviceDescriptor,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Resolves the installed package's daemon entrypoint -- an explicit
168
+ * `packed.daemonService` manifest first, then detectVehicleDaemonService()
169
+ * -- and builds the exact ServiceSpec its own `service install` command
170
+ * would build. npm sources only for now; git:/local sources resolve to a
171
+ * different on-disk layout not yet supported here.
172
+ */
173
+ export function resolveDaemonServiceSpec(piHome: string, source: string): ResolveDaemonServiceResult {
174
+ const packageName = npmPackageName(source);
175
+ if (!packageName) return { ok: false, reason: "daemon-service installation only supports npm: sources today" };
176
+
177
+ const packageDir = join(piHome, "npm", "node_modules", packageName);
178
+ const pkg = readPackageJson(packageDir);
179
+ if (!pkg) return { ok: false, reason: `could not read ${join(packageDir, "package.json")} -- is ${packageName} installed?` };
180
+
181
+ const manifest = pkg.packed?.daemonService;
182
+ if (manifest && typeof manifest.binPath === "string" && manifest.binPath.length > 0) {
183
+ return {
184
+ ok: true,
185
+ spec: buildSpec({
186
+ binPath: join(packageDir, manifest.binPath),
187
+ args: manifest.args,
188
+ name: manifest.name ?? unscopedName(packageName),
189
+ displayName: manifest.displayName,
190
+ }),
191
+ };
192
+ }
193
+
194
+ const detected = detectVehicleDaemonService(packageDir, packageName, join(piHome, "npm", "node_modules"));
195
+ if (detected) return { ok: true, spec: buildSpec(detected) };
196
+
197
+ return {
198
+ ok: false,
199
+ notADaemon: true,
200
+ reason: `${packageName} does not declare a packed.daemonService manifest and no Vehicle-shaped daemon dependency was detected`,
201
+ };
202
+ }
203
+
204
+ export interface DaemonServiceInstaller {
205
+ install(
206
+ piHome: string,
207
+ source: string,
208
+ ): { ok: true; result: ServiceInstallResult; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean };
209
+ restart(
210
+ piHome: string,
211
+ source: string,
212
+ ): { ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean };
213
+ }
214
+
215
+ export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
216
+ install(
217
+ piHome: string,
218
+ source: string,
219
+ ): { ok: true; result: ServiceInstallResult; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean } {
220
+ const resolved = resolveDaemonServiceSpec(piHome, source);
221
+ if (!resolved.ok) return resolved;
222
+ const result = installUserService(resolved.spec, createNodeServiceInstallDeps());
223
+ return { ok: true, result, spec: resolved.spec };
224
+ }
225
+
226
+ restart(
227
+ piHome: string,
228
+ source: string,
229
+ ): { ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean } {
230
+ const resolved = resolveDaemonServiceSpec(piHome, source);
231
+ if (!resolved.ok) return resolved;
232
+ const deps = createNodeServiceInstallDeps();
233
+ if (!isServiceInstalled(resolved.spec, deps)) {
234
+ return { ok: true, restarted: false, reason: `no persistent service is registered for ${resolved.spec.name}`, spec: resolved.spec };
235
+ }
236
+ const result = restartUserService(resolved.spec, deps);
237
+ return { ok: true, restarted: result.restarted, reason: result.reason, spec: resolved.spec };
238
+ }
239
+ }
240
+
241
+ /** Linux/systemd only, matching installUserService's platform coverage order -- macOS/Windows report unsupported rather than guessing at launchctl/reg.exe restart equivalents this module doesn't implement. */
242
+ export function restartUserService(spec: ServiceSpec, deps: ServiceInstallDeps): { restarted: boolean; reason?: string } {
243
+ const platform = deps.platform ?? process.platform;
244
+ if (platform !== "linux")
245
+ return { restarted: false, reason: `restart is not supported on platform "${platform}" yet -- restart ${spec.name} manually` };
246
+ const result = deps.runCommand("systemctl", ["--user", "restart", basename(spec.descriptorPath)]);
247
+ if (!result.ok) return { restarted: false, reason: `systemctl --user restart failed: ${result.output}` };
248
+ return { restarted: true };
249
+ }
@@ -0,0 +1,110 @@
1
+ import { dirname } from "node:path";
2
+ import {
3
+ type MaintenanceTask,
4
+ type RunningDaemon,
5
+ runDaemonProcess,
6
+ type StartDaemonOptions,
7
+ startDaemon,
8
+ } from "@danypops/vehicle-server/daemon";
9
+ import { ensureAuthToken } from "@danypops/vehicle-server/paths";
10
+ import { generateIndex, indexPath, indexStatus } from "../index/build-index.ts";
11
+ import { catalogStatus, syncCatalog } from "../packages/catalog.ts";
12
+ import { latestVersion, openDb } from "../packages/db.ts";
13
+ import { ExecInstaller } from "../packages/install.ts";
14
+ import { defaultPiHome, readInstalledPackages } from "../packages/installed.ts";
15
+ import type { Installer, Registry } from "../packages/package.ts";
16
+ import { HttpRegistry } from "../registry/registry.ts";
17
+ import {
18
+ CATALOG_INTERVAL_DEFAULT_MS,
19
+ ENV,
20
+ IDLE_BUDGET_DEFAULT_MS,
21
+ INDEX_INTERVAL_DEFAULT_MS,
22
+ WATCH_INTERVAL_DEFAULT_MS,
23
+ WATCHDOG_TICK_MS,
24
+ } from "../shared/constants.ts";
25
+ import { createLogger } from "../shared/log.ts";
26
+ import { legacyPackedStateDirectory, migrateLegacyPackedState, type PackedPaths, resolvePackedPaths } from "../shared/paths.ts";
27
+ import { envMs } from "../shared/state.ts";
28
+ import { createApp } from "./service.ts";
29
+ import { checkUpdates, saveUpdates } from "./watcher.ts";
30
+
31
+ const logger = createLogger("daemon");
32
+
33
+ export interface StartPackedDaemonOptions {
34
+ paths?: PackedPaths;
35
+ reg?: Registry;
36
+ inst?: Installer;
37
+ piHome?: string;
38
+ maintenanceTasks?: MaintenanceTask[];
39
+ idleBudgetMs?: number;
40
+ migrateLegacy?: boolean;
41
+ }
42
+
43
+ function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOptions {
44
+ const paths = options.paths ?? resolvePackedPaths();
45
+ if (options.migrateLegacy ?? options.paths === undefined) migrateLegacyPackedState(paths, legacyPackedStateDirectory());
46
+ const token = ensureAuthToken(paths.token, "Packed");
47
+ const reg = options.reg ?? new HttpRegistry();
48
+ const inst = options.inst ?? new ExecInstaller();
49
+ const piHome = options.piHome ?? defaultPiHome();
50
+ const database = openDb(paths.database);
51
+ const configuredMaintenanceTasks = options.maintenanceTasks ?? [
52
+ {
53
+ name: "package-update-check",
54
+ intervalMs: envMs(ENV.WATCH_SECS, WATCH_INTERVAL_DEFAULT_MS),
55
+ run: async () => {
56
+ const updates = checkUpdates((name) => latestVersion(database, name), readInstalledPackages(piHome));
57
+ await saveUpdates(paths.stateDirectory, { checkedAt: new Date().toISOString(), updates });
58
+ },
59
+ },
60
+ {
61
+ name: "catalog-sync",
62
+ intervalMs: envMs(ENV.CATALOG_SECS, CATALOG_INTERVAL_DEFAULT_MS),
63
+ run: async () => {
64
+ const ttlMs = envMs(ENV.CATALOG_SECS, CATALOG_INTERVAL_DEFAULT_MS);
65
+ const dataDirectory = dirname(paths.database);
66
+ if (catalogStatus(dataDirectory, ttlMs).stale) await syncCatalog(reg, dataDirectory);
67
+ },
68
+ },
69
+ {
70
+ name: "index-build",
71
+ intervalMs: envMs(ENV.INDEX_SECS, INDEX_INTERVAL_DEFAULT_MS),
72
+ run: async () => {
73
+ const ttlMs = envMs(ENV.INDEX_SECS, INDEX_INTERVAL_DEFAULT_MS);
74
+ const dataDirectory = dirname(paths.database);
75
+ if (indexStatus(indexPath(dataDirectory), ttlMs).stale) await generateIndex(reg, dataDirectory, indexPath(dataDirectory));
76
+ },
77
+ },
78
+ ];
79
+ const maintenanceTasks = configuredMaintenanceTasks.filter((task) => task.intervalMs > 0);
80
+
81
+ if (options.maintenanceTasks === undefined) {
82
+ for (const task of maintenanceTasks) {
83
+ void Promise.resolve(task.run()).catch((error) =>
84
+ logger.error(`maintenance task failed: ${task.name}`, { error: error instanceof Error ? error.message : String(error) }),
85
+ );
86
+ }
87
+ }
88
+
89
+ return {
90
+ daemonLabel: "Packed",
91
+ handlePath: paths.handle,
92
+ logger,
93
+ maintenanceTasks,
94
+ idleBudgetMs: options.idleBudgetMs ?? envMs(ENV.IDLE_SECS, IDLE_BUDGET_DEFAULT_MS),
95
+ idleTickMs: WATCHDOG_TICK_MS,
96
+ buildApp: () => createApp({ reg, inst, token, stateDir: paths.stateDirectory, dataDir: dirname(paths.database), piHome }),
97
+ onShutdown: () => database.close(),
98
+ };
99
+ }
100
+
101
+ export function startPackedDaemon(options: StartPackedDaemonOptions = {}): Promise<RunningDaemon> {
102
+ return startDaemon(daemonOptions(options));
103
+ }
104
+
105
+ export function serveMain(): void {
106
+ runDaemonProcess({
107
+ ...daemonOptions({}),
108
+ onListen: ({ host, port }) => logger.info("listening", { host, port }),
109
+ });
110
+ }