@dog_world/dak 0.1.0 → 0.1.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/dist/cli.js +0 -0
- package/package.json +2 -2
- package/dist/src/cli.js +0 -97
- package/dist/src/commands.js +0 -291
- package/dist/src/config.js +0 -65
- package/dist/src/constants.js +0 -26
- package/dist/src/linker.js +0 -132
- package/dist/src/paths.js +0 -147
- package/dist/src/resources.js +0 -86
- package/dist/src/state.js +0 -83
- package/dist/src/types.js +0 -5
- package/dist/tests/cli.test.js +0 -38
- package/dist/tests/commands.test.js +0 -218
- package/dist/tests/config-state-resources.test.js +0 -146
- package/dist/tests/e2e.test.js +0 -74
- package/dist/tests/linker.test.js +0 -234
- package/dist/tests/paths.test.js +0 -120
package/dist/src/paths.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/** 路径工具函数 */
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { realpath } from 'node:fs/promises';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
/**
|
|
6
|
-
* 将路径中的 ~ 展开为用户主目录。
|
|
7
|
-
* @param input 输入路径
|
|
8
|
-
* @param homeDir 用户主目录绝对路径
|
|
9
|
-
* @returns 展开后的绝对路径
|
|
10
|
-
*/
|
|
11
|
-
export function expandHome(input, homeDir) {
|
|
12
|
-
if (input === '~')
|
|
13
|
-
return homeDir;
|
|
14
|
-
if (input.startsWith('~/'))
|
|
15
|
-
return homeDir + input.slice(1);
|
|
16
|
-
return input;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* 将输入路径解析为绝对路径。
|
|
20
|
-
* 支持 ~ 展开、相对路径拼接、自动 normalize。
|
|
21
|
-
* @param input 输入路径(支持 ~ 前缀或相对路径)
|
|
22
|
-
* @param baseDir 相对路径的基准目录
|
|
23
|
-
* @param homeDir 用于 ~ 展开的主目录
|
|
24
|
-
* @returns 解析后的绝对路径
|
|
25
|
-
*/
|
|
26
|
-
export function toAbsolutePath(input, baseDir, homeDir) {
|
|
27
|
-
const expanded = expandHome(input, homeDir);
|
|
28
|
-
if (expanded.startsWith('/')) {
|
|
29
|
-
return resolve(expanded);
|
|
30
|
-
}
|
|
31
|
-
return resolve(baseDir, expanded);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* 判断名称是否为隐藏项(以 . 开头)。
|
|
35
|
-
* @param name 文件/目录名称
|
|
36
|
-
* @returns 是否隐藏
|
|
37
|
-
*/
|
|
38
|
-
export function isHiddenItem(name) {
|
|
39
|
-
const base = dirname(name) === '.' ? name : name.split('/').pop() ?? name;
|
|
40
|
-
return base.startsWith('.');
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* 校验资源项名称是否合法。
|
|
44
|
-
* @param name 资源项名称
|
|
45
|
-
* @throws 名称非法时抛出错误
|
|
46
|
-
*/
|
|
47
|
-
export function assertSafeItemName(name) {
|
|
48
|
-
const base = dirname(name) === '.' ? name : name.split('/').pop() ?? name;
|
|
49
|
-
// 先检查 . 和 ..(它们是合法 hidden item,但应报 "Invalid")
|
|
50
|
-
if (name === '.' || name === '..') {
|
|
51
|
-
throw new Error('Invalid resource item name');
|
|
52
|
-
}
|
|
53
|
-
if (isHiddenItem(base)) {
|
|
54
|
-
throw new Error('Hidden resource items are ignored');
|
|
55
|
-
}
|
|
56
|
-
if (name.includes('/') || name.includes('\\') || name.includes('.')) {
|
|
57
|
-
throw new Error('Invalid resource item name');
|
|
58
|
-
}
|
|
59
|
-
if (name === '..') {
|
|
60
|
-
throw new Error('Invalid resource item name');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* 判断 child 路径是否在 parent 路径内部(严格前缀匹配,防止边界欺骗)。
|
|
65
|
-
* @param child 子路径
|
|
66
|
-
* @param parent 父路径
|
|
67
|
-
* @returns 是否在内部
|
|
68
|
-
*/
|
|
69
|
-
export function isPathInside(child, parent) {
|
|
70
|
-
const normalizedChild = resolve(child);
|
|
71
|
-
let normalizedParent = resolve(parent);
|
|
72
|
-
if (!normalizedParent.endsWith('/')) {
|
|
73
|
-
normalizedParent += '/';
|
|
74
|
-
}
|
|
75
|
-
// 子路径在父目录内(严格前缀)
|
|
76
|
-
if (normalizedChild.startsWith(normalizedParent))
|
|
77
|
-
return true;
|
|
78
|
-
// 子路径等于父目录本身
|
|
79
|
-
if (normalizedChild === parent || normalizedChild + '/' === normalizedParent)
|
|
80
|
-
return true;
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* 获取路径的可比较真实路径。
|
|
85
|
-
* 尝试 fs.realpath,失败时 fallback 到 path.resolve。
|
|
86
|
-
* @param input 输入路径
|
|
87
|
-
* @returns 绝对路径
|
|
88
|
-
*/
|
|
89
|
-
export async function realComparablePath(input) {
|
|
90
|
-
try {
|
|
91
|
-
return await realpath(input);
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return resolve(input);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* 解析 symlink 的实际目标路径。
|
|
99
|
-
* @param linkPath 链接文件的实际路径
|
|
100
|
-
* @param rawTarget 链接原始 target(相对或绝对)
|
|
101
|
-
* @returns 解析后的绝对目标路径
|
|
102
|
-
*/
|
|
103
|
-
export async function resolveLinkTarget(linkPath, rawTarget) {
|
|
104
|
-
if (rawTarget.startsWith('/')) {
|
|
105
|
-
return resolve(rawTarget);
|
|
106
|
-
}
|
|
107
|
-
const realParent = await realParentJoined(dirname(linkPath));
|
|
108
|
-
const resolved = join(realParent, rawTarget);
|
|
109
|
-
return resolve(resolved);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* 获取路径的"真实父目录 + 剩余相对部分"拼接结果。
|
|
113
|
-
* 向上逐级查找最近存在的目录取 realpath,再拼接剩余部分。
|
|
114
|
-
* @param path 目标路径
|
|
115
|
-
* @returns 拼接后的绝对路径
|
|
116
|
-
*/
|
|
117
|
-
export async function realParentJoined(path) {
|
|
118
|
-
const dir = dirname(path);
|
|
119
|
-
const basename = path.split('/').pop() ?? '';
|
|
120
|
-
// 向上逐级查找最近存在的目录
|
|
121
|
-
let current = dir;
|
|
122
|
-
const segments = [];
|
|
123
|
-
let foundExisting = null;
|
|
124
|
-
let remainingSegments = [];
|
|
125
|
-
while (current !== segments.join('/')) {
|
|
126
|
-
if (existsSync(current)) {
|
|
127
|
-
foundExisting = current;
|
|
128
|
-
// 计算还需要拼接的部分
|
|
129
|
-
const dirSegments = dir.split('/').filter(Boolean);
|
|
130
|
-
const foundSegments = foundExisting.split('/').filter(Boolean);
|
|
131
|
-
remainingSegments = dirSegments.slice(foundSegments.length);
|
|
132
|
-
remainingSegments.push(basename);
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
segments.unshift(current.split('/').pop() ?? '');
|
|
136
|
-
const parent = dirname(current);
|
|
137
|
-
if (parent === current)
|
|
138
|
-
break;
|
|
139
|
-
current = parent;
|
|
140
|
-
}
|
|
141
|
-
if (foundExisting) {
|
|
142
|
-
const realAncestor = await realpath(foundExisting);
|
|
143
|
-
const remaining = remainingSegments.join('/');
|
|
144
|
-
return join(realAncestor, remaining);
|
|
145
|
-
}
|
|
146
|
-
return resolve(path);
|
|
147
|
-
}
|
package/dist/src/resources.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/** 资源扫描与格式化 */
|
|
2
|
-
import { readdir, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { RESOURCE_TYPES } from './types.js';
|
|
6
|
-
import { assertSafeItemName, isHiddenItem } from './paths.js';
|
|
7
|
-
/**
|
|
8
|
-
* 确保 store 目录和三类资源目录存在。
|
|
9
|
-
*/
|
|
10
|
-
export async function ensureStoreLayout(storePath, _config) {
|
|
11
|
-
await mkdir(storePath, { recursive: true });
|
|
12
|
-
for (const type of RESOURCE_TYPES) {
|
|
13
|
-
await mkdir(join(storePath, type), { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* 扫描 store 中的资源目录(一级子项)。
|
|
18
|
-
*/
|
|
19
|
-
export async function scanResources(storePath, _config) {
|
|
20
|
-
const result = {
|
|
21
|
-
skills: [],
|
|
22
|
-
hooks: [],
|
|
23
|
-
agents: [],
|
|
24
|
-
};
|
|
25
|
-
for (const type of RESOURCE_TYPES) {
|
|
26
|
-
const dir = join(storePath, type);
|
|
27
|
-
if (!existsSync(dir))
|
|
28
|
-
continue;
|
|
29
|
-
try {
|
|
30
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
31
|
-
const items = [];
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
const name = entry.name;
|
|
34
|
-
if (isHiddenItem(name))
|
|
35
|
-
continue;
|
|
36
|
-
try {
|
|
37
|
-
assertSafeItemName(name);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const fullPath = join(dir, name);
|
|
43
|
-
let kind;
|
|
44
|
-
if (entry.isSymbolicLink())
|
|
45
|
-
kind = 'symlink';
|
|
46
|
-
else if (entry.isDirectory())
|
|
47
|
-
kind = 'directory';
|
|
48
|
-
else
|
|
49
|
-
kind = 'file';
|
|
50
|
-
items.push({ name, kind, path: fullPath });
|
|
51
|
-
}
|
|
52
|
-
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
-
result[type] = items;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// ignore unreadable dir
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* 格式化资源列表输出(供 `dak list` 使用)。
|
|
63
|
-
* 输出稳定,支持 snapshot 测试。
|
|
64
|
-
*/
|
|
65
|
-
export function formatResourceList(resources) {
|
|
66
|
-
const lines = [];
|
|
67
|
-
const labels = {
|
|
68
|
-
skills: 'Skills:',
|
|
69
|
-
hooks: 'Hooks:',
|
|
70
|
-
agents: 'Agents:',
|
|
71
|
-
};
|
|
72
|
-
for (const type of RESOURCE_TYPES) {
|
|
73
|
-
lines.push(labels[type]);
|
|
74
|
-
const items = resources[type];
|
|
75
|
-
if (items.length === 0) {
|
|
76
|
-
lines.push(' (empty)');
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
for (const item of items) {
|
|
80
|
-
const kind = item.kind === 'directory' ? ' [dir]' : item.kind === 'symlink' ? ' [link]' : '';
|
|
81
|
-
lines.push(` ${item.name}${kind}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return lines.join('\n');
|
|
86
|
-
}
|
package/dist/src/state.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/** 状态读写 */
|
|
2
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { STATE_FILE } from './constants.js';
|
|
6
|
-
/** 空状态 */
|
|
7
|
-
const EMPTY_STATE = { version: 1, targets: {} };
|
|
8
|
-
/**
|
|
9
|
-
* 读取状态文件,不存在时返回空状态。
|
|
10
|
-
*/
|
|
11
|
-
export async function readState(storePath) {
|
|
12
|
-
const statePath = join(storePath, STATE_FILE);
|
|
13
|
-
if (!existsSync(statePath))
|
|
14
|
-
return EMPTY_STATE;
|
|
15
|
-
const raw = await readFile(statePath, 'utf-8');
|
|
16
|
-
const state = JSON.parse(raw);
|
|
17
|
-
if (state.version !== 1) {
|
|
18
|
-
throw new Error(`unsupported state version: ${state.version}`);
|
|
19
|
-
}
|
|
20
|
-
return state;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 写入状态文件。
|
|
24
|
-
*/
|
|
25
|
-
export async function writeState(storePath, state) {
|
|
26
|
-
const statePath = join(storePath, STATE_FILE);
|
|
27
|
-
await mkdir(storePath, { recursive: true });
|
|
28
|
-
await writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* 确保状态文件存在(不存在则创建空状态)。
|
|
32
|
-
*/
|
|
33
|
-
export async function writeStateIfMissing(storePath) {
|
|
34
|
-
const statePath = join(storePath, STATE_FILE);
|
|
35
|
-
if (existsSync(statePath))
|
|
36
|
-
return;
|
|
37
|
-
await writeState(storePath, EMPTY_STATE);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* 确保目标 state 存在(skills/hooks/agents 三个 map)。
|
|
41
|
-
*/
|
|
42
|
-
export function ensureTargetState(state, targetName) {
|
|
43
|
-
if (!state.targets[targetName]) {
|
|
44
|
-
state.targets[targetName] = {
|
|
45
|
-
skills: {},
|
|
46
|
-
hooks: {},
|
|
47
|
-
agents: {},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return state;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* 替换某个 target/resource 的所有 records。
|
|
54
|
-
*/
|
|
55
|
-
export function replaceTargetResourceState(state, targetName, resourceType, records) {
|
|
56
|
-
ensureTargetState(state, targetName);
|
|
57
|
-
state.targets[targetName][resourceType] = records;
|
|
58
|
-
return state;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* 新增或更新单条 record。
|
|
62
|
-
*/
|
|
63
|
-
export function upsertLinkRecord(state, targetName, resourceType, itemName, record) {
|
|
64
|
-
ensureTargetState(state, targetName);
|
|
65
|
-
state.targets[targetName][resourceType][itemName] = record;
|
|
66
|
-
return state;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* 删除单条 record,返回是否删除成功(存在才删)。
|
|
70
|
-
*/
|
|
71
|
-
export function removeLinkRecord(state, targetName, resourceType, itemName) {
|
|
72
|
-
if (state.targets[targetName]?.[resourceType]) {
|
|
73
|
-
delete state.targets[targetName][resourceType][itemName];
|
|
74
|
-
}
|
|
75
|
-
return state;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* 从 state 中移除整个 target 的 state。
|
|
79
|
-
*/
|
|
80
|
-
export function removeTargetState(state, targetName) {
|
|
81
|
-
delete state.targets[targetName];
|
|
82
|
-
return state;
|
|
83
|
-
}
|
package/dist/src/types.js
DELETED
package/dist/tests/cli.test.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseArgv, main } from '../src/cli.js';
|
|
3
|
-
describe('parseArgv', () => {
|
|
4
|
-
it('link codex --store /tmp/store --on-conflict backup', () => {
|
|
5
|
-
const result = parseArgv(['link', 'codex', '--store', '/tmp/store', '--on-conflict', 'backup']);
|
|
6
|
-
expect(result.command).toBe('link');
|
|
7
|
-
expect(result.target).toBe('codex');
|
|
8
|
-
expect(result.store).toBe('/tmp/store');
|
|
9
|
-
expect(result.onConflict).toBe('backup');
|
|
10
|
-
});
|
|
11
|
-
it('非法 conflict policy 报错', () => {
|
|
12
|
-
expect(() => parseArgv(['link', 'codex', '--on-conflict', 'invalid']))
|
|
13
|
-
.toThrow('Invalid conflict policy');
|
|
14
|
-
});
|
|
15
|
-
it('status 无 target', () => {
|
|
16
|
-
const result = parseArgv(['status', '--store', '/tmp/store']);
|
|
17
|
-
expect(result.command).toBe('status');
|
|
18
|
-
expect(result.target).toBeUndefined();
|
|
19
|
-
});
|
|
20
|
-
it('link 缺 target 报错', () => {
|
|
21
|
-
expect(() => parseArgv(['link']))
|
|
22
|
-
.toThrow('target is required');
|
|
23
|
-
});
|
|
24
|
-
it('unlink 缺 target 报错', () => {
|
|
25
|
-
expect(() => parseArgv(['unlink']))
|
|
26
|
-
.toThrow('target is required');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
describe('main', () => {
|
|
30
|
-
it('未知命令报错', async () => {
|
|
31
|
-
const code = await main(['unknown']);
|
|
32
|
-
expect(code).toBe(1);
|
|
33
|
-
});
|
|
34
|
-
it('link 缺 target 返回 1', async () => {
|
|
35
|
-
const code = await main(['link']);
|
|
36
|
-
expect(code).toBe(1);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { runInit, runList, runLink, runStatus, runUpdate, runUnlink, } from '../src/commands.js';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
const TMP_PREFIX = 'dak-cmd-';
|
|
7
|
-
const FIXED_NOW = new Date('2025-06-28T10:00:00.000Z');
|
|
8
|
-
function makeStore(tmp) {
|
|
9
|
-
const store = join(tmp, 'store');
|
|
10
|
-
mkdirSync(store, { recursive: true });
|
|
11
|
-
return store;
|
|
12
|
-
}
|
|
13
|
-
function makeTarget(tmp, name) {
|
|
14
|
-
const target = join(tmp, name);
|
|
15
|
-
mkdirSync(target, { recursive: true });
|
|
16
|
-
return target;
|
|
17
|
-
}
|
|
18
|
-
function makeHome(tmp) {
|
|
19
|
-
const home = join(tmp, 'home');
|
|
20
|
-
mkdirSync(join(home, '.codex'), { recursive: true });
|
|
21
|
-
mkdirSync(join(home, '.claude'), { recursive: true });
|
|
22
|
-
return home;
|
|
23
|
-
}
|
|
24
|
-
describe('runInit', () => {
|
|
25
|
-
let tmp;
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
|
|
28
|
-
});
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
rmSync(tmp, { recursive: true });
|
|
31
|
-
});
|
|
32
|
-
it('创建 store/config/state/resource dirs', async () => {
|
|
33
|
-
const storePath = join(tmp, 'init-store');
|
|
34
|
-
const output = await runInit({ store: storePath, homeDir: '/tmp/home' });
|
|
35
|
-
expect(output).toContain('Initialized');
|
|
36
|
-
// config 存在
|
|
37
|
-
const config = JSON.parse(readFileSync(join(storePath, 'dak.config.json'), 'utf-8'));
|
|
38
|
-
expect(config.store).toBe(storePath);
|
|
39
|
-
// state 存在
|
|
40
|
-
const state = JSON.parse(readFileSync(join(storePath, '.dak-state.json'), 'utf-8'));
|
|
41
|
-
expect(state.version).toBe(1);
|
|
42
|
-
// resource dirs 存在
|
|
43
|
-
for (const type of ['skills', 'hooks', 'agents']) {
|
|
44
|
-
expect(require('node:fs').existsSync(join(storePath, type))).toBe(true);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('runList', () => {
|
|
49
|
-
let tmp;
|
|
50
|
-
let store;
|
|
51
|
-
let home;
|
|
52
|
-
beforeEach(async () => {
|
|
53
|
-
tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
|
|
54
|
-
store = makeStore(tmp);
|
|
55
|
-
home = makeHome(tmp);
|
|
56
|
-
await runInit({ store, homeDir: home });
|
|
57
|
-
mkdirSync(join(store, 'skills'), { recursive: true });
|
|
58
|
-
mkdirSync(join(store, 'skills', 'my-skill'));
|
|
59
|
-
mkdirSync(join(store, 'hooks'), { recursive: true });
|
|
60
|
-
mkdirSync(join(store, 'hooks', 'my-hook'));
|
|
61
|
-
mkdirSync(join(store, 'agents'), { recursive: true });
|
|
62
|
-
});
|
|
63
|
-
afterEach(() => {
|
|
64
|
-
rmSync(tmp, { recursive: true });
|
|
65
|
-
});
|
|
66
|
-
it('输出三类资源', async () => {
|
|
67
|
-
const output = await runList({ store, homeDir: home });
|
|
68
|
-
expect(output).toContain('Skills:');
|
|
69
|
-
expect(output).toContain('Hooks:');
|
|
70
|
-
expect(output).toContain('Agents:');
|
|
71
|
-
expect(output).toContain('my-skill');
|
|
72
|
-
expect(output).toContain('my-hook');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
describe('runLink', () => {
|
|
76
|
-
let tmp;
|
|
77
|
-
let store;
|
|
78
|
-
let home;
|
|
79
|
-
let codex;
|
|
80
|
-
beforeEach(async () => {
|
|
81
|
-
tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
|
|
82
|
-
store = makeStore(tmp);
|
|
83
|
-
home = makeHome(tmp);
|
|
84
|
-
codex = join(home, '.codex'); // 使用 home 下的 .codex
|
|
85
|
-
mkdirSync(join(codex, 'skills'), { recursive: true });
|
|
86
|
-
mkdirSync(join(codex, 'agents'), { recursive: true });
|
|
87
|
-
// 初始化 config(使用真实存在的 homeDir)
|
|
88
|
-
await runInit({ store, homeDir: home });
|
|
89
|
-
// 写入 store 资源
|
|
90
|
-
writeFileSync(join(store, 'skills', 'foo'), 'x');
|
|
91
|
-
mkdirSync(join(store, 'hooks', 'my-hook'));
|
|
92
|
-
writeFileSync(join(store, 'agents', 'bar'), 'x');
|
|
93
|
-
});
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
rmSync(tmp, { recursive: true });
|
|
96
|
-
});
|
|
97
|
-
it('runLink(codex) 为三类资源创建 symlink 并写 state', async () => {
|
|
98
|
-
const output = await runLink('codex', { store, homeDir: home, now: FIXED_NOW });
|
|
99
|
-
expect(output).toContain('created');
|
|
100
|
-
// symlink 已创建
|
|
101
|
-
expect(readFileSync(join(codex, 'skills', 'foo'), 'utf-8')).toBe('x');
|
|
102
|
-
expect(readFileSync(join(codex, 'agents', 'bar'), 'utf-8')).toBe('x');
|
|
103
|
-
// state 存在
|
|
104
|
-
const state = JSON.parse(readFileSync(join(store, '.dak-state.json'), 'utf-8'));
|
|
105
|
-
expect(state.targets['codex'].skills['foo']).toBeDefined();
|
|
106
|
-
});
|
|
107
|
-
it('runLink(all) 支持配置里的所有 targets', async () => {
|
|
108
|
-
const claude = join(home, '.claude');
|
|
109
|
-
mkdirSync(join(claude, 'skills'), { recursive: true });
|
|
110
|
-
mkdirSync(join(claude, 'agents'), { recursive: true });
|
|
111
|
-
const output = await runLink('all', { store, homeDir: home, now: FIXED_NOW });
|
|
112
|
-
expect(output).toContain('created');
|
|
113
|
-
expect(readFileSync(join(claude, 'skills', 'foo'), 'utf-8')).toBe('x');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('runUpdate', () => {
|
|
117
|
-
let tmp;
|
|
118
|
-
let store;
|
|
119
|
-
let home;
|
|
120
|
-
let codex;
|
|
121
|
-
beforeEach(async () => {
|
|
122
|
-
tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
|
|
123
|
-
store = makeStore(tmp);
|
|
124
|
-
home = makeHome(tmp);
|
|
125
|
-
codex = join(home, '.codex');
|
|
126
|
-
mkdirSync(join(codex, 'skills'), { recursive: true });
|
|
127
|
-
await runInit({ store, homeDir: home });
|
|
128
|
-
// 初始资源
|
|
129
|
-
writeFileSync(join(store, 'skills', 'foo'), 'x');
|
|
130
|
-
});
|
|
131
|
-
afterEach(() => {
|
|
132
|
-
rmSync(tmp, { recursive: true });
|
|
133
|
-
});
|
|
134
|
-
it('store 新增 item 后补 symlink', async () => {
|
|
135
|
-
await runLink('codex', { store, homeDir: '/home/test', now: FIXED_NOW });
|
|
136
|
-
// store 新增 bar
|
|
137
|
-
writeFileSync(join(store, 'skills', 'bar'), 'y');
|
|
138
|
-
const output = await runUpdate({ store, homeDir: '/home/test', now: FIXED_NOW });
|
|
139
|
-
expect(output).toContain('created');
|
|
140
|
-
expect(readFileSync(join(codex, 'skills', 'bar'), 'utf-8')).toBe('y');
|
|
141
|
-
});
|
|
142
|
-
it('store 删除 item 后安全删除旧 symlink', async () => {
|
|
143
|
-
await runLink('codex', { store, homeDir: home, now: FIXED_NOW });
|
|
144
|
-
// 删除 store 中的 foo
|
|
145
|
-
rmSync(join(store, 'skills', 'foo'));
|
|
146
|
-
const output = await runUpdate({ store, homeDir: home, now: FIXED_NOW });
|
|
147
|
-
expect(output).toContain('deleted');
|
|
148
|
-
// target symlink 已删除,读取应抛错
|
|
149
|
-
expect(() => readFileSync(join(codex, 'skills', 'foo'))).toThrow();
|
|
150
|
-
});
|
|
151
|
-
it('只 link 过 codex 时不得刷新 claude-code', async () => {
|
|
152
|
-
const claude = join(home, '.claude');
|
|
153
|
-
mkdirSync(join(claude, 'skills'), { recursive: true });
|
|
154
|
-
// 只 link codex
|
|
155
|
-
await runLink('codex', { store, homeDir: home, now: FIXED_NOW });
|
|
156
|
-
// update 应只更新 codex
|
|
157
|
-
const output = await runUpdate({ store, homeDir: home, now: FIXED_NOW });
|
|
158
|
-
expect(output).not.toContain('claude-code');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
describe('runStatus', () => {
|
|
162
|
-
let tmp;
|
|
163
|
-
let store;
|
|
164
|
-
let home;
|
|
165
|
-
let codex;
|
|
166
|
-
beforeEach(async () => {
|
|
167
|
-
tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
|
|
168
|
-
store = makeStore(tmp);
|
|
169
|
-
home = makeHome(tmp);
|
|
170
|
-
codex = join(home, '.codex');
|
|
171
|
-
mkdirSync(join(codex, 'skills'), { recursive: true });
|
|
172
|
-
await runInit({ store, homeDir: home });
|
|
173
|
-
writeFileSync(join(store, 'skills', 'foo'), 'x');
|
|
174
|
-
writeFileSync(join(store, 'skills', 'stale-item'), 'x');
|
|
175
|
-
});
|
|
176
|
-
afterEach(() => {
|
|
177
|
-
rmSync(tmp, { recursive: true });
|
|
178
|
-
});
|
|
179
|
-
it('输出 linked/missing/stale', async () => {
|
|
180
|
-
// link foo
|
|
181
|
-
await runLink('codex', { store, homeDir: home, now: FIXED_NOW });
|
|
182
|
-
// 创建 missing item(store 中存在但未 link)
|
|
183
|
-
mkdirSync(join(store, 'skills', 'missing-item'));
|
|
184
|
-
// stale:store 已删但 state 有记录
|
|
185
|
-
rmSync(join(store, 'skills', 'stale-item'));
|
|
186
|
-
const output = await runStatus({ store, homeDir: home });
|
|
187
|
-
expect(output).toContain('linked');
|
|
188
|
-
expect(output).toContain('missing');
|
|
189
|
-
expect(output).toContain('stale');
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
describe('runUnlink', () => {
|
|
193
|
-
let tmp;
|
|
194
|
-
let store;
|
|
195
|
-
let home;
|
|
196
|
-
let codex;
|
|
197
|
-
beforeEach(async () => {
|
|
198
|
-
tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
|
|
199
|
-
store = makeStore(tmp);
|
|
200
|
-
home = makeHome(tmp);
|
|
201
|
-
codex = join(home, '.codex');
|
|
202
|
-
mkdirSync(join(codex, 'skills'), { recursive: true });
|
|
203
|
-
await runInit({ store, homeDir: home });
|
|
204
|
-
writeFileSync(join(store, 'skills', 'foo'), 'x');
|
|
205
|
-
});
|
|
206
|
-
afterEach(() => {
|
|
207
|
-
rmSync(tmp, { recursive: true });
|
|
208
|
-
});
|
|
209
|
-
it('只删除 dak 管理的 symlink,不删除 store 资源', async () => {
|
|
210
|
-
await runLink('codex', { store, homeDir: home, now: FIXED_NOW });
|
|
211
|
-
const output = await runUnlink('codex', { store, homeDir: home, now: FIXED_NOW });
|
|
212
|
-
expect(output).toContain('deleted');
|
|
213
|
-
// store 源文件仍在
|
|
214
|
-
expect(readFileSync(join(store, 'skills', 'foo'), 'utf-8')).toBe('x');
|
|
215
|
-
// target 已删除
|
|
216
|
-
expect(() => readFileSync(join(codex, 'skills', 'foo'))).toThrow();
|
|
217
|
-
});
|
|
218
|
-
});
|