node-command 2.0.4 → 2.0.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/bin/nc.mjs +23 -37
- package/lib/setup.mjs +108 -10
- package/package.json +8 -22
package/bin/nc.mjs
CHANGED
|
@@ -2867,31 +2867,37 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
|
|
|
2867
2867
|
const pair = paired.find(selected => selected.ncRoot === root);
|
|
2868
2868
|
return pair ? pair.helperRoot === root ? `${root} (co-located protected helper)` : `${root} (paired protected helper: ${pair.helperRoot})` : root;
|
|
2869
2869
|
}).join(', ')}`);
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2870
|
+
const pairedRoots = new Set(paired.map(selected => selected.ncRoot));
|
|
2871
|
+
const canRemovePaired = os.platform() === 'linux' && process.getuid?.() === 0 && process.geteuid?.() === 0;
|
|
2872
|
+
let failed = 0;
|
|
2873
|
+
// A non-root caller can never execute the paired protected root. Report it
|
|
2874
|
+
// for explicit root maintenance, but keep going so the caller's own readable
|
|
2875
|
+
// tree is still removed in this invocation. No implicit sudo is attempted.
|
|
2876
|
+
const skippedPaired = canRemovePaired ? [] : [...pairedRoots];
|
|
2877
|
+
for (const root of skippedPaired) {
|
|
2878
|
+
console.error(`error: [TRUSTED_ROOT_MAINTENANCE_REQUIRED] The paired system installation at ${root} requires explicitly invoked real/effective Root maintenance; it was not modified.`);
|
|
2879
|
+
console.error('suggested_action: Run `sudo npx --yes node-command@latest uninstall --yes` to remove this trusted root installation explicitly.');
|
|
2880
|
+
failed += 1;
|
|
2881
|
+
}
|
|
2882
|
+
const removable = install.filter(root => !skippedPaired.includes(root));
|
|
2876
2883
|
if (unreadable.length > 0) {
|
|
2877
2884
|
console.log(
|
|
2878
|
-
`
|
|
2885
|
+
`Explicit root maintenance required for system install(s): ${unreadable.join(', ')} (not readable as the current user).`
|
|
2879
2886
|
);
|
|
2880
2887
|
}
|
|
2881
|
-
if (!yes) {
|
|
2888
|
+
if (removable.length > 0 && !yes) {
|
|
2882
2889
|
if (!process.stdin.isTTY) {
|
|
2883
2890
|
console.error(`error: [CONFIRMATION_REQUIRED] ${command} needs confirmation in non-interactive shells.`);
|
|
2884
2891
|
console.error(`suggested_action: Re-run as \`nodecommand ${command} --yes\`.`);
|
|
2885
2892
|
process.exit(1);
|
|
2886
2893
|
}
|
|
2887
|
-
const confirmed = await confirmPrompt(`${command === 'uninstall' ? 'Uninstall' : 'Remove'} ${
|
|
2894
|
+
const confirmed = await confirmPrompt(`${command === 'uninstall' ? 'Uninstall' : 'Remove'} ${removable.length === 1 ? 'this installation' : 'these installations'}? [y/N] `);
|
|
2888
2895
|
if (!confirmed) {
|
|
2889
2896
|
console.log('Aborted (nothing removed).');
|
|
2890
2897
|
return;
|
|
2891
2898
|
}
|
|
2892
2899
|
}
|
|
2893
|
-
|
|
2894
|
-
for (const root of install) {
|
|
2900
|
+
for (const root of removable) {
|
|
2895
2901
|
const pair = paired.find(selected => selected.ncRoot === root);
|
|
2896
2902
|
const script = pair ? LINUX_PAIRED_UNINSTALL_ENTRY : uninstallerForRoot(root);
|
|
2897
2903
|
if (!pair && !fs.existsSync(script)) {
|
|
@@ -2924,33 +2930,13 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
|
|
|
2924
2930
|
lock.release();
|
|
2925
2931
|
}
|
|
2926
2932
|
}
|
|
2927
|
-
// Unreadable system roots remain visible, but never
|
|
2933
|
+
// Unreadable system roots remain visible, but are never executed: an
|
|
2934
|
+
// ordinary caller cannot verify the tree, so removal is explicit root
|
|
2935
|
+
// maintenance. This deliberately adds no implicit sudo.
|
|
2928
2936
|
for (const root of unreadable) {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
lock = acquireInstallLock(root);
|
|
2933
|
-
} catch (err) {
|
|
2934
|
-
if (err instanceof PairingError && err.code === 'SETUP_IN_PROGRESS') {
|
|
2935
|
-
reportSetupError(err);
|
|
2936
|
-
failed += 1;
|
|
2937
|
-
continue;
|
|
2938
|
-
}
|
|
2939
|
-
// Best effort: an unreadable system root usually cannot host a writable
|
|
2940
|
-
// lock from an ordinary caller. The privileged uninstaller remains the
|
|
2941
|
-
// authority; do not turn a lock-permission failure into a false refusal.
|
|
2942
|
-
}
|
|
2943
|
-
try {
|
|
2944
|
-
runUninstallerScript(script);
|
|
2945
|
-
console.log(`Removed ${root} (exit 0).`);
|
|
2946
|
-
} catch (err) {
|
|
2947
|
-
const rc = typeof err?.status === 'number' ? err.status : 1;
|
|
2948
|
-
console.error(`error: [UNINSTALL_FAILED] uninstaller for ${root} exited with status ${rc}: ${sanitizeForDisplay(err.message, 300)}`);
|
|
2949
|
-
console.error(uninstallFailureSuggestion());
|
|
2950
|
-
failed += 1;
|
|
2951
|
-
} finally {
|
|
2952
|
-
lock?.release();
|
|
2953
|
-
}
|
|
2937
|
+
console.error(`error: [TRUSTED_ROOT_MAINTENANCE_REQUIRED] The system installation at ${root} is not readable by the current user; it was not modified.`);
|
|
2938
|
+
console.error('suggested_action: Run `sudo npx --yes node-command@latest uninstall --yes` to remove this trusted root installation explicitly.');
|
|
2939
|
+
failed += 1;
|
|
2954
2940
|
}
|
|
2955
2941
|
if (failed > 0) process.exit(1);
|
|
2956
2942
|
console.log(completionMessage);
|
package/lib/setup.mjs
CHANGED
|
@@ -391,6 +391,44 @@ export function probeInstallRoot(root, io = {}) {
|
|
|
391
391
|
return 'foreign';
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Removal-strict install-tree probe. Removing a tree is destructive, so a
|
|
396
|
+
* readable `config/nc-client.env` marker is required; an `app/` directory
|
|
397
|
+
* alone (an incomplete or foreign tree) is never a removal target.
|
|
398
|
+
* Distinguishes:
|
|
399
|
+
* - 'install' config marker is readable
|
|
400
|
+
* - 'absent' root directory does not exist
|
|
401
|
+
* - 'foreign' root exists but is not a directory, or the config marker is
|
|
402
|
+
* missing / not a regular file
|
|
403
|
+
* - 'unreadable' root or config marker denies access (EACCES/EPERM)
|
|
404
|
+
* @param {string} root
|
|
405
|
+
* @param {{ statSync?: (p: string) => { isDirectory(): boolean, isFile?(): boolean }, accessSync?: (p: string, mode?: number) => void }} [io]
|
|
406
|
+
* @returns {'install'|'absent'|'foreign'|'unreadable'}
|
|
407
|
+
*/
|
|
408
|
+
export function probeRemovalRoot(root, io = {}) {
|
|
409
|
+
const statSync = io.statSync || fs.statSync;
|
|
410
|
+
const accessSync = io.accessSync || fs.accessSync;
|
|
411
|
+
try {
|
|
412
|
+
const st = statSync(root);
|
|
413
|
+
if (!st.isDirectory()) return 'foreign';
|
|
414
|
+
} catch (err) {
|
|
415
|
+
return err && err.code === 'ENOENT' ? 'absent' : 'unreadable';
|
|
416
|
+
}
|
|
417
|
+
const marker = path.join(root, 'config', 'nc-client.env');
|
|
418
|
+
try {
|
|
419
|
+
const st = statSync(marker);
|
|
420
|
+
if (typeof st.isFile === 'function' && !st.isFile()) return 'foreign';
|
|
421
|
+
} catch (err) {
|
|
422
|
+
return err && (err.code === 'EACCES' || err.code === 'EPERM') ? 'unreadable' : 'foreign';
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
accessSync(marker, fs.constants.R_OK);
|
|
426
|
+
return 'install';
|
|
427
|
+
} catch (err) {
|
|
428
|
+
return err && (err.code === 'EACCES' || err.code === 'EPERM') ? 'unreadable' : 'foreign';
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
394
432
|
/**
|
|
395
433
|
* Canonical machine-wide roots that exist but cannot be inspected by the
|
|
396
434
|
* current unprivileged caller. Setup must escalate before deciding anything
|
|
@@ -407,32 +445,92 @@ export function unreadableSystemInstallations({ platform = os.platform(), env =
|
|
|
407
445
|
return probeInstallRoot(root, io) === 'unreadable' ? [root] : [];
|
|
408
446
|
}
|
|
409
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Every fixed path `linuxPairedInstallation()` must stat/read. A root-owned
|
|
450
|
+
* 0600 metadata file is lstat-able for an ordinary user but unreadable, so
|
|
451
|
+
* both the stat and the read permission are probed.
|
|
452
|
+
*/
|
|
453
|
+
const PAIRED_PROTECTED_PATHS = [
|
|
454
|
+
'/opt/nc',
|
|
455
|
+
'/opt/nc/libexec',
|
|
456
|
+
'/opt/nc/libexec/privileged-helper',
|
|
457
|
+
'/opt/nc/libexec/privileged-helper/helper-bootstrap.json',
|
|
458
|
+
'/opt/nc/libexec/nc-linux-peercred',
|
|
459
|
+
'/opt/nc/app',
|
|
460
|
+
'/opt/nc/app/launcher.mjs',
|
|
461
|
+
'/opt/nc/config',
|
|
462
|
+
'/opt/nc/config/nc-client.env',
|
|
463
|
+
'/var/lib/nodecommand',
|
|
464
|
+
'/var/lib/nodecommand/nc',
|
|
465
|
+
'/var/lib/nodecommand/nc/config',
|
|
466
|
+
'/var/lib/nodecommand/nc/config/nc-client.env',
|
|
467
|
+
'/var/lib/nodecommand/nc/app/launcher.mjs',
|
|
468
|
+
'/var/lib/nc-lifecycle',
|
|
469
|
+
'/var/lib/nc-lifecycle/selected.json',
|
|
470
|
+
'/etc/systemd/system/nc-client.service',
|
|
471
|
+
'/etc/systemd/user/nc-client.service',
|
|
472
|
+
'/etc/systemd/system/nc-privileged-helper.service',
|
|
473
|
+
'/etc/systemd/system/nc-privileged-helper.socket',
|
|
474
|
+
'/etc/tmpfiles.d/nc-privileged-helper.conf'
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
/** True when the fixed protected helper paths deny this account access. */
|
|
478
|
+
function pairedMetadataUnreadable(io) {
|
|
479
|
+
const lstat = io.lstatSync || fs.lstatSync;
|
|
480
|
+
const access = io.accessSync || fs.accessSync;
|
|
481
|
+
for (const file of PAIRED_PROTECTED_PATHS) {
|
|
482
|
+
try {
|
|
483
|
+
lstat(file);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (err?.code === 'EACCES' || err?.code === 'EPERM') return true;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
access(file, fs.constants.R_OK);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
if (err?.code === 'EACCES' || err?.code === 'EPERM') return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
410
497
|
/**
|
|
411
498
|
* Removal targets split by accessibility. `install` roots are removed by the
|
|
412
499
|
* local flow; `unreadable` roots (system installs seen from a non-root shell)
|
|
413
500
|
* report explicit trusted-maintenance requirements instead of being
|
|
414
501
|
* silently reported as "nothing to remove".
|
|
415
502
|
* @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string, io?: object }} [options]
|
|
416
|
-
* @returns {{ install: string[], unreadable: string[], paired
|
|
503
|
+
* @returns {{ install: string[], unreadable: string[], paired: { ncRoot: string, helperRoot: string, clientId: string }[] }}
|
|
417
504
|
*/
|
|
418
505
|
export function findRemovalTargets({ io = {}, ...rest } = {}) {
|
|
419
506
|
const install = [];
|
|
420
507
|
const unreadable = [];
|
|
421
|
-
|
|
508
|
+
let pair = null;
|
|
509
|
+
if ((rest.platform || os.platform()) === 'linux') {
|
|
510
|
+
try {
|
|
511
|
+
pair = linuxPairedInstallation(io);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
// Protected metadata this account cannot stat or read is not a local
|
|
514
|
+
// removal target. Inventory discovery continues so an ordinary user can
|
|
515
|
+
// still remove their own readable tree; nothing here executes a root.
|
|
516
|
+
// Any other validation failure remains a hard stop.
|
|
517
|
+
if (err?.code !== 'TRUSTED_ROOT_MAINTENANCE_REQUIRED' || !pairedMetadataUnreadable(io)) throw err;
|
|
518
|
+
pair = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
422
521
|
const env = rest.env || process.env;
|
|
423
522
|
if (pair && env.NC_ROOT && ![pair.ncRoot, pair.helperRoot].includes(env.NC_ROOT)) throw new Error('TRUSTED_ROOT_MAINTENANCE_REQUIRED: NC_ROOT conflicts with the protected paired installation.');
|
|
424
523
|
const roots = candidateInstallRoots(pair ? { ...rest, env: { ...env, NC_ROOT: undefined } } : rest);
|
|
425
524
|
for (const root of pair ? [...new Set([...roots, pair.ncRoot])] : roots) {
|
|
426
525
|
if (root === pair?.helperRoot && root !== pair.ncRoot) continue;
|
|
427
|
-
const status =
|
|
428
|
-
if (status === 'install') install.push(root);
|
|
429
|
-
else if (status === 'unreadable') unreadable.push(root);
|
|
430
|
-
}
|
|
431
|
-
if (pair) {
|
|
432
|
-
if (install.length !== 1 || install[0] !== pair.ncRoot || unreadable.length) throw new Error('TRUSTED_ROOT_MAINTENANCE_REQUIRED: conflicting paired installation inventory; preserve all roots.');
|
|
433
|
-
return { install, unreadable, paired: [pair] };
|
|
526
|
+
const status = probeRemovalRoot(root, io);
|
|
527
|
+
if (status === 'install' && !install.includes(root)) install.push(root);
|
|
528
|
+
else if (status === 'unreadable' && !unreadable.includes(root)) unreadable.push(root);
|
|
434
529
|
}
|
|
435
|
-
|
|
530
|
+
// The paired relation is only actionable when its root is itself readable.
|
|
531
|
+
// An unverifiable/foreign paired root is reported through `unreadable`.
|
|
532
|
+
const paired = pair && install.includes(pair.ncRoot) ? [pair] : [];
|
|
533
|
+
return { install, unreadable, paired };
|
|
436
534
|
}
|
|
437
535
|
|
|
438
536
|
/**
|
package/package.json
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-command",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
|
-
"nc": "bin/nc.mjs",
|
|
8
|
-
"nodecommand": "bin/nc.mjs",
|
|
9
|
-
"nc-agent": "bin/nc.mjs",
|
|
10
|
-
"node-command": "bin/nc.mjs"
|
|
10
|
+
"nc": "./bin/nc.mjs",
|
|
11
|
+
"nodecommand": "./bin/nc.mjs",
|
|
12
|
+
"nc-agent": "./bin/nc.mjs",
|
|
13
|
+
"node-command": "./bin/nc.mjs"
|
|
11
14
|
},
|
|
12
15
|
"files": [
|
|
13
16
|
"bin/",
|
|
14
17
|
"lib/"
|
|
15
|
-
],
|
|
16
|
-
"description": "Node Command agent CLI — connect this machine to your AI assistant fleet",
|
|
17
|
-
"repository": {
|
|
18
|
-
"url": "git+https://github.com/huseyincig/NodeCommand.git",
|
|
19
|
-
"type": "git"
|
|
20
|
-
},
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=20"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"node-command",
|
|
26
|
-
"nodecommand",
|
|
27
|
-
"mcp",
|
|
28
|
-
"remote",
|
|
29
|
-
"ai-assistant",
|
|
30
|
-
"agent",
|
|
31
|
-
"cli"
|
|
32
18
|
]
|
|
33
19
|
}
|