build-strap 4.0.0-dev.56 → 4.1.0-dev.59
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/package.json +1 -1
- package/src/brew.d.ts +6 -0
- package/src/brew.js +74 -0
- package/src/cp.js +1 -0
- package/src/docker.d.ts +4 -1
- package/src/docker.js +78 -1
- package/src/dockerCompose.d.ts +1 -0
- package/src/dockerCompose.js +9 -1
- package/src/env.d.ts +19 -0
- package/src/env.js +80 -0
- package/src/fetch.d.ts +1 -0
- package/src/fetch.js +20 -0
- package/src/fs.d.ts +6 -0
- package/src/fs.js +38 -0
- package/src/gh.d.ts +7 -0
- package/src/gh.js +51 -0
- package/src/git.d.ts +6 -1
- package/src/git.js +24 -4
- package/src/index.d.ts +3 -0
- package/src/index.js +3 -0
- package/src/macos.d.ts +3 -0
- package/src/macos.js +36 -0
- package/src/npm.js +3 -1
- package/src/promise.d.ts +7 -0
- package/src/promise.js +15 -0
- package/src/prompt.d.ts +2 -0
- package/src/prompt.js +19 -0
- package/src/ssh.d.ts +1 -0
- package/src/ssh.js +13 -0
package/package.json
CHANGED
package/src/brew.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SpawnOptions } from './cp.js';
|
|
2
|
+
export declare const ensureBrewInstalled: () => Promise<void>;
|
|
3
|
+
export declare const brew: (args: string[], options?: SpawnOptions | undefined) => Promise<import("./cp.js").SpawnResult>;
|
|
4
|
+
export declare const brewBundleCheck: (brewFile: string) => Promise<boolean>;
|
|
5
|
+
export declare const brewBundleInstall: (brewFile: string) => Promise<void>;
|
|
6
|
+
export declare const ensureBrewfileInstalled: (brewFile: string) => Promise<void>;
|
package/src/brew.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn } from './cp.js';
|
|
5
|
+
import { cmdExists } from './env.js';
|
|
6
|
+
import { appendIfMissing } from './fs.js';
|
|
7
|
+
const brewPath = async () => {
|
|
8
|
+
for (const testPath of [
|
|
9
|
+
'/opt/homebrew/bin/brew',
|
|
10
|
+
'/usr/local/bin/brew',
|
|
11
|
+
'/home/linuxbrew/.linuxbrew/bin/brew',
|
|
12
|
+
`${os.homedir()}/.linuxbrew/bin/brew`,
|
|
13
|
+
]) {
|
|
14
|
+
if (await fs.pathExists(testPath)) {
|
|
15
|
+
return testPath;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
throw new Error('brew not found in default install path');
|
|
19
|
+
};
|
|
20
|
+
const brewPathExists = async () => {
|
|
21
|
+
try {
|
|
22
|
+
return !!(await brewPath());
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const appendBrewShellEnv = async () => {
|
|
29
|
+
const brewDir = path.dirname(await brewPath());
|
|
30
|
+
await appendIfMissing(path.join(os.homedir(), '.zprofile'), `\neval "$(${brewDir}/brew shellenv)"`);
|
|
31
|
+
if (!process.env.PATH?.includes(brewDir)) {
|
|
32
|
+
process.env.PATH = `${brewDir}:${process.env.PATH}`;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export const ensureBrewInstalled = async () => {
|
|
36
|
+
if (!(await cmdExists('brew'))) {
|
|
37
|
+
// handle the case where we have previously run `./bs` but have not yet
|
|
38
|
+
// restarted shell.
|
|
39
|
+
if (await brewPathExists()) {
|
|
40
|
+
await appendBrewShellEnv();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await spawn('/bin/bash', [
|
|
44
|
+
'-c',
|
|
45
|
+
'$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)',
|
|
46
|
+
], { stdio: 'inherit' });
|
|
47
|
+
if (!(await cmdExists('brew'))) {
|
|
48
|
+
if (!(await brewPathExists())) {
|
|
49
|
+
throw new Error('Could not find brew executable, add it to your path manually');
|
|
50
|
+
}
|
|
51
|
+
await appendBrewShellEnv();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
export const brew = async (args, options) => spawn(await brewPath(), args, options);
|
|
56
|
+
export const brewBundleCheck = async (brewFile) => {
|
|
57
|
+
try {
|
|
58
|
+
await brew(['bundle', 'check', '--file', brewFile]);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export const brewBundleInstall = async (brewFile) => {
|
|
66
|
+
await brew(['bundle', 'install', '--file', brewFile], {
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
export const ensureBrewfileInstalled = async (brewFile) => {
|
|
71
|
+
if (!(await brewBundleCheck(brewFile))) {
|
|
72
|
+
await brewBundleInstall(brewFile);
|
|
73
|
+
}
|
|
74
|
+
};
|
package/src/cp.js
CHANGED
|
@@ -63,6 +63,7 @@ export async function spawn(command, args, opts) {
|
|
|
63
63
|
p.on('close', handleExit);
|
|
64
64
|
p.on('exit', handleExit);
|
|
65
65
|
// only fires if we failed to spawn
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
67
|
p.on('error', (err) => {
|
|
67
68
|
// Here we have a workaround that reverses a "feature" of cross-spawn on Windows. See:
|
|
68
69
|
// https://github.com/moxystudio/node-cross-spawn/blob/master/lib/enoent.js#L23
|
package/src/docker.d.ts
CHANGED
|
@@ -64,5 +64,8 @@ export interface CopyFilesFromDockerImageConfig {
|
|
|
64
64
|
ignoreErrors?: boolean;
|
|
65
65
|
imageId: string;
|
|
66
66
|
}
|
|
67
|
-
export declare function copyFilesFromDockerImage({ filePaths, ignoreErrors, imageId, }: CopyFilesFromDockerImageConfig): Promise<
|
|
67
|
+
export declare function copyFilesFromDockerImage({ filePaths, ignoreErrors, imageId, }: CopyFilesFromDockerImageConfig): Promise<void>;
|
|
68
|
+
export declare const dockerIsRunning: () => Promise<boolean>;
|
|
69
|
+
export declare const ensureDockerRunning: (timeoutSeconds?: number) => Promise<void>;
|
|
70
|
+
export declare const ensureDockerInstalled: () => Promise<void>;
|
|
68
71
|
export {};
|
package/src/docker.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import bytes from 'bytes';
|
|
2
|
+
import fs from 'fs-extra';
|
|
2
3
|
import isReachable from 'is-reachable';
|
|
3
4
|
import moment from 'moment';
|
|
5
|
+
import os from 'os';
|
|
4
6
|
import path from 'path';
|
|
5
7
|
import { spawn } from './cp.js';
|
|
6
8
|
import { dockerContainerKill, dockerContainerRunDaemon, } from './docker.container.js';
|
|
7
9
|
import { dockerNetworkConnect } from './docker.network.js';
|
|
10
|
+
import { cmdExists, isArm, isMac } from './env.js';
|
|
11
|
+
import { downloadFile } from './fetch.js';
|
|
12
|
+
import { mountDmg, unmountDmg } from './macos.js';
|
|
8
13
|
import { getCfg, getPkgName, getPkgScope } from './pkg.js';
|
|
9
14
|
import { buildLog } from './run.js';
|
|
10
15
|
import { getDevBranch, getVersion } from './version.js';
|
|
@@ -140,7 +145,9 @@ export async function dockerPullAndRunContainer(options) {
|
|
|
140
145
|
await dockerPull({ image, offline, testUrl });
|
|
141
146
|
}
|
|
142
147
|
catch (err) {
|
|
143
|
-
|
|
148
|
+
if (err instanceof Error) {
|
|
149
|
+
buildLog(`docker pull failed (${err.message}), attempting to continue...`);
|
|
150
|
+
}
|
|
144
151
|
}
|
|
145
152
|
const { id } = await dockerContainerRunDaemon({ cmd, image, runArgs });
|
|
146
153
|
if (network) {
|
|
@@ -186,3 +193,73 @@ export async function copyFilesFromDockerImage({ filePaths, ignoreErrors, imageI
|
|
|
186
193
|
throw new Error('Some files failed to copy');
|
|
187
194
|
}
|
|
188
195
|
}
|
|
196
|
+
export const dockerIsRunning = async () => {
|
|
197
|
+
try {
|
|
198
|
+
const result = (await spawn('docker', ['info'], {
|
|
199
|
+
captureOutput: true,
|
|
200
|
+
})).output;
|
|
201
|
+
return !result.toLowerCase().includes('error');
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
export const ensureDockerRunning = async (timeoutSeconds = 600) => {
|
|
208
|
+
if (!(await dockerIsRunning())) {
|
|
209
|
+
if (isMac()) {
|
|
210
|
+
buildLog('docker is not running, starting it...');
|
|
211
|
+
await spawn('open', ['-a', '/Applications/Docker.app']);
|
|
212
|
+
let attempts = 0;
|
|
213
|
+
while (!(await dockerIsRunning())) {
|
|
214
|
+
attempts += 1;
|
|
215
|
+
if (attempts % 5 === 0) {
|
|
216
|
+
process.stdout.write('.');
|
|
217
|
+
}
|
|
218
|
+
if (attempts > timeoutSeconds) {
|
|
219
|
+
throw new Error('Timeout starting docker');
|
|
220
|
+
}
|
|
221
|
+
await new Promise((resolve) => {
|
|
222
|
+
setTimeout(resolve, 1000);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (attempts >= 5) {
|
|
226
|
+
process.stdout.write('\n');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
throw new Error('Docker is not running, please start it manually');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
export const ensureDockerInstalled = async () => {
|
|
235
|
+
if (!(await cmdExists('docker'))) {
|
|
236
|
+
if (isMac()) {
|
|
237
|
+
const dockerUrl = `https://desktop.docker.com/mac/main/${isArm() ? 'arm64' : 'amd64'}/Docker.dmg`;
|
|
238
|
+
const dockerDmg = path.join(os.tmpdir(), 'Docker.dmg');
|
|
239
|
+
buildLog(`docker not found, downloading (${isArm() ? 'arm' : 'intel'})...`);
|
|
240
|
+
await downloadFile(dockerUrl, dockerDmg);
|
|
241
|
+
const dockerVolume = path.join(os.tmpdir(), 'Docker.volume');
|
|
242
|
+
const dockerApp = path.join(dockerVolume, 'Docker.app');
|
|
243
|
+
try {
|
|
244
|
+
buildLog('Mounting Docker.dmg...');
|
|
245
|
+
await mountDmg(dockerDmg, dockerVolume);
|
|
246
|
+
const dockerAppInstalled = path.resolve('/Applications', 'Docker.app');
|
|
247
|
+
buildLog('Copying to applications...');
|
|
248
|
+
await fs.copy(dockerApp, dockerAppInstalled);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
buildLog('Failed to install docker automatically. Get it at https://docs.docker.com/get-docker/ and install manually.');
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
buildLog('Unmounting Docker.dmg...');
|
|
256
|
+
await unmountDmg(dockerVolume);
|
|
257
|
+
await fs.remove(dockerDmg);
|
|
258
|
+
}
|
|
259
|
+
buildLog('Docker installed!');
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
throw new Error('docker is not installed. Get it at https://docs.docker.com/get-docker/');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
package/src/dockerCompose.d.ts
CHANGED
|
@@ -151,4 +151,5 @@ export interface DockerContainerInfo extends UpResult {
|
|
|
151
151
|
url?: null | string;
|
|
152
152
|
}
|
|
153
153
|
export declare function dockerComposeRunService(key: string, upOptions?: UpOptions, dockerCompose?: null | string | DockerCompose): Promise<DockerContainerInfo>;
|
|
154
|
+
export declare const ensureDockerComposeInstalled: () => Promise<void>;
|
|
154
155
|
export {};
|
package/src/dockerCompose.js
CHANGED
|
@@ -18,6 +18,7 @@ import { dockerContainerFind, dockerContainerRunDaemon, } from './docker.contain
|
|
|
18
18
|
import { dockerPull } from './docker.js';
|
|
19
19
|
import { dockerNetworkCreate, dockerNetworkDelete, } from './docker.network.js';
|
|
20
20
|
import { dockerVolumeCreate, dockerVolumeRm, } from './docker.volume.js';
|
|
21
|
+
import { cmdExists } from './env.js';
|
|
21
22
|
import { mapValuesAsync } from './maps.js';
|
|
22
23
|
import { buildLog } from './run.js';
|
|
23
24
|
export const dockerComposeCreateResourceTracker = () => ({
|
|
@@ -405,7 +406,9 @@ export async function dockerComposeTeardown({ includeDefaults = false, includeVo
|
|
|
405
406
|
await dockerNetworkDelete(name);
|
|
406
407
|
}
|
|
407
408
|
catch (err) {
|
|
408
|
-
|
|
409
|
+
if (err instanceof Error) {
|
|
410
|
+
buildLog(`Error deleting network '${name}': ${err.message}`);
|
|
411
|
+
}
|
|
409
412
|
}
|
|
410
413
|
}));
|
|
411
414
|
}
|
|
@@ -437,3 +440,8 @@ export async function dockerComposeRunService(key, upOptions = {}, dockerCompose
|
|
|
437
440
|
url,
|
|
438
441
|
};
|
|
439
442
|
}
|
|
443
|
+
export const ensureDockerComposeInstalled = async () => {
|
|
444
|
+
if (!(await cmdExists('docker-compose'))) {
|
|
445
|
+
throw new Error('docker-compose is not installed. Get it a https://docs.docker.com/compose/install/');
|
|
446
|
+
}
|
|
447
|
+
};
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const isMac: () => boolean;
|
|
2
|
+
export declare const isLinux: () => boolean;
|
|
3
|
+
export declare const isWindows: () => boolean;
|
|
4
|
+
export declare const isArm: () => boolean;
|
|
5
|
+
export declare const isAppleSilicon: () => boolean;
|
|
6
|
+
export declare const shellType: () => string | null;
|
|
7
|
+
export declare const shellEnvFile: () => string | null;
|
|
8
|
+
export declare const whichCmd: (cmd: string) => Promise<null | string>;
|
|
9
|
+
export declare const cmdExists: (cmd: string) => Promise<boolean>;
|
|
10
|
+
interface ReadShellEnvVarOptions {
|
|
11
|
+
envFile?: string | null;
|
|
12
|
+
refreshEnv?: boolean;
|
|
13
|
+
shell?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const readShellEnvVar: (name: string, { refreshEnv, envFile, shell, }?: ReadShellEnvVarOptions) => Promise<string | null>;
|
|
16
|
+
export declare const appendEnvVarToProfile: (name: string, value: string, { envFile }?: ReadShellEnvVarOptions) => Promise<void>;
|
|
17
|
+
export declare const ensureEnvVarValue: (name: string, value: string, opts?: ReadShellEnvVarOptions | undefined) => Promise<void>;
|
|
18
|
+
export declare const ensureEnvVarSet: (name: string, getValue: () => Promise<string>) => Promise<void>;
|
|
19
|
+
export {};
|
package/src/env.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn } from './cp.js';
|
|
5
|
+
import { appendIfMissing } from './fs.js';
|
|
6
|
+
const boolCache = new Map();
|
|
7
|
+
const cacheBool = (key, fn) => {
|
|
8
|
+
if (!boolCache.has(key)) {
|
|
9
|
+
boolCache.set(key, fn());
|
|
10
|
+
}
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
12
|
+
return boolCache.get(key);
|
|
13
|
+
};
|
|
14
|
+
export const isMac = () => cacheBool('isMac', () => os.platform() === 'darwin');
|
|
15
|
+
export const isLinux = () => cacheBool('isLinux', () => os.platform() === 'linux');
|
|
16
|
+
export const isWindows = () => cacheBool('isWindows', () => os.platform() === 'win32');
|
|
17
|
+
export const isArm = () => cacheBool('isArm', () => os.arch() === 'arm64' ||
|
|
18
|
+
(isMac() && os.cpus().some((cpu) => cpu.model.includes(' M1 '))));
|
|
19
|
+
export const isAppleSilicon = () => cacheBool('isAppleSilicon', () => isMac() && isArm());
|
|
20
|
+
const stringCache = new Map();
|
|
21
|
+
const cacheString = (key, fn) => {
|
|
22
|
+
if (!stringCache.has(key)) {
|
|
23
|
+
stringCache.set(key, fn());
|
|
24
|
+
}
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
26
|
+
return stringCache.get(key);
|
|
27
|
+
};
|
|
28
|
+
export const shellType = () => cacheString('shellType', () => os.userInfo().shell.split('/').pop() || null);
|
|
29
|
+
export const shellEnvFile = () => cacheString('shellEnvFile', () => {
|
|
30
|
+
switch (shellType()) {
|
|
31
|
+
case 'zsh':
|
|
32
|
+
return path.join(os.homedir(), '.zprofile');
|
|
33
|
+
case 'bash':
|
|
34
|
+
return path.join(os.homedir(), '.bash_profile');
|
|
35
|
+
default:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
export const whichCmd = async (cmd) => {
|
|
40
|
+
const binPath = (await spawn('which', [cmd], { captureOutput: true })).output.trim();
|
|
41
|
+
if (!binPath || !(await fs.pathExists(binPath))) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return binPath;
|
|
45
|
+
};
|
|
46
|
+
export const cmdExists = async (cmd) => !!whichCmd(cmd);
|
|
47
|
+
export const readShellEnvVar = async (name, { refreshEnv = false, envFile = shellEnvFile(), shell = os.userInfo().shell, } = {}) => {
|
|
48
|
+
if (refreshEnv && (!envFile || !(await fs.pathExists(envFile)))) {
|
|
49
|
+
throw new Error(`Could not find shell env file: ${envFile}`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return (await spawn(shell, ['-c', `${refreshEnv ? `source ${envFile} && ` : ''}printenv ${name}`], {
|
|
53
|
+
captureOutput: true,
|
|
54
|
+
})).stdout.trim();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
export const appendEnvVarToProfile = async (name, value, { envFile = shellEnvFile() } = {}) => {
|
|
61
|
+
if (!envFile) {
|
|
62
|
+
throw new Error('envFile must be set');
|
|
63
|
+
}
|
|
64
|
+
await appendIfMissing(path.join(os.homedir(), envFile), `\nexport ${name}=${value}`);
|
|
65
|
+
};
|
|
66
|
+
export const ensureEnvVarValue = async (name, value, opts) => {
|
|
67
|
+
const currentValue = await readShellEnvVar(name, opts);
|
|
68
|
+
if (currentValue !== value) {
|
|
69
|
+
await appendEnvVarToProfile(name, value, opts);
|
|
70
|
+
}
|
|
71
|
+
process.env[name] = value;
|
|
72
|
+
};
|
|
73
|
+
export const ensureEnvVarSet = async (name, getValue) => {
|
|
74
|
+
const currentValue = await readShellEnvVar(name);
|
|
75
|
+
if (!currentValue) {
|
|
76
|
+
const newValue = await getValue();
|
|
77
|
+
await appendEnvVarToProfile(name, newValue);
|
|
78
|
+
process.env[name] = newValue;
|
|
79
|
+
}
|
|
80
|
+
};
|
package/src/fetch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const downloadFile: (url: string, saveAs: string, overwrite?: boolean) => Promise<void>;
|
package/src/fetch.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export const downloadFile = async (url, saveAs, overwrite = true) => {
|
|
5
|
+
const res = await fetch(url);
|
|
6
|
+
const { body, ok } = res;
|
|
7
|
+
if (!ok || !body) {
|
|
8
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
9
|
+
}
|
|
10
|
+
await fs.ensureDir(path.dirname(saveAs));
|
|
11
|
+
if (overwrite) {
|
|
12
|
+
await fs.remove(saveAs);
|
|
13
|
+
}
|
|
14
|
+
const fileStream = fs.createWriteStream(saveAs);
|
|
15
|
+
await new Promise((resolve, reject) => {
|
|
16
|
+
body.pipe(fileStream);
|
|
17
|
+
body.on('error', reject);
|
|
18
|
+
fileStream.on('finish', resolve);
|
|
19
|
+
});
|
|
20
|
+
};
|
package/src/fs.d.ts
CHANGED
|
@@ -3,3 +3,9 @@ export declare const readDir: (pattern: string, options?: glob.IOptions | undefi
|
|
|
3
3
|
export declare const moveDir: (source: string, target: string) => Promise<void>;
|
|
4
4
|
export declare const copyDir: (source: string, target: string, fileGlob?: string, globOptions?: null | glob.IOptions, renameFn?: (name: string) => string) => Promise<void>;
|
|
5
5
|
export declare const cleanDir: (pattern: string, globOptions?: false | glob.IOptions | undefined) => Promise<void>;
|
|
6
|
+
export declare const producedPathExists: (producer: () => Promise<string>) => Promise<boolean>;
|
|
7
|
+
export declare const copyIfMissing: (src: string, dest: string) => Promise<void>;
|
|
8
|
+
declare type StringGenFn = () => string | Promise<string>;
|
|
9
|
+
export declare const writeIfMissing: (dest: string, content: string | StringGenFn) => Promise<boolean>;
|
|
10
|
+
export declare const appendIfMissing: (dest: string, content: string | StringGenFn, testContent?: string | StringGenFn | undefined) => Promise<void>;
|
|
11
|
+
export {};
|
package/src/fs.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import glob from 'glob';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import rimraf from 'rimraf';
|
|
5
|
+
import { resolveOnce } from './promise.js';
|
|
6
|
+
import { buildLog } from './run.js';
|
|
5
7
|
export const readDir = async (pattern, options) => new Promise((resolve, reject) => {
|
|
6
8
|
glob(pattern, options || {}, (err, result) => err ? reject(err) : resolve(result));
|
|
7
9
|
});
|
|
@@ -36,3 +38,39 @@ export const copyDir = async (source, target, fileGlob = '**/*.*', globOptions =
|
|
|
36
38
|
export const cleanDir = async (pattern, globOptions) => new Promise((resolve, reject) => {
|
|
37
39
|
rimraf(pattern, { glob: globOptions }, (err) => err ? reject(err) : resolve());
|
|
38
40
|
});
|
|
41
|
+
export const producedPathExists = async (producer) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = (await producer()).trim();
|
|
44
|
+
return !!result && fs.pathExists(result);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const copyIfMissing = async (src, dest) => {
|
|
51
|
+
if (!(await fs.pathExists(dest))) {
|
|
52
|
+
buildLog(`${dest} not found, copying...`);
|
|
53
|
+
await fs.ensureDir(path.dirname(dest));
|
|
54
|
+
await fs.copy(src, dest);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const resolveStringGen = async (content) => (typeof content === 'function' ? content() : content);
|
|
58
|
+
export const writeIfMissing = async (dest, content) => {
|
|
59
|
+
if (!(await fs.pathExists(dest))) {
|
|
60
|
+
buildLog(`${dest} not found, writing...`);
|
|
61
|
+
await fs.ensureDir(path.dirname(dest));
|
|
62
|
+
await fs.writeFile(dest, await resolveStringGen(content));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
export const appendIfMissing = async (dest, content, testContent) => {
|
|
68
|
+
if (!(await writeIfMissing(dest, content))) {
|
|
69
|
+
const existingContent = await fs.readFile(dest, 'utf8');
|
|
70
|
+
const getContent = resolveOnce(() => resolveStringGen(content));
|
|
71
|
+
if (!existingContent.includes(await (testContent ? resolveStringGen(testContent) : getContent()))) {
|
|
72
|
+
buildLog(`${dest} not yet updated, appending...`);
|
|
73
|
+
await fs.appendFile(dest, await getContent());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
package/src/gh.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SpawnOptions } from './cp.js';
|
|
2
|
+
export declare const gh: (args: string[], options?: SpawnOptions | undefined) => Promise<import("./cp.js").SpawnResult>;
|
|
3
|
+
export declare const ghRepoClone: (repo: string, repoDir: string) => Promise<void>;
|
|
4
|
+
export declare const ensureGhAuthLogin: (host?: string, scopes?: string | null) => Promise<void>;
|
|
5
|
+
export declare const ensureGhSshKeyAdded: (privateKeyPath?: string | undefined, keyTitle?: string | undefined) => Promise<void>;
|
|
6
|
+
export declare const ensureGhConfig: (key: string, value: string, host?: string) => Promise<void>;
|
|
7
|
+
export declare const ensureGhConfigSshGitProtocol: (host?: string) => Promise<void>;
|
package/src/gh.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { spawn } from './cp.js';
|
|
3
|
+
import { buildLog } from './run.js';
|
|
4
|
+
import { ensureSshKeyGenerated } from './ssh.js';
|
|
5
|
+
export const gh = async (args, options) => spawn('gh', args, options);
|
|
6
|
+
export const ghRepoClone = async (repo, repoDir) => {
|
|
7
|
+
await gh(['repo', 'clone', repo, repoDir], {
|
|
8
|
+
stdio: 'inherit',
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
export const ensureGhAuthLogin = async (host = 'github.com', scopes = null) => {
|
|
12
|
+
const configured = (await gh(['auth', 'status'], { rejectOnErrorCode: false })).code === 0;
|
|
13
|
+
if (!configured) {
|
|
14
|
+
buildLog('Logging in to GitHub...');
|
|
15
|
+
await gh(['auth', 'login', '-h', host, '-w', ...(scopes ? ['-s', scopes] : [])], {
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export const ensureGhSshKeyAdded = async (privateKeyPath, keyTitle) => {
|
|
21
|
+
let listResult = await gh(['ssh-key', 'list'], {
|
|
22
|
+
captureOutput: true,
|
|
23
|
+
rejectOnErrorCode: false,
|
|
24
|
+
});
|
|
25
|
+
if (listResult.code !== 0) {
|
|
26
|
+
// Probably don't have the scope
|
|
27
|
+
buildLog('Adding admin:public_key scope to GitHub cli...');
|
|
28
|
+
await gh(['auth', 'refresh', '-s', 'admin:public_key'], {
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
});
|
|
31
|
+
listResult = await gh(['ssh-key', 'list'], {
|
|
32
|
+
captureOutput: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const pubKeyPath = `${await ensureSshKeyGenerated(privateKeyPath)}.pub`;
|
|
36
|
+
const pubKey = await fs.readFile(pubKeyPath, 'utf8');
|
|
37
|
+
const existing = listResult.output.split('\n').find((line) => {
|
|
38
|
+
const [, key] = line.split('\t');
|
|
39
|
+
return pubKey.includes(key);
|
|
40
|
+
});
|
|
41
|
+
if (!existing) {
|
|
42
|
+
buildLog('Adding SSH key to GitHub...');
|
|
43
|
+
await spawn('gh', ['ssh-key', 'add', pubKeyPath, ...(keyTitle ? ['-t', keyTitle] : [])], {
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
export const ensureGhConfig = async (key, value, host = 'github.com') => {
|
|
49
|
+
await gh(['config', 'set', '-h', host, key, value]);
|
|
50
|
+
};
|
|
51
|
+
export const ensureGhConfigSshGitProtocol = async (host = 'github.com') => ensureGhConfig('git_protocol', 'ssh', host);
|
package/src/git.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { SpawnOptions } from './cp.js';
|
|
2
|
+
export declare const git: (args: string[], opts?: SpawnOptions | undefined) => Promise<string>;
|
|
2
3
|
export declare function gitBranch(): Promise<string>;
|
|
3
4
|
export declare function gitRevId(): Promise<string>;
|
|
4
5
|
declare type GitInfo = {
|
|
@@ -6,4 +7,8 @@ declare type GitInfo = {
|
|
|
6
7
|
revision: string;
|
|
7
8
|
};
|
|
8
9
|
export declare function gitInfo(noCache?: boolean): Promise<GitInfo>;
|
|
10
|
+
export declare const ensureGitLfsInstalled: () => Promise<void>;
|
|
11
|
+
export declare const gitConfigRead: (key: string, global?: boolean) => Promise<string>;
|
|
12
|
+
export declare const gitConfigWrite: (key: string, value: string, global?: boolean) => Promise<void>;
|
|
13
|
+
export declare const ensureGitConfigUserEmail: (emailRegex?: RegExp, emailPrompt?: string, global?: boolean) => Promise<void>;
|
|
9
14
|
export {};
|
package/src/git.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
import { spawn } from './cp.js';
|
|
2
|
+
import { userQuestion } from './prompt.js';
|
|
3
|
+
import { buildLog } from './run.js';
|
|
4
|
+
export const git = async (args, opts) => (await spawn('git', args, { captureOutput: true, ...opts })).stdout.trim();
|
|
5
5
|
export async function gitBranch() {
|
|
6
6
|
return (await git(['symbolic-ref', '--short', 'HEAD']))
|
|
7
7
|
.replace(/[_+/]/g, '-')
|
|
@@ -22,3 +22,23 @@ export async function gitInfo(noCache = false) {
|
|
|
22
22
|
}
|
|
23
23
|
return info;
|
|
24
24
|
}
|
|
25
|
+
export const ensureGitLfsInstalled = async () => {
|
|
26
|
+
await git(['lfs', 'install'], { stdio: 'inherit' });
|
|
27
|
+
};
|
|
28
|
+
export const gitConfigRead = async (key, global = true) => git(['config', ...(global ? ['--global'] : []), key], {
|
|
29
|
+
rejectOnErrorCode: false,
|
|
30
|
+
});
|
|
31
|
+
export const gitConfigWrite = async (key, value, global = true) => {
|
|
32
|
+
await git(['config', ...(global ? ['--global'] : []), key, value]);
|
|
33
|
+
};
|
|
34
|
+
export const ensureGitConfigUserEmail = async (emailRegex = /.+@.+/, emailPrompt = 'Enter your email address: ', global = true) => {
|
|
35
|
+
const existing = await gitConfigRead('user.email', global);
|
|
36
|
+
if (emailRegex.test(existing)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
buildLog('Setting up git user config...');
|
|
40
|
+
const name = await userQuestion('Enter your full name: ');
|
|
41
|
+
await gitConfigWrite('user.name', name, global);
|
|
42
|
+
const email = await userQuestion(emailPrompt, (input) => emailRegex.test(input));
|
|
43
|
+
await gitConfigWrite('user.email', email, global);
|
|
44
|
+
};
|
package/src/index.d.ts
CHANGED
|
@@ -10,10 +10,13 @@ export * from './docker.volume.js';
|
|
|
10
10
|
export * from './dockerCleanup.js';
|
|
11
11
|
export * from './dockerCompose.js';
|
|
12
12
|
export * from './fs.js';
|
|
13
|
+
export * from './gh.js';
|
|
13
14
|
export * from './git.js';
|
|
14
15
|
export * from './npm.js';
|
|
15
16
|
export * from './pkg.js';
|
|
16
17
|
export * from './publish.js';
|
|
18
|
+
export * from './promise.js';
|
|
19
|
+
export * from './prompt.js';
|
|
17
20
|
export * from './run.js';
|
|
18
21
|
export * from './tgz.js';
|
|
19
22
|
export * from './version.js';
|
package/src/index.js
CHANGED
|
@@ -10,10 +10,13 @@ export * from './docker.volume.js';
|
|
|
10
10
|
export * from './dockerCleanup.js';
|
|
11
11
|
export * from './dockerCompose.js';
|
|
12
12
|
export * from './fs.js';
|
|
13
|
+
export * from './gh.js';
|
|
13
14
|
export * from './git.js';
|
|
14
15
|
export * from './npm.js';
|
|
15
16
|
export * from './pkg.js';
|
|
16
17
|
export * from './publish.js';
|
|
18
|
+
export * from './promise.js';
|
|
19
|
+
export * from './prompt.js';
|
|
17
20
|
export * from './run.js';
|
|
18
21
|
export * from './tgz.js';
|
|
19
22
|
export * from './version.js';
|
package/src/macos.d.ts
ADDED
package/src/macos.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { spawn } from './cp.js';
|
|
3
|
+
import { cmdExists, isMac } from './env.js';
|
|
4
|
+
import { producedPathExists } from './fs.js';
|
|
5
|
+
import { buildLog } from './run.js';
|
|
6
|
+
export const mountDmg = async (dmgPath, mountPath) => {
|
|
7
|
+
await fs.ensureDir(mountPath);
|
|
8
|
+
await spawn('hdiutil', [
|
|
9
|
+
'attach',
|
|
10
|
+
dmgPath,
|
|
11
|
+
'-mountpoint',
|
|
12
|
+
mountPath,
|
|
13
|
+
'-nobrowse',
|
|
14
|
+
'-quiet',
|
|
15
|
+
]);
|
|
16
|
+
};
|
|
17
|
+
export const unmountDmg = async (mountPath) => {
|
|
18
|
+
await spawn('hdiutil', ['detach', mountPath, '-force', '-quiet']);
|
|
19
|
+
};
|
|
20
|
+
export const ensureXcodeCmdInstalled = async (ignoreNonMac = true) => {
|
|
21
|
+
if (isMac()) {
|
|
22
|
+
if (!ignoreNonMac) {
|
|
23
|
+
throw new Error('Cannot install Xcode on non-macOS');
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!(await cmdExists('xcode-select'))) {
|
|
28
|
+
throw new Error('XCode is not installed, get from the app store.');
|
|
29
|
+
}
|
|
30
|
+
if (!(await producedPathExists(async () => (await spawn('xcode-select', ['-p'], { captureOutput: true })).output))) {
|
|
31
|
+
buildLog('Installing XCode Command Line Tools...');
|
|
32
|
+
await spawn('sudo', ['xcode-select', '--install'], {
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
package/src/npm.js
CHANGED
|
@@ -38,7 +38,9 @@ export async function npmGetVersions(packageName, npmPath) {
|
|
|
38
38
|
})).stdout.trim());
|
|
39
39
|
}
|
|
40
40
|
catch (err) {
|
|
41
|
-
|
|
41
|
+
if (err instanceof Error) {
|
|
42
|
+
buildLog(`warning: failed to find existing package for ${packageName} in npm registry: ${err.message}`);
|
|
43
|
+
}
|
|
42
44
|
return [];
|
|
43
45
|
}
|
|
44
46
|
}
|
package/src/promise.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This generates a function that, when invoked, will only invoke the producer once.
|
|
3
|
+
* On subsequent calls, the previously produced value will be returned.
|
|
4
|
+
* @param producer a function that produces a promise, will be invoked at most once
|
|
5
|
+
* @returns a function that returns a promise
|
|
6
|
+
*/
|
|
7
|
+
export declare const resolveOnce: <T>(producer: () => Promise<T>) => () => Promise<T>;
|
package/src/promise.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This generates a function that, when invoked, will only invoke the producer once.
|
|
3
|
+
* On subsequent calls, the previously produced value will be returned.
|
|
4
|
+
* @param producer a function that produces a promise, will be invoked at most once
|
|
5
|
+
* @returns a function that returns a promise
|
|
6
|
+
*/
|
|
7
|
+
export const resolveOnce = (producer) => {
|
|
8
|
+
let promisedResult;
|
|
9
|
+
return async () => {
|
|
10
|
+
if (!promisedResult) {
|
|
11
|
+
promisedResult = producer();
|
|
12
|
+
}
|
|
13
|
+
return promisedResult;
|
|
14
|
+
};
|
|
15
|
+
};
|
package/src/prompt.d.ts
ADDED
package/src/prompt.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'process';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
export const ask = async (question) => {
|
|
4
|
+
// Once we move to Node 18+, can replace this with a builtin Promise interface
|
|
5
|
+
const rl = readline.createInterface({ input, output });
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
rl.question(question, (value) => {
|
|
8
|
+
rl.close();
|
|
9
|
+
resolve(value.trim());
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export const userQuestion = async (question, test = (v) => !!v) => {
|
|
14
|
+
let answer;
|
|
15
|
+
do {
|
|
16
|
+
answer = await ask(question);
|
|
17
|
+
} while (!(await test(answer)));
|
|
18
|
+
return answer;
|
|
19
|
+
};
|
package/src/ssh.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ensureSshKeyGenerated: (keyPath?: string | undefined) => Promise<string>;
|
package/src/ssh.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn } from './cp.js';
|
|
5
|
+
import { buildLog } from './run.js';
|
|
6
|
+
export const ensureSshKeyGenerated = async (keyPath) => {
|
|
7
|
+
const sshKeyPath = keyPath || path.join(os.homedir(), '.ssh', 'id_ed25519');
|
|
8
|
+
if (!(await fs.pathExists(sshKeyPath))) {
|
|
9
|
+
buildLog('Generating SSH key (passphrase is optional)...');
|
|
10
|
+
await spawn('ssh-keygen', ['-t', 'ed25519', '-f', sshKeyPath]);
|
|
11
|
+
}
|
|
12
|
+
return sshKeyPath;
|
|
13
|
+
};
|