prolog-notebook 0.3.1 → 0.4.0
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 +25 -0
- package/README.md +8 -0
- package/bin/prolog-notebook.mjs +37 -6
- package/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/update.js +158 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] — 2026-08-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **The CLI notices when it is out of date.** On real work — `run` — it asks npm at most once
|
|
8
|
+
a day whether there is something newer, and says nothing unless there is:
|
|
9
|
+
|
|
10
|
+
A newer Prolog Notebook is available: 0.4.0 → 0.5.0
|
|
11
|
+
Update with: npm i -g prolog-notebook
|
|
12
|
+
|
|
13
|
+
`--check-update` forces the question and answers it either way, including *you are on the
|
|
14
|
+
latest* — which the daily check deliberately never says, because a tool that congratulates
|
|
15
|
+
you on every command is one you learn to read past.
|
|
16
|
+
|
|
17
|
+
It stays out of the way by design: never for `--help` or `--version`, never under `--quiet`,
|
|
18
|
+
never when `CI` or `NO_UPDATE_NOTIFIER` is set, and never between the reader and their
|
|
19
|
+
answers — the request is started before the run and collected after it. The notice goes to
|
|
20
|
+
**stderr**, because `run --stdout` is a notebook going down a pipe.
|
|
21
|
+
|
|
22
|
+
A registry it cannot reach is reported once a day rather than on every command: silence
|
|
23
|
+
there would be indistinguishable from *you are up to date*, which is the one thing a broken
|
|
24
|
+
check must not look like. The answer is remembered in `~/.cache/prolog-notebook/`
|
|
25
|
+
(XDG-aware), not beside the package, because a global install is often read-only. A private
|
|
26
|
+
registry is honoured through npm's own `registry` config.
|
|
27
|
+
|
|
3
28
|
## [0.3.1] — 2026-08-30
|
|
4
29
|
|
|
5
30
|
### Changed
|
package/README.md
CHANGED
|
@@ -91,12 +91,20 @@ chapter changes nothing.
|
|
|
91
91
|
| `--stdout` | print the result instead of writing the file |
|
|
92
92
|
| `--quiet` | report only failures |
|
|
93
93
|
| `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
|
|
94
|
+
| `--check-update` | ask npm whether a newer one exists, and say so either way |
|
|
94
95
|
|
|
95
96
|
Two things it will not do. A query stopped at the limit is written **without** a terminator,
|
|
96
97
|
which is the format's way of saying the search was never exhausted — `false.` there would be a
|
|
97
98
|
forgery. And if a program cell fails to load, nothing is written at all: every answer below it
|
|
98
99
|
was produced against a chapter that does not exist.
|
|
99
100
|
|
|
101
|
+
When it does real work it asks npm, at most once a day, whether there is a newer version, and
|
|
102
|
+
says nothing unless there is. Never for `--help` or `--version`, never under `--quiet`, never
|
|
103
|
+
when `CI` or `NO_UPDATE_NOTIFIER` is set, and never blocking the run. A registry it cannot
|
|
104
|
+
reach is reported once a day rather than on every command — silence there would be
|
|
105
|
+
indistinguishable from *you are up to date*. Ask outright with `--check-update` and it answers
|
|
106
|
+
either way.
|
|
107
|
+
|
|
100
108
|
It has no defence against a non-terminating goal yet — the engine runs in this process, so
|
|
101
109
|
`loop :- loop.` hangs the command. Say the word `--limit` all you like; a runaway *consult* is
|
|
102
110
|
not a solution count. Fixing it properly means a worker thread, and it is the prerequisite for
|
package/bin/prolog-notebook.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { basename } from 'node:path';
|
|
|
9
9
|
import { parse, NotebookError } from '../src/format.js';
|
|
10
10
|
import { prologVersion } from '../src/engine.js';
|
|
11
11
|
import { buildLine, currentBuild } from '../src/build-info.js';
|
|
12
|
-
import { banner } from '../src/version.js';
|
|
12
|
+
import { banner, VERSION } from '../src/version.js';
|
|
13
|
+
import { updateNotice } from '../src/update.js';
|
|
13
14
|
import { exportSource } from '../src/export.js';
|
|
14
15
|
import { runNotebook, DEFAULT_LIMIT } from '../src/run.js';
|
|
15
16
|
|
|
@@ -38,11 +39,12 @@ const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
|
|
|
38
39
|
prolog-notebook run <file.prolog.md>... run every cell, write the answers back
|
|
39
40
|
|
|
40
41
|
Options
|
|
41
|
-
--limit <n>
|
|
42
|
-
--stdout
|
|
43
|
-
--quiet
|
|
44
|
-
--version
|
|
45
|
-
-
|
|
42
|
+
--limit <n> solutions to take from one query before stopping (default ${DEFAULT_LIMIT})
|
|
43
|
+
--stdout print the result instead of writing the file
|
|
44
|
+
--quiet report only failures
|
|
45
|
+
--version version, engine and copyright
|
|
46
|
+
--check-update ask npm whether a newer one exists, and say so either way
|
|
47
|
+
-h, --help this
|
|
46
48
|
|
|
47
49
|
A query that stops at the limit is written without a terminator, which is the
|
|
48
50
|
format's way of saying the search was never exhausted. Nothing is invented.
|
|
@@ -96,10 +98,21 @@ async function main(argv) {
|
|
|
96
98
|
return 0;
|
|
97
99
|
}
|
|
98
100
|
if (args.includes('--version') || args.includes('-V')) {
|
|
101
|
+
// No update check here, deliberately: --version and --help are what someone
|
|
102
|
+
// types at an install that is not working, and they stay instant and offline.
|
|
99
103
|
process.stdout.write(await version());
|
|
100
104
|
return 0;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
// Asked for explicitly. On its own it is the whole command; alongside `run` it
|
|
108
|
+
// forces the check that would otherwise wait for the day to turn over.
|
|
109
|
+
const asked = args.includes('--check-update');
|
|
110
|
+
if (asked && args.filter((a) => !a.startsWith('-')).length === 0) {
|
|
111
|
+
const notice = await updateNotice({ version: VERSION, force: true });
|
|
112
|
+
process.stderr.write(`${notice}\n`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
const command = args.shift();
|
|
104
117
|
if (command !== 'run') {
|
|
105
118
|
process.stderr.write(`unknown command "${command}"\n\n${USAGE}`);
|
|
@@ -119,6 +132,7 @@ async function main(argv) {
|
|
|
119
132
|
options.limit = value;
|
|
120
133
|
} else if (arg === '--stdout') options.stdout = true;
|
|
121
134
|
else if (arg === '--quiet') options.quiet = true;
|
|
135
|
+
else if (arg === '--check-update') { /* handled above, and not a file */ }
|
|
122
136
|
else if (arg.startsWith('-')) {
|
|
123
137
|
process.stderr.write(`unknown option "${arg}"\n\n${USAGE}`);
|
|
124
138
|
return 2;
|
|
@@ -131,6 +145,18 @@ async function main(argv) {
|
|
|
131
145
|
}
|
|
132
146
|
if (!options.quiet) process.stderr.write(`${RUNAWAY_WARNING}\n`);
|
|
133
147
|
|
|
148
|
+
// STARTED NOW, READ AT THE END. The registry is somebody else's machine on
|
|
149
|
+
// somebody else's network, and none of that should stand between the reader
|
|
150
|
+
// and their answers — so the question is asked while the work happens and the
|
|
151
|
+
// answer is collected once it is done.
|
|
152
|
+
//
|
|
153
|
+
// Not asked at all under --quiet, unless it was asked for outright: --quiet
|
|
154
|
+
// means "report only failures", and news about a newer version is not one. Not
|
|
155
|
+
// starting the request is better than starting it and discarding the answer.
|
|
156
|
+
const update = options.quiet && !asked
|
|
157
|
+
? Promise.resolve(null)
|
|
158
|
+
: updateNotice({ version: VERSION, force: asked }).catch(() => null);
|
|
159
|
+
|
|
134
160
|
// One engine for the whole invocation, restarted between files. A notebook is
|
|
135
161
|
// a world of its own — one cell is one virtual file, and two chapters may
|
|
136
162
|
// define the same predicate — so carrying clauses across would let a file pass
|
|
@@ -143,6 +169,11 @@ async function main(argv) {
|
|
|
143
169
|
await session.restart();
|
|
144
170
|
status = Math.max(status, await runFile(file, session, options));
|
|
145
171
|
}
|
|
172
|
+
|
|
173
|
+
// stderr, always: `run --stdout` is a notebook going down a pipe, and a version
|
|
174
|
+
// notice in the middle of it would corrupt the file it is printing.
|
|
175
|
+
const notice = await update;
|
|
176
|
+
if (notice) process.stderr.write(`${notice}\n`);
|
|
146
177
|
return status;
|
|
147
178
|
}
|
|
148
179
|
|
package/package.json
CHANGED
package/src/build-info.json
CHANGED
package/src/update.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Is there a newer one? Asked at most once a day, answered quietly, and never in
|
|
2
|
+
// the way (869erkqpc).
|
|
3
|
+
//
|
|
4
|
+
// THE RULES, because an update notifier is the easiest thing in a CLI to make
|
|
5
|
+
// annoying:
|
|
6
|
+
//
|
|
7
|
+
// - It says NOTHING when you are up to date. A tool that congratulates you on
|
|
8
|
+
// every command is one you learn to read past, and then it is not there when
|
|
9
|
+
// it has something to say. `--check-update` is the exception: you asked, so it
|
|
10
|
+
// answers either way.
|
|
11
|
+
// - It never runs for `--help` or `--version`. Those are what someone types at a
|
|
12
|
+
// broken install, and they must stay instant and offline.
|
|
13
|
+
// - It never runs in CI, and honours NO_UPDATE_NOTIFIER. `--check` will run this
|
|
14
|
+
// command on every push one day; a build that fails because a registry was slow
|
|
15
|
+
// is worse than no notice at all.
|
|
16
|
+
// - A registry it cannot reach is said ONCE A DAY, not on every command. Silence
|
|
17
|
+
// there would be indistinguishable from "you are up to date", which is the one
|
|
18
|
+
// thing a broken check must not look like — but a proxy that blocks npm should
|
|
19
|
+
// not put a line of red in front of somebody on every run either.
|
|
20
|
+
// - Everything else that can go wrong is silent: no cache directory, a registry
|
|
21
|
+
// that answers with nonsense. None of it is the reader's problem.
|
|
22
|
+
//
|
|
23
|
+
// The clock, the cache and "what is the latest version" are all arguments, so
|
|
24
|
+
// everything above is tested without touching the wire; the fetch itself is the
|
|
25
|
+
// only part that ever does.
|
|
26
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
|
|
30
|
+
export const DAY = 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/** The npm registry's smallest useful answer: one document, one field. */
|
|
33
|
+
export const REGISTRY = 'https://registry.npmjs.org';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where the last answer is remembered.
|
|
37
|
+
*
|
|
38
|
+
* XDG when it is set, `~/.cache` otherwise. Not beside the package: a global
|
|
39
|
+
* install is often read-only, and a check that needs write access to node_modules
|
|
40
|
+
* is a check that quietly stops happening.
|
|
41
|
+
*/
|
|
42
|
+
export function cachePath(env = process.env, home = homedir()) {
|
|
43
|
+
const base = env.XDG_CACHE_HOME || join(home, '.cache');
|
|
44
|
+
return join(base, 'prolog-notebook', 'update-check.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reading and writing that file, and shrugging when it cannot. */
|
|
48
|
+
export function fileStore(path = cachePath()) {
|
|
49
|
+
return {
|
|
50
|
+
read() {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
write(state) {
|
|
58
|
+
try {
|
|
59
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
60
|
+
writeFileSync(path, JSON.stringify(state));
|
|
61
|
+
} catch {
|
|
62
|
+
// A read-only or missing home directory means the check happens every
|
|
63
|
+
// time instead of once a day. That is a slower notifier, not a failure.
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ask the registry which version is `latest`.
|
|
71
|
+
*
|
|
72
|
+
* The abbreviated document rather than the full packument: the full one carries
|
|
73
|
+
* every version ever published and can be megabytes, for a question with a
|
|
74
|
+
* one-line answer.
|
|
75
|
+
*
|
|
76
|
+
* @returns {Promise<string|null>} null for anything that goes wrong
|
|
77
|
+
*/
|
|
78
|
+
export async function latestFromRegistry({
|
|
79
|
+
// A private registry is a fact about the machine, not about this package, so
|
|
80
|
+
// npm's own setting is honoured before the default — and an explicit override
|
|
81
|
+
// exists because a test needs somewhere to point that is not the internet.
|
|
82
|
+
registry = process.env.PROLOG_NOTEBOOK_REGISTRY || process.env.npm_config_registry || REGISTRY,
|
|
83
|
+
name = 'prolog-notebook',
|
|
84
|
+
timeout = 2000,
|
|
85
|
+
fetchImpl = fetch,
|
|
86
|
+
} = {}) {
|
|
87
|
+
try {
|
|
88
|
+
const base = String(registry).replace(/\/+$/, '');
|
|
89
|
+
const response = await fetchImpl(`${base}/${name}/latest`, {
|
|
90
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
91
|
+
signal: AbortSignal.timeout(timeout),
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) return null;
|
|
94
|
+
const body = await response.json();
|
|
95
|
+
return typeof body?.version === 'string' ? body.version : null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Newer, older, or the same — by the numbers only.
|
|
103
|
+
*
|
|
104
|
+
* A prerelease is never newer than the release it precedes, and comparing them
|
|
105
|
+
* properly is a semver library's job. This one is deciding whether to print a
|
|
106
|
+
* sentence, so `0.4.0-rc.1` is simply not an upgrade from `0.3.1`.
|
|
107
|
+
*/
|
|
108
|
+
export function isNewer(latest, current) {
|
|
109
|
+
if (/-/.test(String(latest))) return false;
|
|
110
|
+
const parts = (v) => String(v).split('.').map((n) => Number.parseInt(n, 10));
|
|
111
|
+
const [a, b] = [parts(latest), parts(current)];
|
|
112
|
+
if (a.some(Number.isNaN) || b.some(Number.isNaN)) return false;
|
|
113
|
+
for (let i = 0; i < 3; i++) {
|
|
114
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The line to print, or null for silence.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} options
|
|
123
|
+
* @param {string} options.version what this copy is
|
|
124
|
+
* @param {boolean} [options.force] the reader asked, so answer either way
|
|
125
|
+
* @param {number} [options.now]
|
|
126
|
+
* @param {number} [options.ttl]
|
|
127
|
+
* @param {{read: Function, write: Function}} [options.store]
|
|
128
|
+
* @param {() => Promise<string|null>} [options.latest]
|
|
129
|
+
* @param {object} [options.env]
|
|
130
|
+
* @returns {Promise<string|null>}
|
|
131
|
+
*/
|
|
132
|
+
export async function updateNotice({
|
|
133
|
+
version,
|
|
134
|
+
force = false,
|
|
135
|
+
now = Date.now(),
|
|
136
|
+
ttl = DAY,
|
|
137
|
+
store = fileStore(),
|
|
138
|
+
latest = latestFromRegistry,
|
|
139
|
+
env = process.env,
|
|
140
|
+
} = {}) {
|
|
141
|
+
if (!force && (env.CI || env.NO_UPDATE_NOTIFIER)) return null;
|
|
142
|
+
|
|
143
|
+
const remembered = store.read();
|
|
144
|
+
const fresh = !force && remembered && now - remembered.checked < ttl;
|
|
145
|
+
const newest = fresh ? remembered.latest : await latest();
|
|
146
|
+
// A failed attempt is remembered too, so an offline machine asks once a day and
|
|
147
|
+
// says so once a day, rather than asking — and complaining — on every command.
|
|
148
|
+
if (!fresh) store.write({ checked: now, latest: newest ?? null });
|
|
149
|
+
|
|
150
|
+
if (!newest) {
|
|
151
|
+
return fresh && !force ? null : 'Could not reach the npm registry to check for updates.';
|
|
152
|
+
}
|
|
153
|
+
if (isNewer(newest, version)) {
|
|
154
|
+
return `A newer Prolog Notebook is available: ${version} → ${newest}\n`
|
|
155
|
+
+ 'Update with: npm i -g prolog-notebook';
|
|
156
|
+
}
|
|
157
|
+
return force ? `Prolog Notebook ${version} is the latest.` : null;
|
|
158
|
+
}
|
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.
|
|
13
|
+
export const VERSION = '0.4.0';
|
|
14
14
|
|
|
15
15
|
/** The two facts a licence notice is actually made of. */
|
|
16
16
|
export const YEAR = '2026';
|