dsh-github-app 0.0.0-stage → 0.0.2

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/uninstall.mjs ADDED
@@ -0,0 +1,124 @@
1
+ import { PACKAGE_NAME } from "./constants.mjs";
2
+ import { dshHomeDir, pluginDir } from "./settings.mjs";
3
+ import { join, resolve } from "node:path";
4
+ import { existsSync, readFileSync, rmSync } from "node:fs";
5
+ // src/uninstall.ts
6
+ /**
7
+ * Removes plugin-owned artifacts after the bundle is uninstalled.
8
+ *
9
+ * DSH has no uninstall lifecycle: removing a bundle disposes the plugin fiber
10
+ * exactly like disabling it, an HMR reload, or a host restart. Disposal alone
11
+ * therefore proves nothing. The watcher keeps reading the profile manifest
12
+ * after disposal and deletes plugin data only after the package has stayed
13
+ * absent from both `dependencies` and `dsh.profile.bundles` for a quiet
14
+ * period. Until the package is gone, the manifest still declares it, so a
15
+ * disable, reload, or restart leaves the data alone.
16
+ *
17
+ * The watcher lives in the host process, so this covers removals performed
18
+ * while a DSH host is running. Removing the package with no host running
19
+ * (`dsh plugin remove`) leaves the data directory for manual cleanup.
20
+ */
21
+ /** How often the watcher re-reads the profile manifest. */
22
+ const WATCH_INTERVAL_MS = 1e3;
23
+ /** Consecutive absent readings required before deleting anything. */
24
+ const WATCH_CONFIRMATIONS = 15;
25
+ /** Upper bound on one watcher's lifetime. */
26
+ const WATCH_TIMEOUT_MS = 3e5;
27
+ const activeWatchers = /* @__PURE__ */ new Set();
28
+ function isRecord(value) {
29
+ return value !== null && typeof value === "object" && !Array.isArray(value);
30
+ }
31
+ /**
32
+ * Whether a profile manifest still claims this package.
33
+ *
34
+ * @param profileDir profile root holding `package.json`.
35
+ * @param packageName bundle package name.
36
+ * @returns true when declared, false when absent, null when the manifest
37
+ * cannot be read — callers must treat null as "still installed".
38
+ */
39
+ function profileDeclaresPackage(profileDir, packageName = PACKAGE_NAME) {
40
+ let manifest;
41
+ try {
42
+ manifest = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8"));
43
+ } catch {
44
+ return null;
45
+ }
46
+ if (!isRecord(manifest)) return null;
47
+ if (isRecord(manifest.dependencies) && Object.hasOwn(manifest.dependencies, packageName)) return true;
48
+ if (isRecord(manifest.devDependencies) && Object.hasOwn(manifest.devDependencies, packageName)) return true;
49
+ const dsh = isRecord(manifest.dsh) ? manifest.dsh : {};
50
+ const profile = isRecord(dsh.profile) ? dsh.profile : {};
51
+ return (Array.isArray(profile.bundles) ? profile.bundles : []).includes(packageName);
52
+ }
53
+ /**
54
+ * Delete the plugin data directory. Paths are derived from `DSH_HOME`, never
55
+ * from user input.
56
+ *
57
+ * @param env environment to read `DSH_HOME` from.
58
+ * @returns true when the directory was removed.
59
+ */
60
+ function removePluginArtifacts(env = process.env) {
61
+ const home = resolve(dshHomeDir(env));
62
+ const dir = resolve(pluginDir(env));
63
+ if (dir !== join(home, "dsh-github-app")) return false;
64
+ if (!existsSync(dir)) return false;
65
+ try {
66
+ rmSync(dir, {
67
+ recursive: true,
68
+ force: true
69
+ });
70
+ } catch (error) {
71
+ console.warn("[dsh-github-app] cannot remove plugin data:", error instanceof Error && error.message ? error.message : error);
72
+ return false;
73
+ }
74
+ return true;
75
+ }
76
+ /**
77
+ * Watch the profile for this package's removal and delete plugin artifacts.
78
+ *
79
+ * @param options profile location and timing knobs.
80
+ * @returns a function that stops the watcher before it acts.
81
+ */
82
+ function watchForUninstallCleanup(options) {
83
+ const { profileDir, packageName = PACKAGE_NAME, env = process.env } = options;
84
+ const intervalMs = Math.max(1, options.intervalMs ?? WATCH_INTERVAL_MS);
85
+ const confirmations = Math.max(1, options.confirmations ?? WATCH_CONFIRMATIONS);
86
+ const deadline = Date.now() + Math.max(intervalMs, options.timeoutMs ?? WATCH_TIMEOUT_MS);
87
+ let absentReadings = 0;
88
+ let timer;
89
+ let stopped = false;
90
+ const stop = () => {
91
+ if (stopped) return;
92
+ stopped = true;
93
+ if (timer !== void 0) clearTimeout(timer);
94
+ activeWatchers.delete(stop);
95
+ };
96
+ const tick = () => {
97
+ if (stopped) return;
98
+ if (profileDeclaresPackage(profileDir, packageName) === false) absentReadings += 1;
99
+ else absentReadings = 0;
100
+ if (absentReadings >= confirmations) {
101
+ stop();
102
+ removePluginArtifacts(env);
103
+ return;
104
+ }
105
+ if (Date.now() >= deadline) {
106
+ stop();
107
+ return;
108
+ }
109
+ timer = setTimeout(tick, intervalMs);
110
+ timer.unref();
111
+ };
112
+ activeWatchers.add(stop);
113
+ timer = setTimeout(tick, intervalMs);
114
+ timer.unref();
115
+ return stop;
116
+ }
117
+ /**
118
+ * Cancel watchers started by this module instance, for example when the
119
+ * plugin mounts again after an HMR reload or a reinstall.
120
+ */
121
+ function cancelUninstallCleanup() {
122
+ for (const stop of [...activeWatchers]) stop();
123
+ }
124
+ export { cancelUninstallCleanup, profileDeclaresPackage, removePluginArtifacts, watchForUninstallCleanup };
@@ -0,0 +1 @@
1
+ export declare function errorMessage(error: unknown, fallback: string): string;
@@ -0,0 +1,5 @@
1
+ // src/utils/errorMessage.ts
2
+ function errorMessage(error, fallback) {
3
+ return error instanceof Error && error.message ? error.message : fallback;
4
+ }
5
+ export { errorMessage };