dotmd-cli 0.53.0 → 0.55.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/bin/dotmd.mjs +14 -0
- package/package.json +4 -2
- package/scripts/postinstall.mjs +36 -0
- package/src/commands.mjs +1 -1
- package/src/hud.mjs +37 -2
- package/src/update.mjs +116 -0
package/bin/dotmd.mjs
CHANGED
|
@@ -37,6 +37,7 @@ const FLAG_SPECS = {
|
|
|
37
37
|
hud: { flags: new Set(['--json', '--subagent']), values: new Set() },
|
|
38
38
|
guard: { flags: new Set(), values: new Set() },
|
|
39
39
|
misuse: { flags: new Set(['--json', '--tail', '--by-rule', '--repo']), values: new Set(['--tail', '--repo']) },
|
|
40
|
+
update: { flags: new Set(['--check', '--cli-only', '--plugin-only']), values: new Set() },
|
|
40
41
|
check: { flags: new Set(['--fix', '--errors-only', '--no-collapse', '--json', '--verbose']), values: new Set() },
|
|
41
42
|
doctor: { flags: new Set(['--apply', '--yes', '--dry-run', '-n', '--statuses', '--migrate-template', '--migrate-prompts', '--frontmatter-fix', '--project', '--json', '--include-archived']), values: new Set() },
|
|
42
43
|
runlist: { flags: new Set(['--json', '--full', '--no-index', '--show-files']), values: new Set(), subcommands: new Set(['next']) },
|
|
@@ -157,6 +158,18 @@ Rules:
|
|
|
157
158
|
Every catch is appended to the cross-repo misuse log. Disable with DOTMD_GUARD=0.
|
|
158
159
|
Read the log with \`dotmd misuse\`.`,
|
|
159
160
|
|
|
161
|
+
update: `dotmd update — update the dotmd CLI and the Claude Code plugin together
|
|
162
|
+
|
|
163
|
+
dotmd update npm i -g dotmd-cli + claude plugin update dotmd@dotmd
|
|
164
|
+
dotmd update --check report CLI vs plugin versions, do nothing (network-free)
|
|
165
|
+
dotmd update --cli-only only the npm CLI
|
|
166
|
+
dotmd update --plugin-only only the plugin
|
|
167
|
+
|
|
168
|
+
The plugin and CLI ship in lockstep; a release bumps both. Updating the plugin
|
|
169
|
+
requires a session restart (or /reload-plugins) to apply. The plugin step needs
|
|
170
|
+
the \`claude\` CLI on PATH — otherwise it prints the \`/plugin update\` command to
|
|
171
|
+
run from a session instead.`,
|
|
172
|
+
|
|
160
173
|
misuse: `dotmd misuse — read the cross-repo guard log (~/.claude/logs/dotmd-misuse.log)
|
|
161
174
|
|
|
162
175
|
dotmd misuse last 20 intercepted wrong-moves
|
|
@@ -1305,6 +1318,7 @@ async function main() {
|
|
|
1305
1318
|
// Lifecycle commands
|
|
1306
1319
|
if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
|
|
1307
1320
|
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config); return; }
|
|
1321
|
+
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config); return; }
|
|
1308
1322
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1309
1323
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1310
1324
|
if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotmd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
15
|
"src/",
|
|
16
|
+
"scripts/postinstall.mjs",
|
|
16
17
|
"dotmd.config.example.mjs"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|
|
@@ -39,8 +40,9 @@
|
|
|
39
40
|
"homepage": "https://github.com/reowens/dotmd#readme",
|
|
40
41
|
"scripts": {
|
|
41
42
|
"test": "node --test test/*.test.mjs",
|
|
43
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
42
44
|
"preversion": "npm test",
|
|
43
|
-
"version": "node bin/dotmd.mjs hud >/dev/null 2>&1; git add .claude/commands docs/docs.md 2>/dev/null; true",
|
|
45
|
+
"version": "node bin/dotmd.mjs hud >/dev/null 2>&1; node scripts/sync-plugin-version.mjs; git add .claude/commands docs/docs.md plugins/dotmd/.claude-plugin/plugin.json .claude-plugin/marketplace.json 2>/dev/null; true",
|
|
44
46
|
"postversion": "bash scripts/postversion.sh"
|
|
45
47
|
},
|
|
46
48
|
"engines": {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Runs after `npm install dotmd-cli`. Because the dotmd Claude Code plugin is a
|
|
2
|
+
// separate artifact from this CLI, upgrading the CLI alone leaves the plugin
|
|
3
|
+
// (hooks/skill/commands) on its old version. This script bridges that — but
|
|
4
|
+
// conservatively:
|
|
5
|
+
//
|
|
6
|
+
// - Only on GLOBAL installs (`npm i -g`). A project devDep / CI / Docker
|
|
7
|
+
// install must never touch a user's Claude Code state.
|
|
8
|
+
// - Default: just print a one-line nudge. A CLI install silently mutating the
|
|
9
|
+
// agent's plugin cache is surprising; opt in with DOTMD_AUTO_PLUGIN_UPDATE=1
|
|
10
|
+
// to actually run the refresh.
|
|
11
|
+
// - NEVER fail the install: everything is swallowed and we always exit 0. A
|
|
12
|
+
// nonzero postinstall would break `npm i -g dotmd-cli`.
|
|
13
|
+
// - Skipped entirely under `npm install --ignore-scripts`.
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Lifecycle env: npm sets this to "true" for global installs.
|
|
18
|
+
if (process.env.npm_config_global !== 'true') process.exit(0);
|
|
19
|
+
|
|
20
|
+
const hasClaude = (() => {
|
|
21
|
+
try {
|
|
22
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
23
|
+
return spawnSync(cmd, ['claude'], { encoding: 'utf8' }).status === 0;
|
|
24
|
+
} catch { return false; }
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
if (process.env.DOTMD_AUTO_PLUGIN_UPDATE === '1' && hasClaude) {
|
|
28
|
+
spawnSync('claude', ['plugin', 'update', 'dotmd@dotmd'], { stdio: 'ignore', timeout: 60000 });
|
|
29
|
+
process.stdout.write('dotmd: refreshed the Claude Code plugin — restart your session (or /reload-plugins) to apply.\n');
|
|
30
|
+
} else {
|
|
31
|
+
process.stdout.write('dotmd CLI installed. Using the Claude Code plugin? Run `dotmd update` to refresh it too, then restart.\n');
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Best effort only — never break the install.
|
|
35
|
+
}
|
|
36
|
+
process.exit(0);
|
package/src/commands.mjs
CHANGED
|
@@ -8,6 +8,6 @@ export const KNOWN_COMMANDS = [
|
|
|
8
8
|
'unblocks', 'health', 'glossary', 'modules', 'module',
|
|
9
9
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
10
10
|
'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
|
|
11
|
-
'guard', 'misuse',
|
|
11
|
+
'guard', 'misuse', 'update',
|
|
12
12
|
'ship', 'self-check',
|
|
13
13
|
];
|
package/src/hud.mjs
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
5
|
import { asString, toRepoPath, currentSessionId } from './util.mjs';
|
|
5
|
-
import { dim } from './color.mjs';
|
|
6
|
+
import { dim, yellow } from './color.mjs';
|
|
6
7
|
import { buildIndex } from './index.mjs';
|
|
7
8
|
import { refreshStaleSlashCommands } from './claude-commands.mjs';
|
|
8
9
|
import { readJournalEntries, journalFilePath } from './journal.mjs';
|
|
10
|
+
import { compareVersions } from './update.mjs';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
14
|
+
|
|
15
|
+
// Detect when the running plugin's bundled version disagrees with this CLI's
|
|
16
|
+
// version. Since every release bumps both in lockstep, a mismatch means exactly
|
|
17
|
+
// one channel is behind. Network-free: the plugin's hook sets CLAUDE_PLUGIN_ROOT
|
|
18
|
+
// to the plugin dir, whose plugin.json carries its version. Gated to the
|
|
19
|
+
// version-keyed *cache* install — a directory-source plugin tracks content live
|
|
20
|
+
// and its version label lags benignly, so we don't nag local dev. Returns a
|
|
21
|
+
// one-line notice or null (silent when in sync). Surfaces to the agent because
|
|
22
|
+
// hud output is injected as SessionStart/SubagentStart context.
|
|
23
|
+
export function detectVersionDrift(env = process.env) {
|
|
24
|
+
try {
|
|
25
|
+
const root = env.CLAUDE_PLUGIN_ROOT;
|
|
26
|
+
if (!root) return null;
|
|
27
|
+
const cacheSeg = `${path.sep}plugins${path.sep}cache${path.sep}`;
|
|
28
|
+
if (!root.includes(cacheSeg)) return null;
|
|
29
|
+
const pj = path.join(root, '.claude-plugin', 'plugin.json');
|
|
30
|
+
if (!existsSync(pj)) return null;
|
|
31
|
+
const pluginVersion = JSON.parse(readFileSync(pj, 'utf8')).version;
|
|
32
|
+
const cmp = compareVersions(pluginVersion, pkg.version);
|
|
33
|
+
if (cmp === null || cmp === 0) return null;
|
|
34
|
+
if (cmp < 0) return `dotmd plugin ${pluginVersion} is behind the CLI ${pkg.version} — run \`dotmd update\` then restart.`;
|
|
35
|
+
return `dotmd CLI ${pkg.version} is behind the plugin ${pluginVersion} — run \`dotmd update\` (or npm i -g dotmd-cli).`;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
// Statuses that count as "actionable" for a prompt are derived from config:
|
|
11
42
|
// types.prompt.context.expanded (the statuses the user wants prominently shown).
|
|
@@ -201,11 +232,14 @@ const SUBAGENT_PRIMER = [
|
|
|
201
232
|
export function runHud(argv, config) {
|
|
202
233
|
const json = argv.includes('--json');
|
|
203
234
|
|
|
235
|
+
const drift = detectVersionDrift();
|
|
236
|
+
|
|
204
237
|
// SubagentStart hook entry point — emit the compact primer and return. No
|
|
205
238
|
// index build, no journal read, no slash-command heal: a subagent doesn't
|
|
206
239
|
// need the operator-facing machinery, just the verbs and the guardrails.
|
|
207
240
|
if (argv.includes('--subagent')) {
|
|
208
241
|
process.stdout.write(dim(SUBAGENT_PRIMER) + '\n');
|
|
242
|
+
if (drift) process.stdout.write(yellow(drift) + '\n');
|
|
209
243
|
return;
|
|
210
244
|
}
|
|
211
245
|
|
|
@@ -222,7 +256,7 @@ export function runHud(argv, config) {
|
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
if (json) {
|
|
225
|
-
process.stdout.write(JSON.stringify(hud, null, 2) + '\n');
|
|
259
|
+
process.stdout.write(JSON.stringify({ ...hud, drift: drift ?? null }, null, 2) + '\n');
|
|
226
260
|
return;
|
|
227
261
|
}
|
|
228
262
|
|
|
@@ -237,4 +271,5 @@ export function runHud(argv, config) {
|
|
|
237
271
|
// `dotmd hud --json` for programmatic callers. The hook's job is purely to
|
|
238
272
|
// teach the verbs, never to report status.
|
|
239
273
|
process.stdout.write(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] → oldest pending prompt)') + '\n');
|
|
274
|
+
if (drift) process.stdout.write(yellow(drift) + '\n');
|
|
240
275
|
}
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { green, dim, yellow } from './color.mjs';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
10
|
+
|
|
11
|
+
const NPM_PKG = 'dotmd-cli';
|
|
12
|
+
const DEFAULT_PLUGIN_ID = 'dotmd@dotmd';
|
|
13
|
+
|
|
14
|
+
// Parse an x.y.z prefix; returns [major, minor, patch] or null.
|
|
15
|
+
function parseVer(v) {
|
|
16
|
+
if (typeof v !== 'string') return null;
|
|
17
|
+
const m = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
18
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// -1 if a<b, 0 if equal, 1 if a>b, null if either is unparseable.
|
|
22
|
+
export function compareVersions(a, b) {
|
|
23
|
+
const pa = parseVer(a), pb = parseVer(b);
|
|
24
|
+
if (!pa || !pb) return null;
|
|
25
|
+
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read Claude Code's plugin install record to find the installed dotmd plugin's
|
|
30
|
+
// id + version. Network-free. `opts.home` is injectable for tests. Returns
|
|
31
|
+
// { id, version } or null when nothing is installed / the file is absent.
|
|
32
|
+
export function readInstalledPlugin(opts = {}) {
|
|
33
|
+
const home = opts.home || os.homedir();
|
|
34
|
+
const file = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
35
|
+
try {
|
|
36
|
+
const j = JSON.parse(readFileSync(file, 'utf8'));
|
|
37
|
+
const plugins = j.plugins || {};
|
|
38
|
+
const id = plugins[DEFAULT_PLUGIN_ID]
|
|
39
|
+
? DEFAULT_PLUGIN_ID
|
|
40
|
+
: Object.keys(plugins).find(k => /^dotmd@/.test(k));
|
|
41
|
+
if (!id) return null;
|
|
42
|
+
const entry = Array.isArray(plugins[id]) ? plugins[id][0] : plugins[id];
|
|
43
|
+
return { id, version: entry?.version ?? null };
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Decide which steps `dotmd update` should run. Pure — no side effects — so the
|
|
50
|
+
// orchestration is unit-testable. `opts` = { cliOnly, pluginOnly }; `ctx` =
|
|
51
|
+
// { plugin: {id,version}|null, hasClaude, hasNpm }.
|
|
52
|
+
export function planUpdate(opts, ctx) {
|
|
53
|
+
const steps = [];
|
|
54
|
+
if (!opts.pluginOnly) {
|
|
55
|
+
steps.push(ctx.hasNpm
|
|
56
|
+
? { kind: 'cli', cmd: ['npm', 'i', '-g', `${NPM_PKG}@latest`] }
|
|
57
|
+
: { kind: 'skip', reason: 'npm not found on PATH — skipping CLI update' });
|
|
58
|
+
}
|
|
59
|
+
if (!opts.cliOnly) {
|
|
60
|
+
if (!ctx.plugin) {
|
|
61
|
+
steps.push({ kind: 'skip', reason: 'dotmd plugin not installed — skipping plugin update' });
|
|
62
|
+
} else if (!ctx.hasClaude) {
|
|
63
|
+
steps.push({ kind: 'skip', reason: `claude CLI not found — run \`/plugin update ${ctx.plugin.id}\` from a session instead` });
|
|
64
|
+
} else {
|
|
65
|
+
steps.push({ kind: 'plugin', cmd: ['claude', 'plugin', 'update', ctx.plugin.id] });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return steps;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function which(bin) {
|
|
72
|
+
try {
|
|
73
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
74
|
+
return spawnSync(cmd, [bin], { encoding: 'utf8' }).status === 0;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function runUpdate(argv, _config) {
|
|
81
|
+
const check = argv.includes('--check');
|
|
82
|
+
const cliOnly = argv.includes('--cli-only');
|
|
83
|
+
const pluginOnly = argv.includes('--plugin-only');
|
|
84
|
+
const plugin = readInstalledPlugin();
|
|
85
|
+
|
|
86
|
+
if (check) {
|
|
87
|
+
process.stdout.write(`dotmd CLI: ${pkg.version}\n`);
|
|
88
|
+
if (plugin) {
|
|
89
|
+
const cmp = compareVersions(plugin.version, pkg.version);
|
|
90
|
+
const tag = cmp === 0 ? green('in sync')
|
|
91
|
+
: cmp === null ? dim('(unknown)')
|
|
92
|
+
: cmp < 0 ? yellow('behind — run `dotmd update`')
|
|
93
|
+
: yellow('ahead — CLI is behind');
|
|
94
|
+
process.stdout.write(`dotmd plugin: ${plugin.version ?? '?'} (${plugin.id}) ${tag}\n`);
|
|
95
|
+
} else {
|
|
96
|
+
process.stdout.write(dim('dotmd plugin: not installed\n'));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
102
|
+
let ran = false;
|
|
103
|
+
for (const s of steps) {
|
|
104
|
+
if (s.kind === 'skip') {
|
|
105
|
+
process.stdout.write(dim(`skip: ${s.reason}\n`));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(dim(`$ ${s.cmd.join(' ')}\n`));
|
|
109
|
+
const r = spawnSync(s.cmd[0], s.cmd.slice(1), { stdio: 'inherit' });
|
|
110
|
+
ran = true;
|
|
111
|
+
if (r.status !== 0) process.stdout.write(yellow(`(${s.cmd[0]} exited ${r.status ?? '?'})\n`));
|
|
112
|
+
}
|
|
113
|
+
if (ran) {
|
|
114
|
+
process.stdout.write(green('\n✓ restart your Claude Code session (or /reload-plugins) to apply.\n'));
|
|
115
|
+
}
|
|
116
|
+
}
|