suitescript-verify 0.1.0 → 0.3.0
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 +6 -6
- package/dist/cli.js +23 -5
- package/dist/config.js +32 -4
- package/dist/doctor.js +5 -1
- package/dist/init.js +63 -17
- package/dist/run.js +9 -5
- package/dist/sdf.js +6 -3
- package/dist/shared.js +15 -1
- package/package.json +2 -2
- package/templates/sdf/manifest.xml +1 -1
- package/templates/sdf/runner.xml +1 -1
package/README.md
CHANGED
|
@@ -9,15 +9,15 @@ npm install --save-dev suitescript-verify @oracle/suitecloud-cli
|
|
|
9
9
|
npx suitescript-verify help
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`init` scaffolds `verification/probes`, `verification/sdf`,
|
|
12
|
+
`init` scaffolds `verification/probes`, `verification/sdf`, your results folder and a committed `verification/.env` of layout keys, then prints a to-do list. It creates no credentials. The package imposes no layout for results: where results pages live is your choice, set as `SUITESCRIPT_VERIFY_RESULTS_DIR` in the committed `.env` (a path inside the project). `init` will not pick it for you: at a terminal it asks (suggesting `docs/verification/results`), and without one it needs `--results-dir <path>` (for example `init --results-dir docs/verification/results`). A project that already sets the key is not asked again, and a command that needs the folder without the key stops and names it. `doctor` checks the SuiteCloud CLI, account, sandbox, certificates, passkey and project files, and says which failing steps are the maintainer's own; it exits 1 while any fails.
|
|
13
13
|
|
|
14
14
|
`certificate` generates the runner key pair (EC P-256) and the SDF key pair (RSA-4096) outside the project with `openssl` (`--keys-dir`, default `~/.suitescript-verify`), `passkey` and `env set NAME=value` save settings in the gitignored `verification/.env.local` (values are never printed; a different existing value is kept unless `--overwrite`), and `setup-ci` registers the SDF credentials with the SuiteCloud CLI.
|
|
15
15
|
|
|
16
16
|
`setup-ci` prepares a pipeline: it registers the SDF credentials (auth id `suitescript-verify`) with your project's SuiteCloud CLI and passes the passkey to it through the environment, never an argument. On success it prints the `SUITESCRIPT_VERIFY_` names the pipeline must supply as its own secrets (account, client id, both certificate ids, both private key paths, passkey), never a value. It writes no file, no credential variable and no license variable, and it holds no credential itself: the pipeline provides them. Accepting Oracle's SuiteCloud license in the pipeline is yours.
|
|
17
17
|
|
|
18
|
-
`deploy` generates the runner and an SDF project into `verification/sdf` (the generated `src/`
|
|
18
|
+
`deploy` generates the runner and an SDF project into `verification/sdf` (the generated `src/` there is yours to gitignore) from the RESTlet probes in `verification/probes`, then dry-runs it through your project's SuiteCloud CLI. A real deploy needs `--approved` and a person at a terminal who types `DEPLOY`. `cleanup` does the same with the runner's deployment switched off and needs `CLEANUP`. `--dir <path>` moves `verification/`.
|
|
19
19
|
|
|
20
|
-
`run <probe> [--draft]` runs one probe by name through the deployed runner. The name must be one of the probes in the folder: the runner only runs the fixed list generated into it and never code from a request. Output is scrubbed of the account, client and certificate ids, and refused if it still looks like an identifier. A probe with an `<probe>.expect.json` is compared with it and a difference exits 2 as drift. `--draft` writes
|
|
20
|
+
`run <probe> [--draft]` runs one probe by name through the deployed runner. The name must be one of the probes in the folder: the runner only runs the fixed list generated into it and never code from a request. Output is scrubbed of the account, client and certificate ids, and refused if it still looks like an identifier. A probe with an `<probe>.expect.json` is compared with it and a difference exits 2 as drift. `--draft` writes `<probe>.md` into your results folder (`SUITESCRIPT_VERIFY_RESULTS_DIR`; never over an existing page) with the test, NetSuite version and results, and empty explanation and decision sections. Probes must declare `@scriptType Restlet` and no `@writes`.
|
|
21
21
|
|
|
22
22
|
A probe takes data by declaring parameters in its leading JSDoc block, one tag per parameter: `@param <name> <pattern>`. The name is lowercase-initial letters and digits (`recordType`); the pattern is a JavaScript regular expression (Unicode mode) that the whole value must match, so `[a-z_]+` cannot match `a;b`. Pass each value with `run <probe> --param name=value`. Every declared parameter is required and its value is always a string. The runner validates the parameters against the declarations before any request, rejecting an invalid, undeclared, missing or repeated one, and a probe that declares none takes none; the generated runner checks them again and hands the probe only declared strings in `run(params)`. A value goes in the JSON request body and is never evaluated or turned into code. A bad `@param` tag (no pattern, an invalid pattern, a duplicate name) stops `run` and `deploy`.
|
|
23
23
|
|
|
@@ -33,13 +33,13 @@ define(["N/record"], (record) => ({
|
|
|
33
33
|
}));
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Every command that reaches the sandbox refuses an account that is not a sandbox (`digits_SBn`). The runner uses the script id `customscript_suitescript_verify_runner` and is limited to a role with the ID `customrole_suitescript_verify`, which you create in the sandbox.
|
|
36
|
+
Every command that reaches the sandbox refuses an account that is not a sandbox (`digits_SBn`). The runner uses the script id `customscript_suitescript_verify_runner` and is limited to a role with the ID `customrole_suitescript_verify`, which you create in the sandbox. A project that names its own role sets `SUITESCRIPT_VERIFY_ROLE_ID` (for example `SUITESCRIPT_VERIFY_ROLE_ID=customrole_my_runner`); the generated SDF project uses it in the manifest and the runner. Like every `SUITESCRIPT_VERIFY_` setting it layers from the committed `verification/.env`, the gitignored `.env.local` and the process environment. The value must match `^customrole_[a-z0-9_]+$`; a bad value stops every command that reaches the sandbox, and the error names the key, never the value. `doctor` reports the id in use.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
The verification method and the lessons learned running probes are in the repository, not the package: see [the verification guide](https://github.com/nemchik/suitescript-verify/blob/main/docs/verification/README.md) and [the lessons](https://github.com/nemchik/suitescript-verify/blob/main/docs/verification/lessons.md).
|
|
39
39
|
|
|
40
40
|
## Development
|
|
41
41
|
|
|
42
|
-
You need the Node.js and npm versions in `package.json` `devEngines`.
|
|
42
|
+
You need the Node.js and npm versions in `package.json` `devEngines` (mirrored in `.nvmrc`). `npm pack` and `npm test` refuse any other Node version.
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
45
|
npm install
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
2
3
|
import { certificate } from "./certificate.js";
|
|
3
|
-
import { loadConfig } from "./config.js";
|
|
4
|
+
import { loadConfig, resultsDirGuidance, resultsDirKey, suggestedResultsDir, } from "./config.js";
|
|
4
5
|
import { formatChecks, names, runChecks } from "./doctor.js";
|
|
5
6
|
import { envNames } from "./envfile.js";
|
|
6
7
|
import { init } from "./init.js";
|
|
@@ -10,7 +11,7 @@ import { confirmWrite, flagValue, positionals, requireSandbox, saveEnv, suiteclo
|
|
|
10
11
|
export const USAGE = `Usage: suitescript-verify <command> [options]
|
|
11
12
|
|
|
12
13
|
Commands:
|
|
13
|
-
init
|
|
14
|
+
init [--results-dir <path>] Scaffold the folders and .env, and print a to-do list; the results folder is yours to choose (suggested: docs/verification/results)
|
|
14
15
|
doctor Check the setup; each failing step says who does it
|
|
15
16
|
certificate [--only runner|sdf] [--keys-dir <path>] [--overwrite | --keep]
|
|
16
17
|
Generate the key pairs outside the project
|
|
@@ -53,8 +54,23 @@ function doctor(args) {
|
|
|
53
54
|
console.log(formatChecks(checks));
|
|
54
55
|
return config.problems.length === 0 && checks.every((c) => c.ok) ? 0 : 1;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
/** The results folder `init` was told to use: the flag, or a prompt at a terminal when the key is not set. Never a silent default. */
|
|
58
|
+
async function chooseResultsDir(config, args) {
|
|
59
|
+
const flagged = flagValue(args, "--results-dir");
|
|
60
|
+
// Already configured: the flag is passed on only so `init` can say it was ignored.
|
|
61
|
+
if (config.resultsDir !== undefined || flagged !== undefined)
|
|
62
|
+
return flagged;
|
|
63
|
+
if (!process.stdin.isTTY) {
|
|
64
|
+
throw new UsageError(`${resultsDirKey} is not set. ${resultsDirGuidance} Nothing was created.`);
|
|
65
|
+
}
|
|
66
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
67
|
+
const answer = await rl.question(`Where should results pages live in this project? [${suggestedResultsDir}] `);
|
|
68
|
+
rl.close();
|
|
69
|
+
return answer.trim() || suggestedResultsDir;
|
|
70
|
+
}
|
|
71
|
+
async function initCommand(args) {
|
|
72
|
+
const config = reporting(args);
|
|
73
|
+
console.log(init(process.cwd(), config, await chooseResultsDir(config, args)));
|
|
58
74
|
return 0;
|
|
59
75
|
}
|
|
60
76
|
async function deploy(args) {
|
|
@@ -64,6 +80,7 @@ async function deploy(args) {
|
|
|
64
80
|
const { files, slugs } = buildSdfProject(config.sdfDir, config.probesDir, {
|
|
65
81
|
deployed: true,
|
|
66
82
|
extraTags: config.extraTags,
|
|
83
|
+
roleId: config.roleId,
|
|
67
84
|
});
|
|
68
85
|
console.log(`Probes in the runner: ${slugs.join(", ") || "(none)"}`);
|
|
69
86
|
console.log(`Files to upload: ${files.join(", ")}`);
|
|
@@ -92,6 +109,7 @@ async function cleanup(args) {
|
|
|
92
109
|
buildSdfProject(config.sdfDir, config.probesDir, {
|
|
93
110
|
deployed: false,
|
|
94
111
|
extraTags: config.extraTags,
|
|
112
|
+
roleId: config.roleId,
|
|
95
113
|
});
|
|
96
114
|
return suitecloud(project, config, [
|
|
97
115
|
"project:deploy",
|
|
@@ -183,7 +201,7 @@ export async function main(args) {
|
|
|
183
201
|
if (command === "doctor")
|
|
184
202
|
return doctor(rest);
|
|
185
203
|
if (command === "init")
|
|
186
|
-
return initCommand(rest);
|
|
204
|
+
return await initCommand(rest);
|
|
187
205
|
throw new UsageError(`Unknown command "${command}".\n\n${USAGE}`);
|
|
188
206
|
}
|
|
189
207
|
catch (error) {
|
package/dist/config.js
CHANGED
|
@@ -8,8 +8,17 @@ export const layoutKeys = [
|
|
|
8
8
|
"SUITESCRIPT_VERIFY_RESULTS_DIR",
|
|
9
9
|
"SUITESCRIPT_VERIFY_SDF_DIR",
|
|
10
10
|
];
|
|
11
|
+
export const resultsDirKey = "SUITESCRIPT_VERIFY_RESULTS_DIR";
|
|
12
|
+
/** What `init` suggests for the results folder; it is never applied without the project's say-so. */
|
|
13
|
+
export const suggestedResultsDir = "docs/verification/results";
|
|
14
|
+
/** What every command says when the results folder is not chosen: one sentence, so the commands cannot drift apart. */
|
|
15
|
+
export const resultsDirGuidance = `Choose where results pages live in your project, then run suitescript-verify init --results-dir <path> (${suggestedResultsDir} is the suggested value) or set ${resultsDirKey} in the committed .env.`;
|
|
11
16
|
/** The committed `.env` may also register extra probe tags: a comma-separated list of tag names, not a path. */
|
|
12
17
|
export const extraTagsKey = "SUITESCRIPT_VERIFY_EXTRA_TAGS";
|
|
18
|
+
/** The committed `.env` may also name the runner role: a NetSuite custom role id. */
|
|
19
|
+
export const roleIdKey = "SUITESCRIPT_VERIFY_ROLE_ID";
|
|
20
|
+
export const defaultRoleId = "customrole_suitescript_verify";
|
|
21
|
+
const roleIdPattern = /^customrole_[a-z0-9_]+$/;
|
|
13
22
|
const tagName = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
14
23
|
/** The tag names in a registration value, without the `@`; blank entries are dropped. */
|
|
15
24
|
export const parseTagList = (value) => value
|
|
@@ -41,7 +50,7 @@ function readEnvFile(path) {
|
|
|
41
50
|
? parseEnv(readFileSync(path, "utf8"))
|
|
42
51
|
: new Map();
|
|
43
52
|
}
|
|
44
|
-
const insideProject = (project, path) => {
|
|
53
|
+
export const insideProject = (project, path) => {
|
|
45
54
|
const rel = relative(project, resolve(project, path));
|
|
46
55
|
return !isAbsolute(rel) && rel.split(/[\\/]/)[0] !== "..";
|
|
47
56
|
};
|
|
@@ -49,11 +58,19 @@ const insideProject = (project, path) => {
|
|
|
49
58
|
const looksSecret = (value) => /-----BEGIN /.test(value) ||
|
|
50
59
|
/^eyJ[\w-]+\.[\w-]+\.[\w-]+$/.test(value) ||
|
|
51
60
|
/^[A-Za-z0-9+_=-]{32,}$/.test(value);
|
|
61
|
+
const roleIdProblem = (file) => ({
|
|
62
|
+
message: `${roleIdKey} in ${file} is not a NetSuite custom role id like customrole_suitescript_verify (lowercase letters, digits and underscores after customrole_).`,
|
|
63
|
+
});
|
|
52
64
|
/** Checks the committed `.env` against the allowlist. Messages name the key, never the value. */
|
|
53
65
|
function checkCommitted(project, committed, file) {
|
|
54
66
|
const problems = [];
|
|
55
67
|
for (const [key, value] of committed) {
|
|
56
|
-
if (key ===
|
|
68
|
+
if (key === roleIdKey) {
|
|
69
|
+
// The pattern is the whole check: a valid id may be long enough to look like a token.
|
|
70
|
+
if (!roleIdPattern.test(value))
|
|
71
|
+
problems.push(roleIdProblem(file));
|
|
72
|
+
}
|
|
73
|
+
else if (key === extraTagsKey) {
|
|
57
74
|
if (!parseTagList(value).every((name) => tagName.test(name))) {
|
|
58
75
|
problems.push({
|
|
59
76
|
message: `${key} in ${file} is not a comma-separated list of tag names like flavor,other.`,
|
|
@@ -67,7 +84,7 @@ function checkCommitted(project, committed, file) {
|
|
|
67
84
|
}
|
|
68
85
|
else if (!layoutKeys.includes(key)) {
|
|
69
86
|
problems.push({
|
|
70
|
-
message: `${key} in ${file} is not a layout key. The committed file holds layout only (${[...layoutKeys, extraTagsKey].join(", ")}); put anything else in .env.local or the process environment.`,
|
|
87
|
+
message: `${key} in ${file} is not a layout key. The committed file holds layout only (${[...layoutKeys, extraTagsKey, roleIdKey].join(", ")}); put anything else in .env.local or the process environment.`,
|
|
71
88
|
});
|
|
72
89
|
}
|
|
73
90
|
else if (!insideProject(project, value)) {
|
|
@@ -125,6 +142,13 @@ export function loadConfig(project, dir, processEnv = process.env) {
|
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
const path = (key, fallback) => resolve(project, values[key] || fallback);
|
|
145
|
+
const roleId = values[roleIdKey] || defaultRoleId;
|
|
146
|
+
// The committed file reports its own bad value; this covers .env.local and the environment.
|
|
147
|
+
const committedRole = committed.get(roleIdKey);
|
|
148
|
+
const roleProblems = roleIdPattern.test(roleId) ||
|
|
149
|
+
(committedRole !== undefined && !roleIdPattern.test(committedRole))
|
|
150
|
+
? []
|
|
151
|
+
: [roleIdProblem(".env.local or the environment")];
|
|
128
152
|
const shown = relative(project, envFile).replaceAll("\\", "/");
|
|
129
153
|
return {
|
|
130
154
|
extraTags: parseTagList(values[extraTagsKey] ?? ""),
|
|
@@ -132,8 +156,12 @@ export function loadConfig(project, dir, processEnv = process.env) {
|
|
|
132
156
|
problems: [
|
|
133
157
|
...checkCommitted(project, committed, shown),
|
|
134
158
|
...checkLocalIgnored(project, localFile),
|
|
159
|
+
...roleProblems,
|
|
135
160
|
],
|
|
136
|
-
resultsDir:
|
|
161
|
+
resultsDir: values[resultsDirKey]
|
|
162
|
+
? resolve(project, values[resultsDirKey])
|
|
163
|
+
: undefined,
|
|
164
|
+
roleId,
|
|
137
165
|
root,
|
|
138
166
|
sdfDir: path("SUITESCRIPT_VERIFY_SDF_DIR", join(root, "sdf")),
|
|
139
167
|
values,
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createPrivateKey } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { insideProject, resultsDirGuidance, resultsDirKey, } from "./config.js";
|
|
4
5
|
export const names = {
|
|
5
6
|
account: "SUITESCRIPT_VERIFY_ACCOUNT",
|
|
6
7
|
certificateId: "SUITESCRIPT_VERIFY_CERTIFICATE_ID",
|
|
@@ -76,10 +77,13 @@ export function runChecks(project, config, env = process.env) {
|
|
|
76
77
|
// A malformed project.json fails the step.
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
add(`The results folder is set, inside the project (${resultsDirKey})`, config.resultsDir !== undefined &&
|
|
81
|
+
insideProject(project, config.resultsDir), "maintainer", resultsDirGuidance);
|
|
82
|
+
const absent = [config.probesDir, config.resultsDir, config.sdfDir].filter((dir) => dir !== undefined && !existsSync(dir));
|
|
80
83
|
add("Project files: the probe, results and SDF folders exist and the SDF project has a default auth id", absent.length === 0 && authId !== "", absent.length > 0 ? "agent" : "maintainer", absent.length > 0
|
|
81
84
|
? "Run suitescript-verify init to create the missing folders."
|
|
82
85
|
: "Run account:setup:ci from the SDF folder once the steps above pass, so project.json gets a default auth id.");
|
|
86
|
+
add(`Runner role id: ${config.roleId}`, true, "agent", "");
|
|
83
87
|
return checks;
|
|
84
88
|
}
|
|
85
89
|
export function formatChecks(checks) {
|
package/dist/init.js
CHANGED
|
@@ -1,35 +1,81 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, relative } from "node:path";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { insideProject, resultsDirKey } from "./config.js";
|
|
4
|
+
import { UsageError } from "./shared.js";
|
|
5
|
+
/** A path relative to the project, with forward slashes, for messages and the committed `.env`. */
|
|
6
|
+
const shown = (project, path) => relative(project, path).replaceAll("\\", "/");
|
|
3
7
|
/**
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
8
|
+
* Which folder holds results: the one the configuration already sets, else the
|
|
9
|
+
* one the project just chose. `ignoredNote` is non-empty when a choice was
|
|
10
|
+
* given but the key was already set. Refuses a choice outside the project
|
|
11
|
+
* before anything is created.
|
|
6
12
|
*/
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
function pickResultsDir(project, config, chosenResultsDir) {
|
|
14
|
+
const chosen = chosenResultsDir === undefined
|
|
15
|
+
? undefined
|
|
16
|
+
: resolve(project, chosenResultsDir);
|
|
17
|
+
if (chosen !== undefined && !insideProject(project, chosen)) {
|
|
18
|
+
throw new UsageError(`${resultsDirKey} must be a path inside the project, because the committed .env holds layout only. Nothing was created.`);
|
|
19
|
+
}
|
|
20
|
+
if (config.resultsDir !== undefined) {
|
|
21
|
+
const ignoredNote = chosen !== undefined && chosen !== config.resultsDir
|
|
22
|
+
? `${resultsDirKey} is already set to ${shown(project, config.resultsDir)}, so --results-dir ${shown(project, chosen)} was ignored. Change the key itself to move the results folder.\n\n`
|
|
23
|
+
: "";
|
|
24
|
+
return { alreadySet: true, dir: config.resultsDir, ignoredNote };
|
|
25
|
+
}
|
|
26
|
+
if (chosen === undefined) {
|
|
27
|
+
throw new Error("init needs a results folder; the caller must choose one.");
|
|
28
|
+
}
|
|
29
|
+
return { alreadySet: false, dir: chosen, ignoredNote: "" };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Writes the layout keys to the committed `.env`: a new file with all of them,
|
|
33
|
+
* or, for an existing one, only the results key when it is missing. Returns
|
|
34
|
+
* whether the file was created.
|
|
35
|
+
*/
|
|
36
|
+
function writeLayoutEnv(project, config, resultsDir) {
|
|
12
37
|
const envFile = join(config.root, ".env");
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
38
|
+
const results = `${resultsDirKey}=${shown(project, resultsDir.dir)}`;
|
|
39
|
+
if (!existsSync(envFile)) {
|
|
15
40
|
mkdirSync(dirname(envFile), { recursive: true });
|
|
16
41
|
writeFileSync(envFile, [
|
|
17
42
|
"# Layout only: this file is committed. Credentials go in .env.local, which is gitignored.",
|
|
18
|
-
`SUITESCRIPT_VERIFY_PROBES_DIR=${
|
|
19
|
-
|
|
20
|
-
`SUITESCRIPT_VERIFY_SDF_DIR=${
|
|
43
|
+
`SUITESCRIPT_VERIFY_PROBES_DIR=${shown(project, config.probesDir)}`,
|
|
44
|
+
results,
|
|
45
|
+
`SUITESCRIPT_VERIFY_SDF_DIR=${shown(project, config.sdfDir)}`,
|
|
21
46
|
"",
|
|
22
47
|
].join("\n"));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (!resultsDir.alreadySet) {
|
|
51
|
+
const text = readFileSync(envFile, "utf8");
|
|
52
|
+
const lead = text === "" || text.endsWith("\n") ? "" : "\n";
|
|
53
|
+
appendFileSync(envFile, `${lead}${results}\n`);
|
|
23
54
|
}
|
|
24
|
-
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Scaffolds the folders and a committed `.env` of layout keys, then returns
|
|
59
|
+
* the to-do list for the person. Creates no credential and installs nothing.
|
|
60
|
+
* `chosenResultsDir` is the project's own choice for the results folder; it is
|
|
61
|
+
* used only when the configuration does not already set one.
|
|
62
|
+
*/
|
|
63
|
+
export function init(project, config, chosenResultsDir) {
|
|
64
|
+
const results = pickResultsDir(project, config, chosenResultsDir);
|
|
65
|
+
const dirs = [config.probesDir, results.dir, config.sdfDir];
|
|
66
|
+
for (const dir of dirs)
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
const created = writeLayoutEnv(project, config, results);
|
|
69
|
+
const envFile = shown(project, join(config.root, ".env"));
|
|
70
|
+
const local = shown(project, join(config.root, ".env.local"));
|
|
25
71
|
return [
|
|
26
|
-
`${created ? "Created" : "Kept"} ${
|
|
72
|
+
`${results.ignoredNote}${created ? "Created" : "Kept"} ${envFile} and the folders ${dirs.map((dir) => shown(project, dir)).join(", ")}. No credentials were created.`,
|
|
27
73
|
"",
|
|
28
74
|
"To do (each step is yours, the maintainer's):",
|
|
29
75
|
"1. Install the SuiteCloud CLI: npm install --save-dev @oracle/suitecloud-cli. Read Oracle's license and accept it yourself; this package never does.",
|
|
30
76
|
`2. Add ${local} to .gitignore before saving any value: it holds credentials.`,
|
|
31
77
|
"3. In a sandbox, create the integration record, the role and the certificate mappings, and generate the private keys outside the repository.",
|
|
32
|
-
`4. Save the account, client id, certificate ids, key paths and a passkey in ${local}
|
|
78
|
+
`4. Save the account, client id, certificate ids, key paths and a passkey in ${local}. Setup is described in the package README: https://github.com/nemchik/suitescript-verify#readme`,
|
|
33
79
|
"5. Run suitescript-verify doctor until every step passes.",
|
|
34
80
|
].join("\n");
|
|
35
81
|
}
|
package/dist/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, relative } from "node:path";
|
|
|
3
3
|
import { clientAssertion, restletUrl, scrub, tokenUrl } from "./netsuite.js";
|
|
4
4
|
import { anchored, isRestlet, isSlug, listProbes, matchesExpect, read, readExpect, tagProblems, } from "./probes.js";
|
|
5
5
|
import { resultsDraft } from "./results.js";
|
|
6
|
-
import { positionals, requireSandbox, UsageError } from "./shared.js";
|
|
6
|
+
import { positionals, requireResultsDir, requireSandbox, UsageError, } from "./shared.js";
|
|
7
7
|
/** Every `--param name=value` in the arguments, in order. The value may start with anything. */
|
|
8
8
|
function suppliedParams(args) {
|
|
9
9
|
const out = [];
|
|
@@ -63,6 +63,10 @@ function validateParams(probe, args) {
|
|
|
63
63
|
* code: the runner only knows the probes generated into it by `deploy`.
|
|
64
64
|
*/
|
|
65
65
|
export async function runProbe(project, config, args) {
|
|
66
|
+
// A missing results key is the project's own layout choice: report it before the credentials.
|
|
67
|
+
const resultsDir = args.includes("--draft")
|
|
68
|
+
? requireResultsDir(config)
|
|
69
|
+
: undefined;
|
|
66
70
|
const creds = requireSandbox(config);
|
|
67
71
|
const name = positionals(args)[0] ?? "";
|
|
68
72
|
const probes = listProbes(config.probesDir, config.extraTags);
|
|
@@ -73,7 +77,7 @@ export async function runProbe(project, config, args) {
|
|
|
73
77
|
throw new UsageError(`Unknown probe "${name}". Known: ${probes.map((p) => p.slug).join(", ") || "(none)"}`);
|
|
74
78
|
}
|
|
75
79
|
if (!isRestlet(probe)) {
|
|
76
|
-
throw new UsageError(`${probe.slug} needs a ${probe.scriptType || "(undeclared)"} script. The runner only runs RESTlet probes (declare @scriptType Restlet); ask the user to run it by hand
|
|
80
|
+
throw new UsageError(`${probe.slug} needs a ${probe.scriptType || "(undeclared)"} script. The runner only runs RESTlet probes (declare @scriptType Restlet); ask the user to run it by hand in a sandbox console.`);
|
|
77
81
|
}
|
|
78
82
|
if (probe.writes) {
|
|
79
83
|
throw new UsageError(`${probe.slug} declares @writes, which needs the user's separate approval. That path is not automated.`);
|
|
@@ -156,9 +160,9 @@ export async function runProbe(project, config, args) {
|
|
|
156
160
|
console.log(`\nDRIFT: the output differs from ${probe.slug}.expect.json. The platform changed, or the expectation is wrong.`);
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
|
-
if (
|
|
163
|
+
if (resultsDir !== undefined) {
|
|
160
164
|
const probeFile = join(config.probesDir, `${probe.slug}.js`);
|
|
161
|
-
const page = join(
|
|
165
|
+
const page = join(resultsDir, `${probe.slug}.md`);
|
|
162
166
|
const shown = relative(project, page).replaceAll("\\", "/");
|
|
163
167
|
if (existsSync(page)) {
|
|
164
168
|
console.log(`\n${shown} already exists; not overwriting.`);
|
|
@@ -169,7 +173,7 @@ export async function runProbe(project, config, args) {
|
|
|
169
173
|
output: body.result,
|
|
170
174
|
probe,
|
|
171
175
|
probeFile,
|
|
172
|
-
resultsDir
|
|
176
|
+
resultsDir,
|
|
173
177
|
scriptSource: read(probeFile),
|
|
174
178
|
version: body.version ?? "unknown",
|
|
175
179
|
}), known);
|
package/dist/sdf.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { defaultRoleId } from "./config.js";
|
|
3
4
|
import { fileCabinetFolder } from "./netsuite.js";
|
|
4
5
|
import { isRestlet, listProbes, read, tagProblems, } from "./probes.js";
|
|
5
6
|
import { UsageError } from "./shared.js";
|
|
@@ -24,7 +25,7 @@ export function generateRunner(probes, source) {
|
|
|
24
25
|
* the SDF folder. Only RESTlet probes go in: the runner is a RESTlet. With
|
|
25
26
|
* `deployed: false` the runner's deployment is switched off (cleanup).
|
|
26
27
|
*/
|
|
27
|
-
export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], }) {
|
|
28
|
+
export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], roleId = defaultRoleId, }) {
|
|
28
29
|
const probes = listProbes(probesDir, extraTags).filter(isRestlet);
|
|
29
30
|
const bad = probes.filter((probe) => tagProblems(probe).length > 0);
|
|
30
31
|
if (bad.length > 0) {
|
|
@@ -39,9 +40,11 @@ export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], }
|
|
|
39
40
|
mkdirSync(join(src, "Objects"), { recursive: true });
|
|
40
41
|
writeFileSync(join(sdfDir, "package.json"), template("sdf/package.json"));
|
|
41
42
|
writeFileSync(join(sdfDir, "suitecloud.config.js"), template("sdf/suitecloud.config.js"));
|
|
42
|
-
writeFileSync(join(src, "manifest.xml"), template("sdf/manifest.xml"));
|
|
43
|
+
writeFileSync(join(src, "manifest.xml"), template("sdf/manifest.xml").replaceAll("%%ROLE_ID%%", roleId));
|
|
43
44
|
writeFileSync(join(src, "deploy.xml"), template("sdf/deploy.xml"));
|
|
44
|
-
writeFileSync(join(src, "Objects", "customscript_suitescript_verify_runner.xml"), template("sdf/runner.xml")
|
|
45
|
+
writeFileSync(join(src, "Objects", "customscript_suitescript_verify_runner.xml"), template("sdf/runner.xml")
|
|
46
|
+
.replace("%%DEPLOYED%%", deployed ? "T" : "F")
|
|
47
|
+
.replaceAll("%%ROLE_ID%%", roleId));
|
|
45
48
|
const slugs = probes.map((probe) => probe.slug);
|
|
46
49
|
const files = ["runner.js"];
|
|
47
50
|
writeFileSync(join(folder, "runner.js"), generateRunner(probes, template("runner.template.js")));
|
package/dist/shared.js
CHANGED
|
@@ -2,12 +2,19 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { resultsDirGuidance, resultsDirKey } from "./config.js";
|
|
5
6
|
import { isSandboxAccount, names } from "./doctor.js";
|
|
6
7
|
import { applyEnvUpdate, planEnvUpdate } from "./envfile.js";
|
|
7
8
|
/** A refusal or mistake to report to the person; the message holds no secret. */
|
|
8
9
|
export class UsageError extends Error {
|
|
9
10
|
}
|
|
10
|
-
const valueFlags = [
|
|
11
|
+
const valueFlags = [
|
|
12
|
+
"--dir",
|
|
13
|
+
"--keys-dir",
|
|
14
|
+
"--only",
|
|
15
|
+
"--param",
|
|
16
|
+
"--results-dir",
|
|
17
|
+
];
|
|
11
18
|
/** The value after a flag, when given. */
|
|
12
19
|
export function flagValue(args, flag) {
|
|
13
20
|
const at = args.indexOf(flag);
|
|
@@ -110,6 +117,13 @@ export async function saveEnv(config, args, entries) {
|
|
|
110
117
|
console.log(`Kept the existing ${name}. To replace it, ask the user, then rerun with --overwrite.`);
|
|
111
118
|
}
|
|
112
119
|
}
|
|
120
|
+
/** The results folder, or a refusal naming the key: the package imposes no layout for results. */
|
|
121
|
+
export function requireResultsDir(config) {
|
|
122
|
+
if (config.resultsDir === undefined) {
|
|
123
|
+
throw new UsageError(`${resultsDirKey} is not set. ${resultsDirGuidance}`);
|
|
124
|
+
}
|
|
125
|
+
return config.resultsDir;
|
|
126
|
+
}
|
|
113
127
|
/** The runner credentials, or a refusal: every command that reaches the sandbox needs a sandbox account. */
|
|
114
128
|
export function requireSandbox(config) {
|
|
115
129
|
const v = config.values;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suitescript-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Run probes in a NetSuite sandbox to verify what SuiteScript modules do, with guardrails for agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"suitescript",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"git-pull-run": "2.1.0",
|
|
52
52
|
"lefthook": "2.2.2",
|
|
53
|
-
"skills": "1.
|
|
53
|
+
"skills": "1.8.0",
|
|
54
54
|
"typescript": "7.0.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
package/templates/sdf/runner.xml
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<scriptdeployments>
|
|
8
8
|
<scriptdeployment scriptid="customdeploy_suitescript_verify_runner">
|
|
9
9
|
<allroles>F</allroles>
|
|
10
|
-
<audslctrole>[
|
|
10
|
+
<audslctrole>[%%ROLE_ID%%]</audslctrole>
|
|
11
11
|
<isdeployed>%%DEPLOYED%%</isdeployed>
|
|
12
12
|
<loglevel>DEBUG</loglevel>
|
|
13
13
|
<status>RELEASED</status>
|