libdragon 10.8.0 → 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.
@@ -11,11 +11,11 @@ const {
11
11
 
12
12
  /**
13
13
  * Create a new container
14
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
14
15
  */
15
16
  const initContainer = async (libdragonInfo) => {
16
17
  let newId;
17
18
  try {
18
- // Create a new container
19
19
  log('Creating new container...');
20
20
  newId = (
21
21
  await spawnProcess('docker', [
@@ -75,6 +75,9 @@ const initContainer = async (libdragonInfo) => {
75
75
  return newId;
76
76
  };
77
77
 
78
+ /**
79
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
80
+ */
78
81
  const start = async (libdragonInfo) => {
79
82
  const running =
80
83
  libdragonInfo.containerId &&
@@ -99,14 +102,18 @@ const start = async (libdragonInfo) => {
99
102
  return id;
100
103
  };
101
104
 
102
- module.exports = {
105
+ module.exports = /** @type {const} */ ({
103
106
  name: 'start',
107
+ /**
108
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
109
+ */
104
110
  fn: async (libdragonInfo) => {
105
111
  const containerId = await start(libdragonInfo);
106
112
  print(containerId);
107
113
  return { ...libdragonInfo, containerId };
108
114
  },
109
115
  start,
116
+ forwardsRestParams: false,
110
117
  usage: {
111
118
  name: 'start',
112
119
  summary: 'Start the container for current project.',
@@ -114,4 +121,4 @@ module.exports = {
114
121
 
115
122
  Must be run in an initialized libdragon project.`,
116
123
  },
117
- };
124
+ });
@@ -2,6 +2,9 @@ const { spawnProcess } = require('../helpers');
2
2
 
3
3
  const { checkContainerRunning } = require('./utils');
4
4
 
5
+ /**
6
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
7
+ */
5
8
  const stop = async (libdragonInfo) => {
6
9
  const running =
7
10
  libdragonInfo.containerId &&
@@ -14,9 +17,10 @@ const stop = async (libdragonInfo) => {
14
17
  return libdragonInfo;
15
18
  };
16
19
 
17
- module.exports = {
20
+ module.exports = /** @type {const} */ ({
18
21
  name: 'stop',
19
22
  fn: stop,
23
+ forwardsRestParams: false,
20
24
  usage: {
21
25
  name: 'stop',
22
26
  summary: 'Stop the container for current project.',
@@ -24,4 +28,4 @@ module.exports = {
24
28
 
25
29
  Must be run in an initialized libdragon project.`,
26
30
  },
27
- };
31
+ });
@@ -2,6 +2,9 @@ const { log } = require('../helpers');
2
2
  const { updateImage, destroyContainer } = require('./utils');
3
3
  const { start } = require('./start');
4
4
 
5
+ /**
6
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
7
+ */
5
8
  async function syncImageAndStart(libdragonInfo) {
6
9
  const oldImageName = libdragonInfo.imageName;
7
10
  const imageName = libdragonInfo.options.DOCKER_IMAGE ?? oldImageName;
@@ -3,6 +3,9 @@ const { LIBDRAGON_GIT, LIBDRAGON_BRANCH } = require('../constants');
3
3
  const { runGitMaybeHost, installDependencies } = require('./utils');
4
4
  const { syncImageAndStart } = require('./update-and-start');
5
5
 
6
+ /**
7
+ * @param {import('../project-info').LibdragonInfo} info
8
+ */
6
9
  const update = async (info) => {
7
10
  info = await syncImageAndStart(info);
8
11
 
@@ -33,9 +36,10 @@ const update = async (info) => {
33
36
  await installDependencies(info);
34
37
  };
35
38
 
36
- module.exports = {
39
+ module.exports = /** @type {const} */ ({
37
40
  name: 'update',
38
41
  fn: update,
42
+ forwardsRestParams: false,
39
43
  usage: {
40
44
  name: 'update',
41
45
  summary: 'Update libdragon and do an install.',
@@ -44,4 +48,4 @@ module.exports = {
44
48
  Must be run in an initialized libdragon project.`,
45
49
  group: ['docker'],
46
50
  },
47
- };
51
+ });
@@ -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,6 +115,14 @@ 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
  */
118
+
119
+ /**
120
+ *
121
+ * @param {import('../project-info').LibdragonInfo} libdragonInfo
122
+ * @param {string[]} params
123
+ * @param {import('../helpers').SpawnOptions} options
124
+ * @returns
125
+ */
113
126
  async function runGitMaybeHost(libdragonInfo, params, options = {}) {
114
127
  assert(
115
128
  libdragonInfo.vendorStrategy !== 'manual',
@@ -143,6 +156,9 @@ async function runGitMaybeHost(libdragonInfo, params, options = {}) {
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)) {
@@ -79,6 +106,11 @@ async function findContainerId(libdragonInfo) {
79
106
  }
80
107
  }
81
108
 
109
+ /**
110
+ * @param {string} start
111
+ * @param {string} relativeFile
112
+ * @return {Promise<string>}
113
+ */
82
114
  async function findLibdragonRoot(
83
115
  start = '.',
84
116
  relativeFile = path.join(LIBDRAGON_PROJECT_MANIFEST, CONFIG_FILE)
@@ -106,18 +138,19 @@ async function findGitRoot() {
106
138
  }
107
139
  }
108
140
 
109
- async function readProjectInfo(info) {
141
+ /**
142
+ * @param {LibdragonInfo} optionInfo
143
+ */
144
+ const readProjectInfo = async function (optionInfo) {
110
145
  // No need to do anything here if the action does not depend on the project
111
146
  // The only exception is the init and destroy actions, which do not need an
112
147
  // existing project but readProjectInfo must always run to analyze the situation
113
- const forceReadProjectInfo = [initAction, destroyAction].includes(
114
- info.options.CURRENT_ACTION
115
- );
116
148
  if (
117
- info.options.CURRENT_ACTION.mustHaveProject === false &&
118
- !forceReadProjectInfo
149
+ NO_PROJECT_ACTIONS.includes(
150
+ /** @type {ActionsNoProject} */ (optionInfo.options.CURRENT_ACTION.name)
151
+ )
119
152
  ) {
120
- return info;
153
+ return /** @type {CLIInfo} */ (optionInfo);
121
154
  }
122
155
 
123
156
  const migratedRoot = await findLibdragonRoot();
@@ -130,15 +163,19 @@ async function readProjectInfo(info) {
130
163
  path.join(LIBDRAGON_PROJECT_MANIFEST, IMAGE_FILE)
131
164
  ));
132
165
 
133
- if (!projectRoot && !forceReadProjectInfo) {
166
+ if (
167
+ !projectRoot &&
168
+ !['init', 'destroy'].includes(optionInfo.options.CURRENT_ACTION.name)
169
+ ) {
134
170
  throw new ParameterError(
135
171
  'This is not a libdragon project. Initialize with `libdragon init` first.',
136
- info.options.CURRENT_ACTION.name
172
+ optionInfo.options.CURRENT_ACTION.name
137
173
  );
138
174
  }
139
175
 
140
- info = {
141
- ...info,
176
+ /** @type {LibdragonInfo} */
177
+ let info = {
178
+ ...optionInfo,
142
179
  root: projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot()),
143
180
  userInfo: os.userInfo(),
144
181
 
@@ -211,10 +248,10 @@ async function readProjectInfo(info) {
211
248
  log(`Active vendor strategy: ${info.vendorStrategy}`, true);
212
249
 
213
250
  return info;
214
- }
251
+ };
215
252
 
216
253
  /**
217
- * @param info This is only the base info without options
254
+ * @param { LibdragonInfo | void } info This is only the base info without options
218
255
  * fn and command line options
219
256
  */
220
257
  async function writeProjectInfo(info) {