nc-agent 0.1.2 → 0.1.4
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/README.md +16 -0
- package/bin/nc.mjs +137 -25
- package/lib/setup.mjs +8 -6
- package/package.json +9 -4
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# nc-agent
|
|
2
|
+
|
|
3
|
+
Node Command device pairing and agent installer.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Interactive RFC 8628 pairing (8-character code displayed in terminal)
|
|
9
|
+
npx nc-agent
|
|
10
|
+
|
|
11
|
+
# Direct pairing with host ID and enrollment token
|
|
12
|
+
npx nc-agent <host_id> <enrollment_token>
|
|
13
|
+
|
|
14
|
+
# Uninstall the agent from the canonical system directory
|
|
15
|
+
npx nc-agent uninstall
|
|
16
|
+
```
|
package/bin/nc.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
findRemovalTargets,
|
|
16
16
|
getNcRootForMode,
|
|
17
17
|
installerEnvForMode,
|
|
18
|
+
isElevated,
|
|
18
19
|
modeWarningFor,
|
|
19
20
|
runUninstallerScript,
|
|
20
21
|
uninstallFailureSuggestion,
|
|
@@ -324,21 +325,24 @@ export async function downloadToFile(targetUrl, destinationPath, extraHeaders =
|
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
/**
|
|
327
|
-
* Determines the installation root (NC_ROOT) dynamically based on
|
|
328
|
-
*
|
|
328
|
+
* Determines the canonical installation root (NC_ROOT) dynamically based on
|
|
329
|
+
* OS:
|
|
330
|
+
* - Windows: %ProgramData%\NC (the machine-wide root; user-space
|
|
331
|
+
* %LOCALAPPDATA%\nc is never a default — see getNcRootForMode('user'))
|
|
329
332
|
* - macOS: ~/Library/Application Support/nc
|
|
330
333
|
* - Linux (root): /opt/nc
|
|
331
334
|
* - Linux (non-root): ~/.local/share/nc
|
|
335
|
+
* An explicit NC_ROOT always wins. Probes are injectable for deterministic
|
|
336
|
+
* tests.
|
|
337
|
+
* @param {{ platform?: string, env?: NodeJS.ProcessEnv }} [options]
|
|
332
338
|
* @returns {string}
|
|
333
339
|
*/
|
|
334
|
-
export function getNcRoot() {
|
|
335
|
-
if (
|
|
336
|
-
return
|
|
340
|
+
export function getNcRoot({ platform = os.platform(), env = process.env } = {}) {
|
|
341
|
+
if (env.NC_ROOT) {
|
|
342
|
+
return env.NC_ROOT;
|
|
337
343
|
}
|
|
338
|
-
const platform = os.platform();
|
|
339
344
|
if (platform === 'win32') {
|
|
340
|
-
|
|
341
|
-
return path.join(localAppData, 'nc');
|
|
345
|
+
return path.join(env.ProgramData || 'C:\\ProgramData', 'NC');
|
|
342
346
|
}
|
|
343
347
|
if (platform === 'darwin') {
|
|
344
348
|
return path.join(os.homedir(), 'Library', 'Application Support', 'nc');
|
|
@@ -1375,6 +1379,7 @@ function printCliHelp() {
|
|
|
1375
1379
|
'Usage:',
|
|
1376
1380
|
' nodecommand pair this machine via the browser device flow',
|
|
1377
1381
|
' nodecommand setup [host_id token] install the agent (auto system/user mode; no args = browser device flow)',
|
|
1382
|
+
' nodecommand uninstall [--yes] remove the agent from the canonical root (alias: remove)',
|
|
1378
1383
|
' nodecommand remove [--yes] uninstall the agent (asks for confirmation unless --yes)',
|
|
1379
1384
|
' nodecommand --diagnostics preview a redacted diagnostics bundle (dry-run, writes nothing)',
|
|
1380
1385
|
' nodecommand --diagnostics --apply write the diagnostics bundle (.tar.gz) for a support ticket',
|
|
@@ -1406,6 +1411,10 @@ async function main() {
|
|
|
1406
1411
|
await runSetup(subArgs);
|
|
1407
1412
|
return;
|
|
1408
1413
|
}
|
|
1414
|
+
if (subcommand === 'uninstall') {
|
|
1415
|
+
await runUninstall(subArgs);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1409
1418
|
if (subcommand === 'remove') {
|
|
1410
1419
|
await runRemove(subArgs);
|
|
1411
1420
|
return;
|
|
@@ -1525,11 +1534,53 @@ function printSetupSuccess({ hostId, serverUrl, installMode = undefined, resumed
|
|
|
1525
1534
|
console.log('status: agent credentials verified online');
|
|
1526
1535
|
}
|
|
1527
1536
|
|
|
1537
|
+
/**
|
|
1538
|
+
* Guidance printed when Windows UAC elevation for a system-wide install is
|
|
1539
|
+
* unavailable (headless shell) or was declined by the operator.
|
|
1540
|
+
*/
|
|
1541
|
+
const WINDOWS_ELEVATION_INSTALL_DECLINED =
|
|
1542
|
+
'Elevation unavailable or declined. Please run as Administrator in an elevated shell for a system-wide install at C:\\ProgramData\\NC.';
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Guidance printed when Windows UAC elevation for uninstallation is
|
|
1546
|
+
* unavailable (headless shell) or was declined by the operator.
|
|
1547
|
+
*/
|
|
1548
|
+
const WINDOWS_ELEVATION_UNINSTALL_DECLINED =
|
|
1549
|
+
'Elevation unavailable or declined. Please run as Administrator in an elevated shell to uninstall the agent from C:\\ProgramData\\NC.';
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Re-runs the current CLI invocation elevated through the Windows UAC prompt
|
|
1553
|
+
* (`Start-Process -Verb RunAs -Wait`; stdio stays inherited so the elevated
|
|
1554
|
+
* console is visible) and returns the spawnSync result. Never throws: a
|
|
1555
|
+
* missing powershell or a declined prompt resolves to null/non-zero.
|
|
1556
|
+
*/
|
|
1557
|
+
function requestWindowsElevation() {
|
|
1558
|
+
const args = [...process.argv.slice(1)];
|
|
1559
|
+
try {
|
|
1560
|
+
return child_process.spawnSync(
|
|
1561
|
+
'powershell',
|
|
1562
|
+
[
|
|
1563
|
+
'-NoProfile',
|
|
1564
|
+
'-Command',
|
|
1565
|
+
`Start-Process -FilePath "${process.execPath}" -ArgumentList ${args
|
|
1566
|
+
.map((a) => `"${String(a).replace(/"/g, '`"')}"`)
|
|
1567
|
+
.join(',')} -Verb RunAs -Wait`
|
|
1568
|
+
],
|
|
1569
|
+
{ stdio: 'inherit' }
|
|
1570
|
+
);
|
|
1571
|
+
} catch {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1528
1576
|
/**
|
|
1529
1577
|
* `nodecommand setup [host_id enrollment_token]`.
|
|
1530
1578
|
*
|
|
1531
|
-
* Mode is automatic (never prompted): elevated processes install
|
|
1532
|
-
*
|
|
1579
|
+
* Mode is automatic (never prompted): elevated processes install machine-wide.
|
|
1580
|
+
* POSIX: a non-root admin requests sudo; a refusal falls back to a per-user
|
|
1581
|
+
* install with a single-line capability notice. Windows: a standard shell
|
|
1582
|
+
* requests UAC elevation instead and hard-stops (exit 1) when elevation is
|
|
1583
|
+
* unavailable or declined — never a silent %LOCALAPPDATA% degradation.
|
|
1533
1584
|
* Without arguments a fresh enrollment is requested via the browser device
|
|
1534
1585
|
* flow; with `host_id` + `token` (dashboard flow) the supplied enrollment is
|
|
1535
1586
|
* redeemed directly. Enrollment always goes through POST /api/enroll with the
|
|
@@ -1563,10 +1614,21 @@ export async function runSetup(args = []) {
|
|
|
1563
1614
|
}
|
|
1564
1615
|
let installMode = decideInstallMode();
|
|
1565
1616
|
// A non-root admin must not silently fall back to a per-user install: the
|
|
1566
|
-
// install root, service scope and privileged helper all differ.
|
|
1567
|
-
//
|
|
1568
|
-
// install lands machine-wide at the canonical system
|
|
1617
|
+
// install root, service scope and privileged helper all differ. POSIX
|
|
1618
|
+
// requests sudo and re-runs elevated; Windows requests UAC (Start-Process
|
|
1619
|
+
// -Verb RunAs) so the install lands machine-wide at the canonical system
|
|
1620
|
+
// root. A refused or unavailable Windows elevation is a hard stop: the CLI
|
|
1621
|
+
// never silently degrades a system install into %LOCALAPPDATA%.
|
|
1569
1622
|
if (installMode === 'user' && canElevate() && !process.env.NC_ELEVATED) {
|
|
1623
|
+
if (os.platform() === 'win32') {
|
|
1624
|
+
if (process.stdin.isTTY) {
|
|
1625
|
+
console.log('Admin rights required for a system-wide install. Requesting UAC elevation…');
|
|
1626
|
+
const elevated = requestWindowsElevation();
|
|
1627
|
+
if (elevated && elevated.status === 0) process.exit(0);
|
|
1628
|
+
}
|
|
1629
|
+
console.error(WINDOWS_ELEVATION_INSTALL_DECLINED);
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1570
1632
|
console.log('Admin rights required for a system-wide install. Requesting sudo…');
|
|
1571
1633
|
const elevated = child_process.spawnSync(
|
|
1572
1634
|
'sudo',
|
|
@@ -1636,30 +1698,47 @@ export function confirmPrompt(question, openInterface = null) {
|
|
|
1636
1698
|
}
|
|
1637
1699
|
|
|
1638
1700
|
/**
|
|
1639
|
-
* `
|
|
1640
|
-
*
|
|
1641
|
-
* executed (never a downloaded copy) and its exit status is reported.
|
|
1701
|
+
* Shared engine for `remove`/`uninstall`: idempotent (no install found means
|
|
1702
|
+
* success, exit 0); otherwise the uninstaller living inside each installed
|
|
1703
|
+
* tree is executed (never a downloaded copy) and its exit status is reported.
|
|
1704
|
+
* Target discovery is centered on the canonical root (candidateInstallRoots
|
|
1705
|
+
* lists it first) and treats an unreadable system tree as installed, so a
|
|
1706
|
+
* non-root shell routes it to the elevated uninstaller instead of reporting
|
|
1707
|
+
* "nothing to remove".
|
|
1642
1708
|
* @param {string[]} args
|
|
1709
|
+
* @param {{ command: string, emptyMessage: string, elevateOnWindows?: boolean, completionMessage: string }} options
|
|
1643
1710
|
*/
|
|
1644
|
-
|
|
1711
|
+
async function runRemoval(args, { command, emptyMessage, elevateOnWindows = false, completionMessage }) {
|
|
1645
1712
|
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
|
1646
1713
|
printCliHelp();
|
|
1647
1714
|
return;
|
|
1648
1715
|
}
|
|
1649
|
-
assertKnownFlags(args, ['--yes', '-y', '--help', '-h'],
|
|
1716
|
+
assertKnownFlags(args, ['--yes', '-y', '--help', '-h'], command);
|
|
1650
1717
|
const stray = args.filter((a) => !a.startsWith('-') && a !== 'help');
|
|
1651
1718
|
if (stray.length > 0) {
|
|
1652
|
-
console.error(`error: [UNKNOWN_ARGUMENT] \`
|
|
1653
|
-
console.error(
|
|
1719
|
+
console.error(`error: [UNKNOWN_ARGUMENT] \`${command}\` takes no positional arguments (got: ${stray.join(', ')}).`);
|
|
1720
|
+
console.error(`suggested_action: Run \`nodecommand ${command} --yes\` (or plain \`nodecommand ${command}\` to be asked).`);
|
|
1654
1721
|
process.exit(1);
|
|
1655
1722
|
}
|
|
1656
1723
|
const yes = args.includes('--yes') || args.includes('-y');
|
|
1657
1724
|
const { install, unreadable } = findRemovalTargets();
|
|
1658
1725
|
const targets = [...install, ...unreadable];
|
|
1659
1726
|
if (targets.length === 0) {
|
|
1660
|
-
console.log(
|
|
1727
|
+
console.log(emptyMessage);
|
|
1661
1728
|
return;
|
|
1662
1729
|
}
|
|
1730
|
+
// Windows: a machine-wide tree at C:\ProgramData\NC cannot be removed from
|
|
1731
|
+
// a standard shell — request UAC (Start-Process -Verb RunAs) and re-run this
|
|
1732
|
+
// same command elevated. Declined or headless elevation is a hard stop.
|
|
1733
|
+
if (elevateOnWindows && os.platform() === 'win32' && !isElevated()) {
|
|
1734
|
+
if (process.stdin.isTTY) {
|
|
1735
|
+
console.log('Admin rights required to remove the system-wide agent. Requesting UAC elevation…');
|
|
1736
|
+
const elevated = requestWindowsElevation();
|
|
1737
|
+
if (elevated && elevated.status === 0) process.exit(0);
|
|
1738
|
+
}
|
|
1739
|
+
console.error(WINDOWS_ELEVATION_UNINSTALL_DECLINED);
|
|
1740
|
+
process.exit(1);
|
|
1741
|
+
}
|
|
1663
1742
|
console.log(`Found Node Command installation(s): ${targets.join(', ')}`);
|
|
1664
1743
|
if (unreadable.length > 0) {
|
|
1665
1744
|
console.log(
|
|
@@ -1668,11 +1747,11 @@ export async function runRemove(args = []) {
|
|
|
1668
1747
|
}
|
|
1669
1748
|
if (!yes) {
|
|
1670
1749
|
if (!process.stdin.isTTY) {
|
|
1671
|
-
console.error(
|
|
1672
|
-
console.error(
|
|
1750
|
+
console.error(`error: [CONFIRMATION_REQUIRED] ${command} needs confirmation in non-interactive shells.`);
|
|
1751
|
+
console.error(`suggested_action: Re-run as \`nodecommand ${command} --yes\`.`);
|
|
1673
1752
|
process.exit(1);
|
|
1674
1753
|
}
|
|
1675
|
-
const confirmed = await confirmPrompt(
|
|
1754
|
+
const confirmed = await confirmPrompt(`${command === 'uninstall' ? 'Uninstall' : 'Remove'} ${targets.length === 1 ? 'this installation' : 'these installations'}? [y/N] `);
|
|
1676
1755
|
if (!confirmed) {
|
|
1677
1756
|
console.log('Aborted (nothing removed).');
|
|
1678
1757
|
return;
|
|
@@ -1713,7 +1792,40 @@ export async function runRemove(args = []) {
|
|
|
1713
1792
|
}
|
|
1714
1793
|
}
|
|
1715
1794
|
if (failed > 0) process.exit(1);
|
|
1716
|
-
console.log(
|
|
1795
|
+
console.log(completionMessage);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* `nodecommand remove [--yes]` — legacy name for the canonical uninstall.
|
|
1800
|
+
* @param {string[]} args
|
|
1801
|
+
*/
|
|
1802
|
+
export async function runRemove(args = []) {
|
|
1803
|
+
return runRemoval(args, {
|
|
1804
|
+
command: 'remove',
|
|
1805
|
+
emptyMessage: 'No Node Command installation found (nothing to remove).',
|
|
1806
|
+
completionMessage: 'Remove complete.'
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* `nodecommand uninstall [--yes]` (alias: `remove`). Resolves the canonical
|
|
1812
|
+
* install root (`getNcRoot()`), verifies an agent tree actually lives there
|
|
1813
|
+
* (readable markers, or an unreadable system tree that requires elevation),
|
|
1814
|
+
* and invokes the in-tree platform uninstaller:
|
|
1815
|
+
* - Windows: `powershell -NoProfile -ExecutionPolicy Bypass -File <root>\uninstall.ps1`
|
|
1816
|
+
* after requesting UAC (`Start-Process -Verb RunAs`) when not elevated
|
|
1817
|
+
* - POSIX: `sudo <root>/uninstall.sh`, run directly when already root
|
|
1818
|
+
* When no agent is installed the command reports the canonical root and
|
|
1819
|
+
* exits cleanly.
|
|
1820
|
+
* @param {string[]} args
|
|
1821
|
+
*/
|
|
1822
|
+
export async function runUninstall(args = []) {
|
|
1823
|
+
return runRemoval(args, {
|
|
1824
|
+
command: 'uninstall',
|
|
1825
|
+
emptyMessage: `No active NC agent installation found at ${getNcRoot()}.`,
|
|
1826
|
+
elevateOnWindows: true,
|
|
1827
|
+
completionMessage: 'Uninstall complete.'
|
|
1828
|
+
});
|
|
1717
1829
|
}
|
|
1718
1830
|
|
|
1719
1831
|
/**
|
package/lib/setup.mjs
CHANGED
|
@@ -74,17 +74,19 @@ export function decideInstallMode(options = {}) {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* True when the current user CAN elevate to a machine-wide install even when
|
|
77
|
-
* the process is not root yet:
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* the process is not root yet: an already-elevated process, a Windows
|
|
78
|
+
* standard shell (UAC `Start-Process -Verb RunAs` is always available), or
|
|
79
|
+
* POSIX admin groups (sudo/admin/wheel). Used so a non-root admin is asked
|
|
80
|
+
* for elevation instead of silently degrading to a per-user install
|
|
81
|
+
* (different root, service scope and no privileged helper).
|
|
81
82
|
* @param {{ platform?: string, getuid?: (() => number)|null, isAdmin?: boolean|null, runAdminCheck?: (() => boolean)|null }} [options]
|
|
82
83
|
*/
|
|
83
84
|
export function canElevate(options = {}) {
|
|
84
85
|
if (isElevated(options)) return true;
|
|
85
86
|
const platform = options.platform || os.platform();
|
|
86
|
-
// Windows:
|
|
87
|
-
|
|
87
|
+
// Windows: a non-elevated process can still relaunch itself through the
|
|
88
|
+
// UAC prompt, so elevation is available even when not administrator.
|
|
89
|
+
if (platform === 'win32') return true;
|
|
88
90
|
try {
|
|
89
91
|
const r = child_process.spawnSync('id', ['-nG'], { encoding: 'utf8' });
|
|
90
92
|
const groups = r && typeof r.stdout === 'string' ? r.stdout : '';
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nc-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Node Command device pairing and agent installer. Run npx nc-agent to pair this machine with your Node Command workspace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"nc-agent": "./bin/nc.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"README.md"
|
|
14
|
+
]
|
|
10
15
|
}
|