prolog-notebook 0.4.1 → 0.4.3
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 +40 -0
- package/README.md +27 -0
- package/bin/prolog-notebook.mjs +104 -8
- package/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/update.js +33 -11
- package/src/upgrade.js +128 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.3] — 2026-08-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **It offers to update itself, and then carries on** — one command, no re-run:
|
|
8
|
+
|
|
9
|
+
$ prolog-notebook run ch04.prolog.md
|
|
10
|
+
You have Prolog Notebook 0.4.2. The latest is 0.4.3.
|
|
11
|
+
Update and continue on the new version? [Y/n] y
|
|
12
|
+
Updating with npm i -g prolog-notebook@0.4.3
|
|
13
|
+
You now have Prolog Notebook 0.4.3.
|
|
14
|
+
Continuing on the new version.
|
|
15
|
+
✓ p-family
|
|
16
|
+
✓ q-is-son — 6 solutions
|
|
17
|
+
|
|
18
|
+
The offer comes **before** the work, because that is the only point at which the answer can
|
|
19
|
+
change anything. It costs a network round trip once a day rather than once a run, since the
|
|
20
|
+
answer is cached. Continuing is a child process on the same path — npm replaces the contents
|
|
21
|
+
of the package directory, so the command the reader typed now holds the new version — with
|
|
22
|
+
stdio inherited, the exit code proxied, and an environment marker that stops the new process
|
|
23
|
+
checking again.
|
|
24
|
+
|
|
25
|
+
- **`prolog-notebook upgrade`**, for when you already know. It replaces this copy only where
|
|
26
|
+
it can prove how it was installed: a global `npm i -g` it will do, a dependency of somebody's
|
|
27
|
+
project it will not touch, and a source checkout is git's business. It installs the exact
|
|
28
|
+
version you were told about rather than `@latest`, which can move in between.
|
|
29
|
+
|
|
30
|
+
Both only where there is somebody to ask — stdin and stderr must be terminals. In a pipe, a
|
|
31
|
+
script or CI the notice comes after the work, with the command to type.
|
|
32
|
+
|
|
33
|
+
## [0.4.2] — 2026-08-30
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **The update check asked the registry for a media type it is entitled to refuse**, so on some
|
|
38
|
+
of npm's edges every check answered `Could not reach the npm registry to check for updates.`
|
|
39
|
+
`application/vnd.npm.install-v1+json` is defined for the packument; `/{name}/latest` may
|
|
40
|
+
answer 406 to it, and does. Measured both ways from one machine minutes apart, which is what
|
|
41
|
+
made it look transient. It now asks for plain JSON, which that endpoint has always served.
|
|
42
|
+
|
|
3
43
|
## [0.4.1] — 2026-08-30
|
|
4
44
|
|
|
5
45
|
### Changed
|
package/README.md
CHANGED
|
@@ -93,6 +93,10 @@ chapter changes nothing.
|
|
|
93
93
|
| `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
|
|
94
94
|
| `--check-update` | ask npm whether a newer one exists, and say so either way |
|
|
95
95
|
|
|
96
|
+
```sh
|
|
97
|
+
prolog-notebook upgrade # fetch the latest
|
|
98
|
+
```
|
|
99
|
+
|
|
96
100
|
Two things it will not do. A query stopped at the limit is written **without** a terminator,
|
|
97
101
|
which is the format's way of saying the search was never exhausted — `false.` there would be a
|
|
98
102
|
forgery. And if a program cell fails to load, nothing is written at all: every answer below it
|
|
@@ -105,6 +109,29 @@ reach is reported once a day rather than on every command — silence there woul
|
|
|
105
109
|
indistinguishable from *you are up to date*. Ask outright with `--check-update` and it answers
|
|
106
110
|
either way.
|
|
107
111
|
|
|
112
|
+
When it finds something newer **and you are at a terminal**, it offers to fetch it *before*
|
|
113
|
+
doing the work — and if you say yes it upgrades, then runs your command on the new version.
|
|
114
|
+
One command, no re-run:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
$ prolog-notebook run ch04.prolog.md
|
|
118
|
+
You have Prolog Notebook 0.4.0. The latest is 0.4.2.
|
|
119
|
+
Update and continue on the new version? [Y/n] y
|
|
120
|
+
Updating with npm i -g prolog-notebook@0.4.2
|
|
121
|
+
You now have Prolog Notebook 0.4.2.
|
|
122
|
+
Continuing on the new version.
|
|
123
|
+
✓ p-family
|
|
124
|
+
✓ q-is-son — 6 solutions
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
That costs a network round trip once a day rather than once a run, because the answer is
|
|
128
|
+
cached. Down a pipe or in a script there is nobody to ask, so it prints `Update with:
|
|
129
|
+
prolog-notebook upgrade` after the work instead — a question nobody can answer is a hang.
|
|
130
|
+
|
|
131
|
+
`upgrade` replaces this copy only when it can prove how it was installed. A global `npm i -g`
|
|
132
|
+
it will do; a dependency of somebody's project it will not touch, and a source checkout is
|
|
133
|
+
git's business. Guessing wrong there breaks a project while trying to help.
|
|
134
|
+
|
|
108
135
|
It has no defence against a non-terminating goal yet — the engine runs in this process, so
|
|
109
136
|
`loop :- loop.` hangs the command. Say the word `--limit` all you like; a runaway *consult* is
|
|
110
137
|
not a solution count. Fixing it properly means a worker thread, and it is the prerequisite for
|
package/bin/prolog-notebook.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { prologVersion } from '../src/engine.js';
|
|
|
11
11
|
import { buildLine, currentBuild } from '../src/build-info.js';
|
|
12
12
|
import { banner, VERSION } from '../src/version.js';
|
|
13
13
|
import { updateNotice } from '../src/update.js';
|
|
14
|
+
import { confirm, describeInstall, globalRoot, install, relaunch, upgradePlan } from '../src/upgrade.js';
|
|
14
15
|
import { exportSource } from '../src/export.js';
|
|
15
16
|
import { runNotebook, DEFAULT_LIMIT } from '../src/run.js';
|
|
16
17
|
|
|
@@ -37,6 +38,7 @@ const require = createRequire(import.meta.url);
|
|
|
37
38
|
const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
|
|
38
39
|
|
|
39
40
|
prolog-notebook run <file.prolog.md>... run every cell, write the answers back
|
|
41
|
+
prolog-notebook upgrade fetch the latest version
|
|
40
42
|
|
|
41
43
|
Options
|
|
42
44
|
--limit <n> solutions to take from one query before stopping (default ${DEFAULT_LIMIT})
|
|
@@ -91,6 +93,63 @@ async function version() {
|
|
|
91
93
|
return `${lines.join('\n')}\n\n`;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Fetch the latest, if this copy is one we know how to replace.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} version what to install
|
|
100
|
+
* @returns {Promise<number>} an exit code
|
|
101
|
+
*/
|
|
102
|
+
async function upgrade(version) {
|
|
103
|
+
const packageRoot = new URL('..', import.meta.url).pathname;
|
|
104
|
+
const kind = describeInstall({ packageRoot, globalRoot: await globalRoot() });
|
|
105
|
+
const plan = upgradePlan(kind, version);
|
|
106
|
+
if (plan.say) {
|
|
107
|
+
process.stderr.write(`${plan.say}\n`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
process.stderr.write(`Updating with ${NPM_LINE} ${plan.argv.join(' ')}\n`);
|
|
111
|
+
if (!(await install(plan.argv))) {
|
|
112
|
+
process.stderr.write('npm could not complete the update.\n');
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
process.stderr.write(`You now have Prolog Notebook ${version}.\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Offer it, but only where a question is a question.
|
|
121
|
+
*
|
|
122
|
+
* A pipe, a script and CI are all places where waiting for an answer is a hang,
|
|
123
|
+
* so the notice is simply printed there. Both streams are checked because the
|
|
124
|
+
* question goes to stderr and the answer comes from stdin.
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* Is there somebody at the other end?
|
|
128
|
+
*
|
|
129
|
+
* Both streams, because the question goes to stderr and the answer comes back on
|
|
130
|
+
* stdin. A pipe, a script and CI are all places where a question is a hang.
|
|
131
|
+
*/
|
|
132
|
+
function canAsk() {
|
|
133
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function offerUpgrade(newer) {
|
|
137
|
+
if (!canAsk()) {
|
|
138
|
+
// Nobody to ask, so say what to type instead. `prolog-notebook upgrade`
|
|
139
|
+
// rather than the npm line: it knows how this copy was installed, and the
|
|
140
|
+
// npm line is wrong for a project dependency.
|
|
141
|
+
process.stderr.write('Update with: prolog-notebook upgrade\n');
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (!(await confirm('Update now?'))) {
|
|
145
|
+
process.stderr.write(' (run `prolog-notebook upgrade` whenever you like)\n');
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return upgrade(newer);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const NPM_LINE = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
152
|
+
|
|
94
153
|
async function main(argv) {
|
|
95
154
|
const args = argv.slice(2);
|
|
96
155
|
if (!args.length || args.includes('-h') || args.includes('--help')) {
|
|
@@ -108,18 +167,25 @@ async function main(argv) {
|
|
|
108
167
|
// forces the check that would otherwise wait for the day to turn over.
|
|
109
168
|
const asked = args.includes('--check-update');
|
|
110
169
|
if (asked && args.filter((a) => !a.startsWith('-')).length === 0) {
|
|
111
|
-
const
|
|
112
|
-
process.stderr.write(`${
|
|
113
|
-
return 0;
|
|
170
|
+
const { message, newer } = await updateNotice({ version: VERSION, force: true });
|
|
171
|
+
if (message) process.stderr.write(`${message}\n`);
|
|
172
|
+
return newer ? (await offerUpgrade(newer)) ?? 0 : 0;
|
|
114
173
|
}
|
|
115
174
|
|
|
116
175
|
const command = args.shift();
|
|
176
|
+
if (command === 'upgrade') {
|
|
177
|
+
const { message, newer } = await updateNotice({ version: VERSION, force: true });
|
|
178
|
+
if (message) process.stderr.write(`${message}\n`);
|
|
179
|
+
return newer ? upgrade(newer) : 0;
|
|
180
|
+
}
|
|
117
181
|
if (command !== 'run') {
|
|
118
182
|
process.stderr.write(`unknown command "${command}"\n\n${USAGE}`);
|
|
119
183
|
return 2;
|
|
120
184
|
}
|
|
121
185
|
|
|
122
186
|
const options = { limit: DEFAULT_LIMIT, stdout: false, quiet: false };
|
|
187
|
+
// Whether the offer has already been made, before the work started.
|
|
188
|
+
let checked = false;
|
|
123
189
|
const files = [];
|
|
124
190
|
while (args.length) {
|
|
125
191
|
const arg = args.shift();
|
|
@@ -145,6 +211,31 @@ async function main(argv) {
|
|
|
145
211
|
}
|
|
146
212
|
if (!options.quiet) process.stderr.write(`${RUNAWAY_WARNING}\n`);
|
|
147
213
|
|
|
214
|
+
// BEFORE THE WORK, when there is somebody to ask — because the point of asking
|
|
215
|
+
// is to run the NEW version, and that is only possible while there is still
|
|
216
|
+
// something to run. Afterwards the files are written and the answer comes too
|
|
217
|
+
// late to change them.
|
|
218
|
+
//
|
|
219
|
+
// It costs a network round trip once a day, not once a run: the rest of the day
|
|
220
|
+
// is a file read. Measured at 25-120 ms against npm, against a run that spends
|
|
221
|
+
// seconds in Prolog.
|
|
222
|
+
if (canAsk() && !options.quiet) {
|
|
223
|
+
const ahead = await updateNotice({ version: VERSION, force: asked })
|
|
224
|
+
.catch(() => ({ message: null, newer: null }));
|
|
225
|
+
if (ahead.message) process.stderr.write(`${ahead.message}\n`);
|
|
226
|
+
if (ahead.newer && await confirm('Update and continue on the new version?')) {
|
|
227
|
+
if ((await upgrade(ahead.newer)) === 0) {
|
|
228
|
+
process.stderr.write('Continuing on the new version.\n');
|
|
229
|
+
// The path has not changed — npm replaced what is behind it — so this is
|
|
230
|
+
// the same command, running the bytes that have just arrived.
|
|
231
|
+
return relaunch(process.argv);
|
|
232
|
+
}
|
|
233
|
+
process.stderr.write('Carrying on with the version you have.\n');
|
|
234
|
+
}
|
|
235
|
+
// Asked and answered: the check below has nothing left to say.
|
|
236
|
+
checked = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
148
239
|
// STARTED NOW, READ AT THE END. The registry is somebody else's machine on
|
|
149
240
|
// somebody else's network, and none of that should stand between the reader
|
|
150
241
|
// and their answers — so the question is asked while the work happens and the
|
|
@@ -153,9 +244,11 @@ async function main(argv) {
|
|
|
153
244
|
// Not asked at all under --quiet, unless it was asked for outright: --quiet
|
|
154
245
|
// means "report only failures", and news about a newer version is not one. Not
|
|
155
246
|
// starting the request is better than starting it and discarding the answer.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
247
|
+
// The other half: nobody to ask, so the question is asked ALONGSIDE the work
|
|
248
|
+
// and reported at the end. A terminal has had its offer already.
|
|
249
|
+
const update = checked || (options.quiet && !asked)
|
|
250
|
+
? Promise.resolve({ message: null, newer: null })
|
|
251
|
+
: updateNotice({ version: VERSION, force: asked }).catch(() => ({ message: null, newer: null }));
|
|
159
252
|
|
|
160
253
|
// One engine for the whole invocation, restarted between files. A notebook is
|
|
161
254
|
// a world of its own — one cell is one virtual file, and two chapters may
|
|
@@ -172,8 +265,11 @@ async function main(argv) {
|
|
|
172
265
|
|
|
173
266
|
// stderr, always: `run --stdout` is a notebook going down a pipe, and a version
|
|
174
267
|
// notice in the middle of it would corrupt the file it is printing.
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
268
|
+
const { message, newer } = await update;
|
|
269
|
+
if (message) process.stderr.write(`${message}\n`);
|
|
270
|
+
// Offered AFTER the work, and never re-running it: the files are written, and
|
|
271
|
+
// a command that repeated itself on a newer version would write them twice.
|
|
272
|
+
if (newer) await offerUpgrade(newer);
|
|
177
273
|
return status;
|
|
178
274
|
}
|
|
179
275
|
|
package/package.json
CHANGED
package/src/build-info.json
CHANGED
package/src/update.js
CHANGED
|
@@ -69,9 +69,16 @@ export function fileStore(path = cachePath()) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Ask the registry which version is `latest`.
|
|
71
71
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* `/{name}/latest` rather than the packument: the packument carries every version
|
|
73
|
+
* ever published and can be megabytes, for a question with a one-line answer.
|
|
74
|
+
* This endpoint answers in about 2 kB.
|
|
75
|
+
*
|
|
76
|
+
* PLAIN JSON, AND NOT THE ABBREVIATED TYPE. `application/vnd.npm.install-v1+json`
|
|
77
|
+
* is defined for the packument, and this endpoint is entitled to refuse it — some
|
|
78
|
+
* of npm's edges serve it anyway and others answer 406, which is why the first
|
|
79
|
+
* version of this shipped a notifier that reported "could not reach the registry"
|
|
80
|
+
* to some people and worked for others. Measured both ways, from one machine,
|
|
81
|
+
* minutes apart.
|
|
75
82
|
*
|
|
76
83
|
* @returns {Promise<string|null>} null for anything that goes wrong
|
|
77
84
|
*/
|
|
@@ -87,7 +94,7 @@ export async function latestFromRegistry({
|
|
|
87
94
|
try {
|
|
88
95
|
const base = String(registry).replace(/\/+$/, '');
|
|
89
96
|
const response = await fetchImpl(`${base}/${name}/latest`, {
|
|
90
|
-
headers: { accept: 'application/
|
|
97
|
+
headers: { accept: 'application/json' },
|
|
91
98
|
signal: AbortSignal.timeout(timeout),
|
|
92
99
|
});
|
|
93
100
|
if (!response.ok) return null;
|
|
@@ -117,7 +124,11 @@ export function isNewer(latest, current) {
|
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
120
|
-
*
|
|
127
|
+
* What to say, and what was found.
|
|
128
|
+
*
|
|
129
|
+
* An object rather than a string because a caller may want to ACT on it — offer
|
|
130
|
+
* to upgrade — and parsing our own sentence back into a version number would be
|
|
131
|
+
* a worse way to learn what we already knew.
|
|
121
132
|
*
|
|
122
133
|
* @param {object} options
|
|
123
134
|
* @param {string} options.version what this copy is
|
|
@@ -127,7 +138,7 @@ export function isNewer(latest, current) {
|
|
|
127
138
|
* @param {{read: Function, write: Function}} [options.store]
|
|
128
139
|
* @param {() => Promise<string|null>} [options.latest]
|
|
129
140
|
* @param {object} [options.env]
|
|
130
|
-
* @returns {Promise<string|null>}
|
|
141
|
+
* @returns {Promise<{message: string|null, newer: string|null}>}
|
|
131
142
|
*/
|
|
132
143
|
export async function updateNotice({
|
|
133
144
|
version,
|
|
@@ -138,7 +149,12 @@ export async function updateNotice({
|
|
|
138
149
|
latest = latestFromRegistry,
|
|
139
150
|
env = process.env,
|
|
140
151
|
} = {}) {
|
|
141
|
-
|
|
152
|
+
const quiet = { message: null, newer: null };
|
|
153
|
+
// UPGRADED is the loop guard: the process that replaced itself re-runs this
|
|
154
|
+
// command on the new version, and that one must not go round again. A failed
|
|
155
|
+
// or partial upgrade would otherwise re-exec for ever.
|
|
156
|
+
if (!force && (env.CI || env.NO_UPDATE_NOTIFIER || env.PROLOG_NOTEBOOK_UPGRADED)) return quiet;
|
|
157
|
+
if (env.PROLOG_NOTEBOOK_UPGRADED) return quiet;
|
|
142
158
|
|
|
143
159
|
const remembered = store.read();
|
|
144
160
|
const fresh = !force && remembered && now - remembered.checked < ttl;
|
|
@@ -148,14 +164,20 @@ export async function updateNotice({
|
|
|
148
164
|
if (!fresh) store.write({ checked: now, latest: newest ?? null });
|
|
149
165
|
|
|
150
166
|
if (!newest) {
|
|
151
|
-
return fresh && !force
|
|
167
|
+
return fresh && !force
|
|
168
|
+
? quiet
|
|
169
|
+
: { message: 'Could not reach the npm registry to check for updates.', newer: null };
|
|
152
170
|
}
|
|
153
171
|
// BOTH LINES START WITH WHAT YOU HAVE, because that is the question being
|
|
154
172
|
// asked. "Prolog Notebook 0.4.0 is the latest" states a fact about the world
|
|
155
173
|
// and leaves the reader to work out that it is also a fact about them.
|
|
156
174
|
if (isNewer(newest, version)) {
|
|
157
|
-
return
|
|
158
|
-
|
|
175
|
+
return {
|
|
176
|
+
message: `You have Prolog Notebook ${version}. The latest is ${newest}.`,
|
|
177
|
+
newer: newest,
|
|
178
|
+
};
|
|
159
179
|
}
|
|
160
|
-
return force
|
|
180
|
+
return force
|
|
181
|
+
? { message: `You have the latest version of Prolog Notebook, ${version}.`, newer: null }
|
|
182
|
+
: quiet;
|
|
161
183
|
}
|
package/src/upgrade.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Updating itself, which is mostly a question of knowing how it was installed.
|
|
2
|
+
//
|
|
3
|
+
// A global `npm i -g`, a project dependency, an npx run, a pnpm or bun global,
|
|
4
|
+
// a git checkout — each needs a different answer, and running `npm i -g` from
|
|
5
|
+
// the wrong one either fails or upgrades something the reader did not mean. So
|
|
6
|
+
// this proves the global case and REFUSES THE REST WITH THE RIGHT COMMAND
|
|
7
|
+
// rather than guessing: a tool that breaks somebody's project while trying to
|
|
8
|
+
// help is worse than one that tells them what to type.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here happens without being asked. See bin/prolog-notebook.mjs for the
|
|
11
|
+
// prompt, which only appears at a terminal — a pipe, a script and CI are all
|
|
12
|
+
// places where a question is a hang.
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { execFile } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
16
|
+
|
|
17
|
+
const run = promisify(execFile);
|
|
18
|
+
|
|
19
|
+
/** npm is a batch file on Windows, and spawn will not find it otherwise. */
|
|
20
|
+
export const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Which kind of copy is this?
|
|
24
|
+
*
|
|
25
|
+
* @param {{packageRoot: string, globalRoot: string|null}} where
|
|
26
|
+
* @returns {'global'|'local'|'source'}
|
|
27
|
+
*/
|
|
28
|
+
export function describeInstall({ packageRoot, globalRoot }) {
|
|
29
|
+
if (globalRoot && packageRoot.startsWith(globalRoot)) return 'global';
|
|
30
|
+
// `node_modules` in the path and not the global root: somebody's project
|
|
31
|
+
// depends on this, and upgrading it globally would leave that project on the
|
|
32
|
+
// version it pinned while changing a tool they did not ask about.
|
|
33
|
+
if (packageRoot.includes(`${'node_modules'}`)) return 'local';
|
|
34
|
+
return 'source';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What to do about it — a command to run, or words to print.
|
|
39
|
+
*
|
|
40
|
+
* @param {'global'|'local'|'source'} kind
|
|
41
|
+
* @param {string} version
|
|
42
|
+
* @returns {{argv: string[]}|{say: string}}
|
|
43
|
+
*/
|
|
44
|
+
export function upgradePlan(kind, version) {
|
|
45
|
+
if (kind === 'global') return { argv: ['i', '-g', `prolog-notebook@${version}`] };
|
|
46
|
+
if (kind === 'local') {
|
|
47
|
+
return {
|
|
48
|
+
say: 'This copy is a dependency of a project rather than a global install.\n'
|
|
49
|
+
+ `Upgrade it there: npm i prolog-notebook@${version}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
say: 'This copy is a source checkout, not an install. Upgrade it with git.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Where npm keeps global packages, or null if it will not say. */
|
|
58
|
+
export async function globalRoot(exec = run) {
|
|
59
|
+
try {
|
|
60
|
+
const { stdout } = await exec(NPM, ['root', '-g'], { timeout: 5000 });
|
|
61
|
+
return stdout.trim() || null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ask a yes/no question, defaulting to yes.
|
|
69
|
+
*
|
|
70
|
+
* ON STDERR, always: `run --stdout` is a notebook going down a pipe, and a
|
|
71
|
+
* question in the middle of it would corrupt the file it is writing.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<boolean>}
|
|
74
|
+
*/
|
|
75
|
+
export async function confirm(question, { input = process.stdin, output = process.stderr } = {}) {
|
|
76
|
+
const { createInterface } = await import('node:readline/promises');
|
|
77
|
+
const rl = createInterface({ input, output });
|
|
78
|
+
try {
|
|
79
|
+
const answer = await rl.question(`${question} [Y/n] `);
|
|
80
|
+
return !/^n/i.test(answer.trim());
|
|
81
|
+
} catch {
|
|
82
|
+
// Ctrl-C, a closed stream: not an answer, so not a yes.
|
|
83
|
+
return false;
|
|
84
|
+
} finally {
|
|
85
|
+
rl.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Do it, showing npm's own output rather than a spinner of our own.
|
|
91
|
+
*
|
|
92
|
+
* @returns {Promise<boolean>} whether npm was happy
|
|
93
|
+
*/
|
|
94
|
+
export function install(argv, spawnImpl = spawn) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const child = spawnImpl(NPM, argv, { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
97
|
+
child.on('error', () => resolve(false));
|
|
98
|
+
child.on('close', (code) => resolve(code === 0));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run this same command again, on the version that has just replaced us.
|
|
104
|
+
*
|
|
105
|
+
* THE PATH DOES NOT CHANGE, which is what makes this work: npm replaces the
|
|
106
|
+
* contents of the package directory, and the bin the reader typed still points
|
|
107
|
+
* at the same file. So the script to run is the one we are already running — its
|
|
108
|
+
* bytes are simply new.
|
|
109
|
+
*
|
|
110
|
+
* A child rather than a true exec, because Node has no execve: stdio is
|
|
111
|
+
* inherited so it looks like one process, and the child's exit code becomes
|
|
112
|
+
* ours. The marker in the environment stops the new process checking for updates
|
|
113
|
+
* again, which is what would otherwise turn a failed upgrade into a loop.
|
|
114
|
+
*
|
|
115
|
+
* @param {string[]} argv the original process.argv
|
|
116
|
+
* @param {Function} [spawnImpl]
|
|
117
|
+
* @returns {Promise<number>} the exit code to leave with
|
|
118
|
+
*/
|
|
119
|
+
export function relaunch(argv, spawnImpl = spawn) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const child = spawnImpl(argv[0], argv.slice(1), {
|
|
122
|
+
stdio: 'inherit',
|
|
123
|
+
env: { ...process.env, PROLOG_NOTEBOOK_UPGRADED: '1' },
|
|
124
|
+
});
|
|
125
|
+
child.on('error', () => resolve(1));
|
|
126
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
127
|
+
});
|
|
128
|
+
}
|
package/src/version.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export const NAME = 'Prolog Notebook';
|
|
11
11
|
|
|
12
12
|
/** Must equal package.json's `version` — test/run.test.mjs enforces it. */
|
|
13
|
-
export const VERSION = '0.4.
|
|
13
|
+
export const VERSION = '0.4.3';
|
|
14
14
|
|
|
15
15
|
/** The two facts a licence notice is actually made of. */
|
|
16
16
|
export const YEAR = '2026';
|