home-hosted 0.6.1 → 0.6.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/README.md +14 -8
- package/dist/cli.js +126 -31
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,8 +40,9 @@ You run a handful of services at home. The usual choices are extremes — 🧟 `
|
|
|
40
40
|
forget about, 📜 a hand-written systemd unit per service (times six), or 🐳 a whole docker/k8s
|
|
41
41
|
stack??? - too extreme! — plus 😩 monitoring, rebooting and changing the host machine, yuck!
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
puts the whole stack on one page, with deep backup support
|
|
43
|
+
🙂✨ home-hosted enhances on top: a panel/supervisor that starts them, watches them, restarts what
|
|
44
|
+
dies, and puts the whole stack on one page, with deep backup support — whether a server is a plain
|
|
45
|
+
command or a `docker compose` stack.
|
|
45
46
|
|
|
46
47
|
```text
|
|
47
48
|
┌──────────────────────────────────────┐
|
|
@@ -54,8 +55,13 @@ puts the whole stack on one page, with deep backup support.
|
|
|
54
55
|
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
55
56
|
│ gateway │ │ files │ │ bot │
|
|
56
57
|
│ :4000 │ │ :4010 │ │ ... │
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
└────┬────┘ └─────────┘ └─────────┘
|
|
59
|
+
│ compose up -d
|
|
60
|
+
▼
|
|
61
|
+
┌────────────┬────────────┬────────────┐
|
|
62
|
+
│ gateway │ postgres │ redis │ restarts ↻
|
|
63
|
+
└────────────┴────────────┴────────────┘
|
|
64
|
+
health ✓ (the published port is the probe)
|
|
59
65
|
```
|
|
60
66
|
|
|
61
67
|
| | |
|
|
@@ -147,7 +153,7 @@ data/ # per-server data directories, declared through da
|
|
|
147
153
|
|
|
148
154
|
One clone, `pnpm install --frozen-lockfile`, `pnpm run up` — the setup is up on any machine with Node.
|
|
149
155
|
Worked example, with per-server data inside the project:
|
|
150
|
-
**[hhosted-
|
|
156
|
+
**[hhosted-ai-pack](https://github.com/NamesMT/hhosted-ai-pack)**.
|
|
151
157
|
|
|
152
158
|
<sub>Call them as `pnpm run up` — `pnpm up` is pnpm's own update, not your script.</sub>
|
|
153
159
|
|
|
@@ -190,8 +196,8 @@ home-hosted set-token --generate
|
|
|
190
196
|
# hh_9uA2… (printed once; only its hash is kept, mode 0600)
|
|
191
197
|
|
|
192
198
|
curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
|
|
193
|
-
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/
|
|
194
|
-
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=
|
|
199
|
+
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/omniroute/restart
|
|
200
|
+
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=omniroute' # SSE
|
|
195
201
|
home-hosted status --json # machine-readable: pid, url, health, paths
|
|
196
202
|
```
|
|
197
203
|
|
|
@@ -396,7 +402,7 @@ archive and restore. Definitions come back, data lands where *this* machine's co
|
|
|
396
402
|
`autostart` entries come up immediately.
|
|
397
403
|
|
|
398
404
|
It works because an archive carries its own `servers.config.json` and paths are matched by the
|
|
399
|
-
**declaration** (`
|
|
405
|
+
**declaration** (`omniroute:DATA_DIR`), not by an absolute path from the source machine. A restore never
|
|
400
406
|
writes where no config declares.
|
|
401
407
|
|
|
402
408
|
</details>
|
package/dist/cli.js
CHANGED
|
@@ -6821,7 +6821,7 @@ var init_config_watch = __esmMin((() => {
|
|
|
6821
6821
|
if (this.disposed || this.watcher !== null) return;
|
|
6822
6822
|
try {
|
|
6823
6823
|
this.watcher = fs.watch(path.dirname(this.options.file), (_event, filename) => {
|
|
6824
|
-
if (filename !== null && filename !== path.basename(this.options.file)) return;
|
|
6824
|
+
if (filename !== null && path.basename(filename) !== path.basename(this.options.file)) return;
|
|
6825
6825
|
this.schedule();
|
|
6826
6826
|
});
|
|
6827
6827
|
this.watcher.on("error", (error) => {
|
|
@@ -7785,6 +7785,104 @@ function isSafeUiEntry(entry) {
|
|
|
7785
7785
|
if (cleaned.length === 0) return false;
|
|
7786
7786
|
return cleaned.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && !segment.includes(":"));
|
|
7787
7787
|
}
|
|
7788
|
+
function withInstallLock(dataRoot, run) {
|
|
7789
|
+
const key = path.resolve(dataRoot);
|
|
7790
|
+
const next = (installLocks.get(key) ?? Promise.resolve()).then(run, run);
|
|
7791
|
+
installLocks.set(key, next.catch(() => {}));
|
|
7792
|
+
return next;
|
|
7793
|
+
}
|
|
7794
|
+
/** Renames the staged tree into place, keeping the old one until that has certainly worked. */
|
|
7795
|
+
function commitSwap(dataRoot, resting, staged, meta) {
|
|
7796
|
+
return withInstallLock(dataRoot, async () => {
|
|
7797
|
+
try {
|
|
7798
|
+
fs.renameSync(staged, resting);
|
|
7799
|
+
} catch {
|
|
7800
|
+
const previous = `${resting}.previous-${process.pid}-${Date.now()}`;
|
|
7801
|
+
const hadPrevious = fs.existsSync(resting);
|
|
7802
|
+
if (hadPrevious) fs.renameSync(resting, previous);
|
|
7803
|
+
try {
|
|
7804
|
+
fs.renameSync(staged, resting);
|
|
7805
|
+
} catch (error) {
|
|
7806
|
+
try {
|
|
7807
|
+
fs.rmSync(resting, {
|
|
7808
|
+
recursive: true,
|
|
7809
|
+
force: true
|
|
7810
|
+
});
|
|
7811
|
+
if (hadPrevious && fs.existsSync(previous)) fs.renameSync(previous, resting);
|
|
7812
|
+
fs.rmSync(previous, {
|
|
7813
|
+
recursive: true,
|
|
7814
|
+
force: true
|
|
7815
|
+
});
|
|
7816
|
+
} catch {
|
|
7817
|
+
return {
|
|
7818
|
+
ok: false,
|
|
7819
|
+
error: `${describeError$1(error)} — the previous UI is kept at ${previous}`
|
|
7820
|
+
};
|
|
7821
|
+
}
|
|
7822
|
+
return {
|
|
7823
|
+
ok: false,
|
|
7824
|
+
error: describeError$1(error)
|
|
7825
|
+
};
|
|
7826
|
+
}
|
|
7827
|
+
fs.rmSync(previous, {
|
|
7828
|
+
recursive: true,
|
|
7829
|
+
force: true
|
|
7830
|
+
});
|
|
7831
|
+
}
|
|
7832
|
+
sweepBackups(dataRoot, resting);
|
|
7833
|
+
return {
|
|
7834
|
+
ok: true,
|
|
7835
|
+
meta
|
|
7836
|
+
};
|
|
7837
|
+
});
|
|
7838
|
+
}
|
|
7839
|
+
function sweepBackups(dataRoot, resting) {
|
|
7840
|
+
const prefix = `${path.basename(resting)}.previous-`;
|
|
7841
|
+
for (const entry of readDataRoot(dataRoot)) if (entry.startsWith(prefix)) fs.rmSync(path.join(dataRoot, entry), {
|
|
7842
|
+
recursive: true,
|
|
7843
|
+
force: true
|
|
7844
|
+
});
|
|
7845
|
+
}
|
|
7846
|
+
/** The most recently abandoned UI tree, or null when there is not one. */
|
|
7847
|
+
function newestBackup(dataRoot, resting) {
|
|
7848
|
+
const prefix = `${path.basename(resting)}.previous-`;
|
|
7849
|
+
let best = null;
|
|
7850
|
+
let bestStamp = -1;
|
|
7851
|
+
for (const entry of readDataRoot(dataRoot)) {
|
|
7852
|
+
if (!entry.startsWith(prefix)) continue;
|
|
7853
|
+
const full = path.join(dataRoot, entry);
|
|
7854
|
+
if (!fs.existsSync(path.join(full, "index.html"))) continue;
|
|
7855
|
+
const stamp = Number(entry.slice(prefix.length).split("-").pop() ?? "");
|
|
7856
|
+
if (Number.isFinite(stamp) && stamp > bestStamp) {
|
|
7857
|
+
bestStamp = stamp;
|
|
7858
|
+
best = full;
|
|
7859
|
+
}
|
|
7860
|
+
}
|
|
7861
|
+
return best;
|
|
7862
|
+
}
|
|
7863
|
+
/** Dropped staging trees and superseded backups, once the live UI is whole. */
|
|
7864
|
+
function sweepJunk(dataRoot) {
|
|
7865
|
+
let swept = 0;
|
|
7866
|
+
for (const entry of readDataRoot(dataRoot)) {
|
|
7867
|
+
if (!entry.startsWith(".ui-staging-") && !entry.startsWith(`.ui.previous-`)) continue;
|
|
7868
|
+
fs.rmSync(path.join(dataRoot, entry), {
|
|
7869
|
+
recursive: true,
|
|
7870
|
+
force: true
|
|
7871
|
+
});
|
|
7872
|
+
swept += 1;
|
|
7873
|
+
}
|
|
7874
|
+
return swept;
|
|
7875
|
+
}
|
|
7876
|
+
function readDataRoot(dataRoot) {
|
|
7877
|
+
try {
|
|
7878
|
+
return fs.readdirSync(dataRoot);
|
|
7879
|
+
} catch {
|
|
7880
|
+
return [];
|
|
7881
|
+
}
|
|
7882
|
+
}
|
|
7883
|
+
function describeError$1(error) {
|
|
7884
|
+
return error instanceof Error ? error.message : String(error);
|
|
7885
|
+
}
|
|
7788
7886
|
/**
|
|
7789
7887
|
* Where the site actually starts: the archive root, or a single wrapper directory
|
|
7790
7888
|
* (`zip -r ui.zip dist` is a common way to build one).
|
|
@@ -7812,7 +7910,7 @@ function countFiles(root) {
|
|
|
7812
7910
|
})) if (entry.isFile() && entry.name !== META) total += 1;
|
|
7813
7911
|
return Math.max(1, total);
|
|
7814
7912
|
}
|
|
7815
|
-
var META, MAX_ENTRIES, MAX_BYTES, MAX_NAME, manifestSchema, UiService;
|
|
7913
|
+
var META, MAX_ENTRIES, MAX_BYTES, MAX_NAME, manifestSchema, UiService, installLocks;
|
|
7816
7914
|
var init_ui = __esmMin((() => {
|
|
7817
7915
|
init_atomic();
|
|
7818
7916
|
init_archive();
|
|
@@ -7872,7 +7970,7 @@ var init_ui = __esmMin((() => {
|
|
|
7872
7970
|
ok: false,
|
|
7873
7971
|
error: "the upload is not a zip archive"
|
|
7874
7972
|
};
|
|
7875
|
-
const staging = path.join(this.options.dataRoot, `.ui-staging-${Date.now()}`);
|
|
7973
|
+
const staging = path.join(this.options.dataRoot, `.ui-staging-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
7876
7974
|
try {
|
|
7877
7975
|
return await this.stage(archivePath, staging, fallbackName, installedTag);
|
|
7878
7976
|
} catch (error) {
|
|
@@ -7941,43 +8039,38 @@ var init_ui = __esmMin((() => {
|
|
|
7941
8039
|
...manifest?.asset === void 0 ? {} : { asset: manifest.asset },
|
|
7942
8040
|
...manifest?.unix === void 0 ? {} : { unix: manifest.unix }
|
|
7943
8041
|
};
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
8042
|
+
writeFileAtomic(path.join(root, META), `${JSON.stringify(meta, null, 2)}\n`);
|
|
8043
|
+
return commitSwap(this.options.dataRoot, this.directory, root, meta);
|
|
8044
|
+
}
|
|
8045
|
+
/**
|
|
8046
|
+
* Puts a half-finished install back together, and is safe to call on every boot.
|
|
8047
|
+
*
|
|
8048
|
+
* An install that was interrupted between its two renames — a kill, a power cut, a
|
|
8049
|
+
* crash — leaves `.ui` missing with the user's only copy sitting in a `.previous-*`
|
|
8050
|
+
* sibling. Without this the panel would quietly serve the stock UI forever, because
|
|
8051
|
+
* `custom` is false and nothing ever looked at the backup.
|
|
8052
|
+
*/
|
|
8053
|
+
recover() {
|
|
8054
|
+
const resting = this.directory;
|
|
8055
|
+
let restored = null;
|
|
8056
|
+
if (!fs.existsSync(path.join(resting, "index.html"))) {
|
|
8057
|
+
const backup = newestBackup(this.options.dataRoot, resting);
|
|
8058
|
+
if (backup !== null) {
|
|
8059
|
+
fs.rmSync(resting, {
|
|
7957
8060
|
recursive: true,
|
|
7958
8061
|
force: true
|
|
7959
8062
|
});
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
ok: false,
|
|
7963
|
-
error: `${error instanceof Error ? error.message : String(error)} — the previous UI is kept at ${previous}`
|
|
7964
|
-
};
|
|
8063
|
+
fs.renameSync(backup, resting);
|
|
8064
|
+
restored = path.basename(backup);
|
|
7965
8065
|
}
|
|
7966
|
-
return {
|
|
7967
|
-
ok: false,
|
|
7968
|
-
error: error instanceof Error ? error.message : String(error)
|
|
7969
|
-
};
|
|
7970
8066
|
}
|
|
7971
|
-
fs.rmSync(previous, {
|
|
7972
|
-
recursive: true,
|
|
7973
|
-
force: true
|
|
7974
|
-
});
|
|
7975
8067
|
return {
|
|
7976
|
-
|
|
7977
|
-
|
|
8068
|
+
restored,
|
|
8069
|
+
swept: sweepJunk(this.options.dataRoot)
|
|
7978
8070
|
};
|
|
7979
8071
|
}
|
|
7980
8072
|
};
|
|
8073
|
+
installLocks = /* @__PURE__ */ new Map();
|
|
7981
8074
|
}));
|
|
7982
8075
|
//#endregion
|
|
7983
8076
|
//#region src/providers/ui-release.ts
|
|
@@ -8481,6 +8574,8 @@ async function runControlPlane(options) {
|
|
|
8481
8574
|
dataRoot,
|
|
8482
8575
|
stockDir: path.join(packageRoot, "uis", "stock", "dist")
|
|
8483
8576
|
});
|
|
8577
|
+
const uiRecovery = ui.recover();
|
|
8578
|
+
if (uiRecovery.restored !== null) logger.warn(`ui: restored the installed UI from ${uiRecovery.restored} — an earlier update was interrupted`);
|
|
8484
8579
|
const hub = new EventHub();
|
|
8485
8580
|
let app;
|
|
8486
8581
|
const token = newToken();
|