dsh-home-hosted 0.4.1 → 0.4.3
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/docs/DESIGN.md +25 -5
- package/lib/index.js +126 -19
- package/lib/index.js.map +3 -3
- package/lib/types/home-hosted/dsh-entry.d.ts +45 -2
- package/lib/types/home-hosted/entries.d.ts +7 -0
- package/lib/types/service.d.ts +16 -0
- package/package.json +1 -1
package/docs/DESIGN.md
CHANGED
|
@@ -62,10 +62,27 @@ merely offers `lib/bin.js` (a clone), which beats a `dsh` on PATH. A clone has
|
|
|
62
62
|
no manifest, so it is found by that build-file name — a guess that must never
|
|
63
63
|
outrank a real install.
|
|
64
64
|
|
|
65
|
-
The row's stored command is repaired
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
The row's stored command is repaired while the plugin runs (`launcherRepair`),
|
|
66
|
+
including a row that names a bare `dsh`: PATH is exactly what pointed at the copy
|
|
67
|
+
that stopped working. A row this plugin created has its command replaced; an
|
|
68
|
+
adopted row that deliberately runs a different absolute dsh is left alone.
|
|
69
|
+
|
|
70
|
+
### A project that depends on dsh keeps it, by name
|
|
71
|
+
|
|
72
|
+
The launcher is the fallback, not the common case. `home-hosted` resolves a bare
|
|
73
|
+
command itself (`resolveCommand(command, cwd, projectDir)` in its process
|
|
74
|
+
provider) by checking `node_modules/.bin/<command>` under the row's cwd and then
|
|
75
|
+
its own project directory, and only then PATH. So a row whose project installed
|
|
76
|
+
dsh as a dependency is written as a bare `dsh` with that project as its cwd: it
|
|
77
|
+
runs the copy the project's own lockfile chose, through the shim its own package
|
|
78
|
+
manager wrote, with nothing absolute to move when the project or the store does.
|
|
79
|
+
|
|
80
|
+
That is also why this plugin never shells out to a package manager for it:
|
|
81
|
+
`npx dsh` falls back to the registry and to a global when the local bin is
|
|
82
|
+
missing, `pnpm dsh` needs pnpm on the boot PATH, and `nlx --local` would add a
|
|
83
|
+
global tool to a boot path for a lookup the panel already performs. The launcher
|
|
84
|
+
is still what a row gets when a bare name would *not* reach the pinned image —
|
|
85
|
+
a clone run without its own `.bin`, or a global-only machine.
|
|
69
86
|
|
|
70
87
|
## One panel root and one boot artifact per state root
|
|
71
88
|
|
|
@@ -78,7 +95,10 @@ explicit `$HHOSTED_HOME` still wins, and a panel already living at
|
|
|
78
95
|
every server look like it vanished), but otherwise the root is the instance's own
|
|
79
96
|
`<stateDir>/panel`. Only a root with a `servers.config.json` counts, or a
|
|
80
97
|
`run.json` whose pid is alive — a killed panel leaves a runtime file behind, and
|
|
81
|
-
adopting on that would point a fresh install at somebody's dead root.
|
|
98
|
+
adopting on that would point a fresh install at somebody's dead root. Adoption is
|
|
99
|
+
also limited to an instance that already exists: the machine's own harness home,
|
|
100
|
+
or a state dir that already holds settings. A scratch `DSH_HOME` or a test
|
|
101
|
+
project must not reach over and drive the panel some other install owns.
|
|
82
102
|
|
|
83
103
|
The boot artifact name follows the state root too, but keyed on the *machine*
|
|
84
104
|
default: `home-hosted` only for `~/.dsh/dsh-home-hosted`, `home-hosted-<hash>`
|
package/lib/index.js
CHANGED
|
@@ -1989,10 +1989,20 @@ function resolveHomeHostedHome(stateDir, env = process.env, homeDir = os3.homedi
|
|
|
1989
1989
|
const configured = env.HHOSTED_HOME?.trim();
|
|
1990
1990
|
if (configured)
|
|
1991
1991
|
return { home: resolveUnder(configured, homeDir), source: "env" };
|
|
1992
|
+
const state = resolveUnder(stateDir, homeDir);
|
|
1992
1993
|
const legacy = path6.join(homeDir, ".home-hosted");
|
|
1993
|
-
if (hasLivePanel(legacy))
|
|
1994
|
+
if (isEstablishedInstance(state, homeDir) && hasLivePanel(legacy))
|
|
1994
1995
|
return { home: legacy, source: "legacy" };
|
|
1995
|
-
return { home: path6.join(
|
|
1996
|
+
return { home: path6.join(state, "panel"), source: "instance" };
|
|
1997
|
+
}
|
|
1998
|
+
function isEstablishedInstance(stateDir, homeDir) {
|
|
1999
|
+
if (stateDir === path6.join(homeDir, ".dsh", "dsh-home-hosted"))
|
|
2000
|
+
return true;
|
|
2001
|
+
try {
|
|
2002
|
+
return fs7.statSync(path6.join(stateDir, "settings.json")).isFile();
|
|
2003
|
+
} catch {
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
1996
2006
|
}
|
|
1997
2007
|
function hasLivePanel(home) {
|
|
1998
2008
|
try {
|
|
@@ -2851,10 +2861,48 @@ function readdirNames(dir) {
|
|
|
2851
2861
|
return [];
|
|
2852
2862
|
}
|
|
2853
2863
|
}
|
|
2864
|
+
function projectDshEntry(dir, depth = 4) {
|
|
2865
|
+
let current = resolveDir(dir);
|
|
2866
|
+
for (let hop = 0; hop <= depth; hop += 1) {
|
|
2867
|
+
const shim = path10.join(current, "node_modules", ".bin", "dsh");
|
|
2868
|
+
const shimmed = resolveShimmedCli(shim, process7.execPath);
|
|
2869
|
+
if (shimmed?.cliEntry != null && isEntry(shimmed.cliEntry))
|
|
2870
|
+
return shimmed.cliEntry;
|
|
2871
|
+
const found = dshCandidatesUnder(current).map((entry) => ({ entry, version: versionOf(entry) })).sort((a, b) => compareVersions(b.version ?? "0.0.0", a.version ?? "0.0.0"));
|
|
2872
|
+
if (found[0] !== void 0)
|
|
2873
|
+
return found[0].entry;
|
|
2874
|
+
const parent = path10.dirname(current);
|
|
2875
|
+
if (parent === current)
|
|
2876
|
+
break;
|
|
2877
|
+
current = parent;
|
|
2878
|
+
}
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
function resolveDir(dir) {
|
|
2882
|
+
try {
|
|
2883
|
+
return fs12.realpathSync(dir);
|
|
2884
|
+
} catch {
|
|
2885
|
+
return path10.resolve(dir);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2854
2888
|
async function resolveDshLaunch(options = {}) {
|
|
2855
2889
|
const argv1 = options.argv1 === void 0 ? process7.argv[1] : options.argv1;
|
|
2856
2890
|
const dshHome2 = options.dshHome ?? dshHome();
|
|
2857
2891
|
const stateDir = options.stateDir ?? pluginStateDir();
|
|
2892
|
+
const projectDir = options.projectDir === void 0 ? process7.cwd() : options.projectDir;
|
|
2893
|
+
const declared = projectDir === null || projectDir.trim().length === 0 ? null : projectDshEntry(projectDir);
|
|
2894
|
+
if (declared !== null && realPath(declared) !== realPath(launcherFor(stateDir, declared))) {
|
|
2895
|
+
const launcher = writeDshLauncher({
|
|
2896
|
+
stateDir,
|
|
2897
|
+
dshHome: dshHome2,
|
|
2898
|
+
resolvedEntry: declared,
|
|
2899
|
+
pinnedEntry: declared,
|
|
2900
|
+
repin: true,
|
|
2901
|
+
entryExtension: path10.extname(declared),
|
|
2902
|
+
searchRoots: [dshSearchRoot(declared), projectDir].filter((root) => typeof root === "string" && root.length > 0)
|
|
2903
|
+
});
|
|
2904
|
+
return { program: process7.execPath, args: [declared], cliEntry: declared, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2905
|
+
}
|
|
2858
2906
|
const local = typeof argv1 === "string" && argv1.length > 0 ? launchedEntry(argv1) : null;
|
|
2859
2907
|
if (local !== null && realPath(local) !== realPath(dshLauncherPath(stateDir, path10.extname(local)))) {
|
|
2860
2908
|
const launcher = writeDshLauncher({
|
|
@@ -2895,6 +2943,9 @@ async function resolveDshLaunch(options = {}) {
|
|
|
2895
2943
|
const launch = resolveShimmedCli(found, process7.execPath);
|
|
2896
2944
|
return launch === null ? null : { ...launch, launcherPath: null };
|
|
2897
2945
|
}
|
|
2946
|
+
function launcherFor(stateDir, entry) {
|
|
2947
|
+
return dshLauncherPath(stateDir, path10.extname(entry));
|
|
2948
|
+
}
|
|
2898
2949
|
function buildCommand(launch, launcherPath2) {
|
|
2899
2950
|
if (launcherPath2 !== null && launcherPath2.length > 0)
|
|
2900
2951
|
return { command: process7.execPath, entryArgs: [launcherPath2] };
|
|
@@ -2909,13 +2960,24 @@ function entryScript(config) {
|
|
|
2909
2960
|
const rest = config.args ?? [];
|
|
2910
2961
|
return rest.find((arg) => path10.isAbsolute(arg)) ?? program;
|
|
2911
2962
|
}
|
|
2912
|
-
function
|
|
2963
|
+
function localDshCommand(projectDir, entry) {
|
|
2964
|
+
if (typeof projectDir !== "string" || projectDir.trim().length === 0 || entry === null)
|
|
2965
|
+
return null;
|
|
2966
|
+
const shim = resolveShimmedCli(path10.join(projectDir, "node_modules", ".bin", "dsh"))?.cliEntry ?? null;
|
|
2967
|
+
if (shim === null)
|
|
2968
|
+
return null;
|
|
2969
|
+
return realPath(shim) === realPath(entry) ? "dsh" : null;
|
|
2970
|
+
}
|
|
2971
|
+
function pinnedScriptFor(launch, projectDir) {
|
|
2913
2972
|
if (launch === null)
|
|
2914
2973
|
return null;
|
|
2974
|
+
const entry = launch.cliEntry ?? launch.args.find((arg) => path10.isAbsolute(arg)) ?? null;
|
|
2975
|
+
if (localDshCommand(projectDir, entry) === "dsh")
|
|
2976
|
+
return "dsh";
|
|
2915
2977
|
const launcher = launch.launcherPath;
|
|
2916
2978
|
if (launcher !== null && launcher.length > 0)
|
|
2917
2979
|
return launcher;
|
|
2918
|
-
return
|
|
2980
|
+
return entry;
|
|
2919
2981
|
}
|
|
2920
2982
|
function isBareCommand(stored) {
|
|
2921
2983
|
return !/[/\\]/.test(stored) && (stored === "dsh" || /^dsh\.(?:cmd|exe)$/i.test(stored));
|
|
@@ -2930,23 +2992,24 @@ function needsLauncherRepair(config) {
|
|
|
2930
2992
|
return true;
|
|
2931
2993
|
return (config.args ?? []).some((arg) => path10.isAbsolute(arg));
|
|
2932
2994
|
}
|
|
2933
|
-
function launcherRepair(config, launch) {
|
|
2934
|
-
const pinned = pinnedScriptFor(launch);
|
|
2995
|
+
function launcherRepair(config, launch, options = {}) {
|
|
2996
|
+
const pinned = pinnedScriptFor(launch, options.projectDir);
|
|
2935
2997
|
if (pinned === null || launch === null)
|
|
2936
2998
|
return null;
|
|
2999
|
+
const targetBare = pinned === "dsh";
|
|
3000
|
+
const storedBare = isBareCommand(entryScript(config) ?? "");
|
|
2937
3001
|
const stored = entryScript(config);
|
|
2938
3002
|
if (stored === null || stored === pinned)
|
|
2939
3003
|
return null;
|
|
2940
|
-
|
|
2941
|
-
if (!bare) {
|
|
3004
|
+
if (!storedBare && options.ownedCommand !== true) {
|
|
2942
3005
|
const target = resolveShimmedCli(stored, process7.execPath)?.cliEntry ?? stored;
|
|
2943
|
-
if (target !==
|
|
3006
|
+
if (target !== launch.cliEntry && target !== pinned)
|
|
2944
3007
|
return null;
|
|
2945
3008
|
}
|
|
2946
3009
|
const args = config.args ?? [];
|
|
2947
|
-
const
|
|
2948
|
-
const rest =
|
|
2949
|
-
return { command: process7.execPath, args: [pinned, ...rest] };
|
|
3010
|
+
const script = storedBare ? -1 : args.findIndex((arg) => path10.isAbsolute(arg) || arg === pinned);
|
|
3011
|
+
const rest = script === -1 ? args : args.filter((_, index) => index !== script);
|
|
3012
|
+
return targetBare ? { command: "dsh", args: rest, ...typeof options.projectDir === "string" && options.projectDir.length > 0 ? { cwd: options.projectDir } : {} } : { command: process7.execPath, args: [pinned, ...rest] };
|
|
2950
3013
|
}
|
|
2951
3014
|
function buildDshEntry(facts) {
|
|
2952
3015
|
const launch = facts.launch;
|
|
@@ -2964,7 +3027,9 @@ function buildDshEntry(facts) {
|
|
|
2964
3027
|
);
|
|
2965
3028
|
}
|
|
2966
3029
|
const launcherArgs = profile === "web" ? ["web"] : ["--profile", profile];
|
|
2967
|
-
const
|
|
3030
|
+
const projectDir = facts.projectDir === void 0 || facts.projectDir === null ? process7.cwd() : facts.projectDir;
|
|
3031
|
+
const local = launch === null ? null : localDshCommand(projectDir, launch.cliEntry);
|
|
3032
|
+
const generated = launch === null || local === "dsh" ? { command: "dsh", entryArgs: [] } : buildCommand(launch, facts.launcherPath ?? null);
|
|
2968
3033
|
return {
|
|
2969
3034
|
id: facts.id,
|
|
2970
3035
|
label: "DSH web",
|
|
@@ -2974,7 +3039,7 @@ function buildDshEntry(facts) {
|
|
|
2974
3039
|
args: [...generated.entryArgs, ...launcherArgs, ...appArgs],
|
|
2975
3040
|
bind: "local",
|
|
2976
3041
|
port: facts.port,
|
|
2977
|
-
cwd:
|
|
3042
|
+
cwd: projectDir,
|
|
2978
3043
|
env: {},
|
|
2979
3044
|
dataEnvs: { DSH_HOME: facts.dshHome },
|
|
2980
3045
|
onPortConflict: "kill",
|
|
@@ -3205,6 +3270,11 @@ function ownedDrift(live, intent) {
|
|
|
3205
3270
|
drift.push("stop.killPortHolders");
|
|
3206
3271
|
return drift;
|
|
3207
3272
|
}
|
|
3273
|
+
function isCreatedEntry(snapshot) {
|
|
3274
|
+
if (snapshot === void 0)
|
|
3275
|
+
return false;
|
|
3276
|
+
return Object.keys(snapshot).length === 1 && typeof snapshot.id === "string";
|
|
3277
|
+
}
|
|
3208
3278
|
function snapshotOwned(live) {
|
|
3209
3279
|
const killPortHolders = live.stop?.killPortHolders;
|
|
3210
3280
|
return {
|
|
@@ -3588,13 +3658,19 @@ var HomeHostedService = class extends Service {
|
|
|
3588
3658
|
tokenDetail = "";
|
|
3589
3659
|
/** When a missing managed entry was last put back, so a poll cannot become a write loop. */
|
|
3590
3660
|
entryRecoveryAt = 0;
|
|
3661
|
+
/** When a managed row was last re-pointed at the pinned dsh, for the same reason. */
|
|
3662
|
+
commandRepairAt = 0;
|
|
3591
3663
|
/** The last panel inventory, so a model-step assembly never reads the disk. */
|
|
3592
3664
|
instanceCache = null;
|
|
3593
3665
|
snapshotsFile;
|
|
3594
|
-
|
|
3666
|
+
projectDir() {
|
|
3667
|
+
return this.options.projectDir ?? process10.cwd();
|
|
3668
|
+
}
|
|
3669
|
+
async resolveDsh(dshHome2, projectDir = this.projectDir()) {
|
|
3595
3670
|
return await (this.options.resolveDsh ?? ((options) => resolveDshLaunch(options)))({
|
|
3596
3671
|
dshHome: dshHome2,
|
|
3597
|
-
stateDir: this.options.stateDir
|
|
3672
|
+
stateDir: this.options.stateDir,
|
|
3673
|
+
projectDir
|
|
3598
3674
|
});
|
|
3599
3675
|
}
|
|
3600
3676
|
// -------------------------------------------------------------------------
|
|
@@ -4030,7 +4106,8 @@ var HomeHostedService = class extends Service {
|
|
|
4030
4106
|
profile: detectProfile(process10.argv, process10.env),
|
|
4031
4107
|
dshHome: harnessHome,
|
|
4032
4108
|
launch: dsh,
|
|
4033
|
-
launcherPath: dsh?.launcherPath ?? null
|
|
4109
|
+
launcherPath: dsh?.launcherPath ?? null,
|
|
4110
|
+
projectDir: this.projectDir()
|
|
4034
4111
|
});
|
|
4035
4112
|
return { ...generated, ...patch };
|
|
4036
4113
|
}
|
|
@@ -4045,7 +4122,8 @@ var HomeHostedService = class extends Service {
|
|
|
4045
4122
|
return null;
|
|
4046
4123
|
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
4047
4124
|
const dsh = await this.resolveDsh(harnessHome);
|
|
4048
|
-
|
|
4125
|
+
const owned = isCreatedEntry(this.snapshots()[live.id]);
|
|
4126
|
+
return launcherRepair(live, dsh, { ownedCommand: owned, projectDir: this.projectDir() });
|
|
4049
4127
|
}
|
|
4050
4128
|
async writeOwned(intent) {
|
|
4051
4129
|
const live = (await this.liveEntries()).get(intent.id) ?? null;
|
|
@@ -4477,6 +4555,7 @@ var HomeHostedService = class extends Service {
|
|
|
4477
4555
|
*/
|
|
4478
4556
|
async reconcile() {
|
|
4479
4557
|
await this.ensureManagedEntry();
|
|
4558
|
+
await this.repairManagedCommand();
|
|
4480
4559
|
await this.repairBootEntry();
|
|
4481
4560
|
await this.migrateBootEntryName();
|
|
4482
4561
|
}
|
|
@@ -4525,6 +4604,34 @@ var HomeHostedService = class extends Service {
|
|
|
4525
4604
|
} catch {
|
|
4526
4605
|
}
|
|
4527
4606
|
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Re-point a managed `dsh` row that runs the wrong dsh.
|
|
4609
|
+
*
|
|
4610
|
+
* A row created before the launcher existed, or by an older release that took
|
|
4611
|
+
* whatever PATH answered, keeps starting that copy — and no click would fix
|
|
4612
|
+
* it, because the plugin only writes the row when the toggle is applied. So
|
|
4613
|
+
* the drift is repaired where the entry's existence is already checked, under
|
|
4614
|
+
* the same throttle, and only for a row this plugin owns.
|
|
4615
|
+
*/
|
|
4616
|
+
async repairManagedCommand() {
|
|
4617
|
+
const id = this.options.defaultEntryId;
|
|
4618
|
+
if (!this.options.settings.get().manageDsh || !this.options.settings.intentFor(id).autostart)
|
|
4619
|
+
return;
|
|
4620
|
+
const now = Date.now();
|
|
4621
|
+
if (now - this.commandRepairAt < ENTRY_RECOVERY_INTERVAL_MS)
|
|
4622
|
+
return;
|
|
4623
|
+
const live = (await this.liveEntries()).get(id)?.config ?? null;
|
|
4624
|
+
if (live === null || this.snapshots()[id] === void 0 || !needsLauncherRepair(live))
|
|
4625
|
+
return;
|
|
4626
|
+
const repair = await this.dshCommandRepair(live);
|
|
4627
|
+
if (repair === null)
|
|
4628
|
+
return;
|
|
4629
|
+
this.commandRepairAt = now;
|
|
4630
|
+
try {
|
|
4631
|
+
await this.writeOwned({ ...this.options.settings.intentFor(id), autostart: true });
|
|
4632
|
+
} catch {
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4528
4635
|
/** Re-assert the boot entry when the OS still has it but it stopped working. */
|
|
4529
4636
|
async repairBootEntry() {
|
|
4530
4637
|
if (!this.options.settings.get().autostart.enabled)
|
|
@@ -4578,7 +4685,7 @@ var HomeHostedService = class extends Service {
|
|
|
4578
4685
|
boot: await this.bootStatus(),
|
|
4579
4686
|
// A deleted managed entry is put back here as well as at startup: whoever
|
|
4580
4687
|
// deleted it is looking at the page that reports it missing.
|
|
4581
|
-
entries: await this.ensureManagedEntry().then(async () => await this.entriesStatus()),
|
|
4688
|
+
entries: await this.ensureManagedEntry().then(async () => await this.repairManagedCommand()).then(async () => await this.entriesStatus()),
|
|
4582
4689
|
servers,
|
|
4583
4690
|
settings: this.options.settings.get(),
|
|
4584
4691
|
cli,
|