node-command 2.0.4 → 2.0.5

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.
Files changed (3) hide show
  1. package/bin/nc.mjs +7 -27
  2. package/lib/setup.mjs +39 -1
  3. package/package.json +4 -18
package/bin/nc.mjs CHANGED
@@ -2875,7 +2875,7 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
2875
2875
  }
2876
2876
  if (unreadable.length > 0) {
2877
2877
  console.log(
2878
- `Elevated removal needed for system install(s): ${unreadable.join(', ')} (not readable as the current user).`
2878
+ `Explicit root maintenance required for system install(s): ${unreadable.join(', ')} (not readable as the current user).`
2879
2879
  );
2880
2880
  }
2881
2881
  if (!yes) {
@@ -2924,33 +2924,13 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
2924
2924
  lock.release();
2925
2925
  }
2926
2926
  }
2927
- // Unreadable system roots remain visible, but never trigger implicit sudo.
2927
+ // Unreadable system roots remain visible, but are never executed: an
2928
+ // ordinary caller cannot verify the tree, so removal is explicit root
2929
+ // maintenance. This deliberately adds no implicit sudo.
2928
2930
  for (const root of unreadable) {
2929
- const script = uninstallerForRoot(root);
2930
- let lock = null;
2931
- try {
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
- }
2931
+ console.error(`error: [TRUSTED_ROOT_MAINTENANCE_REQUIRED] The system installation at ${root} is not readable by the current user; it was not modified.`);
2932
+ console.error('suggested_action: Run `sudo npx --yes node-command@latest uninstall --yes` to remove this trusted root installation explicitly.');
2933
+ failed += 1;
2954
2934
  }
2955
2935
  if (failed > 0) process.exit(1);
2956
2936
  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
@@ -424,7 +462,7 @@ export function findRemovalTargets({ io = {}, ...rest } = {}) {
424
462
  const roots = candidateInstallRoots(pair ? { ...rest, env: { ...env, NC_ROOT: undefined } } : rest);
425
463
  for (const root of pair ? [...new Set([...roots, pair.ncRoot])] : roots) {
426
464
  if (root === pair?.helperRoot && root !== pair.ncRoot) continue;
427
- const status = probeInstallRoot(root, io);
465
+ const status = probeRemovalRoot(root, io);
428
466
  if (status === 'install') install.push(root);
429
467
  else if (status === 'unreadable') unreadable.push(root);
430
468
  }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "node-command",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
6
9
  "bin": {
7
10
  "nc": "bin/nc.mjs",
8
11
  "nodecommand": "bin/nc.mjs",
@@ -12,22 +15,5 @@
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
  }