ns-gm 1.0.3

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.
@@ -0,0 +1,274 @@
1
+ const { EXIT_CODES } = require('../utils/exitCodes');
2
+
3
+ /**
4
+ * Help data structure with all commands
5
+ */
6
+ const helpData = {
7
+ commands: {
8
+ init: {
9
+ brief: "Start the local NetSuite proxy server",
10
+ syntax: "ns-gm init",
11
+ description: "Starts the Express proxy server used by ns-gm commands. The server runs on port 9292 (configurable in config.json) as a detached background process.",
12
+ options: [],
13
+ examples: ["ns-gm init"],
14
+ clarifications: [
15
+ "Server runs as detached process (continues after CLI exits)",
16
+ "Auto-stops after 15 minutes of inactivity (configurable)",
17
+ "Only one instance can run at a time",
18
+ "Check server status with: curl http://localhost:9292/health"
19
+ ]
20
+ },
21
+ run: {
22
+ brief: "Execute SuiteScript 2.1 code against NetSuite",
23
+ syntax: "ns-gm run --code <code> | --file <path>",
24
+ description: "Executes arbitrary SuiteScript 2.1 code against your NetSuite account via the runner RESTlet. Code runs in NetSuite's SuiteScript runtime with access to all N/* modules.",
25
+ options: [
26
+ { flag: "--code <code>", description: "Inline SuiteScript code to execute (for short snippets)" },
27
+ { flag: "--file <path>", description: "Path to file containing SuiteScript code (for longer scripts)" }
28
+ ],
29
+ examples: [
30
+ 'ns-gm run --code "return 2 + 2"',
31
+ 'ns-gm run --code "const rec = record.load({type: \'customer\', id: 123}); return rec.getValue({fieldId: \'entityid\'});"',
32
+ "ns-gm run --file script.js"
33
+ ],
34
+ clarifications: [
35
+ "Output is the return value of the executed code (what you return from the function)",
36
+ "Errors include stack trace, line numbers, and error type",
37
+ "Requires proxy running (run 'ns-gm init' first)",
38
+ "Governance usage is tracked and returned in response",
39
+ "Use log.debug(), log.audit(), log.error() for logging - retrieve with 'ns-gm logs'",
40
+ "Supports all N/* modules (search, record, query, runtime, format, etc.)"
41
+ ]
42
+ },
43
+ env: {
44
+ brief: "Get the current NetSuite environment type",
45
+ syntax: "ns-gm env",
46
+ description: "Runs the equivalent of: ns-gm run --code \"return runtime.envType\".",
47
+ options: [],
48
+ examples: [
49
+ "ns-gm env"
50
+ ],
51
+ clarifications: [
52
+ "Requires proxy running (run 'ns-gm init' first)",
53
+ "Requires an active profile (run 'ns-gm setup' first)"
54
+ ]
55
+ },
56
+ logs: {
57
+ brief: "Retrieve script execution logs from NetSuite",
58
+ syntax: "ns-gm logs [--script-id <scriptId>] [options]",
59
+ description: "Retrieves execution logs from NetSuite's Script Execution Log for any deployed script. Useful for debugging code executed via 'ns-gm run' or any other script.",
60
+ options: [
61
+ { flag: "--script-id <scriptId>", description: "Script ID to retrieve logs for (e.g., customscript_my_script)" },
62
+ { flag: "--date-from <date>", description: "Start date in YYYY-MM-DD format" },
63
+ { flag: "--date-to <date>", description: "End date in YYYY-MM-DD format" },
64
+ { flag: "--type <type>", description: "Log type: DEBUG, AUDIT, ERROR, EMERGENCY" },
65
+ { flag: "--page <number>", description: "Page number for pagination (default: 0)" },
66
+ { flag: "--page-size <number>", description: "Number of logs per page (default: 20)" }
67
+ ],
68
+ examples: [
69
+ "ns-gm logs --script-id customscript_ns_gm_",
70
+ "ns-gm logs --script-id customscript_sync_handler --type ERROR",
71
+ "ns-gm logs --script-id customscript_my_script --date-from 2025-12-01 --date-to 2025-12-15"
72
+ ],
73
+ clarifications: [
74
+ "Script ID is converted to internal ID automatically",
75
+ "Returns logs in reverse chronological order (newest first)",
76
+ "Default date range is last 7 days if not specified",
77
+ "Pagination starts at page 0",
78
+ "Governance cost: ~10 units per query (SuiteQL search)",
79
+ "Use this to see logs from code executed via 'ns-gm run'"
80
+ ]
81
+ },
82
+ stop: {
83
+ brief: "Stop the local NetSuite proxy server",
84
+ syntax: "ns-gm stop",
85
+ description: "Gracefully stops the local Express proxy server by finding the process using port 9292 and terminating it.",
86
+ options: [],
87
+ examples: ["ns-gm stop"],
88
+ clarifications: [
89
+ "Finds process by port number (9292 by default)",
90
+ "Sends termination signal to the process",
91
+ "Verifies server stopped via health check",
92
+ "Safe to run even if server not running (no error)"
93
+ ]
94
+ },
95
+ setup: {
96
+ brief: "Interactive credential configuration",
97
+ syntax: "ns-gm setup [options]",
98
+ description: "Interactive wizard for profile alias setup. Lets you select an existing alias or create a new one, then stores credentials in ~/.ns-gm/credentials.json.",
99
+ options: [
100
+ { flag: "--show", description: "Display current configuration with secrets masked" }
101
+ ],
102
+ examples: [
103
+ "ns-gm setup",
104
+ "ns-gm setup --show"
105
+ ],
106
+ clarifications: [
107
+ "Prompts show existing aliases plus 'new'",
108
+ "Choosing existing alias only switches active profile",
109
+ "Private key path is validated during setup",
110
+ "Press Ctrl+C to cancel without saving",
111
+ "Credentials are stored in ~/.ns-gm/credentials.json",
112
+ "Private key material is not stored in the credentials file"
113
+ ]
114
+ },
115
+ help: {
116
+ brief: "Show help information for commands",
117
+ syntax: "ns-gm help [command] [options]",
118
+ description: "Displays help information for all commands or a specific command. Output format can be JSON (default, for AI agents) or plain text (for humans).",
119
+ options: [
120
+ { flag: "[command]", description: "Optional command name to get detailed help for (init, run, env, logs, stop, setup)" },
121
+ { flag: "--format <format>", description: "Output format: json (default) or text" }
122
+ ],
123
+ examples: [
124
+ "ns-gm help",
125
+ "ns-gm help run",
126
+ "ns-gm help --format text",
127
+ "ns-gm help logs --format json"
128
+ ],
129
+ clarifications: [
130
+ "JSON format is default for AI agent consumption",
131
+ "Use --format text for human-readable output",
132
+ "Without [command] argument, shows all commands with brief descriptions",
133
+ "With [command] argument, shows detailed help for that command"
134
+ ]
135
+ }
136
+ },
137
+ exitCodes: {
138
+ "0": "SUCCESS - Command completed successfully",
139
+ "1": "GENERAL_ERROR - Unknown or general error",
140
+ "2": "CONFIG_ERROR - Configuration error (port conflict, missing profile/config)",
141
+ "3": "NETWORK_ERROR - Network error (proxy unreachable, connection failed)",
142
+ "4": "VALIDATION_ERROR - Validation error (missing parameters, file not found)",
143
+ "5": "EXECUTION_ERROR - Code execution failed in NetSuite",
144
+ "6": "AUTH_ERROR - OAuth authentication failed"
145
+ }
146
+ };
147
+
148
+ /**
149
+ * Format help output as JSON
150
+ */
151
+ function formatJSON(data) {
152
+ return JSON.stringify(data, null, 2);
153
+ }
154
+
155
+ /**
156
+ * Format help output as plain text
157
+ */
158
+ function formatText(data) {
159
+ let output = '';
160
+
161
+ if (data.command) {
162
+ // Detailed help for specific command
163
+ output += `Command: ${data.command}\n`;
164
+ output += `Brief: ${data.brief}\n`;
165
+ output += `Syntax: ${data.syntax}\n\n`;
166
+
167
+ if (data.description) {
168
+ output += `Description:\n${data.description}\n\n`;
169
+ }
170
+
171
+ if (data.options && data.options.length > 0) {
172
+ output += `Options:\n`;
173
+ data.options.forEach(opt => {
174
+ output += ` ${opt.flag.padEnd(30)} ${opt.description}\n`;
175
+ });
176
+ output += '\n';
177
+ }
178
+
179
+ if (data.examples && data.examples.length > 0) {
180
+ output += `Examples:\n`;
181
+ data.examples.forEach(ex => {
182
+ output += ` ${ex}\n`;
183
+ });
184
+ output += '\n';
185
+ }
186
+
187
+ if (data.clarifications && data.clarifications.length > 0) {
188
+ output += `Clarifications:\n`;
189
+ data.clarifications.forEach(cl => {
190
+ output += ` - ${cl}\n`;
191
+ });
192
+ output += '\n';
193
+ }
194
+
195
+ if (data.exitCodes) {
196
+ output += `Exit Codes:\n`;
197
+ Object.keys(data.exitCodes).forEach(code => {
198
+ output += ` ${code}: ${data.exitCodes[code]}\n`;
199
+ });
200
+ }
201
+ } else {
202
+ // Overview of all commands
203
+ output += `NetSuite GM - Help\n`;
204
+ output += `${'='.repeat(50)}\n\n`;
205
+ output += `Available Commands:\n\n`;
206
+
207
+ Object.keys(data.commands).forEach(cmd => {
208
+ const cmdData = data.commands[cmd];
209
+ output += ` ${cmd.padEnd(12)} ${cmdData.brief}\n`;
210
+ });
211
+
212
+ output += `\n\nExit Codes:\n`;
213
+ Object.keys(data.exitCodes).forEach(code => {
214
+ output += ` ${code}: ${data.exitCodes[code]}\n`;
215
+ });
216
+
217
+ output += `\n\nFor detailed help: ns-gm help <command>\n`;
218
+ }
219
+
220
+ return output;
221
+ }
222
+
223
+ /**
224
+ * Help command implementation
225
+ */
226
+ async function helpCommand(commandName, options) {
227
+ try {
228
+ const format = options.format || 'json';
229
+
230
+ if (!commandName) {
231
+ // Show overview of all commands
232
+ const data = {
233
+ commands: {},
234
+ exitCodes: helpData.exitCodes
235
+ };
236
+
237
+ // Include brief descriptions only
238
+ Object.keys(helpData.commands).forEach(cmd => {
239
+ data.commands[cmd] = {
240
+ brief: helpData.commands[cmd].brief,
241
+ syntax: helpData.commands[cmd].syntax
242
+ };
243
+ });
244
+
245
+ const output = format === 'text' ? formatText(data) : formatJSON(data);
246
+ console.log(output);
247
+ } else {
248
+ // Show detailed help for specific command
249
+ const cmdData = helpData.commands[commandName];
250
+
251
+ if (!cmdData) {
252
+ console.error(`Unknown command: ${commandName}`);
253
+ console.error(`Available commands: ${Object.keys(helpData.commands).join(', ')}`);
254
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
255
+ }
256
+
257
+ const data = {
258
+ command: commandName,
259
+ ...cmdData,
260
+ exitCodes: helpData.exitCodes
261
+ };
262
+
263
+ const output = format === 'text' ? formatText(data) : formatJSON(data);
264
+ console.log(output);
265
+ }
266
+
267
+ process.exit(EXIT_CODES.SUCCESS);
268
+ } catch (error) {
269
+ console.error(`Help error: ${error.message}`);
270
+ process.exit(EXIT_CODES.GENERAL_ERROR);
271
+ }
272
+ }
273
+
274
+ module.exports = helpCommand;
@@ -0,0 +1,107 @@
1
+ const { spawn } = require('child_process');
2
+ const axios = require('axios');
3
+ const path = require('path');
4
+ const net = require('net');
5
+ const { EXIT_CODES, exitWithCode } = require('../utils/exitCodes');
6
+ const config = require('../utils/config');
7
+ const { loadConfigWithWarning } = require('../utils/config');
8
+
9
+ const PORT = config.proxyPort;
10
+ const PROXY_URL = `http://localhost:${PORT}`;
11
+
12
+ /**
13
+ * Check if a port is in use
14
+ */
15
+ function isPortInUse(port) {
16
+ return new Promise((resolve) => {
17
+ const server = net.createServer();
18
+
19
+ server.once('error', (err) => {
20
+ if (err.code === 'EADDRINUSE') {
21
+ resolve(true); // Port is in use
22
+ } else {
23
+ resolve(false);
24
+ }
25
+ });
26
+
27
+ server.once('listening', () => {
28
+ server.close();
29
+ resolve(false); // Port is available
30
+ });
31
+
32
+ server.listen(port);
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Wait for proxy server to become healthy
38
+ */
39
+ async function waitForHealth(maxAttempts = 10, delayMs = 500) {
40
+ for (let i = 0; i < maxAttempts; i++) {
41
+ try {
42
+ const response = await axios.get(`${PROXY_URL}/health`, { timeout: 1000 });
43
+ if (response.data.status === 'ok') {
44
+ return true;
45
+ }
46
+ } catch (error) {
47
+ // Server not ready yet, wait and retry
48
+ }
49
+
50
+ await new Promise(resolve => setTimeout(resolve, delayMs));
51
+ }
52
+
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Start the proxy server
58
+ */
59
+ async function initCommand() {
60
+ try {
61
+ // Load config with warning (shows warning if config.json not found)
62
+ loadConfigWithWarning();
63
+
64
+ // Check if proxy is already running
65
+ const portInUse = await isPortInUse(PORT);
66
+
67
+ if (portInUse) {
68
+ // Verify it's our proxy by checking /health
69
+ try {
70
+ const response = await axios.get(`${PROXY_URL}/health`, { timeout: 2000 });
71
+ if (response.data.status === 'ok') {
72
+ console.log(`Proxy server is already running on port ${PORT}`);
73
+ console.log(`Health check: ${PROXY_URL}/health`);
74
+ return;
75
+ }
76
+ } catch (error) {
77
+ exitWithCode(EXIT_CODES.CONFIG_ERROR, `Port ${PORT} is in use by another process. Please stop it or choose a different port.`);
78
+ }
79
+ }
80
+
81
+ // Start proxy server as detached process
82
+ const serverPath = path.join(__dirname, '../../server/app.js');
83
+ const child = spawn('node', [serverPath], {
84
+ detached: true,
85
+ stdio: 'ignore'
86
+ });
87
+
88
+ child.unref(); // Allow parent to exit independently
89
+
90
+ console.log('Starting proxy server...');
91
+
92
+ // Wait for server to become healthy
93
+ const healthy = await waitForHealth();
94
+
95
+ if (healthy) {
96
+ console.log(`✓ Proxy server started successfully on port ${PORT}`);
97
+ console.log(`Health check: ${PROXY_URL}/health`);
98
+ } else {
99
+ exitWithCode(EXIT_CODES.CONFIG_ERROR, 'Failed to start proxy server. Check your ns-gm configuration.');
100
+ }
101
+
102
+ } catch (error) {
103
+ exitWithCode(EXIT_CODES.GENERAL_ERROR, `Error starting proxy server: ${error.message}`);
104
+ }
105
+ }
106
+
107
+ module.exports = initCommand;
@@ -0,0 +1,118 @@
1
+ const axios = require('axios');
2
+ const { EXIT_CODES, exitWithCode } = require('../utils/exitCodes');
3
+ const config = require('../utils/config');
4
+
5
+ const PORT = config.proxyPort;
6
+ const PROXY_URL = `http://localhost:${PORT}`;
7
+
8
+ /**
9
+ * Format and display logs
10
+ */
11
+ function displayLogs(response) {
12
+ const { success, logs, governance, error } = response;
13
+
14
+ // Handle case where query succeeded but no logs found (success: false, error: null)
15
+ if (!success && !error) {
16
+ console.log('\nNo logs found for the specified criteria.');
17
+ if (governance) {
18
+ console.log(`Governance used: ${governance.initial - governance.remaining} units`);
19
+ }
20
+ return;
21
+ }
22
+
23
+ if (!success) {
24
+ console.error('\n✗ Failed to retrieve logs\n');
25
+ console.error('Error:', error || 'Unknown error');
26
+ return;
27
+ }
28
+
29
+ if (!logs || logs.length === 0) {
30
+ console.log('\nNo logs found for the specified criteria.');
31
+ return;
32
+ }
33
+
34
+ console.log(`\nFound ${logs.length} log entries:\n`);
35
+ console.log('─'.repeat(80));
36
+
37
+ logs.forEach((log, index) => {
38
+ console.log(`\n[${index + 1}] ${log.date || 'Unknown Date'}`);
39
+ console.log(` Type: ${log.type || 'N/A'}`);
40
+ console.log(` Title: ${log.title || 'N/A'}`);
41
+ if (log.details) {
42
+ console.log(` Details: ${log.details}`);
43
+ }
44
+ if (log.scriptType) {
45
+ console.log(` Script Type: ${log.scriptType}`);
46
+ }
47
+ });
48
+
49
+ console.log('\n' + '─'.repeat(80));
50
+
51
+ if (governance) {
52
+ console.log(`\nGovernance used: ${governance.initial - governance.remaining} units`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Check if proxy server is running
58
+ */
59
+ async function checkProxyHealth() {
60
+ try {
61
+ const response = await axios.get(`${PROXY_URL}/health`, { timeout: 2000 });
62
+ return response.data.status === 'ok';
63
+ } catch (error) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Retrieve logs command
70
+ */
71
+ async function logsCommand(options) {
72
+ try {
73
+ const { scriptId, dateFrom, dateTo, type, page, pageSize } = options;
74
+
75
+ // Check if proxy is running
76
+ const proxyHealthy = await checkProxyHealth();
77
+ if (!proxyHealthy) {
78
+ console.error('Error: Proxy server is not running.');
79
+ exitWithCode(EXIT_CODES.NETWORK_ERROR, 'Start it with: ns-gm init');
80
+ }
81
+
82
+ // Build request payload
83
+ const payload = {};
84
+
85
+ if (scriptId) payload.scriptId = scriptId;
86
+ if (dateFrom) payload.dateFrom = dateFrom;
87
+ if (dateTo) payload.dateTo = dateTo;
88
+ if (type) payload.type = type.toUpperCase();
89
+ if (page) payload.pageIndex = parseInt(page);
90
+ if (pageSize) payload.pageSize = parseInt(pageSize);
91
+
92
+ // Retrieve logs
93
+ const targetScript = scriptId ? scriptId : 'RESTlet runner';
94
+ console.log(`Retrieving logs for ${targetScript}...`);
95
+ const response = await axios.post(`${PROXY_URL}/logs`, payload, {
96
+ timeout: 30000 // 30 second timeout
97
+ });
98
+
99
+ // Display logs
100
+ displayLogs(response.data);
101
+
102
+ } catch (error) {
103
+ if (error.response) {
104
+ // Server responded with error
105
+ console.error('\n✗ Failed to retrieve logs\n');
106
+ const errorMsg = error.response.data.error || error.message;
107
+ console.error('Error:', errorMsg);
108
+ exitWithCode(EXIT_CODES.EXECUTION_ERROR);
109
+ } else if (error.code === 'ECONNREFUSED') {
110
+ console.error('Error: Cannot connect to proxy server.');
111
+ exitWithCode(EXIT_CODES.NETWORK_ERROR, 'Start it with: ns-gm init');
112
+ } else {
113
+ exitWithCode(EXIT_CODES.GENERAL_ERROR, `Error: ${error.message}`);
114
+ }
115
+ }
116
+ }
117
+
118
+ module.exports = logsCommand;
@@ -0,0 +1,118 @@
1
+ const axios = require('axios');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { EXIT_CODES, exitWithCode } = require('../utils/exitCodes');
5
+ const config = require('../utils/config');
6
+
7
+ const PORT = config.proxyPort;
8
+ const PROXY_URL = `http://localhost:${PORT}`;
9
+
10
+ /**
11
+ * Format and display execution results
12
+ */
13
+ function displayResults(response) {
14
+ const { success, result, executionTime, governance, logs, error } = response;
15
+
16
+ if (success) {
17
+ console.log('\n✓ Execution successful\n');
18
+ console.log('Result:', result);
19
+ console.log(`\nExecution time: ${executionTime}ms`);
20
+
21
+ if (governance) {
22
+ console.log('\nGovernance:');
23
+ console.log(` Initial: ${governance.initial}`);
24
+ console.log(` Remaining: ${governance.remaining}`);
25
+ console.log(` Used: ${governance.used}`);
26
+ }
27
+
28
+ if (logs && logs.length > 0) {
29
+ console.log('\nLogs:');
30
+ logs.forEach(log => console.log(` ${log}`));
31
+ }
32
+ } else {
33
+ console.error('\n✗ Execution failed\n');
34
+ console.error('Error:', error || 'Unknown error');
35
+
36
+ if (logs && logs.length > 0) {
37
+ console.log('\nLogs:');
38
+ logs.forEach(log => console.log(` ${log}`));
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Check if proxy server is running
45
+ */
46
+ async function checkProxyHealth() {
47
+ try {
48
+ const response = await axios.get(`${PROXY_URL}/health`, { timeout: 2000 });
49
+ return response.data.status === 'ok';
50
+ } catch (error) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute code command
57
+ */
58
+ async function runCommand(options) {
59
+ try {
60
+ const { code, file } = options;
61
+
62
+ // Validate input
63
+ if (!code && !file) {
64
+ console.error('Error: Must provide either --code or --file');
65
+ console.error('Usage: ns-gm run --code "return 2+2"');
66
+ console.error(' or: ns-gm run --file script.js');
67
+ exitWithCode(EXIT_CODES.VALIDATION_ERROR);
68
+ }
69
+
70
+ if (code && file) {
71
+ exitWithCode(EXIT_CODES.VALIDATION_ERROR, 'Error: Cannot use both --code and --file. Choose one.');
72
+ }
73
+
74
+ // Get code to execute
75
+ let codeToExecute = code;
76
+ if (file) {
77
+ const filePath = path.resolve(file);
78
+ if (!fs.existsSync(filePath)) {
79
+ exitWithCode(EXIT_CODES.VALIDATION_ERROR, `Error: File not found: ${filePath}`);
80
+ }
81
+ codeToExecute = fs.readFileSync(filePath, 'utf8');
82
+ }
83
+
84
+ // Check if proxy is running
85
+ const proxyHealthy = await checkProxyHealth();
86
+ if (!proxyHealthy) {
87
+ console.error('Error: Proxy server is not running.');
88
+ exitWithCode(EXIT_CODES.NETWORK_ERROR, 'Start it with: ns-gm init');
89
+ }
90
+
91
+ // Execute code
92
+ console.log('Executing code...');
93
+ const response = await axios.post(`${PROXY_URL}/run`, {
94
+ code: codeToExecute
95
+ }, {
96
+ timeout: 30000 // 30 second timeout
97
+ });
98
+
99
+ // Display results
100
+ displayResults(response.data);
101
+
102
+ } catch (error) {
103
+ if (error.response) {
104
+ // Server responded with error
105
+ console.error('\n✗ Execution failed\n');
106
+ const errorMsg = error.response.data.error || error.message;
107
+ console.error('Error:', errorMsg);
108
+ exitWithCode(EXIT_CODES.EXECUTION_ERROR);
109
+ } else if (error.code === 'ECONNREFUSED') {
110
+ console.error('Error: Cannot connect to proxy server.');
111
+ exitWithCode(EXIT_CODES.NETWORK_ERROR, 'Start it with: ns-gm init');
112
+ } else {
113
+ exitWithCode(EXIT_CODES.GENERAL_ERROR, `Error: ${error.message}`);
114
+ }
115
+ }
116
+ }
117
+
118
+ module.exports = runCommand;