prolog-notebook 0.6.2 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.4] — 2026-08-31
4
+
5
+ ### Changed
6
+
7
+ - **`prolog-notebook <command> --help` answers about that command only.** It printed the whole
8
+ card — all five commands — which makes the reader find their command again in a page they did
9
+ not ask for. What works anywhere is still listed, because it is as true of the command they
10
+ asked about as of any other, and the note about a stopped search now travels with `--limit`
11
+ rather than sitting under everything.
12
+
13
+ It works wherever the command is named: `view --help`, `--help view`, and
14
+ `view chapter.prolog.md -h` all ask the same question, as do the `run` and `exec` aliases.
15
+ Name no command, or one that does not exist, and the whole card comes back as before.
16
+
17
+ - **The help, the per-command help and the misplaced-option message are derived from one
18
+ table.** They were three lists kept by hand, which is how the help came to advertise
19
+ `--check-update` under no command while three of the four commands refused it.
20
+
21
+ ## [0.6.3] — 2026-08-30
22
+
23
+ `prolog-notebook build lists.prolog.md --check-update` answered *unknown option* — on a flag the
24
+ help listed under no command, exactly as though it were global.
25
+
26
+ ### Fixed
27
+
28
+ - **`--check-update` works with every command**, and forces the check past the daily TTL, which
29
+ is the whole point of asking. It was accepted by `execute` alone.
30
+
31
+ - **…and it answers down a pipe.** The check was skipped entirely without a terminal, so an
32
+ explicit check in a script reported nothing — indistinguishable from *you are up to date*,
33
+ which is the one thing an update notice must never look like. A terminal is needed to **offer**
34
+ an upgrade, never to **report** one.
35
+
36
+ - **`build --port 90` was accepted and ignored.** `view` and `build` share a parser, so each
37
+ quietly took the other's flags. A flag that is read and thrown away is worse than one refused:
38
+ it looks like it worked.
39
+
40
+ - **`clear` never looked for an update.** 0.5.1 established that every command doing real work
41
+ goes through one door, before the work; `clear` shipped afterwards and missed it, while
42
+ rewriting the author's notebook.
43
+
44
+ ### Changed
45
+
46
+ - **The help lists each option under the command that takes it.** Only `--check-update`,
47
+ `--version` and `-h` are listed as working anywhere, because only those three do.
48
+
49
+ - **A misplaced option says where it lives** — `--limit belongs to execute, not to view` —
50
+ rather than `unknown option` about a flag that is real and two lines further up.
51
+
3
52
  ## [0.6.2] — 2026-08-30
4
53
 
5
54
  From a field report: a built chapter opened in Chrome and Safari showed no controls and a Run
package/README.md CHANGED
@@ -132,16 +132,24 @@ chapter.prolog.md: 4 answers removed
132
132
  `clear` empties every output block and touches nothing else; `execute` fills them in again from
133
133
  the engine. A chapter with no answers is a valid chapter — one that has not been executed yet.
134
134
 
135
+ Each option belongs to a command, and typing one under the wrong command tells you which:
136
+
137
+ | flag | on | |
138
+ |---|---|---|
139
+ | `--limit <n>` | `execute` | solutions to take from one query before stopping. Default 100. |
140
+ | `--stdout` | `execute`, `clear` | print the result instead of writing the file |
141
+ | `--quiet` | `execute`, `clear` | report only failures |
142
+ | `--out <dir>` | `build` | where it writes. Default `<file>-site` |
143
+ | `--port <n>` | `view` | what it listens on. Default 8777, and it takes another if that one is busy |
144
+ | `--no-open` | `view` | print the URL instead of opening a browser |
145
+
146
+ Three work anywhere, because they are about the tool rather than about a notebook:
147
+
135
148
  | flag | |
136
149
  |---|---|
137
- | `--limit <n>` | solutions to take from one query before stopping. Default 100. |
138
- | `--stdout` | print the result instead of writing the file |
139
- | `--quiet` | report only failures |
140
- | `--out <dir>` | where `build` writes. Default `<file>-site` |
141
- | `--port <n>` | what `view` listens on. Default 8777, and it takes another if that one is busy |
142
- | `--no-open` | `view` prints the URL instead of opening a browser |
143
150
  | `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
144
- | `--check-update` | ask npm whether a newer one exists, and say so either way |
151
+ | `--check-update` | ask npm whether a newer one exists, and say so either way — forced, whatever the daily check thinks, and answered even down a pipe |
152
+ | `-h`, `--help` | help for the command you named, or all of them if you named none |
145
153
 
146
154
  ```sh
147
155
  prolog-notebook upgrade # fetch the latest
@@ -37,29 +37,121 @@ for (const stream of [process.stdout, process.stderr]) {
37
37
  // src/version.js, where a page can import it too.
38
38
  const require = createRequire(import.meta.url);
39
39
 
40
- const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
41
-
42
- prolog-notebook view <file.prolog.md> read it in a browser, cells and all
43
- prolog-notebook build <file.prolog.md> write a page you can host or send
44
- prolog-notebook execute <file.prolog.md>... run every query, write the answers in
45
- prolog-notebook clear <file.prolog.md>... take the answers back out
46
- prolog-notebook upgrade fetch the latest version
47
-
48
- Options
49
- --limit <n> solutions to take from one query before stopping (default ${DEFAULT_LIMIT})
50
- --stdout print the result instead of writing the file
51
- --quiet report only failures
52
- --out <dir> where build writes (default: <file>-site)
53
- --port <n> what view listens on (default 8777)
54
- --no-open view prints the URL instead of opening a browser
55
- --version version, engine and copyright
40
+ /**
41
+ * THE COMMANDS, AND WHAT EACH ONE TAKES — one table, three readers (869erqra0).
42
+ *
43
+ * The whole help, a single command's help, and the message a misplaced option
44
+ * gets are all derived from here. They were three lists kept by hand, which is
45
+ * how the help came to advertise flags that no command accepted.
46
+ *
47
+ * Options belong to COMMANDS, not to the tool. Listing them in one flat block
48
+ * reads as a promise that every one works everywhere, and only three of them do.
49
+ */
50
+ const COMMANDS = {
51
+ view: {
52
+ usage: 'prolog-notebook view <file.prolog.md>',
53
+ blurb: 'read it in a browser, cells and all',
54
+ options: [
55
+ ['--port <n>', 'what it listens on (default 8777)'],
56
+ ['--no-open', 'print the URL instead of opening a browser'],
57
+ ],
58
+ },
59
+ build: {
60
+ usage: 'prolog-notebook build <file.prolog.md>',
61
+ blurb: 'write a page you can host or send',
62
+ options: [['--out <dir>', 'where it writes (default: <file>-site)']],
63
+ },
64
+ execute: {
65
+ usage: 'prolog-notebook execute <file.prolog.md>...',
66
+ blurb: 'run every query, write the answers in',
67
+ options: [
68
+ ['--limit <n>', `solutions to take from one query before stopping (default ${DEFAULT_LIMIT})`],
69
+ ['--stdout', 'print the result instead of writing the file'],
70
+ ['--quiet', 'report only failures'],
71
+ ],
72
+ // Belongs to --limit, so it goes wherever --limit goes and nowhere else.
73
+ note: 'A query that stops at the limit is written without a terminator, which is the\n'
74
+ + "format's way of saying the search was never exhausted. Nothing is invented.\n",
75
+ },
76
+ clear: {
77
+ usage: 'prolog-notebook clear <file.prolog.md>...',
78
+ blurb: 'take the answers back out',
79
+ options: [
80
+ ['--stdout', 'print the result instead of writing the file'],
81
+ ['--quiet', 'report only failures'],
82
+ ],
83
+ },
84
+ upgrade: {
85
+ usage: 'prolog-notebook upgrade',
86
+ blurb: 'fetch the latest version',
87
+ options: [],
88
+ },
89
+ };
90
+
91
+ /** `exec` and `run` reach `execute` and are deliberately undocumented (869erp0jd). */
92
+ const ALIASES = { exec: 'execute', run: 'execute' };
93
+
94
+ /** The command this argument names, aliases resolved, or null. */
95
+ const commandNamed = (arg) => (COMMANDS[arg] ? arg : ALIASES[arg] ?? null);
96
+
97
+ const ANYWHERE = `Anywhere
56
98
  --check-update ask npm whether a newer one exists, and say so either way
99
+ --version version, engine and copyright
57
100
  -h, --help this
58
-
59
- A query that stops at the limit is written without a terminator, which is the
60
- format's way of saying the search was never exhausted. Nothing is invented.
61
101
  `;
62
102
 
103
+ /** One command, laid out exactly as it is laid out in the full help. */
104
+ function commandHelp(name) {
105
+ const { usage, blurb, options } = COMMANDS[name];
106
+ return [` ${usage.padEnd(44)}${blurb}`]
107
+ .concat(options.map(([flag, what]) => ` ${flag.padEnd(16)}${what}`))
108
+ .join('\n');
109
+ }
110
+
111
+ const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
112
+
113
+ ${Object.keys(COMMANDS).map(commandHelp).join('\n\n')}
114
+
115
+ ${ANYWHERE}
116
+ ${COMMANDS.execute.note}`;
117
+
118
+ /**
119
+ * JUST THE COMMAND ASKED ABOUT (869erqra0).
120
+ *
121
+ * The Captain, on being shown all five for `build --help`: "not really. If I run
122
+ * prolog-notebook cmd --help I want only help on that cmd." Printing everything
123
+ * makes the reader find their command again in a page they did not ask for.
124
+ *
125
+ * What works anywhere stays, because it is true of the command they asked about
126
+ * as much as of any other.
127
+ */
128
+ function helpFor(name) {
129
+ const { note } = COMMANDS[name];
130
+ return `${commandHelp(name)}\n\n${ANYWHERE}${note ? `\n${note}` : ''}`;
131
+ }
132
+
133
+ /**
134
+ * Which command each option belongs to, so a misplaced one can say where it lives.
135
+ *
136
+ * Derived, so a flag added to a command above cannot be forgotten here.
137
+ * `unknown option "--limit"` is true and unhelpful when the flag is real and two
138
+ * lines further up the same help.
139
+ */
140
+ const BELONGS_TO = {};
141
+ for (const [name, { options }] of Object.entries(COMMANDS)) {
142
+ for (const [flag] of options) {
143
+ const bare = flag.split(' ')[0];
144
+ BELONGS_TO[bare] = BELONGS_TO[bare] ? `${BELONGS_TO[bare]} and ${name}` : name;
145
+ }
146
+ }
147
+
148
+ function unknownOption(arg, command) {
149
+ const home = BELONGS_TO[arg];
150
+ return home && home !== command
151
+ ? `${arg} belongs to ${home}, not to ${command}\n`
152
+ : `unknown option "${arg}"\n`;
153
+ }
154
+
63
155
  /**
64
156
  * A runaway goal hangs this process — the engine is in-process here, so there is
65
157
  * no thread left to notice (869ejgyax). Stated rather than implied, because the
@@ -158,11 +250,18 @@ function canAsk() {
158
250
  * to a newer version, null to carry on here.
159
251
  */
160
252
  async function upgradeFirst({ quiet = false, asked = false } = {}) {
161
- if (!canAsk() || (quiet && !asked)) return null;
253
+ // ASKED OUTRIGHT ALWAYS ANSWERS (869erqra0). A terminal is needed to OFFER the
254
+ // upgrade, never to report one: `--check-update` down a pipe used to check
255
+ // nothing and say nothing, which is indistinguishable from "you are up to
256
+ // date" — the one thing this must never look like.
257
+ if (!asked && (!canAsk() || quiet)) return null;
162
258
  const ahead = await updateNotice({ version: VERSION, force: asked })
163
259
  .catch(() => ({ message: null, newer: null }));
164
260
  if (ahead.message) process.stderr.write(`${ahead.message}\n`);
165
- if (!ahead.newer || !(await confirm('Update and continue on the new version?'))) return null;
261
+ // The notice is printed above whatever happens next; only the question needs
262
+ // somebody at the other end to answer it.
263
+ if (!ahead.newer || !canAsk()) return null;
264
+ if (!(await confirm('Update and continue on the new version?'))) return null;
166
265
  if ((await upgrade(ahead.newer)) !== 0) {
167
266
  process.stderr.write('Carrying on with the version you have.\n');
168
267
  return null;
@@ -193,7 +292,11 @@ const NPM_LINE = process.platform === 'win32' ? 'npm.cmd' : 'npm';
193
292
  async function main(argv) {
194
293
  const args = argv.slice(2);
195
294
  if (!args.length || args.includes('-h') || args.includes('--help')) {
196
- process.stdout.write(USAGE);
295
+ // Whichever command they named, wherever they named it: `view --help`,
296
+ // `--help view` and `view chapter.prolog.md -h` all ask the same question.
297
+ // A help flag that is positional is its own small annoyance.
298
+ const named = args.map((arg) => commandNamed(arg)).find(Boolean);
299
+ process.stdout.write(named ? helpFor(named) : USAGE);
197
300
  return 0;
198
301
  }
199
302
  if (args.includes('--version') || args.includes('-V')) {
@@ -213,8 +316,8 @@ async function main(argv) {
213
316
  }
214
317
 
215
318
  const command = args.shift();
216
- if (command === 'view' || command === 'build') return page(command, args);
217
- if (command === 'clear') return clear(args);
319
+ if (command === 'view' || command === 'build') return page(command, args, asked);
320
+ if (command === 'clear') return clear(args, asked);
218
321
  if (command === 'upgrade') {
219
322
  const { message, newer } = await updateNotice({ version: VERSION, force: true });
220
323
  if (message) process.stderr.write(`${message}\n`);
@@ -246,7 +349,7 @@ async function main(argv) {
246
349
  else if (arg === '--quiet') options.quiet = true;
247
350
  else if (arg === '--check-update') { /* handled above, and not a file */ }
248
351
  else if (arg.startsWith('-')) {
249
- process.stderr.write(`unknown option "${arg}"\n\n${USAGE}`);
352
+ process.stderr.write(unknownOption(arg, 'execute'));
250
353
  return 2;
251
354
  } else files.push(arg);
252
355
  }
@@ -259,7 +362,9 @@ async function main(argv) {
259
362
 
260
363
  const jump = await upgradeFirst({ quiet: options.quiet, asked });
261
364
  if (jump !== null) return jump;
262
- checked = canAsk() && (!options.quiet || asked);
365
+ // Whatever upgradeFirst has just reported must not be reported again below.
366
+ // Asked outright, it always reports now, terminal or not.
367
+ checked = asked || (canAsk() && !options.quiet);
263
368
 
264
369
  // STARTED NOW, READ AT THE END. The registry is somebody else's machine on
265
370
  // somebody else's network, and none of that should stand between the reader
@@ -309,14 +414,16 @@ async function main(argv) {
309
414
  * No engine, no network and no update check: this is a text operation on a file
310
415
  * the reader already has.
311
416
  */
312
- async function clear(args) {
417
+ async function clear(args, asked = false) {
313
418
  const options = { stdout: false, quiet: false };
314
419
  const files = [];
315
420
  for (const arg of args) {
316
421
  if (arg === '--stdout') options.stdout = true;
317
422
  else if (arg === '--quiet') options.quiet = true;
423
+ // Handled by the caller, and not a file.
424
+ else if (arg === '--check-update') { /* global */ }
318
425
  else if (arg.startsWith('-')) {
319
- process.stderr.write(`unknown option "${arg}"\n`);
426
+ process.stderr.write(unknownOption(arg, 'clear'));
320
427
  return 2;
321
428
  } else files.push(arg);
322
429
  }
@@ -325,6 +432,13 @@ async function clear(args) {
325
432
  return 2;
326
433
  }
327
434
 
435
+ // THE SAME DOOR AS EVERY OTHER COMMAND THAT DOES REAL WORK (869erqra0). 0.5.1
436
+ // put `view` and `build` through it and this command shipped afterwards, so it
437
+ // never looked. Emptying somebody's notebook is as real as work gets here, and
438
+ // before the work is the only point at which a newer version changes anything.
439
+ const jump = await upgradeFirst({ quiet: options.quiet, asked });
440
+ if (jump !== null) return jump;
441
+
328
442
  let status = 0;
329
443
  for (const file of files) {
330
444
  let source;
@@ -361,11 +475,20 @@ async function clear(args) {
361
475
  * whole reason a built page is readable before any engine arrives. Use `run` to
362
476
  * put them there.
363
477
  */
364
- async function page(command, args) {
478
+ async function page(command, args, asked = false) {
365
479
  const options = { out: null, port: 8777, open: true };
366
480
  const files = [];
367
481
  while (args.length) {
368
482
  const arg = args.shift();
483
+ // ONE PARSER, TWO COMMANDS, AND THEY DO NOT TAKE THE SAME FLAGS. `view` and
484
+ // `build` share this function, so `build --port 90` was quietly accepted and
485
+ // ignored while the help said --port was view's (869erqra0). A flag that is
486
+ // read and thrown away is worse than one that is refused: it looks like it
487
+ // worked.
488
+ if (BELONGS_TO[arg] && BELONGS_TO[arg] !== command) {
489
+ process.stderr.write(unknownOption(arg, command));
490
+ return 2;
491
+ }
369
492
  if (arg === '--out') options.out = args.shift();
370
493
  else if (arg === '--port') {
371
494
  options.port = Number(args.shift());
@@ -374,8 +497,10 @@ async function page(command, args) {
374
497
  return 2;
375
498
  }
376
499
  } else if (arg === '--no-open') options.open = false;
500
+ // Handled by the caller, and not a file.
501
+ else if (arg === '--check-update') { /* global */ }
377
502
  else if (arg.startsWith('-')) {
378
- process.stderr.write(`unknown option "${arg}"\n`);
503
+ process.stderr.write(unknownOption(arg, command));
379
504
  return 2;
380
505
  } else files.push(arg);
381
506
  }
@@ -387,7 +512,7 @@ async function page(command, args) {
387
512
  // The same offer the execute path makes, and for the same reason: a server about to
388
513
  // start, or a directory about to be written, is work that a newer version
389
514
  // should be doing.
390
- const jump = await upgradeFirst();
515
+ const jump = await upgradeFirst({ asked });
391
516
  if (jump !== null) return jump;
392
517
 
393
518
  const file = files[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prolog-notebook",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Jupyter-style notebooks for Prolog. Runs in the browser, installs nothing.",
5
5
  "type": "module",
6
6
  "main": "./src/node.js",
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "bdbce1b",
3
- "built": "2026-08-30 22:41:37 UTC"
2
+ "commit": "9973408",
3
+ "built": "2026-08-30 23:05:20 UTC"
4
4
  }
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.6.2';
13
+ export const VERSION = '0.6.4';
14
14
 
15
15
  /** The two facts a licence notice is actually made of. */
16
16
  export const YEAR = '2026';