clawvault 2.4.4 → 2.4.6
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/clawvault.js +9 -0
- package/bin/command-registration.test.js +13 -1
- package/bin/help-contract.test.js +2 -0
- package/bin/register-config-commands.js +153 -0
- package/bin/register-config-route-commands.test.js +114 -0
- package/bin/register-kanban-commands.js +56 -0
- package/bin/register-kanban-commands.test.js +83 -0
- package/bin/register-resilience-commands.js +37 -2
- package/bin/register-resilience-commands.test.js +81 -0
- package/bin/register-route-commands.js +114 -0
- package/bin/register-task-commands.js +58 -7
- package/bin/register-task-commands.test.js +46 -0
- package/bin/test-helpers/cli-command-fixtures.js +20 -0
- package/dist/{chunk-XDCFXFGH.js → chunk-22WE3J4F.js} +1 -1
- package/dist/chunk-3PJIGGWV.js +49 -0
- package/dist/{chunk-FDJIZKCW.js → chunk-6B3JWM7J.js} +12 -48
- package/dist/{chunk-DEFBIVQ3.js → chunk-6QLRSPLZ.js} +170 -7
- package/dist/{chunk-MZZJLQNQ.js → chunk-F55HGNU4.js} +6 -0
- package/dist/{chunk-SNEMCQP7.js → chunk-FD2ZA65C.js} +1 -1
- package/dist/{chunk-GBIDDDSL.js → chunk-FKQJB6XC.js} +1 -1
- package/dist/{chunk-FEFPBHH4.js → chunk-H6WQUUNK.js} +453 -23
- package/dist/{chunk-BMOQI62Q.js → chunk-HNMFXFYP.js} +5 -3
- package/dist/{chunk-DHJPXGC7.js → chunk-JTO7NZLS.js} +1 -1
- package/dist/chunk-LLN5SPGL.js +399 -0
- package/dist/chunk-OIWVQYQF.js +284 -0
- package/dist/{chunk-IFTEGE4D.js → chunk-P2ZH6AN5.js} +4 -2
- package/dist/commands/backlog.js +1 -1
- package/dist/commands/blocked.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/context.js +4 -3
- package/dist/commands/doctor.js +6 -5
- package/dist/commands/kanban.d.ts +63 -0
- package/dist/commands/kanban.js +21 -0
- package/dist/commands/observe.js +4 -3
- package/dist/commands/rebuild.js +4 -3
- package/dist/commands/recover.d.ts +13 -1
- package/dist/commands/recover.js +10 -2
- package/dist/commands/replay.js +4 -3
- package/dist/commands/setup.js +3 -2
- package/dist/commands/sleep.js +6 -5
- package/dist/commands/status.js +6 -5
- package/dist/commands/task.d.ts +18 -6
- package/dist/commands/task.js +69 -12
- package/dist/commands/wake.js +5 -4
- package/dist/index.d.ts +29 -1
- package/dist/index.js +60 -15
- package/dist/lib/task-utils.d.ts +41 -10
- package/dist/lib/task-utils.js +5 -1
- package/package.json +2 -2
- package/dist/chunk-IWYZAXKJ.js +0 -146
package/bin/clawvault.js
CHANGED
|
@@ -16,6 +16,9 @@ import { registerResilienceCommands } from './register-resilience-commands.js';
|
|
|
16
16
|
import { registerSessionLifecycleCommands } from './register-session-lifecycle-commands.js';
|
|
17
17
|
import { registerTemplateCommands } from './register-template-commands.js';
|
|
18
18
|
import { registerVaultOperationsCommands } from './register-vault-operations-commands.js';
|
|
19
|
+
import { registerConfigCommands } from './register-config-commands.js';
|
|
20
|
+
import { registerRouteCommands } from './register-route-commands.js';
|
|
21
|
+
import { registerKanbanCommands } from './register-kanban-commands.js';
|
|
19
22
|
|
|
20
23
|
import { registerTaskCommands } from './register-task-commands.js';
|
|
21
24
|
|
|
@@ -91,8 +94,14 @@ registerTaskCommands(program, {
|
|
|
91
94
|
chalk,
|
|
92
95
|
resolveVaultPath
|
|
93
96
|
});
|
|
97
|
+
registerKanbanCommands(program, {
|
|
98
|
+
chalk,
|
|
99
|
+
resolveVaultPath
|
|
100
|
+
});
|
|
94
101
|
|
|
95
102
|
registerTailscaleCommands(program, { chalk });
|
|
103
|
+
registerConfigCommands(program, { chalk, resolveVaultPath });
|
|
104
|
+
registerRouteCommands(program, { chalk, resolveVaultPath });
|
|
96
105
|
|
|
97
106
|
// Parse and run
|
|
98
107
|
program.parse();
|
|
@@ -9,6 +9,8 @@ import { registerResilienceCommands } from './register-resilience-commands.js';
|
|
|
9
9
|
import { registerSessionLifecycleCommands } from './register-session-lifecycle-commands.js';
|
|
10
10
|
import { registerTemplateCommands } from './register-template-commands.js';
|
|
11
11
|
import { registerVaultOperationsCommands } from './register-vault-operations-commands.js';
|
|
12
|
+
import { registerConfigCommands } from './register-config-commands.js';
|
|
13
|
+
import { registerRouteCommands } from './register-route-commands.js';
|
|
12
14
|
import {
|
|
13
15
|
chalkStub,
|
|
14
16
|
createGetVaultStub,
|
|
@@ -107,6 +109,14 @@ describe('CLI command registration modules', () => {
|
|
|
107
109
|
runQmd: async () => {}
|
|
108
110
|
});
|
|
109
111
|
registerTemplateCommands(program, { chalk: chalkStub });
|
|
112
|
+
registerConfigCommands(program, {
|
|
113
|
+
chalk: chalkStub,
|
|
114
|
+
resolveVaultPath: stubResolveVaultPath
|
|
115
|
+
});
|
|
116
|
+
registerRouteCommands(program, {
|
|
117
|
+
chalk: chalkStub,
|
|
118
|
+
resolveVaultPath: stubResolveVaultPath
|
|
119
|
+
});
|
|
110
120
|
|
|
111
121
|
const names = listCommandNames(program);
|
|
112
122
|
expect(names).toEqual(expect.arrayContaining([
|
|
@@ -129,7 +139,9 @@ describe('CLI command registration modules', () => {
|
|
|
129
139
|
'sleep',
|
|
130
140
|
'handoff',
|
|
131
141
|
'recap',
|
|
132
|
-
'template'
|
|
142
|
+
'template',
|
|
143
|
+
'config',
|
|
144
|
+
'route'
|
|
133
145
|
]));
|
|
134
146
|
|
|
135
147
|
const templateCommand = program.commands.find((command) => command.name() === 'template');
|
|
@@ -12,6 +12,8 @@ describe('CLI help contract', () => {
|
|
|
12
12
|
expect(help).toContain('replay');
|
|
13
13
|
expect(help).toContain('repair-session');
|
|
14
14
|
expect(help).toContain('template');
|
|
15
|
+
expect(help).toContain('config');
|
|
16
|
+
expect(help).toContain('route');
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
it('documents context auto profile and compat strict options', () => {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime config command registrations backed by .clawvault.json.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function stringifyValue(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
if (value.every((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')) {
|
|
8
|
+
return value.join(', ');
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
}
|
|
12
|
+
if (value && typeof value === 'object') {
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
if (value === null || value === undefined) {
|
|
16
|
+
return '(unset)';
|
|
17
|
+
}
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function flattenConfig(value, prefix = '') {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
23
|
+
return [{ key: prefix || '(root)', value: stringifyValue(value) }];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rows = [];
|
|
27
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
const next = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
const entry = value[key];
|
|
31
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
32
|
+
rows.push(...flattenConfig(entry, next));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
rows.push({ key: next, value: stringifyValue(entry) });
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printTable(rows) {
|
|
41
|
+
if (rows.length === 0) {
|
|
42
|
+
console.log('No config values found.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const keyWidth = Math.max(
|
|
46
|
+
'KEY'.length,
|
|
47
|
+
...rows.map((row) => row.key.length)
|
|
48
|
+
);
|
|
49
|
+
const valueWidth = Math.max(
|
|
50
|
+
'VALUE'.length,
|
|
51
|
+
...rows.map((row) => row.value.length)
|
|
52
|
+
);
|
|
53
|
+
const header = `${'KEY'.padEnd(keyWidth)} ${'VALUE'.padEnd(valueWidth)}`;
|
|
54
|
+
const divider = `${'-'.repeat(keyWidth)} ${'-'.repeat(valueWidth)}`;
|
|
55
|
+
console.log(header);
|
|
56
|
+
console.log(divider);
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
console.log(`${row.key.padEnd(keyWidth)} ${row.value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeConfigKey(key) {
|
|
63
|
+
return String(key || '').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
|
|
67
|
+
const config = program
|
|
68
|
+
.command('config')
|
|
69
|
+
.description('Read and modify runtime config');
|
|
70
|
+
|
|
71
|
+
config
|
|
72
|
+
.command('get <key>')
|
|
73
|
+
.description('Read a runtime config value (dot-notation supported)')
|
|
74
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
75
|
+
.action(async (key, options) => {
|
|
76
|
+
try {
|
|
77
|
+
const { getConfigValue, SUPPORTED_CONFIG_KEYS } = await import('../dist/index.js');
|
|
78
|
+
const normalizedKey = normalizeConfigKey(key);
|
|
79
|
+
if (!SUPPORTED_CONFIG_KEYS.includes(normalizedKey)) {
|
|
80
|
+
throw new Error(`Unsupported config key: ${normalizedKey}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const value = getConfigValue(resolveVaultPath(options.vault), normalizedKey);
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
console.log(value.join(','));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value && typeof value === 'object') {
|
|
89
|
+
console.log(JSON.stringify(value, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(value === undefined || value === null ? '' : String(value));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
config
|
|
100
|
+
.command('set <key> <value>')
|
|
101
|
+
.description('Set a runtime config value')
|
|
102
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
103
|
+
.action(async (key, value, options) => {
|
|
104
|
+
try {
|
|
105
|
+
const { setConfigValue, SUPPORTED_CONFIG_KEYS } = await import('../dist/index.js');
|
|
106
|
+
const normalizedKey = normalizeConfigKey(key);
|
|
107
|
+
if (!SUPPORTED_CONFIG_KEYS.includes(normalizedKey)) {
|
|
108
|
+
throw new Error(`Unsupported config key: ${normalizedKey}`);
|
|
109
|
+
}
|
|
110
|
+
const result = setConfigValue(resolveVaultPath(options.vault), normalizedKey, value);
|
|
111
|
+
console.log(chalk.green(`✓ Updated ${normalizedKey}`));
|
|
112
|
+
console.log(chalk.dim(` Value: ${stringifyValue(result.value)}`));
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
config
|
|
120
|
+
.command('list')
|
|
121
|
+
.description('List all config values')
|
|
122
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
123
|
+
.action(async (options) => {
|
|
124
|
+
try {
|
|
125
|
+
const { listConfig } = await import('../dist/index.js');
|
|
126
|
+
const values = listConfig(resolveVaultPath(options.vault));
|
|
127
|
+
const rows = flattenConfig(values);
|
|
128
|
+
printTable(rows);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
config
|
|
136
|
+
.command('reset')
|
|
137
|
+
.description('Reset runtime config values to defaults')
|
|
138
|
+
.option('--confirm', 'Confirm reset')
|
|
139
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
140
|
+
.action(async (options) => {
|
|
141
|
+
try {
|
|
142
|
+
if (!options.confirm) {
|
|
143
|
+
throw new Error('Refusing to reset config without --confirm.');
|
|
144
|
+
}
|
|
145
|
+
const { resetConfig } = await import('../dist/index.js');
|
|
146
|
+
resetConfig(resolveVaultPath(options.vault));
|
|
147
|
+
console.log(chalk.green('✓ Config reset to defaults'));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerConfigCommands } from './register-config-commands.js';
|
|
4
|
+
import { registerRouteCommands } from './register-route-commands.js';
|
|
5
|
+
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
getConfigValueMock,
|
|
9
|
+
setConfigValueMock,
|
|
10
|
+
listConfigMock,
|
|
11
|
+
resetConfigMock,
|
|
12
|
+
addRouteRuleMock,
|
|
13
|
+
listRouteRulesMock,
|
|
14
|
+
removeRouteRuleMock,
|
|
15
|
+
testRouteRuleMock
|
|
16
|
+
} = vi.hoisted(() => ({
|
|
17
|
+
getConfigValueMock: vi.fn(),
|
|
18
|
+
setConfigValueMock: vi.fn(),
|
|
19
|
+
listConfigMock: vi.fn(),
|
|
20
|
+
resetConfigMock: vi.fn(),
|
|
21
|
+
addRouteRuleMock: vi.fn(),
|
|
22
|
+
listRouteRulesMock: vi.fn(),
|
|
23
|
+
removeRouteRuleMock: vi.fn(),
|
|
24
|
+
testRouteRuleMock: vi.fn()
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('../dist/index.js', () => ({
|
|
28
|
+
SUPPORTED_CONFIG_KEYS: [
|
|
29
|
+
'name',
|
|
30
|
+
'categories',
|
|
31
|
+
'theme',
|
|
32
|
+
'observe.model',
|
|
33
|
+
'observe.provider',
|
|
34
|
+
'context.maxResults',
|
|
35
|
+
'context.defaultProfile',
|
|
36
|
+
'graph.maxHops'
|
|
37
|
+
],
|
|
38
|
+
getConfigValue: getConfigValueMock,
|
|
39
|
+
setConfigValue: setConfigValueMock,
|
|
40
|
+
listConfig: listConfigMock,
|
|
41
|
+
resetConfig: resetConfigMock,
|
|
42
|
+
addRouteRule: addRouteRuleMock,
|
|
43
|
+
listRouteRules: listRouteRulesMock,
|
|
44
|
+
removeRouteRule: removeRouteRuleMock,
|
|
45
|
+
testRouteRule: testRouteRuleMock
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
function buildProgram() {
|
|
49
|
+
const program = new Command();
|
|
50
|
+
const resolveVaultPath = (value) => value ?? '/vault';
|
|
51
|
+
registerConfigCommands(program, { chalk: chalkStub, resolveVaultPath });
|
|
52
|
+
registerRouteCommands(program, { chalk: chalkStub, resolveVaultPath });
|
|
53
|
+
return program;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runCommand(args) {
|
|
57
|
+
const program = buildProgram();
|
|
58
|
+
await program.parseAsync(args, { from: 'user' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
getConfigValueMock.mockReturnValue('demo-vault');
|
|
64
|
+
setConfigValueMock.mockReturnValue({ value: 'demo-vault' });
|
|
65
|
+
listConfigMock.mockReturnValue({ name: 'demo-vault', context: { maxResults: 5 } });
|
|
66
|
+
resetConfigMock.mockReturnValue(undefined);
|
|
67
|
+
addRouteRuleMock.mockReturnValue({ pattern: 'Pedro', target: 'people/pedro', priority: 1 });
|
|
68
|
+
listRouteRulesMock.mockReturnValue([{ pattern: 'Pedro', target: 'people/pedro', priority: 1 }]);
|
|
69
|
+
removeRouteRuleMock.mockReturnValue(true);
|
|
70
|
+
testRouteRuleMock.mockReturnValue({ pattern: 'Pedro', target: 'people/pedro', priority: 1 });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('config and route command registrations', () => {
|
|
74
|
+
it('executes config get/set/list subcommands', async () => {
|
|
75
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
76
|
+
try {
|
|
77
|
+
await runCommand(['config', 'get', 'name']);
|
|
78
|
+
expect(getConfigValueMock).toHaveBeenCalledWith('/vault', 'name');
|
|
79
|
+
|
|
80
|
+
await runCommand(['config', 'set', 'categories', 'people,projects']);
|
|
81
|
+
expect(setConfigValueMock).toHaveBeenCalledWith('/vault', 'categories', 'people,projects');
|
|
82
|
+
|
|
83
|
+
await runCommand(['config', 'list']);
|
|
84
|
+
expect(listConfigMock).toHaveBeenCalledWith('/vault');
|
|
85
|
+
|
|
86
|
+
const output = logSpy.mock.calls.map((call) => call.join(' ')).join('\n');
|
|
87
|
+
expect(output).toContain('context.maxResults');
|
|
88
|
+
} finally {
|
|
89
|
+
logSpy.mockRestore();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('executes route add/remove/test subcommands', async () => {
|
|
94
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
95
|
+
try {
|
|
96
|
+
await runCommand(['route', 'add', 'Pedro', 'people/pedro']);
|
|
97
|
+
expect(addRouteRuleMock).toHaveBeenCalledWith('/vault', 'Pedro', 'people/pedro');
|
|
98
|
+
|
|
99
|
+
await runCommand(['route', 'remove', 'Pedro']);
|
|
100
|
+
expect(removeRouteRuleMock).toHaveBeenCalledWith('/vault', 'Pedro');
|
|
101
|
+
|
|
102
|
+
await runCommand(['route', 'test', 'Talked to Pedro']);
|
|
103
|
+
expect(testRouteRuleMock).toHaveBeenCalledWith('/vault', 'Talked to Pedro');
|
|
104
|
+
|
|
105
|
+
await runCommand(['route', 'list']);
|
|
106
|
+
expect(listRouteRulesMock).toHaveBeenCalledWith('/vault');
|
|
107
|
+
|
|
108
|
+
const output = logSpy.mock.calls.map((call) => call.join(' ')).join('\n');
|
|
109
|
+
expect(output).toContain('Route matched');
|
|
110
|
+
} finally {
|
|
111
|
+
logSpy.mockRestore();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kanban command registrations for ClawVault.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function registerKanbanCommands(
|
|
6
|
+
program,
|
|
7
|
+
{ chalk, resolveVaultPath }
|
|
8
|
+
) {
|
|
9
|
+
const kanbanCmd = program
|
|
10
|
+
.command('kanban')
|
|
11
|
+
.description('Sync Obsidian Kanban boards with task frontmatter');
|
|
12
|
+
|
|
13
|
+
kanbanCmd
|
|
14
|
+
.command('sync')
|
|
15
|
+
.description('Generate and sync an Obsidian Kanban board from tasks')
|
|
16
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
17
|
+
.option('--output <path>', 'Board markdown path (default: Board.md)')
|
|
18
|
+
.option('--group-by <field>', 'Grouping field (status, priority, project, owner)')
|
|
19
|
+
.option('--filter-project <project>', 'Only include tasks from a project')
|
|
20
|
+
.option('--filter-owner <owner>', 'Only include tasks for an owner')
|
|
21
|
+
.option('--include-done', 'Include done tasks')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
const vaultPath = resolveVaultPath(options.vault);
|
|
25
|
+
const { kanbanCommand } = await import('../dist/commands/kanban.js');
|
|
26
|
+
await kanbanCommand(vaultPath, 'sync', {
|
|
27
|
+
output: options.output,
|
|
28
|
+
groupBy: options.groupBy,
|
|
29
|
+
filterProject: options.filterProject,
|
|
30
|
+
filterOwner: options.filterOwner,
|
|
31
|
+
includeDone: options.includeDone
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
kanbanCmd
|
|
40
|
+
.command('import')
|
|
41
|
+
.description('Import lane state from an Obsidian Kanban board into tasks')
|
|
42
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
43
|
+
.option('--output <path>', 'Board markdown path (default: Board.md)')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
try {
|
|
46
|
+
const vaultPath = resolveVaultPath(options.vault);
|
|
47
|
+
const { kanbanCommand } = await import('../dist/commands/kanban.js');
|
|
48
|
+
await kanbanCommand(vaultPath, 'import', {
|
|
49
|
+
output: options.output
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerKanbanCommands } from './register-kanban-commands.js';
|
|
4
|
+
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
+
|
|
6
|
+
const { kanbanCommandMock } = vi.hoisted(() => ({
|
|
7
|
+
kanbanCommandMock: vi.fn()
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('../dist/commands/kanban.js', () => ({
|
|
11
|
+
kanbanCommand: kanbanCommandMock
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
function buildProgram() {
|
|
15
|
+
const program = new Command();
|
|
16
|
+
registerKanbanCommands(program, {
|
|
17
|
+
chalk: chalkStub,
|
|
18
|
+
resolveVaultPath: (value) => value ?? '/vault'
|
|
19
|
+
});
|
|
20
|
+
return program;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runCommand(args) {
|
|
24
|
+
const program = buildProgram();
|
|
25
|
+
await program.parseAsync(args, { from: 'user' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('register-kanban-commands', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('registers sync and import subcommands with expected options', () => {
|
|
34
|
+
const program = buildProgram();
|
|
35
|
+
const kanbanCommand = program.commands.find((command) => command.name() === 'kanban');
|
|
36
|
+
expect(kanbanCommand).toBeDefined();
|
|
37
|
+
|
|
38
|
+
const syncCommand = kanbanCommand?.commands.find((command) => command.name() === 'sync');
|
|
39
|
+
expect(syncCommand).toBeDefined();
|
|
40
|
+
const syncFlags = syncCommand?.options.map((option) => option.flags) ?? [];
|
|
41
|
+
expect(syncFlags).toEqual(expect.arrayContaining([
|
|
42
|
+
'--output <path>',
|
|
43
|
+
'--group-by <field>',
|
|
44
|
+
'--filter-project <project>',
|
|
45
|
+
'--filter-owner <owner>',
|
|
46
|
+
'--include-done'
|
|
47
|
+
]));
|
|
48
|
+
|
|
49
|
+
const importCommand = kanbanCommand?.commands.find((command) => command.name() === 'import');
|
|
50
|
+
expect(importCommand).toBeDefined();
|
|
51
|
+
const importFlags = importCommand?.options.map((option) => option.flags) ?? [];
|
|
52
|
+
expect(importFlags).toEqual(expect.arrayContaining(['--output <path>']));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('dispatches sync and import actions to the kanban command handler', async () => {
|
|
56
|
+
await runCommand([
|
|
57
|
+
'kanban',
|
|
58
|
+
'sync',
|
|
59
|
+
'--group-by',
|
|
60
|
+
'priority',
|
|
61
|
+
'--output',
|
|
62
|
+
'Board.md',
|
|
63
|
+
'--filter-project',
|
|
64
|
+
'apollo',
|
|
65
|
+
'--filter-owner',
|
|
66
|
+
'alice',
|
|
67
|
+
'--include-done'
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
expect(kanbanCommandMock).toHaveBeenCalledWith('/vault', 'sync', {
|
|
71
|
+
output: 'Board.md',
|
|
72
|
+
groupBy: 'priority',
|
|
73
|
+
filterProject: 'apollo',
|
|
74
|
+
filterOwner: 'alice',
|
|
75
|
+
includeDone: true
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await runCommand(['kanban', 'import', '--output', 'Board.md']);
|
|
79
|
+
expect(kanbanCommandMock).toHaveBeenCalledWith('/vault', 'import', {
|
|
80
|
+
output: 'Board.md'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -45,13 +45,48 @@ export function registerResilienceCommands(program, { chalk, resolveVaultPath })
|
|
|
45
45
|
.command('recover')
|
|
46
46
|
.description('Check for context death and recover state')
|
|
47
47
|
.option('--clear', 'Clear the dirty death flag after recovery')
|
|
48
|
+
.option('--check', 'Check dirty death flag without clearing it')
|
|
49
|
+
.option('--list', 'List saved checkpoints (newest first)')
|
|
48
50
|
.option('--verbose', 'Show full checkpoint and handoff content')
|
|
49
51
|
.option('-v, --vault <path>', 'Vault path')
|
|
50
52
|
.option('--json', 'Output as JSON')
|
|
51
53
|
.action(async (options) => {
|
|
52
54
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
if (options.check && options.list) {
|
|
56
|
+
throw new Error('--check and --list cannot be used together.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
recover,
|
|
61
|
+
formatRecoveryInfo,
|
|
62
|
+
checkRecoveryStatus,
|
|
63
|
+
formatRecoveryCheckStatus,
|
|
64
|
+
listCheckpoints,
|
|
65
|
+
formatCheckpointList
|
|
66
|
+
} = await import('../dist/commands/recover.js');
|
|
67
|
+
const vaultPath = resolveVaultPath(options.vault);
|
|
68
|
+
|
|
69
|
+
if (options.check) {
|
|
70
|
+
const status = await checkRecoveryStatus(vaultPath);
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log(JSON.stringify(status, null, 2));
|
|
73
|
+
} else {
|
|
74
|
+
console.log(formatRecoveryCheckStatus(status));
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.list) {
|
|
80
|
+
const checkpoints = listCheckpoints(vaultPath);
|
|
81
|
+
if (options.json) {
|
|
82
|
+
console.log(JSON.stringify(checkpoints, null, 2));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(formatCheckpointList(checkpoints));
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const info = await recover(vaultPath, {
|
|
55
90
|
clearFlag: options.clear,
|
|
56
91
|
verbose: options.verbose
|
|
57
92
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerResilienceCommands } from './register-resilience-commands.js';
|
|
4
|
+
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
recoverMock,
|
|
8
|
+
formatRecoveryInfoMock,
|
|
9
|
+
checkRecoveryStatusMock,
|
|
10
|
+
formatRecoveryCheckStatusMock,
|
|
11
|
+
listCheckpointsMock,
|
|
12
|
+
formatCheckpointListMock
|
|
13
|
+
} = vi.hoisted(() => ({
|
|
14
|
+
recoverMock: vi.fn(),
|
|
15
|
+
formatRecoveryInfoMock: vi.fn(),
|
|
16
|
+
checkRecoveryStatusMock: vi.fn(),
|
|
17
|
+
formatRecoveryCheckStatusMock: vi.fn(),
|
|
18
|
+
listCheckpointsMock: vi.fn(),
|
|
19
|
+
formatCheckpointListMock: vi.fn()
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../dist/commands/recover.js', () => ({
|
|
23
|
+
recover: recoverMock,
|
|
24
|
+
formatRecoveryInfo: formatRecoveryInfoMock,
|
|
25
|
+
checkRecoveryStatus: checkRecoveryStatusMock,
|
|
26
|
+
formatRecoveryCheckStatus: formatRecoveryCheckStatusMock,
|
|
27
|
+
listCheckpoints: listCheckpointsMock,
|
|
28
|
+
formatCheckpointList: formatCheckpointListMock
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
function buildProgram() {
|
|
32
|
+
const program = new Command();
|
|
33
|
+
registerResilienceCommands(program, {
|
|
34
|
+
chalk: chalkStub,
|
|
35
|
+
resolveVaultPath: (value) => value ?? '/vault'
|
|
36
|
+
});
|
|
37
|
+
return program;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runCommand(args) {
|
|
41
|
+
const program = buildProgram();
|
|
42
|
+
await program.parseAsync(args, { from: 'user' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
recoverMock.mockResolvedValue({ died: false });
|
|
48
|
+
formatRecoveryInfoMock.mockReturnValue('recover');
|
|
49
|
+
checkRecoveryStatusMock.mockResolvedValue({ died: false, deathTime: null, checkpoint: null });
|
|
50
|
+
formatRecoveryCheckStatusMock.mockReturnValue('check');
|
|
51
|
+
listCheckpointsMock.mockReturnValue([]);
|
|
52
|
+
formatCheckpointListMock.mockReturnValue('list');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('register-resilience-commands', () => {
|
|
56
|
+
it('routes recover --check through check helpers', async () => {
|
|
57
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
58
|
+
try {
|
|
59
|
+
await runCommand(['recover', '--check']);
|
|
60
|
+
expect(checkRecoveryStatusMock).toHaveBeenCalledWith('/vault');
|
|
61
|
+
expect(formatRecoveryCheckStatusMock).toHaveBeenCalled();
|
|
62
|
+
expect(recoverMock).not.toHaveBeenCalled();
|
|
63
|
+
expect(logSpy).toHaveBeenCalledWith('check');
|
|
64
|
+
} finally {
|
|
65
|
+
logSpy.mockRestore();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('routes recover --list through checkpoint listing helpers', async () => {
|
|
70
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
71
|
+
try {
|
|
72
|
+
await runCommand(['recover', '--list']);
|
|
73
|
+
expect(listCheckpointsMock).toHaveBeenCalledWith('/vault');
|
|
74
|
+
expect(formatCheckpointListMock).toHaveBeenCalled();
|
|
75
|
+
expect(recoverMock).not.toHaveBeenCalled();
|
|
76
|
+
expect(logSpy).toHaveBeenCalledWith('list');
|
|
77
|
+
} finally {
|
|
78
|
+
logSpy.mockRestore();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|