# linkany

[English](README.md) | [中文](README.zh-CN.md)

`linkany` 是一个 **macOS/Linux** 上的“安全 symlink 管理器”，围绕一个 `manifest` 文件（或 in-memory manifest）维护一组“source ↔ target”的链接关系。设计原则是：**安全第一、可追溯、默认拒绝任何可能导致数据丢失的行为**。

## CLI（命令行）

`linkany` 同时提供 **库 API** 与 **CLI**。CLI 的核心设计是：支持设置一个“全局默认 manifest”，让你后续无需重复传 `--manifest`。

### 设置/查看默认 manifest

- 设置默认 manifest（写入全局配置，路径会被 resolve 成绝对路径）：
  - `linkany manifest set ./linkany.manifest.json`
- 查看当前默认 manifest：
  - `linkany manifest show`
- 清空默认 manifest：
  - `linkany manifest clear`

### 在命令中使用 manifest（优先级）

- **优先级**：`-m/--manifest <path>`（单次覆盖） > 全局默认 manifest
- 示例：
  - 使用默认 manifest：`linkany install`
  - 单次覆盖：`linkany install -m ./other.manifest.json`

### 常用命令

- `linkany add --source <path> --target <path> [--kind file|dir] [--atomic|--no-atomic] [-m <manifest>] [--dry-run] [--plan] [--no-audit]`
- `linkany remove <key> [--keep-link] [-m <manifest>] [--dry-run] [--plan] [--no-audit]`
- `linkany install [-m <manifest>] [--dry-run] [--plan] [--no-audit]`
- `linkany uninstall [-m <manifest>] [--dry-run] [--plan] [--no-audit]`

### 全局配置文件路径（XDG）

- 若设置了 `XDG_CONFIG_HOME`：`$XDG_CONFIG_HOME/linkany/config.json`
- 否则：`~/.config/linkany/config.json`
- 格式：

```json
{ "manifestPath": "/abs/path/to/manifest.json" }
```

## 能力概览

- **仅使用 symlink**：如果 symlink 失败（权限/文件系统限制等），直接报错，不会退化为 copy 安装。
- **文件 & 目录**：同时支持文件和目录链接。
- **安全策略**：
  - `add`：当 `source` 和 `target` 同时存在（且 `target` 不是指向 `source` 的 symlink）时 **拒绝**。
  - `remove/uninstall`：只会删除 `target` 的 symlink，**绝不删除 source**。
  - `install`：如果发现某个 `target` 存在但不是 symlink，会 **整体 abort**，避免误伤真实文件/目录。
- **原子性（尽力而为）**：
  - 创建/替换 symlink 时优先使用 `target.tmp.<rand>`，再 `rename` 到位。
  - 替换已有 symlink 时会优先把旧 target 移到 `target.bak.<timestamp>.<rand>`（便于恢复）。
- **审计日志（可选）**：默认每次操作都会把 `Result` 追加写入 JSONL（默认 `${manifestPath}.log.jsonl`），也可关闭。
- **dry-run / plan 输出**：
  - `opts.dryRun=true` 时不触发任何文件系统写操作（不 symlink/rename/unlink），只返回计划与结果结构。
  - `opts.includePlanText=true` 时会在 `Result.planText` 中附带可读的 plan 文本。
- **rollback 协议（best-effort）**：`Result.rollbackSteps` 会尽力给出“可逆步骤”的回滚计划（例如 move/symlink 的逆操作）。目前是协议与数据结构，未提供一键 rollback API。

## Manifest 格式（v1）

```json
{
  "version": 1,
  "installs": [
    {
      "id": "optional-stable-id",
      "source": "path/to/source",
      "target": "path/to/target",
      "kind": "file",
      "atomic": true
    }
  ]
}
```

说明：

- `source/target` 支持绝对路径或相对路径；相对路径以 **manifest 文件所在目录** 为基准。
- `id` 可选；如果没有 `id`，内部默认以 `target` 作为该条目的 identity（用于 remove）。
- `kind` 可选：`file | dir`。不写时，`add` 会尽力推断；`install` 会从 source 的实际类型推断。
- `atomic` 默认 `true`。
- 允许存在额外字段（`linkany` 会尽量保留并写回）。

## API

四个核心 API 的 `manifest` 参数支持两种形态（合并为一个入口）：

- **文件模式**：`manifest` 为 manifest 文件路径（string）
- **in-memory 模式**：`manifest` 为 manifest JSON/对象

四个核心 API 的返回值统一为：

- `{ result, manifest }`（`result` 为本次操作结果，`manifest` 为操作后的 manifest 对象）

### `add(manifest, { source, target, kind?, atomic? }, opts?)`

用途：把一条映射写入 manifest，并把 `target` 收敛为指向 `source` 的 symlink。

核心语义：

- **source 不存在**：自动创建空 source（文件：空文件；目录：空目录）。
- **target 已存在且不是 symlink、source 不存在**：会执行一次“安全迁移”：
  - copy `target -> source`
  - 将原 `target` 移到 `target.bak.<timestamp>.<rand>`
  - 再把 `target` 改成指向 `source` 的 symlink
- **source 与 target 同时存在**：拒绝（error），要求用户手动处理冲突。

### `remove(manifest, key, opts?)`

用途：从 manifest 移除一条映射，并且 **默认删除 target 的 symlink**。

- `key`：优先匹配 `id`，否则匹配 `target`。
- `opts.keepLink=true` 可仅移除 manifest 记录，不删除 target symlink。
- **永远不删除 source**。

### `install(manifest, opts?)`

用途：按 manifest 全量落地，确保每个 `target` 都是指向 `source` 的 symlink。

安全策略：

- 任意一条出现以下情况，都会 **abort 且不做任何变更**：
  - source 不存在
  - target 存在但不是 symlink

### `uninstall(manifest, opts?)`

用途：按 manifest 全量撤销，只删除 `target` 的 symlink；**永远不删除 source**。

### in-memory 模式的额外 options

当 `manifest` 传入 JSON/对象（而不是路径）时，`opts` 额外支持：

- `baseDir?: string`：用于解析相对路径（默认 `process.cwd()`）
- `manifestPath?: string`：仅用于 `Result.manifestPath` 与 audit 默认路径推导（不会触发读写 manifest 文件）

## 审计日志（Audit Log）

- 默认写入：`${manifestPath}.log.jsonl`
- 每行是一条 JSON（完整 `Result`），包含：执行步骤、错误、耗时、变更摘要。
- 可通过 `opts.auditLogPath` 指定自定义路径。
- 如需完全关闭审计（由上层自行处理），传 `opts.audit=false`；CLI 对应 `--no-audit`。

## Options（opts）

适用于四个 API 的通用 options（`CommonOptions`）：

- `audit?: boolean`：是否写审计（默认开启）
- `auditLogPath?: string`：覆盖默认审计日志路径。
- `dryRun?: boolean`：只返回计划/结果，不写文件系统。
- `includePlanText?: boolean`：在 `Result.planText` 中包含可读 plan 文本。
- `logger?: { info/warn/error }`：注入日志实现（可选）。

## 目录结构（维护者）

```text
src/
  api/        # 4 个对外操作，分别一个文件
  core/       # 执行引擎：plan/apply/fs/audit/runner/backup
  manifest/   # manifest 类型与读写（写回保持未知字段）
  cli/        # CLI 相关模块（全局配置等）
  cli.ts      # CLI 入口（argv 解析与命令分发）
  index.ts    # 对外统一导出
  types.ts    # 公共类型（Result/Step/Options）
```

更详细的维护说明见 `KNOWLEDGE_BASE.md`。


