@prohost/cli 0.16.5 → 0.16.6
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 +9 -0
- package/README.md +3 -3
- package/dist/host/command.d.ts +3 -1
- package/dist/host/command.js +91 -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,15 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.6
|
|
10
|
+
|
|
11
|
+
- **A hand-off note that can't be written undoes the dedicated install.** The note is part of the
|
|
12
|
+
takeover: without it the computer's key stayed in both homes, and a plain `host unpair` in the
|
|
13
|
+
original one would remove the computer under the running service. Now the service is
|
|
14
|
+
uninstalled and the key the install copied is removed (on Windows its administrators' copy too),
|
|
15
|
+
and the install fails with what to fix. A service home that already held the key before keeps
|
|
16
|
+
it, and the install still fails.
|
|
17
|
+
|
|
9
18
|
## 0.16.5
|
|
10
19
|
|
|
11
20
|
- **A dedicated install takes the computer's key over.** `sudo prohost host install --dedicated`
|
package/README.md
CHANGED
|
@@ -247,9 +247,9 @@ holder:** once the service home has the computer, the home it was taken from (`-
|
|
|
247
247
|
or `$SUDO_USER`'s) keeps only a note — `{handed_to: {label, service_user, service_home, at},
|
|
248
248
|
computer_id, name, account, code_host_home?}`, no key — written as that home's owner.
|
|
249
249
|
`host status` there reports `handed_to`; `unpair`, `pair` and a personal `install` refuse and
|
|
250
|
-
name the service.
|
|
251
|
-
|
|
252
|
-
leaves the key where it was. `sudo prohost host uninstall --label <id>` gives the key back to
|
|
250
|
+
name the service. The note is part of the takeover: if it can't be written, the install is
|
|
251
|
+
undone (the service uninstalled, the key it copied removed) and fails, so the key is never left in
|
|
252
|
+
both homes; a failed install leaves the key where it was. `sudo prohost host uninstall --label <id>` gives the key back to
|
|
253
253
|
that home (so `prohost host install` runs it there again); `sudo prohost host unpair --label <id>`
|
|
254
254
|
removes the computer. Both read the service's home from its installed definition (root's file),
|
|
255
255
|
never from the note, which the home's owner can edit. **Updates drain:**
|
package/dist/host/command.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* prohost host pause | resume
|
|
12
12
|
* prohost host migrate [--from <home>]… [--home <dir,…>] [--keep-services] [--dry-run] [--json]
|
|
13
13
|
*/
|
|
14
|
-
import type { InstallIo } from './install.js';
|
|
14
|
+
import type { InstallIo, ServiceSpec } from './install.js';
|
|
15
15
|
import type { LoginInfo } from './logins.js';
|
|
16
16
|
import { runMigrate } from './migrate.js';
|
|
17
17
|
type Flags = Record<string, string | boolean>;
|
|
@@ -63,5 +63,7 @@ export declare function statusJson(env: NodeJS.ProcessEnv, now?: number, options
|
|
|
63
63
|
export declare function signInSpawn(command: string, args: string[], platform?: NodeJS.Platform): [string, string[], {
|
|
64
64
|
windowsVerbatimArguments?: boolean;
|
|
65
65
|
}];
|
|
66
|
+
/** Whether the service manager still has `spec`'s dedicated service (loaded / active / a task). */
|
|
67
|
+
export declare function serviceStillLoaded(spec: ServiceSpec, serviceIo: InstallIo, pidAlive?: (pid: number) => boolean): boolean;
|
|
66
68
|
export declare function runHost(subcommand: string | undefined, flags: Flags, printHelp: () => void, io?: HostIo): Promise<number>;
|
|
67
69
|
export {};
|
package/dist/host/command.js
CHANGED
|
@@ -29,7 +29,7 @@ import { addAccount, isValidLabel, loadAccounts, parseAccountRuntime, accountEnv
|
|
|
29
29
|
import { revokeComputer } from './api.js';
|
|
30
30
|
import { readSharedObject, acquireHostLock, clearComputer, computerOs, controlPath, handoffNotice, handoffOf, hostLockPath, SERVICE_LABEL, readHandoff, readObject, statePath, tryLoadComputer, writePrivateFile, } from './computer.js';
|
|
31
31
|
import { journalPath, readEvents } from './events.js';
|
|
32
|
-
import { LAUNCHD_HOST_LABEL, defaultLabel, defaultServiceRunner, installService, installedDedicatedService, publishedComputer, publishedComputerId, readServiceCredential, rewriteHomeCredential, windowsServiceCredentialPath, windowsHomesDir, windowsTaskOf, serviceFilePath, serviceUserHome, sharedBase, sharedDirFor, uninstallService, } from './install.js';
|
|
32
|
+
import { LAUNCHD_HOST_LABEL, defaultLabel, defaultServiceRunner, installService, installedDedicatedService, systemdUnitOf, publishedComputer, publishedComputerId, readServiceCredential, rewriteHomeCredential, windowsServiceCredentialPath, windowsHomesDir, windowsTaskOf, serviceFilePath, serviceUserHome, sharedBase, sharedDirFor, uninstallService, } from './install.js';
|
|
33
33
|
import { LegacyHarnesses } from './legacy.js';
|
|
34
34
|
import { detectLogins } from './logins.js';
|
|
35
35
|
import { adoptComputer, loadAdopted, runMigrate } from './migrate.js';
|
|
@@ -490,6 +490,45 @@ function serviceCredential(platform, home, user, serviceIo, label) {
|
|
|
490
490
|
return null;
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
+
/** Whether the service manager still has `spec`'s dedicated service (loaded / active / a task). */
|
|
494
|
+
export function serviceStillLoaded(spec, serviceIo, pidAlive = processAlive) {
|
|
495
|
+
const run = serviceIo.run ?? defaultServiceRunner;
|
|
496
|
+
try {
|
|
497
|
+
if (spec.platform === 'darwin')
|
|
498
|
+
return run('launchctl', ['print', `system/${spec.label}`]).status === 0;
|
|
499
|
+
if (spec.platform === 'win32') {
|
|
500
|
+
if (run('schtasks', ['/Query', '/TN', windowsTaskOf(spec)]).status === 0)
|
|
501
|
+
return true;
|
|
502
|
+
// Deleting a task does not stop what it started: the host's own lock names a live process.
|
|
503
|
+
// Only "no such file" means no lock; any other read error is an unknown, so still there (fail closed).
|
|
504
|
+
const lock = (serviceIo.readFile ?? ((f) => {
|
|
505
|
+
try {
|
|
506
|
+
return readFileSync(f, 'utf8');
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
if (e.code === 'ENOENT')
|
|
510
|
+
return undefined;
|
|
511
|
+
throw e;
|
|
512
|
+
}
|
|
513
|
+
}))(path.win32.join(spec.home, 'host', 'service.lock'));
|
|
514
|
+
const pid = lock ? JSON.parse(lock).pid : undefined;
|
|
515
|
+
return typeof pid === 'number' && Number.isInteger(pid) && pid > 0 && pidAlive(pid);
|
|
516
|
+
}
|
|
517
|
+
return run('systemctl', ['is-active', '--quiet', systemdUnitOf(spec)]).status === 0;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function processAlive(pid) {
|
|
524
|
+
try {
|
|
525
|
+
process.kill(pid, 0);
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
return e.code === 'EPERM';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
493
532
|
/** The home a dedicated install took its computer from: `--from-home`, else (under sudo `$PROHOST_HOME` is root's) `$SUDO_USER`'s. */
|
|
494
533
|
function handedFromHome(flags, env, platform) {
|
|
495
534
|
return str(flags, 'from-home') ?? (env.SUDO_USER && !env.PROHOST_HOME ? homeOfUser(platform, env.SUDO_USER) : prohostHome(env));
|
|
@@ -756,6 +795,8 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
756
795
|
(env.SUDO_USER && !env.PROHOST_HOME ? homeOfUser(platform, env.SUDO_USER) : ownHome);
|
|
757
796
|
// The key this install takes over, read before it runs: only a copy it really made is retired.
|
|
758
797
|
const handedFrom = dedicated && path.resolve(adoptFrom) !== path.resolve(home) ? tryLoadComputer({ ...env, PROHOST_HOME: adoptFrom }) : null;
|
|
798
|
+
// Whether the service home already held a key before this install (then it copies none).
|
|
799
|
+
const serviceHadKey = handedFrom ? serviceCredential(platform, home, serviceUser, io.serviceIo ?? {}, spec.label) !== null : false;
|
|
759
800
|
const outcome = installService(spec, io.serviceIo, {
|
|
760
801
|
...(tokenFile ? { claudeTokenFile: tokenFile } : {}),
|
|
761
802
|
...(dedicated ? { adoptFrom } : {}),
|
|
@@ -790,8 +831,55 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
790
831
|
});
|
|
791
832
|
const problem = rewriteHomeCredential(platform, adoptFrom, `${JSON.stringify(note, null, 2)}\n`, io.serviceIo);
|
|
792
833
|
if (problem) {
|
|
793
|
-
|
|
794
|
-
|
|
834
|
+
// The note is part of the takeover: without it the key would live in both homes, and an
|
|
835
|
+
// `unpair` here would remove the computer under the service. What this install did is undone.
|
|
836
|
+
err(` ✗ Could not leave the hand-off note in ${adoptFrom} (${problem}).`);
|
|
837
|
+
// What the original home holds now decides the undo (on Windows a rename can land before a
|
|
838
|
+
// later check fails): its key still there → undo; the note there after all → handed over;
|
|
839
|
+
// neither → the service keeps the only key.
|
|
840
|
+
const sourceKey = tryLoadComputer({ ...env, PROHOST_HOME: adoptFrom });
|
|
841
|
+
const sourceNote = readHandoff({ ...env, PROHOST_HOME: adoptFrom });
|
|
842
|
+
const handedAnyway = !sourceKey && sourceNote?.handed_to.label === spec.label && sourceNote?.computer_id === handedFrom.computer_id;
|
|
843
|
+
if (handedAnyway) {
|
|
844
|
+
err(` ! The note is in place after all: the computer's key is the service's alone. Check ${path.join(adoptFrom, 'host', 'computer.json')}.`);
|
|
845
|
+
}
|
|
846
|
+
else if (sourceKey?.computer_id !== handedFrom.computer_id) {
|
|
847
|
+
err(` ✗ ${adoptFrom} no longer holds this computer's key, so ${spec.label} keeps it: check ${path.join(adoptFrom, 'host', 'computer.json')}, never delete ${home}.`);
|
|
848
|
+
return 1;
|
|
849
|
+
}
|
|
850
|
+
else if (!serviceHadKey) {
|
|
851
|
+
const undone = (() => {
|
|
852
|
+
try {
|
|
853
|
+
return uninstallService(spec, io.serviceIo);
|
|
854
|
+
}
|
|
855
|
+
catch (e) {
|
|
856
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
857
|
+
}
|
|
858
|
+
})();
|
|
859
|
+
// Removed and no longer loaded, not just "nothing failed": a Windows task that would not
|
|
860
|
+
// stop removes nothing, and a launchd / systemd service whose stop failed may still run.
|
|
861
|
+
const serviceGone = undone.ok && undone.message.includes('✓ Removed') && !serviceStillLoaded(spec, io.serviceIo ?? {});
|
|
862
|
+
const keyGone = serviceGone && !rewriteHomeCredential(platform, home, undefined, io.serviceIo);
|
|
863
|
+
if (keyGone && platform === 'win32') {
|
|
864
|
+
try {
|
|
865
|
+
(io.serviceIo?.removeFile ?? ((f) => rmSync(f, { force: true })))(windowsServiceCredentialPath(spec));
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
/* reported below with the rest */
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
err(serviceGone && keyGone
|
|
872
|
+
? ` ✗ Undone: ${spec.label} is uninstalled and ${home} holds no key; ${adoptFrom} keeps its key. Fix the problem above, then ` +
|
|
873
|
+
(platform === 'win32' ? `remove the folder ${home} and install again.` : 'install again.')
|
|
874
|
+
: ` ✗ 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.`);
|
|
875
|
+
return 1;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
err(` ✗ ${home} already held this key before this install, so ${adoptFrom} still does too: never run \`prohost host unpair\` from that home. ` +
|
|
879
|
+
`Fix the problem above, then install again.`);
|
|
880
|
+
}
|
|
881
|
+
if (!handedAnyway)
|
|
882
|
+
return 1;
|
|
795
883
|
}
|
|
796
884
|
else {
|
|
797
885
|
out(` The computer's key is the service's alone now: ${adoptFrom} keeps a note of where it went.`);
|
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.6";
|
|
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.6";
|
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.6';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|