runlist 0.99.1 → 0.100.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/GETTING_STARTED.md +249 -0
- package/README.md +433 -51
- package/bin/runlist.mjs +404 -53
- package/package.json +3 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/runlist-codex/bin/consume-prompt.mjs +45 -0
- package/plugins/runlist-codex/bin/runlist-hook +29 -11
- package/plugins/runlist-codex/bin/session-start.mjs +16 -0
- package/plugins/runlist-codex/bin/subagent-start.mjs +16 -0
- package/plugins/runlist-codex/hooks/hooks.json +11 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +27 -11
- package/plugins/runlist-codex/skills/runlist-setup/SKILL.md +109 -0
- package/runlist.config.example.mjs +6 -0
- package/src/agent-context.mjs +8 -2
- package/src/agent-hosts.mjs +305 -0
- package/src/app-create.mjs +11 -1
- package/src/atomic-mutation.mjs +140 -35
- package/src/baton.mjs +127 -33
- package/src/codex-integration.mjs +57 -7
- package/src/commands.mjs +92 -38
- package/src/config-command.mjs +115 -0
- package/src/config-setting.mjs +163 -0
- package/src/config.mjs +57 -12
- package/src/doctor-hosts.mjs +192 -0
- package/src/doctor.mjs +16 -6
- package/src/export.mjs +11 -0
- package/src/git.mjs +58 -2
- package/src/guard.mjs +41 -13
- package/src/handoff-record.mjs +290 -30
- package/src/handoffs-help.mjs +143 -0
- package/src/handoffs-mode.mjs +486 -0
- package/src/handoffs-scope.mjs +98 -0
- package/src/help.mjs +72 -0
- package/src/host-integration.mjs +7 -1
- package/src/hud.mjs +174 -29
- package/src/index.mjs +11 -4
- package/src/init.mjs +255 -160
- package/src/install.mjs +35 -18
- package/src/installation.mjs +110 -27
- package/src/journal.mjs +2 -2
- package/src/lifecycle.mjs +22 -11
- package/src/managed-path.mjs +68 -4
- package/src/migrate.mjs +7 -0
- package/src/naming.mjs +26 -0
- package/src/new.mjs +202 -91
- package/src/pickup-card.mjs +1 -1
- package/src/plan-changes.mjs +49 -0
- package/src/prompt-handoff.mjs +16 -0
- package/src/prompts.mjs +101 -27
- package/src/rename.mjs +13 -0
- package/src/setup-carry.mjs +291 -0
- package/src/setup-waiting.mjs +20 -0
- package/src/setup-worker.mjs +257 -0
- package/src/setup.mjs +404 -0
- package/src/statuses.mjs +5 -2
- package/src/update.mjs +239 -79
- package/src/use.mjs +4 -2
- package/src/validate.mjs +7 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Codex's default personal marketplace is discovered without a CLI registration.
|
|
2
2
|
// The npm package carries this plugin so installation does not need a Git checkout.
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -11,6 +12,35 @@ const SOURCE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..',
|
|
|
11
12
|
const NAME = 'runlist-codex';
|
|
12
13
|
const MARKER = '.runlist-generated.json';
|
|
13
14
|
|
|
15
|
+
// Compare the whole distributed tree, including hook scripts and manifests.
|
|
16
|
+
// The ownership marker is metadata, never an asset or proof of source parity.
|
|
17
|
+
export function codexAssetFingerprint(root) {
|
|
18
|
+
const hash = createHash('sha256');
|
|
19
|
+
let files = 0, bytes = 0;
|
|
20
|
+
function visit(dir, prefix = '', depth = 0) {
|
|
21
|
+
if (depth > 32) throw new Error('asset depth limit');
|
|
22
|
+
for (const name of readdirSync(dir).sort()) {
|
|
23
|
+
if (!prefix && name === MARKER) continue;
|
|
24
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
25
|
+
const file = path.join(dir, name);
|
|
26
|
+
const stat = lstatSync(file);
|
|
27
|
+
if (++files > 2000 || (bytes += stat.size) > 64 * 1024 * 1024) throw new Error('asset inventory limit');
|
|
28
|
+
const kind = stat.isSymbolicLink() ? 'link' : stat.isDirectory() ? 'dir' : stat.isFile() ? 'file' : 'other';
|
|
29
|
+
hash.update(JSON.stringify([relative, kind, kind === 'file' ? stat.mode & 0o111 : null]) + '\n');
|
|
30
|
+
if (kind === 'dir') visit(file, relative, depth + 1);
|
|
31
|
+
else if (kind === 'link') hash.update(JSON.stringify(readlinkSync(file)) + '\n');
|
|
32
|
+
else if (kind === 'file') hash.update(readFileSync(file));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try { visit(root); return hash.digest('hex'); } catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function codexAssetParity(installedPath, sourcePath = SOURCE) {
|
|
39
|
+
const expected = codexAssetFingerprint(sourcePath);
|
|
40
|
+
const actual = codexAssetFingerprint(installedPath);
|
|
41
|
+
return { state: !expected || !actual ? 'unknown' : expected === actual ? 'match' : 'mismatch', expected, actual };
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
export function codexPaths(homedir = os.homedir()) {
|
|
15
45
|
const root = path.join(homedir, '.agents', 'plugins');
|
|
16
46
|
// The implicit personal marketplace file lives under ~/.agents/plugins,
|
|
@@ -32,24 +62,44 @@ export function codexStatus({ version, homedir = os.homedir() } = {}) {
|
|
|
32
62
|
let installed = null;
|
|
33
63
|
try { installed = JSON.parse(readFileSync(path.join(paths.plugin, MARKER), 'utf8')).version; } catch { /* absent or foreign */ }
|
|
34
64
|
const exists = existsSync(paths.plugin);
|
|
35
|
-
|
|
65
|
+
const sourceParity = installed ? codexAssetParity(paths.plugin) : { state: 'unknown' };
|
|
66
|
+
let marketplaceState = existsSync(paths.marketplace) ? 'unreadable' : 'missing';
|
|
67
|
+
let marketplaceRegistered = false, foreignRegistration = false;
|
|
68
|
+
try {
|
|
69
|
+
const marketplace = marketplaceAt(paths.marketplace);
|
|
70
|
+
const entry = marketplace.plugins.find(item => item?.name === NAME);
|
|
71
|
+
marketplaceState = existsSync(paths.marketplace) ? 'readable' : 'missing';
|
|
72
|
+
marketplaceRegistered = entry?.source?.source === 'local' && entry.source.path === `./plugins/${NAME}`;
|
|
73
|
+
foreignRegistration = Boolean(entry && !marketplaceRegistered);
|
|
74
|
+
} catch { /* An unreadable registration cannot establish a managed source. */ }
|
|
75
|
+
return { ...paths, exists, foreign: (exists && !installed) || foreignRegistration, version: installed, sourceParity,
|
|
76
|
+
marketplaceState, marketplaceRegistered,
|
|
77
|
+
stale: Boolean(installed && ((version && installed !== version) || sourceParity.state !== 'match' || !marketplaceRegistered)) };
|
|
36
78
|
}
|
|
37
79
|
|
|
38
80
|
// The marketplace source is not the plugin Codex loads. Ask the host about its
|
|
39
81
|
// installed copy rather than treating our generated source marker as proof.
|
|
40
|
-
export function installedCodexStatus({ homedir = os.homedir(), run = spawnSync } = {}) {
|
|
82
|
+
export function installedCodexStatus({ homedir = os.homedir(), run = spawnSync, env = process.env, binary, offline = false } = {}) {
|
|
83
|
+
if (offline) return { ok: false, state: 'unknown', loaded: 'unknown', reason: 'offline inventory cannot attest the host installed copy or loaded session' };
|
|
41
84
|
try {
|
|
42
85
|
const marketplace = marketplaceAt(codexPaths(homedir).marketplace).name;
|
|
43
86
|
const pluginId = `${NAME}@${marketplace}`;
|
|
44
|
-
const result = run(executableName('codex'), ['plugin', 'list', '--marketplace', marketplace, '--json'], {
|
|
45
|
-
encoding: 'utf8', shell: process.platform === 'win32', timeout: 30_000,
|
|
87
|
+
const result = run(binary || executableName('codex'), ['plugin', 'list', '--marketplace', marketplace, '--json'], {
|
|
88
|
+
encoding: 'utf8', shell: process.platform === 'win32' && (!binary || /\.(?:cmd|bat)$/i.test(binary)), timeout: 30_000, env: { ...env, HOME: homedir, USERPROFILE: homedir },
|
|
46
89
|
});
|
|
47
90
|
if (result.status !== 0) return { ok: false, reason: result.error?.message ?? `codex plugin list exited ${result.status ?? '?'}` };
|
|
48
91
|
const installed = JSON.parse(result.stdout).installed;
|
|
49
92
|
if (!Array.isArray(installed)) return { ok: false, reason: 'codex plugin list returned no installed plugin records' };
|
|
50
93
|
const plugin = installed.find(entry => entry.pluginId === pluginId && entry.installed === true);
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
if (!plugin?.version) return { ok: false, reason: `${pluginId} is not installed in Codex` };
|
|
95
|
+
// Host versions do not establish byte parity. Inspect a host-provided path
|
|
96
|
+
// when this CLI supports one, otherwise retain the explicit unknown state.
|
|
97
|
+
const installedPath = plugin.installPath ?? plugin.installedPath ?? plugin.pluginPath ?? plugin.path;
|
|
98
|
+
const sourceParity = typeof installedPath === 'string' && path.isAbsolute(installedPath)
|
|
99
|
+
? codexAssetParity(installedPath, codexPaths(homedir).plugin) : { state: 'unknown' };
|
|
100
|
+
return { ok: sourceParity.state !== 'mismatch', version: plugin.version, pluginId, sourceParity, loaded: 'unknown',
|
|
101
|
+
...(sourceParity.state === 'mismatch' ? { reason: 'installed Codex plugin assets differ from the managed source' } : {}),
|
|
102
|
+
...(typeof installedPath === 'string' && path.isAbsolute(installedPath) ? { installedPath } : {}) };
|
|
53
103
|
} catch (err) {
|
|
54
104
|
return { ok: false, reason: `cannot verify installed Codex plugin: ${err.message}` };
|
|
55
105
|
}
|
package/src/commands.mjs
CHANGED
|
@@ -7,8 +7,9 @@ const flag = (...names) => Object.freeze({ names: Object.freeze(names), arity: 0
|
|
|
7
7
|
const value = (...names) => Object.freeze({ names: Object.freeze(names), arity: 1 });
|
|
8
8
|
const optionalValue = (...names) => Object.freeze({ names: Object.freeze(names), arity: 'optional' });
|
|
9
9
|
const positionals = (min = 0, max = min) => Object.freeze({ min, max });
|
|
10
|
-
const form = (syntax, { subcommands = [], args = positionals(), options = [], passthrough = false, dashPositionalsAfter = Infinity } = {}) => Object.freeze({
|
|
10
|
+
const form = (syntax, { subcommands = [], args = positionals(), options = [], passthrough = false, dashPositionalsAfter = Infinity, planning } = {}) => Object.freeze({
|
|
11
11
|
syntax,
|
|
12
|
+
planning,
|
|
12
13
|
subcommands: Object.freeze(subcommands),
|
|
13
14
|
positionals: args,
|
|
14
15
|
options: Object.freeze(options),
|
|
@@ -47,12 +48,29 @@ const command = (name, policy, group, forms, extra = {}) => Object.freeze({
|
|
|
47
48
|
forms: Object.freeze(forms),
|
|
48
49
|
aliases: Object.freeze(extra.aliases ?? []),
|
|
49
50
|
visibility: extra.visibility ?? 'public',
|
|
51
|
+
planning: extra.planning ?? null,
|
|
50
52
|
});
|
|
51
53
|
|
|
54
|
+
// `planning` marks a command (or one form of it) that only means something
|
|
55
|
+
// with planning on. In a handoffs-only project (src/handoffs-mode.mjs) the
|
|
56
|
+
// dispatcher stops it before it runs: a 'view' prints the planning-off line
|
|
57
|
+
// and succeeds, an 'action' refuses. A new plan or doc command opts in here;
|
|
58
|
+
// test/handoffs-planning-gate.test.mjs walks this registry.
|
|
59
|
+
const VIEW = Object.freeze({ planning: 'view' });
|
|
60
|
+
const ACTION = Object.freeze({ planning: 'action' });
|
|
61
|
+
|
|
52
62
|
const definitions = [
|
|
53
63
|
command('help', none, 'setup', [form('[topic]', { args: positionals(0, 1) })]),
|
|
54
64
|
command('completions', none, 'setup', [form('<bash|zsh>', { args: positionals(1, 1) })]),
|
|
55
|
-
command('init', mutates('repository setup paths and config files'), 'setup', [form('', { options: [flag('--force')] })]),
|
|
65
|
+
command('init', mutates('repository setup paths and config files; ignore rules only by explicit opt-in'), 'setup', [form('', { options: [flag('--force'), flag('--gitignore'), value('--prompt-tracking')] })], ACTION),
|
|
66
|
+
command('setup', mutates('explicit project onboarding choices; noninteractive preview unless --yes; host installation only when selected'), 'setup', [form('', {
|
|
67
|
+
options: [flag('--yes'), flag('--no-carry-prompts'), value('--docs-root'), value('--prompt-tracking'), flag('--gitignore'), value('--host'), value('--first-plan'), value('--current-state'), value('--next-step'), value('--adopt'), flag('--json')],
|
|
68
|
+
})]),
|
|
69
|
+
command('config', mutates('validated promptTracking export only; show/get are read-only'), 'setup', [
|
|
70
|
+
form('show', { subcommands: ['show'], options: [flag('--json')] }),
|
|
71
|
+
form('get <key>', { subcommands: ['get'], args: positionals(1, 1), options: [flag('--json')] }),
|
|
72
|
+
form('set <key> <value>', { subcommands: ['set'], args: positionals(2, 2), options: [flag('--json')] }),
|
|
73
|
+
]),
|
|
56
74
|
command('watch', mutates('proxy: child command policy applies'), 'setup', [form('[command...]', { args: positionals(0, Infinity), passthrough: true })]),
|
|
57
75
|
command('desktop', mutates('explicit desktop installation; otherwise launch only'), 'setup', [
|
|
58
76
|
form('[--app <application>]', {options:[value('--app')]}),
|
|
@@ -62,34 +80,34 @@ const definitions = [
|
|
|
62
80
|
|
|
63
81
|
command('list', none, 'read', [form('', { options: [flag('--json'), flag('--verbose')] })]),
|
|
64
82
|
command('json', none, 'read', [form('')]),
|
|
65
|
-
command('coverage', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
83
|
+
command('coverage', none, 'read', [form('', { options: [flag('--json')] })], VIEW),
|
|
66
84
|
command('yardstick', mutates('explicit owner assessments and existing archive workflow; show is read-only'), 'mutate', [
|
|
67
|
-
form('show [plan]', { subcommands: ['show'], args: positionals(0, 1), options: [flag('--json')] }),
|
|
85
|
+
form('show [plan]', { planning: 'view', subcommands: ['show'], args: positionals(0, 1), options: [flag('--json')] }),
|
|
68
86
|
form('mark <plan> serves', { subcommands: ['mark'], args: positionals(2, 2), options: [value('--reason'), flag('--json'), flag('--no-index')] }),
|
|
69
87
|
form('close <plan> --reason <text>', { subcommands: ['close'], args: positionals(1, 1), options: [value('--reason'), flag('--json'), flag('--no-index')] }),
|
|
70
88
|
form('fold <plan> --into <plan> --reason <text>', { subcommands: ['fold'], args: positionals(1, 1), options: [value('--into'), value('--reason'), flag('--json'), flag('--no-index')] }),
|
|
71
89
|
form('clear <plan>', { subcommands: ['clear'], args: positionals(1, 1), options: [flag('--json'), flag('--no-index')] }),
|
|
72
|
-
]),
|
|
73
|
-
command('stats', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
74
|
-
command('graph', none, 'read', [form('', { options: [flag('--dot'), flag('--json'), value('--status'), value('--module'), value('--surface')] })]),
|
|
75
|
-
command('deps', none, 'read', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), value('--depth')] })]),
|
|
90
|
+
], ACTION),
|
|
91
|
+
command('stats', none, 'read', [form('', { options: [flag('--json')] })], VIEW),
|
|
92
|
+
command('graph', none, 'read', [form('', { options: [flag('--dot'), flag('--json'), value('--status'), value('--module'), value('--surface')] })], VIEW),
|
|
93
|
+
command('deps', none, 'read', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), value('--depth')] })], VIEW),
|
|
76
94
|
command('briefing', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
77
95
|
command('context', none, 'read', [form('', { options: [flag('--json'), flag('--compact'), flag('--summarize'), value('--model'), value('--sections'), value('--max-bytes')] })]),
|
|
78
96
|
command('agent-context', none, 'read', [form('', { options: [flag('--json'), value('--sections'), value('--max-bytes')] })]),
|
|
79
97
|
command('hud', none, 'read', [form('', { options: [flag('--json'), flag('--subagent'), flag('--prompt-submit')] })]),
|
|
80
|
-
command('focus', none, 'read', [form('[status]', { args: positionals(0, 1), options: [flag('--json')] })]),
|
|
98
|
+
command('focus', none, 'read', [form('[status]', { args: positionals(0, 1), options: [flag('--json')] })], VIEW),
|
|
81
99
|
command('query', none, 'read', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })]),
|
|
82
100
|
command('grep', none, 'read', [form('<term> [terms...]', { args: positionals(1, Infinity), options: QUERY_OPTIONS })]),
|
|
83
101
|
command('plans', none, 'read', [
|
|
84
102
|
form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS }),
|
|
85
103
|
form('status [terms...]', { subcommands: ['status'], args: positionals(0, Infinity), options: QUERY_OPTIONS }),
|
|
86
|
-
]),
|
|
87
|
-
command('runlists', none, 'read', [form('', { options: [flag('--json'), value('--limit'), value('--sort')] })]),
|
|
88
|
-
command('roadmaps', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
104
|
+
], VIEW),
|
|
105
|
+
command('runlists', none, 'read', [form('', { options: [flag('--json'), value('--limit'), value('--sort')] })], VIEW),
|
|
106
|
+
command('roadmaps', none, 'read', [form('', { options: [flag('--json')] })], VIEW),
|
|
89
107
|
command('diff', none, 'read', [form('[file]', { args: positionals(0, 1), options: [flag('--stat'), value('--since'), flag('--summarize'), value('--model')] })]),
|
|
90
108
|
command('summary', none, 'read', [form('<file>', { args: positionals(1, 1), options: [value('--model'), value('--max-tokens'), flag('--json')] })]),
|
|
91
|
-
command('unblocks', none, 'read', [form('<file>', { args: positionals(1, 1), options: [flag('--json')] })]),
|
|
92
|
-
command('health', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
109
|
+
command('unblocks', none, 'read', [form('<file>', { args: positionals(1, 1), options: [flag('--json')] })], VIEW),
|
|
110
|
+
command('health', none, 'read', [form('', { options: [flag('--json')] })], VIEW),
|
|
93
111
|
command('flags', none, 'read', [form('', { options: [flag('--all'), flag('--json')] })]),
|
|
94
112
|
command('flag', mutates('the flags log under the state directory'), 'mutate', [
|
|
95
113
|
form('add <file[:line]> <text...>', { subcommands: ['add'], args: positionals(2, Infinity), options: [value('--severity'), value('--by')] }),
|
|
@@ -99,7 +117,7 @@ const definitions = [
|
|
|
99
117
|
form('show <id>', { subcommands: ['show'], args: positionals(1, 1) }),
|
|
100
118
|
form('sync <check-name> [findings]', { subcommands: ['sync'], args: positionals(1, 2), dashPositionalsAfter: 1 }),
|
|
101
119
|
]),
|
|
102
|
-
command('glossary', none, 'read', [form('[term]', { args: positionals(0, 1), options: [flag('--list'), flag('--json')] })]),
|
|
120
|
+
command('glossary', none, 'read', [form('[term]', { args: positionals(0, 1), options: [flag('--list'), flag('--json')] })], VIEW),
|
|
103
121
|
command('model', mutates('~/.runlist/model.json and the local model server'), 'read', [
|
|
104
122
|
form('', { options: [flag('--json')] }),
|
|
105
123
|
form('status', { subcommands: ['status'], options: [flag('--json')] }),
|
|
@@ -112,21 +130,21 @@ const definitions = [
|
|
|
112
130
|
command('show', none, 'read', [form('<file...>', { args: positionals(1, Infinity), options: [flag('--json'), flag('--card'), flag('--all'), flag('--frontmatter'), value('--section', '-s'), value('--from')] })]),
|
|
113
131
|
command('xref', mutates('the flags log under the state directory with --check --flag; otherwise read-only'), 'read', [
|
|
114
132
|
form('<file...> | --check [--flag]', { args: positionals(0, Infinity), options: [flag('--json'), flag('--check'), flag('--flag')] }),
|
|
115
|
-
]),
|
|
116
|
-
command('record', mutates('native flag or decision through the shared domain writer'), 'mutate', [form('<file> <action>', { args: positionals(2, 2), options: [value('--note'), value('--option'), value('--expected-revision'), flag('--json')] })]),
|
|
117
|
-
command('decision', mutates('selected decision source through the source editor'), 'mutate', [form('<file> <id>', { args: positionals(2, 2), options: [value('--disposition'), value('--note'), value('--choice'), value('--expected-revision'), flag('--json')] })]),
|
|
118
|
-
command('decisions', none, 'read', [form('[doc-or-id]', { args: positionals(0, 1), options: [flag('--all'), flag('--json'), flag('--check')] })]),
|
|
119
|
-
command('modules', none, 'read', [form('', { options: [value('--sort'), value('--limit'), flag('--all'), flag('--json')] })]),
|
|
120
|
-
command('module', none, 'read', [form('<name>', { args: positionals(1, 1), options: [value('--sort'), flag('--json')] })]),
|
|
121
|
-
command('surfaces', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
133
|
+
], VIEW),
|
|
134
|
+
command('record', mutates('native flag or decision through the shared domain writer'), 'mutate', [form('<file> <action>', { args: positionals(2, 2), options: [value('--note'), value('--option'), value('--expected-revision'), flag('--json')] })], ACTION),
|
|
135
|
+
command('decision', mutates('selected decision source through the source editor'), 'mutate', [form('<file> <id>', { args: positionals(2, 2), options: [value('--disposition'), value('--note'), value('--choice'), value('--expected-revision'), flag('--json')] })], ACTION),
|
|
136
|
+
command('decisions', none, 'read', [form('[doc-or-id]', { args: positionals(0, 1), options: [flag('--all'), flag('--json'), flag('--check')] })], VIEW),
|
|
137
|
+
command('modules', none, 'read', [form('', { options: [value('--sort'), value('--limit'), flag('--all'), flag('--json')] })], VIEW),
|
|
138
|
+
command('module', none, 'read', [form('<name>', { args: positionals(1, 1), options: [value('--sort'), flag('--json')] })], VIEW),
|
|
139
|
+
command('surfaces', none, 'read', [form('', { options: [flag('--json')] })], VIEW),
|
|
122
140
|
command('journal', none, 'read', [form('', { options: [value('--tail'), flag('--errors'), value('--session'), value('--since'), flag('--by-command'), flag('--help-topics'), flag('--json')] })]),
|
|
123
141
|
command('errors', none, 'read', [form('', { options: [flag('--json'), flag('--by-family'), value('--limit'), value('--tail'), value('--repo')] })]),
|
|
124
142
|
command('misuse', none, 'read', [form('', { options: [flag('--json'), value('--tail'), flag('--by-rule'), value('--repo')] })]),
|
|
125
143
|
|
|
126
144
|
command('roadmap', mutates('managed source when `next`; otherwise read-only'), 'workflow', [
|
|
127
145
|
form('[hub]', { args: positionals(0, 1), options: [flag('--json')] }),
|
|
128
|
-
form('next [hub]', { subcommands: ['next'], args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index')] }),
|
|
129
|
-
]),
|
|
146
|
+
form('next [hub]', { planning: 'action', subcommands: ['next'], args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index')] }),
|
|
147
|
+
], VIEW),
|
|
130
148
|
command('prompts', mutates('managed sources/destinations by subcommand'), 'workflow', [
|
|
131
149
|
form('', { options: [flag('--json'), value('--status'), flag('--include-archived'), value('--sort'), value('--limit'), flag('--all')] }),
|
|
132
150
|
form('list', { subcommands: ['list', 'status'], options: [flag('--json'), value('--status'), flag('--include-archived'), value('--sort'), value('--limit'), flag('--all')] }),
|
|
@@ -141,18 +159,18 @@ const definitions = [
|
|
|
141
159
|
], { aliases: ['prompt'] }),
|
|
142
160
|
command('use', mutates('managed source when starting/consuming; docs remain read-only'), 'workflow', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
|
|
143
161
|
command('next', mutates('managed prompt source and same-root archive destination'), 'workflow', [form('', { options: [flag('--json'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
|
|
144
|
-
command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), value('--parent'), flag('--template'), flag('--force'), flag('--replace'), flag('--done'), flag('--subagent'), flag('--json')] })]),
|
|
162
|
+
command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), value('--parent'), flag('--template'), flag('--force'), flag('--replace'), flag('--done'), value('--next-step'), value('--current-state'), flag('--subagent'), flag('--json')] })]),
|
|
145
163
|
command('runlist', mutates('managed hubs/children and managed scaffold destinations'), 'workflow', [
|
|
146
164
|
form('<hub>', { args: positionals(1, 1), options: [flag('--json')] }),
|
|
147
|
-
form('next <hub>', { subcommands: ['next'], args: positionals(1, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files')] }),
|
|
148
|
-
form('add <hub> <child...>', { subcommands: ['add'], args: positionals(2, Infinity), options: [flag('--json')] }),
|
|
149
|
-
form('remove <hub> <child...>', { subcommands: ['remove'], args: positionals(2, Infinity), options: [flag('--json'), flag('--clear-parent')] }),
|
|
150
|
-
form('reorder <hub> <child...>', { subcommands: ['reorder'], args: positionals(2, Infinity), options: [flag('--json'), value('--before'), value('--after')] }),
|
|
151
|
-
]),
|
|
165
|
+
form('next <hub>', { planning: 'action', subcommands: ['next'], args: positionals(1, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files')] }),
|
|
166
|
+
form('add <hub> <child...>', { planning: 'action', subcommands: ['add'], args: positionals(2, Infinity), options: [flag('--json')] }),
|
|
167
|
+
form('remove <hub> <child...>', { planning: 'action', subcommands: ['remove'], args: positionals(2, Infinity), options: [flag('--json'), flag('--clear-parent')] }),
|
|
168
|
+
form('reorder <hub> <child...>', { planning: 'action', subcommands: ['reorder'], args: positionals(2, Infinity), options: [flag('--json'), value('--before'), value('--after')] }),
|
|
169
|
+
], VIEW),
|
|
152
170
|
|
|
153
171
|
command('export', mutates('external output path intentionally unrestricted'), 'mutate', [form('[file]', { args: positionals(0, 1), options: [value('--format'), value('--output'), value('--status'), value('--module'), value('--root'), value('--type')] })]),
|
|
154
172
|
command('guard', mutates('global misuse log; no managed document writes'), 'mutate', [form('')]),
|
|
155
|
-
command('update', mutates('global CLI and
|
|
173
|
+
command('update', mutates('global CLI and installed host integrations; --check is offline and read-only'), 'mutate', [form('', { options: [flag('--check'), flag('--json'), flag('--cli-only'), flag('--plugin-only'), value('--host'), value('--claude-config'), value('--claude-bin')] })]),
|
|
156
174
|
command('install', mutates('agent-host plugin directory outside the repo'), 'mutate', [
|
|
157
175
|
form('[host]', { args: positionals(0, 1), options: [flag('--remove'), flag('--force'), flag('--json'), value('--path')] }),
|
|
158
176
|
]),
|
|
@@ -162,9 +180,9 @@ const definitions = [
|
|
|
162
180
|
command('archive', mutates('managed source and same-root destination'), 'mutate', [form('<file>', { args: positionals(1, 1), options: [...LIFECYCLE_OPTIONS, flag('--closeout-template'), flag('--fix-refs'), flag('--strings')] })]),
|
|
163
181
|
command('bulk', mutates('managed source sweep; archive destinations preserve roots'), 'mutate', [
|
|
164
182
|
form('archive <files...>', { subcommands: ['archive'], args: positionals(1, Infinity), options: [flag('--json'), flag('--no-index'), flag('--show-files')] }),
|
|
165
|
-
form('tag [files...]', { subcommands: ['tag'], args: positionals(0, Infinity), options: [value('--type'), value('--status'), flag('--json')] }),
|
|
183
|
+
form('tag [files...]', { planning: 'action', subcommands: ['tag'], args: positionals(0, Infinity), options: [value('--type'), value('--status'), flag('--json')] }),
|
|
166
184
|
]),
|
|
167
|
-
command('bulk-tag', mutates('managed source sweep'), 'mutate', [form('[files...]', { args: positionals(0, Infinity), options: [value('--type'), value('--status'), flag('--json')] })]),
|
|
185
|
+
command('bulk-tag', mutates('managed source sweep'), 'mutate', [form('[files...]', { args: positionals(0, Infinity), options: [value('--type'), value('--status'), flag('--json')] })], ACTION),
|
|
168
186
|
command('touch', mutates('managed source or managed source sweep'), 'mutate', [form('[file...]', { args: positionals(0, Infinity), options: [flag('--git')] })]),
|
|
169
187
|
command('new', mutates('managed document destination; external body input unrestricted'), 'mutate', [form('[type] <name> [body...]', {
|
|
170
188
|
args: positionals(0, Infinity),
|
|
@@ -179,9 +197,9 @@ const definitions = [
|
|
|
179
197
|
form('repair', { subcommands: ['repair'], options: [flag('--fix'), flag('--strings')] }),
|
|
180
198
|
form('<old> <new>', { args: positionals(2, 2), options: [flag('--fix'), flag('--strings')] }),
|
|
181
199
|
]),
|
|
182
|
-
command('fix-membership', mutates('managed source sweep'), 'mutate', [form('[hubs...]', { args: positionals(0, Infinity), options: [flag('--json')] })]),
|
|
183
|
-
command('sync-status', mutates('managed source sweep'), 'mutate', [form('[hubs...]', { args: positionals(0, Infinity), options: [flag('--adopt'), flag('--json')] })]),
|
|
184
|
-
command('doctor', mutates('managed sweeps
|
|
200
|
+
command('fix-membership', mutates('managed source sweep'), 'mutate', [form('[hubs...]', { args: positionals(0, Infinity), options: [flag('--json')] })], ACTION),
|
|
201
|
+
command('sync-status', mutates('managed source sweep'), 'mutate', [form('[hubs...]', { args: positionals(0, Infinity), options: [flag('--adopt'), flag('--json')] })], ACTION),
|
|
202
|
+
command('doctor', mutates('managed sweeps and maintenance by mode; --hosts reports read-only or applies explicitly selected host repairs'), 'mutate', [form('[path]', { args: positionals(0, 1), options: [flag('--apply', '--yes'), flag('--statuses'), optionalValue('--migrate-template'), flag('--migrate-prompts'), flag('--frontmatter-fix'), flag('--project'), flag('--prompts'), flag('--transactions'), flag('--claims'), flag('--session'), flag('--hosts'), value('--repair'), value('--claude-config'), value('--claude-bin'), value('--older-than'), flag('--json'), flag('--include-archived')] })]),
|
|
185
203
|
command('statuses', mutates('project config path; document scan is read-only'), 'mutate', [
|
|
186
204
|
form('list', { subcommands: ['list'], options: [value('--type'), flag('--json')] }),
|
|
187
205
|
form('add <name>', { subcommands: ['add'], args: positionals(1, 1), options: STATUS_PROPERTY_OPTIONS }),
|
|
@@ -200,8 +218,8 @@ const definitions = [
|
|
|
200
218
|
command('finish', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
|
|
201
219
|
command('handoff', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
|
|
202
220
|
|
|
203
|
-
command('stale', none, 'preset', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })]),
|
|
204
|
-
command('actionable', none, 'preset', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })]),
|
|
221
|
+
command('stale', none, 'preset', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })], VIEW),
|
|
222
|
+
command('actionable', none, 'preset', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })], VIEW),
|
|
205
223
|
];
|
|
206
224
|
|
|
207
225
|
function schemaErrors(schema) {
|
|
@@ -213,6 +231,9 @@ function schemaErrors(schema) {
|
|
|
213
231
|
names.add(entry.name);
|
|
214
232
|
if (!entry.group) errors.push(`${entry.name}: missing help group`);
|
|
215
233
|
if (!entry.forms.length) errors.push(`${entry.name}: missing forms`);
|
|
234
|
+
for (const planning of [entry.planning, ...entry.forms.map(commandForm => commandForm.planning)]) {
|
|
235
|
+
if (planning != null && planning !== 'view' && planning !== 'action') errors.push(`${entry.name}: planning must be 'view' or 'action'`);
|
|
236
|
+
}
|
|
216
237
|
for (const alias of entry.aliases) {
|
|
217
238
|
if (!alias || names.has(alias) || aliases.has(alias)) errors.push(`${entry.name}: duplicate alias ${alias}`);
|
|
218
239
|
aliases.add(alias);
|
|
@@ -310,6 +331,11 @@ function scanArgs(name, argv, options, passthrough = false, dashPositionalsAfter
|
|
|
310
331
|
if (!option) {
|
|
311
332
|
if (passthrough) continue;
|
|
312
333
|
if (positional.length >= dashPositionalsAfter) { positional.push(arg); continue; }
|
|
334
|
+
if (name === 'show' && arg === '--lines') {
|
|
335
|
+
throw new Error('Unknown flag for `runlist show`: --lines\n\n'
|
|
336
|
+
+ 'Drop --lines. Continue with `runlist show <file> --section "<heading>" --from <line>`.\n'
|
|
337
|
+
+ 'Section output is bounded automatically; --from is an absolute file line.');
|
|
338
|
+
}
|
|
313
339
|
throw new Error(`Unknown flag for \`runlist ${name}\`: ${arg}`);
|
|
314
340
|
}
|
|
315
341
|
if (option.arity === 1) {
|
|
@@ -325,6 +351,34 @@ function scanArgs(name, argv, options, passthrough = false, dashPositionalsAfter
|
|
|
325
351
|
return positional;
|
|
326
352
|
}
|
|
327
353
|
|
|
354
|
+
// Help selects a form without requiring the arguments needed to execute it.
|
|
355
|
+
export function commandHelpContext(name, argv) {
|
|
356
|
+
const definition = commandDefinition(name);
|
|
357
|
+
if (!definition) return null;
|
|
358
|
+
const options = optionMap(definition.forms);
|
|
359
|
+
for (const option of GLOBAL_OPTIONS) {
|
|
360
|
+
for (const alias of option.names) if (!options.has(alias)) options.set(alias, option);
|
|
361
|
+
}
|
|
362
|
+
const positional = scanArgs(definition.name, argv, options);
|
|
363
|
+
const selected = definition.forms.find(entry => entry.subcommands.includes(positional[0]));
|
|
364
|
+
return { positional, subcommand: selected?.subcommands[0] ?? null };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Whether a command, in the form these arguments select, needs planning on:
|
|
368
|
+
// `{ requirement: 'view' | 'action', subcommand }`, or null (see VIEW/ACTION
|
|
369
|
+
// above). Read before argument validation, so a planning command missing its
|
|
370
|
+
// arguments still hears that planning is off rather than its usage.
|
|
371
|
+
export function commandPlanning(name, argv) {
|
|
372
|
+
const definition = commandDefinition(name);
|
|
373
|
+
if (!definition) return null;
|
|
374
|
+
let subcommand = null;
|
|
375
|
+
try { subcommand = commandHelpContext(definition.name, normalizeCommandArgs(definition.name, argv)).subcommand; } catch { /* unknown flag: the command's own form */ }
|
|
376
|
+
const selected = definition.forms.find(entry => entry.subcommands.includes(subcommand))
|
|
377
|
+
?? definition.forms.find(entry => entry.subcommands.length === 0);
|
|
378
|
+
const requirement = selected?.planning ?? definition.planning;
|
|
379
|
+
return requirement ? { requirement, subcommand } : null;
|
|
380
|
+
}
|
|
381
|
+
|
|
328
382
|
export function normalizeCommandArgs(name, argv) {
|
|
329
383
|
const canonical = canonicalCommand(name);
|
|
330
384
|
if (canonical !== 'roadmap') return [...argv];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { assertSnapshotCurrent, replaceSnapshot, snapshotFile } from './atomic-mutation.mjs';
|
|
5
|
+
import { renderPromptTrackingSetting } from './config-setting.mjs';
|
|
6
|
+
import { promptDirectory, promptRouting } from './new.mjs';
|
|
7
|
+
|
|
8
|
+
const KEY = 'promptTracking';
|
|
9
|
+
const VALUES = ['untracked', 'tracked', 'local'];
|
|
10
|
+
const READABLE = Object.freeze([KEY, 'mode', 'root', 'docsRoots', 'index.path', 'archiveDir', 'promptDirectory']);
|
|
11
|
+
const USAGE = 'Usage: runlist config show [--json] | get <key> [--json] | set promptTracking <untracked|tracked|local> [--json]';
|
|
12
|
+
|
|
13
|
+
function inspectSettings(config) {
|
|
14
|
+
const source = key => config.settingSources?.[key] ?? 'unknown';
|
|
15
|
+
return {
|
|
16
|
+
promptTracking: { value: config.promptTracking, source: config.promptTrackingSource },
|
|
17
|
+
mode: { value: config.mode ?? 'plans', source: source('mode') },
|
|
18
|
+
root: { value: config.raw?.root ?? '.', source: source('root') },
|
|
19
|
+
docsRoots: { value: config.docsRoots ?? [config.docsRoot], source: 'derived' },
|
|
20
|
+
'index.path': { value: config.indexPath ?? null, source: source('index.path') },
|
|
21
|
+
archiveDir: { value: config.archiveDir, source: source('archiveDir') },
|
|
22
|
+
// Markdown template overrides change content, not routing. Inspect only
|
|
23
|
+
// layout so a broken content template cannot prevent settings inspection.
|
|
24
|
+
promptDirectory: { value: promptDirectory(config, promptRouting(config)), source: 'derived' },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function manualSetting(key, configPath) {
|
|
29
|
+
const file = configPath ?? 'runlist.config.mjs (create it with runlist setup)';
|
|
30
|
+
if (key === 'mode') return `A handoffs-only project turns planning on with runlist setup, which previews the change to ${file} first.`;
|
|
31
|
+
if (key === 'root' || key === 'docsRoots') return `Edit the root export in ${file}, review dependent index/template paths, then run runlist init --dry-run and runlist check. A new project can choose --docs-root during setup. Existing files are not moved automatically.`;
|
|
32
|
+
if (key === 'promptDirectory') return `Edit templates.prompt.dir or templates.prompt.targetRoot and the root export in ${file}, then inspect runlist config get promptDirectory. Existing prompts are not moved automatically.`;
|
|
33
|
+
return `Edit ${key === 'index.path' ? 'the index.path property (or legacy readme.path)' : 'the archiveDir export'} in ${file}, then run runlist init --dry-run and runlist check. Existing files are not moved automatically.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkSyntax(content) {
|
|
37
|
+
try {
|
|
38
|
+
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: content, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error("Config is not a supported JavaScript module. Edit promptTracking manually in your config; no changes were made.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertRegularConfig(configPath, repoRoot) {
|
|
45
|
+
const resolved = path.resolve(configPath);
|
|
46
|
+
// Replacing a symlink would sever it. Refuse symlinked parent paths as well
|
|
47
|
+
// so the loaded source and the publication path cannot silently diverge.
|
|
48
|
+
const boundary = path.resolve(repoRoot);
|
|
49
|
+
for (let cursor = resolved; ; cursor = path.dirname(cursor)) {
|
|
50
|
+
if (lstatSync(cursor).isSymbolicLink()) throw new Error(`Config path uses a symlink: ${cursor}. Edit the config manually; no changes were made.`);
|
|
51
|
+
if (cursor === boundary || path.dirname(cursor) === cursor) break;
|
|
52
|
+
}
|
|
53
|
+
if (!lstatSync(resolved).isFile()) throw new Error(`Config path is not a regular file: ${resolved}`);
|
|
54
|
+
return realpathSync(resolved);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runConfig(argv, config, { dryRun = false, out = process.stdout, testHooks } = {}) {
|
|
58
|
+
if (argv.filter(arg => arg === '--json').length > 1 || argv.some(arg => arg.startsWith('-') && arg !== '--json')) throw new Error(USAGE);
|
|
59
|
+
const json = argv.includes('--json');
|
|
60
|
+
const args = argv.filter(arg => arg !== '--json');
|
|
61
|
+
const [action, key, value] = args;
|
|
62
|
+
if (!['show', 'get', 'set'].includes(action) || args.length !== ({ show: 1, get: 2, set: 3 })[action]) throw new Error(USAGE);
|
|
63
|
+
if (action !== 'show' && !READABLE.includes(key)) throw new Error(`Unsupported config key '${key}'. Readable settings: ${READABLE.join(', ')}. Editable setting: ${KEY}. Edit other exports manually in ${config.configPath ?? 'runlist.config.mjs (create it with runlist setup)'}.`);
|
|
64
|
+
const configPath = config.configPath ?? null;
|
|
65
|
+
if (action === 'set' && key !== KEY) throw new Error(`${key} is read-only in the CLI. ${manualSetting(key, configPath)} No changes were made.`);
|
|
66
|
+
const settings = action === 'set'
|
|
67
|
+
? { [KEY]: { value: config.promptTracking, source: config.promptTrackingSource } }
|
|
68
|
+
: inspectSettings(config);
|
|
69
|
+
const setting = { key: key ?? KEY, ...settings[key ?? KEY], configPath };
|
|
70
|
+
let result;
|
|
71
|
+
if (action === 'show') {
|
|
72
|
+
result = { supportedSettings: [KEY], readableSettings: READABLE, editableSettings: [KEY], settings, configPath };
|
|
73
|
+
} else if (action === 'get') {
|
|
74
|
+
result = { supportedSettings: [KEY], readableSettings: READABLE, editableSettings: [KEY], ...setting };
|
|
75
|
+
} else {
|
|
76
|
+
if (!VALUES.includes(value)) throw new Error("promptTracking must be 'untracked', 'tracked', or 'local'.");
|
|
77
|
+
if (!configPath || !config.configFound || !existsSync(configPath)) throw new Error('No project config found. Run runlist setup before config set.');
|
|
78
|
+
const file = assertRegularConfig(configPath, config.repoRoot);
|
|
79
|
+
const snapshot = snapshotFile(file);
|
|
80
|
+
checkSyntax(snapshot.content);
|
|
81
|
+
const edit = renderPromptTrackingSetting(snapshot.content, value);
|
|
82
|
+
checkSyntax(edit.content);
|
|
83
|
+
const changed = edit.content !== snapshot.content;
|
|
84
|
+
result = { action, supportedSettings: [KEY], key: KEY, previousValue: edit.previousValue, value, source: 'config', configPath, changed, dryRun };
|
|
85
|
+
if (!dryRun && changed) {
|
|
86
|
+
replaceSnapshot(snapshot, edit.content, {
|
|
87
|
+
repoRoot: config.repoRoot,
|
|
88
|
+
testHooks: {
|
|
89
|
+
...testHooks,
|
|
90
|
+
beforeReplacePublish(info) {
|
|
91
|
+
testHooks?.beforeReplacePublish?.(info);
|
|
92
|
+
assertRegularConfig(configPath, config.repoRoot);
|
|
93
|
+
assertSnapshotCurrent(snapshot);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (json) out.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
100
|
+
else {
|
|
101
|
+
out.write(`Editable setting: ${KEY}\n`);
|
|
102
|
+
out.write(`Config path: ${configPath ?? '(none; run runlist setup to create one)'}\n`);
|
|
103
|
+
if (action === 'show') {
|
|
104
|
+
for (const [name, entry] of Object.entries(settings)) out.write(`${name}: ${JSON.stringify(entry.value)} (source: ${entry.source}${name === KEY ? '; editable' : '; read-only'})\n`);
|
|
105
|
+
} else {
|
|
106
|
+
if (action === 'set') out.write(`${dryRun ? '[dry-run] Would set' : changedLabel(result.changed)} ${KEY} = ${value}\n`);
|
|
107
|
+
out.write(`Effective value${action === 'set' && dryRun ? ' after proposed change' : ''}: ${action === 'set' ? value : JSON.stringify(setting.value)}\n`);
|
|
108
|
+
out.write(`Source: ${action === 'set' ? 'config' : setting.source}\n`);
|
|
109
|
+
}
|
|
110
|
+
if ((action === 'show' || key === KEY) && (action === 'set' ? value : settings.promptTracking.value) === 'untracked') out.write('Leave pending prompts untracked to reduce commit churn. git add -A can still stage them.\n');
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function changedLabel(changed) { return changed ? 'Set' : 'Already set'; }
|