omakit 0.1.2 → 0.1.4

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.
@@ -16,13 +16,16 @@
16
16
  // against.
17
17
  //
18
18
  // It is not a self-updater in the sense this repository warns other people
19
- // about. It does not fetch and execute arbitrary code: it fast-forwards a Git
20
- // checkout the user cloned themselves, from the remote they cloned it from, and
21
- // it refuses if any of that is not true.
19
+ // about. It does not fetch and execute arbitrary code: it hands the update to
20
+ // the installer that put the tool here. A Git checkout is fast-forwarded from
21
+ // the remote it was cloned from; an npm install is reinstalled by the `npm` on
22
+ // PATH, with frozen arguments, at the exact version the registry named, and
23
+ // only when that version is newer. It refuses if any of that is not true.
22
24
 
23
25
  import { execFileSync } from "node:child_process"
24
- import { existsSync } from "node:fs"
26
+ import { existsSync, readFileSync } from "node:fs"
25
27
  import { join, resolve, sep } from "node:path"
28
+ import { getJson } from "./github.mjs"
26
29
  import { progress } from "./progress.mjs"
27
30
  import { action, colourEnabled, GUTTER, mark, styler, verdict, wrap } from "./style.mjs"
28
31
 
@@ -40,11 +43,64 @@ export function installKind(repoRoot) {
40
43
  export function upgradeCommand(repoRoot, name = "omakit") {
41
44
  return {
42
45
  git: "omakit upgrade",
43
- npm: `npm i -g ${name}@latest`,
46
+ npm: "omakit upgrade",
44
47
  distro: `sudo pacman -Syu ${name}`,
45
48
  }[installKind(repoRoot)]
46
49
  }
47
50
 
51
+ /**
52
+ * The frozen shape of the one `npm` invocation this tool makes. The package
53
+ * spec appended to it is `<name>@<version>` with the version the registry just
54
+ * reported, never `latest`, so what is printed is what is run. No sudo, no
55
+ * script execution (`--ignore-scripts`), and tests/unit/read-only.test.mjs
56
+ * asserts that npm is spawned nowhere else and with nothing else.
57
+ */
58
+ export const NPM_UPGRADE_ARGS = Object.freeze(["install", "--global", "--ignore-scripts", "--no-fund", "--no-audit"])
59
+
60
+ /** The newest published version, or null when the registry did not answer. */
61
+ export async function latestOnRegistry(name) {
62
+ try {
63
+ const meta = await getJson(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`)
64
+ return meta?.version || null
65
+ } catch {
66
+ return null
67
+ }
68
+ }
69
+
70
+ function installedVersion(repoRoot) {
71
+ try {
72
+ return JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version || null
73
+ } catch {
74
+ return null
75
+ }
76
+ }
77
+
78
+ /** Where the `npm` on PATH installs global packages, or null when there is no npm. */
79
+ function npmGlobalRoot() {
80
+ try {
81
+ return resolve(execFileSync("npm", ["root", "--global"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim())
82
+ } catch {
83
+ return null
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The frozen shape of the one other question this tool asks npm: where its
89
+ * global prefix is, whose `bin` is where `npm install --global` put the
90
+ * `omakit` command. Read-only; tests/unit/read-only.test.mjs holds npm to
91
+ * this shape, `root --global` and the install above, and to this file.
92
+ */
93
+ export const NPM_PREFIX_ARGS = Object.freeze(["prefix", "--global"])
94
+
95
+ /** The `npm` on PATH's global prefix, or null when there is no npm. */
96
+ export function npmGlobalPrefix() {
97
+ try {
98
+ return resolve(execFileSync("npm", [...NPM_PREFIX_ARGS], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim())
99
+ } catch {
100
+ return null
101
+ }
102
+ }
103
+
48
104
  function git(dir, args) {
49
105
  return execFileSync("git", ["-C", dir, ...args], {
50
106
  encoding: "utf8",
@@ -70,7 +126,7 @@ export function isExpectedRemote(url, expected = REPOSITORY) {
70
126
  * a way to point the command at somebody else's repository, because nothing
71
127
  * on the command line reaches it.
72
128
  */
73
- export async function upgrade({ repoRoot, stream = process.stdout, dryRun = false, expectedRemote = REPOSITORY }) {
129
+ export async function upgrade({ repoRoot, stream = process.stdout, dryRun = false, expectedRemote = REPOSITORY, latest = latestOnRegistry, npmRoot = npmGlobalRoot, name = "omakit" }) {
74
130
  const c = styler(colourEnabled(stream))
75
131
  const out = (line = "") => stream.write(`${line}\n`)
76
132
  const lines = (list) => { for (const line of list) out(line) }
@@ -83,13 +139,10 @@ export async function upgrade({ repoRoot, stream = process.stdout, dryRun = fals
83
139
  const ok = (text) => out(`${mark("pass", c)}${wrap(text, { indent: GUTTER }, c).join("\n").trimStart()}`)
84
140
 
85
141
  if (!existsSync(join(repoRoot, ".git"))) {
86
- const kind = installKind(repoRoot)
87
- return refuse(
88
- kind === "distro"
89
- ? "this is a distro package under /usr, so omakit leaves upgrades to pacman."
90
- : "this is an npm package install, so omakit leaves upgrades to npm.",
91
- upgradeCommand(repoRoot),
92
- )
142
+ if (installKind(repoRoot) === "distro") {
143
+ return refuse("this is a distro package under /usr, so omakit leaves upgrades to the package manager.", upgradeCommand(repoRoot))
144
+ }
145
+ return upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, refuse, note, ok, out, lines, c })
93
146
  }
94
147
 
95
148
  let remote
@@ -177,3 +230,60 @@ export async function upgrade({ repoRoot, stream = process.stdout, dryRun = fals
177
230
  lines(wrap("The marketplace pin did not move: this updated the tool, not the commit its rules are read from. `omakit doctor` says whether that pin is behind, and docs/UPSTREAM_CONTRACT.md says what moving it involves.", {}, c))
178
231
  return { ok: true, changed: true, from: before, to: after, commits: log.length }
179
232
  }
233
+
234
+ /**
235
+ * The npm route. `latest` and `npmRoot` are injectable for the tests only, the
236
+ * way `expectedRemote` is: nothing on the command line reaches them.
237
+ */
238
+ async function upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, refuse, note, ok, out, lines, c }) {
239
+ const root = resolve(repoRoot)
240
+ const globalRoot = npmRoot()
241
+ if (!globalRoot) {
242
+ return refuse("this is an npm package install, but no `npm` is on PATH to update it with.", `npm install --global ${name}@latest`)
243
+ }
244
+ if (root !== join(globalRoot, name)) {
245
+ return refuse(
246
+ `this omakit is installed at ${root}, but the npm on PATH installs global packages under ${globalRoot}. Updating with a different npm would leave this one where it is.`,
247
+ `npm install --global ${name}@latest`,
248
+ )
249
+ }
250
+ const current = installedVersion(root)
251
+ const spinner = progress({ stream: stream === process.stdout ? process.stderr : stream })
252
+ spinner.phase("asking the npm registry for the newest published version")
253
+ const newest = await latest(name)
254
+ spinner.done()
255
+ if (!newest) {
256
+ return refuse("the npm registry did not answer, so there is nothing to compare against.", "Connect to the network, then run `omakit upgrade` again.")
257
+ }
258
+ if (newest === current) {
259
+ ok(`already current at ${current}, the newest published version`)
260
+ out()
261
+ lines(wrap("The marketplace pin is a separate thing and is never touched here. `omakit doctor` says whether it is behind.", {}, c))
262
+ return { ok: true, changed: false, version: current }
263
+ }
264
+ const spec = `${name}@${newest}`
265
+ if (dryRun) {
266
+ note(`${newest} is published, this is ${current}; not applied (--dry-run)`)
267
+ out(`${" ".repeat(GUTTER)}${c("prose", `npm ${[...NPM_UPGRADE_ARGS, spec].join(" ")}`)}`)
268
+ out()
269
+ lines(action("omakit upgrade", c, { indent: 0 }))
270
+ return { ok: true, changed: false, version: current, available: newest }
271
+ }
272
+ spinner.phase(`npm install --global ${spec}`)
273
+ try {
274
+ execFileSync("npm", [...NPM_UPGRADE_ARGS, spec], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
275
+ } catch (error) {
276
+ spinner.done()
277
+ const reason = String(error?.stderr || "").trim().split("\n").filter((line) => /^npm (?:error|ERR!)/.test(line)).pop() || "npm install failed"
278
+ return refuse(`npm could not install ${spec}: ${reason.replace(/^npm (?:error|ERR!)\s*/, "")}`, `npm ${[...NPM_UPGRADE_ARGS, spec].join(" ")}`)
279
+ }
280
+ spinner.done()
281
+ const after = installedVersion(root)
282
+ if (after !== newest) {
283
+ return refuse(`npm finished, but ${root} reports ${after || "no version"} rather than ${newest}.`, `npm ${[...NPM_UPGRADE_ARGS, spec].join(" ")}`)
284
+ }
285
+ ok(`${current} to ${after}, through the npm that installed it`)
286
+ out()
287
+ lines(wrap("The marketplace pin did not move: this updated the tool, not the commit its rules are read from. `omakit doctor` says whether that pin is behind, and docs/UPSTREAM_CONTRACT.md says what moving it involves.", {}, c))
288
+ return { ok: true, changed: true, from: current, to: after }
289
+ }
@@ -72,9 +72,9 @@ export const COMMANDS = Object.freeze([
72
72
  {
73
73
  signature: "omakit upgrade [--dry-run]",
74
74
  lines: [
75
- "Fast-forward this checkout of omakit itself. Refuses a dirty tree, an",
76
- "unexpected remote and anything that is not a fast-forward. Never moves",
77
- "the marketplace pin.",
75
+ "Update omakit through the installer that made it: npm, at the exact",
76
+ "version the registry names, or a fast-forward of a clone. Refuses",
77
+ "anything else, and never moves the marketplace pin.",
78
78
  ],
79
79
  },
80
80
  {
@@ -110,9 +110,10 @@ export const TARGET_NOTE = "<target> is a local Git repository path, or <https u
110
110
  */
111
111
  export const AUTHENTICATION = Object.freeze([
112
112
  "Read-only, and optional. omakit uses your `gh` login if you have one, and",
113
- "otherwise goes unauthenticated. `submit` and `verify` need no network at",
114
- `all; \`watch\` and \`parity\` are capped at ${UNAUTHENTICATED_LIMIT} requests an hour without a`,
115
- "login. omakit never writes a credential anywhere.",
113
+ "otherwise goes unauthenticated. `verify` needs no network; `submit` reads",
114
+ "two things online and `--offline` turns both off; `watch` and `parity` are",
115
+ `capped at ${UNAUTHENTICATED_LIMIT} requests an hour without a login. omakit never writes a`,
116
+ "credential anywhere.",
116
117
  ])
117
118
 
118
119
  /**