ns-gm 1.0.3 → 1.0.5
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 +40 -6
- package/package.json +3 -3
- package/src/cli.js +14 -0
- package/src/commands/help.js +26 -1
- package/src/commands/setup-ci.js +65 -0
package/README.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
# ns-gm
|
|
1
|
+
# ns-gm (NetSuite God Mode CLI)
|
|
2
2
|
|
|
3
|
-
NetSuite CLI for running SuiteScript snippets and fetching logs through a local proxy + RESTlet.
|
|
3
|
+
NetSuite CLI for running SuiteScript snippets, files and fetching logs through a local proxy + RESTlet.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm
|
|
9
|
-
npm link
|
|
8
|
+
npm i -g ns-gm
|
|
10
9
|
```
|
|
11
10
|
|
|
12
11
|
Then use:
|
|
@@ -15,6 +14,13 @@ Then use:
|
|
|
15
14
|
ns-gm --help
|
|
16
15
|
```
|
|
17
16
|
|
|
17
|
+
For local development from source:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm link
|
|
22
|
+
```
|
|
23
|
+
|
|
18
24
|
## NetSuite Setup (OAuth 2.0 M2M)
|
|
19
25
|
|
|
20
26
|
### 1) Integration Record
|
|
@@ -44,7 +50,7 @@ After saving, copy:
|
|
|
44
50
|
Generate keypair (PowerShell example):
|
|
45
51
|
|
|
46
52
|
```powershell
|
|
47
|
-
$dir = "C:\Users\
|
|
53
|
+
$dir = "C:\Users\user\Documents\ns-gm-certs"
|
|
48
54
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
49
55
|
openssl req -new -x509 -nodes -days 365 -newkey rsa:4096 -keyout "$dir\private_key.pem" -out "$dir\public_key.pem" -subj "/CN=ns-gm-oauth"
|
|
50
56
|
```
|
|
@@ -74,7 +80,7 @@ Setup is alias-based. It lets you pick an existing alias or create `new`.
|
|
|
74
80
|
- `accountId`: `1234567_SB1`
|
|
75
81
|
- `clientId`: `6f8d...` (OAuth 2.0 Client ID from integration record)
|
|
76
82
|
- `certificateId`: `custcertificate_oauth2_prod` (kid from M2M mapping)
|
|
77
|
-
- `privateKeyPath`: `C:\Users\
|
|
83
|
+
- `privateKeyPath`: `C:\Users\user\Documents\ns-gm-certs\private_key.pem`
|
|
78
84
|
- `restletUrl`: `https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=customscript_ns_gm_restlet&deploy=1`
|
|
79
85
|
- `scope`: `restlets`
|
|
80
86
|
|
|
@@ -84,6 +90,34 @@ Show active profile/config:
|
|
|
84
90
|
ns-gm setup --show
|
|
85
91
|
```
|
|
86
92
|
|
|
93
|
+
## Non-interactive Setup (CI/Sandbox)
|
|
94
|
+
|
|
95
|
+
Use `setup:ci` for headless environments. This command upserts a profile alias and sets it active.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ns-gm setup:ci \
|
|
99
|
+
--alias prod-main \
|
|
100
|
+
--account 1234567_SB1 \
|
|
101
|
+
--clientid 6f8d... \
|
|
102
|
+
--certificateid custcertificate_oauth2_prod \
|
|
103
|
+
--privatekeypath C:\Users\user\Documents\ns-gm-certs\private_key.pem \
|
|
104
|
+
--restleturl "https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=customscript_ns_gm_restlet&deploy=1" \
|
|
105
|
+
--scope restlets
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Required flags:
|
|
109
|
+
|
|
110
|
+
- `--alias`
|
|
111
|
+
- `--account`
|
|
112
|
+
- `--clientid`
|
|
113
|
+
- `--certificateid`
|
|
114
|
+
- `--privatekeypath`
|
|
115
|
+
- `--restleturl`
|
|
116
|
+
|
|
117
|
+
Optional flags:
|
|
118
|
+
|
|
119
|
+
- `--scope` (defaults to `restlets`)
|
|
120
|
+
|
|
87
121
|
Credentials are stored at:
|
|
88
122
|
|
|
89
123
|
`~/.ns-gm/credentials.json`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ns-gm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "CLI tool for executing SuiteScript code against NetSuite",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/Project-X-Innovation/ns-gm"
|
|
24
|
+
"url": "git+https://github.com/Project-X-Innovation/ns-gm.git"
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/Project-X-Innovation/ns-gm#readme",
|
|
27
27
|
"bugs": {
|
|
@@ -50,4 +50,4 @@
|
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=24.0.0"
|
|
52
52
|
}
|
|
53
|
-
}
|
|
53
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ const envCommand = require('./commands/env');
|
|
|
8
8
|
const logsCommand = require('./commands/logs');
|
|
9
9
|
const stopCommand = require('./commands/stop');
|
|
10
10
|
const setupCommand = require('./commands/setup');
|
|
11
|
+
const setupCiCommand = require('./commands/setup-ci');
|
|
11
12
|
const helpCommand = require('./commands/help');
|
|
12
13
|
|
|
13
14
|
const program = new Command();
|
|
@@ -62,6 +63,19 @@ program
|
|
|
62
63
|
.option('--show', 'Show current configuration (secrets masked)')
|
|
63
64
|
.action(setupCommand);
|
|
64
65
|
|
|
66
|
+
// Setup CI command - Non-interactive credential configuration
|
|
67
|
+
program
|
|
68
|
+
.command('setup:ci')
|
|
69
|
+
.description('Non-interactive credential configuration for CI/sandbox')
|
|
70
|
+
.option('--alias <alias>', 'Profile alias to create/update and activate')
|
|
71
|
+
.option('--account <accountId>', 'NetSuite Account ID')
|
|
72
|
+
.option('--clientid <clientId>', 'OAuth 2.0 Client ID')
|
|
73
|
+
.option('--certificateid <certificateId>', 'Certificate ID (kid)')
|
|
74
|
+
.option('--privatekeypath <path>', 'Private key path (.pem)')
|
|
75
|
+
.option('--restleturl <url>', 'RESTlet URL')
|
|
76
|
+
.option('--scope <scope>', 'OAuth scope (defaults to restlets)')
|
|
77
|
+
.action(setupCiCommand);
|
|
78
|
+
|
|
65
79
|
// Help command - Show command documentation
|
|
66
80
|
program
|
|
67
81
|
.command('help [command]')
|
package/src/commands/help.js
CHANGED
|
@@ -112,12 +112,37 @@ const helpData = {
|
|
|
112
112
|
"Private key material is not stored in the credentials file"
|
|
113
113
|
]
|
|
114
114
|
},
|
|
115
|
+
'setup:ci': {
|
|
116
|
+
brief: "Non-interactive credential configuration for CI/sandbox",
|
|
117
|
+
syntax: "ns-gm setup:ci --alias <alias> --account <accountId> --clientid <clientId> --certificateid <certificateId> --privatekeypath <path> --restleturl <url> [--scope <scope>]",
|
|
118
|
+
description: "Non-interactive setup for CI/sandbox usage. Upserts the specified alias profile and sets it as the active profile in ~/.ns-gm/credentials.json.",
|
|
119
|
+
options: [
|
|
120
|
+
{ flag: "--alias <alias>", description: "Profile alias to create/update and set active" },
|
|
121
|
+
{ flag: "--account <accountId>", description: "NetSuite account ID" },
|
|
122
|
+
{ flag: "--clientid <clientId>", description: "OAuth 2.0 client ID" },
|
|
123
|
+
{ flag: "--certificateid <certificateId>", description: "Certificate ID (kid)" },
|
|
124
|
+
{ flag: "--privatekeypath <path>", description: "Path to private key PEM file" },
|
|
125
|
+
{ flag: "--restleturl <url>", description: "Deployed RESTlet URL" },
|
|
126
|
+
{ flag: "--scope <scope>", description: "OAuth scope (defaults to restlets)" }
|
|
127
|
+
],
|
|
128
|
+
examples: [
|
|
129
|
+
"ns-gm setup:ci --alias prod-main --account 1234567_SB1 --clientid abc123 --certificateid custcertificate_oauth2_prod --privatekeypath ./private_key.pem --restleturl \"https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=customscript_ns_gm_restlet&deploy=1\"",
|
|
130
|
+
"ns-gm setup:ci --alias prod-main --account 1234567_SB1 --clientid abc123 --certificateid custcertificate_oauth2_prod --privatekeypath ./private_key.pem --restleturl \"https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=customscript_ns_gm_restlet&deploy=1\" --scope restlets"
|
|
131
|
+
],
|
|
132
|
+
clarifications: [
|
|
133
|
+
"All setup flags except --scope are required",
|
|
134
|
+
"Profile alias is upserted (create or overwrite) by alias name",
|
|
135
|
+
"Successful command always sets the alias as active",
|
|
136
|
+
"Private key path must exist on disk when command runs",
|
|
137
|
+
"RESTlet URL must include /app/site/hosting/restlet.nl and query params"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
115
140
|
help: {
|
|
116
141
|
brief: "Show help information for commands",
|
|
117
142
|
syntax: "ns-gm help [command] [options]",
|
|
118
143
|
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
144
|
options: [
|
|
120
|
-
{ flag: "[command]", description: "Optional command name to get detailed help for (init, run, env, logs, stop, setup)" },
|
|
145
|
+
{ flag: "[command]", description: "Optional command name to get detailed help for (init, run, env, logs, stop, setup, setup:ci)" },
|
|
121
146
|
{ flag: "--format <format>", description: "Output format: json (default) or text" }
|
|
122
147
|
],
|
|
123
148
|
examples: [
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { EXIT_CODES, exitWithCode } = require('../utils/exitCodes');
|
|
4
|
+
const {
|
|
5
|
+
saveProfile,
|
|
6
|
+
setActiveAlias,
|
|
7
|
+
STORE_PATH
|
|
8
|
+
} = require('../utils/profileStore');
|
|
9
|
+
|
|
10
|
+
const RESTLET_URL_REGEX = /^https:\/\/.+\/app\/site\/hosting\/restlet\.nl\?.+/;
|
|
11
|
+
|
|
12
|
+
function requireTrimmedValue(value, label) {
|
|
13
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
14
|
+
exitWithCode(EXIT_CODES.VALIDATION_ERROR, `${label} is required`);
|
|
15
|
+
}
|
|
16
|
+
return value.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveAndValidatePrivateKeyPath(privateKeyPathInput) {
|
|
20
|
+
const resolvedPath = path.resolve(privateKeyPathInput.trim());
|
|
21
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
22
|
+
exitWithCode(EXIT_CODES.VALIDATION_ERROR, `File does not exist: ${resolvedPath}`);
|
|
23
|
+
}
|
|
24
|
+
return resolvedPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateRestletUrl(restletUrl) {
|
|
28
|
+
if (!RESTLET_URL_REGEX.test(restletUrl)) {
|
|
29
|
+
exitWithCode(EXIT_CODES.VALIDATION_ERROR, 'Invalid RESTlet URL');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function setupCiCommand(options) {
|
|
34
|
+
try {
|
|
35
|
+
const alias = requireTrimmedValue(options.alias, 'Alias');
|
|
36
|
+
const accountId = requireTrimmedValue(options.account, 'Account ID');
|
|
37
|
+
const clientId = requireTrimmedValue(options.clientid, 'Client ID');
|
|
38
|
+
const certificateId = requireTrimmedValue(options.certificateid, 'Certificate ID');
|
|
39
|
+
const privateKeyPathInput = requireTrimmedValue(options.privatekeypath, 'Private key path');
|
|
40
|
+
const restletUrl = requireTrimmedValue(options.restleturl, 'RESTlet URL');
|
|
41
|
+
const scopeInput = typeof options.scope === 'string' ? options.scope.trim() : '';
|
|
42
|
+
|
|
43
|
+
const privateKeyPath = resolveAndValidatePrivateKeyPath(privateKeyPathInput);
|
|
44
|
+
validateRestletUrl(restletUrl);
|
|
45
|
+
|
|
46
|
+
const profile = {
|
|
47
|
+
accountId,
|
|
48
|
+
clientId,
|
|
49
|
+
certificateId,
|
|
50
|
+
privateKeyPath,
|
|
51
|
+
restletUrl,
|
|
52
|
+
scope: scopeInput || 'restlets'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
saveProfile(alias, profile);
|
|
56
|
+
setActiveAlias(alias);
|
|
57
|
+
|
|
58
|
+
console.log(`Saved profile "${alias}" and set it as active.`);
|
|
59
|
+
console.log(`Credentials store: ${STORE_PATH}`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
exitWithCode(EXIT_CODES.GENERAL_ERROR, `Setup CI error: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = setupCiCommand;
|