@dog_world/dak 0.1.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.
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ const DAK_CLI = join(process.cwd(), 'dist', 'cli.js');
7
+ const TMP_PREFIX = 'dak-e2e-';
8
+ function tmpRoot() {
9
+ return mkdtempSync(join(tmpdir(), TMP_PREFIX));
10
+ }
11
+ describe('dak e2e', () => {
12
+ let root;
13
+ let home;
14
+ let store;
15
+ beforeAll(() => {
16
+ root = tmpRoot();
17
+ home = join(root, 'home');
18
+ store = join(root, 'dak-store');
19
+ mkdirSync(store, { recursive: true });
20
+ });
21
+ afterAll(() => {
22
+ rmSync(root, { recursive: true, force: true });
23
+ });
24
+ function exec(args) {
25
+ return execFileSync(process.execPath, [DAK_CLI, ...args], {
26
+ env: { ...process.env, HOME: home },
27
+ encoding: 'utf-8',
28
+ });
29
+ }
30
+ it('init --store', () => {
31
+ const out = exec(['init', '--store', store]);
32
+ expect(out).toContain('Initialized');
33
+ expect(exec(['list', '--store', store])).toContain('Skills:');
34
+ });
35
+ it('写入 skills/foo 后 link codex', () => {
36
+ writeFileSync(join(store, 'skills', 'foo'), 'hello');
37
+ const out = exec(['link', 'codex', '--store', store, '--on-conflict', 'skip']);
38
+ expect(out).toContain('created');
39
+ const linkPath = join(home, '.codex', 'skills', 'foo');
40
+ expect(execFileSync('readlink', [linkPath], { encoding: 'utf-8' }).trim()).toBe(join(store, 'skills', 'foo'));
41
+ });
42
+ it('删除 foo 新增 bar 后 update', () => {
43
+ rmSync(join(store, 'skills', 'foo'));
44
+ writeFileSync(join(store, 'skills', 'bar'), 'world');
45
+ const out = exec(['update', '--store', store]);
46
+ expect(out).toContain('deleted');
47
+ expect(out).toContain('created');
48
+ // bar 已链接
49
+ const linkPath = join(home, '.codex', 'skills', 'bar');
50
+ expect(readFileSync(linkPath, 'utf-8')).toBe('world');
51
+ });
52
+ it('unlink codex', () => {
53
+ const out = exec(['unlink', 'codex', '--store', store]);
54
+ expect(out).toContain('deleted');
55
+ // 源文件仍在
56
+ expect(readFileSync(join(store, 'skills', 'bar'), 'utf-8')).toBe('world');
57
+ });
58
+ it('--on-conflict backup', () => {
59
+ // 先创建真实文件
60
+ mkdirSync(join(home, '.codex', 'skills'), { recursive: true });
61
+ writeFileSync(join(home, '.codex', 'skills', 'foo'), 'real-file');
62
+ writeFileSync(join(store, 'skills', 'foo'), 'store-file');
63
+ const out = exec(['link', 'codex', '--store', store, '--on-conflict', 'backup']);
64
+ expect(out).toContain('backed-up');
65
+ });
66
+ it('非交互环境未传 --on-conflict 且目标已有真实文件,默认 skip', () => {
67
+ writeFileSync(join(home, '.codex', 'skills', 'baz'), 'existing');
68
+ writeFileSync(join(store, 'skills', 'baz'), 'new');
69
+ const out = exec(['link', 'codex', '--store', store]);
70
+ expect(out).toContain('conflict');
71
+ // 真实文件未被覆盖
72
+ expect(readFileSync(join(home, '.codex', 'skills', 'baz'), 'utf-8')).toBe('existing');
73
+ });
74
+ });
@@ -0,0 +1,234 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { classifyTarget, linkItem, safeDeleteManagedLink } from '../src/linker.js';
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ const TMP_PREFIX = 'dak-linker-';
7
+ function makeStore(tmp) {
8
+ const store = join(tmp, 'store');
9
+ mkdirSync(join(store, 'skills'), { recursive: true });
10
+ mkdirSync(join(store, 'hooks'), { recursive: true });
11
+ mkdirSync(join(store, 'agents'), { recursive: true });
12
+ return store;
13
+ }
14
+ function makeTarget(tmp, name) {
15
+ const target = join(tmp, name);
16
+ mkdirSync(target, { recursive: true });
17
+ return target;
18
+ }
19
+ describe('classifyTarget', () => {
20
+ let tmp;
21
+ beforeEach(() => {
22
+ tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
23
+ });
24
+ afterEach(() => {
25
+ rmSync(tmp, { recursive: true });
26
+ });
27
+ it('missing 当 target 不存在', async () => {
28
+ const result = await classifyTarget(join(tmp, 'no-such-dir', 'item'), '/abs/source');
29
+ expect(result).toBe('missing');
30
+ });
31
+ it('linked 当 symlink 指向 expected source', async () => {
32
+ const source = join(tmp, 'store', 'skills', 'foo');
33
+ mkdirSync(join(tmp, 'store', 'skills'), { recursive: true });
34
+ writeFileSync(source, 'x');
35
+ const targetDir = join(tmp, 'codex', 'skills');
36
+ mkdirSync(targetDir, { recursive: true });
37
+ const targetPath = join(targetDir, 'foo');
38
+ symlinkSync(source, targetPath);
39
+ const result = await classifyTarget(targetPath, source);
40
+ expect(result).toBe('linked');
41
+ });
42
+ it('conflict 当 target 是真实文件', async () => {
43
+ const targetPath = join(tmp, 'real-file');
44
+ writeFileSync(targetPath, 'x');
45
+ const result = await classifyTarget(targetPath, '/abs/source');
46
+ expect(result).toBe('conflict');
47
+ });
48
+ it('broken 当 symlink 指向不存在目标', async () => {
49
+ const targetPath = join(tmp, 'broken-link');
50
+ symlinkSync('/no/such/target', targetPath);
51
+ const result = await classifyTarget(targetPath, '/abs/source');
52
+ expect(result).toBe('broken');
53
+ });
54
+ });
55
+ describe('linkItem', () => {
56
+ let tmp;
57
+ let store;
58
+ let targetRoot;
59
+ beforeEach(() => {
60
+ tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
61
+ store = makeStore(tmp);
62
+ targetRoot = makeTarget(tmp, 'codex');
63
+ mkdirSync(join(targetRoot, 'skills'), { recursive: true });
64
+ writeFileSync(join(store, 'skills', 'foo'), 'x');
65
+ });
66
+ afterEach(() => {
67
+ rmSync(tmp, { recursive: true });
68
+ });
69
+ const now = new Date('2025-01-01T00:00:00.000Z');
70
+ it('missing 时创建 symlink', async () => {
71
+ const outcome = await linkItem({
72
+ sourcePath: join(store, 'skills', 'foo'),
73
+ targetPath: join(targetRoot, 'skills', 'foo'),
74
+ resourceType: 'skills',
75
+ itemName: 'foo',
76
+ storePath: store,
77
+ policy: 'skip',
78
+ now,
79
+ });
80
+ expect(outcome.status).toBe('created');
81
+ expect(readFileSync(join(targetRoot, 'skills', 'foo'), 'utf-8')).toBe('x');
82
+ });
83
+ it('linked 时直接返回', async () => {
84
+ const targetPath = join(targetRoot, 'skills', 'foo');
85
+ symlinkSync(join(store, 'skills', 'foo'), targetPath);
86
+ const outcome = await linkItem({
87
+ sourcePath: join(store, 'skills', 'foo'),
88
+ targetPath,
89
+ resourceType: 'skills',
90
+ itemName: 'foo',
91
+ storePath: store,
92
+ policy: 'skip',
93
+ now,
94
+ });
95
+ expect(outcome.status).toBe('linked');
96
+ });
97
+ it('conflict 时 skip 返回 conflict', async () => {
98
+ writeFileSync(join(targetRoot, 'skills', 'foo'), 'old');
99
+ const outcome = await linkItem({
100
+ sourcePath: join(store, 'skills', 'foo'),
101
+ targetPath: join(targetRoot, 'skills', 'foo'),
102
+ resourceType: 'skills',
103
+ itemName: 'foo',
104
+ storePath: store,
105
+ policy: 'skip',
106
+ now,
107
+ });
108
+ expect(outcome.status).toBe('conflict');
109
+ expect(readFileSync(join(targetRoot, 'skills', 'foo'), 'utf-8')).toBe('old');
110
+ });
111
+ it('backup 移动旧内容后创建 symlink', async () => {
112
+ writeFileSync(join(targetRoot, 'skills', 'foo'), 'old');
113
+ const outcome = await linkItem({
114
+ sourcePath: join(store, 'skills', 'foo'),
115
+ targetPath: join(targetRoot, 'skills', 'foo'),
116
+ resourceType: 'skills',
117
+ itemName: 'foo',
118
+ storePath: store,
119
+ policy: 'backup',
120
+ now,
121
+ });
122
+ expect(outcome.status).toBe('backed-up');
123
+ // symlink 已创建
124
+ const stat = await import('node:fs/promises').then(m => m.stat(join(targetRoot, 'skills', 'foo')));
125
+ expect(stat).toBeDefined();
126
+ // 备份存在
127
+ const ts = now.toISOString().replace(/[:.]/g, '-');
128
+ const backupPath = join(targetRoot, '.dak-backup', ts, 'skills', 'foo');
129
+ expect(readFileSync(backupPath, 'utf-8')).toBe('old');
130
+ });
131
+ it('overwrite 删除旧内容后创建 symlink', async () => {
132
+ writeFileSync(join(targetRoot, 'skills', 'foo'), 'old');
133
+ const outcome = await linkItem({
134
+ sourcePath: join(store, 'skills', 'foo'),
135
+ targetPath: join(targetRoot, 'skills', 'foo'),
136
+ resourceType: 'skills',
137
+ itemName: 'foo',
138
+ storePath: store,
139
+ policy: 'overwrite',
140
+ now,
141
+ });
142
+ expect(outcome.status).toBe('overwritten');
143
+ expect(readFileSync(join(targetRoot, 'skills', 'foo'), 'utf-8')).toBe('x');
144
+ });
145
+ it('broken symlink 视为 conflict;overwrite 可替换', async () => {
146
+ const targetPath = join(targetRoot, 'skills', 'foo');
147
+ symlinkSync('/no/target', targetPath);
148
+ const outcome = await linkItem({
149
+ sourcePath: join(store, 'skills', 'foo'),
150
+ targetPath,
151
+ resourceType: 'skills',
152
+ itemName: 'foo',
153
+ storePath: store,
154
+ policy: 'overwrite',
155
+ now,
156
+ });
157
+ expect(outcome.status).toBe('overwritten');
158
+ expect(readFileSync(targetPath, 'utf-8')).toBe('x');
159
+ });
160
+ });
161
+ describe('safeDeleteManagedLink', () => {
162
+ let tmp;
163
+ let store;
164
+ beforeEach(() => {
165
+ tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
166
+ store = makeStore(tmp);
167
+ });
168
+ afterEach(() => {
169
+ rmSync(tmp, { recursive: true });
170
+ });
171
+ it('删除正确的 symlink 返回 deleted', async () => {
172
+ const source = join(store, 'skills', 'foo');
173
+ writeFileSync(source, 'x');
174
+ const targetDir = join(tmp, 'codex', 'skills');
175
+ mkdirSync(targetDir, { recursive: true });
176
+ const targetPath = join(targetDir, 'foo');
177
+ symlinkSync(source, targetPath);
178
+ const record = { source, target: targetPath, linkedAt: new Date().toISOString() };
179
+ const result = await safeDeleteManagedLink(record, store);
180
+ expect(result).toBe('deleted');
181
+ });
182
+ it('target 不存在返回 missing', async () => {
183
+ const record = {
184
+ source: join(store, 'skills', 'foo'),
185
+ target: join(tmp, 'codex', 'skills', 'foo'),
186
+ linkedAt: new Date().toISOString(),
187
+ };
188
+ const result = await safeDeleteManagedLink(record, store);
189
+ expect(result).toBe('missing');
190
+ });
191
+ it('真实文件不删除,返回 conflict', async () => {
192
+ const targetDir = join(tmp, 'codex', 'skills');
193
+ mkdirSync(targetDir, { recursive: true });
194
+ const targetPath = join(targetDir, 'foo');
195
+ writeFileSync(targetPath, 'x');
196
+ const record = {
197
+ source: join(store, 'skills', 'foo'),
198
+ target: targetPath,
199
+ linkedAt: new Date().toISOString(),
200
+ };
201
+ const result = await safeDeleteManagedLink(record, store);
202
+ expect(result).toBe('conflict');
203
+ expect(require('node:fs').existsSync(targetPath)).toBe(true);
204
+ });
205
+ it('指向其他 store 的 symlink 不删除,返回 conflict', async () => {
206
+ const otherStore = join(tmp, 'other-store');
207
+ mkdirSync(join(otherStore, 'skills'), { recursive: true });
208
+ const source = join(otherStore, 'skills', 'foo');
209
+ writeFileSync(source, 'x');
210
+ const targetDir = join(tmp, 'codex', 'skills');
211
+ mkdirSync(targetDir, { recursive: true });
212
+ const targetPath = join(targetDir, 'foo');
213
+ symlinkSync(source, targetPath);
214
+ const record = { source, target: targetPath, linkedAt: new Date().toISOString() };
215
+ const result = await safeDeleteManagedLink(record, store);
216
+ expect(result).toBe('conflict');
217
+ });
218
+ it('symlink 改指向同一 store 另一个 item,返回 conflict', async () => {
219
+ const source1 = join(store, 'skills', 'foo');
220
+ const source2 = join(store, 'skills', 'bar');
221
+ writeFileSync(source1, 'x');
222
+ writeFileSync(source2, 'x');
223
+ const targetDir = join(tmp, 'codex', 'skills');
224
+ mkdirSync(targetDir, { recursive: true });
225
+ const targetPath = join(targetDir, 'foo');
226
+ symlinkSync(source1, targetPath);
227
+ // 用户改成指向 bar
228
+ rmSync(targetPath);
229
+ symlinkSync(source2, targetPath);
230
+ const record = { source: source1, target: targetPath, linkedAt: new Date().toISOString() };
231
+ const result = await safeDeleteManagedLink(record, store);
232
+ expect(result).toBe('conflict');
233
+ });
234
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { expandHome, toAbsolutePath, isHiddenItem, assertSafeItemName, isPathInside, realComparablePath, resolveLinkTarget, realParentJoined, } from '../src/paths.js';
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ describe('expandHome', () => {
7
+ it('展开 ~ 为指定 home', () => {
8
+ expect(expandHome('~/foo', '/home/test')).toBe('/home/test/foo');
9
+ });
10
+ it('~ 本身展开为 home', () => {
11
+ expect(expandHome('~', '/home/test')).toBe('/home/test');
12
+ });
13
+ it('非 ~ 路径原样返回', () => {
14
+ expect(expandHome('/abs/path', '/home/test')).toBe('/abs/path');
15
+ });
16
+ });
17
+ describe('toAbsolutePath', () => {
18
+ it('相对路径按 baseDir 解析', () => {
19
+ expect(toAbsolutePath('foo/bar', '/base', '/home')).toBe('/base/foo/bar');
20
+ });
21
+ it('~ 路径展开后解析', () => {
22
+ expect(toAbsolutePath('~/.config', '/base', '/home')).toBe('/home/.config');
23
+ });
24
+ it('绝对路径 resolve', () => {
25
+ expect(toAbsolutePath('/foo/../bar', '/base', '/home')).toBe('/bar');
26
+ });
27
+ });
28
+ describe('isHiddenItem', () => {
29
+ it('.hidden 返回 true', () => {
30
+ expect(isHiddenItem('.hidden')).toBe(true);
31
+ });
32
+ it('.foo 返回 true', () => {
33
+ expect(isHiddenItem('.foo')).toBe(true);
34
+ });
35
+ it('foo 返回 false', () => {
36
+ expect(isHiddenItem('foo')).toBe(false);
37
+ });
38
+ });
39
+ describe('assertSafeItemName', () => {
40
+ it('隐藏项抛出', () => {
41
+ expect(() => assertSafeItemName('.hidden')).toThrow('Hidden resource items are ignored');
42
+ });
43
+ it('斜杠抛出', () => {
44
+ expect(() => assertSafeItemName('group/foo')).toThrow('Invalid resource item name');
45
+ });
46
+ it('反斜杠抛出', () => {
47
+ expect(() => assertSafeItemName('group\\foo')).toThrow('Invalid resource item name');
48
+ });
49
+ it('. 单独抛出', () => {
50
+ expect(() => assertSafeItemName('.')).toThrow('Invalid resource item name');
51
+ });
52
+ it('.. 抛出', () => {
53
+ expect(() => assertSafeItemName('..')).toThrow('Invalid resource item name');
54
+ });
55
+ it('合法名称不抛', () => {
56
+ expect(() => assertSafeItemName('foo')).not.toThrow();
57
+ });
58
+ });
59
+ describe('isPathInside', () => {
60
+ it('/tmp/store 内路径返回 true', () => {
61
+ expect(isPathInside('/tmp/store/a', '/tmp/store')).toBe(true);
62
+ });
63
+ it('/tmp/store-other 不被 /tmp/store 骗过', () => {
64
+ expect(isPathInside('/tmp/store-other/a', '/tmp/store')).toBe(false);
65
+ });
66
+ it('/tmp/store 自身返回 true', () => {
67
+ expect(isPathInside('/tmp/store', '/tmp/store')).toBe(true);
68
+ });
69
+ it('/tmp/store 深层返回 true', () => {
70
+ expect(isPathInside('/tmp/store/a/b/c', '/tmp/store')).toBe(true);
71
+ });
72
+ it('/tmp/other 返回 false', () => {
73
+ expect(isPathInside('/tmp/other', '/tmp/store')).toBe(false);
74
+ });
75
+ });
76
+ describe('realComparablePath', () => {
77
+ it('真实路径文件返回 resolved path', async () => {
78
+ const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
79
+ const f = join(tmp, 'file.txt');
80
+ writeFileSync(f, 'x');
81
+ const result = await realComparablePath(f);
82
+ expect(result).toBe(f);
83
+ rmSync(tmp, { recursive: true });
84
+ });
85
+ });
86
+ describe('resolveLinkTarget', () => {
87
+ it('绝对 target 直接 resolve', async () => {
88
+ const result = await resolveLinkTarget('/tmp/link', '/abs/target');
89
+ expect(result).toBe('/abs/target');
90
+ });
91
+ it('相对 target 按 link parent 解析', async () => {
92
+ const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
93
+ const linkPath = join(tmp, 'sub', 'link');
94
+ const sourcePath = join(tmp, 'src');
95
+ mkdirSync(join(tmp, 'sub'), { recursive: true });
96
+ mkdirSync(sourcePath, { recursive: true });
97
+ writeFileSync(join(sourcePath, 'item'), 'x');
98
+ const result = await resolveLinkTarget(linkPath, '../src/item');
99
+ expect(result).toBe(join(sourcePath, 'item'));
100
+ rmSync(tmp, { recursive: true });
101
+ });
102
+ });
103
+ describe('realParentJoined', () => {
104
+ it('父目录存在时用真实路径拼接', async () => {
105
+ const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
106
+ const dir = join(tmp, 'real-dir');
107
+ mkdirSync(dir);
108
+ const result = await realParentJoined(join(dir, 'child'));
109
+ expect(result).toBe(join(dir, 'child'));
110
+ rmSync(tmp, { recursive: true });
111
+ });
112
+ it('父目录不存在时向上找祖先', async () => {
113
+ const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
114
+ const existing = join(tmp, 'existing');
115
+ mkdirSync(existing);
116
+ const result = await realParentJoined(join(existing, 'missing', 'child'));
117
+ expect(result).toBe(join(existing, 'missing', 'child'));
118
+ rmSync(tmp, { recursive: true });
119
+ });
120
+ });
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /** dak 核心类型定义 */
2
+ /** 默认资源类型;自定义类型在 config.resourceTypes 中声明 */
3
+ export const DEFAULT_RESOURCE_TYPES = ['skills', 'hooks', 'agents'];
4
+ /** 冲突处理策略 */
5
+ export const CONFLICT_POLICIES = ['skip', 'backup', 'overwrite'];
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@dog_world/dak",
3
+ "version": "0.1.0",
4
+ "description": "Dog Agents Kit - agent skills CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "dak": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "vitest run",
16
+ "type-check": "tsc --noEmit --project tsconfig.json",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "agent-skills",
22
+ "skills",
23
+ "ai-agents",
24
+ "claude-code",
25
+ "codex"
26
+ ],
27
+ "license": "MIT",
28
+ "author": "dog_world",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/zzy1099207684/dog-agents-kit.git"
32
+ },
33
+ "homepage": "https://github.com/zzy1099207684/dog-agents-kit#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/zzy1099207684/dog-agents-kit/issues"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20",
45
+ "typescript": "^5",
46
+ "vitest": "^2"
47
+ }
48
+ }