dotmd-cli 0.69.0 → 0.70.1
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/README.md +144 -964
- package/bin/dotmd.mjs +251 -202
- package/dotmd.config.example.mjs +5 -8
- package/package.json +6 -10
- package/src/agent-context.mjs +132 -0
- package/src/atomic-mutation.mjs +1505 -0
- package/src/baton.mjs +109 -114
- package/src/bulk-tag.mjs +7 -7
- package/src/check-collapse.mjs +2 -2
- package/src/commands.mjs +326 -12
- package/src/completions.mjs +38 -98
- package/src/config.mjs +18 -3
- package/src/diff.mjs +7 -3
- package/src/doctor.mjs +25 -15
- package/src/export.mjs +154 -25
- package/src/fix-refs.mjs +2 -0
- package/src/frontmatter-fix.mjs +9 -7
- package/src/frontmatter.mjs +3 -2
- package/src/git.mjs +722 -14
- package/src/graph.mjs +53 -25
- package/src/guard.mjs +163 -60
- package/src/hud.mjs +65 -76
- package/src/index-file.mjs +28 -16
- package/src/index.mjs +21 -13
- package/src/init.mjs +1 -1
- package/src/journal.mjs +145 -12
- package/src/lifecycle.mjs +596 -294
- package/src/lint.mjs +117 -56
- package/src/managed-path.mjs +192 -0
- package/src/migrate-prompts.mjs +2 -0
- package/src/migrate-template.mjs +2 -0
- package/src/migrate.mjs +7 -1
- package/src/new.mjs +135 -54
- package/src/output-identity.mjs +106 -0
- package/src/pickup-card.mjs +24 -10
- package/src/pickup.mjs +457 -0
- package/src/prompts.mjs +134 -75
- package/src/query.mjs +22 -10
- package/src/reference-planner.mjs +292 -0
- package/src/rename.mjs +65 -73
- package/src/render.mjs +24 -11
- package/src/runlist.mjs +109 -71
- package/src/section.mjs +2 -1
- package/src/ship.mjs +39 -20
- package/src/stats.mjs +1 -1
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +11 -26
- package/src/summary.mjs +14 -3
- package/src/update.mjs +38 -10
- package/src/use.mjs +4 -1
- package/src/util.mjs +1 -0
- package/src/validate.mjs +53 -17
- package/src/watch.mjs +6 -1
- package/src/notion.mjs +0 -528
package/src/statuses.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { collectDocFiles } from './index.mjs';
|
|
|
14
14
|
import { asString, toRepoPath, die } from './util.mjs';
|
|
15
15
|
import { bold, dim, green, yellow } from './color.mjs';
|
|
16
16
|
import { isInteractive, promptText } from './prompt.mjs';
|
|
17
|
+
import { resolveStatusMetadata } from './status-metadata.mjs';
|
|
17
18
|
import {
|
|
18
19
|
parseStatusesBlock,
|
|
19
20
|
renderEntryLine,
|
|
@@ -95,34 +96,18 @@ function runListStatuses(args, config) {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function describeTypeStatuses(typeName, config) {
|
|
98
|
-
const typeDef = config.raw?.types?.[typeName];
|
|
99
|
-
if (!typeDef) return {};
|
|
100
99
|
const result = {};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const statusList = Array.isArray(typeDef.statuses) ? typeDef.statuses : Object.keys(typeDef.statuses ?? {});
|
|
104
|
-
const ctx = typeDef.context ?? {};
|
|
105
|
-
const ctxByStatus = {};
|
|
106
|
-
for (const [bucket, names] of Object.entries(ctx)) {
|
|
107
|
-
for (const n of names) ctxByStatus[n] = bucket;
|
|
108
|
-
}
|
|
109
|
-
const stale = typeDef.staleDays ?? {};
|
|
110
|
-
const lc = config.lifecycle;
|
|
111
|
-
const moduleReq = config.moduleRequiredStatuses;
|
|
112
|
-
|
|
113
|
-
for (const name of statusList) {
|
|
114
|
-
const skipStale = lc.skipStaleFor.has(name);
|
|
115
|
-
const skipWarnings = lc.skipWarningsFor.has(name);
|
|
116
|
-
const quiet = skipStale && skipWarnings;
|
|
100
|
+
for (const metadata of resolveStatusMetadata(config).byType[typeName] ?? []) {
|
|
101
|
+
const { name } = metadata;
|
|
117
102
|
result[name] = {
|
|
118
|
-
context:
|
|
119
|
-
staleDays:
|
|
120
|
-
requiresModule:
|
|
121
|
-
terminal:
|
|
122
|
-
archive:
|
|
123
|
-
skipStale,
|
|
124
|
-
skipWarnings,
|
|
125
|
-
quiet,
|
|
103
|
+
context: metadata.context,
|
|
104
|
+
staleDays: metadata.staleDays,
|
|
105
|
+
requiresModule: metadata.requiresModule,
|
|
106
|
+
terminal: metadata.terminal,
|
|
107
|
+
archive: metadata.archive,
|
|
108
|
+
skipStale: metadata.skipStale,
|
|
109
|
+
skipWarnings: metadata.skipWarnings,
|
|
110
|
+
quiet: metadata.quiet,
|
|
126
111
|
};
|
|
127
112
|
}
|
|
128
113
|
return result;
|
package/src/summary.mjs
CHANGED
|
@@ -37,10 +37,13 @@ export function runSummary(argv, config) {
|
|
|
37
37
|
const opts = {};
|
|
38
38
|
if (model) opts.model = model;
|
|
39
39
|
if (maxTokens) opts.maxTokens = maxTokens;
|
|
40
|
+
const previewSkipped = Boolean(config._execution?.suppressSideEffects);
|
|
40
41
|
|
|
41
42
|
let summary;
|
|
42
43
|
try {
|
|
43
|
-
summary =
|
|
44
|
+
summary = previewSkipped
|
|
45
|
+
? null
|
|
46
|
+
: config.hooks.summarizeDoc
|
|
44
47
|
? config.hooks.summarizeDoc(body, meta)
|
|
45
48
|
: summarizeDocBody(body, meta, opts);
|
|
46
49
|
} catch (err) {
|
|
@@ -49,13 +52,21 @@ export function runSummary(argv, config) {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
if (json) {
|
|
52
|
-
process.stdout.write(JSON.stringify({
|
|
55
|
+
process.stdout.write(JSON.stringify({
|
|
56
|
+
path: repoPath,
|
|
57
|
+
title,
|
|
58
|
+
status,
|
|
59
|
+
summary: summary ?? null,
|
|
60
|
+
summaryStatus: previewSkipped ? 'skipped-preview' : summary ? 'generated' : 'unavailable',
|
|
61
|
+
}, null, 2) + '\n');
|
|
53
62
|
return;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
process.stdout.write(`${bold(title)} ${dim(`(${status})`)}\n`);
|
|
57
66
|
process.stdout.write(`${dim(repoPath)}\n\n`);
|
|
58
|
-
if (
|
|
67
|
+
if (previewSkipped) {
|
|
68
|
+
process.stdout.write(dim('[preview] Summary generation skipped; models and custom summarizeDoc hooks are not invoked.') + '\n');
|
|
69
|
+
} else if (summary) {
|
|
59
70
|
process.stdout.write(`${summary}\n`);
|
|
60
71
|
} else {
|
|
61
72
|
process.stdout.write(dim('Summary unavailable (model call failed or uv not installed).') + '\n');
|
package/src/update.mjs
CHANGED
|
@@ -29,23 +29,31 @@ export function compareVersions(a, b) {
|
|
|
29
29
|
// Read Claude Code's plugin install record to find the installed dotmd plugin's
|
|
30
30
|
// id + version. Network-free. `opts.home` is injectable for tests. Returns
|
|
31
31
|
// { id, version } or null when nothing is installed / the file is absent.
|
|
32
|
-
export function
|
|
32
|
+
export function readInstalledPluginRecords(opts = {}) {
|
|
33
33
|
const home = opts.home || os.homedir();
|
|
34
34
|
const file = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
35
35
|
try {
|
|
36
36
|
const j = JSON.parse(readFileSync(file, 'utf8'));
|
|
37
37
|
const plugins = j.plugins || {};
|
|
38
|
-
const id =
|
|
39
|
-
?
|
|
40
|
-
:
|
|
38
|
+
const id = opts.id
|
|
39
|
+
? (plugins[opts.id] ? opts.id : null)
|
|
40
|
+
: plugins[DEFAULT_PLUGIN_ID]
|
|
41
|
+
? DEFAULT_PLUGIN_ID
|
|
42
|
+
: Object.keys(plugins).find(k => /^dotmd@/.test(k));
|
|
41
43
|
if (!id) return null;
|
|
42
|
-
const
|
|
43
|
-
return { id,
|
|
44
|
+
const entries = Array.isArray(plugins[id]) ? plugins[id] : [plugins[id]];
|
|
45
|
+
return { id, entries: entries.filter(Boolean) };
|
|
44
46
|
} catch {
|
|
45
47
|
return null;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export function readInstalledPlugin(opts = {}) {
|
|
52
|
+
const records = readInstalledPluginRecords(opts);
|
|
53
|
+
if (!records) return null;
|
|
54
|
+
return { id: records.id, version: records.entries[0]?.version ?? null };
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
// Decide which steps `dotmd update` should run. Pure — no side effects — so the
|
|
50
58
|
// orchestration is unit-testable. `opts` = { cliOnly, pluginOnly }; `ctx` =
|
|
51
59
|
// { plugin: {id,version}|null, hasClaude, hasNpm }.
|
|
@@ -77,7 +85,11 @@ function which(bin) {
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
function executableName(bin) {
|
|
89
|
+
return process.platform === 'win32' && !bin.endsWith('.cmd') ? `${bin}.cmd` : bin;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function runUpdate(argv, _config, opts = {}) {
|
|
81
93
|
const check = argv.includes('--check');
|
|
82
94
|
const cliOnly = argv.includes('--cli-only');
|
|
83
95
|
const pluginOnly = argv.includes('--plugin-only');
|
|
@@ -99,18 +111,34 @@ export function runUpdate(argv, _config) {
|
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
114
|
+
if (opts.dryRun) {
|
|
115
|
+
for (const step of steps) {
|
|
116
|
+
if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
|
|
117
|
+
else process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}\n`));
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
102
121
|
let ran = false;
|
|
122
|
+
let failed = false;
|
|
103
123
|
for (const s of steps) {
|
|
104
124
|
if (s.kind === 'skip') {
|
|
105
125
|
process.stdout.write(dim(`skip: ${s.reason}\n`));
|
|
106
126
|
continue;
|
|
107
127
|
}
|
|
108
128
|
process.stdout.write(dim(`$ ${s.cmd.join(' ')}\n`));
|
|
109
|
-
const r = spawnSync(s.cmd[0], s.cmd.slice(1), {
|
|
129
|
+
const r = spawnSync(executableName(s.cmd[0]), s.cmd.slice(1), {
|
|
130
|
+
stdio: 'inherit',
|
|
131
|
+
shell: process.platform === 'win32',
|
|
132
|
+
});
|
|
110
133
|
ran = true;
|
|
111
|
-
if (r.status !== 0)
|
|
134
|
+
if (r.status !== 0) {
|
|
135
|
+
failed = true;
|
|
136
|
+
process.stdout.write(yellow(`(${s.cmd[0]} exited ${r.status ?? '?'})\n`));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
112
139
|
}
|
|
113
|
-
if (ran) {
|
|
140
|
+
if (ran && !failed) {
|
|
114
141
|
process.stdout.write(green('\n✓ restart your Claude Code session (or /reload-plugins) to apply.\n'));
|
|
115
142
|
}
|
|
143
|
+
if (failed) process.exitCode = 1;
|
|
116
144
|
}
|
package/src/use.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
4
|
import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
|
|
4
5
|
import { consumePrompt, pendingPromptsOldestFirst, resolvePromptInput } from './prompts.mjs';
|
|
@@ -26,7 +27,9 @@ export async function runUse(argv, config, opts = {}) {
|
|
|
26
27
|
// Exact path first, then prompt slugs (they keep precedence on a slug
|
|
27
28
|
// collision — consuming a prompt is the more common intent), then the
|
|
28
29
|
// shared resolver for plan/doc slugs, which dies with did-you-mean on miss.
|
|
30
|
+
const cwdPath = path.resolve(process.cwd(), positional);
|
|
29
31
|
const filePath = resolveDocPath(positional, config)
|
|
32
|
+
?? (existsSync(cwdPath) ? cwdPath : null)
|
|
30
33
|
?? resolvePromptInput(positional, config, { dieOnMiss: false })
|
|
31
34
|
?? resolveDocArg(positional, config);
|
|
32
35
|
|
package/src/util.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { dim } from './color.mjs';
|
|
|
6
6
|
// Stable identifier for the current shell/agent session. Used for journal
|
|
7
7
|
// attribution and hint de-duplication — not for any plan locking.
|
|
8
8
|
export function currentSessionId() {
|
|
9
|
+
if (process.env.DOTMD_SESSION_ID) return process.env.DOTMD_SESSION_ID;
|
|
9
10
|
if (process.env.CLAUDE_CODE_SESSION_ID) return process.env.CLAUDE_CODE_SESSION_ID;
|
|
10
11
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
11
12
|
if (process.env.TERM_SESSION_ID) return `term:${process.env.TERM_SESSION_ID}`;
|
package/src/validate.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { asString, resolveRefPath, suggestCandidates } from './util.mjs';
|
|
3
|
-
import { getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
|
|
3
|
+
import { getGitLastModified, getGitLastModifiedBatch, getGitLastSubstantiveModifiedBatch } from './git.mjs';
|
|
4
4
|
import { toRepoPath } from './util.mjs';
|
|
5
5
|
|
|
6
6
|
const NOW = new Date();
|
|
@@ -156,17 +156,27 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
156
156
|
if (!config.lifecycle.skipWarningsFor.has(doc.status)) {
|
|
157
157
|
for (const { singular, plural } of [{ singular: 'module', plural: 'modules' }, { singular: 'surface', plural: 'surfaces' }]) {
|
|
158
158
|
const singularValue = frontmatter[singular];
|
|
159
|
-
if (!
|
|
160
|
-
const
|
|
159
|
+
if (!Object.prototype.hasOwnProperty.call(frontmatter, singular)) continue;
|
|
160
|
+
const rawPluralValue = frontmatter[plural];
|
|
161
|
+
const pluralValue = Array.isArray(rawPluralValue)
|
|
162
|
+
? rawPluralValue
|
|
163
|
+
: rawPluralValue === undefined ? [] : [rawPluralValue];
|
|
164
|
+
const singularValues = Array.isArray(singularValue) ? singularValue : [singularValue];
|
|
161
165
|
const merged = [];
|
|
162
|
-
for (const v of [
|
|
166
|
+
for (const v of [...singularValues, ...pluralValue]) {
|
|
163
167
|
if (typeof v === 'string' && v && !merged.includes(v)) merged.push(v);
|
|
164
168
|
}
|
|
165
|
-
const target = `${plural}: [${merged.map(v =>
|
|
169
|
+
const target = `${plural}: [${merged.map(v => JSON.stringify(v)).join(', ')}]`;
|
|
170
|
+
const autoFixableString = value => typeof value === 'string' && !/(^|\s)#/.test(value);
|
|
171
|
+
const autoFixable = singularValues.every(autoFixableString)
|
|
172
|
+
&& pluralValue.every(autoFixableString);
|
|
173
|
+
const guidance = autoFixable
|
|
174
|
+
? `use \`${target}\`. Run \`dotmd lint --fix\` to migrate.`
|
|
175
|
+
: `use a \`${plural}:\` YAML list. Remove the deprecated \`${singular}:\` block manually and preserve all of its values.`;
|
|
166
176
|
doc.warnings.push({
|
|
167
177
|
path: doc.path,
|
|
168
178
|
level: 'warning',
|
|
169
|
-
message: `\`${singular}:\` (singular) is deprecated —
|
|
179
|
+
message: `\`${singular}:\` (singular) is deprecated — ${guidance}`,
|
|
170
180
|
});
|
|
171
181
|
}
|
|
172
182
|
}
|
|
@@ -519,16 +529,35 @@ export function checkRoadmapHubExecutionMode(docs, config) {
|
|
|
519
529
|
return warnings;
|
|
520
530
|
}
|
|
521
531
|
|
|
522
|
-
export function checkGitStaleness(docs, config) {
|
|
532
|
+
export function checkGitStaleness(docs, config, options = {}) {
|
|
523
533
|
const warnings = [];
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
534
|
+
const eligibleDocs = docs.filter(doc => !config.lifecycle.skipStaleFor.has(doc.status) && doc.updated);
|
|
535
|
+
const pathspecs = (config.docsRoots || [config.docsRoot])
|
|
536
|
+
.map(root => toRepoPath(root, config.repoRoot) || '.');
|
|
537
|
+
const gitMetadata = getGitLastModifiedBatch(
|
|
538
|
+
config.repoRoot,
|
|
539
|
+
eligibleDocs.map(doc => doc.path),
|
|
540
|
+
{ pathspecs, ...options },
|
|
541
|
+
);
|
|
542
|
+
const driftCandidates = eligibleDocs.filter(doc => {
|
|
543
|
+
const gitDate = gitMetadata.dates.get(doc.path) ?? null;
|
|
544
|
+
return Boolean(gitDate && gitDate.slice(0, 10) > doc.updated.slice(0, 10));
|
|
545
|
+
});
|
|
546
|
+
const candidatePaths = driftCandidates.map(doc => doc.path);
|
|
547
|
+
const candidateMetadata = {
|
|
548
|
+
dates: new Map(candidatePaths.filter(p => gitMetadata.dates.has(p)).map(p => [p, gitMetadata.dates.get(p)])),
|
|
549
|
+
commits: new Map(candidatePaths.filter(p => gitMetadata.commits?.has(p)).map(p => [p, gitMetadata.commits.get(p)])),
|
|
550
|
+
history: new Map(candidatePaths.filter(p => gitMetadata.history?.has(p)).map(p => [p, gitMetadata.history.get(p)])),
|
|
551
|
+
complete: gitMetadata.complete,
|
|
552
|
+
reason: gitMetadata.reason,
|
|
553
|
+
};
|
|
554
|
+
const substantiveMetadata = candidatePaths.length > 0
|
|
555
|
+
? getGitLastSubstantiveModifiedBatch(config.repoRoot, candidatePaths, candidateMetadata, options)
|
|
556
|
+
: candidateMetadata;
|
|
528
557
|
|
|
529
|
-
|
|
558
|
+
for (const doc of driftCandidates) {
|
|
559
|
+
const gitDate = substantiveMetadata.dates.get(doc.path) ?? null;
|
|
530
560
|
if (!gitDate) continue;
|
|
531
|
-
|
|
532
561
|
const gitDay = gitDate.slice(0, 10);
|
|
533
562
|
const fmDay = doc.updated.slice(0, 10);
|
|
534
563
|
|
|
@@ -540,6 +569,13 @@ export function checkGitStaleness(docs, config) {
|
|
|
540
569
|
});
|
|
541
570
|
}
|
|
542
571
|
}
|
|
572
|
+
if (!substantiveMetadata.complete) {
|
|
573
|
+
warnings.push({
|
|
574
|
+
path: toRepoPath(config.docsRoot, config.repoRoot) || '.',
|
|
575
|
+
level: 'warning',
|
|
576
|
+
message: `Git metadata is incomplete (${substantiveMetadata.reason}); staleness checks used known dates only.`,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
543
579
|
return warnings;
|
|
544
580
|
}
|
|
545
581
|
|
|
@@ -582,9 +618,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
|
|
|
582
618
|
|
|
583
619
|
// 4. Heading drift: case + name variants
|
|
584
620
|
const headingDrift = [
|
|
585
|
-
{ wrong: /^##\s+Open questions\s*$/m, right: '## Open Questions' },
|
|
586
621
|
{ wrong: /^##\s+(Non-goals|Out of scope|Out of Scope|out of scope)\s*$/m, right: '## Non-Goals' },
|
|
587
|
-
{ wrong: /^##\s+open questions\s*$/m, right: '## Open Questions' },
|
|
588
622
|
];
|
|
589
623
|
for (const { wrong, right } of headingDrift) {
|
|
590
624
|
const m = body.match(wrong);
|
|
@@ -654,8 +688,10 @@ export function computeDaysSinceUpdate(updated) {
|
|
|
654
688
|
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
655
689
|
}
|
|
656
690
|
|
|
657
|
-
export function computeIsStale(status, updated, config) {
|
|
658
|
-
const
|
|
691
|
+
export function computeIsStale(status, updated, config, type = null) {
|
|
692
|
+
const typeStaleDays = type ? config.raw?.types?.[type]?.staleDays : null;
|
|
693
|
+
const hasTypeDays = Object.prototype.hasOwnProperty.call(typeStaleDays ?? {}, status);
|
|
694
|
+
const staleAfterDays = hasTypeDays ? typeStaleDays[status] : (config.staleDaysByStatus[status] ?? null);
|
|
659
695
|
if (staleAfterDays == null) return false;
|
|
660
696
|
|
|
661
697
|
const daysSinceUpdate = computeDaysSinceUpdate(updated);
|
package/src/watch.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { dim } from './color.mjs';
|
|
5
6
|
|
|
7
|
+
export function watchCliPath(moduleUrl = import.meta.url) {
|
|
8
|
+
return path.resolve(path.dirname(fileURLToPath(moduleUrl)), '..', 'bin', 'dotmd.mjs');
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export function runWatch(argv, config) {
|
|
7
12
|
const subCommand = argv.length > 0 ? argv : ['list'];
|
|
8
|
-
const cliPath =
|
|
13
|
+
const cliPath = watchCliPath();
|
|
9
14
|
|
|
10
15
|
let lastRun = 0;
|
|
11
16
|
const DEBOUNCE = 300;
|