@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.
package/dist/config.js ADDED
@@ -0,0 +1,105 @@
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 { DEFAULT_RESOURCE_TYPES } from './types.js';
7
+ import { assertSafeItemName, toAbsolutePath } from './paths.js';
8
+ /**
9
+ * 解析 config 声明的资源类型集合。
10
+ * 缺失或空时回退 DEFAULT_RESOURCE_TYPES。
11
+ */
12
+ export function declaredResourceTypes(config) {
13
+ const types = config.resourceTypes ?? [...DEFAULT_RESOURCE_TYPES];
14
+ return types.length > 0 ? [...types] : [...DEFAULT_RESOURCE_TYPES];
15
+ }
16
+ /**
17
+ * 解析 store 路径。
18
+ * @param storeArg CLI --store 参数
19
+ * @param homeDir 用户主目录
20
+ */
21
+ export function resolveStorePath(storeArg, homeDir) {
22
+ const home = homeDir ?? process.env.HOME ?? '';
23
+ if (storeArg) {
24
+ return toAbsolutePath(storeArg, home, home);
25
+ }
26
+ return toAbsolutePath(DEFAULT_STORE, home, home);
27
+ }
28
+ /**
29
+ * 创建默认配置(内存对象,不写入磁盘)。
30
+ * @param storePath 绝对 store 路径
31
+ * @param homeDir 用户主目录
32
+ */
33
+ export function createDefaultConfig(storePath, homeDir) {
34
+ const home = homeDir ?? process.env.HOME ?? '';
35
+ const targets = {};
36
+ for (const [name, t] of Object.entries(DEFAULT_CONFIG.targets)) {
37
+ targets[name] = {
38
+ path: toAbsolutePath(t.path, home, home),
39
+ resources: t.resources,
40
+ };
41
+ }
42
+ return { store: storePath, resourceTypes: [...DEFAULT_RESOURCE_TYPES], targets };
43
+ }
44
+ /**
45
+ * 从磁盘读取配置并校验结构。
46
+ * 校验:targets 存在;resourceTypes 合法;target.resources key 必须在声明的 resourceTypes 内。
47
+ * store 顶层资源目录的布局由 ensureStoreLayout 保证,不在 readConfig 校验。
48
+ * @param storePath store 绝对路径
49
+ */
50
+ export async function readConfig(storePath) {
51
+ const configPath = join(storePath, CONFIG_FILE);
52
+ const raw = await readFile(configPath, 'utf-8');
53
+ const config = JSON.parse(raw);
54
+ validateConfigShape(config);
55
+ return config;
56
+ }
57
+ /**
58
+ * 校验 config 结构:targets 必须存在;resourceTypes 中每个类型名必须合法;
59
+ * target.resources 的 key 必须在声明的 resourceTypes 内。
60
+ * @throws 结构非法时抛错
61
+ */
62
+ function validateConfigShape(config) {
63
+ if (!config || typeof config !== 'object' || !config.targets) {
64
+ throw new Error('Invalid config: targets missing');
65
+ }
66
+ const validResources = new Set(declaredResourceTypes(config));
67
+ for (const type of validResources) {
68
+ assertSafeItemName(type);
69
+ }
70
+ for (const [name, t] of Object.entries(config.targets)) {
71
+ if (!t || typeof t.path !== 'string') {
72
+ throw new Error(`Invalid config: target ${name} missing path`);
73
+ }
74
+ if (t.resources) {
75
+ for (const key of Object.keys(t.resources)) {
76
+ if (!validResources.has(key)) {
77
+ throw new Error(`Invalid config: target ${name} has unknown resource type ${key}`);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * 仅当配置文件不存在时写入。
85
+ */
86
+ export async function writeConfigIfMissing(storePath, config) {
87
+ const configPath = join(storePath, CONFIG_FILE);
88
+ if (existsSync(configPath))
89
+ return;
90
+ await mkdir(storePath, { recursive: true });
91
+ await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
92
+ }
93
+ /**
94
+ * 校验 config.store 与当前加载 store 是否一致。
95
+ * 相对路径按 store 根目录解析;`~` 按当前命令 home 解析(计划全局约束)。
96
+ * @throws 不一致时抛错
97
+ */
98
+ export function validateConfigStore(config, storePath, homeDir) {
99
+ const home = homeDir ?? process.env.HOME ?? '';
100
+ const configStore = toAbsolutePath(config.store, storePath, home);
101
+ const currentStore = toAbsolutePath(storePath, home, home);
102
+ if (configStore !== currentStore) {
103
+ throw new Error('config store mismatch');
104
+ }
105
+ }
@@ -0,0 +1,23 @@
1
+ /** dak 常量定义 */
2
+ import { DEFAULT_RESOURCE_TYPES } from './types.js';
3
+ /** 默认资源仓库目录 */
4
+ export const DEFAULT_STORE = '~/.dog-agents-kit';
5
+ /** 配置文件文件名 */
6
+ export const CONFIG_FILE = 'dak.config.json';
7
+ /** 状态文件名 */
8
+ export const STATE_FILE = '.dak-state.json';
9
+ /** 默认配置 */
10
+ export const DEFAULT_CONFIG = {
11
+ store: DEFAULT_STORE,
12
+ resourceTypes: [...DEFAULT_RESOURCE_TYPES],
13
+ targets: {
14
+ 'codex': {
15
+ path: '~/.codex',
16
+ resources: { skills: 'skills', hooks: 'hooks', agents: 'agents' },
17
+ },
18
+ 'claudecode': {
19
+ path: '~/.claude',
20
+ resources: { skills: 'skills', hooks: 'hooks', agents: 'agents' },
21
+ },
22
+ },
23
+ };
package/dist/linker.js ADDED
@@ -0,0 +1,177 @@
1
+ /** symlink 引擎与冲突策略 */
2
+ import { mkdir, unlink, rename, lstat, readlink, stat } from 'node:fs/promises';
3
+ import { symlinkSync } from 'node:fs';
4
+ import { dirname, join, relative } from 'node:path';
5
+ import { resolveLinkTarget, realParentJoined, isPathInside } from './paths.js';
6
+ /**
7
+ * 解析 symlink 的逻辑目标绝对路径(不跟随 symlink,不依赖目标存在)。
8
+ * 用 readlink 取原始 target,再用 resolveLinkTarget 按链接父目录解析。
9
+ * 断链 symlink 也能解析,因为它不 stat 目标。
10
+ */
11
+ async function logicalSymlinkTarget(linkPath) {
12
+ const raw = await readlink(linkPath);
13
+ return resolveLinkTarget(linkPath, raw);
14
+ }
15
+ /**
16
+ * 分类目标路径状态。
17
+ * - lstat 判存在:ENOENT → missing;非 symlink → conflict。
18
+ * - stat 跟随 symlink 判目标是否存在:不存在 → broken。
19
+ * - readlink 逻辑解析与 expectedSource 比较(不 realpath),避免 store 处于
20
+ * symlink 后方时假 conflict:相等 → linked,否则 → conflict。
21
+ */
22
+ export async function classifyTarget(targetPath, expectedSource) {
23
+ let linkStat;
24
+ try {
25
+ linkStat = await lstat(targetPath);
26
+ }
27
+ catch (e) {
28
+ if (e?.code === 'ENOENT')
29
+ return 'missing';
30
+ throw e;
31
+ }
32
+ if (!linkStat.isSymbolicLink())
33
+ return 'conflict';
34
+ // 目标不存在(断链)
35
+ try {
36
+ await stat(targetPath);
37
+ }
38
+ catch (e) {
39
+ if (e?.code === 'ENOENT')
40
+ return 'broken';
41
+ throw e;
42
+ }
43
+ try {
44
+ const logicalTarget = await logicalSymlinkTarget(targetPath);
45
+ if (logicalTarget === expectedSource)
46
+ return 'linked';
47
+ return 'conflict';
48
+ }
49
+ catch {
50
+ return 'broken';
51
+ }
52
+ }
53
+ /**
54
+ * 创建相对 symlink(Linux/macOS)。
55
+ * Windows 目录 junction 另做处理,这里先实现相对 symlink。
56
+ */
57
+ async function createRelativeSymlink(sourcePath, targetPath) {
58
+ const targetParent = await realParentJoined(dirname(targetPath));
59
+ const rel = relative(targetParent, sourcePath);
60
+ // 确保父目录存在
61
+ await mkdir(dirname(targetPath), { recursive: true });
62
+ // Windows 下目录 symlink 需要权限,这里统一用 symlink
63
+ symlinkSync(rel, targetPath);
64
+ }
65
+ /**
66
+ * 移动旧内容到备份目录。
67
+ * 备份根目录 = 目标 resource 根目录的父目录(即 target 根目录)下 .dak-backup。
68
+ * 时间戳格式固定为 YYYYMMDDTHHmmssSSSZ(UTC)。
69
+ */
70
+ async function moveToBackup(targetPath, storePath, resourceType, itemName, now) {
71
+ const ts = formatBackupTimestamp(now);
72
+ // targetPath = <targetRoot>/<resourceType>/<item>
73
+ // resource root parent = targetRoot = dirname(dirname(targetPath))
74
+ const targetRoot = dirname(dirname(targetPath));
75
+ const backupRoot = join(targetRoot, '.dak-backup');
76
+ const backupPath = join(backupRoot, ts, resourceType, itemName);
77
+ await mkdir(dirname(backupPath), { recursive: true });
78
+ await rename(targetPath, backupPath);
79
+ return backupPath;
80
+ }
81
+ /**
82
+ * 格式化备份时间戳为 YYYYMMDDTHHmmssSSSZ(UTC),不带分隔符。
83
+ */
84
+ function formatBackupTimestamp(now) {
85
+ const iso = now.toISOString(); // YYYY-MM-DDTHH:mm:ss.sssZ
86
+ return iso.replace(/[-:]/g, '').replace('.', '');
87
+ }
88
+ /**
89
+ * 执行链接操作。
90
+ * linked 时也返回 record,供上层刷新 state(linkedAt/source 可能需要校正)。
91
+ */
92
+ export async function linkItem(input) {
93
+ const { sourcePath, targetPath, resourceType, itemName, storePath, resolveConflict, now } = input;
94
+ const category = await classifyTarget(targetPath, sourcePath);
95
+ const timestamp = now ?? new Date();
96
+ const record = {
97
+ source: sourcePath,
98
+ target: targetPath,
99
+ linkedAt: timestamp.toISOString(),
100
+ };
101
+ if (category === 'linked') {
102
+ return { status: 'linked', record };
103
+ }
104
+ let policy;
105
+ if (category === 'conflict' || category === 'broken') {
106
+ // 仅真实冲突才解析策略:有 resolver 就问,否则回退静态 policy(默认 skip)
107
+ if (resolveConflict) {
108
+ policy = await resolveConflict();
109
+ }
110
+ else {
111
+ policy = input.policy ?? 'skip';
112
+ }
113
+ if (policy === 'skip') {
114
+ return { status: 'conflict' };
115
+ }
116
+ // 删除旧内容(broken symlink 或真实文件)
117
+ if (policy === 'backup') {
118
+ await moveToBackup(targetPath, storePath, resourceType, itemName, timestamp);
119
+ }
120
+ else if (policy === 'overwrite') {
121
+ try {
122
+ await unlink(targetPath);
123
+ }
124
+ catch {
125
+ // 可能不存在或已被删除,忽略
126
+ }
127
+ }
128
+ }
129
+ else {
130
+ // missing:直接创建,无需策略
131
+ policy = 'skip';
132
+ }
133
+ // 创建新链接
134
+ await createRelativeSymlink(sourcePath, targetPath);
135
+ return {
136
+ status: category === 'missing' ? 'created' : policy === 'backup' ? 'backed-up' : 'overwritten',
137
+ record,
138
+ };
139
+ }
140
+ /**
141
+ * 安全删除 dak 管理的 symlink。
142
+ * 用 lstat 判存在(断链 symlink 也成功),用 readlink 逻辑解析比较:
143
+ * - target 不存在(lstat 抛 ENOENT):missing
144
+ * - target 不是 symlink:conflict
145
+ * - 逻辑目标不在当前 store 内:conflict
146
+ * - 逻辑目标不等于 record.source:conflict
147
+ * 满足全部条件才 unlink。断链 symlink 也能被删除(source 已删,逻辑目标仍可解析)。
148
+ */
149
+ export async function safeDeleteManagedLink(record, storePath) {
150
+ const { target, source } = record;
151
+ let linkStat;
152
+ try {
153
+ linkStat = await lstat(target);
154
+ }
155
+ catch (e) {
156
+ if (e?.code === 'ENOENT')
157
+ return 'missing';
158
+ throw e;
159
+ }
160
+ if (!linkStat.isSymbolicLink())
161
+ return 'conflict';
162
+ let logicalTarget;
163
+ try {
164
+ logicalTarget = await logicalSymlinkTarget(target);
165
+ }
166
+ catch {
167
+ return 'conflict';
168
+ }
169
+ // 检查是否指向当前 store
170
+ if (!isPathInside(logicalTarget, storePath))
171
+ return 'conflict';
172
+ // 检查是否指向 record.source
173
+ if (logicalTarget !== source)
174
+ return 'conflict';
175
+ await unlink(target);
176
+ return 'deleted';
177
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,144 @@
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('\\')) {
57
+ throw new Error('Invalid resource item name');
58
+ }
59
+ }
60
+ /**
61
+ * 判断 child 路径是否在 parent 路径内部(严格前缀匹配,防止边界欺骗)。
62
+ * @param child 子路径
63
+ * @param parent 父路径
64
+ * @returns 是否在内部
65
+ */
66
+ export function isPathInside(child, parent) {
67
+ const normalizedChild = resolve(child);
68
+ let normalizedParent = resolve(parent);
69
+ if (!normalizedParent.endsWith('/')) {
70
+ normalizedParent += '/';
71
+ }
72
+ // 子路径在父目录内(严格前缀)
73
+ if (normalizedChild.startsWith(normalizedParent))
74
+ return true;
75
+ // 子路径等于父目录本身
76
+ if (normalizedChild === parent || normalizedChild + '/' === normalizedParent)
77
+ return true;
78
+ return false;
79
+ }
80
+ /**
81
+ * 获取路径的可比较真实路径。
82
+ * 尝试 fs.realpath,失败时 fallback 到 path.resolve。
83
+ * @param input 输入路径
84
+ * @returns 绝对路径
85
+ */
86
+ export async function realComparablePath(input) {
87
+ try {
88
+ return await realpath(input);
89
+ }
90
+ catch {
91
+ return resolve(input);
92
+ }
93
+ }
94
+ /**
95
+ * 解析 symlink 的实际目标路径。
96
+ * @param linkPath 链接文件的实际路径
97
+ * @param rawTarget 链接原始 target(相对或绝对)
98
+ * @returns 解析后的绝对目标路径
99
+ */
100
+ export async function resolveLinkTarget(linkPath, rawTarget) {
101
+ if (rawTarget.startsWith('/')) {
102
+ return resolve(rawTarget);
103
+ }
104
+ const realParent = await realParentJoined(dirname(linkPath));
105
+ const resolved = join(realParent, rawTarget);
106
+ return resolve(resolved);
107
+ }
108
+ /**
109
+ * 获取路径的"真实父目录 + 剩余相对部分"拼接结果。
110
+ * 向上逐级查找最近存在的目录取 realpath,再拼接剩余部分。
111
+ * @param path 目标路径
112
+ * @returns 拼接后的绝对路径
113
+ */
114
+ export async function realParentJoined(path) {
115
+ const dir = dirname(path);
116
+ const basename = path.split('/').pop() ?? '';
117
+ // 向上逐级查找最近存在的目录
118
+ let current = dir;
119
+ const segments = [];
120
+ let foundExisting = null;
121
+ let remainingSegments = [];
122
+ while (current !== segments.join('/')) {
123
+ if (existsSync(current)) {
124
+ foundExisting = current;
125
+ // 计算还需要拼接的部分
126
+ const dirSegments = dir.split('/').filter(Boolean);
127
+ const foundSegments = foundExisting.split('/').filter(Boolean);
128
+ remainingSegments = dirSegments.slice(foundSegments.length);
129
+ remainingSegments.push(basename);
130
+ break;
131
+ }
132
+ segments.unshift(current.split('/').pop() ?? '');
133
+ const parent = dirname(current);
134
+ if (parent === current)
135
+ break;
136
+ current = parent;
137
+ }
138
+ if (foundExisting) {
139
+ const realAncestor = await realpath(foundExisting);
140
+ const remaining = remainingSegments.join('/');
141
+ return join(realAncestor, remaining);
142
+ }
143
+ return resolve(path);
144
+ }
@@ -0,0 +1,93 @@
1
+ /** 资源扫描与格式化 */
2
+ import { readdir, mkdir } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { declaredResourceTypes } from './config.js';
6
+ import { assertSafeItemName, isHiddenItem } from './paths.js';
7
+ /**
8
+ * 资源类型在 list 输出中的显示标题。
9
+ * 默认类型固定标题;自定义类型首字母大写 + 冒号。
10
+ */
11
+ function resourceLabel(type) {
12
+ const defaults = {
13
+ skills: 'Skills:',
14
+ hooks: 'Hooks:',
15
+ agents: 'Agents:',
16
+ };
17
+ if (defaults[type])
18
+ return defaults[type];
19
+ const cap = type.charAt(0).toUpperCase() + type.slice(1);
20
+ return `${cap}:`;
21
+ }
22
+ /**
23
+ * 确保 store 目录和声明的各资源类型目录存在。
24
+ */
25
+ export async function ensureStoreLayout(storePath, config) {
26
+ await mkdir(storePath, { recursive: true });
27
+ for (const type of declaredResourceTypes(config)) {
28
+ await mkdir(join(storePath, type), { recursive: true });
29
+ }
30
+ }
31
+ /**
32
+ * 扫描 store 中声明的资源类型目录(一级子项)。
33
+ */
34
+ export async function scanResources(storePath, config) {
35
+ const result = {};
36
+ for (const type of declaredResourceTypes(config)) {
37
+ result[type] = [];
38
+ const dir = join(storePath, type);
39
+ if (!existsSync(dir))
40
+ continue;
41
+ try {
42
+ const entries = await readdir(dir, { withFileTypes: true });
43
+ const items = [];
44
+ for (const entry of entries) {
45
+ const name = entry.name;
46
+ if (isHiddenItem(name))
47
+ continue;
48
+ try {
49
+ assertSafeItemName(name);
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ const fullPath = join(dir, name);
55
+ let kind;
56
+ if (entry.isSymbolicLink())
57
+ kind = 'symlink';
58
+ else if (entry.isDirectory())
59
+ kind = 'directory';
60
+ else
61
+ kind = 'file';
62
+ items.push({ name, kind, path: fullPath });
63
+ }
64
+ items.sort((a, b) => a.name.localeCompare(b.name));
65
+ result[type] = items;
66
+ }
67
+ catch {
68
+ // ignore unreadable dir
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+ /**
74
+ * 格式化资源列表输出(供 `dak list` 使用)。
75
+ * 输出稳定,支持 snapshot 测试。
76
+ */
77
+ export function formatResourceList(resources) {
78
+ const lines = [];
79
+ for (const type of Object.keys(resources)) {
80
+ lines.push(resourceLabel(type));
81
+ const items = resources[type];
82
+ if (items.length === 0) {
83
+ lines.push(' (empty)');
84
+ }
85
+ else {
86
+ for (const item of items) {
87
+ const kind = item.kind === 'directory' ? ' [dir]' : item.kind === 'symlink' ? ' [link]' : '';
88
+ lines.push(` ${item.name}${kind}`);
89
+ }
90
+ }
91
+ }
92
+ return lines.join('\n');
93
+ }
@@ -0,0 +1,97 @@
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
+ }