node-command 1.2.2 → 2.0.0

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/lib/setup.mjs ADDED
@@ -0,0 +1,333 @@
1
+ // nodecommand setup/remove shared logic.
2
+ //
3
+ // Pure, dependency-free helpers behind the `setup` / `remove` subcommands so
4
+ // the mode decision table and install-root detection stay unit-testable with
5
+ // injected platform/privilege probes (no process mocking needed).
6
+
7
+ import child_process from 'node:child_process';
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ /** Single-line notice printed when setup falls back to user mode. */
13
+ export const LIMITED_CAPS_WARNING =
14
+ 'limited: privileged_shell_exec and container tools are off (re-run as administrator for full capabilities)';
15
+
16
+ /**
17
+ * Detects Windows administrator rights via `net session` (exit 0 ⇔ admin).
18
+ * Never throws: any failure means "not admin".
19
+ * @param {(cmd: string, args: string[]) => { status: number|null }} [run]
20
+ */
21
+ export function detectWindowsAdmin(run = null) {
22
+ try {
23
+ const runner =
24
+ run ||
25
+ ((cmd, args) => {
26
+ try {
27
+ const r = child_process.spawnSync(cmd, args, { stdio: 'ignore' });
28
+ return { status: typeof r.status === 'number' ? r.status : 1 };
29
+ } catch {
30
+ return { status: 1 };
31
+ }
32
+ });
33
+ return runner('net', ['session']).status === 0;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * True when the current process can perform a machine-wide (system) install.
41
+ * Privilege probes are injectable for deterministic tests.
42
+ * @param {{ platform?: string, getuid?: (() => number)|null, isAdmin?: boolean|null, runAdminCheck?: (() => boolean)|null }} [options]
43
+ */
44
+ export function isElevated({ platform = os.platform(), getuid = null, isAdmin = null, runAdminCheck = null } = {}) {
45
+ if (platform === 'win32') {
46
+ if (typeof isAdmin === 'boolean') return isAdmin;
47
+ if (typeof runAdminCheck === 'function') {
48
+ try {
49
+ return !!runAdminCheck();
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ return detectWindowsAdmin();
55
+ }
56
+ const getter = typeof getuid === 'function' ? getuid : typeof process.getuid === 'function' ? process.getuid : null;
57
+ if (typeof getter !== 'function') return false;
58
+ try {
59
+ return getter() === 0;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Automatic install-mode decision. The user is never asked: elevated
67
+ * processes install machine-wide, everything else installs per-user.
68
+ * @param {{ platform?: string, getuid?: (() => number)|null, isAdmin?: boolean|null, runAdminCheck?: (() => boolean)|null }} [options]
69
+ * @returns {'system'|'user'}
70
+ */
71
+ export function decideInstallMode(options = {}) {
72
+ return isElevated(options) ? 'system' : 'user';
73
+ }
74
+
75
+ /**
76
+ * True when the current user CAN elevate to a machine-wide install even when
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).
82
+ * @param {{ platform?: string, getuid?: (() => number)|null, isAdmin?: boolean|null, runAdminCheck?: (() => boolean)|null }} [options]
83
+ */
84
+ export function canElevate(options = {}) {
85
+ if (isElevated(options)) return true;
86
+ const platform = options.platform || os.platform();
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;
90
+ try {
91
+ const r = child_process.spawnSync('id', ['-nG'], { encoding: 'utf8' });
92
+ const groups = r && typeof r.stdout === 'string' ? r.stdout : '';
93
+ return /\b(sudo|admin|wheel)\b/.test(groups);
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Warning text for the given mode: a single line for user mode, null for
101
+ * system mode (callers assert presence/absence on this, not on stdout).
102
+ * @param {'system'|'user'} mode
103
+ */
104
+ export function modeWarningFor(mode) {
105
+ return mode === 'user' ? LIMITED_CAPS_WARNING : null;
106
+ }
107
+
108
+ /**
109
+ * Environment additions forwarded to the OS installer scripts so the
110
+ * privileged/unprivileged layout matches the CLI mode decision. All three
111
+ * installers (linux/macos/windows) honor NC_MODE; the *_INSTALL flags force
112
+ * the matching branch even when auto-detection would disagree (e.g. a
113
+ * root-owned NC_ROOT reused by a test harness).
114
+ * @param {'system'|'user'} mode
115
+ */
116
+ export function installerEnvForMode(mode) {
117
+ if (mode === 'user') return { NC_MODE: 'user', NC_USER_INSTALL: '1' };
118
+ return { NC_MODE: 'system', NC_SYSTEM_INSTALL: '1' };
119
+ }
120
+
121
+ /**
122
+ * Explicit NC_ROOT overrides the whole candidate list: when the caller names
123
+ * a target tree, only that tree is considered (deterministic for tests and
124
+ * for "remove exactly what I pointed at"). Otherwise both the machine-wide
125
+ * and the per-user tree are listed so `remove` finds installs made under a
126
+ * different privilege level than the current process.
127
+ * @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string }} [options]
128
+ */
129
+ export function candidateInstallRoots({ platform = os.platform(), env = process.env, homedir = os.homedir() } = {}) {
130
+ if (env.NC_ROOT) return [env.NC_ROOT];
131
+ const roots = [];
132
+ if (platform === 'win32') {
133
+ const programData = env.ProgramData || 'C:\\ProgramData';
134
+ const localAppData = env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local');
135
+ roots.push(path.join(programData, 'NC'), path.join(localAppData, 'nc'));
136
+ } else if (platform === 'darwin') {
137
+ roots.push('/opt/nc', path.join(homedir, 'Library', 'Application Support', 'nc'));
138
+ } else {
139
+ const dataHome = env.XDG_DATA_HOME || path.join(homedir, '.local', 'share');
140
+ roots.push('/opt/nc', path.join(dataHome, 'nc'));
141
+ }
142
+ return [...new Set(roots)];
143
+ }
144
+
145
+ /**
146
+ * True when the directory looks like a real agent tree. Mirrors the safety
147
+ * guard inside the uninstall scripts (config/nc-client.env or app/).
148
+ * @param {string} root
149
+ * @param {(p: string) => boolean} [existsSync]
150
+ */
151
+ export function isInstallRoot(root, existsSync = fs.existsSync) {
152
+ try {
153
+ return (
154
+ existsSync(path.join(root, 'config', 'nc-client.env')) || existsSync(path.join(root, 'app'))
155
+ );
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Tri-state install-tree probe. A root-owned system tree (0750/0700) is
163
+ * unreadable to a non-root caller: a plain boolean would report "nothing to
164
+ * remove" for a machine-wide install that clearly exists, and the sudo path
165
+ * would never be reached. Distinguishes:
166
+ * - 'install' markers readable → normal removal path
167
+ * - 'absent' root directory does not exist
168
+ * - 'foreign' root exists and is traversable but carries no markers
169
+ * - 'unreadable' root exists but markers cannot be stat'ed (EACCES/EPERM)
170
+ * → needs elevation; the uninstaller's own safety guard
171
+ * still refuses non-NC trees when run under sudo.
172
+ * @param {string} root
173
+ * @param {{ statSync?: (p: string) => { isDirectory(): boolean }, accessSync?: (p: string, mode?: number) => void }} [io]
174
+ * @returns {'install'|'absent'|'foreign'|'unreadable'}
175
+ */
176
+ export function probeInstallRoot(root, io = {}) {
177
+ const statSync = io.statSync || fs.statSync;
178
+ const accessSync = io.accessSync || fs.accessSync;
179
+ try {
180
+ const st = statSync(root);
181
+ if (!st.isDirectory()) return 'foreign';
182
+ } catch (err) {
183
+ return err && err.code === 'ENOENT' ? 'absent' : 'unreadable';
184
+ }
185
+ for (const marker of [path.join(root, 'config', 'nc-client.env'), path.join(root, 'app')]) {
186
+ try {
187
+ accessSync(marker);
188
+ return 'install';
189
+ } catch (err) {
190
+ if (err && (err.code === 'EACCES' || err.code === 'EPERM')) return 'unreadable';
191
+ }
192
+ }
193
+ return 'foreign';
194
+ }
195
+
196
+ /**
197
+ * Removal targets split by accessibility. `install` roots are removed by the
198
+ * local flow; `unreadable` roots (system installs seen from a non-root shell)
199
+ * are routed to the elevated runUninstallerScript path instead of being
200
+ * silently reported as "nothing to remove".
201
+ * @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string, io?: object }} [options]
202
+ * @returns {{ install: string[], unreadable: string[] }}
203
+ */
204
+ export function findRemovalTargets({ io = {}, ...rest } = {}) {
205
+ const install = [];
206
+ const unreadable = [];
207
+ for (const root of candidateInstallRoots(rest)) {
208
+ const status = probeInstallRoot(root, io);
209
+ if (status === 'install') install.push(root);
210
+ else if (status === 'unreadable') unreadable.push(root);
211
+ }
212
+ return { install, unreadable };
213
+ }
214
+
215
+ /**
216
+ * Installed roots only, in candidate order.
217
+ * @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string, existsSync?: (p: string) => boolean }} [options]
218
+ */
219
+ export function findInstallations({ existsSync = fs.existsSync, ...rest } = {}) {
220
+ return candidateInstallRoots(rest).filter((r) => isInstallRoot(r, existsSync));
221
+ }
222
+
223
+ /**
224
+ * Mode-aware install root. NC_ROOT still wins when set; otherwise the root
225
+ * mirrors the tree the OS installer builds for that mode — critically on
226
+ * win32, where system installs target %ProgramData%\NC while user installs
227
+ * target %LOCALAPPDATA%\nc (getNcRoot() has no notion of mode and would put
228
+ * an admin setup's credentials in the user tree the agent never reads).
229
+ * @param {'system'|'user'} mode
230
+ * @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string }} [options]
231
+ */
232
+ export function getNcRootForMode(mode, { platform = os.platform(), env = process.env, homedir = os.homedir() } = {}) {
233
+ if (env.NC_ROOT) return env.NC_ROOT;
234
+ if (platform === 'win32') {
235
+ if (mode === 'system') return path.join(env.ProgramData || 'C:\\ProgramData', 'NC');
236
+ const localAppData = env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local');
237
+ return path.join(localAppData, 'nc');
238
+ }
239
+ if (mode === 'system') return '/opt/nc';
240
+ if (platform === 'darwin') return path.join(homedir, 'Library', 'Application Support', 'nc');
241
+ const dataHome = env.XDG_DATA_HOME || path.join(homedir, '.local', 'share');
242
+ return path.join(dataHome, 'nc');
243
+ }
244
+
245
+ /**
246
+ * Uninstaller script living inside an installed tree. The scripts are
247
+ * self-locating ($ROOT derived from their own path), so no root argument is
248
+ * passed — presence of the tree itself is the authorization.
249
+ * @param {string} root
250
+ * @param {string} [platform]
251
+ */
252
+ export function uninstallerForRoot(root, platform = os.platform()) {
253
+ if (platform === 'win32') return path.join(root, 'uninstall.ps1');
254
+ return path.join(root, 'uninstall.sh');
255
+ }
256
+
257
+ /**
258
+ * Suggested-action line printed when an uninstaller fails. The unix text
259
+ * names the direct recovery command; win32 keeps the generic wording because
260
+ * the .ps1 uninstaller already runs with the caller's own rights (no sudo
261
+ * equivalent is attempted there).
262
+ * @param {string} [platform]
263
+ */
264
+ export function uninstallFailureSuggestion(platform = os.platform()) {
265
+ const base = 'Re-run with administrator/root rights, then retry.';
266
+ if (platform === 'win32') return `suggested_action: ${base}`;
267
+ return `suggested_action: ${base} (or run: sudo nodecommand remove --yes)`;
268
+ }
269
+
270
+ /**
271
+ * Runs one uninstaller script, forwarding the inherited environment.
272
+ *
273
+ * Unix elevation: a system-wide tree is installed by root, so its
274
+ * uninstaller is root-owned (mode 0700) and a non-root caller cannot even
275
+ * read it — plain `bash <script>` always fails there with status 126. When
276
+ * the caller is not root the script is therefore run via `sudo -E bash`,
277
+ * keeping `stdio: 'inherit'` so the sudo password prompt stays visible.
278
+ * When sudo itself is missing (ENOENT) the direct call is retried once so
279
+ * user-owned trees stay removable without sudo; any other sudo failure
280
+ * propagates unchanged and the caller reports the usual [UNINSTALL_FAILED]
281
+ * guidance — behavior never gets worse, it only starts working when sudo
282
+ * exists. Already-root callers run the script directly (no sudo).
283
+ *
284
+ * Security boundary: the caller (runRemove) only ever passes the
285
+ * uninstallerForRoot() path of a tree that already passed the
286
+ * isInstallRoot() check inside findInstallations() — elevation is never
287
+ * applied to an arbitrary path. Windows is untouched: the .ps1 uninstaller
288
+ * runs with the caller's own rights, so no elevation is attempted there.
289
+ * @param {string} scriptPath
290
+ * @param {string|{ platform?: string, execFile?: (cmd: string, args: string[], opts?: object) => unknown, getuid?: (() => number)|null }} [platform]
291
+ * @param {{ execFile?: (cmd: string, args: string[], opts?: object) => unknown, getuid?: (() => number)|null }} [options]
292
+ */
293
+ export function runUninstallerScript(scriptPath, platform = os.platform(), options = {}) {
294
+ let opts = options || {};
295
+ if (typeof platform !== 'string') {
296
+ opts = platform || {};
297
+ platform = opts.platform || os.platform();
298
+ }
299
+ const execFile = opts.execFile || child_process.execFileSync;
300
+ if (platform === 'win32') {
301
+ execFile(
302
+ 'powershell.exe',
303
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath],
304
+ { stdio: 'inherit' }
305
+ );
306
+ return;
307
+ }
308
+ const getuid =
309
+ typeof opts.getuid === 'function'
310
+ ? opts.getuid
311
+ : typeof process.getuid === 'function'
312
+ ? () => process.getuid()
313
+ : null;
314
+ let uid = null;
315
+ try {
316
+ uid = typeof getuid === 'function' ? getuid() : null;
317
+ } catch {
318
+ uid = null;
319
+ }
320
+ if (uid === 0) {
321
+ execFile('bash', [scriptPath], { stdio: 'inherit' });
322
+ return;
323
+ }
324
+ try {
325
+ execFile('sudo', ['-E', 'bash', scriptPath], { stdio: 'inherit' });
326
+ } catch (err) {
327
+ // Without a sudo binary there is nothing to elevate with: retry the
328
+ // historical direct call so user-owned trees stay removable. Any other
329
+ // sudo failure (denied, wrong password, script error) propagates.
330
+ if (!err || err.code !== 'ENOENT') throw err;
331
+ execFile('bash', [scriptPath], { stdio: 'inherit' });
332
+ }
333
+ }
package/package.json CHANGED
@@ -1,40 +1,38 @@
1
1
  {
2
2
  "name": "node-command",
3
- "version": "1.2.2",
4
- "description": "A command runner with node support.",
5
- "main": "./index.js",
6
- "scripts": {
7
- "test": "mocha test.js"
8
- },
9
- "engines": {
10
- "node": ">=0.10.0"
11
- },
3
+ "version": "2.0.0",
4
+ "description": "Node Command agent CLI — connect this machine to your AI assistant fleet",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
12
7
  "bin": {
13
- "nand": "./cli.js"
8
+ "node-command": "./bin/nc.mjs",
9
+ "nodecommand": "./bin/nc.mjs",
10
+ "nc-agent": "./bin/nc.mjs",
11
+ "nc": "./bin/nc.mjs"
14
12
  },
15
13
  "files": [
16
- "cli.js",
17
- "index.js"
14
+ "bin/",
15
+ "lib/"
18
16
  ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "keywords": [
21
+ "node-command",
22
+ "nodecommand",
23
+ "mcp",
24
+ "remote",
25
+ "ai-assistant",
26
+ "agent",
27
+ "cli"
28
+ ],
29
+ "homepage": "https://github.com/huseyincig/NodeCommand",
19
30
  "repository": {
20
31
  "type": "git",
21
- "url": "https://github.com/node-command"
32
+ "url": "git+https://github.com/huseyincig/NodeCommand.git"
22
33
  },
23
- "author": "legodude17 <legodudejb@gmail.com> (https://github.com/legodude17)",
24
- "license": "MIT",
25
34
  "bugs": {
26
- "url": "https://github.com/node-command/issues"
27
- },
28
- "homepage": "https://github.com/node-command",
29
- "dependencies": {
30
- "async": "^2.0.0-rc.6",
31
- "asynquence": "^0.9.0",
32
- "common-callback-names": "^1.0.2",
33
- "function-args": "^1.0.2",
34
- "is-executable": "^1.0.1",
35
- "minimist": "^1.2.0"
35
+ "url": "https://github.com/huseyincig/NodeCommand/issues"
36
36
  },
37
- "devDependencies": {
38
- "mocha": "2.2.5"
39
- }
37
+ "author": "Hüseyin Hadi Çığ <huseyincig@users.noreply.github.com>"
40
38
  }
package/index.js DELETED
@@ -1,117 +0,0 @@
1
- 'use strict';
2
- var path = require('path');
3
- var async = require('async');
4
- var ASQ = require('asynquence');
5
- var isExe = require('is-executable');
6
- var execute = require('child_process').execFile;
7
- var getArgNames = require('function-args');
8
- var names = require('common-callback-names')
9
- function call(fn, args, cb, context) {
10
- var argNames = getArgNames(fn);
11
- var finalArgs = [];
12
- argNames.forEach(function (v) {
13
- v = v.trim();
14
- if (v === 'args') {
15
- finalArgs.push(args);
16
- } else if (names.includes(v)) {
17
- finalArgs.push(cb);
18
- } else {
19
- finalArgs.push(args.shift());
20
- }
21
- });
22
- try {
23
- fn.apply(context || Object.create(null), finalArgs);
24
- } catch (e) {cb(); }
25
- }
26
- module.exports = function(file, args, options, cb){
27
- options = options || {};
28
- ASQ(function (done) {
29
- isExe(file).then(function (res) {
30
- if (res) {
31
- execute(file, args, function (error, stdout, stderr) {
32
- if (error && options.logErrors) {
33
- console.error(error);
34
- }
35
- if (!options.silent) {
36
- stdout.length && console.log(stdout);
37
- stderr.length && console.error(stderr.length);
38
- }
39
- done(true);
40
- });
41
- } else {
42
- done();
43
- }
44
- }, done.fail);
45
- }).then(function (done, msg) {
46
- if (msg) {
47
- done(msg);
48
- }
49
- isExe(path.join(process.cwd(), 'node_modules', '.bin', file), function (res) {
50
- execute(file, args, function (error, stdout, stderr) {
51
- if (error && options.logErrors) {
52
- console.error(error);
53
- }
54
- if (!options.silent) {
55
- stdout.length && console.log(stdout);
56
- stderr.length && console.error(stderr.length);
57
- }
58
- done(true);
59
- });
60
- });
61
- }).then(function (done, msg) {
62
- if (msg) {
63
- return done(msg);
64
- }
65
- var good = false;
66
- var arr = process.env.PATH.split(path.delimiter);
67
- async.forEachOf(arr.concat('--color'), function (v, i, cb) {
68
- isExe(path.join(v, file)).then(function (res) {
69
- if (res) {
70
- execute(path.join(v, file), args, function (error, stdout, stderr) {
71
- if (error && options.logErrors) {
72
- console.error(error);
73
- }
74
- good = true;
75
- if (!options.silent) {
76
- stdout.length && console.log(stdout);
77
- stderr.length && console.error(stderr);
78
- }
79
- cb();
80
- });
81
- } else {
82
- cb();
83
- }
84
- }, done.fail);
85
- }, function (err) {
86
- if (err) {
87
- return done.fail(err);
88
- }
89
- done(good);
90
- });
91
- }).then(function (done, msg) {
92
- if (msg) {
93
- return done(true);
94
- }
95
- try {
96
- var path = require.resolve(file);
97
- } catch(e) {
98
- return done(false);
99
- }
100
- if (path) {
101
- if (options.silent) {
102
- var log = console.log, error = console.error;
103
- console.log = console.error = function (){};
104
- }
105
- call(require(path), args, done.errfcb);
106
- setTimeout(done, options.timeout || 10000);
107
- if (options.silent) {
108
- console.log = log;
109
- console.error = error;
110
- }
111
- } else {
112
- done(false);
113
- }
114
- }).then(function (done, msg) {
115
- cb(null, msg);
116
- });
117
- };
package/license DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) legodude17 <legodudejb@gmail.com> (https://github.com/)
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
package/readme.md DELETED
@@ -1,57 +0,0 @@
1
- # node-command [![Build Status][travis-badge]][travis-link] [![Dependency Status][gemnasium-badge]][gemnasium-link]
2
- > A command runner with node support.
3
-
4
- ## Install
5
-
6
- ```sh
7
- $ npm install --save node-command
8
- ```
9
-
10
-
11
- ## Usage
12
-
13
- ```js
14
- var nodeCommand = require('node-command');
15
-
16
- nodeCommand(
17
- name, // The name to try
18
- args, // Arguments to pass
19
- options, // The options
20
- cb // Callback
21
- );
22
- ```
23
- ### Options
24
- #### Silent
25
- Type: `boolean`
26
- Default: `false`
27
- Suppress all messages.
28
- #### Error log
29
- Type: `boolean`
30
- Default: `false`
31
- Show error messages from `child_process.execFile`.
32
- ## CLI
33
-
34
- ```sh
35
- $ npm install -g node-command
36
- ```
37
-
38
- ```sh
39
- $ nand --help
40
-
41
- Usage: nand [options] -- <file> [args...]
42
-
43
- Options:
44
- -e | --error-log Log errors from child processes
45
- -s | --silent Suppress all messages.
46
- Example
47
- nand -e -- node example.js
48
- ```
49
- ## License
50
-
51
- MIT © [legodude17](https://github.com/legodude17)
52
-
53
- [travis-badge]: http://img.shields.io/travis/node-command.svg?style=flat-square
54
- [travis-link]: https://travis-ci.org/node-command
55
-
56
- [gemnasium-badge]: http://img.shields.io/gemnasium/node-command.svg?style=flat-square
57
- [gemnasium-link]: https://gemnasium.com/node-command