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 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 install
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\simos\Documents\ns-gm-certs"
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\simos\Documents\ns-gm-certs\private_key.pem`
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",
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]')
@@ -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;