@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 CHANGED
File without changes
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dog_world/dak",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Dog Agents Kit - agent skills CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "dak": "./dist/cli.js"
7
+ "dak": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "dist",
package/dist/src/cli.js DELETED
@@ -1,97 +0,0 @@
1
- #!/usr/bin/env node
2
- /** dak CLI 入口 */
3
- import { runInit, runList, runLink, runStatus, runUpdate, runUnlink } from './commands.js';
4
- /** 解析命令行参数(不引入额外依赖) */
5
- export function parseArgv(argv) {
6
- const result = { command: 'list' };
7
- for (let i = 0; i < argv.length; i++) {
8
- const arg = argv[i];
9
- if (arg === '--store' && argv[i + 1]) {
10
- result.store = argv[i + 1];
11
- i++;
12
- continue;
13
- }
14
- if (arg === '--on-conflict' && argv[i + 1]) {
15
- const val = argv[i + 1];
16
- if (!['skip', 'backup', 'overwrite'].includes(val)) {
17
- throw new Error('Invalid conflict policy');
18
- }
19
- result.onConflict = val;
20
- i++;
21
- continue;
22
- }
23
- // 第一个非 flag 参数为命令或 target
24
- if (!arg.startsWith('--')) {
25
- const cmd = arg;
26
- if (['init', 'list', 'link', 'status', 'update', 'unlink'].includes(cmd)) {
27
- result.command = cmd;
28
- // link/unlink 后跟 target
29
- if (cmd === 'link' || cmd === 'unlink') {
30
- const next = argv[i + 1];
31
- if (!next || next.startsWith('--')) {
32
- throw new Error(`${cmd} target is required`);
33
- }
34
- result.target = next;
35
- i++;
36
- }
37
- }
38
- else {
39
- throw new Error(`Unknown command: ${arg}`);
40
- }
41
- }
42
- }
43
- // link/unlink 必须有 target
44
- if ((result.command === 'link' || result.command === 'unlink') && !result.target) {
45
- throw new Error(`${result.command} target is required`);
46
- }
47
- return result;
48
- }
49
- /** 主入口 */
50
- export async function main(args = process.argv.slice(2)) {
51
- const homeDir = process.env.HOME ?? '';
52
- const interactive = process.stdin.isTTY && process.stdout.isTTY;
53
- try {
54
- const parsed = parseArgv(args);
55
- const opts = { store: parsed.store, homeDir };
56
- // 冲突策略
57
- if (parsed.onConflict) {
58
- opts.conflictPolicy = parsed.onConflict;
59
- }
60
- else if (interactive) {
61
- opts.interactive = true;
62
- opts.chooseConflict = async (_target, _resource, _item) => {
63
- // 简单实现:默认 skip
64
- return 'skip';
65
- };
66
- }
67
- switch (parsed.command) {
68
- case 'init':
69
- console.log(await runInit(opts));
70
- break;
71
- case 'list':
72
- console.log(await runList(opts));
73
- break;
74
- case 'link':
75
- console.log(await runLink(parsed.target, opts));
76
- break;
77
- case 'status':
78
- console.log(await runStatus(opts));
79
- break;
80
- case 'update':
81
- console.log(await runUpdate(opts));
82
- break;
83
- case 'unlink':
84
- console.log(await runUnlink(parsed.target, opts));
85
- break;
86
- }
87
- return 0;
88
- }
89
- catch (e) {
90
- console.error(`Error: ${e.message}`);
91
- return 1;
92
- }
93
- }
94
- // 直接运行时执行 main
95
- if (process.argv[1] === import.meta.url) {
96
- main().then(code => process.exit(code));
97
- }
@@ -1,291 +0,0 @@
1
- /** 命令工作流 */
2
- import { join } from 'node:path';
3
- import { readConfig, createDefaultConfig, validateConfigStore, writeConfigIfMissing } from './config.js';
4
- import { readState, writeState, writeStateIfMissing, upsertLinkRecord, removeLinkRecord, removeTargetState } from './state.js';
5
- import { scanResources, formatResourceList, ensureStoreLayout } from './resources.js';
6
- import { classifyTarget, linkItem, safeDeleteManagedLink } from './linker.js';
7
- import { RESOURCE_TYPES } from './types.js';
8
- /**
9
- * 加载上下文(store/config/state)。
10
- */
11
- async function loadContext(opts) {
12
- const homeDir = opts.homeDir ?? process.env.HOME ?? '';
13
- const storePath = opts.store ?? join(homeDir, '.dog-agents-kit');
14
- const config = await readConfig(storePath);
15
- validateConfigStore(config, storePath);
16
- const state = await readState(storePath);
17
- return { storePath, config, state, homeDir };
18
- }
19
- /**
20
- * 解析目标名称(支持 all)。
21
- */
22
- function resolveTargetNames(target, config) {
23
- if (target === 'all') {
24
- return Object.keys(config.targets ?? {});
25
- }
26
- if (!config.targets?.[target]) {
27
- throw new Error(`unknown target: ${target}`);
28
- }
29
- return [target];
30
- }
31
- /**
32
- * 获取目标 resource 路径(未配置则返回 null)。
33
- */
34
- function targetResourcePath(config, targetName, resourceType) {
35
- const t = config.targets?.[targetName];
36
- if (!t?.resources?.[resourceType])
37
- return null;
38
- return join(t.path, t.resources[resourceType]);
39
- }
40
- /**
41
- * 确定冲突策略。
42
- */
43
- function selectedPolicy(opts, _target, _resource, _item) {
44
- if (opts.conflictPolicy)
45
- return opts.conflictPolicy;
46
- if (opts.interactive && opts.chooseConflict) {
47
- return opts.chooseConflict(_target, _resource, _item);
48
- }
49
- return 'skip';
50
- }
51
- /** ─── Commands ─── **/
52
- /** 初始化 store */
53
- export async function runInit(opts) {
54
- const homeDir = opts.homeDir ?? process.env.HOME ?? '';
55
- const storePath = opts.store ?? join(homeDir, '.dog-agents-kit');
56
- const config = createDefaultConfig(storePath, homeDir);
57
- await ensureStoreLayout(storePath, config);
58
- await writeConfigIfMissing(storePath, config);
59
- await writeStateIfMissing(storePath);
60
- return `Initialized dak store at ${storePath}`;
61
- }
62
- /** 列出资源 */
63
- export async function runList(opts) {
64
- const { storePath, config } = await loadContext(opts);
65
- const resources = await scanResources(storePath, config);
66
- return formatResourceList(resources);
67
- }
68
- /** 链接资源到目标 */
69
- export async function runLink(targetArg, opts) {
70
- const { storePath, config, state, homeDir } = await loadContext(opts);
71
- const targetNames = resolveTargetNames(targetArg, config);
72
- const lines = [];
73
- let linked = 0, created = 0, skipped = 0, conflicts = 0;
74
- for (const tName of targetNames) {
75
- for (const resourceType of RESOURCE_TYPES) {
76
- const targetPath = targetResourcePath(config, tName, resourceType);
77
- if (!targetPath)
78
- continue; // target 未配置此 resource,跳过
79
- const items = await scanResources(storePath, config);
80
- for (const item of items[resourceType]) {
81
- const itemTargetPath = join(targetPath, item.name);
82
- const policy = selectedPolicy(opts, tName, resourceType, item.name);
83
- const outcome = await linkItem({
84
- sourcePath: item.path,
85
- targetPath: itemTargetPath,
86
- resourceType,
87
- itemName: item.name,
88
- storePath,
89
- policy,
90
- now: opts.now,
91
- });
92
- // 写 state
93
- if (outcome.record) {
94
- upsertLinkRecord(state, tName, resourceType, item.name, outcome.record);
95
- }
96
- let status = '';
97
- switch (outcome.status) {
98
- case 'linked':
99
- status = 'linked';
100
- linked++;
101
- break;
102
- case 'created':
103
- status = 'created';
104
- created++;
105
- break;
106
- case 'backed-up':
107
- status = 'backed-up';
108
- created++;
109
- break;
110
- case 'overwritten':
111
- status = 'overwritten';
112
- created++;
113
- break;
114
- case 'conflict':
115
- status = 'conflict';
116
- conflicts++;
117
- break;
118
- }
119
- lines.push(`${tName} ${resourceType}/${item.name} ${status} ${itemTargetPath}`);
120
- }
121
- }
122
- }
123
- await writeState(storePath, state);
124
- lines.push(`Summary: linked=${linked} created=${created} skipped=${skipped} conflicts=${conflicts}`);
125
- return lines.join('\n');
126
- }
127
- /** 状态检查 */
128
- export async function runStatus(opts) {
129
- const { storePath, config, state, homeDir } = await loadContext(opts);
130
- const storeItems = await scanResources(storePath, config);
131
- const lines = [];
132
- for (const [tName, tState] of Object.entries(state.targets)) {
133
- for (const resourceType of RESOURCE_TYPES) {
134
- const targetPath = targetResourcePath(config, tName, resourceType);
135
- if (!targetPath)
136
- continue;
137
- const records = tState[resourceType] ?? {};
138
- const storeItemsForType = storeItems[resourceType] ?? [];
139
- const storeItemMap = new Map(storeItemsForType.map(i => [i.name, i]));
140
- // 当前 store 中的 items
141
- for (const item of storeItemsForType) {
142
- const itemTargetPath = join(targetPath, item.name);
143
- const category = await classifyTarget(itemTargetPath, item.path);
144
- lines.push(`${tName} ${resourceType}/${item.name} ${category} ${itemTargetPath}`);
145
- }
146
- // state 中已存在但 store 已删除(stale)
147
- for (const [itemName, record] of Object.entries(records)) {
148
- if (storeItemMap.has(itemName))
149
- continue;
150
- const itemTargetPath = record.target;
151
- lines.push(`${tName} ${resourceType}/${itemName} stale ${itemTargetPath}`);
152
- }
153
- }
154
- }
155
- return lines.join('\n');
156
- }
157
- /** 更新(补链接 + 清理 stale) */
158
- export async function runUpdate(opts) {
159
- const { storePath, config, state, homeDir } = await loadContext(opts);
160
- const lines = [];
161
- let created = 0, deleted = 0, missingCount = 0, skipped = 0, conflicts = 0;
162
- // target 集合只能来自 state.targets(已 link 过的 targets)
163
- const targetNames = Object.keys(state.targets);
164
- for (const tName of targetNames) {
165
- const tState = state.targets[tName];
166
- for (const resourceType of RESOURCE_TYPES) {
167
- const targetPath = targetResourcePath(config, tName, resourceType);
168
- if (!targetPath)
169
- continue;
170
- const records = { ...(tState[resourceType] ?? {}) };
171
- // 1. 清理 stale
172
- for (const [itemName, record] of Object.entries(records)) {
173
- const sourcePath = record.source;
174
- if (!existsSync(sourcePath)) {
175
- // stale:state 中有记录但 store 已无源
176
- const result = await safeDeleteManagedLink(record, storePath);
177
- switch (result) {
178
- case 'deleted':
179
- removeLinkRecord(state, tName, resourceType, itemName);
180
- deleted++;
181
- lines.push(`${tName} ${resourceType}/${itemName} deleted ${record.target}`);
182
- break;
183
- case 'missing':
184
- removeLinkRecord(state, tName, resourceType, itemName);
185
- missingCount++;
186
- lines.push(`${tName} ${resourceType}/${itemName} missing ${record.target}`);
187
- break;
188
- case 'conflict':
189
- conflicts++;
190
- lines.push(`${tName} ${resourceType}/${itemName} conflict ${record.target}`);
191
- break;
192
- }
193
- }
194
- }
195
- // 2. 为当前 store item 补链接
196
- const storeItems = await scanResources(storePath, config);
197
- for (const item of storeItems[resourceType]) {
198
- const itemTargetPath = join(targetPath, item.name);
199
- const policy = selectedPolicy(opts, tName, resourceType, item.name);
200
- const outcome = await linkItem({
201
- sourcePath: item.path,
202
- targetPath: itemTargetPath,
203
- resourceType,
204
- itemName: item.name,
205
- storePath,
206
- policy,
207
- now: opts.now,
208
- });
209
- if (outcome.record) {
210
- upsertLinkRecord(state, tName, resourceType, item.name, outcome.record);
211
- }
212
- switch (outcome.status) {
213
- case 'linked':
214
- skipped++;
215
- lines.push(`${tName} ${resourceType}/${item.name} linked ${itemTargetPath}`);
216
- break;
217
- case 'created':
218
- created++;
219
- lines.push(`${tName} ${resourceType}/${item.name} created ${itemTargetPath}`);
220
- break;
221
- case 'backed-up':
222
- created++;
223
- lines.push(`${tName} ${resourceType}/${item.name} backed-up ${itemTargetPath}`);
224
- break;
225
- case 'overwritten':
226
- created++;
227
- lines.push(`${tName} ${resourceType}/${item.name} overwritten ${itemTargetPath}`);
228
- break;
229
- case 'conflict':
230
- conflicts++;
231
- lines.push(`${tName} ${resourceType}/${item.name} conflict ${itemTargetPath}`);
232
- break;
233
- }
234
- }
235
- }
236
- }
237
- await writeState(storePath, state);
238
- lines.push(`Summary: created=${created} deleted=${deleted} missing=${missingCount} skipped=${skipped} conflicts=${conflicts}`);
239
- return lines.join('\n');
240
- }
241
- /** 取消链接 */
242
- export async function runUnlink(targetArg, opts) {
243
- const { storePath, state, homeDir } = await loadContext(opts);
244
- const config = await readConfig(storePath);
245
- const targetNames = resolveTargetNames(targetArg, config);
246
- const lines = [];
247
- let deleted = 0, missingCount = 0, conflicts = 0;
248
- for (const tName of targetNames) {
249
- const tState = state.targets[tName];
250
- if (!tState)
251
- continue;
252
- for (const resourceType of RESOURCE_TYPES) {
253
- const records = { ...(tState[resourceType] ?? {}) };
254
- for (const [itemName, record] of Object.entries(records)) {
255
- const result = await safeDeleteManagedLink(record, storePath);
256
- switch (result) {
257
- case 'deleted':
258
- removeLinkRecord(state, tName, resourceType, itemName);
259
- deleted++;
260
- lines.push(`${tName} ${resourceType}/${itemName} deleted ${record.target}`);
261
- break;
262
- case 'missing':
263
- removeLinkRecord(state, tName, resourceType, itemName);
264
- missingCount++;
265
- lines.push(`${tName} ${resourceType}/${itemName} missing ${record.target}`);
266
- break;
267
- case 'conflict':
268
- conflicts++;
269
- lines.push(`${tName} ${resourceType}/${itemName} conflict ${record.target}`);
270
- break;
271
- }
272
- }
273
- }
274
- // 如果 target 下所有 records 都已删完,移除 target state
275
- if (tState && Object.values(tState).every(m => Object.keys(m).length === 0)) {
276
- removeTargetState(state, tName);
277
- }
278
- }
279
- await writeState(storePath, state);
280
- lines.push(`Summary: deleted=${deleted} missing=${missingCount} conflicts=${conflicts}`);
281
- return lines.join('\n');
282
- }
283
- // existsSync 辅助(内联避免额外导入)
284
- function existsSync(p) {
285
- try {
286
- return require('node:fs').existsSync(p);
287
- }
288
- catch {
289
- return false;
290
- }
291
- }
@@ -1,65 +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 { DEFAULT_CONFIG, DEFAULT_STORE, CONFIG_FILE } from './constants.js';
6
- import { toAbsolutePath } from './paths.js';
7
- /**
8
- * 解析 store 路径。
9
- * @param storeArg CLI --store 参数
10
- * @param homeDir 用户主目录
11
- */
12
- export function resolveStorePath(storeArg, homeDir) {
13
- const home = homeDir ?? process.env.HOME ?? '';
14
- if (storeArg) {
15
- return toAbsolutePath(storeArg, home, home);
16
- }
17
- return toAbsolutePath(DEFAULT_STORE, home, home);
18
- }
19
- /**
20
- * 创建默认配置(内存对象,不写入磁盘)。
21
- * @param storePath 绝对 store 路径
22
- * @param homeDir 用户主目录
23
- */
24
- export function createDefaultConfig(storePath, homeDir) {
25
- const home = homeDir ?? process.env.HOME ?? '';
26
- const targets = {};
27
- for (const [name, t] of Object.entries(DEFAULT_CONFIG.targets)) {
28
- targets[name] = {
29
- path: toAbsolutePath(t.path, home, home),
30
- resources: t.resources,
31
- };
32
- }
33
- return { store: storePath, targets };
34
- }
35
- /**
36
- * 从磁盘读取配置。
37
- * @param storePath store 绝对路径
38
- */
39
- export async function readConfig(storePath) {
40
- const configPath = join(storePath, CONFIG_FILE);
41
- const raw = await readFile(configPath, 'utf-8');
42
- const config = JSON.parse(raw);
43
- return config;
44
- }
45
- /**
46
- * 仅当配置文件不存在时写入。
47
- */
48
- export async function writeConfigIfMissing(storePath, config) {
49
- const configPath = join(storePath, CONFIG_FILE);
50
- if (existsSync(configPath))
51
- return;
52
- await mkdir(storePath, { recursive: true });
53
- await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
54
- }
55
- /**
56
- * 校验 config.store 与当前加载 store 是否一致。
57
- * @throws 不一致时抛错
58
- */
59
- export function validateConfigStore(config, storePath) {
60
- const configStore = resolveStorePath(config.store);
61
- const currentStore = resolveStorePath(storePath);
62
- if (configStore !== currentStore) {
63
- throw new Error('config store mismatch');
64
- }
65
- }
@@ -1,26 +0,0 @@
1
- /** dak 常量定义 */
2
- /** 默认资源仓库目录 */
3
- export const DEFAULT_STORE = '~/.dog-agents-kit';
4
- /** 配置文件文件名 */
5
- export const CONFIG_FILE = 'dak.config.json';
6
- /** 状态文件名 */
7
- export const STATE_FILE = '.dak-state.json';
8
- /** 默认配置 */
9
- export const DEFAULT_CONFIG = {
10
- store: DEFAULT_STORE,
11
- targets: {
12
- 'codex': {
13
- path: '~/.codex',
14
- resources: { skills: 'skills', hooks: 'hooks', agents: 'agents' },
15
- },
16
- 'claude-code': {
17
- path: '~/.claude',
18
- resources: { skills: 'skills', hooks: 'hooks', agents: 'agents' },
19
- },
20
- },
21
- };
22
- /** 空状态模板 */
23
- export const EMPTY_STATE = {
24
- version: 1,
25
- targets: {},
26
- };
@@ -1,132 +0,0 @@
1
- /** symlink 引擎与冲突策略 */
2
- import { mkdir, unlink, rename, lstat, realpath } from 'node:fs/promises';
3
- import { existsSync, symlinkSync } from 'node:fs';
4
- import { dirname, join, relative } from 'node:path';
5
- import { realParentJoined, isPathInside } from './paths.js';
6
- import { STATE_FILE } from './constants.js';
7
- /**
8
- * 分类目标路径状态。
9
- * 直接 lstat;broken symlink 的 lstat 成功(是 symlink)但 realpath 抛错。
10
- */
11
- export async function classifyTarget(targetPath, expectedSource) {
12
- try {
13
- const linkStat = await lstat(targetPath);
14
- if (!linkStat.isSymbolicLink())
15
- return 'conflict';
16
- try {
17
- const realTarget = await realpath(targetPath);
18
- if (realTarget === expectedSource)
19
- return 'linked';
20
- return 'conflict';
21
- }
22
- catch {
23
- return 'broken';
24
- }
25
- }
26
- catch (e) {
27
- if (e?.code === 'ENOENT')
28
- return 'missing';
29
- throw e;
30
- }
31
- }
32
- /**
33
- * 创建相对 symlink(Linux/macOS)。
34
- * Windows 目录 junction 另做处理,这里先实现相对 symlink。
35
- */
36
- async function createRelativeSymlink(sourcePath, targetPath) {
37
- const targetParent = await realParentJoined(dirname(targetPath));
38
- const rel = relative(targetParent, sourcePath);
39
- // 确保父目录存在
40
- await mkdir(dirname(targetPath), { recursive: true });
41
- // Windows 下目录 symlink 需要权限,这里统一用 symlink
42
- symlinkSync(rel, targetPath);
43
- }
44
- /**
45
- * 移动旧内容到备份目录。
46
- * 备份根目录 = 目标 resource 根目录的父目录(即 target 根目录)下 .dak-backup
47
- */
48
- async function moveToBackup(targetPath, storePath, resourceType, itemName, now) {
49
- const ts = now.toISOString().replace(/[:.]/g, '-');
50
- // targetPath = <targetRoot>/<resourceType>/<item>
51
- // resource root parent = targetRoot = dirname(dirname(targetPath))
52
- const targetRoot = dirname(dirname(targetPath));
53
- const backupRoot = join(targetRoot, '.dak-backup');
54
- const backupPath = join(backupRoot, ts, resourceType, itemName);
55
- await mkdir(dirname(backupPath), { recursive: true });
56
- await rename(targetPath, backupPath);
57
- return backupPath;
58
- }
59
- /**
60
- * 执行链接操作。
61
- */
62
- export async function linkItem(input) {
63
- const { sourcePath, targetPath, resourceType, itemName, storePath, policy, now } = input;
64
- const category = await classifyTarget(targetPath, sourcePath);
65
- if (category === 'linked') {
66
- return { status: 'linked' };
67
- }
68
- const timestamp = now ?? new Date();
69
- if (category === 'conflict' || category === 'broken') {
70
- if (policy === 'skip') {
71
- return { status: 'conflict' };
72
- }
73
- // 删除旧内容(broken symlink 或真实文件)
74
- if (policy === 'backup') {
75
- await moveToBackup(targetPath, storePath, resourceType, itemName, timestamp);
76
- }
77
- else if (policy === 'overwrite') {
78
- try {
79
- await unlink(targetPath);
80
- }
81
- catch {
82
- // 可能不存在或已被删除,忽略
83
- }
84
- }
85
- }
86
- // 创建新链接
87
- await createRelativeSymlink(sourcePath, targetPath);
88
- const record = {
89
- source: sourcePath,
90
- target: targetPath,
91
- linkedAt: timestamp.toISOString(),
92
- };
93
- return {
94
- status: category === 'missing' ? 'created' : policy === 'backup' ? 'backed-up' : 'overwritten',
95
- record,
96
- };
97
- }
98
- /**
99
- * 安全删除 dak 管理的 symlink。
100
- * 仅当所有条件满足时删除:
101
- * - target 存在且是 symlink
102
- * - symlink 指向当前 store 内的路径
103
- * - symlink 指向 record.source
104
- */
105
- export async function safeDeleteManagedLink(record, storePath) {
106
- const { target, source } = record;
107
- if (!existsSync(target))
108
- return 'missing';
109
- try {
110
- const linkStat = await lstat(target);
111
- if (!linkStat.isSymbolicLink())
112
- return 'conflict';
113
- }
114
- catch {
115
- return 'conflict';
116
- }
117
- const realTarget = await realpath(target);
118
- // 检查是否指向当前 store
119
- if (!isPathInside(realTarget, storePath))
120
- return 'conflict';
121
- // 检查是否指向 record.source
122
- if (realTarget !== source)
123
- return 'conflict';
124
- await unlink(target);
125
- return 'deleted';
126
- }
127
- /**
128
- * 获取 state 文件路径。
129
- */
130
- export function statePathFor(storePath) {
131
- return join(storePath, STATE_FILE);
132
- }