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.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/ns_gm_restlet.js +452 -0
- package/package.json +53 -0
- package/server/app.js +154 -0
- package/server/auth.js +216 -0
- package/src/cli.js +78 -0
- package/src/commands/env.js +12 -0
- package/src/commands/help.js +274 -0
- package/src/commands/init.js +107 -0
- package/src/commands/logs.js +118 -0
- package/src/commands/run.js +118 -0
- package/src/commands/setup.js +179 -0
- package/src/commands/stop.js +98 -0
- package/src/utils/config.js +39 -0
- package/src/utils/exitCodes.js +37 -0
- package/src/utils/profileStore.js +107 -0
|
@@ -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
|
+
};
|