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,179 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const prompts = require('prompts');
4
+ const { EXIT_CODES, exitWithCode } = require('../utils/exitCodes');
5
+ const {
6
+ loadStore,
7
+ listAliases,
8
+ saveProfile,
9
+ setActiveAlias,
10
+ getActiveProfile,
11
+ STORE_PATH
12
+ } = require('../utils/profileStore');
13
+
14
+ function onCancel() {
15
+ console.log('\nSetup cancelled.');
16
+ process.exit(EXIT_CODES.SUCCESS);
17
+ }
18
+
19
+ function maskValue(value) {
20
+ if (!value) return '(not set)';
21
+ const text = String(value);
22
+ if (text.length <= 8) return '***';
23
+ return `${text.slice(0, 4)}***${text.slice(-4)}`;
24
+ }
25
+
26
+ function validateAlias(alias, existingAliases) {
27
+ const cleanAlias = alias.trim();
28
+ if (!cleanAlias) {
29
+ return 'Alias is required';
30
+ }
31
+
32
+ if (existingAliases.includes(cleanAlias)) {
33
+ return `Alias "${cleanAlias}" already exists`;
34
+ }
35
+
36
+ return true;
37
+ }
38
+
39
+ function validatePrivateKeyPath(privateKeyPathInput) {
40
+ const resolvedPath = path.resolve(privateKeyPathInput.trim());
41
+ if (!fs.existsSync(resolvedPath)) {
42
+ return `File does not exist: ${resolvedPath}`;
43
+ }
44
+ return true;
45
+ }
46
+
47
+ async function selectAlias(aliases, activeAlias) {
48
+ const choices = aliases.map(alias => ({
49
+ title: alias === activeAlias ? `${alias} (active)` : alias,
50
+ value: alias
51
+ }));
52
+ choices.push({ title: 'new', value: 'new' });
53
+
54
+ const response = await prompts({
55
+ type: 'select',
56
+ name: 'selectedAlias',
57
+ message: 'Select a profile alias:',
58
+ choices,
59
+ initial: activeAlias && aliases.includes(activeAlias) ? aliases.indexOf(activeAlias) : choices.length - 1
60
+ }, { onCancel });
61
+
62
+ return response.selectedAlias;
63
+ }
64
+
65
+ async function createNewAlias(existingAliases) {
66
+ const answers = await prompts([
67
+ {
68
+ type: 'text',
69
+ name: 'alias',
70
+ message: 'Profile alias:',
71
+ validate: value => validateAlias(value, existingAliases)
72
+ },
73
+ {
74
+ type: 'text',
75
+ name: 'accountId',
76
+ message: 'NetSuite Account ID:',
77
+ validate: value => value.trim().length > 0 || 'Account ID is required'
78
+ },
79
+ {
80
+ type: 'text',
81
+ name: 'clientId',
82
+ message: 'OAuth 2.0 Client ID:',
83
+ validate: value => value.trim().length > 0 || 'Client ID is required'
84
+ },
85
+ {
86
+ type: 'text',
87
+ name: 'certificateId',
88
+ message: 'Certificate ID (kid):',
89
+ validate: value => value.trim().length > 0 || 'Certificate ID is required'
90
+ },
91
+ {
92
+ type: 'text',
93
+ name: 'privateKeyPath',
94
+ message: 'Private key path (.pem):',
95
+ validate: validatePrivateKeyPath
96
+ },
97
+ {
98
+ type: 'text',
99
+ name: 'restletUrl',
100
+ message: 'RESTlet URL:',
101
+ validate: value => /^https:\/\/.+\/app\/site\/hosting\/restlet\.nl\?.+/.test(value.trim()) || 'Invalid RESTlet URL'
102
+ },
103
+ {
104
+ type: 'text',
105
+ name: 'scope',
106
+ message: 'OAuth scope:',
107
+ initial: 'restlets',
108
+ validate: value => value.trim().length > 0 || 'Scope is required'
109
+ }
110
+ ], { onCancel });
111
+
112
+ const alias = answers.alias.trim();
113
+ const profile = {
114
+ accountId: answers.accountId.trim(),
115
+ clientId: answers.clientId.trim(),
116
+ certificateId: answers.certificateId.trim(),
117
+ privateKeyPath: path.resolve(answers.privateKeyPath.trim()),
118
+ restletUrl: answers.restletUrl.trim(),
119
+ scope: answers.scope.trim()
120
+ };
121
+
122
+ saveProfile(alias, profile);
123
+ setActiveAlias(alias);
124
+
125
+ console.log(`\nSaved profile "${alias}" and set it as active.`);
126
+ console.log(`Credentials store: ${STORE_PATH}`);
127
+ }
128
+
129
+ async function showConfig() {
130
+ const store = loadStore();
131
+ const activeProfile = getActiveProfile();
132
+
133
+ if (!activeProfile) {
134
+ console.log('No active profile configured. Run: ns-gm setup');
135
+ return;
136
+ }
137
+
138
+ console.log('\nCurrent ns-gm profile configuration:\n');
139
+ console.log(`Credentials store: ${STORE_PATH}`);
140
+ console.log(`Active alias: ${store.activeAlias}`);
141
+ console.log(`Account ID: ${activeProfile.accountId}`);
142
+ console.log(`Client ID: ${maskValue(activeProfile.clientId)}`);
143
+ console.log(`Certificate ID: ${maskValue(activeProfile.certificateId)}`);
144
+ console.log(`Private Key Path: ${activeProfile.privateKeyPath}`);
145
+ console.log(`RESTlet URL: ${activeProfile.restletUrl}`);
146
+ console.log(`Scope: ${activeProfile.scope || 'restlets'}`);
147
+ console.log('');
148
+ }
149
+
150
+ async function setupCommand(options) {
151
+ try {
152
+ if (options.show) {
153
+ await showConfig();
154
+ return;
155
+ }
156
+
157
+ const store = loadStore();
158
+ const aliases = listAliases();
159
+ console.log('NetSuite GM - Profile Setup\n');
160
+
161
+ const selectedAlias = await selectAlias(aliases, store.activeAlias);
162
+ if (!selectedAlias) {
163
+ onCancel();
164
+ return;
165
+ }
166
+
167
+ if (selectedAlias !== 'new') {
168
+ setActiveAlias(selectedAlias);
169
+ console.log(`\nActive alias set to "${selectedAlias}".`);
170
+ return;
171
+ }
172
+
173
+ await createNewAlias(aliases);
174
+ } catch (error) {
175
+ exitWithCode(EXIT_CODES.GENERAL_ERROR, `Setup error: ${error.message}`);
176
+ }
177
+ }
178
+
179
+ module.exports = setupCommand;
@@ -0,0 +1,98 @@
1
+ const axios = require('axios');
2
+ const { exec } = require('child_process');
3
+ const { EXIT_CODES, exitWithCode } = require('../utils/exitCodes');
4
+ const config = require('../utils/config');
5
+
6
+ const PORT = config.proxyPort;
7
+ const PROXY_URL = `http://localhost:${PORT}`;
8
+
9
+ /**
10
+ * Check if proxy server is running
11
+ */
12
+ async function checkProxyHealth() {
13
+ try {
14
+ const response = await axios.get(`${PROXY_URL}/health`, { timeout: 2000 });
15
+ return response.data.status === 'ok';
16
+ } catch (error) {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Find and kill the process using the proxy port
23
+ */
24
+ function killProcessOnPort(port) {
25
+ return new Promise((resolve, reject) => {
26
+ // Use netstat to find the process ID
27
+ exec(`netstat -ano | findstr :${port}`, (error, stdout, stderr) => {
28
+ if (error || !stdout) {
29
+ reject(new Error('No process found on port ' + port));
30
+ return;
31
+ }
32
+
33
+ // Parse the PID from netstat output
34
+ const lines = stdout.split('\n');
35
+ const listening = lines.find(line => line.includes('LISTENING'));
36
+
37
+ if (!listening) {
38
+ reject(new Error('No listening process found on port ' + port));
39
+ return;
40
+ }
41
+
42
+ // Extract PID (last column)
43
+ const parts = listening.trim().split(/\s+/);
44
+ const pid = parts[parts.length - 1];
45
+
46
+ if (!pid || pid === '0') {
47
+ reject(new Error('Could not determine process ID'));
48
+ return;
49
+ }
50
+
51
+ // Kill the process (use /F /PID without doubling slashes)
52
+ exec(`taskkill /F /PID ${pid}`, (killError, killStdout, killStderr) => {
53
+ if (killError) {
54
+ reject(new Error(`Failed to kill process ${pid}: ${killError.message}`));
55
+ return;
56
+ }
57
+ resolve(pid);
58
+ });
59
+ });
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Stop the proxy server
65
+ */
66
+ async function stopCommand() {
67
+ try {
68
+ // Check if proxy is running
69
+ const isRunning = await checkProxyHealth();
70
+
71
+ if (!isRunning) {
72
+ console.log('Proxy server is not running.');
73
+ return;
74
+ }
75
+
76
+ console.log('Stopping proxy server...');
77
+
78
+ // Kill the process
79
+ const pid = await killProcessOnPort(PORT);
80
+
81
+ // Wait a moment for the process to fully stop
82
+ await new Promise(resolve => setTimeout(resolve, 1000));
83
+
84
+ // Verify it stopped
85
+ const stillRunning = await checkProxyHealth();
86
+
87
+ if (stillRunning) {
88
+ exitWithCode(EXIT_CODES.GENERAL_ERROR, 'Failed to stop proxy server. It may still be running.');
89
+ }
90
+
91
+ console.log(`✓ Proxy server stopped successfully (PID: ${pid})`);
92
+
93
+ } catch (error) {
94
+ exitWithCode(EXIT_CODES.GENERAL_ERROR, `Error stopping proxy server: ${error.message}`);
95
+ }
96
+ }
97
+
98
+ module.exports = stopCommand;
@@ -0,0 +1,39 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Load configuration from config.json
6
+ * @param {boolean} showWarning - Whether to show warning if config not found
7
+ * @returns {Object} Configuration object
8
+ */
9
+ function loadConfig(showWarning = false) {
10
+ try {
11
+ const configPath = path.join(__dirname, '../../config.json');
12
+ const configData = fs.readFileSync(configPath, 'utf8');
13
+ return JSON.parse(configData);
14
+ } catch (error) {
15
+ // Return default configuration if file doesn't exist or is invalid
16
+ if (showWarning) {
17
+ console.warn('Warning: Could not load config.json, using defaults');
18
+ }
19
+ return {
20
+ proxyPort: 9292,
21
+ proxyInactivityTimeout: 900000,
22
+ defaultLogCount: 20
23
+ };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Load configuration with warning (for init command)
29
+ * @returns {Object} Configuration object
30
+ */
31
+ function loadConfigWithWarning() {
32
+ return loadConfig(true);
33
+ }
34
+
35
+ // Load and cache configuration (without warning)
36
+ const config = loadConfig(false);
37
+
38
+ module.exports = config;
39
+ module.exports.loadConfigWithWarning = loadConfigWithWarning;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Semantic exit codes for CLI commands
3
+ *
4
+ * These exit codes provide meaningful information about why the CLI failed,
5
+ * making it easier for scripts and AI agents to handle errors programmatically.
6
+ */
7
+
8
+ const EXIT_CODES = {
9
+ SUCCESS: 0, // Command completed successfully
10
+ GENERAL_ERROR: 1, // General/unknown error
11
+ CONFIG_ERROR: 2, // Configuration error (missing profile/config, invalid config)
12
+ NETWORK_ERROR: 3, // Network error (proxy unreachable, NetSuite connection failed)
13
+ VALIDATION_ERROR: 4, // Validation error (missing required parameters)
14
+ EXECUTION_ERROR: 5, // Execution error (code execution failed in NetSuite)
15
+ AUTH_ERROR: 6 // Authentication error (OAuth failed)
16
+ };
17
+
18
+ /**
19
+ * Exit the process with a semantic exit code
20
+ * @param {number} code - Exit code from EXIT_CODES
21
+ * @param {string} message - Optional error message to display
22
+ */
23
+ function exitWithCode(code, message) {
24
+ if (message) {
25
+ if (code === EXIT_CODES.SUCCESS) {
26
+ console.log(message);
27
+ } else {
28
+ console.error(message);
29
+ }
30
+ }
31
+ process.exit(code);
32
+ }
33
+
34
+ module.exports = {
35
+ EXIT_CODES,
36
+ exitWithCode
37
+ };
@@ -0,0 +1,107 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const STORE_DIR = path.join(os.homedir(), '.ns-gm');
6
+ const STORE_PATH = path.join(STORE_DIR, 'credentials.json');
7
+ const EMPTY_STORE = { activeAlias: null, profiles: {} };
8
+
9
+ function normalizeStore(store) {
10
+ if (!store || typeof store !== 'object') {
11
+ return { ...EMPTY_STORE };
12
+ }
13
+
14
+ const activeAlias = typeof store.activeAlias === 'string' ? store.activeAlias : null;
15
+ const profiles = store.profiles && typeof store.profiles === 'object' ? store.profiles : {};
16
+
17
+ return { activeAlias, profiles };
18
+ }
19
+
20
+ function loadStore() {
21
+ try {
22
+ if (!fs.existsSync(STORE_PATH)) {
23
+ return { ...EMPTY_STORE };
24
+ }
25
+
26
+ const raw = fs.readFileSync(STORE_PATH, 'utf8');
27
+ return normalizeStore(JSON.parse(raw));
28
+ } catch (error) {
29
+ return { ...EMPTY_STORE };
30
+ }
31
+ }
32
+
33
+ function saveStore(store) {
34
+ const normalized = normalizeStore(store);
35
+ fs.mkdirSync(STORE_DIR, { recursive: true });
36
+ fs.writeFileSync(STORE_PATH, JSON.stringify(normalized, null, 2), 'utf8');
37
+ return normalized;
38
+ }
39
+
40
+ function listAliases() {
41
+ const store = loadStore();
42
+ return Object.keys(store.profiles).sort();
43
+ }
44
+
45
+ function saveProfile(alias, profile) {
46
+ const cleanAlias = typeof alias === 'string' ? alias.trim() : '';
47
+ if (!cleanAlias) {
48
+ throw new Error('Alias is required');
49
+ }
50
+
51
+ const requiredFields = ['accountId', 'clientId', 'certificateId', 'privateKeyPath', 'restletUrl'];
52
+ for (const field of requiredFields) {
53
+ if (!profile[field] || !String(profile[field]).trim()) {
54
+ throw new Error(`Missing required profile field: ${field}`);
55
+ }
56
+ }
57
+
58
+ const store = loadStore();
59
+ store.profiles[cleanAlias] = {
60
+ accountId: String(profile.accountId).trim(),
61
+ clientId: String(profile.clientId).trim(),
62
+ certificateId: String(profile.certificateId).trim(),
63
+ privateKeyPath: String(profile.privateKeyPath).trim(),
64
+ restletUrl: String(profile.restletUrl).trim(),
65
+ scope: profile.scope && String(profile.scope).trim() ? String(profile.scope).trim() : 'restlets'
66
+ };
67
+
68
+ saveStore(store);
69
+ return store.profiles[cleanAlias];
70
+ }
71
+
72
+ function setActiveAlias(alias) {
73
+ const cleanAlias = typeof alias === 'string' ? alias.trim() : '';
74
+ if (!cleanAlias) {
75
+ throw new Error('Alias is required');
76
+ }
77
+
78
+ const store = loadStore();
79
+ if (!store.profiles[cleanAlias]) {
80
+ throw new Error(`Alias not found: ${cleanAlias}`);
81
+ }
82
+
83
+ store.activeAlias = cleanAlias;
84
+ saveStore(store);
85
+ }
86
+
87
+ function getActiveProfile() {
88
+ const store = loadStore();
89
+ if (!store.activeAlias || !store.profiles[store.activeAlias]) {
90
+ return null;
91
+ }
92
+
93
+ return {
94
+ alias: store.activeAlias,
95
+ ...store.profiles[store.activeAlias]
96
+ };
97
+ }
98
+
99
+ module.exports = {
100
+ STORE_PATH,
101
+ loadStore,
102
+ saveStore,
103
+ listAliases,
104
+ saveProfile,
105
+ setActiveAlias,
106
+ getActiveProfile
107
+ };