dsh-home-hosted 0.4.1 → 0.4.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/docs/DESIGN.md +21 -4
- package/lib/index.js +114 -17
- 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
|
|
package/lib/index.js
CHANGED
|
@@ -2851,10 +2851,48 @@ function readdirNames(dir) {
|
|
|
2851
2851
|
return [];
|
|
2852
2852
|
}
|
|
2853
2853
|
}
|
|
2854
|
+
function projectDshEntry(dir, depth = 4) {
|
|
2855
|
+
let current = resolveDir(dir);
|
|
2856
|
+
for (let hop = 0; hop <= depth; hop += 1) {
|
|
2857
|
+
const shim = path10.join(current, "node_modules", ".bin", "dsh");
|
|
2858
|
+
const shimmed = resolveShimmedCli(shim, process7.execPath);
|
|
2859
|
+
if (shimmed?.cliEntry != null && isEntry(shimmed.cliEntry))
|
|
2860
|
+
return shimmed.cliEntry;
|
|
2861
|
+
const found = dshCandidatesUnder(current).map((entry) => ({ entry, version: versionOf(entry) })).sort((a, b) => compareVersions(b.version ?? "0.0.0", a.version ?? "0.0.0"));
|
|
2862
|
+
if (found[0] !== void 0)
|
|
2863
|
+
return found[0].entry;
|
|
2864
|
+
const parent = path10.dirname(current);
|
|
2865
|
+
if (parent === current)
|
|
2866
|
+
break;
|
|
2867
|
+
current = parent;
|
|
2868
|
+
}
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
function resolveDir(dir) {
|
|
2872
|
+
try {
|
|
2873
|
+
return fs12.realpathSync(dir);
|
|
2874
|
+
} catch {
|
|
2875
|
+
return path10.resolve(dir);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2854
2878
|
async function resolveDshLaunch(options = {}) {
|
|
2855
2879
|
const argv1 = options.argv1 === void 0 ? process7.argv[1] : options.argv1;
|
|
2856
2880
|
const dshHome2 = options.dshHome ?? dshHome();
|
|
2857
2881
|
const stateDir = options.stateDir ?? pluginStateDir();
|
|
2882
|
+
const projectDir = options.projectDir === void 0 ? process7.cwd() : options.projectDir;
|
|
2883
|
+
const declared = projectDir === null || projectDir.trim().length === 0 ? null : projectDshEntry(projectDir);
|
|
2884
|
+
if (declared !== null && realPath(declared) !== realPath(launcherFor(stateDir, declared))) {
|
|
2885
|
+
const launcher = writeDshLauncher({
|
|
2886
|
+
stateDir,
|
|
2887
|
+
dshHome: dshHome2,
|
|
2888
|
+
resolvedEntry: declared,
|
|
2889
|
+
pinnedEntry: declared,
|
|
2890
|
+
repin: true,
|
|
2891
|
+
entryExtension: path10.extname(declared),
|
|
2892
|
+
searchRoots: [dshSearchRoot(declared), projectDir].filter((root) => typeof root === "string" && root.length > 0)
|
|
2893
|
+
});
|
|
2894
|
+
return { program: process7.execPath, args: [declared], cliEntry: declared, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2895
|
+
}
|
|
2858
2896
|
const local = typeof argv1 === "string" && argv1.length > 0 ? launchedEntry(argv1) : null;
|
|
2859
2897
|
if (local !== null && realPath(local) !== realPath(dshLauncherPath(stateDir, path10.extname(local)))) {
|
|
2860
2898
|
const launcher = writeDshLauncher({
|
|
@@ -2895,6 +2933,9 @@ async function resolveDshLaunch(options = {}) {
|
|
|
2895
2933
|
const launch = resolveShimmedCli(found, process7.execPath);
|
|
2896
2934
|
return launch === null ? null : { ...launch, launcherPath: null };
|
|
2897
2935
|
}
|
|
2936
|
+
function launcherFor(stateDir, entry) {
|
|
2937
|
+
return dshLauncherPath(stateDir, path10.extname(entry));
|
|
2938
|
+
}
|
|
2898
2939
|
function buildCommand(launch, launcherPath2) {
|
|
2899
2940
|
if (launcherPath2 !== null && launcherPath2.length > 0)
|
|
2900
2941
|
return { command: process7.execPath, entryArgs: [launcherPath2] };
|
|
@@ -2909,13 +2950,24 @@ function entryScript(config) {
|
|
|
2909
2950
|
const rest = config.args ?? [];
|
|
2910
2951
|
return rest.find((arg) => path10.isAbsolute(arg)) ?? program;
|
|
2911
2952
|
}
|
|
2912
|
-
function
|
|
2953
|
+
function localDshCommand(projectDir, entry) {
|
|
2954
|
+
if (typeof projectDir !== "string" || projectDir.trim().length === 0 || entry === null)
|
|
2955
|
+
return null;
|
|
2956
|
+
const shim = resolveShimmedCli(path10.join(projectDir, "node_modules", ".bin", "dsh"))?.cliEntry ?? null;
|
|
2957
|
+
if (shim === null)
|
|
2958
|
+
return null;
|
|
2959
|
+
return realPath(shim) === realPath(entry) ? "dsh" : null;
|
|
2960
|
+
}
|
|
2961
|
+
function pinnedScriptFor(launch, projectDir) {
|
|
2913
2962
|
if (launch === null)
|
|
2914
2963
|
return null;
|
|
2964
|
+
const entry = launch.cliEntry ?? launch.args.find((arg) => path10.isAbsolute(arg)) ?? null;
|
|
2965
|
+
if (localDshCommand(projectDir, entry) === "dsh")
|
|
2966
|
+
return "dsh";
|
|
2915
2967
|
const launcher = launch.launcherPath;
|
|
2916
2968
|
if (launcher !== null && launcher.length > 0)
|
|
2917
2969
|
return launcher;
|
|
2918
|
-
return
|
|
2970
|
+
return entry;
|
|
2919
2971
|
}
|
|
2920
2972
|
function isBareCommand(stored) {
|
|
2921
2973
|
return !/[/\\]/.test(stored) && (stored === "dsh" || /^dsh\.(?:cmd|exe)$/i.test(stored));
|
|
@@ -2930,23 +2982,24 @@ function needsLauncherRepair(config) {
|
|
|
2930
2982
|
return true;
|
|
2931
2983
|
return (config.args ?? []).some((arg) => path10.isAbsolute(arg));
|
|
2932
2984
|
}
|
|
2933
|
-
function launcherRepair(config, launch) {
|
|
2934
|
-
const pinned = pinnedScriptFor(launch);
|
|
2985
|
+
function launcherRepair(config, launch, options = {}) {
|
|
2986
|
+
const pinned = pinnedScriptFor(launch, options.projectDir);
|
|
2935
2987
|
if (pinned === null || launch === null)
|
|
2936
2988
|
return null;
|
|
2989
|
+
const targetBare = pinned === "dsh";
|
|
2990
|
+
const storedBare = isBareCommand(entryScript(config) ?? "");
|
|
2937
2991
|
const stored = entryScript(config);
|
|
2938
2992
|
if (stored === null || stored === pinned)
|
|
2939
2993
|
return null;
|
|
2940
|
-
|
|
2941
|
-
if (!bare) {
|
|
2994
|
+
if (!storedBare && options.ownedCommand !== true) {
|
|
2942
2995
|
const target = resolveShimmedCli(stored, process7.execPath)?.cliEntry ?? stored;
|
|
2943
|
-
if (target !==
|
|
2996
|
+
if (target !== launch.cliEntry && target !== pinned)
|
|
2944
2997
|
return null;
|
|
2945
2998
|
}
|
|
2946
2999
|
const args = config.args ?? [];
|
|
2947
|
-
const
|
|
2948
|
-
const rest =
|
|
2949
|
-
return { command: process7.execPath, args: [pinned, ...rest] };
|
|
3000
|
+
const script = storedBare ? -1 : args.findIndex((arg) => path10.isAbsolute(arg) || arg === pinned);
|
|
3001
|
+
const rest = script === -1 ? args : args.filter((_, index) => index !== script);
|
|
3002
|
+
return targetBare ? { command: "dsh", args: rest, ...typeof options.projectDir === "string" && options.projectDir.length > 0 ? { cwd: options.projectDir } : {} } : { command: process7.execPath, args: [pinned, ...rest] };
|
|
2950
3003
|
}
|
|
2951
3004
|
function buildDshEntry(facts) {
|
|
2952
3005
|
const launch = facts.launch;
|
|
@@ -2964,7 +3017,9 @@ function buildDshEntry(facts) {
|
|
|
2964
3017
|
);
|
|
2965
3018
|
}
|
|
2966
3019
|
const launcherArgs = profile === "web" ? ["web"] : ["--profile", profile];
|
|
2967
|
-
const
|
|
3020
|
+
const projectDir = facts.projectDir === void 0 || facts.projectDir === null ? process7.cwd() : facts.projectDir;
|
|
3021
|
+
const local = launch === null ? null : localDshCommand(projectDir, launch.cliEntry);
|
|
3022
|
+
const generated = launch === null || local === "dsh" ? { command: "dsh", entryArgs: [] } : buildCommand(launch, facts.launcherPath ?? null);
|
|
2968
3023
|
return {
|
|
2969
3024
|
id: facts.id,
|
|
2970
3025
|
label: "DSH web",
|
|
@@ -2974,7 +3029,7 @@ function buildDshEntry(facts) {
|
|
|
2974
3029
|
args: [...generated.entryArgs, ...launcherArgs, ...appArgs],
|
|
2975
3030
|
bind: "local",
|
|
2976
3031
|
port: facts.port,
|
|
2977
|
-
cwd:
|
|
3032
|
+
cwd: projectDir,
|
|
2978
3033
|
env: {},
|
|
2979
3034
|
dataEnvs: { DSH_HOME: facts.dshHome },
|
|
2980
3035
|
onPortConflict: "kill",
|
|
@@ -3205,6 +3260,11 @@ function ownedDrift(live, intent) {
|
|
|
3205
3260
|
drift.push("stop.killPortHolders");
|
|
3206
3261
|
return drift;
|
|
3207
3262
|
}
|
|
3263
|
+
function isCreatedEntry(snapshot) {
|
|
3264
|
+
if (snapshot === void 0)
|
|
3265
|
+
return false;
|
|
3266
|
+
return Object.keys(snapshot).length === 1 && typeof snapshot.id === "string";
|
|
3267
|
+
}
|
|
3208
3268
|
function snapshotOwned(live) {
|
|
3209
3269
|
const killPortHolders = live.stop?.killPortHolders;
|
|
3210
3270
|
return {
|
|
@@ -3588,13 +3648,19 @@ var HomeHostedService = class extends Service {
|
|
|
3588
3648
|
tokenDetail = "";
|
|
3589
3649
|
/** When a missing managed entry was last put back, so a poll cannot become a write loop. */
|
|
3590
3650
|
entryRecoveryAt = 0;
|
|
3651
|
+
/** When a managed row was last re-pointed at the pinned dsh, for the same reason. */
|
|
3652
|
+
commandRepairAt = 0;
|
|
3591
3653
|
/** The last panel inventory, so a model-step assembly never reads the disk. */
|
|
3592
3654
|
instanceCache = null;
|
|
3593
3655
|
snapshotsFile;
|
|
3594
|
-
|
|
3656
|
+
projectDir() {
|
|
3657
|
+
return this.options.projectDir ?? process10.cwd();
|
|
3658
|
+
}
|
|
3659
|
+
async resolveDsh(dshHome2, projectDir = this.projectDir()) {
|
|
3595
3660
|
return await (this.options.resolveDsh ?? ((options) => resolveDshLaunch(options)))({
|
|
3596
3661
|
dshHome: dshHome2,
|
|
3597
|
-
stateDir: this.options.stateDir
|
|
3662
|
+
stateDir: this.options.stateDir,
|
|
3663
|
+
projectDir
|
|
3598
3664
|
});
|
|
3599
3665
|
}
|
|
3600
3666
|
// -------------------------------------------------------------------------
|
|
@@ -4030,7 +4096,8 @@ var HomeHostedService = class extends Service {
|
|
|
4030
4096
|
profile: detectProfile(process10.argv, process10.env),
|
|
4031
4097
|
dshHome: harnessHome,
|
|
4032
4098
|
launch: dsh,
|
|
4033
|
-
launcherPath: dsh?.launcherPath ?? null
|
|
4099
|
+
launcherPath: dsh?.launcherPath ?? null,
|
|
4100
|
+
projectDir: this.projectDir()
|
|
4034
4101
|
});
|
|
4035
4102
|
return { ...generated, ...patch };
|
|
4036
4103
|
}
|
|
@@ -4045,7 +4112,8 @@ var HomeHostedService = class extends Service {
|
|
|
4045
4112
|
return null;
|
|
4046
4113
|
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
4047
4114
|
const dsh = await this.resolveDsh(harnessHome);
|
|
4048
|
-
|
|
4115
|
+
const owned = isCreatedEntry(this.snapshots()[live.id]);
|
|
4116
|
+
return launcherRepair(live, dsh, { ownedCommand: owned, projectDir: this.projectDir() });
|
|
4049
4117
|
}
|
|
4050
4118
|
async writeOwned(intent) {
|
|
4051
4119
|
const live = (await this.liveEntries()).get(intent.id) ?? null;
|
|
@@ -4477,6 +4545,7 @@ var HomeHostedService = class extends Service {
|
|
|
4477
4545
|
*/
|
|
4478
4546
|
async reconcile() {
|
|
4479
4547
|
await this.ensureManagedEntry();
|
|
4548
|
+
await this.repairManagedCommand();
|
|
4480
4549
|
await this.repairBootEntry();
|
|
4481
4550
|
await this.migrateBootEntryName();
|
|
4482
4551
|
}
|
|
@@ -4525,6 +4594,34 @@ var HomeHostedService = class extends Service {
|
|
|
4525
4594
|
} catch {
|
|
4526
4595
|
}
|
|
4527
4596
|
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Re-point a managed `dsh` row that runs the wrong dsh.
|
|
4599
|
+
*
|
|
4600
|
+
* A row created before the launcher existed, or by an older release that took
|
|
4601
|
+
* whatever PATH answered, keeps starting that copy — and no click would fix
|
|
4602
|
+
* it, because the plugin only writes the row when the toggle is applied. So
|
|
4603
|
+
* the drift is repaired where the entry's existence is already checked, under
|
|
4604
|
+
* the same throttle, and only for a row this plugin owns.
|
|
4605
|
+
*/
|
|
4606
|
+
async repairManagedCommand() {
|
|
4607
|
+
const id = this.options.defaultEntryId;
|
|
4608
|
+
if (!this.options.settings.get().manageDsh || !this.options.settings.intentFor(id).autostart)
|
|
4609
|
+
return;
|
|
4610
|
+
const now = Date.now();
|
|
4611
|
+
if (now - this.commandRepairAt < ENTRY_RECOVERY_INTERVAL_MS)
|
|
4612
|
+
return;
|
|
4613
|
+
const live = (await this.liveEntries()).get(id)?.config ?? null;
|
|
4614
|
+
if (live === null || this.snapshots()[id] === void 0 || !needsLauncherRepair(live))
|
|
4615
|
+
return;
|
|
4616
|
+
const repair = await this.dshCommandRepair(live);
|
|
4617
|
+
if (repair === null)
|
|
4618
|
+
return;
|
|
4619
|
+
this.commandRepairAt = now;
|
|
4620
|
+
try {
|
|
4621
|
+
await this.writeOwned({ ...this.options.settings.intentFor(id), autostart: true });
|
|
4622
|
+
} catch {
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4528
4625
|
/** Re-assert the boot entry when the OS still has it but it stopped working. */
|
|
4529
4626
|
async repairBootEntry() {
|
|
4530
4627
|
if (!this.options.settings.get().autostart.enabled)
|
|
@@ -4578,7 +4675,7 @@ var HomeHostedService = class extends Service {
|
|
|
4578
4675
|
boot: await this.bootStatus(),
|
|
4579
4676
|
// A deleted managed entry is put back here as well as at startup: whoever
|
|
4580
4677
|
// deleted it is looking at the page that reports it missing.
|
|
4581
|
-
entries: await this.ensureManagedEntry().then(async () => await this.entriesStatus()),
|
|
4678
|
+
entries: await this.ensureManagedEntry().then(async () => await this.repairManagedCommand()).then(async () => await this.entriesStatus()),
|
|
4582
4679
|
servers,
|
|
4583
4680
|
settings: this.options.settings.get(),
|
|
4584
4681
|
cli,
|