@monosmith/cli 2.0.0-beta.5 → 2.0.0-beta.51

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 (2) hide show
  1. package/bin/smith.js +145 -26
  2. package/package.json +7 -7
package/bin/smith.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
  // Runs the smith binary npm installed for this platform, from one of the
4
- // optional dependencies; nothing is downloaded after install.
5
- const { spawnSync } = require('node:child_process')
4
+ // optional dependencies; nothing is downloaded after install. When that package
5
+ // is missing, it says why and offers a fix, and changes nothing without a yes.
6
+ const { spawn, spawnSync } = require('node:child_process')
6
7
  const fs = require('node:fs')
7
8
 
8
9
  const platforms = {
@@ -13,33 +14,151 @@ const platforms = {
13
14
  "win32-arm64": "@monosmith/cli-win32-arm64",
14
15
  "win32-x64": "@monosmith/cli-win32-x64"
15
16
  }
16
- const key = process.platform + '-' + process.arch
17
- const pkg = platforms[key]
18
- if (!pkg) {
19
- console.error('smith has no build for ' + key)
20
- process.exit(1)
17
+ const NAME = 'smith'
18
+ const PROBE_TIMEOUT_MS = 5000
19
+ const RETRY_EVERY_MS = 30000
20
+ const RETRY_FOR_MS = 10 * 60 * 1000
21
+
22
+ // probe asks npm, with a short limit, whether pkg@version is on the registry.
23
+ // It returns 'exists', 'absent' or 'unknown' (npm could not be asked).
24
+ function probe(d, pkg) {
25
+ const r = d.npm(['view', pkg + '@' + d.version, 'version'])
26
+ if (r.error || r.signal || r.status === null) return 'unknown'
27
+ if (r.status === 0) return String(r.stdout).trim() === d.version ? 'exists' : 'absent'
28
+ return /E404|404 Not Found|is not in this registry/i.test(String(r.stderr)) ? 'absent' : 'unknown'
29
+ }
30
+
31
+ // ask puts a question and returns true for yes. Enter takes the default.
32
+ async function ask(d, question, defaultYes) {
33
+ const answer = String(await d.ask(question + (defaultYes ? ' [Y/n] ' : ' [y/N] '))).trim().toLowerCase()
34
+ if (answer === '') return defaultYes
35
+ return answer === 'y' || answer === 'yes'
21
36
  }
22
- const file = process.platform === 'win32' ? 'smith.exe' : 'smith'
23
- let binary
24
- try {
25
- binary = require.resolve(pkg + '/bin/' + file)
26
- } catch {
27
- console.error('smith: ' + pkg + ' is not installed; reinstall without --omit=optional or --no-optional')
28
- process.exit(1)
37
+
38
+ // install runs the exact command it printed, and says if it worked.
39
+ function install(d, pkg) {
40
+ const r = d.npm(['install', '-g', pkg + '@' + d.version], { stdio: 'inherit', timeout: 0 })
41
+ return !r.error && r.status === 0
29
42
  }
30
- if (process.platform !== 'win32') {
31
- try {
32
- fs.accessSync(binary, fs.constants.X_OK)
33
- } catch {
34
- try { fs.chmodSync(binary, 0o755) } catch { /* a read-only install keeps its mode */ }
43
+
44
+ // offerInstall is situation (a): the package is on the registry now.
45
+ async function offerInstall(d, pkg) {
46
+ const command = 'npm install -g ' + pkg + '@' + d.version
47
+ d.log(NAME + ': ' + pkg + '@' + d.version + ' is on the npm registry, so your install missed it (an interrupted or partial install).')
48
+ d.log(NAME + ': to fix it, run: ' + command)
49
+ let yes = false
50
+ if (d.env.SMITH_AUTO_FIX === '1') {
51
+ d.log(NAME + ': SMITH_AUTO_FIX=1 is set, so it runs now.')
52
+ yes = true
53
+ } else if (d.isTTY) {
54
+ yes = await ask(d, 'Install it now?', true)
55
+ }
56
+ if (!yes) return null
57
+ if (!install(d, pkg)) {
58
+ d.log(NAME + ': the install failed; run the command above yourself to see why.')
59
+ return null
35
60
  }
61
+ return d.resolve(pkg)
36
62
  }
37
- const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' })
38
- if (result.error) {
39
- console.error('smith: ' + result.error.message)
40
- process.exit(1)
63
+
64
+ // waitAndRetry is situation (b) when the person agrees to wait.
65
+ async function waitAndRetry(d, pkg) {
66
+ for (let waited = 0; waited < RETRY_FOR_MS; waited += RETRY_EVERY_MS) {
67
+ d.log(NAME + ': waiting for ' + pkg + '@' + d.version + ' to reach the registry (' + Math.round(waited / 1000) + ' s of ' + RETRY_FOR_MS / 1000 + ' s); press Ctrl+C to stop.')
68
+ await d.sleep(RETRY_EVERY_MS)
69
+ const state = probe(d, pkg)
70
+ if (state === 'exists') return offerInstall(d, pkg)
71
+ if (state === 'unknown') {
72
+ d.log(NAME + ': npm could not be asked any more, so it stops waiting.')
73
+ return null
74
+ }
75
+ }
76
+ d.log(NAME + ': ' + pkg + '@' + d.version + ' is still not on the registry after ' + RETRY_FOR_MS / 60000 + ' minutes. Try again later.')
77
+ return null
41
78
  }
42
- if (result.signal) {
43
- process.kill(process.pid, result.signal)
79
+
80
+ // resolveBinary returns the path of the binary of this platform. When the
81
+ // platform package is missing it tells which of three situations this is
82
+ // (on the registry now, not there yet, or npm cannot be asked) and returns
83
+ // null unless a fix was agreed to and worked.
84
+ async function resolveBinary(d) {
85
+ const pkg = platforms[d.key]
86
+ if (!pkg) {
87
+ d.log(NAME + ' has no build for ' + d.key)
88
+ return null
89
+ }
90
+ const found = d.resolve(pkg)
91
+ if (found) return found
92
+ d.log(NAME + ': ' + pkg + ' is not installed. npm skips an optional package without an error when it cannot get it.')
93
+ switch (probe(d, pkg)) {
94
+ case 'exists':
95
+ return offerInstall(d, pkg)
96
+ case 'absent':
97
+ d.log(NAME + ': ' + pkg + '@' + d.version + ' is not on the npm registry yet. A new release can take 5 to 10 minutes to spread. Try again in a few minutes.')
98
+ if (d.isTTY && await ask(d, 'Wait and retry every 30 seconds, for up to 10 minutes?', false)) return waitAndRetry(d, pkg)
99
+ return null
100
+ default:
101
+ d.log(NAME + ': npm could not be asked (it is not in PATH, it did not answer in ' + PROBE_TIMEOUT_MS / 1000 + ' s, or there is no network).')
102
+ d.log(NAME + ': when you are online, run: npm install -g ' + pkg + '@' + d.version + ' (or reinstall without --omit=optional or --no-optional)')
103
+ return null
104
+ }
105
+ }
106
+
107
+ // realDeps is what the launcher uses; tests pass their own.
108
+ function realDeps() {
109
+ const file = process.platform === 'win32' ? NAME + '.exe' : NAME
110
+ const isTTY = Boolean(process.stdin.isTTY && process.stderr.isTTY)
111
+ return {
112
+ key: process.platform + '-' + process.arch,
113
+ version: require('../package.json').version,
114
+ env: process.env,
115
+ isTTY,
116
+ log: (line) => console.error(line),
117
+ resolve: (pkg) => {
118
+ try { return require.resolve(pkg + '/bin/' + file) } catch { return null }
119
+ },
120
+ // npm is a .cmd file on Windows, which needs a shell to start.
121
+ npm: (args, options) => spawnSync('npm', args, { encoding: 'utf8', timeout: PROBE_TIMEOUT_MS, shell: process.platform === 'win32', ...options }),
122
+ ask: (question) => new Promise((resolve) => {
123
+ const rl = require('node:readline').createInterface({ input: process.stdin, output: process.stderr })
124
+ rl.question(question, (answer) => { rl.close(); resolve(answer) })
125
+ }),
126
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
127
+ }
128
+ }
129
+
130
+ function launch(binary) {
131
+ if (process.platform !== 'win32') {
132
+ try {
133
+ fs.accessSync(binary, fs.constants.X_OK)
134
+ } catch {
135
+ try { fs.chmodSync(binary, 0o755) } catch { /* a read-only install keeps its mode */ }
136
+ }
137
+ }
138
+ // A stop request must reach the binary: forward it, never orphan the child.
139
+ const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' })
140
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
141
+ process.on(sig, () => { child.kill(sig) })
142
+ }
143
+ child.on('error', (error) => {
144
+ console.error(NAME + ': ' + error.message)
145
+ process.exit(1)
146
+ })
147
+ child.on('exit', (code, signal) => {
148
+ if (signal) {
149
+ process.removeAllListeners(signal)
150
+ process.kill(process.pid, signal)
151
+ return
152
+ }
153
+ process.exit(code ?? 1)
154
+ })
155
+ }
156
+
157
+ module.exports = { resolveBinary, probe }
158
+
159
+ if (require.main === module) {
160
+ resolveBinary(realDeps()).then((binary) => {
161
+ if (binary) launch(binary)
162
+ else process.exit(1)
163
+ })
44
164
  }
45
- process.exit(result.status ?? 1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monosmith/cli",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-beta.51",
4
4
  "description": "Runs a monorepo in Node, Go, Python, Rust, .NET, Ruby, PHP, C++, Dart, the JVM, Bicep, Lua, AppleScript, VBA, Swift and Objective-C: tasks from a cache, CI and releases",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/monosmith-app/monosmith",
@@ -18,11 +18,11 @@
18
18
  "node": "\u003e=18"
19
19
  },
20
20
  "optionalDependencies": {
21
- "@monosmith/cli-darwin-arm64": "2.0.0-beta.5",
22
- "@monosmith/cli-darwin-x64": "2.0.0-beta.5",
23
- "@monosmith/cli-linux-arm64": "2.0.0-beta.5",
24
- "@monosmith/cli-linux-x64": "2.0.0-beta.5",
25
- "@monosmith/cli-win32-arm64": "2.0.0-beta.5",
26
- "@monosmith/cli-win32-x64": "2.0.0-beta.5"
21
+ "@monosmith/cli-darwin-arm64": "2.0.0-beta.51",
22
+ "@monosmith/cli-darwin-x64": "2.0.0-beta.51",
23
+ "@monosmith/cli-linux-arm64": "2.0.0-beta.51",
24
+ "@monosmith/cli-linux-x64": "2.0.0-beta.51",
25
+ "@monosmith/cli-win32-arm64": "2.0.0-beta.51",
26
+ "@monosmith/cli-win32-x64": "2.0.0-beta.51"
27
27
  }
28
28
  }