dotmd-cli 0.54.0 → 0.56.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 +20 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +36 -0
- package/src/commands.mjs +1 -1
- package/src/hud.mjs +53 -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
|
|
@@ -1220,7 +1233,12 @@ async function main() {
|
|
|
1220
1233
|
restArgs.push(args[i]);
|
|
1221
1234
|
}
|
|
1222
1235
|
|
|
1223
|
-
|
|
1236
|
+
// Hook commands (`hud`, `guard`) fire in EVERY repo via the globally-enabled
|
|
1237
|
+
// plugin — `guard` runs on every Bash/Read/Edit. They must stay silent where
|
|
1238
|
+
// dotmd isn't used, so don't nag them about a missing config (they no-op
|
|
1239
|
+
// cleanly on their own). The warning is still useful for interactive commands.
|
|
1240
|
+
const HOOK_COMMANDS = new Set(['hud', 'guard']);
|
|
1241
|
+
if (!config.configFound && command !== 'init' && !HOOK_COMMANDS.has(command)) {
|
|
1224
1242
|
warn('No dotmd config found — using defaults. Run `dotmd init` to create one.');
|
|
1225
1243
|
}
|
|
1226
1244
|
|
|
@@ -1305,6 +1323,7 @@ async function main() {
|
|
|
1305
1323
|
// Lifecycle commands
|
|
1306
1324
|
if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
|
|
1307
1325
|
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config); return; }
|
|
1326
|
+
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config); return; }
|
|
1308
1327
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1309
1328
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1310
1329
|
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.56.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,6 +40,7 @@
|
|
|
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
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"
|
|
@@ -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).
|
|
@@ -198,17 +229,36 @@ const SUBAGENT_PRIMER = [
|
|
|
198
229
|
'git add/commit a prompt (they are session-local, often gitignored); hand-edit a `status:` field (use `dotmd set`).',
|
|
199
230
|
].join('\n');
|
|
200
231
|
|
|
232
|
+
// The plugin's SessionStart/SubagentStart hooks fire in EVERY repo (it's enabled
|
|
233
|
+
// globally), but the primer only helps where dotmd is actually used. Gate on a
|
|
234
|
+
// discovered config: `dotmd init` writes dotmd.config.mjs, so "has a config" is
|
|
235
|
+
// the zero-false-positive signal for "this is a dotmd repo." A bare docs/ dir is
|
|
236
|
+
// deliberately NOT enough — too many repos have one. In a non-dotmd repo the hook
|
|
237
|
+
// then contributes nothing to the session: no primer, no index build, no heal.
|
|
238
|
+
function isDotmdRepo(config) {
|
|
239
|
+
return Boolean(config?.configFound);
|
|
240
|
+
}
|
|
241
|
+
|
|
201
242
|
export function runHud(argv, config) {
|
|
202
243
|
const json = argv.includes('--json');
|
|
203
244
|
|
|
245
|
+
const drift = detectVersionDrift();
|
|
246
|
+
const dotmdRepo = isDotmdRepo(config);
|
|
247
|
+
|
|
204
248
|
// SubagentStart hook entry point — emit the compact primer and return. No
|
|
205
249
|
// index build, no journal read, no slash-command heal: a subagent doesn't
|
|
206
250
|
// need the operator-facing machinery, just the verbs and the guardrails.
|
|
207
251
|
if (argv.includes('--subagent')) {
|
|
252
|
+
if (!dotmdRepo) return; // silent in repos that don't use dotmd
|
|
208
253
|
process.stdout.write(dim(SUBAGENT_PRIMER) + '\n');
|
|
254
|
+
if (drift) process.stdout.write(yellow(drift) + '\n');
|
|
209
255
|
return;
|
|
210
256
|
}
|
|
211
257
|
|
|
258
|
+
// Non-dotmd repo, and not a programmatic --json caller → contribute nothing to
|
|
259
|
+
// the session. Skip the index build, slash-heal, primer, and drift line.
|
|
260
|
+
if (!dotmdRepo && !json) return;
|
|
261
|
+
|
|
212
262
|
const hud = buildHud(config);
|
|
213
263
|
|
|
214
264
|
// Self-heal stale slash-command files. Wrapped: a broken scaffolder must
|
|
@@ -222,7 +272,7 @@ export function runHud(argv, config) {
|
|
|
222
272
|
}
|
|
223
273
|
|
|
224
274
|
if (json) {
|
|
225
|
-
process.stdout.write(JSON.stringify(hud, null, 2) + '\n');
|
|
275
|
+
process.stdout.write(JSON.stringify({ ...hud, drift: drift ?? null }, null, 2) + '\n');
|
|
226
276
|
return;
|
|
227
277
|
}
|
|
228
278
|
|
|
@@ -237,4 +287,5 @@ export function runHud(argv, config) {
|
|
|
237
287
|
// `dotmd hud --json` for programmatic callers. The hook's job is purely to
|
|
238
288
|
// teach the verbs, never to report status.
|
|
239
289
|
process.stdout.write(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] → oldest pending prompt)') + '\n');
|
|
290
|
+
if (drift) process.stdout.write(yellow(drift) + '\n');
|
|
240
291
|
}
|
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
|
+
}
|