@sasjs/cli 6.20.0 → 6.22.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/build/cli.d.ts CHANGED
@@ -1,2 +1,20 @@
1
+ /**
2
+ * The line printed when the arguments name no command the CLI registers.
3
+ *
4
+ * Kept apart from the printing so it can be asserted without standing up a
5
+ * logger, and so the two cases - a typo, and no argument at all - stay
6
+ * distinguishable in a script's output.
7
+ *
8
+ * @param name - the command as typed, empty when none was given
9
+ */
10
+ export declare const unknownCommandMessage: (name: string) => string;
11
+ /**
12
+ * The registered command nearest to what was typed, when one is close enough to
13
+ * be worth naming - so `sasjs lc` finds `logcheck` rather than only saying no.
14
+ *
15
+ * @param name - the command as typed
16
+ * @returns the suggestion, or `undefined` when nothing is close
17
+ */
18
+ export declare const closestCommand: (name: string) => string | undefined;
1
19
  export declare function cli(args: string[]): Promise<void>;
2
20
  export declare function instantiateLogger(): Promise<void>;
package/build/cli.js CHANGED
@@ -36,29 +36,103 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
36
36
  }
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.closestCommand = exports.unknownCommandMessage = void 0;
39
40
  exports.cli = cli;
40
41
  exports.instantiateLogger = instantiateLogger;
41
42
  var logger_1 = require("@sasjs/utils/logger");
42
43
  var utils_1 = require("./utils");
43
44
  var parse_1 = require("./types/command/parse");
45
+ var help_1 = require("./commands/help/help");
46
+ var command_1 = require("./types/command");
47
+ /**
48
+ * The line printed when the arguments name no command the CLI registers.
49
+ *
50
+ * Kept apart from the printing so it can be asserted without standing up a
51
+ * logger, and so the two cases - a typo, and no argument at all - stay
52
+ * distinguishable in a script's output.
53
+ *
54
+ * @param name - the command as typed, empty when none was given
55
+ */
56
+ var unknownCommandMessage = function (name) {
57
+ return name
58
+ ? "Invalid SASjs command: ".concat(name, ".")
59
+ : 'No command given - here is what this CLI can do.';
60
+ };
61
+ exports.unknownCommandMessage = unknownCommandMessage;
62
+ /**
63
+ * The registered command nearest to what was typed, when one is close enough to
64
+ * be worth naming - so `sasjs lc` finds `logcheck` rather than only saying no.
65
+ *
66
+ * @param name - the command as typed
67
+ * @returns the suggestion, or `undefined` when nothing is close
68
+ */
69
+ var closestCommand = function (name) {
70
+ var _a;
71
+ var typed = name.toLowerCase();
72
+ var names = (0, parse_1.commandNames)();
73
+ return (_a = names
74
+ .map(function (candidate) { return ({
75
+ candidate: candidate,
76
+ // A prefix is what the user was reaching for, so it outranks a short
77
+ // edit distance: `logc` means `logcheck`, not `doc`.
78
+ prefix: candidate.toLowerCase().startsWith(typed),
79
+ distance: editDistance(typed, candidate.toLowerCase())
80
+ }); })
81
+ .filter(function (_a) {
82
+ var prefix = _a.prefix, distance = _a.distance;
83
+ return prefix || distance <= 2;
84
+ })
85
+ .sort(function (a, b) { return Number(b.prefix) - Number(a.prefix) || a.distance - b.distance; })[0]) === null || _a === void 0 ? void 0 : _a.candidate;
86
+ };
87
+ exports.closestCommand = closestCommand;
88
+ var editDistance = function (a, b) {
89
+ var rows = Array.from({ length: a.length + 1 }, function (_, i) { return [i]; });
90
+ for (var j = 1; j <= b.length; j++)
91
+ rows[0][j] = j;
92
+ for (var i = 1; i <= a.length; i++) {
93
+ for (var j = 1; j <= b.length; j++) {
94
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
95
+ }
96
+ }
97
+ return rows[a.length][b.length];
98
+ };
44
99
  function cli(args) {
45
100
  return __awaiter(this, void 0, void 0, function () {
46
- var command, returnCode;
47
- return __generator(this, function (_a) {
48
- switch (_a.label) {
101
+ var command, name_1, suggestion, returnCode;
102
+ var _a, _b, _c, _d;
103
+ return __generator(this, function (_e) {
104
+ switch (_e.label) {
49
105
  case 0: return [4 /*yield*/, (0, utils_1.setProjectDir)(args)];
50
106
  case 1:
51
- _a.sent();
107
+ _e.sent();
52
108
  return [4 /*yield*/, instantiateLogger()];
53
109
  case 2:
54
- _a.sent();
110
+ _e.sent();
55
111
  return [4 /*yield*/, (0, utils_1.loadProjectEnvVariables)()];
56
112
  case 3:
57
- _a.sent();
113
+ _e.sent();
58
114
  command = (0, parse_1.parse)(args);
59
- return [4 /*yield*/, command.execute()];
115
+ if (!!command) return [3 /*break*/, 6];
116
+ name_1 = (0, parse_1.requestedCommandName)(args);
117
+ (_a = process.logger) === null || _a === void 0 ? void 0 : _a.error((0, exports.unknownCommandMessage)(name_1));
118
+ if (!!name_1) return [3 /*break*/, 5];
119
+ return [4 /*yield*/, (0, help_1.printHelpText)()];
60
120
  case 4:
61
- returnCode = _a.sent();
121
+ _e.sent();
122
+ process.exit(command_1.ReturnCode.InvalidCommand);
123
+ return [2 /*return*/];
124
+ case 5:
125
+ suggestion = (0, exports.closestCommand)(name_1);
126
+ if (suggestion) {
127
+ (_b = process.logger) === null || _b === void 0 ? void 0 : _b.info("Did you mean `sasjs ".concat(suggestion, "`?"));
128
+ }
129
+ (_c = process.logger) === null || _c === void 0 ? void 0 : _c.info("Commands: ".concat((0, parse_1.commandNames)().sort().join(', ')));
130
+ (_d = process.logger) === null || _d === void 0 ? void 0 : _d.info('Run `sasjs help` for the full text, or `sasjs <command> --help` for one command.');
131
+ process.exit(command_1.ReturnCode.InvalidCommand);
132
+ return [2 /*return*/];
133
+ case 6: return [4 /*yield*/, command.execute()];
134
+ case 7:
135
+ returnCode = _e.sent();
62
136
  process.exit(returnCode);
63
137
  return [2 /*return*/];
64
138
  }
package/build/cli.spec.js CHANGED
@@ -89,6 +89,7 @@ var test_1 = require("./utils/test");
89
89
  var envVariablesModule = __importStar(require("./utils/loadEnvVariables"));
90
90
  var projectDirModule = __importStar(require("./utils/setProjectDir"));
91
91
  var parseModule = __importStar(require("./types/command/parse"));
92
+ var helpModule = __importStar(require("./commands/help/help"));
92
93
  var utils_1 = require("@sasjs/utils");
93
94
  var commandBase_1 = require("./types/command/commandBase");
94
95
  var command_1 = require("./types/command");
@@ -163,6 +164,81 @@ describe('CLI command parsing', function () {
163
164
  }
164
165
  });
165
166
  }); });
167
+ describe('when the arguments name no command', function () {
168
+ var helpSpy = function () {
169
+ return jest
170
+ .spyOn(helpModule, 'printHelpText')
171
+ .mockImplementation(function () {
172
+ return Promise.resolve({ outputCommands: '', outputAliases: '' });
173
+ });
174
+ };
175
+ it('should print the whole help when no command is given', function () { return __awaiter(void 0, void 0, void 0, function () {
176
+ var help;
177
+ return __generator(this, function (_a) {
178
+ switch (_a.label) {
179
+ case 0:
180
+ jest.spyOn(parseModule, 'parse').mockImplementation(function () { return undefined; });
181
+ jest
182
+ .spyOn(parseModule, 'requestedCommandName')
183
+ .mockImplementation(function () { return ''; });
184
+ jest.spyOn(parseModule, 'commandNames').mockImplementation(function () { return ['run']; });
185
+ help = helpSpy();
186
+ return [4 /*yield*/, (0, cli_1.cli)(['node', 'sasjs'])];
187
+ case 1:
188
+ _a.sent();
189
+ expect(help).toHaveBeenCalledTimes(1);
190
+ expect(process.exit).toHaveBeenCalledWith(command_1.ReturnCode.InvalidCommand);
191
+ return [2 /*return*/];
192
+ }
193
+ });
194
+ }); });
195
+ it('should list the commands when the name is unknown', function () { return __awaiter(void 0, void 0, void 0, function () {
196
+ var help;
197
+ return __generator(this, function (_a) {
198
+ switch (_a.label) {
199
+ case 0:
200
+ jest.spyOn(parseModule, 'parse').mockImplementation(function () { return undefined; });
201
+ jest
202
+ .spyOn(parseModule, 'requestedCommandName')
203
+ .mockImplementation(function () { return 'frobnicate'; });
204
+ jest
205
+ .spyOn(parseModule, 'commandNames')
206
+ .mockImplementation(function () { return ['run', 'lint']; });
207
+ help = helpSpy();
208
+ return [4 /*yield*/, (0, cli_1.cli)(['node', 'sasjs', 'frobnicate'])
209
+ // The full text is for `sasjs` on its own; a typo gets the list, which is
210
+ // the part that answers it, rather than two hundred lines.
211
+ ];
212
+ case 1:
213
+ _a.sent();
214
+ // The full text is for `sasjs` on its own; a typo gets the list, which is
215
+ // the part that answers it, rather than two hundred lines.
216
+ expect(help).not.toHaveBeenCalled();
217
+ expect(parseModule.commandNames).toHaveBeenCalled();
218
+ expect(process.exit).toHaveBeenCalledWith(command_1.ReturnCode.InvalidCommand);
219
+ return [2 /*return*/];
220
+ }
221
+ });
222
+ }); });
223
+ it('should not execute anything', function () { return __awaiter(void 0, void 0, void 0, function () {
224
+ return __generator(this, function (_a) {
225
+ switch (_a.label) {
226
+ case 0:
227
+ jest.spyOn(parseModule, 'parse').mockImplementation(function () { return undefined; });
228
+ jest
229
+ .spyOn(parseModule, 'requestedCommandName')
230
+ .mockImplementation(function () { return 'x'; });
231
+ jest.spyOn(parseModule, 'commandNames').mockImplementation(function () { return []; });
232
+ helpSpy();
233
+ return [4 /*yield*/, (0, cli_1.cli)(['node', 'sasjs', 'x'])];
234
+ case 1:
235
+ _a.sent();
236
+ expect(mockCommand.execute).not.toHaveBeenCalled();
237
+ return [2 /*return*/];
238
+ }
239
+ });
240
+ }); });
241
+ });
166
242
  it('should return with the correct exit code on errors', function () { return __awaiter(void 0, void 0, void 0, function () {
167
243
  return __generator(this, function (_a) {
168
244
  switch (_a.label) {
@@ -179,6 +255,25 @@ describe('CLI command parsing', function () {
179
255
  });
180
256
  }); });
181
257
  });
258
+ describe('closestCommand', function () {
259
+ it('suggests a command one letter away', function () {
260
+ expect((0, cli_1.closestCommand)('logchek')).toEqual('logcheck');
261
+ });
262
+ it('suggests the command a prefix belongs to', function () {
263
+ expect((0, cli_1.closestCommand)('logc')).toEqual('logcheck');
264
+ });
265
+ it('suggests nothing when the name is unrelated', function () {
266
+ expect((0, cli_1.closestCommand)('frobnicate')).toBeUndefined();
267
+ });
268
+ });
269
+ describe('unknownCommandMessage', function () {
270
+ it('names the command that was typed', function () {
271
+ expect((0, cli_1.unknownCommandMessage)('frobnicate')).toEqual('Invalid SASjs command: frobnicate.');
272
+ });
273
+ it('says no command was given when there is none', function () {
274
+ expect((0, cli_1.unknownCommandMessage)('')).toEqual('No command given - here is what this CLI can do.');
275
+ });
276
+ });
182
277
  var setupMocks = function () {
183
278
  jest.resetAllMocks();
184
279
  (0, test_1.mockProcessExit)();
@@ -191,5 +286,13 @@ var setupMocks = function () {
191
286
  .spyOn(projectDirModule, 'setProjectDir')
192
287
  .mockImplementation(function () { return Promise.resolve(); });
193
288
  jest.spyOn(parseModule, 'parse').mockImplementation(function () { return mockCommand; });
289
+ jest
290
+ .spyOn(parseModule, 'requestedCommandName')
291
+ .mockImplementation(function () { return 'mock'; });
292
+ jest
293
+ .spyOn(helpModule, 'printHelpText')
294
+ .mockImplementation(function () {
295
+ return Promise.resolve({ outputCommands: '', outputAliases: '' });
296
+ });
194
297
  jest.spyOn(mockCommand, 'execute');
195
298
  };
@@ -209,6 +209,8 @@ function printHelpText() {
209
209
  description: [
210
210
  "lets the user add a build target to either the local project configuration, or to the global .sasjsrc file.",
211
211
  "",
212
+ "[2spaces]NOTE: Providing insecure flag (--insecure or -i) is optional. It allows the command to bypass the HTTPS requirement, and is not recommended.",
213
+ "",
212
214
  "NOTE: This command requires the user to input all the required parameters. More information available in the online documentation: ".concat(chalk_1.default.cyanBright('https://sasjs.io/sasjs-cli-add'))
213
215
  ]
214
216
  },
@@ -220,6 +222,9 @@ function printHelpText() {
220
222
  "With the 'login' sub command (SASVIYA targets only), authentication is performed with a SAS username and password - no OAuth client/secret required - and the resulting tokens are saved.",
221
223
  "Without a sub command, behaves like 'sasjs add cred' (client/secret based authentication).",
222
224
  "",
225
+ "[2spaces]NOTE: Providing insecure flag (--insecure or -i) is optional. It allows the command to bypass the HTTPS requirement, and is not recommended.",
226
+ "[2spaces]NOTE: Providing password-stdin flag (--passwordStdin) is optional. The password is read from stdin instead of being prompted for, which is how a pipeline passes it without a terminal.",
227
+ "",
223
228
  "NOTE: This command requires the user to input all the required parameters. More information available in the online documentation: ".concat(chalk_1.default.cyanBright('https://cli.sasjs.io/auth/'))
224
229
  ]
225
230
  },
@@ -231,7 +236,8 @@ function printHelpText() {
231
236
  "The target can exist either in the local project configuration or in the global .sasjsrc file.",
232
237
  "Providing log flag (--log or -l) is optional. If not present, the log is stored locally with a timestamp. If present, CLI will fetch and save the log to the specified location. If a relative location, it will be relative to the directory in which the command is invoked.",
233
238
  "Providing source flag (--source or -s) is optional. It should point to a JSON file with the following structure: {\"macroVars\": {\"var1\": \"val1\", \"var2\": \"val2\"}}. These will be parsed into SAS \"%let\" statements and pre-append to the SAS code being run.",
234
- "Providing compile flag (--compile or -c) is optional. It is used to compile the program prior to execution. Useful for including dependent macros and includes. More info here: https://cli.sasjs.io/compile"
239
+ "Providing compile flag (--compile or -c) is optional. It is used to compile the program prior to execution. Useful for including dependent macros and includes. More info here: https://cli.sasjs.io/compile",
240
+ "Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log this run returned and fails when the logCheck setting in sasjsconfig.json says so. --no-logcheck switches both the report and the verdict off for this run."
235
241
  ]
236
242
  },
237
243
  {
@@ -243,7 +249,9 @@ function printHelpText() {
243
249
  "The target can exist either in the local project configuration or in the global .sasjsrc file.",
244
250
  "<sasProgramPath> - if this has a leading slash (eg /Public/app/folder/servicename) then it must be the full path. If it is a relative path (eg path/servicename) then it will be pre-pended with the appLoc - which must then be defined in the sasjs config.",
245
251
  "<path/to/log> - Location in which to store the log file. If not provided AND current directory is a sasjs project, it will be saved in sasjsresults else in the current directory",
246
- "<path/to/output> - Location to store the output file. If not provided AND current directory is a sasjs project, an output file will be saved in the sasjsresults folder else in current directory."
252
+ "<path/to/output> - Location to store the output file. If not provided AND current directory is a sasjs project, an output file will be saved in the sasjsresults folder else in current directory.",
253
+ "<path/to/datafile> - (--data or -d) the json file holding the input data passed to the request. --config (-c) takes the json config for the execution itself.",
254
+ "Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log this request returned and fails when the logCheck setting in sasjsconfig.json says so."
247
255
  ]
248
256
  },
249
257
  {
@@ -274,7 +282,8 @@ function printHelpText() {
274
282
  "* ".concat(chalk_1.default.cyanBright('compile'), " - Compiles a SAS program with the contents of a local directory."),
275
283
  "[2spaces]command example: sasjs fs compile /Public/folder --output ./outputProgram",
276
284
  "",
277
- "[2spaces]NOTE: If output flag (-o or --output) is not provided, output will be stored in './fs-compile/{timestamp}' directory relative to working directory."
285
+ "[2spaces]NOTE: If output flag (-o or --output) is not provided, output will be stored in './fs-compile/{timestamp}' directory relative to working directory.",
286
+ "[2spaces]NOTE: Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log the compile returned, and fails when the logCheck setting in sasjsconfig.json says so."
278
287
  ]
279
288
  },
280
289
  {
@@ -290,7 +299,12 @@ function printHelpText() {
290
299
  "[2spaces]NOTE: Providing output flag (--output or -o) is optional. If present, CLI will immediately print out the response JSON. If value is provided, it will be treated as file path to save the response JSON.",
291
300
  "[2spaces]NOTE: Providing log flag (--log or -l) is optional. If present, CLI will fetch and save job log to local file.",
292
301
  "[2spaces]NOTE: Providing ignore warnings (--ignoreWarnings or -i) flag is optional. If present, CLI will return status '0', when the job state is warning.",
293
- "[2spaces]NOTE: Providing verbose (--verbose or -v) flag is optional. If present, CLI will log summary of every HTTP response. If set to 'bleached', CLI will log summary of every HTTP response without extra colors."
302
+ "[2spaces]NOTE: Providing verbose (--verbose or -v) flag is optional. If present, CLI will log summary of every HTTP response. If set to 'bleached', CLI will log summary of every HTTP response without extra colors.",
303
+ "[2spaces]NOTE: Providing source flag (--source or -s) is optional. It points to a JSON file of job variables, of the form {\"macroVars\": {\"var1\": \"val1\"}}.",
304
+ "[2spaces]NOTE: Providing returnStatusOnly (--returnStatusOnly or -r) is deprecated and has no effect: passing it prints a deprecation warning. The job status is returned with --verbose.",
305
+ "[2spaces]NOTE: Providing statusFile flag (--statusFile) is optional. The job status is fetched and saved to the given path; with no path it is printed to the console.",
306
+ "[2spaces]NOTE: Providing streamLog flag (--streamLog) is optional. As the job runs, its log is streamed into the local file given here, rather than fetched once at the end.",
307
+ "[2spaces]NOTE: Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log the job returned, and fails when the logCheck setting in sasjsconfig.json says so."
294
308
  ]
295
309
  },
296
310
  {
@@ -299,7 +313,10 @@ function printHelpText() {
299
313
  description: [
300
314
  "Performs operations on flows of jobs.",
301
315
  "[2spaces]* ".concat(chalk_1.default.cyanBright('execute'), " - triggers flow for execution."),
302
- "[2spaces]command example: sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv"
316
+ "[2spaces]command example: sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv",
317
+ "",
318
+ "[2spaces]NOTE: Providing streamLog flag (--streamLog) is optional. As each job in the flow runs, its log is streamed into the log folder, rather than fetched once the job finishes.",
319
+ "[2spaces]NOTE: Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log of every job in the flow, and fails when the logCheck setting in sasjsconfig.json says so. A log streamed with --streamLog goes straight to its file and is not checked."
303
320
  ]
304
321
  },
305
322
  {
@@ -323,7 +340,8 @@ function printHelpText() {
323
340
  name: 'lint',
324
341
  title: 'lint',
325
342
  description: [
326
- "Provides the capability to identify, for SAS file, whether there are any ERRORs or WARNINGs present and if so, which line number they are on."
343
+ "Provides the capability to identify, for SAS file, whether there are any ERRORs or WARNINGs present and if so, which line number they are on.",
344
+ "[2spaces]command example: sasjs lint --fail-on warning"
327
345
  ]
328
346
  },
329
347
  {
@@ -332,7 +350,11 @@ function printHelpText() {
332
350
  description: [
333
351
  "Checks SAS logs for the messages SAS printed - ERRORs, WARNINGs and the notes that flag a problem - and fails when the failure policy is broken.",
334
352
  "[2spaces]With no path, the project's results folder is checked. No target, no login and no project are needed, so a log produced elsewhere checks the same way.",
335
- "[2spaces]command example: sasjs logcheck --fail-on warning --format json"
353
+ "[2spaces]command example: sasjs logcheck --fail-on warning --format json",
354
+ "",
355
+ "[2spaces]Flags: --fail-on error|warning|note|never (defaults to logCheck.failOn in sasjsconfig.json, else error), --max-warnings <n>, --ignore-code <49-169>, --ignore-path <path segment|glob>, --format human|json|annotations, --json, --verbose.",
356
+ "[2spaces]The same settings live in the logCheck section of sasjsconfig.json: --ignore-code and --ignore-path add to what the file lists, while --fail-on and --max-warnings replace the file's value when they are given.",
357
+ "[2spaces]The exit code is 5 when the policy is broken, which is distinct from lint's 3, so a pipeline can tell a run SAS complained about from code-quality failures."
336
358
  ]
337
359
  },
338
360
  {
@@ -50,6 +50,31 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
50
50
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
51
  }
52
52
  };
53
+ var __read = (this && this.__read) || function (o, n) {
54
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
55
+ if (!m) return o;
56
+ var i = m.call(o), r, ar = [], e;
57
+ try {
58
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
59
+ }
60
+ catch (error) { e = { error: error }; }
61
+ finally {
62
+ try {
63
+ if (r && !r.done && (m = i["return"])) m.call(i);
64
+ }
65
+ finally { if (e) throw e.error; }
66
+ }
67
+ return ar;
68
+ };
69
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
70
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
71
+ if (ar || !(i in from)) {
72
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
73
+ ar[i] = from[i];
74
+ }
75
+ }
76
+ return to.concat(ar || Array.prototype.slice.call(from));
77
+ };
53
78
  Object.defineProperty(exports, "__esModule", { value: true });
54
79
  exports.LintCommand = void 0;
55
80
  var initLint_1 = require("./initLint");
@@ -57,15 +82,35 @@ var processLint_1 = require("./processLint");
57
82
  var command_1 = require("../../types/command");
58
83
  var types_1 = require("../../types");
59
84
  var utils_1 = require("../../utils");
85
+ var subCommands = ['init', 'fix', 'find'];
86
+ var failOnValues = ['error', 'warning'];
60
87
  var syntax = 'lint <subCommand> [filterStrings..]';
61
- var usage = 'sasjs lint [ find | fix | init ]';
88
+ var usage = 'sasjs lint [ find | fix | init ] [--fail-on error|warning]';
62
89
  var description = "Provides the capability to identify, for SAS file, whether there are any ERRORs or WARNINGs present and if so, which line number they are on.";
90
+ /**
91
+ * Inserts the default `find` subcommand unless the first argument is already a
92
+ * subcommand, so `sasjs lint --fail-on warning` and `sasjs lint src/app.sas`
93
+ * both report rather than fail to parse. The check is made on the arguments
94
+ * rather than on their number, because an option and its value count as
95
+ * arguments and used to push the subcommand out of position.
96
+ */
97
+ var withDefaultSubCommand = function (args) {
98
+ var firstPositional = args.slice(3).find(function (arg) { return !arg.startsWith('-'); });
99
+ return firstPositional && subCommands.includes(firstPositional)
100
+ ? args
101
+ : __spreadArray(__spreadArray(__spreadArray([], __read(args.slice(0, 3)), false), ['find'], false), __read(args.slice(3)), false);
102
+ };
63
103
  var examples = [
64
104
  {
65
105
  command: 'sasjs lint',
66
106
  description: 'Lints all .sas files in the current project.\n' +
67
107
  'All the exceptions (along with line / column numbers) will be shown in the console.'
68
108
  },
109
+ {
110
+ command: 'sasjs lint --fail-on warning',
111
+ description: 'Lints all .sas files in the current project and fails on a warning as well as an error.\n' +
112
+ 'The same can be set for every run by adding `"default": "error"` to `severityLevel` in `.sasjslint`.'
113
+ },
69
114
  {
70
115
  command: 'sasjs lint find mf_a mf_b',
71
116
  description: 'Lints all .sas files starting from mf_a or mf_b in the current project.\n' +
@@ -109,23 +154,32 @@ var examples = [
109
154
  var LintCommand = /** @class */ (function (_super) {
110
155
  __extends(LintCommand, _super);
111
156
  function LintCommand(args) {
112
- if (args.length < 4) {
113
- args.splice(3, 0, 'find');
114
- }
115
- return _super.call(this, args, {
157
+ return _super.call(this, withDefaultSubCommand(args), {
116
158
  syntax: syntax,
117
159
  usage: usage,
118
160
  description: description,
119
- examples: examples
161
+ examples: examples,
162
+ parseOptions: {
163
+ 'fail-on': {
164
+ type: 'string',
165
+ description: "Lowest severity that fails the command: ".concat(failOnValues.join(', '), ". Defaults to error, so a warning is reported without failing. Applies to the report run, not to lint fix.")
166
+ }
167
+ }
120
168
  }) || this;
121
169
  }
122
170
  LintCommand.prototype.execute = function () {
123
171
  return __awaiter(this, void 0, void 0, function () {
124
- var _a, subCommand, filterStrings;
125
- return __generator(this, function (_b) {
126
- switch (_b.label) {
172
+ var _a, subCommand, filterStrings, failOn, failOnSeverity;
173
+ var _b;
174
+ return __generator(this, function (_c) {
175
+ switch (_c.label) {
127
176
  case 0:
128
- _a = this.parsed, subCommand = _a.subCommand, filterStrings = _a.filterStrings;
177
+ _a = this.parsed, subCommand = _a.subCommand, filterStrings = _a.filterStrings, failOn = _a.failOn;
178
+ if (failOn !== undefined && !failOnValues.includes(failOn)) {
179
+ (0, utils_1.displayError)(new Error("Invalid value for --fail-on: ".concat(failOn, ". Use one of ").concat(failOnValues.join(', '), ".")));
180
+ return [2 /*return*/, command_1.ReturnCode.InvalidCommand];
181
+ }
182
+ failOnSeverity = (_b = failOn) !== null && _b !== void 0 ? _b : 'error';
129
183
  if (!(subCommand === 'init')) return [3 /*break*/, 2];
130
184
  return [4 /*yield*/, (0, initLint_1.initLint)()
131
185
  .then(function (res) {
@@ -139,7 +193,7 @@ var LintCommand = /** @class */ (function (_super) {
139
193
  (0, utils_1.displayError)(err, 'An error has occurred whilst initialising SASjs Lint.');
140
194
  return command_1.ReturnCode.InternalError;
141
195
  })];
142
- case 1: return [2 /*return*/, _b.sent()];
196
+ case 1: return [2 /*return*/, _c.sent()];
143
197
  case 2:
144
198
  if (!(subCommand === 'fix')) return [3 /*break*/, 4];
145
199
  return [4 /*yield*/, (0, processLint_1.lintFix)(filterStrings)
@@ -148,13 +202,20 @@ var LintCommand = /** @class */ (function (_super) {
148
202
  (0, utils_1.displayError)(err, 'An error has occurred while running SASjs Lint Fix.');
149
203
  return command_1.ReturnCode.LintError;
150
204
  })];
151
- case 3: return [2 /*return*/, _b.sent()];
205
+ case 3: return [2 /*return*/, _c.sent()];
152
206
  case 4: return [4 /*yield*/, (0, processLint_1.processLint)(filterStrings)
153
207
  .then(function (result) {
154
208
  if (result.errors) {
155
209
  (0, utils_1.displayError)('Please fix the identified lint errors.');
156
210
  return command_1.ReturnCode.LintError;
157
211
  }
212
+ if (result.warnings && failOnSeverity === 'warning') {
213
+ var count = result.warningCount;
214
+ (0, utils_1.displayError)(count === 1
215
+ ? '1 lint warning treated as an error because of --fail-on warning. Please fix it.'
216
+ : "".concat(count, " lint warnings treated as errors because of --fail-on warning. Please fix them."));
217
+ return command_1.ReturnCode.LintError;
218
+ }
158
219
  if (result.warnings) {
159
220
  process.logger.warn('Please fix the identified lint warnings.');
160
221
  return command_1.ReturnCode.Success;
@@ -166,7 +227,7 @@ var LintCommand = /** @class */ (function (_super) {
166
227
  (0, utils_1.displayError)(err, 'An error has occurred when processing lint operation.');
167
228
  return command_1.ReturnCode.InternalError;
168
229
  })];
169
- case 5: return [2 /*return*/, _b.sent()];
230
+ case 5: return [2 /*return*/, _c.sent()];
170
231
  }
171
232
  });
172
233
  });
@@ -1,6 +1,10 @@
1
1
  interface LintResult {
2
2
  warnings: boolean;
3
3
  errors: boolean;
4
+ /** Number of WARNING diagnostics found, across every linted file. */
5
+ warningCount: number;
6
+ /** Number of ERROR diagnostics found, across every linted file. */
7
+ errorCount: number;
4
8
  }
5
9
  /**
6
10
  * Fixes lint violations in all .sas files in the current project if no filterArray is provided
@@ -70,6 +70,12 @@ var path_1 = __importDefault(require("path"));
70
70
  var chalk_1 = __importDefault(require("chalk"));
71
71
  var lint_1 = require("@sasjs/lint");
72
72
  var utils_1 = require("@sasjs/utils");
73
+ /**
74
+ * `sasjs lint` is documented to work without a `sasjsconfig.json`, and loading
75
+ * that file is the only thing that sets `process.projectDir` - so a filter
76
+ * string is resolved against the current directory when no project was loaded.
77
+ */
78
+ var getProjectDir = function () { return process.projectDir || process.cwd(); };
73
79
  /**
74
80
  * Fixes lint violations in all .sas files in the current project if no filterArray is provided
75
81
  * Otherwise fixes lint violations of only those .sas files which matches filter array
@@ -125,7 +131,7 @@ function lintFix() {
125
131
  .map(function (pattern) { return "(".concat(pattern, ")"); })
126
132
  .join('|');
127
133
  regex_1 = new RegExp(regexPattern);
128
- return [4 /*yield*/, (0, utils_1.listSasFilesInFolder)(process.projectDir, true, [
134
+ return [4 /*yield*/, (0, utils_1.listSasFilesInFolder)(getProjectDir(), true, [
129
135
  'node_modules'
130
136
  ])];
131
137
  case 2:
@@ -194,7 +200,12 @@ function processLint() {
194
200
  return __generator(this, function (_a) {
195
201
  switch (_a.label) {
196
202
  case 0:
197
- found = { warnings: false, errors: false };
203
+ found = {
204
+ warnings: false,
205
+ errors: false,
206
+ warningCount: 0,
207
+ errorCount: 0
208
+ };
198
209
  sasjsDiagnosticsMap = new Map();
199
210
  if (!filterArray.length) return [3 /*break*/, 5];
200
211
  absoluteFilePaths = filterArray.filter(function (pattern) {
@@ -226,7 +237,7 @@ function processLint() {
226
237
  .map(function (pattern) { return "(".concat(pattern, ")"); })
227
238
  .join('|');
228
239
  regex_2 = new RegExp(regexPattern);
229
- return [4 /*yield*/, (0, utils_1.listSasFilesInFolder)(process.projectDir, true, [
240
+ return [4 /*yield*/, (0, utils_1.listSasFilesInFolder)(getProjectDir(), true, [
230
241
  'node_modules'
231
242
  ])];
232
243
  case 2:
@@ -237,7 +248,7 @@ function processLint() {
237
248
  return __generator(this, function (_d) {
238
249
  switch (_d.label) {
239
250
  case 0:
240
- filePath = path_1.default.join(process.projectDir, file);
251
+ filePath = path_1.default.join(getProjectDir(), file);
241
252
  _b = (_a = sasjsDiagnosticsMap).set;
242
253
  _c = [filePath];
243
254
  return [4 /*yield*/, (0, lint_1.lintFile)(filePath)];
@@ -258,12 +269,10 @@ function processLint() {
258
269
  _a.label = 7;
259
270
  case 7:
260
271
  sasjsDiagnosticsMap.forEach(function (sasjsDiagnostics, filePath) {
261
- found.warnings =
262
- found.warnings ||
263
- !!sasjsDiagnostics.find(function (d) { return d.severity === lint_1.Severity.Warning; });
264
- found.errors =
265
- found.errors ||
266
- !!sasjsDiagnostics.find(function (d) { return d.severity === lint_1.Severity.Error; });
272
+ found.warningCount += sasjsDiagnostics.filter(function (d) { return d.severity === lint_1.Severity.Warning; }).length;
273
+ found.errorCount += sasjsDiagnostics.filter(function (d) { return d.severity === lint_1.Severity.Error; }).length;
274
+ found.warnings = found.warnings || found.warningCount > 0;
275
+ found.errors = found.errors || found.errorCount > 0;
267
276
  if (sasjsDiagnostics.length) {
268
277
  displayDiagnostics(filePath, sasjsDiagnostics);
269
278
  }
@@ -138,10 +138,13 @@ describe('LintCommand', function () {
138
138
  switch (_a.label) {
139
139
  case 0:
140
140
  args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint'], false);
141
- jest
142
- .spyOn(processModule, 'processLint')
143
- .mockImplementation(function () {
144
- return Promise.resolve({ warnings: true, errors: false });
141
+ jest.spyOn(processModule, 'processLint').mockImplementation(function () {
142
+ return Promise.resolve({
143
+ warnings: true,
144
+ errors: false,
145
+ warningCount: 3,
146
+ errorCount: 0
147
+ });
145
148
  });
146
149
  command = new lintCommand_1.LintCommand(args);
147
150
  return [4 /*yield*/, command.execute()];
@@ -178,10 +181,13 @@ describe('LintCommand', function () {
178
181
  switch (_a.label) {
179
182
  case 0:
180
183
  args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint'], false);
181
- jest
182
- .spyOn(processModule, 'processLint')
183
- .mockImplementation(function () {
184
- return Promise.resolve({ warnings: false, errors: true });
184
+ jest.spyOn(processModule, 'processLint').mockImplementation(function () {
185
+ return Promise.resolve({
186
+ warnings: false,
187
+ errors: true,
188
+ warningCount: 0,
189
+ errorCount: 1
190
+ });
185
191
  });
186
192
  command = new lintCommand_1.LintCommand(args);
187
193
  return [4 /*yield*/, command.execute()];
@@ -193,6 +199,111 @@ describe('LintCommand', function () {
193
199
  }
194
200
  });
195
201
  }); });
202
+ it('should default the subcommand when --fail-on comes before the filter', function () {
203
+ var args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', '--fail-on', 'warning', 'appinit'], false);
204
+ var command = new lintCommand_1.LintCommand(args);
205
+ expect(command.name).toEqual('lint');
206
+ expect(command.subCommand).toEqual('find');
207
+ });
208
+ it('should default the subcommand when only --fail-on is given', function () {
209
+ var args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', '--fail-on=warning'], false);
210
+ var command = new lintCommand_1.LintCommand(args);
211
+ expect(command.name).toEqual('lint');
212
+ expect(command.subCommand).toEqual('find');
213
+ });
214
+ it('should keep an explicit subcommand ahead of --fail-on', function () {
215
+ var args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', 'find', '--fail-on', 'warning'], false);
216
+ var command = new lintCommand_1.LintCommand(args);
217
+ expect(command.subCommand).toEqual('find');
218
+ });
219
+ it('should fail on warnings when --fail-on warning is passed', function () { return __awaiter(void 0, void 0, void 0, function () {
220
+ var args, command, returnCode;
221
+ return __generator(this, function (_a) {
222
+ switch (_a.label) {
223
+ case 0:
224
+ args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', '--fail-on', 'warning'], false);
225
+ jest.spyOn(processModule, 'processLint').mockImplementation(function () {
226
+ return Promise.resolve({
227
+ warnings: true,
228
+ errors: false,
229
+ warningCount: 2,
230
+ errorCount: 0
231
+ });
232
+ });
233
+ command = new lintCommand_1.LintCommand(args);
234
+ return [4 /*yield*/, command.execute()];
235
+ case 1:
236
+ returnCode = _a.sent();
237
+ expect(returnCode).toEqual(command_1.ReturnCode.LintError);
238
+ expect(process.logger.error).toHaveBeenCalledWith('', '2 lint warnings treated as errors because of --fail-on warning. Please fix them.');
239
+ return [2 /*return*/];
240
+ }
241
+ });
242
+ }); });
243
+ it('should word a single warning as one error', function () { return __awaiter(void 0, void 0, void 0, function () {
244
+ var args, command, returnCode;
245
+ return __generator(this, function (_a) {
246
+ switch (_a.label) {
247
+ case 0:
248
+ args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', '--fail-on', 'warning'], false);
249
+ jest.spyOn(processModule, 'processLint').mockImplementation(function () {
250
+ return Promise.resolve({
251
+ warnings: true,
252
+ errors: false,
253
+ warningCount: 1,
254
+ errorCount: 0
255
+ });
256
+ });
257
+ command = new lintCommand_1.LintCommand(args);
258
+ return [4 /*yield*/, command.execute()];
259
+ case 1:
260
+ returnCode = _a.sent();
261
+ expect(returnCode).toEqual(command_1.ReturnCode.LintError);
262
+ expect(process.logger.error).toHaveBeenCalledWith('', '1 lint warning treated as an error because of --fail-on warning. Please fix it.');
263
+ return [2 /*return*/];
264
+ }
265
+ });
266
+ }); });
267
+ it('should not fail on warnings when --fail-on error is passed', function () { return __awaiter(void 0, void 0, void 0, function () {
268
+ var args, command, returnCode;
269
+ return __generator(this, function (_a) {
270
+ switch (_a.label) {
271
+ case 0:
272
+ args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', '--fail-on', 'error'], false);
273
+ jest.spyOn(processModule, 'processLint').mockImplementation(function () {
274
+ return Promise.resolve({
275
+ warnings: true,
276
+ errors: false,
277
+ warningCount: 2,
278
+ errorCount: 0
279
+ });
280
+ });
281
+ command = new lintCommand_1.LintCommand(args);
282
+ return [4 /*yield*/, command.execute()];
283
+ case 1:
284
+ returnCode = _a.sent();
285
+ expect(returnCode).toEqual(command_1.ReturnCode.Success);
286
+ expect(process.logger.warn).toHaveBeenCalled();
287
+ return [2 /*return*/];
288
+ }
289
+ });
290
+ }); });
291
+ it('should reject an unknown --fail-on value', function () { return __awaiter(void 0, void 0, void 0, function () {
292
+ var args, command, returnCode;
293
+ return __generator(this, function (_a) {
294
+ switch (_a.label) {
295
+ case 0:
296
+ args = __spreadArray(__spreadArray([], __read(defaultArgs), false), ['lint', '--fail-on', 'note'], false);
297
+ command = new lintCommand_1.LintCommand(args);
298
+ return [4 /*yield*/, command.execute()];
299
+ case 1:
300
+ returnCode = _a.sent();
301
+ expect(returnCode).toEqual(command_1.ReturnCode.InvalidCommand);
302
+ expect(processModule.processLint).not.toHaveBeenCalled();
303
+ return [2 /*return*/];
304
+ }
305
+ });
306
+ }); });
196
307
  });
197
308
  describe('LintCommand - fix', function () {
198
309
  beforeEach(function () {
@@ -295,10 +406,13 @@ var setupMocks = function () {
295
406
  jest.resetAllMocks();
296
407
  jest.mock('../processLint');
297
408
  jest.mock('../initLint');
298
- jest
299
- .spyOn(processModule, 'processLint')
300
- .mockImplementation(function () {
301
- return Promise.resolve({ warnings: false, errors: false });
409
+ jest.spyOn(processModule, 'processLint').mockImplementation(function () {
410
+ return Promise.resolve({
411
+ warnings: false,
412
+ errors: false,
413
+ warningCount: 0,
414
+ errorCount: 0
415
+ });
302
416
  });
303
417
  jest
304
418
  .spyOn(processModule, 'lintFix')
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ var fs_1 = __importDefault(require("fs"));
43
+ var os_1 = __importDefault(require("os"));
44
+ var path_1 = __importDefault(require("path"));
45
+ var processLint_1 = require("../processLint");
46
+ /**
47
+ * `sasjs lint` is documented to run without a `sasjsconfig.json`, and loading
48
+ * that file is the only thing that sets `process.projectDir` - so these cases
49
+ * run with it empty, the way a project directory without a config does.
50
+ */
51
+ describe('processLint', function () {
52
+ var originalCwd = process.cwd();
53
+ var tmpDir;
54
+ beforeEach(function () {
55
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'sasjs-lint-'));
56
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'trailing.sas'), "%put 'hello'; \n");
57
+ process.chdir(tmpDir);
58
+ process.projectDir = '';
59
+ });
60
+ afterEach(function () {
61
+ process.chdir(originalCwd);
62
+ process.projectDir = '';
63
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
64
+ });
65
+ it('should resolve a filter string against the current directory when no project is loaded', function () { return __awaiter(void 0, void 0, void 0, function () {
66
+ var result;
67
+ return __generator(this, function (_a) {
68
+ switch (_a.label) {
69
+ case 0: return [4 /*yield*/, (0, processLint_1.processLint)(['trailing'])];
70
+ case 1:
71
+ result = _a.sent();
72
+ expect(result.warnings).toEqual(true);
73
+ expect(result.warningCount).toBeGreaterThan(0);
74
+ expect(result.errors).toEqual(false);
75
+ expect(result.errorCount).toEqual(0);
76
+ return [2 /*return*/];
77
+ }
78
+ });
79
+ }); });
80
+ it('should still accept an absolute path when no project is loaded', function () { return __awaiter(void 0, void 0, void 0, function () {
81
+ var result;
82
+ return __generator(this, function (_a) {
83
+ switch (_a.label) {
84
+ case 0: return [4 /*yield*/, (0, processLint_1.processLint)([path_1.default.join(tmpDir, 'trailing.sas')])];
85
+ case 1:
86
+ result = _a.sent();
87
+ expect(result.warningCount).toBeGreaterThan(0);
88
+ return [2 /*return*/];
89
+ }
90
+ });
91
+ }); });
92
+ it('should report no findings when the filter matches nothing', function () { return __awaiter(void 0, void 0, void 0, function () {
93
+ var result;
94
+ return __generator(this, function (_a) {
95
+ switch (_a.label) {
96
+ case 0: return [4 /*yield*/, (0, processLint_1.processLint)(['no-such-file'])];
97
+ case 1:
98
+ result = _a.sent();
99
+ expect(result).toEqual({
100
+ warnings: false,
101
+ errors: false,
102
+ warningCount: 0,
103
+ errorCount: 0
104
+ });
105
+ return [2 /*return*/];
106
+ }
107
+ });
108
+ }); });
109
+ it('should count errors separately from warnings', function () { return __awaiter(void 0, void 0, void 0, function () {
110
+ var result;
111
+ return __generator(this, function (_a) {
112
+ switch (_a.label) {
113
+ case 0:
114
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'password.sas'), "%put '{SASENC}'; \n");
115
+ return [4 /*yield*/, (0, processLint_1.processLint)(['password'])];
116
+ case 1:
117
+ result = _a.sent();
118
+ expect(result.errorCount).toBe(1);
119
+ expect(result.errors).toEqual(true);
120
+ // The trailing spaces and the missing header are both warnings, so the
121
+ // counts stay independent of the error.
122
+ expect(result.warningCount).toBeGreaterThanOrEqual(1);
123
+ return [2 /*return*/];
124
+ }
125
+ });
126
+ }); });
127
+ });
@@ -115,7 +115,7 @@ var LogCheckCommand = /** @class */ (function (_super) {
115
115
  },
116
116
  'ignore-path': {
117
117
  type: 'array',
118
- description: 'Log path to leave out, as a substring or a glob. Repeatable.'
118
+ description: 'Log path to leave out, as a path segment or a glob. A non-glob entry matches whole segments only, so `test` covers `test/run.log` but not `latest.log`. Repeatable.'
119
119
  },
120
120
  format: {
121
121
  type: 'string',
@@ -32,8 +32,10 @@ export interface LogCheckConfig {
32
32
  */
33
33
  ignoreCodes: string[];
34
34
  /**
35
- * Log paths to leave out, as a plain substring (`sasjsresults/legacy`) or a
35
+ * Log paths to leave out, as a path segment (`sasjsresults/legacy`) or a
36
36
  * glob (`**&#47;build*.log`). Matched against the path shown in the report.
37
+ * A non-glob entry matches whole segments, so `test` covers `test/run.log`
38
+ * and not `latest.log`.
37
39
  */
38
40
  ignorePaths: string[];
39
41
  /**
@@ -1,8 +1,29 @@
1
1
  import { CommandBase } from './commandBase';
2
2
  /**
3
- * Rewrites a version flag in the command position to the `version` command.
3
+ * Rewrites a version or help flag in the command position to the command it
4
+ * stands for.
4
5
  *
5
- * @param args - the process arguments, as `[node, sasjs, ...]`.
6
+ * @param args - the process arguments, as `[node, sasjs, ...]`
6
7
  */
7
8
  export declare const normaliseArgs: (args: string[]) => string[];
8
- export declare const parse: (args: string[]) => CommandBase;
9
+ /**
10
+ * Resolves the arguments to the command that will run them.
11
+ *
12
+ * @param args - the process arguments, as `[node, sasjs, ...]`
13
+ * @returns the command, or `undefined` when the arguments name no command the
14
+ * CLI registers. Printing the help and choosing the exit code is the caller's
15
+ * job, so this stays a pure lookup.
16
+ */
17
+ export declare const parse: (args: string[]) => CommandBase | undefined;
18
+ /**
19
+ * The command name the arguments asked for, as typed - so a message can repeat
20
+ * it back. Empty when no command was given at all.
21
+ *
22
+ * @param args - the process arguments, as `[node, sasjs, ...]`
23
+ */
24
+ export declare const requestedCommandName: (args: string[]) => string;
25
+ /**
26
+ * Every command the CLI registers, so a misspelling can be answered with the
27
+ * list rather than a pointer to another command.
28
+ */
29
+ export declare const commandNames: () => string[];
@@ -25,33 +25,61 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
25
25
  return to.concat(ar || Array.prototype.slice.call(from));
26
26
  };
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.parse = exports.normaliseArgs = void 0;
28
+ exports.commandNames = exports.requestedCommandName = exports.parse = exports.normaliseArgs = void 0;
29
29
  var getBaseCommand_1 = require("../../utils/getBaseCommand");
30
30
  var commandFactory_1 = require("./commandFactory");
31
31
  // `sasjs version`, `sasjs v`, `sasjs -v` and `sasjs --version` all mean the
32
32
  // same thing. yargs reads a leading `-v`/`--version` as an option rather than
33
33
  // a command, and its own version handling is switched off, so the token is
34
- // rewritten to the command name before dispatch.
34
+ // rewritten to the command name before dispatch. The help flags get the same
35
+ // treatment: `sasjs --help` is the first thing most people try, and letting it
36
+ // fall through made the root answer "Invalid SASjs command: --help".
35
37
  var versionFlags = ['-v', '--version'];
38
+ var helpFlags = ['-h', '--help'];
36
39
  /**
37
- * Rewrites a version flag in the command position to the `version` command.
40
+ * Rewrites a version or help flag in the command position to the command it
41
+ * stands for.
38
42
  *
39
- * @param args - the process arguments, as `[node, sasjs, ...]`.
43
+ * @param args - the process arguments, as `[node, sasjs, ...]`
40
44
  */
41
45
  var normaliseArgs = function (args) {
42
46
  var _a = __read(args, 3), command = _a[2];
43
- return versionFlags.includes(command)
44
- ? __spreadArray(__spreadArray(__spreadArray([], __read(args.slice(0, 2)), false), ['version'], false), __read(args.slice(3)), false) : args;
47
+ if (versionFlags.includes(command)) {
48
+ return __spreadArray(__spreadArray(__spreadArray([], __read(args.slice(0, 2)), false), ['version'], false), __read(args.slice(3)), false);
49
+ }
50
+ if (helpFlags.includes(command)) {
51
+ return __spreadArray(__spreadArray(__spreadArray([], __read(args.slice(0, 2)), false), ['help'], false), __read(args.slice(3)), false);
52
+ }
53
+ return args;
45
54
  };
46
55
  exports.normaliseArgs = normaliseArgs;
56
+ /**
57
+ * Resolves the arguments to the command that will run them.
58
+ *
59
+ * @param args - the process arguments, as `[node, sasjs, ...]`
60
+ * @returns the command, or `undefined` when the arguments name no command the
61
+ * CLI registers. Printing the help and choosing the exit code is the caller's
62
+ * job, so this stays a pure lookup.
63
+ */
47
64
  var parse = function (args) {
48
- var _a;
49
65
  var commandArgs = (0, exports.normaliseArgs)(args);
50
66
  var baseCommand = (0, getBaseCommand_1.getBaseCommand)(commandArgs);
51
- if (!Array.from(commandFactory_1.commandFactory.keys()).includes(baseCommand.name)) {
52
- (_a = process.logger) === null || _a === void 0 ? void 0 : _a.error('Invalid SASjs command! Run `sasjs help` for a full list of available commands.');
53
- process.exit(1);
54
- }
55
- return commandFactory_1.commandFactory.get(baseCommand.name)(commandArgs);
67
+ return commandFactory_1.commandFactory.has(baseCommand.name)
68
+ ? commandFactory_1.commandFactory.get(baseCommand.name)(commandArgs)
69
+ : undefined;
56
70
  };
57
71
  exports.parse = parse;
72
+ /**
73
+ * The command name the arguments asked for, as typed - so a message can repeat
74
+ * it back. Empty when no command was given at all.
75
+ *
76
+ * @param args - the process arguments, as `[node, sasjs, ...]`
77
+ */
78
+ var requestedCommandName = function (args) { var _a; return (_a = (0, exports.normaliseArgs)(args)[2]) !== null && _a !== void 0 ? _a : ''; };
79
+ exports.requestedCommandName = requestedCommandName;
80
+ /**
81
+ * Every command the CLI registers, so a misspelling can be answered with the
82
+ * list rather than a pointer to another command.
83
+ */
84
+ var commandNames = function () { return Array.from(commandFactory_1.commandFactory.keys()); };
85
+ exports.commandNames = commandNames;
@@ -8,10 +8,31 @@ describe('parse', function () {
8
8
  expect(command).toBeInstanceOf(versionCommand_1.VersionCommand);
9
9
  expect(command.name).toEqual('version');
10
10
  });
11
+ it.each(['help', '-h', '--help'])('resolves `sasjs %s` to the help command', function (token) {
12
+ var command = (0, parse_1.parse)(['node', 'sasjs', token]);
13
+ expect(command.name).toEqual('help');
14
+ });
11
15
  it('leaves another command as it is', function () {
12
16
  var command = (0, parse_1.parse)(['node', 'sasjs', 'help']);
13
17
  expect(command.name).toEqual('help');
14
18
  });
19
+ it('returns undefined for a command it does not register', function () {
20
+ expect((0, parse_1.parse)(['node', 'sasjs', 'frobnicate'])).toBeUndefined();
21
+ });
22
+ it('returns undefined when no command is given', function () {
23
+ expect((0, parse_1.parse)(['node', 'sasjs'])).toBeUndefined();
24
+ });
25
+ });
26
+ describe('requestedCommandName', function () {
27
+ it('reports the command as typed', function () {
28
+ expect((0, parse_1.requestedCommandName)(['node', 'sasjs', 'frobnicate'])).toEqual('frobnicate');
29
+ });
30
+ it('reports the version command for a version flag', function () {
31
+ expect((0, parse_1.requestedCommandName)(['node', 'sasjs', '--version'])).toEqual('version');
32
+ });
33
+ it('reports an empty name when there is no command', function () {
34
+ expect((0, parse_1.requestedCommandName)(['node', 'sasjs'])).toEqual('');
35
+ });
15
36
  });
16
37
  describe('normaliseArgs', function () {
17
38
  it.each(['-v', '--version'])('rewrites %s in the command position', function (flag) {
@@ -21,6 +42,13 @@ describe('normaliseArgs', function () {
21
42
  'version'
22
43
  ]);
23
44
  });
45
+ it.each(['-h', '--help'])('rewrites %s in the command position to the help command', function (flag) {
46
+ expect((0, parse_1.normaliseArgs)(['node', 'sasjs', flag])).toEqual([
47
+ 'node',
48
+ 'sasjs',
49
+ 'help'
50
+ ]);
51
+ });
24
52
  it('keeps the arguments that follow the flag', function () {
25
53
  expect((0, parse_1.normaliseArgs)(['node', 'sasjs', '--version', 'extra'])).toEqual([
26
54
  'node',
package/package.json CHANGED
@@ -73,7 +73,7 @@
73
73
  "@sasjs/adapter": "5.1.3",
74
74
  "@sasjs/api": "0.23.0",
75
75
  "@sasjs/core": "6.1.1",
76
- "@sasjs/lint": "5.0.1",
76
+ "@sasjs/lint": "^5.1.0",
77
77
  "@sasjs/utils": "4.9.0",
78
78
  "axios": "1.20.0",
79
79
  "chalk": "4.1.2",
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.20.0"
128
+ "version": "6.22.0"
129
129
  }