@sasjs/cli 5.0.2 → 5.1.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.
@@ -94,7 +94,7 @@ var shell_1 = require("../../utils/shell");
94
94
  */
95
95
  function generateDocs(target, config, outDirectory) {
96
96
  return __awaiter(this, void 0, void 0, function () {
97
- var _a, serverUrl, newOutDirectory, enableLineage, doxyContentFromConfig, _b, macroCoreFolders, macroFolders, programFolders, serviceFolders, jobFolders, combinedFolders, PROJECT_NAME, PROJECT_BRIEF, packageJsonContent, packageJson, e_1, doxyContent, layoutFilePath, LAYOUT_FILE, readMePath, doxyParams, doxyConfigPath, spinner, pathExists, _c, stderr, code, foldersListForDot;
97
+ var _a, serverUrl, newOutDirectory, enableLineage, doxyContentFromConfig, _b, macroCoreFolders, macroFolders, programFolders, serviceFolders, jobFolders, combinedFolders, PROJECT_NAME, PROJECT_BRIEF, packageJsonContent, packageJson, e_1, doxyContent, layoutFilePath, LAYOUT_FILE, readMePath, doxyVariables, doxyConfigPath, spinner, pathExists, _c, stderr, code, foldersListForDot;
98
98
  var _this = this;
99
99
  var _d, _e;
100
100
  return __generator(this, function (_f) {
@@ -163,7 +163,7 @@ function generateDocs(target, config, outDirectory) {
163
163
  case 5:
164
164
  LAYOUT_FILE = _f.sent();
165
165
  readMePath = path_1.default.join(doxyContent.path, doxyContent.readMe);
166
- doxyParams = setVariableCmd({
166
+ doxyVariables = {
167
167
  DOXY_HTML_OUTPUT: newOutDirectory,
168
168
  DOXY_INPUT: "\"".concat(readMePath, "\" ").concat(combinedFolders),
169
169
  DOXY_MAINPAGE: readMePath,
@@ -175,7 +175,7 @@ function generateDocs(target, config, outDirectory) {
175
175
  PROJECT_BRIEF: PROJECT_BRIEF,
176
176
  PROJECT_LOGO: path_1.default.join(doxyContent.path, doxyContent.logo),
177
177
  PROJECT_NAME: PROJECT_NAME
178
- });
178
+ };
179
179
  return [4 /*yield*/, getDoxyConfigPath(doxyContent.path)];
180
180
  case 6:
181
181
  doxyConfigPath = _f.sent();
@@ -194,8 +194,9 @@ function generateDocs(target, config, outDirectory) {
194
194
  case 9: return [4 /*yield*/, (0, utils_1.createFolder)(newOutDirectory)];
195
195
  case 10:
196
196
  _f.sent();
197
- _c = (0, shell_1.exec)("".concat(doxyParams, " doxygen \"").concat(doxyConfigPath, "\""), {
198
- silent: process.env.LOG_LEVEL !== utils_1.LogLevel.Debug
197
+ _c = (0, shell_1.execFile)('doxygen', [doxyConfigPath], {
198
+ silent: process.env.LOG_LEVEL !== utils_1.LogLevel.Debug,
199
+ env: doxyVariables
199
200
  }), stderr = _c.stderr, code = _c.code;
200
201
  if (process.env.LOG_LEVEL !== utils_1.LogLevel.Debug)
201
202
  spinner.stop();
@@ -228,18 +229,6 @@ function generateDocs(target, config, outDirectory) {
228
229
  });
229
230
  });
230
231
  }
231
- function setVariableCmd(params) {
232
- var command = '';
233
- if ((0, utils_1.isWindows)()) {
234
- for (var param in params)
235
- command += "set ".concat(param, "=").concat(params[param], " && ");
236
- }
237
- else {
238
- for (var param in params)
239
- command += "".concat(param, "='").concat(params[param], "' ");
240
- }
241
- return command;
242
- }
243
232
  var getDoxyConfigPath = function (doxyContentPath) { return __awaiter(void 0, void 0, void 0, function () {
244
233
  var doxyFilePath;
245
234
  return __generator(this, function (_a) {
@@ -555,7 +555,7 @@ describe('sasjs doc', function () {
555
555
  return __generator(this, function (_a) {
556
556
  switch (_a.label) {
557
557
  case 0:
558
- jest.spyOn(shell, 'exec').mockReturnValueOnce({
558
+ jest.spyOn(shell, 'execFile').mockReturnValueOnce({
559
559
  code: 1,
560
560
  stderr: 'error: bad config',
561
561
  stdout: ''
@@ -571,7 +571,7 @@ describe('sasjs doc', function () {
571
571
  return __generator(this, function (_a) {
572
572
  switch (_a.label) {
573
573
  case 0:
574
- jest.spyOn(shell, 'exec').mockReturnValueOnce({
574
+ jest.spyOn(shell, 'execFile').mockReturnValueOnce({
575
575
  code: 1,
576
576
  stderr: 'command not found',
577
577
  stdout: ''
@@ -41,7 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.createLintConfigFile = void 0;
43
43
  var path_1 = __importDefault(require("path"));
44
- var getLintConfig_1 = require("@sasjs/lint/utils/getLintConfig");
44
+ var lint_1 = require("@sasjs/lint");
45
45
  var utils_1 = require("@sasjs/utils");
46
46
  /**
47
47
  * Creates a SASjs Lint configuration file.
@@ -54,7 +54,7 @@ var createLintConfigFile = function (parentFolderName) { return __awaiter(void 0
54
54
  switch (_a.label) {
55
55
  case 0:
56
56
  configDestinationPath = path_1.default.join(process.projectDir, parentFolderName, '.sasjslint');
57
- return [4 /*yield*/, (0, utils_1.createFile)(configDestinationPath, JSON.stringify(getLintConfig_1.DefaultLintConfiguration, null, 2))];
57
+ return [4 /*yield*/, (0, utils_1.createFile)(configDestinationPath, JSON.stringify(lint_1.DefaultLintConfiguration, null, 2))];
58
58
  case 1:
59
59
  _a.sent();
60
60
  return [2 /*return*/];
@@ -17,15 +17,27 @@ export interface ShellResult {
17
17
  stderr: string;
18
18
  code: number;
19
19
  }
20
+ export interface ExecOptions {
21
+ /**
22
+ * Whether the command's output also reaches this process's streams, which is
23
+ * how shelljs' option of the same name behaved.
24
+ */
25
+ silent?: boolean;
26
+ /** Extra variables for the child, on top of this process's environment. */
27
+ env?: NodeJS.ProcessEnv;
28
+ }
29
+ /** Runs a command through the platform shell. */
30
+ export declare const exec: (command: string, options?: ExecOptions) => ShellResult;
20
31
  /**
21
- * Runs a command through the platform shell.
32
+ * Runs an executable with an argument array, without a shell.
22
33
  *
23
- * `silent` decides whether the command's output also reaches this process's
24
- * streams, which is how shelljs' option of the same name behaved.
34
+ * `exec` hands its command to a shell, which re-parses everything in it: a path
35
+ * containing a quote, `$` or `&` changes the command's meaning, and a variable
36
+ * assigned on the command line is no different - on Windows the assignment form
37
+ * does not even quote the value. Arguments and `env` given here are passed to
38
+ * the child as they are, so they are safe whatever they contain.
25
39
  */
26
- export declare const exec: (command: string, options?: {
27
- silent?: boolean;
28
- }) => ShellResult;
40
+ export declare const execFile: (command: string, args: string[], options?: ExecOptions) => ShellResult;
29
41
  /**
30
42
  * Copies the contents of a folder into another folder, matching
31
43
  * `cp -r <source>/. <destination>`.
@@ -1,4 +1,15 @@
1
1
  "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
2
13
  var __values = (this && this.__values) || function(o) {
3
14
  var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
4
15
  if (m) return m.call(o);
@@ -14,28 +25,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
25
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
26
  };
16
27
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.which = exports.ls = exports.rm = exports.cp = exports.exec = void 0;
28
+ exports.which = exports.ls = exports.rm = exports.cp = exports.execFile = exports.exec = void 0;
18
29
  var child_process_1 = require("child_process");
19
30
  var fs_1 = __importDefault(require("fs"));
20
31
  var path_1 = __importDefault(require("path"));
21
32
  var utils_1 = require("@sasjs/utils");
22
33
  /**
23
- * Runs a command through the platform shell.
24
- *
25
- * `silent` decides whether the command's output also reaches this process's
26
- * streams, which is how shelljs' option of the same name behaved.
34
+ * The options both runners share. shelljs allowed 20MB of output; spawnSync's
35
+ * 1MB default kills a chatty command mid-run, and a deploy script can easily be
36
+ * chatty.
27
37
  */
28
- var exec = function (command, options) {
38
+ var spawnOptions = function (options) { return ({
39
+ encoding: 'utf8',
40
+ stdio: ['ignore', 'pipe', 'pipe'],
41
+ maxBuffer: 20 * 1024 * 1024,
42
+ env: options.env ? __assign(__assign({}, process.env), options.env) : undefined
43
+ }); };
44
+ /** The result both runners answer with, from one place. */
45
+ var toShellResult = function (result, options) {
29
46
  var _a, _b, _c;
30
- if (options === void 0) { options = {}; }
31
- var result = (0, child_process_1.spawnSync)(command, {
32
- shell: true,
33
- encoding: 'utf8',
34
- stdio: ['ignore', 'pipe', 'pipe'],
35
- // shelljs allowed 20MB of output; spawnSync's 1MB default kills a chatty
36
- // command mid-run, and a deploy script can easily be chatty.
37
- maxBuffer: 20 * 1024 * 1024
38
- });
39
47
  var stdout = (_a = result.stdout) !== null && _a !== void 0 ? _a : '';
40
48
  var stderr = (_b = result.stderr) !== null && _b !== void 0 ? _b : '';
41
49
  if (!options.silent) {
@@ -56,7 +64,26 @@ var exec = function (command, options) {
56
64
  }
57
65
  return { stdout: stdout, stderr: stderr, code: (_c = result.status) !== null && _c !== void 0 ? _c : 0 };
58
66
  };
67
+ /** Runs a command through the platform shell. */
68
+ var exec = function (command, options) {
69
+ if (options === void 0) { options = {}; }
70
+ return toShellResult((0, child_process_1.spawnSync)(command, __assign(__assign({}, spawnOptions(options)), { shell: true })), options);
71
+ };
59
72
  exports.exec = exec;
73
+ /**
74
+ * Runs an executable with an argument array, without a shell.
75
+ *
76
+ * `exec` hands its command to a shell, which re-parses everything in it: a path
77
+ * containing a quote, `$` or `&` changes the command's meaning, and a variable
78
+ * assigned on the command line is no different - on Windows the assignment form
79
+ * does not even quote the value. Arguments and `env` given here are passed to
80
+ * the child as they are, so they are safe whatever they contain.
81
+ */
82
+ var execFile = function (command, args, options) {
83
+ if (options === void 0) { options = {}; }
84
+ return toShellResult((0, child_process_1.spawnSync)(command, args, spawnOptions(options)), options);
85
+ };
86
+ exports.execFile = execFile;
60
87
  /**
61
88
  * Copies the contents of a folder into another folder, matching
62
89
  * `cp -r <source>/. <destination>`.
@@ -54,6 +54,33 @@ describe('shell', function () {
54
54
  expect(result.stdout.length).toEqual(3 * 1024 * 1024);
55
55
  });
56
56
  });
57
+ describe('execFile', function () {
58
+ // The values below are the point: a shell would interpret each of them -
59
+ // `&` and `;` separate commands, `$(...)` and backticks substitute, `'`
60
+ // closes a quote - and on Windows the `set NAME=value` form does not quote
61
+ // the value at all. Nothing here is parsed, so each arrives intact.
62
+ var metacharacters = "a'b&c;$(echo pwned)`id`|d>e";
63
+ it('should answer with stdout for an executable run with an argument array', function () {
64
+ var result = (0, shell_1.execFile)(process.execPath, ['-e', "process.stdout.write('hello')"], { silent: true });
65
+ expect(result.stdout).toEqual('hello');
66
+ expect(result.code).toEqual(0);
67
+ });
68
+ it('should pass an argument through without the shell interpreting it', function () {
69
+ var result = (0, shell_1.execFile)(process.execPath, ['-e', 'process.stdout.write(process.argv[1])', metacharacters], { silent: true });
70
+ expect(result.stdout).toEqual(metacharacters);
71
+ expect(result.code).toEqual(0);
72
+ });
73
+ it('should pass an environment value through without the shell interpreting it', function () {
74
+ var result = (0, shell_1.execFile)(process.execPath, ['-e', 'process.stdout.write(process.env.PROBE)'], { silent: true, env: { PROBE: metacharacters } });
75
+ expect(result.stdout).toEqual(metacharacters);
76
+ expect(result.code).toEqual(0);
77
+ });
78
+ it('should answer with the exit code and stderr for an executable that fails', function () {
79
+ var result = (0, shell_1.execFile)(process.execPath, ['-e', "process.stderr.write('boom'); process.exit(3)"], { silent: true });
80
+ expect(result.code).toEqual(3);
81
+ expect(result.stderr).toContain('boom');
82
+ });
83
+ });
57
84
  describe('ls', function () {
58
85
  beforeEach(function () {
59
86
  fs_1.default.mkdirSync(path_1.default.join(root, 'sasjs-react-seed-app-main'));
@@ -84,6 +84,32 @@ var setEntryPlatform = function (zipPath, platform) {
84
84
  }
85
85
  fs_1.default.writeFileSync(zipPath, buffer);
86
86
  };
87
+ /**
88
+ * Rewrites an entry name in the archive bytes, in both the local file header
89
+ * and the central directory record.
90
+ *
91
+ * yazl refuses to create an entry whose name is absolute or contains `..`, so
92
+ * a traversal entry has to be patched in after the fact. The replacement must
93
+ * be the same length as the original: both records carry the name length, and
94
+ * changing it would move every offset that follows.
95
+ */
96
+ var replaceEntryName = function (zipPath, from, to) {
97
+ if (from.length !== to.length)
98
+ throw new Error('the replacement name must be the same length');
99
+ var buffer = fs_1.default.readFileSync(zipPath);
100
+ var fromBuffer = Buffer.from(from, 'utf8');
101
+ var toBuffer = Buffer.from(to, 'utf8');
102
+ var index = buffer.indexOf(fromBuffer);
103
+ var replaced = 0;
104
+ while (index !== -1) {
105
+ toBuffer.copy(buffer, index);
106
+ replaced++;
107
+ index = buffer.indexOf(fromBuffer, index + 1);
108
+ }
109
+ if (!replaced)
110
+ throw new Error("entry name not found in the archive: ".concat(from));
111
+ fs_1.default.writeFileSync(zipPath, buffer);
112
+ };
87
113
  describe('zip', function () {
88
114
  var tmpRoot = path_1.default.join(__dirname, "zip-spec-".concat((0, test_1.generateUniqueTimestamp)()));
89
115
  var destDir = path_1.default.join(tmpRoot, 'dest');
@@ -279,5 +305,33 @@ describe('zip', function () {
279
305
  }
280
306
  });
281
307
  }); });
308
+ /**
309
+ * The safety of `path.join(destDir, entry.fileName)` rests on the entry
310
+ * name never containing `..` or a leading slash. That is yauzl's
311
+ * validateFileName, which only runs because the reader is opened with
312
+ * decodeStrings - so the property is pinned here rather than left to the
313
+ * comment that documents it.
314
+ */
315
+ it('should refuse an entry whose name traverses out of the destination', function () { return __awaiter(void 0, void 0, void 0, function () {
316
+ var zipPath;
317
+ return __generator(this, function (_a) {
318
+ switch (_a.label) {
319
+ case 0:
320
+ zipPath = path_1.default.join(tmpRoot, 'traversal.zip');
321
+ return [4 /*yield*/, makeZip([{ name: 'aa/evil.txt', content: 'pwned' }], zipPath)];
322
+ case 1:
323
+ _a.sent();
324
+ replaceEntryName(zipPath, 'aa/evil.txt', '../evil.txt');
325
+ return [4 /*yield*/, expect((0, zip_1.extractZip)(zipPath, destDir)).rejects.toThrow()
326
+ // destDir/../evil.txt resolves to tmpRoot/evil.txt
327
+ ];
328
+ case 2:
329
+ _a.sent();
330
+ // destDir/../evil.txt resolves to tmpRoot/evil.txt
331
+ expect(fs_1.default.existsSync(path_1.default.join(tmpRoot, 'evil.txt'))).toBeFalse();
332
+ return [2 /*return*/];
333
+ }
334
+ });
335
+ }); });
282
336
  });
283
337
  });
package/package.json CHANGED
@@ -69,14 +69,13 @@
69
69
  "registry": "https://registry.npmjs.org"
70
70
  },
71
71
  "dependencies": {
72
- "@sasjs/adapter": "5.1.0",
72
+ "@sasjs/adapter": "5.1.1",
73
73
  "@sasjs/core": "5.5.2",
74
- "@sasjs/lint": "4.1.4",
75
- "@sasjs/utils": "4.0.0",
74
+ "@sasjs/utils": "4.0.1",
76
75
  "axios": "1.20.0",
77
76
  "chalk": "4.1.2",
78
- "dotenv": "17.4.2",
79
- "js-base64": "3.9.3",
77
+ "dotenv": "18.0.8",
78
+ "js-base64": "3.9.4",
80
79
  "jsdom": "26.0.0",
81
80
  "lodash.uniqby": "4.7.0",
82
81
  "node-graphviz": "0.1.1",
@@ -94,6 +93,7 @@
94
93
  "@babel/preset-env": "7.29.7",
95
94
  "@babel/preset-typescript": "7.29.7",
96
95
  "@nogoo9/gitleaks": "8.30.1-post.2",
96
+ "@sasjs/lint": "4.2.0",
97
97
  "@types/debug": "4.1.13",
98
98
  "@types/find": "0.2.4",
99
99
  "@types/jest": "30.0.0",
@@ -105,15 +105,15 @@
105
105
  "@types/xml": "1.0.11",
106
106
  "@types/yauzl": "3.4.0",
107
107
  "@types/yazl": "3.3.1",
108
- "babel-jest": "30.5.1",
108
+ "babel-jest": "30.5.2",
109
109
  "copyfiles": "2.4.1",
110
- "jest": "30.5.1",
111
- "jest-cli": "30.5.1",
112
- "jest-environment-jsdom": "30.5.1",
113
- "jest-extended": "4.0.2",
110
+ "jest": "30.5.2",
111
+ "jest-cli": "30.5.2",
112
+ "jest-environment-jsdom": "30.5.2",
113
+ "jest-extended": "7.0.0",
114
114
  "nodemon": "3.1.14",
115
- "prettier": "3.9.6",
116
- "ts-jest": "29.4.12",
115
+ "prettier": "3.9.10",
116
+ "ts-jest": "29.4.15",
117
117
  "ts-node": "10.9.2",
118
118
  "typescript": "5.9.3",
119
119
  "util": "0.12.5"
@@ -122,5 +122,8 @@
122
122
  "ws": "8.21.0",
123
123
  "nanoid": "3.3.18"
124
124
  },
125
- "version": "5.0.2"
125
+ "peerDependencies": {
126
+ "@sasjs/lint": "^4.1.4"
127
+ },
128
+ "version": "5.1.0"
126
129
  }