build-strap 4.0.0 → 4.1.0-dev.60

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.
Files changed (66) hide show
  1. package/package.json +7 -57
  2. package/{dist/src → src}/ant.d.ts +0 -0
  3. package/{dist/src → src}/ant.js +0 -0
  4. package/src/brew.d.ts +6 -0
  5. package/src/brew.js +74 -0
  6. package/{dist/src → src}/cert.d.ts +0 -0
  7. package/{dist/src → src}/cert.js +0 -0
  8. package/{dist/src → src}/clean.d.ts +0 -0
  9. package/{dist/src → src}/clean.js +0 -0
  10. package/{dist/src → src}/copy.d.ts +0 -0
  11. package/{dist/src → src}/copy.js +0 -0
  12. package/{dist/src → src}/cp.d.ts +0 -0
  13. package/{dist/src → src}/cp.js +1 -0
  14. package/{dist/src → src}/docker.container.d.ts +0 -0
  15. package/{dist/src → src}/docker.container.js +0 -0
  16. package/{dist/src → src}/docker.d.ts +4 -1
  17. package/{dist/src → src}/docker.js +78 -1
  18. package/{dist/src → src}/docker.network.d.ts +0 -0
  19. package/{dist/src → src}/docker.network.js +0 -0
  20. package/{dist/src → src}/docker.volume.d.ts +0 -0
  21. package/{dist/src → src}/docker.volume.js +0 -0
  22. package/{dist/src → src}/dockerCleanup.d.ts +0 -0
  23. package/{dist/src → src}/dockerCleanup.js +0 -0
  24. package/{dist/src → src}/dockerCompose.d.ts +1 -0
  25. package/{dist/src → src}/dockerCompose.js +9 -1
  26. package/src/env.d.ts +19 -0
  27. package/src/env.js +80 -0
  28. package/src/fetch.d.ts +1 -0
  29. package/src/fetch.js +20 -0
  30. package/{dist/src → src}/fs.d.ts +6 -0
  31. package/src/fs.js +76 -0
  32. package/src/gh.d.ts +7 -0
  33. package/src/gh.js +51 -0
  34. package/src/git.d.ts +14 -0
  35. package/src/git.js +44 -0
  36. package/{dist/src → src}/index.d.ts +4 -0
  37. package/{dist/src → src}/index.js +4 -0
  38. package/src/macos.d.ts +3 -0
  39. package/src/macos.js +36 -0
  40. package/{dist/src → src}/maps.d.ts +0 -0
  41. package/{dist/src → src}/maps.js +0 -0
  42. package/{dist/src → src}/npm.d.ts +16 -3
  43. package/src/npm.js +143 -0
  44. package/{dist/src → src}/pkg.d.ts +6 -1
  45. package/{dist/src → src}/pkg.js +0 -0
  46. package/src/promise.d.ts +7 -0
  47. package/src/promise.js +15 -0
  48. package/src/prompt.d.ts +2 -0
  49. package/src/prompt.js +19 -0
  50. package/{dist/src → src}/publish.d.ts +0 -0
  51. package/{dist/src → src}/publish.js +0 -0
  52. package/{dist/src → src}/run.d.ts +0 -0
  53. package/{dist/src → src}/run.js +0 -0
  54. package/src/ssh.d.ts +1 -0
  55. package/src/ssh.js +13 -0
  56. package/{dist/src → src}/tgz.d.ts +0 -0
  57. package/{dist/src → src}/tgz.js +0 -0
  58. package/{dist/src → src}/version.d.ts +0 -0
  59. package/{dist/src → src}/version.js +1 -1
  60. package/{dist/src → src}/yarn.d.ts +18 -0
  61. package/src/yarn.js +155 -0
  62. package/dist/src/fs.js +0 -38
  63. package/dist/src/git.d.ts +0 -9
  64. package/dist/src/git.js +0 -24
  65. package/dist/src/npm.js +0 -104
  66. package/dist/src/yarn.js +0 -75
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "build-strap",
3
- "version": "4.0.0",
3
+ "version": "4.1.0-dev.60",
4
4
  "description": "Node scripts for building things",
5
5
  "license": "MIT",
6
6
  "authors": [
@@ -11,26 +11,11 @@
11
11
  "url": "https://github.com/murrayju/build-strap"
12
12
  },
13
13
  "type": "module",
14
- "module": "dist/index.js",
15
- "types": "dist/index.d.js",
14
+ "main": "./src/index.js",
15
+ "types": "./src/index.d.ts",
16
16
  "engines": {
17
17
  "node": ">=16"
18
18
  },
19
- "buildStrap": {
20
- "nodeVersion": "16.13.1",
21
- "yarnVersion": "1.22.17",
22
- "repoType": "git",
23
- "copyright": "Justin Murray",
24
- "releaseBranch": "master",
25
- "devBranch": "dev",
26
- "npm": {
27
- "publish": true
28
- },
29
- "docker": {
30
- "repository": "murrayju",
31
- "registry": "ghcr.io"
32
- }
33
- },
34
19
  "dependencies": {
35
20
  "bytes": "^3.1.1",
36
21
  "chokidar": "^3.5.2",
@@ -43,43 +28,8 @@
43
28
  "lodash-es": "^4.17.21",
44
29
  "moment": "^2.29.1",
45
30
  "node-fetch": "^3.1.0",
31
+ "rimraf": "^3.0.2",
46
32
  "stream-counter": "1",
47
- "tar": "^6.1.11",
48
- "url-join": "^4.0.1"
49
- },
50
- "devDependencies": {
51
- "@tsconfig/node16": "^1.0.2",
52
- "@types/bytes": "^3.1.1",
53
- "@types/cross-spawn": "^6.0.2",
54
- "@types/eslint": "^8.2.1",
55
- "@types/fs-extra": "^9.0.13",
56
- "@types/glob": "^7.2.0",
57
- "@types/js-yaml": "^4.0.5",
58
- "@types/lodash-es": "^4.17.5",
59
- "@types/rimraf": "^3.0.2",
60
- "@types/tar": "^6.1.1",
61
- "@typescript-eslint/eslint-plugin": "^5.8.0",
62
- "@typescript-eslint/parser": "^5.8.0",
63
- "camelcase": "^6.2.0",
64
- "eslint": "^8.5.0",
65
- "eslint-config-airbnb-base": "^15.0.0",
66
- "eslint-config-prettier": "^8.3.0",
67
- "eslint-import-resolver-node": "^0.3.6",
68
- "eslint-plugin-import": "^2.22.1",
69
- "eslint-plugin-jsx-a11y": "^6.4.1",
70
- "eslint-plugin-prettier": "^4.0.0",
71
- "eslint-plugin-react": "^7.22.0",
72
- "eslint-plugin-sort-destructure-keys": "^1.4.0",
73
- "eslint-plugin-sort-keys-fix": "^1.1.2",
74
- "eslint-plugin-typescript-sort-keys": "^2.1.0",
75
- "prettier": "^2.2.1",
76
- "ts-node": "^10.4.0",
77
- "typescript": "^4.5.4"
78
- },
79
- "scripts": {
80
- "target": "node --loader ts-node/esm ./targets/run.ts"
81
- },
82
- "files": [
83
- "dist/src"
84
- ]
85
- }
33
+ "tar": "^6.1.11"
34
+ }
35
+ }
File without changes
File without changes
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
+ };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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
File without changes
File without changes
@@ -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<any>;
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 {};
@@ -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
- buildLog(`docker pull failed (${err.message}), attempting to continue...`);
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
+ };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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 {};
@@ -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
- buildLog(`Error deleting network '${name}': ${err.message}`);
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
+ };
@@ -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 ADDED
@@ -0,0 +1,76 @@
1
+ import fs from 'fs-extra';
2
+ import glob from 'glob';
3
+ import path from 'path';
4
+ import rimraf from 'rimraf';
5
+ import { resolveOnce } from './promise.js';
6
+ import { buildLog } from './run.js';
7
+ export const readDir = async (pattern, options) => new Promise((resolve, reject) => {
8
+ glob(pattern, options || {}, (err, result) => err ? reject(err) : resolve(result));
9
+ });
10
+ export const moveDir = async (source, target) => {
11
+ const dirs = await readDir('**/*.*', {
12
+ cwd: source,
13
+ dot: true,
14
+ nosort: true,
15
+ });
16
+ await Promise.all(dirs.map(async (dir) => {
17
+ const from = path.resolve(source, dir);
18
+ const to = path.resolve(target, dir);
19
+ await fs.ensureDir(path.dirname(to));
20
+ await fs.rename(from, to);
21
+ }));
22
+ };
23
+ export const copyDir = async (source, target, fileGlob = '**/*.*', globOptions = null, renameFn = (n) => n) => {
24
+ const paths = await readDir(fileGlob, {
25
+ cwd: source,
26
+ dot: true,
27
+ nodir: true,
28
+ nosort: true,
29
+ ...globOptions,
30
+ });
31
+ await Promise.all(paths.map(async (p) => {
32
+ const from = path.resolve(source, p);
33
+ const to = path.resolve(target, renameFn(p));
34
+ await fs.ensureDir(path.dirname(to));
35
+ await fs.copyFile(from, to);
36
+ }));
37
+ };
38
+ export const cleanDir = async (pattern, globOptions) => new Promise((resolve, reject) => {
39
+ rimraf(pattern, { glob: globOptions }, (err) => err ? reject(err) : resolve());
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 ADDED
@@ -0,0 +1,14 @@
1
+ import { SpawnOptions } from './cp.js';
2
+ export declare const git: (args: string[], opts?: SpawnOptions | undefined) => Promise<string>;
3
+ export declare function gitBranch(): Promise<string>;
4
+ export declare function gitRevId(): Promise<string>;
5
+ declare type GitInfo = {
6
+ branch: string;
7
+ revision: string;
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>;
14
+ export {};