libdragon 10.7.1 → 10.8.1

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.
@@ -21,6 +21,9 @@ const {
21
21
  const { dockerHostUserParams } = require('./docker-utils');
22
22
  const { installNPMDependencies } = require('./npm-utils');
23
23
 
24
+ /**
25
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
26
+ */
24
27
  const installDependencies = async (libdragonInfo) => {
25
28
  const buildScriptPath = path.join(
26
29
  libdragonInfo.root,
@@ -51,9 +54,8 @@ const installDependencies = async (libdragonInfo) => {
51
54
  /**
52
55
  * Downloads the given docker image. Returns false if the local image is the
53
56
  * same, new image name otherwise.
54
- * @param libdragonInfo
55
- * @param newImageName
56
- * @returns false | string
57
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
58
+ * @param {string} newImageName
57
59
  */
58
60
  const updateImage = async (libdragonInfo, newImageName) => {
59
61
  // Will not take too much time if already have the same
@@ -90,6 +92,9 @@ const updateImage = async (libdragonInfo, newImageName) => {
90
92
  return newImageName;
91
93
  };
92
94
 
95
+ /**
96
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
97
+ */
93
98
  const destroyContainer = async (libdragonInfo) => {
94
99
  if (libdragonInfo.containerId) {
95
100
  await spawnProcess('docker', [
@@ -110,39 +115,50 @@ const destroyContainer = async (libdragonInfo) => {
110
115
  * Invokes host git with provided params. If host does not have git, falls back
111
116
  * to the docker git, with the nix user set to the user running libdragon.
112
117
  */
113
- async function runGitMaybeHost(libdragonInfo, params) {
118
+
119
+ /**
120
+ *
121
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
122
+ * @param {string[]} params
123
+ * @param {import('../helpers').SpawnOptions} options
124
+ * @returns
125
+ */
126
+ async function runGitMaybeHost(libdragonInfo, params, options = {}) {
114
127
  assert(
115
128
  libdragonInfo.vendorStrategy !== 'manual',
116
129
  new Error('Should never run git if vendoring strategy is manual.')
117
130
  );
118
131
  try {
119
132
  const isWin = /^win/.test(process.platform);
120
- await spawnProcess(
133
+ return await spawnProcess(
121
134
  'git',
122
135
  ['-C', libdragonInfo.root, ...params],
123
136
  // Windows git is breaking the TTY somehow - disable TTY for now
124
137
  // We are not able to display progress for the initial clone b/c of this
125
138
  // Enable progress otherwise.
126
139
  isWin
127
- ? { inheritStdin: false }
128
- : { inheritStdout: true, inheritStderr: true }
140
+ ? { inheritStdin: false, ...options }
141
+ : { inheritStdout: true, inheritStderr: true, ...options }
129
142
  );
130
143
  } catch (e) {
131
144
  if (e instanceof CommandError) {
132
145
  throw e;
133
146
  }
134
147
 
135
- await dockerExec(
148
+ return await dockerExec(
136
149
  libdragonInfo,
137
150
  // Use the host user when initializing git as we will need access
138
151
  [...dockerHostUserParams(libdragonInfo)],
139
152
  ['git', ...params],
140
153
  // Let's enable tty here to show the progress
141
- { inheritStdout: true, inheritStderr: true }
154
+ { inheritStdout: true, inheritStderr: true, ...options }
142
155
  );
143
156
  }
144
157
  }
145
158
 
159
+ /**
160
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
161
+ */
146
162
  async function checkContainerAndClean(libdragonInfo) {
147
163
  const id =
148
164
  libdragonInfo.containerId &&
@@ -169,6 +185,9 @@ async function checkContainerAndClean(libdragonInfo) {
169
185
  return id ? libdragonInfo.containerId : undefined;
170
186
  }
171
187
 
188
+ /**
189
+ * @param {string} containerId
190
+ */
172
191
  async function checkContainerRunning(containerId) {
173
192
  const running = (
174
193
  await spawnProcess('docker', [
@@ -181,6 +200,9 @@ async function checkContainerRunning(containerId) {
181
200
  return running ? containerId : undefined;
182
201
  }
183
202
 
203
+ /**
204
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
205
+ */
184
206
  async function initGitAndCacheContainerId(libdragonInfo) {
185
207
  // If there is managed vendoring, make sure we have a git repo. `git init` is
186
208
  // safe anyways...
@@ -5,13 +5,13 @@ const printVersion = async () => {
5
5
  log(`libdragon-cli v${version}`);
6
6
  };
7
7
 
8
- module.exports = {
8
+ module.exports = /** @type {const} */ ({
9
9
  name: 'version',
10
10
  fn: printVersion,
11
- mustHaveProject: false,
11
+ forwardsRestParams: false,
12
12
  usage: {
13
13
  name: 'version',
14
14
  summary: 'Display cli version.',
15
15
  description: `Displays currently running cli version.`,
16
16
  },
17
- };
17
+ });
@@ -1,4 +1,4 @@
1
- module.exports = {
1
+ module.exports = /** @type {const} */ ({
2
2
  DOCKER_HUB_IMAGE: 'ghcr.io/dragonminded/libdragon:latest',
3
3
  LIBDRAGON_GIT: 'https://github.com/DragonMinded/libdragon',
4
4
  LIBDRAGON_BRANCH: 'trunk',
@@ -9,6 +9,8 @@ module.exports = {
9
9
  CONFIG_FILE: 'config.json',
10
10
  DEFAULT_STRATEGY: 'submodule',
11
11
 
12
+ ACCEPTED_STRATEGIES: ['submodule', 'subtree', 'manual'],
13
+
12
14
  // cli exit codes
13
15
  STATUS_OK: 0,
14
16
  STATUS_ERROR: 1,
@@ -16,4 +18,4 @@ module.exports = {
16
18
  STATUS_VALIDATION_ERROR: 4,
17
19
 
18
20
  IMAGE_FILE: 'docker-image', // deprecated
19
- };
21
+ });
@@ -6,31 +6,86 @@ const { spawn } = require('child_process');
6
6
 
7
7
  const { globals } = require('./globals');
8
8
 
9
- // An error caused by a command explicitly run by the user
9
+ /**
10
+ * A structure to keep additional error information
11
+ * @typedef {Object} ErrorState
12
+ * @property {number} code
13
+ * @property {string} out
14
+ * @property {boolean} userCommand
15
+ */
16
+
17
+ /**
18
+ * An error caused by a command exiting with a non-zero exit code
19
+ * @class
20
+ */
10
21
  class CommandError extends Error {
22
+ /**
23
+ * @param {string} message Error message to report
24
+ * @param {ErrorState} errorState
25
+ */
11
26
  constructor(message, { code, out, userCommand }) {
12
27
  super(message);
28
+
29
+ /**
30
+ * Exit code
31
+ * @type {number}
32
+ * @public
33
+ */
13
34
  this.code = code;
35
+
36
+ /**
37
+ * Error output as a single concatanated string
38
+ * @type {string}
39
+ * @public
40
+ */
14
41
  this.out = out;
42
+
43
+ /**
44
+ * true when the error caused by a command explicitly run by the end user
45
+ * @type {boolean}
46
+ * @public
47
+ */
15
48
  this.userCommand = userCommand;
16
49
  }
17
50
  }
18
51
 
19
- // The user provided an unexpected input
52
+ /**
53
+ * An error caused by the user providing an unexpected input
54
+ * @class
55
+ */
20
56
  class ParameterError extends Error {
57
+ /**
58
+ * @param {string} message Error message to report
59
+ * @param {string} actionName
60
+ */
21
61
  constructor(message, actionName) {
22
62
  super(message);
63
+
64
+ /**
65
+ * true when the error caused by a command explicitly run by the end user
66
+ * @type {string}
67
+ * @public
68
+ */
23
69
  this.actionName = actionName;
24
70
  }
25
71
  }
26
72
 
27
- // Something was not as expected to continue the operation
73
+ /**
74
+ * Something was not as expected to continue the operation
75
+ * @class
76
+ */
28
77
  class ValidationError extends Error {
78
+ /**
79
+ * @param {string} message
80
+ */
29
81
  constructor(message) {
30
82
  super(message);
31
83
  }
32
84
  }
33
85
 
86
+ /**
87
+ * @param {string} path
88
+ */
34
89
  async function fileExists(path) {
35
90
  return fs
36
91
  .stat(path)
@@ -41,6 +96,9 @@ async function fileExists(path) {
41
96
  });
42
97
  }
43
98
 
99
+ /**
100
+ * @param {string} path
101
+ */
44
102
  async function dirExists(path) {
45
103
  return fs
46
104
  .stat(path)
@@ -51,9 +109,27 @@ async function dirExists(path) {
51
109
  });
52
110
  }
53
111
 
112
+ /**
113
+ * @typedef {{
114
+ * userCommand?: boolean,
115
+ * inheritStdin?: boolean,
116
+ * inheritStdout?: boolean,
117
+ * inheritStderr?: boolean,
118
+ * spawnOptions?: import('child_process').SpawnOptions,
119
+ * stdin?: fsClassic.ReadStream,
120
+ * }} SpawnOptions
121
+ */
122
+
54
123
  // A simple Promise wrapper for child_process.spawn. Return the err/out streams
55
124
  // from the process by default. Specify inheritStdout / inheritStderr to disable
56
125
  // this and inherit the parent process's stream, passing through the TTY if any.
126
+ /**
127
+ *
128
+ * @param {string} cmd
129
+ * @param {string[]} params
130
+ * @param {SpawnOptions} options
131
+ * @returns {Promise<string>}
132
+ */
57
133
  function spawnProcess(
58
134
  cmd,
59
135
  params = [],
@@ -81,7 +157,10 @@ function spawnProcess(
81
157
  }
82
158
  ) {
83
159
  return new Promise((resolve, reject) => {
160
+ /** @type {Buffer[]} */
84
161
  const stdout = [];
162
+
163
+ /** @type {Buffer[]} */
85
164
  const stderr = [];
86
165
 
87
166
  log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
@@ -102,6 +181,9 @@ function spawnProcess(
102
181
  // See a very old related issue: https://github.com/nodejs/node/issues/947
103
182
  // When the stream is not fully consumed by the pipe target and it exits,
104
183
  // an EPIPE or EOF is thrown. We don't care about those.
184
+ /**
185
+ * @param {Error & {code: string}} err
186
+ */
105
187
  const eatEpipe = (err) => {
106
188
  if (err.code !== 'EPIPE' && err.code !== 'EOF') {
107
189
  throw err;
@@ -140,11 +222,17 @@ function spawnProcess(
140
222
  });
141
223
  }
142
224
 
225
+ /**
226
+ * @param {Error} err
227
+ */
143
228
  const errorHandler = (err) => {
144
229
  command.off('close', closeHandler);
145
230
  reject(err);
146
231
  };
147
232
 
233
+ /**
234
+ * @param {number} code
235
+ */
148
236
  const closeHandler = function (code) {
149
237
  // The stream was fully consumed, if there is this an additional error on
150
238
  // stdout, it must be a legitimate error
@@ -170,9 +258,21 @@ function spawnProcess(
170
258
  });
171
259
  }
172
260
 
173
- function dockerExec(libdragonInfo, dockerParams, cmdWithParams, options) {
261
+ /**
262
+ * @typedef {{
263
+ * (libdragonInfo: import('./project-info').LibdragonInfo, dockerParams: string[], cmdWithParams: string[], options?: SpawnOptions): Promise<string>;
264
+ * (libdragonInfo: import('./project-info').LibdragonInfo, cmdWithParams: string[], options?: SpawnOptions, unused?: unknown): Promise<string>;
265
+ * }} DockerExec
266
+ */
267
+ const dockerExec = /** @type {DockerExec} */ function (
268
+ libdragonInfo,
269
+ dockerParams,
270
+ cmdWithParams,
271
+ /** @type {SpawnOptions} */
272
+ options
273
+ ) {
174
274
  assert(
175
- libdragonInfo.containerId,
275
+ !!libdragonInfo.containerId,
176
276
  new Error('Trying to invoke dockerExec without a containerId.')
177
277
  );
178
278
 
@@ -181,7 +281,7 @@ function dockerExec(libdragonInfo, dockerParams, cmdWithParams, options) {
181
281
  Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
182
282
 
183
283
  if (!haveDockerParams) {
184
- options = cmdWithParams;
284
+ options = /** @type {SpawnOptions} */ (cmdWithParams);
185
285
  }
186
286
 
187
287
  const additionalParams = [];
@@ -215,10 +315,12 @@ function dockerExec(libdragonInfo, dockerParams, cmdWithParams, options) {
215
315
  ],
216
316
  options
217
317
  );
218
- }
318
+ };
219
319
 
220
320
  /**
221
321
  * Recursively copies directories and files
322
+ * @param {string} src
323
+ * @param {string} dst
222
324
  */
223
325
  async function copyDirContents(src, dst) {
224
326
  log(`Copying from ${src} to ${dst}`, true);
@@ -269,14 +371,24 @@ async function copyDirContents(src, dst) {
269
371
  );
270
372
  }
271
373
 
374
+ /**
375
+ * @param {string} p
376
+ */
272
377
  function toPosixPath(p) {
273
378
  return p.replace(new RegExp('\\' + path.sep, 'g'), path.posix.sep);
274
379
  }
275
380
 
381
+ /**
382
+ * @param {string} p
383
+ */
276
384
  function toNativePath(p) {
277
385
  return p.replace(new RegExp('\\' + path.posix.sep, 'g'), path.sep);
278
386
  }
279
387
 
388
+ /**
389
+ * @param {boolean} condition
390
+ * @param {Error} error
391
+ */
280
392
  function assert(condition, error) {
281
393
  if (!condition) {
282
394
  error.message = `[ASSERTION FAILED] ${error.message}`;
@@ -284,12 +396,19 @@ function assert(condition, error) {
284
396
  }
285
397
  }
286
398
 
399
+ /**
400
+ * @param {string} text
401
+ */
287
402
  function print(text) {
288
403
  // eslint-disable-next-line no-console
289
404
  console.log(text);
290
405
  return;
291
406
  }
292
407
 
408
+ /**
409
+ * @param {string} text
410
+ * @param {boolean} verboseOnly
411
+ */
293
412
  function log(text, verboseOnly = false) {
294
413
  if (!verboseOnly) {
295
414
  // eslint-disable-next-line no-console
@@ -2,10 +2,31 @@ const chalk = require('chalk').stderr;
2
2
 
3
3
  const { log } = require('./helpers');
4
4
  const actions = require('./actions');
5
- const { STATUS_BAD_PARAM } = require('./constants');
5
+ const { STATUS_BAD_PARAM, ACCEPTED_STRATEGIES } = require('./constants');
6
6
  const { globals } = require('./globals');
7
7
 
8
+ /** @typedef { import('./actions') } Actions */
9
+
10
+ /** @typedef { typeof import('./constants').ACCEPTED_STRATEGIES[number] } VendorStrategy */
11
+
12
+ /**
13
+ * @template {keyof Actions} [T=keyof Actions]
14
+ * @typedef {{
15
+ * EXTRA_PARAMS: string[];
16
+ * CURRENT_ACTION: Actions[T];
17
+ * VERBOSE?: boolean;
18
+ * DOCKER_IMAGE?: string;
19
+ * VENDOR_DIR?: string;
20
+ * VENDOR_STRAT?: VendorStrategy;
21
+ * FILE?: string;
22
+ * }} CommandlineOptions
23
+ */
24
+
25
+ /**
26
+ * @param {string[]} argv
27
+ */
8
28
  const parseParameters = async (argv) => {
29
+ /** @type CommandlineOptions */
9
30
  const options = {
10
31
  EXTRA_PARAMS: [],
11
32
  CURRENT_ACTION: undefined,
@@ -38,12 +59,19 @@ const parseParameters = async (argv) => {
38
59
  }
39
60
 
40
61
  if (['--strategy', '-s'].includes(val)) {
41
- options.VENDOR_STRAT = argv[++i];
62
+ options.VENDOR_STRAT = /** @type VendorStrategy */ (argv[++i]);
42
63
  continue;
43
64
  } else if (val.indexOf('--strategy=') === 0) {
44
- options.VENDOR_STRAT = val.split('=')[1];
65
+ options.VENDOR_STRAT = /** @type VendorStrategy */ (val.split('=')[1]);
45
66
  continue;
46
67
  }
68
+ if (
69
+ options.VENDOR_STRAT &&
70
+ !ACCEPTED_STRATEGIES.includes(options.VENDOR_STRAT)
71
+ ) {
72
+ log(chalk.red(`Invalid strategy \`${options.VENDOR_STRAT}\``));
73
+ process.exit(STATUS_BAD_PARAM);
74
+ }
47
75
 
48
76
  if (['--file', '-f'].includes(val)) {
49
77
  options.FILE = argv[++i];
@@ -63,7 +91,7 @@ const parseParameters = async (argv) => {
63
91
  process.exit(STATUS_BAD_PARAM);
64
92
  }
65
93
 
66
- options.CURRENT_ACTION = actions[val];
94
+ options.CURRENT_ACTION = actions[/** @type {keyof Actions} */ (val)];
67
95
 
68
96
  if (!options.CURRENT_ACTION) {
69
97
  log(chalk.red(`Invalid action \`${val}\``));
@@ -90,8 +118,12 @@ const parseParameters = async (argv) => {
90
118
  }
91
119
 
92
120
  if (
93
- ![actions.init, actions.install, actions.update].includes(
94
- options.CURRENT_ACTION
121
+ !(
122
+ /** @type {typeof actions[keyof actions][]} */ ([
123
+ actions.init,
124
+ actions.install,
125
+ actions.update,
126
+ ]).includes(options.CURRENT_ACTION)
95
127
  ) &&
96
128
  options.DOCKER_IMAGE
97
129
  ) {
@@ -100,14 +132,13 @@ const parseParameters = async (argv) => {
100
132
  }
101
133
 
102
134
  if (
103
- options.VENDOR_STRAT &&
104
- !['submodule', 'subtree', 'manual'].includes(options.VENDOR_STRAT)
135
+ !(
136
+ /** @type {typeof actions[keyof actions][]} */ ([
137
+ actions.disasm,
138
+ ]).includes(options.CURRENT_ACTION)
139
+ ) &&
140
+ options.FILE
105
141
  ) {
106
- log(chalk.red(`Invalid strategy \`${options.VENDOR_STRAT}\``));
107
- process.exit(STATUS_BAD_PARAM);
108
- }
109
-
110
- if (![actions.disasm].includes(options.CURRENT_ACTION) && options.FILE) {
111
142
  log(chalk.red('Invalid flag: file'));
112
143
  process.exit(STATUS_BAD_PARAM);
113
144
  }
@@ -29,9 +29,36 @@ const {
29
29
  ParameterError,
30
30
  } = require('./helpers');
31
31
 
32
- const initAction = require('./actions/init');
33
- const destroyAction = require('./actions/destroy');
32
+ // These do not need a project to exist.
33
+ const NO_PROJECT_ACTIONS = /** @type {const} */ (['help', 'version']);
34
34
 
35
+ /**
36
+ * @typedef { typeof NO_PROJECT_ACTIONS[number] } ActionsNoProject
37
+ * @typedef { Exclude<keyof import('./parameters').Actions, ActionsNoProject> } ActionsWithProject
38
+ * @typedef { import('./parameters').CommandlineOptions<ActionsNoProject> } ActionsNoProjectOptions
39
+ * @typedef { import('./parameters').CommandlineOptions<ActionsWithProject> } ActionsWithProjectOptions
40
+ *
41
+ * @typedef { {
42
+ * options: ActionsNoProjectOptions
43
+ * } } CLIInfo
44
+ *
45
+ * @typedef { {
46
+ * root: string;
47
+ * userInfo: os.UserInfo<string>;
48
+ * haveProjectConfig: boolean;
49
+ * imageName: string;
50
+ * vendorDirectory: string;
51
+ * vendorStrategy: import('./parameters').VendorStrategy;
52
+ * containerId: string
53
+ * } } ExtendedInfo
54
+ *
55
+ * @typedef { CLIInfo & Partial<ExtendedInfo> } LibdragonInfo
56
+ */
57
+
58
+ /**
59
+ * @param {LibdragonInfo} libdragonInfo
60
+ * @returns string
61
+ */
35
62
  async function findContainerId(libdragonInfo) {
36
63
  const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
37
64
  if (await fileExists(idFile)) {
@@ -66,17 +93,24 @@ async function findContainerId(libdragonInfo) {
66
93
  const idIndex = str.indexOf(shortId);
67
94
  const longId = str.slice(idIndex, idIndex + 64);
68
95
  if (longId.length === 64) {
69
- const newInfo = { ...libdragonInfo, containerId: longId };
70
96
  // This shouldn't happen but if the user somehow deleted the .git folder
71
97
  // (we don't have the container id file at this point) we can recover the
72
98
  // project. `git init` is safe anyways and it is not executed if strategy
73
99
  // is `manual`
74
- await initGitAndCacheContainerId(newInfo);
100
+ await initGitAndCacheContainerId({
101
+ ...libdragonInfo,
102
+ containerId: longId,
103
+ });
75
104
  return longId;
76
105
  }
77
106
  }
78
107
  }
79
108
 
109
+ /**
110
+ * @param {string} start
111
+ * @param {string} relativeFile
112
+ * @return {Promise<string>}
113
+ */
80
114
  async function findLibdragonRoot(
81
115
  start = '.',
82
116
  relativeFile = path.join(LIBDRAGON_PROJECT_MANIFEST, CONFIG_FILE)
@@ -104,18 +138,19 @@ async function findGitRoot() {
104
138
  }
105
139
  }
106
140
 
107
- async function readProjectInfo(info) {
141
+ /**
142
+ * @param {LibdragonInfo} optionInfo
143
+ */
144
+ const readProjectInfo = async function (optionInfo) {
108
145
  // No need to do anything here if the action does not depend on the project
109
146
  // The only exception is the init and destroy actions, which do not need an
110
147
  // existing project but readProjectInfo must always run to analyze the situation
111
- const forceReadProjectInfo = [initAction, destroyAction].includes(
112
- info.options.CURRENT_ACTION
113
- );
114
148
  if (
115
- info.options.CURRENT_ACTION.mustHaveProject === false &&
116
- !forceReadProjectInfo
149
+ NO_PROJECT_ACTIONS.includes(
150
+ /** @type {ActionsNoProject} */ (optionInfo.options.CURRENT_ACTION.name)
151
+ )
117
152
  ) {
118
- return info;
153
+ return /** @type {CLIInfo} */ (optionInfo);
119
154
  }
120
155
 
121
156
  const migratedRoot = await findLibdragonRoot();
@@ -128,15 +163,19 @@ async function readProjectInfo(info) {
128
163
  path.join(LIBDRAGON_PROJECT_MANIFEST, IMAGE_FILE)
129
164
  ));
130
165
 
131
- if (!projectRoot && !forceReadProjectInfo) {
166
+ if (
167
+ !projectRoot &&
168
+ !['init', 'destroy'].includes(optionInfo.options.CURRENT_ACTION.name)
169
+ ) {
132
170
  throw new ParameterError(
133
171
  'This is not a libdragon project. Initialize with `libdragon init` first.',
134
- info.options.CURRENT_ACTION.name
172
+ optionInfo.options.CURRENT_ACTION.name
135
173
  );
136
174
  }
137
175
 
138
- info = {
139
- ...info,
176
+ /** @type {LibdragonInfo} */
177
+ let info = {
178
+ ...optionInfo,
140
179
  root: projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot()),
141
180
  userInfo: os.userInfo(),
142
181
 
@@ -209,10 +248,10 @@ async function readProjectInfo(info) {
209
248
  log(`Active vendor strategy: ${info.vendorStrategy}`, true);
210
249
 
211
250
  return info;
212
- }
251
+ };
213
252
 
214
253
  /**
215
- * @param info This is only the base info without options
254
+ * @param { LibdragonInfo | void } info This is only the base info without options
216
255
  * fn and command line options
217
256
  */
218
257
  async function writeProjectInfo(info) {