libdragon 10.6.0 → 10.7.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/CHANGELOG.md +47 -0
- package/index.js +13 -130
- package/modules/actions/destroy.js +8 -9
- package/modules/actions/disasm.js +32 -27
- package/modules/actions/exec.js +85 -36
- package/modules/actions/help.js +11 -9
- package/modules/actions/index.js +5 -6
- package/modules/actions/init.js +21 -28
- package/modules/actions/install.js +2 -4
- package/modules/actions/make.js +8 -3
- package/modules/actions/start.js +7 -13
- package/modules/actions/stop.js +1 -3
- package/modules/actions/update.js +4 -6
- package/modules/actions/utils.js +36 -46
- package/modules/actions/version.js +1 -1
- package/modules/helpers.js +111 -44
- package/modules/parameters.js +129 -0
- package/modules/project-info.js +43 -11
- package/package.json +1 -1
- package/skeleton/Makefile +1 -1
package/modules/helpers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs/promises');
|
|
3
3
|
const fsClassic = require('fs');
|
|
4
|
-
const chalk = require('chalk');
|
|
4
|
+
const chalk = require('chalk').stderr;
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
|
|
7
7
|
const { globals } = require('./globals');
|
|
@@ -18,8 +18,9 @@ class CommandError extends Error {
|
|
|
18
18
|
|
|
19
19
|
// The user provided an unexpected input
|
|
20
20
|
class ParameterError extends Error {
|
|
21
|
-
constructor(message) {
|
|
21
|
+
constructor(message, actionName) {
|
|
22
22
|
super(message);
|
|
23
|
+
this.actionName = actionName;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -50,51 +51,90 @@ async function dirExists(path) {
|
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
// A simple Promise wrapper for child_process.spawn.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// A simple Promise wrapper for child_process.spawn. Return the err/out streams
|
|
55
|
+
// from the process by default. Specify inheritStdout / inheritStderr to disable
|
|
56
|
+
// this and inherit the parent process's stream, passing through the TTY if any.
|
|
57
|
+
function spawnProcess(
|
|
58
|
+
cmd,
|
|
59
|
+
params = [],
|
|
60
|
+
{
|
|
61
|
+
// Used to decorate the potential CommandError with a prop such that we can
|
|
62
|
+
// report this error back to the user.
|
|
63
|
+
userCommand = false,
|
|
64
|
+
// This is the stream where the process will receive its input.
|
|
65
|
+
stdin,
|
|
66
|
+
// If this is true, the related stream is inherited from the parent process
|
|
67
|
+
// and we cannot read them anymore. So if you need to read a stream, you
|
|
68
|
+
// should disable it. When disabled, the relevant stream cannot be a tty
|
|
69
|
+
// anymore. By default, we expect the caller to read err/out.
|
|
70
|
+
inheritStdin = true,
|
|
71
|
+
inheritStdout = false,
|
|
72
|
+
inheritStderr = false,
|
|
73
|
+
// Passthrough to spawn
|
|
74
|
+
spawnOptions = {},
|
|
75
|
+
} = {
|
|
76
|
+
userCommand: false,
|
|
77
|
+
inheritStdin: true,
|
|
78
|
+
inheritStdout: false,
|
|
79
|
+
inheritStderr: false,
|
|
80
|
+
spawnOptions: {},
|
|
81
|
+
}
|
|
82
|
+
) {
|
|
59
83
|
return new Promise((resolve, reject) => {
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
const stdout = [];
|
|
85
|
+
const stderr = [];
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
|
|
65
|
-
}
|
|
87
|
+
log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
|
|
66
88
|
|
|
67
|
-
const isTTY
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const enableErrorTTY = isTTY && interactive === 'full';
|
|
89
|
+
const enableInTTY = Boolean(process.stdin.isTTY) && inheritStdin;
|
|
90
|
+
const enableOutTTY = Boolean(process.stdout.isTTY) && inheritStdout;
|
|
91
|
+
const enableErrorTTY = Boolean(process.stderr.isTTY) && inheritStderr;
|
|
71
92
|
|
|
72
93
|
const command = spawn(cmd, params, {
|
|
73
|
-
// We should redirect streams together for the TTY to work
|
|
74
|
-
// properly if they are all used as TTY
|
|
75
94
|
stdio: [
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
enableInTTY ? 'inherit' : 'pipe',
|
|
96
|
+
enableOutTTY ? 'inherit' : 'pipe',
|
|
78
97
|
enableErrorTTY ? 'inherit' : 'pipe',
|
|
79
98
|
],
|
|
99
|
+
...spawnOptions,
|
|
80
100
|
});
|
|
81
101
|
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
// See a very old related issue: https://github.com/nodejs/node/issues/947
|
|
103
|
+
// When the stream is not fully consumed by the pipe target and it exits,
|
|
104
|
+
// an EPIPE or EOF is thrown. We don't care about those.
|
|
105
|
+
const eatEpipe = (err) => {
|
|
106
|
+
if (err.code !== 'EPIPE' && err.code !== 'EOF') {
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
// No need to listen for close anymore
|
|
110
|
+
command.off('close', closeHandler);
|
|
111
|
+
|
|
112
|
+
// It was not fully consumed, just resolve into an empty string
|
|
113
|
+
// No one should be using this anyways. Ideally we could clean a few
|
|
114
|
+
// last bytes from the buffers to create a correct utf-8 string.
|
|
115
|
+
resolve('');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!enableInTTY && stdin) {
|
|
119
|
+
stdin.pipe(command.stdin);
|
|
84
120
|
}
|
|
85
121
|
|
|
86
|
-
if (!
|
|
87
|
-
command.
|
|
122
|
+
if (!enableOutTTY && (globals.verbose || userCommand)) {
|
|
123
|
+
command.stdout.pipe(process.stdout);
|
|
124
|
+
process.stdout.once('error', eatEpipe);
|
|
88
125
|
}
|
|
89
126
|
|
|
90
|
-
|
|
91
|
-
if (!enableTTY && !userCommand) {
|
|
127
|
+
if (!inheritStdout) {
|
|
92
128
|
command.stdout.on('data', function (data) {
|
|
93
129
|
stdout.push(Buffer.from(data));
|
|
94
130
|
});
|
|
95
131
|
}
|
|
96
132
|
|
|
97
|
-
if (!enableErrorTTY) {
|
|
133
|
+
if (!enableErrorTTY && (globals.verbose || userCommand)) {
|
|
134
|
+
command.stderr.pipe(process.stderr);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!inheritStderr) {
|
|
98
138
|
command.stderr.on('data', function (data) {
|
|
99
139
|
stderr.push(Buffer.from(data));
|
|
100
140
|
});
|
|
@@ -106,6 +146,9 @@ function spawnProcess(cmd, params = [], userCommand, interactive = false) {
|
|
|
106
146
|
};
|
|
107
147
|
|
|
108
148
|
const closeHandler = function (code) {
|
|
149
|
+
// The stream was fully consumed, if there is this an additional error on
|
|
150
|
+
// stdout, it must be a legitimate error
|
|
151
|
+
process.stdout.off('error', eatEpipe);
|
|
109
152
|
command.off('error', errorHandler);
|
|
110
153
|
if (code === 0) {
|
|
111
154
|
resolve(Buffer.concat(stdout).toString());
|
|
@@ -127,20 +170,39 @@ function spawnProcess(cmd, params = [], userCommand, interactive = false) {
|
|
|
127
170
|
});
|
|
128
171
|
}
|
|
129
172
|
|
|
130
|
-
function dockerExec(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
) {
|
|
173
|
+
function dockerExec(libdragonInfo, dockerParams, cmdWithParams, options) {
|
|
174
|
+
assert(
|
|
175
|
+
libdragonInfo.containerId,
|
|
176
|
+
new Error('Trying to invoke dockerExec without a containerId.')
|
|
177
|
+
);
|
|
178
|
+
|
|
137
179
|
// TODO: assert for invalid args
|
|
138
180
|
const haveDockerParams =
|
|
139
181
|
Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
|
|
183
|
+
if (!haveDockerParams) {
|
|
184
|
+
options = cmdWithParams;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const additionalParams = [];
|
|
188
|
+
|
|
189
|
+
// Docker TTY wants in & out streams both to be a TTY
|
|
190
|
+
// If no options are provided, disable TTY as spawnProcess defaults to no
|
|
191
|
+
// inherit as well.
|
|
192
|
+
const enableTTY = options
|
|
193
|
+
? options.inheritStdout && options.inheritStdin
|
|
194
|
+
: false;
|
|
195
|
+
const ttyEnabled = enableTTY && process.stdout.isTTY && process.stdin.isTTY;
|
|
196
|
+
|
|
197
|
+
if (ttyEnabled) {
|
|
198
|
+
additionalParams.push('-t');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Always enable stdin, also see; https://github.com/anacierdem/libdragon-docker/issues/45
|
|
202
|
+
// Currently we run all exec commands in stdin mode even if the actual process
|
|
203
|
+
// does not need any input. This will eat any user input by default.
|
|
204
|
+
additionalParams.push('-i');
|
|
205
|
+
|
|
144
206
|
return spawnProcess(
|
|
145
207
|
'docker',
|
|
146
208
|
[
|
|
@@ -151,8 +213,7 @@ function dockerExec(
|
|
|
151
213
|
libdragonInfo.containerId,
|
|
152
214
|
...(haveDockerParams ? cmdWithParams : dockerParams),
|
|
153
215
|
],
|
|
154
|
-
|
|
155
|
-
haveDockerParams ? interactive : userCommand
|
|
216
|
+
options
|
|
156
217
|
);
|
|
157
218
|
}
|
|
158
219
|
|
|
@@ -223,16 +284,21 @@ function assert(condition, error) {
|
|
|
223
284
|
}
|
|
224
285
|
}
|
|
225
286
|
|
|
226
|
-
|
|
287
|
+
function print(text) {
|
|
288
|
+
// eslint-disable-next-line no-console
|
|
289
|
+
console.log(text);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
227
293
|
function log(text, verboseOnly = false) {
|
|
228
294
|
if (!verboseOnly) {
|
|
229
295
|
// eslint-disable-next-line no-console
|
|
230
|
-
console.
|
|
296
|
+
console.error(text);
|
|
231
297
|
return;
|
|
232
298
|
}
|
|
233
299
|
if (globals.verbose) {
|
|
234
300
|
// eslint-disable-next-line no-console
|
|
235
|
-
console.
|
|
301
|
+
console.error(chalk.gray(text));
|
|
236
302
|
return;
|
|
237
303
|
}
|
|
238
304
|
}
|
|
@@ -241,6 +307,7 @@ module.exports = {
|
|
|
241
307
|
spawnProcess,
|
|
242
308
|
toPosixPath,
|
|
243
309
|
toNativePath,
|
|
310
|
+
print,
|
|
244
311
|
log,
|
|
245
312
|
dockerExec,
|
|
246
313
|
assert,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const chalk = require('chalk').stderr;
|
|
2
|
+
|
|
3
|
+
const { log } = require('./helpers');
|
|
4
|
+
const actions = require('./actions');
|
|
5
|
+
const { fn: printUsage } = require('./actions/help');
|
|
6
|
+
const { STATUS_BAD_PARAM } = require('./constants');
|
|
7
|
+
const { globals } = require('./globals');
|
|
8
|
+
|
|
9
|
+
const parseParameters = async (argv) => {
|
|
10
|
+
const options = {
|
|
11
|
+
EXTRA_PARAMS: [],
|
|
12
|
+
CURRENT_ACTION: undefined,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (let i = 2; i < argv.length; i++) {
|
|
16
|
+
const val = argv[i];
|
|
17
|
+
|
|
18
|
+
if (['--verbose', '-v'].includes(val)) {
|
|
19
|
+
options.VERBOSE = true;
|
|
20
|
+
globals.verbose = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// TODO: we might move these to actions as well.
|
|
25
|
+
if (['--image', '-i'].includes(val)) {
|
|
26
|
+
options.DOCKER_IMAGE = argv[++i];
|
|
27
|
+
continue;
|
|
28
|
+
} else if (val.indexOf('--image=') === 0) {
|
|
29
|
+
options.DOCKER_IMAGE = val.split('=')[1];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (['--directory', '-d'].includes(val)) {
|
|
34
|
+
options.VENDOR_DIR = argv[++i];
|
|
35
|
+
continue;
|
|
36
|
+
} else if (val.indexOf('--directory=') === 0) {
|
|
37
|
+
options.VENDOR_DIR = val.split('=')[1];
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (['--strategy', '-s'].includes(val)) {
|
|
42
|
+
options.VENDOR_STRAT = argv[++i];
|
|
43
|
+
continue;
|
|
44
|
+
} else if (val.indexOf('--strategy=') === 0) {
|
|
45
|
+
options.VENDOR_STRAT = val.split('=')[1];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (['--file', '-f'].includes(val)) {
|
|
50
|
+
options.FILE = argv[++i];
|
|
51
|
+
continue;
|
|
52
|
+
} else if (val.indexOf('--file=') === 0) {
|
|
53
|
+
options.FILE = val.split('=')[1];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (val.indexOf('-') == 0) {
|
|
58
|
+
log(chalk.red(`Invalid flag \`${val}\``));
|
|
59
|
+
printUsage();
|
|
60
|
+
process.exit(STATUS_BAD_PARAM);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.CURRENT_ACTION) {
|
|
64
|
+
log(chalk.red(`Expected only a single action, found: \`${val}\``));
|
|
65
|
+
printUsage();
|
|
66
|
+
process.exit(STATUS_BAD_PARAM);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
options.CURRENT_ACTION = actions[val];
|
|
70
|
+
|
|
71
|
+
if (!options.CURRENT_ACTION) {
|
|
72
|
+
log(chalk.red(`Invalid action \`${val}\``));
|
|
73
|
+
printUsage();
|
|
74
|
+
process.exit(STATUS_BAD_PARAM);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (options.CURRENT_ACTION.forwardsRestParams) {
|
|
78
|
+
options.EXTRA_PARAMS = argv.slice(i + 1);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!options.CURRENT_ACTION) {
|
|
84
|
+
log(chalk.red('No action provided'));
|
|
85
|
+
printUsage();
|
|
86
|
+
process.exit(STATUS_BAD_PARAM);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
options.CURRENT_ACTION === actions.exec &&
|
|
91
|
+
options.EXTRA_PARAMS.length === 0
|
|
92
|
+
) {
|
|
93
|
+
log(chalk.red('You should provide a command to exec'));
|
|
94
|
+
printUsage(undefined, [options.CURRENT_ACTION.name]);
|
|
95
|
+
process.exit(STATUS_BAD_PARAM);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
![actions.init, actions.install, actions.update].includes(
|
|
100
|
+
options.CURRENT_ACTION
|
|
101
|
+
) &&
|
|
102
|
+
options.DOCKER_IMAGE
|
|
103
|
+
) {
|
|
104
|
+
log(chalk.red('Invalid flag: image'));
|
|
105
|
+
printUsage(undefined, [options.CURRENT_ACTION.name]);
|
|
106
|
+
process.exit(STATUS_BAD_PARAM);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
options.VENDOR_STRAT &&
|
|
111
|
+
!['submodule', 'subtree', 'manual'].includes(options.VENDOR_STRAT)
|
|
112
|
+
) {
|
|
113
|
+
log(chalk.red(`Invalid strategy \`${options.VENDOR_STRAT}\``));
|
|
114
|
+
printUsage();
|
|
115
|
+
process.exit(STATUS_BAD_PARAM);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (![actions.disasm].includes(options.CURRENT_ACTION) && options.FILE) {
|
|
119
|
+
log(chalk.red('Invalid flag: file'));
|
|
120
|
+
printUsage(undefined, [options.CURRENT_ACTION.name]);
|
|
121
|
+
process.exit(STATUS_BAD_PARAM);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { options };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
parseParameters,
|
|
129
|
+
};
|
package/modules/project-info.js
CHANGED
|
@@ -4,8 +4,7 @@ const fs = require('fs/promises');
|
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
checkContainerAndClean,
|
|
7
|
-
|
|
8
|
-
tryCacheContainerId,
|
|
7
|
+
initGitAndCacheContainerId,
|
|
9
8
|
} = require('./actions/utils');
|
|
10
9
|
|
|
11
10
|
const { findNPMRoot } = require('./actions/npm-utils');
|
|
@@ -27,8 +26,12 @@ const {
|
|
|
27
26
|
spawnProcess,
|
|
28
27
|
toPosixPath,
|
|
29
28
|
assert,
|
|
29
|
+
ParameterError,
|
|
30
30
|
} = require('./helpers');
|
|
31
31
|
|
|
32
|
+
const initAction = require('./actions/init');
|
|
33
|
+
const destroyAction = require('./actions/destroy');
|
|
34
|
+
|
|
32
35
|
async function findContainerId(libdragonInfo) {
|
|
33
36
|
const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
|
|
34
37
|
if (await fileExists(idFile)) {
|
|
@@ -49,7 +52,13 @@ async function findContainerId(libdragonInfo) {
|
|
|
49
52
|
])
|
|
50
53
|
)
|
|
51
54
|
.split('\n')
|
|
52
|
-
|
|
55
|
+
// docker seem to save paths with posix separators but make sure we look for
|
|
56
|
+
// both, just in case
|
|
57
|
+
.filter(
|
|
58
|
+
(s) =>
|
|
59
|
+
s.includes(`${toPosixPath(libdragonInfo.root)} `) ||
|
|
60
|
+
s.includes(`${libdragonInfo.root} `)
|
|
61
|
+
);
|
|
53
62
|
|
|
54
63
|
if (candidates.length > 0) {
|
|
55
64
|
const str = candidates[0];
|
|
@@ -57,11 +66,12 @@ async function findContainerId(libdragonInfo) {
|
|
|
57
66
|
const idIndex = str.indexOf(shortId);
|
|
58
67
|
const longId = str.slice(idIndex, idIndex + 64);
|
|
59
68
|
if (longId.length === 64) {
|
|
60
|
-
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
const newInfo = { ...libdragonInfo, containerId: longId };
|
|
70
|
+
// This shouldn't happen but if the user somehow deleted the .git folder
|
|
71
|
+
// (we don't have the container id file at this point) we can recover the
|
|
72
|
+
// project. `git init` is safe anyways and it is not executed if strategy
|
|
73
|
+
// is `manual`
|
|
74
|
+
await initGitAndCacheContainerId(newInfo);
|
|
65
75
|
return longId;
|
|
66
76
|
}
|
|
67
77
|
}
|
|
@@ -90,14 +100,36 @@ async function findGitRoot() {
|
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
async function readProjectInfo() {
|
|
103
|
+
async function readProjectInfo(info) {
|
|
104
|
+
// No need to do anything here if the action does not depend on the project
|
|
105
|
+
// The only exception is the init and destroy actions, which do not need an
|
|
106
|
+
// existing project but readProjectInfo must always run to analyze the situation
|
|
107
|
+
const forceReadProjectInfo = [initAction, destroyAction].includes(
|
|
108
|
+
info.options.CURRENT_ACTION
|
|
109
|
+
);
|
|
110
|
+
if (
|
|
111
|
+
info.options.CURRENT_ACTION.mustHaveProject === false &&
|
|
112
|
+
!forceReadProjectInfo
|
|
113
|
+
) {
|
|
114
|
+
return info;
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
const projectRoot = await findLibdragonRoot();
|
|
95
118
|
|
|
96
|
-
|
|
119
|
+
if (!projectRoot && !forceReadProjectInfo) {
|
|
120
|
+
throw new ParameterError(
|
|
121
|
+
'This is not a libdragon project. Initialize with `libdragon init` first.',
|
|
122
|
+
info.options.CURRENT_ACTION.name
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
info = {
|
|
127
|
+
...info,
|
|
97
128
|
root: projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot()),
|
|
98
129
|
userInfo: os.userInfo(),
|
|
99
130
|
|
|
100
131
|
// Use this to discriminate if there is a project when the command is run
|
|
132
|
+
// Only used for the init action ATM, and it is not ideal to have this here
|
|
101
133
|
haveProjectConfig: !!projectRoot,
|
|
102
134
|
|
|
103
135
|
// Set the defaults immediately, these should be present at all times even
|
|
@@ -169,7 +201,7 @@ async function readProjectInfo() {
|
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
/**
|
|
172
|
-
* @param info This is only the base info without
|
|
204
|
+
* @param info This is only the base info without options
|
|
173
205
|
* fn and command line options
|
|
174
206
|
*/
|
|
175
207
|
async function writeProjectInfo(info) {
|
package/package.json
CHANGED