hypomnema 1.7.3 → 1.8.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +1202 -0
- package/README.ko.md +7 -7
- package/README.md +7 -7
- package/commands/audit.md +1 -1
- package/commands/capture.md +2 -2
- package/commands/crystallize.md +4 -4
- package/commands/doctor.md +15 -1
- package/commands/feedback.md +1 -1
- package/commands/graph.md +1 -1
- package/commands/ingest.md +1 -1
- package/commands/init.md +2 -2
- package/commands/lint.md +1 -1
- package/commands/query.md +1 -1
- package/commands/rename.md +1 -1
- package/commands/resume.md +1 -1
- package/commands/stats.md +1 -1
- package/commands/uninstall.md +17 -5
- package/commands/upgrade.md +2 -2
- package/commands/verify.md +1 -1
- package/docs/ARCHITECTURE.md +9 -4
- package/docs/CONTRIBUTING.md +15 -5
- package/hooks/base-store.mjs +198 -0
- package/hooks/close-gate-store.mjs +436 -0
- package/hooks/hooks.json +2 -1
- package/hooks/hypo-auto-minimal-crystallize.mjs +10 -0
- package/hooks/hypo-close-guard.mjs +24 -4
- package/hooks/hypo-compact-guard.mjs +5 -3
- package/hooks/hypo-cwd-change.mjs +8 -0
- package/hooks/hypo-personal-check.mjs +128 -68
- package/hooks/hypo-session-end.mjs +21 -2
- package/hooks/hypo-session-start.mjs +91 -10
- package/hooks/hypo-shared.mjs +498 -180
- package/hooks/version-check.mjs +44 -6
- package/package.json +10 -3
- package/scripts/capture.mjs +64 -21
- package/scripts/crystallize.mjs +20 -2044
- package/scripts/doctor.mjs +334 -14
- package/scripts/init.mjs +132 -92
- package/scripts/lib/crystallize-args.mjs +50 -0
- package/scripts/lib/crystallize-close-apply.mjs +1830 -0
- package/scripts/lib/crystallize-close-check.mjs +238 -0
- package/scripts/lib/crystallize-close-gate.mjs +58 -0
- package/scripts/lib/crystallize-helpers.mjs +55 -0
- package/scripts/lib/design-history-stale.mjs +26 -7
- package/scripts/lib/extensions.mjs +235 -65
- package/scripts/lib/git-hooks-dir.mjs +214 -2
- package/scripts/lib/plugin-detect.mjs +176 -21
- package/scripts/lib/slug-resolver.mjs +181 -0
- package/scripts/lint.mjs +33 -23
- package/scripts/proposal.mjs +3 -3
- package/scripts/rename.mjs +38 -141
- package/scripts/uninstall.mjs +155 -58
- package/scripts/upgrade.mjs +243 -20
- package/skills/crystallize/SKILL.md +16 -14
- package/skills/graph/SKILL.md +3 -3
- package/skills/ingest/SKILL.md +2 -2
- package/skills/lint/SKILL.md +3 -3
- package/skills/query/SKILL.md +3 -3
- package/skills/verify/SKILL.md +3 -3
- package/templates/hypo-automation.md +59 -17
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +12 -9
- package/templates/hypo-help.md +1 -1
package/hooks/version-check.mjs
CHANGED
|
@@ -156,16 +156,54 @@ export function selectPluginVersion(plugins) {
|
|
|
156
156
|
return typeof v === 'string' ? v : null;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
/**
|
|
159
|
+
/** The command that runs an upgrade apply, in the form the caller's channel can
|
|
160
|
+
* actually run. A plugin-only install has no `hypomnema` on PATH: that binary is
|
|
161
|
+
* the npm package's bin, while the plugin manifest ships commands only. And the
|
|
162
|
+
* slash command takes no `--apply` argument, so naming the flag there describes
|
|
163
|
+
* a call it does not make: it runs the check and appends --apply only after the
|
|
164
|
+
* operator confirms.
|
|
165
|
+
*
|
|
166
|
+
* Lives here because both hooks and scripts need it and the dependency only
|
|
167
|
+
* runs one way (scripts import hooks, never the reverse). This file is already
|
|
168
|
+
* in hooks.json's `shared` list, so it reaches every standalone install.
|
|
169
|
+
*
|
|
170
|
+
* Use UPGRADE_APPLY_EITHER when the target channel is genuinely unknown, e.g.
|
|
171
|
+
* an instruction about a DIFFERENT machine. */
|
|
172
|
+
export function upgradeApplyHint(isPluginChannel) {
|
|
173
|
+
return isPluginChannel
|
|
174
|
+
? '`/hypo:upgrade` (confirm the apply step)'
|
|
175
|
+
: '`hypomnema upgrade --apply`';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const UPGRADE_APPLY_EITHER =
|
|
179
|
+
'`hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install)';
|
|
180
|
+
|
|
181
|
+
/** Channel-specific one-line update instruction.
|
|
182
|
+
*
|
|
183
|
+
* Every channel ends with the same second step. Installing a release does NOT
|
|
184
|
+
* repoint the vault's git pre-commit hook: that hook holds an absolute path
|
|
185
|
+
* baked in at init time, and only `upgrade --apply` rewrites it. No hook can
|
|
186
|
+
* do it either (a hook must never invoke an apply path). Without this line the
|
|
187
|
+
* user updates, sees the new version string, and keeps committing through the
|
|
188
|
+
* install they had when they first ran init. */
|
|
160
189
|
export function buildUpdateLine(channel, current, latest) {
|
|
161
190
|
const head = `[Hypomnema] Update available! ${current} → ${latest}`;
|
|
191
|
+
// The repoint step has to be RUNNABLE on the channel it is printed for. A
|
|
192
|
+
// plugin-only install has no `hypomnema` on PATH (that binary is the npm
|
|
193
|
+
// package's bin; the plugin manifest ships commands only), so its form is the
|
|
194
|
+
// slash command. And the slash command takes no --apply argument: that
|
|
195
|
+
// command runs the check first and only appends --apply after the operator
|
|
196
|
+
// confirms, so naming the flag there would describe a call the command does
|
|
197
|
+
// not make. Both mistakes are the same class as naming a flag no script parses.
|
|
198
|
+
const repoint = (cmd) => `\n → then: ${cmd} (repoints the vault's git hook at the new install)`;
|
|
162
199
|
if (channel === 'plugin') {
|
|
163
|
-
return `${head}\n → run: /plugin marketplace update hypomnema then /reload-plugins`;
|
|
164
|
-
}
|
|
165
|
-
if (channel === 'npm') {
|
|
166
|
-
return `${head}\n → run: npm install -g hypomnema`;
|
|
200
|
+
return `${head}\n → run: /plugin marketplace update hypomnema then /reload-plugins${repoint(upgradeApplyHint(true))}`;
|
|
167
201
|
}
|
|
168
|
-
|
|
202
|
+
// The only remaining displayed channel is npm: computeNotice drops 'unknown'
|
|
203
|
+
// before anything renders, and a dual install reports the channel of the hook
|
|
204
|
+
// root actually running, never a third "mixed" value. A branch for one would
|
|
205
|
+
// be code no user reaches.
|
|
206
|
+
return `${head}\n → run: npm install -g hypomnema${repoint(upgradeApplyHint(false))}`;
|
|
169
207
|
}
|
|
170
208
|
|
|
171
209
|
// ── cache freshness + notice decision (pure) ─────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypomnema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "LLM-native personal wiki system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"scripts/verify.mjs",
|
|
29
29
|
"scripts/weekly-report.mjs",
|
|
30
30
|
"scripts/lib/core-hooks.mjs",
|
|
31
|
+
"scripts/lib/crystallize-args.mjs",
|
|
32
|
+
"scripts/lib/crystallize-close-apply.mjs",
|
|
33
|
+
"scripts/lib/crystallize-close-check.mjs",
|
|
34
|
+
"scripts/lib/crystallize-close-gate.mjs",
|
|
35
|
+
"scripts/lib/crystallize-helpers.mjs",
|
|
31
36
|
"scripts/lib/design-history-stale.mjs",
|
|
32
37
|
"scripts/lib/extensions.mjs",
|
|
33
38
|
"scripts/lib/failure-type.mjs",
|
|
@@ -43,6 +48,7 @@
|
|
|
43
48
|
"scripts/lib/project-create.mjs",
|
|
44
49
|
"scripts/lib/rename-marker.mjs",
|
|
45
50
|
"scripts/lib/schema-vocab.mjs",
|
|
51
|
+
"scripts/lib/slug-resolver.mjs",
|
|
46
52
|
"scripts/lib/template-schema-version.mjs",
|
|
47
53
|
"scripts/lib/wd-match.mjs",
|
|
48
54
|
"scripts/lib/wikilink.mjs",
|
|
@@ -52,7 +58,8 @@
|
|
|
52
58
|
"templates/",
|
|
53
59
|
"docs/",
|
|
54
60
|
".claude-plugin/",
|
|
55
|
-
"README.md"
|
|
61
|
+
"README.md",
|
|
62
|
+
"CHANGELOG.md"
|
|
56
63
|
],
|
|
57
64
|
"keywords": [
|
|
58
65
|
"wiki",
|
|
@@ -91,6 +98,6 @@
|
|
|
91
98
|
"prepublishOnly": "npm test && npm run lint && npm run check:versions && npm run smoke:plugin && node scripts/check-pack-surface.mjs && npm run smoke-pack && npm run check:bilingual && npm run check:tracker-ids"
|
|
92
99
|
},
|
|
93
100
|
"devDependencies": {
|
|
94
|
-
"prettier": "
|
|
101
|
+
"prettier": "3.8.3"
|
|
95
102
|
}
|
|
96
103
|
}
|
package/scripts/capture.mjs
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* acquired through the normal sync path, never by injecting a SHA directly.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import { UPGRADE_APPLY_EITHER } from '../hooks/version-check.mjs';
|
|
35
36
|
import {
|
|
36
37
|
existsSync,
|
|
37
38
|
readFileSync,
|
|
@@ -45,7 +46,8 @@ import {
|
|
|
45
46
|
openSync,
|
|
46
47
|
closeSync,
|
|
47
48
|
statSync,
|
|
48
|
-
|
|
49
|
+
fstatSync,
|
|
50
|
+
fchmodSync,
|
|
49
51
|
} from 'fs';
|
|
50
52
|
import { randomBytes } from 'crypto';
|
|
51
53
|
import { join, dirname, relative, sep } from 'path';
|
|
@@ -75,6 +77,8 @@ import {
|
|
|
75
77
|
scanSettingsHooks,
|
|
76
78
|
parseCapturableHookCommand,
|
|
77
79
|
HOOK_EVENT_ALLOWLIST,
|
|
80
|
+
HOOK_INTERPRETER_EXT,
|
|
81
|
+
DEFAULT_HOOK_INTERPRETER,
|
|
78
82
|
SKILL_ROOT_FILE,
|
|
79
83
|
EXT_PREFIX,
|
|
80
84
|
withSrcExecBits,
|
|
@@ -89,8 +93,9 @@ const HOME = homedir();
|
|
|
89
93
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
90
94
|
|
|
91
95
|
// Captured types and their singular manifest `type` value. commands/agents install
|
|
92
|
-
// as top-level `.md` files; hooks install as
|
|
93
|
-
//
|
|
96
|
+
// as top-level `.md` files; hooks install as an interpreter-specific executable
|
|
97
|
+
// (.mjs/.py/.sh) and additionally register a settings.json entry
|
|
98
|
+
// (reverse of forward-sync); skills install as a DIRECTORY.
|
|
94
99
|
const CAPTURE_TYPES = ['commands', 'agents', 'hooks', 'skills'];
|
|
95
100
|
// The flat, single-file types. `TYPE_EXT` has no `skills` entry on purpose: a skill
|
|
96
101
|
// has no install extension, and letting it through a flat helper would silently
|
|
@@ -100,7 +105,20 @@ const FLAT_TYPES = ['commands', 'agents', 'hooks'];
|
|
|
100
105
|
// registration instead, and skills have their own directory scanner.
|
|
101
106
|
const READDIR_TYPES = ['commands', 'agents'];
|
|
102
107
|
const TYPE_SINGULAR = { commands: 'command', agents: 'agent', hooks: 'hook', skills: 'skill' };
|
|
103
|
-
|
|
108
|
+
// `TYPE_EXT` has no `hooks` entry either, for the same reason as skills: a hook's
|
|
109
|
+
// extension depends on its interpreter (HOOK_INTERPRETER_EXT), not its type.
|
|
110
|
+
// captureFileExt() is the one place that resolves it; every hook-touching call
|
|
111
|
+
// site below goes through that, not this map.
|
|
112
|
+
const TYPE_EXT = { commands: '.md', agents: '.md' };
|
|
113
|
+
|
|
114
|
+
/** The install/wiki-storage extension for one capture candidate. hooks vary by
|
|
115
|
+
* interpreter (mirrors buildHookCommand/parseCapturableHookCommand in
|
|
116
|
+
* extensions.mjs); the other flat types have one fixed extension. */
|
|
117
|
+
function captureFileExt(c) {
|
|
118
|
+
return c.type === 'hooks'
|
|
119
|
+
? HOOK_INTERPRETER_EXT[c.interpreter ?? DEFAULT_HOOK_INTERPRETER]
|
|
120
|
+
: TYPE_EXT[c.type];
|
|
121
|
+
}
|
|
104
122
|
|
|
105
123
|
// Ceilings for a captured skill subtree (skills-capture design §3). A hand-authored
|
|
106
124
|
// skill is a handful of files; a vendored one (gstack: 14,311 files / 1.1GB, mostly
|
|
@@ -123,9 +141,14 @@ function pkgJsonPath() {
|
|
|
123
141
|
* hooks/commands + already-synced hypo-ext-* copies), anything already owned
|
|
124
142
|
* (a recorded SHA for its install path means the wiki already manages it), and
|
|
125
143
|
* non-.md files. Symlink/non-regular is checked by the caller (needs an fs stat).
|
|
144
|
+
* `type` must be a flat readdir type with a single fixed extension (TYPE_EXT);
|
|
145
|
+
* hooks have no entry there (extension depends on interpreter) and are never
|
|
146
|
+
* routed through this function — scanCandidates only calls it for READDIR_TYPES.
|
|
126
147
|
*/
|
|
127
148
|
export function isCaptureCandidate(type, file, recorded) {
|
|
128
|
-
|
|
149
|
+
const ext = TYPE_EXT[type];
|
|
150
|
+
if (!ext) return { ok: false, reason: `capture-by-readdir does not cover ${type}` };
|
|
151
|
+
if (!file.endsWith(ext)) return { ok: false, reason: `not a ${ext} file` };
|
|
129
152
|
const lower = file.toLowerCase();
|
|
130
153
|
if (lower === 'hypo' || lower.startsWith('hypo-')) {
|
|
131
154
|
return { ok: false, reason: 'reserved hypo namespace' };
|
|
@@ -295,10 +318,14 @@ export function planSkillCapture({
|
|
|
295
318
|
/**
|
|
296
319
|
* Reverse-generate the sidecar manifest for a capture candidate. commands/agents
|
|
297
320
|
* carry only `{ type, installName }`; hooks additionally carry the settings
|
|
298
|
-
* registration fields `{ event, matcher?, timeout? }`.
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
321
|
+
* registration fields `{ event, matcher?, timeout?, interpreter? }`. Every
|
|
322
|
+
* optional field is omitted when it would equal its default (an empty-string
|
|
323
|
+
* matcher is already normalized to undefined by the scanner; `node` is the
|
|
324
|
+
* default interpreter) via conditional spread, so the in-memory object never
|
|
325
|
+
* carries an `undefined`-valued key or a redundant explicit default, either of
|
|
326
|
+
* which would break the planCapture deep-equality check against a manifest
|
|
327
|
+
* captured before interpreter support landed (a `.mjs` hook's manifest must stay byte-for-byte
|
|
328
|
+
* identical, or every prior capture becomes a false conflict).
|
|
302
329
|
*/
|
|
303
330
|
function buildWantManifest(c) {
|
|
304
331
|
const manifest = { type: TYPE_SINGULAR[c.type], installName: c.stem };
|
|
@@ -306,6 +333,9 @@ function buildWantManifest(c) {
|
|
|
306
333
|
manifest.event = c.event;
|
|
307
334
|
if (c.matcher !== undefined && c.matcher !== '') manifest.matcher = c.matcher;
|
|
308
335
|
if (c.timeout !== undefined) manifest.timeout = c.timeout;
|
|
336
|
+
if (c.interpreter !== undefined && c.interpreter !== DEFAULT_HOOK_INTERPRETER) {
|
|
337
|
+
manifest.interpreter = c.interpreter;
|
|
338
|
+
}
|
|
309
339
|
}
|
|
310
340
|
return manifest;
|
|
311
341
|
}
|
|
@@ -437,8 +467,9 @@ function subsetOf(keys, allowed) {
|
|
|
437
467
|
* canonical.
|
|
438
468
|
*
|
|
439
469
|
* A candidate must satisfy ALL of: strict command grammar
|
|
440
|
-
* (`parseCapturableHookCommand
|
|
441
|
-
*
|
|
470
|
+
* (`parseCapturableHookCommand`, which also recovers the interpreter — node,
|
|
471
|
+
* python3, or bash) resolving under `~/.claude/hooks/`; the resolved
|
|
472
|
+
* file is a real regular file (no symlink / non-regular); `type === 'command'`;
|
|
442
473
|
* the event is in `HOOK_EVENT_ALLOWLIST`; a preservable shape (F3); the case-folded
|
|
443
474
|
* basename appears exactly once across all settings hooks (F6); and the stem is not
|
|
444
475
|
* a core hook, not the reserved hypo namespace, and not already wiki-owned.
|
|
@@ -478,7 +509,7 @@ export function scanHookCandidates(claudeHome, settingsPath, recorded, coreBasen
|
|
|
478
509
|
skipped.push({ type: 'hooks', command: rec.command, reason: parsed.reason });
|
|
479
510
|
continue;
|
|
480
511
|
}
|
|
481
|
-
const { stem, basename } = parsed;
|
|
512
|
+
const { stem, basename, interpreter } = parsed;
|
|
482
513
|
const label = `hooks/${basename}`;
|
|
483
514
|
|
|
484
515
|
// Duplicate first: a basename registered 2+ times is uniformly skipped so both
|
|
@@ -546,6 +577,7 @@ export function scanHookCandidates(claudeHome, settingsPath, recorded, coreBasen
|
|
|
546
577
|
event: rec.event,
|
|
547
578
|
matcher: rec.matcher,
|
|
548
579
|
timeout: rec.timeout,
|
|
580
|
+
interpreter,
|
|
549
581
|
label,
|
|
550
582
|
});
|
|
551
583
|
}
|
|
@@ -589,17 +621,27 @@ function log(msg) {
|
|
|
589
621
|
// so this still has to be a separate chmod). Omitted for a manifest write: that
|
|
590
622
|
// content is JSON we generated, not a copy of something with a mode worth
|
|
591
623
|
// keeping.
|
|
624
|
+
//
|
|
625
|
+
// The mode is set on the open FD (`fchmodSync`), before the FD is closed, the
|
|
626
|
+
// same ordering the forward writer (extensions.mjs's writeFreshAtomic) already
|
|
627
|
+
// uses. A pathname-based `chmodSync(tmp, ...)` run after `closeSync` reopens
|
|
628
|
+
// `tmp` by name, and a competing process racing this write could have already
|
|
629
|
+
// deleted `tmp` and planted a symlink at that name in the gap between close
|
|
630
|
+
// and chmod — `wx` only protects the file's creation, not everything after it.
|
|
631
|
+
// Chmod-ing the FD closes that window: it always addresses the file this
|
|
632
|
+
// process itself just created, never whatever a symlink at the same name
|
|
633
|
+
// might point to by the time the pathname is looked up again.
|
|
592
634
|
function writeAtomic(dest, buf, srcMode) {
|
|
593
635
|
const tmp = `${dest}.tmp.${process.pid}.${randomBytes(6).toString('hex')}`;
|
|
594
636
|
const fd = openSync(tmp, 'wx');
|
|
595
637
|
try {
|
|
596
638
|
writeFileSync(fd, buf);
|
|
639
|
+
if (srcMode != null) {
|
|
640
|
+
fchmodSync(fd, withSrcExecBits(fstatSync(fd).mode, srcMode));
|
|
641
|
+
}
|
|
597
642
|
} finally {
|
|
598
643
|
closeSync(fd);
|
|
599
644
|
}
|
|
600
|
-
if (srcMode != null) {
|
|
601
|
-
chmodSync(tmp, withSrcExecBits(statSync(tmp).mode, srcMode));
|
|
602
|
-
}
|
|
603
645
|
try {
|
|
604
646
|
renameSync(tmp, dest);
|
|
605
647
|
} catch (err) {
|
|
@@ -982,7 +1024,7 @@ function run(args, { claudeHome = join(HOME, '.claude') } = {}) {
|
|
|
982
1024
|
captureOneSkill({ c, extDir, guard, wikiRoot, args, captured, skipped, created });
|
|
983
1025
|
continue;
|
|
984
1026
|
}
|
|
985
|
-
const fileExt =
|
|
1027
|
+
const fileExt = captureFileExt(c);
|
|
986
1028
|
const wikiStem = `${EXT_PREFIX}${c.stem}`;
|
|
987
1029
|
const typeDir = join(extDir, c.type);
|
|
988
1030
|
const filePath = join(typeDir, `${wikiStem}${fileExt}`);
|
|
@@ -1013,11 +1055,12 @@ function run(args, { claudeHome = join(HOME, '.claude') } = {}) {
|
|
|
1013
1055
|
const plan = planCapture({ wantManifest, srcSha, existingFileSha, existingManifestRaw });
|
|
1014
1056
|
|
|
1015
1057
|
// installFile is the ORIGINAL name the far machine installs under (installName
|
|
1016
|
-
// adoption); for hooks that is `<stem
|
|
1058
|
+
// adoption); for hooks that is `<stem>` plus its interpreter's extension
|
|
1059
|
+
// (`.mjs`/`.py`/`.sh`), for commands/agents `<stem>.md`.
|
|
1017
1060
|
const installFile = `${c.stem}${fileExt}`;
|
|
1018
1061
|
// Ownership keys sync must record for the adoption to count. Hooks track BOTH
|
|
1019
|
-
// the
|
|
1020
|
-
// criterion d); commands/agents track only the single install file.
|
|
1062
|
+
// the main file and its `.manifest.json` sidecar under the install path
|
|
1063
|
+
// (success criterion d); commands/agents track only the single install file.
|
|
1021
1064
|
const requiredKeys =
|
|
1022
1065
|
c.type === 'hooks'
|
|
1023
1066
|
? [`hooks/${installFile}`, `hooks/${c.stem}.manifest.json`]
|
|
@@ -1162,7 +1205,7 @@ function report(captured, skipped, failed, dryRun) {
|
|
|
1162
1205
|
const dest =
|
|
1163
1206
|
c.type === 'skills'
|
|
1164
1207
|
? `extensions/skills/${EXT_PREFIX}${c.stem}/`
|
|
1165
|
-
: `extensions/${c.type}/${EXT_PREFIX}${c.stem}${
|
|
1208
|
+
: `extensions/${c.type}/${EXT_PREFIX}${c.stem}${captureFileExt(c)}`;
|
|
1166
1209
|
log(` ${c.type}/${c.file} → ${dest}`);
|
|
1167
1210
|
}
|
|
1168
1211
|
}
|
|
@@ -1171,7 +1214,7 @@ function report(captured, skipped, failed, dryRun) {
|
|
|
1171
1214
|
log(`Failed to adopt ${failed.length} (see sync warnings; wiki files rolled back).`);
|
|
1172
1215
|
if (!dryRun && captured.some((c) => c.status === 'captured')) {
|
|
1173
1216
|
log('');
|
|
1174
|
-
log(
|
|
1217
|
+
log(`Next: commit + push the wiki, then run ${UPGRADE_APPLY_EITHER} on another machine.`);
|
|
1175
1218
|
}
|
|
1176
1219
|
}
|
|
1177
1220
|
|