@prohost/cli 0.16.7 → 0.16.8
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/CHANGELOG.md +21 -0
- package/dist/host/command.js +99 -27
- package/dist/host/install.js +11 -3
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.8
|
|
10
|
+
|
|
11
|
+
- **A dedicated install never takes over a service home that holds another computer.** When the
|
|
12
|
+
service home already held a different computer's key, the install restarted that computer under
|
|
13
|
+
the label, left the invoking home's key where it was, and reported a takeover. It is now refused
|
|
14
|
+
before anything is installed, naming the computer it holds.
|
|
15
|
+
- **A copy that isn't there afterwards is no takeover.** When the key the install copied into the
|
|
16
|
+
service home can't be read back as that computer's, the service is uninstalled and the command
|
|
17
|
+
fails, naming the file to check (no credential is deleted: which file this install wrote can't be
|
|
18
|
+
told from one it didn't). The invoking home keeps its key.
|
|
19
|
+
- **`uninstall` says when a service is still stopping after it gives the key back.** The key goes
|
|
20
|
+
back at once, as before (the service's definition is already gone, so a later run could never
|
|
21
|
+
find it again). A stop that failed or is still draining (`launchctl bootout`, `systemctl disable
|
|
22
|
+
--now`) is then waited for up to 30 s, and a service still stopping fails the command with what
|
|
23
|
+
to wait for before running the computer from that home. A service copy that can't be deleted
|
|
24
|
+
after the give-back fails the command instead of leaving the key in two homes behind a success.
|
|
25
|
+
- **Windows: a scheduled task that couldn't be deleted is a failure.** `uninstall` reported it as
|
|
26
|
+
"No host service was installed" and removed its files, so the key went back while the task could
|
|
27
|
+
start again on it. It now fails, keeps the task's files (a later uninstall still finds it), and
|
|
28
|
+
gives nothing back.
|
|
29
|
+
|
|
9
30
|
## 0.16.7
|
|
10
31
|
|
|
11
32
|
- **An undo whose cleanup fails says so.** When a dedicated install undoes itself (its hand-off
|
package/dist/host/command.js
CHANGED
|
@@ -520,6 +520,21 @@ export function serviceStillLoaded(spec, serviceIo, pidAlive = processAlive) {
|
|
|
520
520
|
return true;
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
|
+
/** How long a give-back waits for an uninstalled service to finish stopping. */
|
|
524
|
+
const GIVE_BACK_WAIT_MS = 30_000;
|
|
525
|
+
/** Whether `spec`'s service is gone, waiting up to {@link GIVE_BACK_WAIT_MS} for it to stop. */
|
|
526
|
+
function waitUntilGone(spec, serviceIo) {
|
|
527
|
+
const now = serviceIo.now ?? Date.now;
|
|
528
|
+
const sleep = serviceIo.sleep ?? ((ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms));
|
|
529
|
+
const deadline = now() + GIVE_BACK_WAIT_MS;
|
|
530
|
+
for (;;) {
|
|
531
|
+
if (!serviceStillLoaded(spec, serviceIo))
|
|
532
|
+
return true;
|
|
533
|
+
if (now() >= deadline)
|
|
534
|
+
return false;
|
|
535
|
+
sleep(500);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
523
538
|
function processAlive(pid) {
|
|
524
539
|
try {
|
|
525
540
|
process.kill(pid, 0);
|
|
@@ -766,7 +781,18 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
766
781
|
for (const o of shown)
|
|
767
782
|
(o.ok ? out : err)(o.message);
|
|
768
783
|
let ok = shown.every((o) => o.ok);
|
|
769
|
-
|
|
784
|
+
// The key goes back at once: the service's definition is gone, so a later run could never
|
|
785
|
+
// find it again, and a key left only in its home (an interrupted wait, say) would be stranded.
|
|
786
|
+
// Uninstall ignores a stop that failed (`launchctl bootout`, `systemctl disable --now`), so the
|
|
787
|
+
// service may still be draining its runs on the old copy: that is waited for (bounded) only
|
|
788
|
+
// afterwards, to say so.
|
|
789
|
+
const handedSpec = note && handedTo ? { ...spec, label: note.handed_to.label, home: handedTo.home } : undefined;
|
|
790
|
+
// Its definition removed (its task deleted), whatever else failed after: on Windows a copy of
|
|
791
|
+
// the key that couldn't be deleted makes uninstall report not ok, yet the task is gone, and
|
|
792
|
+
// no later run could find the key again. Those failures still fail this command (`ok`).
|
|
793
|
+
const definitionGone = (o) => o.ok || o.message.includes('✓ Removed');
|
|
794
|
+
const handedOutcome = note ? outcomes.find((o) => o.label === note.handed_to.label && definitionGone(o)) : undefined;
|
|
795
|
+
if (note && fromHome && handedOutcome) {
|
|
770
796
|
const key = handedKeyLive;
|
|
771
797
|
if (key && key.computer_id === note.computer_id && handedTo) {
|
|
772
798
|
const problem = rewriteHomeCredential(platform, fromHome, `${JSON.stringify(key, null, 2)}\n`, io.serviceIo);
|
|
@@ -777,17 +803,28 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
777
803
|
else {
|
|
778
804
|
// On Windows the (administrators') service home keeps its copy: a later dedicated
|
|
779
805
|
// install finds that home and writes nothing into it, so it must still hold the key.
|
|
780
|
-
|
|
781
|
-
|
|
806
|
+
const cleared = platform === 'win32' || clearHandedHome(platform, handedTo.home, 'its key', io.serviceIo ?? {}, err);
|
|
807
|
+
if (!cleared) {
|
|
808
|
+
// Back here, and still there: the same key in two homes. Not done until that copy goes.
|
|
809
|
+
ok = false;
|
|
810
|
+
err(` ✗ The key is back in ${fromHome} but also still in ${handedTo.home}: delete that copy before using either.`);
|
|
811
|
+
}
|
|
782
812
|
out(` The computer's key is back in ${fromHome}: prohost host install runs it there (prohost host unpair removes it).`);
|
|
783
813
|
if (platform === 'win32')
|
|
784
814
|
out(` To install it as a dedicated service again, first remove the folder ${handedTo.home}.`);
|
|
785
815
|
}
|
|
786
816
|
}
|
|
787
817
|
else {
|
|
788
|
-
clearHandedHome(platform, fromHome, 'the note', io.serviceIo ?? {}, err)
|
|
818
|
+
if (!clearHandedHome(platform, fromHome, 'the note', io.serviceIo ?? {}, err))
|
|
819
|
+
ok = false;
|
|
789
820
|
out(` ${fromHome} no longer points at ${note.handed_to.label}. Its key was not found: add the computer again with prohost host pair, or remove it on its page in ProhostAI.`);
|
|
790
821
|
}
|
|
822
|
+
// Only now (the key is where it belongs): whether the service really stopped, bounded.
|
|
823
|
+
if (handedSpec && !waitUntilGone(handedSpec, io.serviceIo ?? {})) {
|
|
824
|
+
ok = false;
|
|
825
|
+
err(` ! ${note.handed_to.label} was still stopping after ${GIVE_BACK_WAIT_MS / 1000}s: wait until it has exited ` +
|
|
826
|
+
`(${platform === 'darwin' ? `sudo launchctl print system/${note.handed_to.label}` : platform === 'win32' ? 'Task Manager' : `systemctl status ${systemdUnitOf(handedSpec)}`}) before running this computer from ${fromHome}.`);
|
|
827
|
+
}
|
|
791
828
|
}
|
|
792
829
|
return ok ? 0 : 1;
|
|
793
830
|
}
|
|
@@ -815,8 +852,17 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
815
852
|
(env.SUDO_USER && !env.PROHOST_HOME ? homeOfUser(platform, env.SUDO_USER) : ownHome);
|
|
816
853
|
// The key this install takes over, read before it runs: only a copy it really made is retired.
|
|
817
854
|
const handedFrom = dedicated && path.resolve(adoptFrom) !== path.resolve(home) ? tryLoadComputer({ ...env, PROHOST_HOME: adoptFrom }) : null;
|
|
818
|
-
//
|
|
819
|
-
const
|
|
855
|
+
// The key the service home already holds, read before this install (then it copies none in).
|
|
856
|
+
const serviceBefore = handedFrom ? serviceCredential(platform, home, serviceUser, io.serviceIo ?? {}, spec.label) : null;
|
|
857
|
+
// Another computer's: installing would restart that computer under this label and leave this
|
|
858
|
+
// one's key where it is, reported as a takeover. Refused before anything is installed.
|
|
859
|
+
if (handedFrom && serviceBefore && serviceBefore.computer_id !== handedFrom.computer_id) {
|
|
860
|
+
err(`✗ ${home} already holds the key of another computer ("${serviceBefore.name}", ${serviceBefore.computer_id}), not ` +
|
|
861
|
+
`${adoptFrom}'s ("${handedFrom.name}"): nothing was installed. Remove that computer first ` +
|
|
862
|
+
`(sudo prohost host unpair --label ${spec.label}), then install again.`);
|
|
863
|
+
return 1;
|
|
864
|
+
}
|
|
865
|
+
const serviceHadKey = serviceBefore !== null;
|
|
820
866
|
const outcome = installService(spec, io.serviceIo, {
|
|
821
867
|
...(tokenFile ? { claudeTokenFile: tokenFile } : {}),
|
|
822
868
|
...(dedicated ? { adoptFrom } : {}),
|
|
@@ -828,6 +874,38 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
828
874
|
? ` ! ${home} has no computer yet (none in ${adoptFrom}): pair as yourself, then run this again — or pass --from-home <your PROHOST_HOME>.`
|
|
829
875
|
: ` This home (${home}) has no computer yet: run prohost host pair.`);
|
|
830
876
|
}
|
|
877
|
+
// Undo this install (it copied a key in): the service uninstalled, every copy of the key it made
|
|
878
|
+
// removed, and the original home keeps its key. Returns the exit code.
|
|
879
|
+
const undoInstall = (options = { removeKeys: true }) => {
|
|
880
|
+
const undone = (() => {
|
|
881
|
+
try {
|
|
882
|
+
return uninstallService(spec, io.serviceIo);
|
|
883
|
+
}
|
|
884
|
+
catch (e) {
|
|
885
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
886
|
+
}
|
|
887
|
+
})();
|
|
888
|
+
// Removed and no longer loaded, not just "nothing failed": a Windows task that would not
|
|
889
|
+
// stop removes nothing, and a launchd / systemd service whose stop failed may still run.
|
|
890
|
+
// (Not `undone.ok`: on Windows uninstall reports a key copy it couldn't remove as not ok
|
|
891
|
+
// although the task is gone — that copy is named by removeCopiedKeys below.)
|
|
892
|
+
const serviceGone = undone.message.includes('✓ Removed') && !serviceStillLoaded(spec, io.serviceIo ?? {});
|
|
893
|
+
if (!options.removeKeys) {
|
|
894
|
+
// Which file this install copied can't be told from one it didn't: no credential is deleted.
|
|
895
|
+
err(serviceGone
|
|
896
|
+
? ` ✗ ${spec.label} is uninstalled. Check ${path.join(home, 'host', 'computer.json')} before installing again; ${adoptFrom} keeps its key.`
|
|
897
|
+
: ` ✗ Could not uninstall ${spec.label}: remove it (sudo prohost host uninstall --label ${spec.label}), then check ${path.join(home, 'host', 'computer.json')}.`);
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
const leftover = serviceGone ? removeCopiedKeys(spec, io.serviceIo ?? {}) : [];
|
|
901
|
+
err(serviceGone && leftover.length === 0
|
|
902
|
+
? ` ✗ Undone: ${spec.label} is uninstalled and ${home} holds no key; ${adoptFrom} keeps its key. Fix the problem above, then ` +
|
|
903
|
+
(platform === 'win32' ? `remove the folder ${home} and install again.` : 'install again.')
|
|
904
|
+
: serviceGone
|
|
905
|
+
? ` ✗ Partly undone: ${spec.label} is uninstalled, but its key is still in ${leftover.join(' and ')}: delete it yourself — never run \`prohost host unpair\` from ${adoptFrom} meanwhile.`
|
|
906
|
+
: ` ✗ Could not fully undo it: remove ${spec.label} (sudo prohost host uninstall --label ${spec.label}) and ${path.join(home, 'host', 'computer.json')} yourself — never run \`prohost host unpair\` from ${adoptFrom} meanwhile.`);
|
|
907
|
+
return 1;
|
|
908
|
+
};
|
|
831
909
|
if (outcome.ok && dedicated && handedFrom) {
|
|
832
910
|
// One key, one holder: the home it came from keeps a note of where the computer went, never
|
|
833
911
|
// the key — so an `unpair`, `pair` or personal `install` there can't revoke or run it twice.
|
|
@@ -868,27 +946,7 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
868
946
|
return 1;
|
|
869
947
|
}
|
|
870
948
|
else if (!serviceHadKey) {
|
|
871
|
-
|
|
872
|
-
try {
|
|
873
|
-
return uninstallService(spec, io.serviceIo);
|
|
874
|
-
}
|
|
875
|
-
catch (e) {
|
|
876
|
-
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
877
|
-
}
|
|
878
|
-
})();
|
|
879
|
-
// Removed and no longer loaded, not just "nothing failed": a Windows task that would not
|
|
880
|
-
// stop removes nothing, and a launchd / systemd service whose stop failed may still run.
|
|
881
|
-
// (Not `undone.ok`: on Windows uninstall reports a key copy it couldn't remove as not ok
|
|
882
|
-
// although the task is gone — that copy is named by removeCopiedKeys below.)
|
|
883
|
-
const serviceGone = undone.message.includes('✓ Removed') && !serviceStillLoaded(spec, io.serviceIo ?? {});
|
|
884
|
-
const leftover = serviceGone ? removeCopiedKeys(spec, io.serviceIo ?? {}) : [];
|
|
885
|
-
err(serviceGone && leftover.length === 0
|
|
886
|
-
? ` ✗ Undone: ${spec.label} is uninstalled and ${home} holds no key; ${adoptFrom} keeps its key. Fix the problem above, then ` +
|
|
887
|
-
(platform === 'win32' ? `remove the folder ${home} and install again.` : 'install again.')
|
|
888
|
-
: serviceGone
|
|
889
|
-
? ` ✗ Partly undone: ${spec.label} is uninstalled, but its key is still in ${leftover.join(' and ')}: delete it yourself — never run \`prohost host unpair\` from ${adoptFrom} meanwhile.`
|
|
890
|
-
: ` ✗ Could not fully undo it: remove ${spec.label} (sudo prohost host uninstall --label ${spec.label}) and ${path.join(home, 'host', 'computer.json')} yourself — never run \`prohost host unpair\` from ${adoptFrom} meanwhile.`);
|
|
891
|
-
return 1;
|
|
949
|
+
return undoInstall();
|
|
892
950
|
}
|
|
893
951
|
else {
|
|
894
952
|
err(` ✗ ${home} already held this key before this install, so ${adoptFrom} still does too: never run \`prohost host unpair\` from that home. ` +
|
|
@@ -901,6 +959,20 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
901
959
|
out(` The computer's key is the service's alone now: ${adoptFrom} keeps a note of where it went.`);
|
|
902
960
|
}
|
|
903
961
|
}
|
|
962
|
+
else if (!serviceHadKey) {
|
|
963
|
+
// The copy this install made isn't there afterwards (or can't be read, or is another
|
|
964
|
+
// computer's): no takeover happened, and the service must not run on whatever it holds.
|
|
965
|
+
// Uninstalled; its files are left for a person to judge.
|
|
966
|
+
err(` ✗ ${spec.label} was installed, but ${home} does not hold ${adoptFrom}'s key afterwards ` +
|
|
967
|
+
(serviceHas ? `(it holds ${serviceHas.computer_id}).` : "(it can't be read)."));
|
|
968
|
+
return undoInstall({ removeKeys: false });
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
// It held this computer's key before the install and can't be read now: never reported done.
|
|
972
|
+
err(` ✗ ${home} held this computer's key before this install, but it can't be read now: check ` +
|
|
973
|
+
`${path.join(home, 'host', 'computer.json')}, and never run \`prohost host unpair\` from ${adoptFrom} meanwhile.`);
|
|
974
|
+
return 1;
|
|
975
|
+
}
|
|
904
976
|
}
|
|
905
977
|
if (outcome.ok && dedicated) {
|
|
906
978
|
out(` Its status, for anyone on this computer: ${sharedDirFor(spec)}`);
|
package/dist/host/install.js
CHANGED
|
@@ -1048,9 +1048,17 @@ export function uninstallService(spec, io = {}) {
|
|
|
1048
1048
|
const out = run('schtasks', ['/Delete', '/TN', task, '/F']);
|
|
1049
1049
|
if (out.status === 0)
|
|
1050
1050
|
removed.push(task);
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1051
|
+
// A delete that failed while the task is still there is a failure, never "nothing installed":
|
|
1052
|
+
// the task can start again on its key. Its files stay, so a later uninstall still finds it.
|
|
1053
|
+
const stillThere = out.status !== 0 && run('schtasks', ['/Query', '/TN', task]).status === 0;
|
|
1054
|
+
if (stillThere) {
|
|
1055
|
+
notes.push(`could not delete the task ${task} (${(out.stderr || out.stdout || `exit ${out.status}`).trim()}): it is still installed`);
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
for (const file of [serviceFilePath(spec), windowsWrapperPath(spec)]) {
|
|
1059
|
+
if (exists(file))
|
|
1060
|
+
remove(file);
|
|
1061
|
+
}
|
|
1054
1062
|
}
|
|
1055
1063
|
// The administrators' copy of its key goes with the service: a reinstall never finds an old one.
|
|
1056
1064
|
const copy = windowsServiceCredentialPath(spec);
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.16.
|
|
10
|
+
export declare const CLI_VERSION = "0.16.8";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.16.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.16.8";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.16.
|
|
10
|
+
export const CLI_VERSION = '0.16.8';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|