跳到主要内容

arc space

arc space manages local DID Space data: inspecting what's stored, moving data between two subtrees with a single sync command, and claiming, checking and repairing folder-backed spaces.

arc space 管理本地 DID Space 数据——支撑 DID Space 的那份 per-app、per-user 存储。十个面向用户的子命令:三个读、一个搬数据、一个删、五个管理 folder-backed 的 space——外加第十一个内部命令;见本页底部的 sync-bench

对照版本:arc 2.0.0-beta.48,commit 5a5316bde(main,2026-09-10)——钉的是 commit,不是 semver:另一份构建也曾带着同一个 2.0.0-beta.48 字符串。复制 dump 前先跑 arc --version

bash
arc space <subcommand> [options]

全局 flag(见总览):--json--view--instance / -i(哪个本地 ARC 实例;省略即 default)、--home(实例根;要指定作用在哪个实例,用 --instance)。

检查类子命令(listtreepathrm)接受 --scope 来指定作用在哪个 DID Space,外加 --root-path/--user-did 覆盖数据实际存在哪里。只有 list 接受 --scope alltreepathrm 只接受 instance|user。文件夹类子命令(initcheckrepairsetmigratesync)直接对着一个目录操作——一个路径本身就说明了它属于哪个 space。

每个子命令的默认(非 TTY)视图各不相同——这是重抓这一页时发现的本页自身的漂移:list 的默认视图已经就是下面这种完整的人类可读块;checkmigrate 的默认是 JSON,不是文字;sync 的默认是压成一行的拒绝理由摘要,不是长版解释。--view human 是唯一一个在所有子命令上都稳定一致的 view,本页往下的每一条示例命令都用它——显式加上它,别指望默认视图,因为默认按子命令各不相同,也不是本页的契约。--json 不受这一切影响——它永远是完整的结构化 payload。

下面的例子里,demo space 路径简化成了 ~/notes;其余部分都是真实运行的原样。

查看存了什么

arc space list

列出 app space,带它们的文件数和 root。

用法

bash
arc space list [options]
  • --scope <instance|user|all>:作用在哪个 DID Space(默认 all
  • --root-path <path>:DID Space 存储的 root path,覆盖 config
  • --user-did <did>:user DID,覆盖 config;root path 仍退回 config 里的默认值
  • --all:显示全部条目(默认上限 100)

root 下没有 app space 时,list 每个 scope 打一个块(Instance DID SpaceUser DID Space),各说一句 (no app spaces)——没有单行的「这里什么都没有」空态;每个响应都是按 scope 分块的:

bash
$ arc space list --root-path ~/spaces
Instance DID Space  z1bp5ncJyUiKBv8BVCBH1RVnKks6u9VkW7G
  Root: ~/spaces
  (no app spaces)

User DID Space  z1bp5ncJyUiKBv8BVCBH1RVnKks6u9VkW7G
  Root: ~/spaces
  (no app spaces)

至少有一个 app 存在时,每个块打的是一张小表,不再是 (no app spaces)

bash
$ arc space list --root-path ~/spaces --view human
Instance DID Space  z1bp5ncJyUiKBv8BVCBH1RVnKks6u9VkW7G
  Root: ~/spaces
  DID                  Files  Last Modified
  ───                  ─────  ─────────────
  did:abt:notes-app        0  —

User DID Space  z1bp5ncJyUiKBv8BVCBH1RVnKks6u9VkW7G
  Root: ~/spaces
  DID                  Files  Last Modified
  ───                  ─────  ─────────────
  did:abt:notes-app        0  —

list 的默认(非 TTY)视图本身就是这同一种人类可读、按 scope 分块的样子——它是本页里唯一一个 --view human 加不加都一样的子命令。--json 把同样两个组打成一个 groups 数组,每个元素带 roleuserDidrootPathappstotal

arc space tree

显示一个 app space 的文件树。

用法

bash
arc space tree <app-did> [options]
  • --app-did <did>(必填,也是第一个 positional):要查看的 app DID
  • --scope <instance|user>:哪个 DID Space(默认 instance
  • --path <path>:app fragment 内的路径(默认 /
  • --depth <n>:树的最大深度(默认 99
  • --limit <n>:最多条目数,除非 --all(默认 100
  • --all:显示全部条目,忽略 --limit
  • --root-path <path> / --user-did <did>:跟 list 一样的覆盖项

--app-did 必须以 positional 方式给出——只写 arc space tree --app-did <did>、不带 positional,会被当成缺参数拒绝,即便 --app-did 同时也出现在上面的 flag 表和 --helpOptions: 列表里。tree 对着一个完全没有 space 的 app DID 跑,是一次真实的拒绝(退出码 5App space not found: <did>)——accept-path 的对应项是下面的 arc space path,它对着同一个缺失的 DID 不会报错,因为它只是计算数据存在哪里。

arc space path

打印一个 app DID Space fragment 对应的 AFS 路径,方便直接喂给 arc afs 命令用。

用法

bash
arc space path <app-did> [path] [options]
  • --app-did <did>(必填,也是第一个 positional):要查看的 app DID
  • --scope <instance|user>:哪个 DID Space(默认 instance
  • --path <path>:app fragment 内的路径(默认 /
  • --root-path <path> / --user-did <did>:跟 list 一样的覆盖项

tree 不一样,path 不要求这个 app space 真的存在——它照样打印算出来的 AFS 路径,退出码 0

bash
$ arc space path did:abt:no-such-app --root-path ~/spaces
/spaces/z1bp5ncJyUiKBv8BVCBH1RVnKks6u9VkW7G/blocklets/no-such-app/system

搬数据

arc space sync

在两棵子树之间搬数据——本地文件夹、远程 host,或者一边一个。让 --to--from 收敛。

这是「把数据从 A 搬到 B」的唯一命令,方向随意,任意两端之间都行。方向和两端都是参数,从来不是命令名——想反向拉,把 --from--to 换个位置就行。

它按路径来定位 space,这就是为什么它没有 --server--app-did--scopelayout=files 的 space 在磁盘上就是真实目录,一个路径本身已经说清楚了你指的是哪个 space、哪个 fragment。(--scope 仍然作用于按 DID 定位 space 的 list/tree/path/rm。)全局的 --instance / -i 仍然是选这条命令跟哪个本地 ARC 实例说话;它不是 DID Space 的 id。

用法

bash
arc space sync --from <ref> --to <ref> [options]

每个 <ref> 既可以是本地文件夹,也可以是 https://<host>[/<sub>] 表示某个远程 app 的 user fragment。两种形式只靠 scheme 区分,所以一个打错的 URL 永远不会被悄悄当成相对目录名重新解释。

  • --from <ref>(必填):source
  • --to <ref>(必填):destination
  • --mirror:删掉只在 destination 存在的文件(默认:additive,什么都不删)
  • --dry-run:报告差异,不写任何东西
  • --engine <auto|changelog|cursor|manifest>(默认 auto):强制指定一个 engine。auto 会协商双端都支持的最强那个。点名一个不可用的 engine 是报错,绝不静默降级
  • --verify:sync 完之后,把每个 destination 文件的 CID 跟 source 重新核对一遍。跟 --dry-run 一起用时,它是在断言 destination 已经匹配,不匹配就以非零码退出
  • --full:忽略任何已记录的 cursor,重新读一遍所有东西——不用 cursor,也不读任何一端的索引;两棵树都直接走一遍。第一次 sync 进一个 destination 反正都是 full 的;用这个 flag 重建一个你不再信任的 destination,或者 sync 一个故意在 AFS 之外被改动过的 claimed 文件夹(它的索引预期会落后,不加这个会报 index-diverged 错误——见第三个例子后面的说明)
  • --init:把一个文件夹 destination 声明为 layout=files(source 已 claimed 时默认这样做)
  • --external-writes <none|possible>(默认 possible):给一个已 claimed 的 destination 声明的查询新鲜度,只有 destination 真的被 claim 了才会用到
  • --token <token>:远程端点的 access token,浏览器认证的替代方案
  • --browser:为认证流程打开浏览器(默认 true);无人值守场景配 --token--no-browser

下面每一次运行还会在 stderr 上打一段简短的进度追踪(Scanning source N files, destination M (…ms) / Comparing (…ms) / Syncing X/Y (…ms) / Total: …ms,下面 stdout 的 dump 里省略了),以及一行 File list:,说明两边分别是从索引快照回答的还是退回到扫描——这两条本页别处都没写过,是这次重抓才发现的新东西。

默认是 additive。 第一次 sync 会报告它搬动了什么,并说出它协商到的 engine,以及其它几个为什么被拒绝。--view human 给出每条拒绝理由的长版;默认(非 TTY)视图给的是压成一行的摘要——为什么本页统一用 --view human,见上面 查看存了什么 那条说明:

bash
$ arc space sync --from src --to dst --view human
src → dst
Base path: /
Mode:      additive

Engine:    manifest — the manifest engine works between any two endpoints
  not changelog: the changelog engine is a device↔cloud engine — it needs exactly one local folder (the device) and one remote host (the cloud); neither folder↔folder nor remote↔remote fits it
  not cursor: the cursor engine mirrors (it deletes destination-only paths); this run is additive, so use --mirror to allow deletions or let the manifest engine handle it
File list: source scan (Operation not supported: paginated snapshot), destination scan (Operation not supported: paginated snapshot)

Added:     2
Modified:  0
Deleted:   0
Unchanged: 0

Transferred: 2 files
Verified:    no
Duration:    348ms

这里的 src/dst 是两个从没被 arc space init 过的普通文件夹——这就是为什么 File list: 两边都说 scan (Operation not supported: paginated snapshot):谁都没有索引可以拿来做快照,manifest engine 只能退回直接走一遍树。只存在于 destination 的文件不会被动。想让 destination 变成精确副本就加 --mirror——这第二次运行还先把 src 里的 b.txt 删了,所以 Deleted: 1 反映的是一次真实的删除,不是编出来的数字:

bash
$ arc space sync --from src --to dst --mirror --view human
src → dst
Base path: /
Mode:      mirror (deletes destination-only files)

Engine:    manifest — the manifest engine works between any two endpoints
  not changelog: the changelog engine is a device↔cloud engine — it needs exactly one local folder (the device) and one remote host (the cloud); neither folder↔folder nor remote↔remote fits it
  not cursor: source is not a claimed DID Space, so it keeps no changelog ledger to take a cursor from
File list: source scan (Operation not supported: paginated snapshot), destination scan (Operation not supported: paginated snapshot)

Added:     0
Modified:  0
Deleted:   1
Unchanged: 1

Transferred: 0 files
Verified:    no
Duration:    322ms

source 本身是一个已 claim 的 layout=files space(arc space init)时,同一条拒绝理由会写得不一样——这句话值得记住,因为它点名了真正的解法:

  not cursor: source is layout=files with externalWrites="possible" — the changelog cannot see edits made outside AFS; run `arc space set <from> external-writes=none` (only if the folder really is exclusively AFS-managed) to enable cursor-incremental sync
File list: source snapshot, destination snapshot

(真实捕获,source/destination 都已 claim——File list: 现在两边都说 snapshot,没有 fallback 理由,因为两边都有索引可读。)

--dry-run 只看,--verify 断言。 两个一起用可以核实一个 destination 而不碰它——下面这次运行发现一个被改动过的文件,以 1 退出:

bash
$ arc space sync --from src --to dst --dry-run --verify --view human
src ⇢ (dry-run) dst
Base path: /
Mode:      additive

Engine:    manifest — the manifest engine works between any two endpoints
  not changelog: the changelog engine is a device↔cloud engine — it needs exactly one local folder (the device) and one remote host (the cloud); neither folder↔folder nor remote↔remote fits it
  not cursor: the cursor engine mirrors (it deletes destination-only paths); this run is additive, so use --mirror to allow deletions or let the manifest engine handle it
File list: source scan (Operation not supported: paginated snapshot), destination scan (Operation not supported: paginated snapshot)

Added:     0
Modified:  1
Deleted:   0
Unchanged: 0

Transferred: 0 files
Verified:    no
Duration:    357ms

--dry-run 要求两端都已经存在;只有真正跑一次才会创建 destination 目录。

这次重抓之后才有的新行为:--dry-run --verify 对着一个在 AFS 之外被手改过的、已 claim 的 destination 跑,会直接拒绝,不会把这个漂移报成 Modified——--help--full 自己的说明文字就点名了这个失败:

$ arc space sync --from src-claimed --to dst-claimed --dry-run --verify --view human
ERROR: Provider "did-space-files" cannot reach its "query-freshness" dependency at "/": index has diverged from disk in 1 directory — /: a.txt (size-mismatch) — run `arc space repair --folder dst-claimed` to repair. Or re-run with --full to read both trees directly and ignore the indexes (slower, but it is the right choice for a folder that is deliberately written outside AFS).

(退出码 5。)加 --full(或者先跑一次 arc space repair --folder <dst>)是这条拒绝的 accept-path 对应项——同一个被改动过的文件、同一条命令、多加一个 flag,报出来的是 Modified: 1,退出码 1--verify 不匹配的退出码,不是拒绝),跟上面纯文件夹的例子一模一样。

编辑

arc space rm

删除本地 app space 里的一个文件或目录,递归删。它会走 space 的 ledger——直接 rm -rf 会在索引里留下一条只有 arc space repair 才能清掉的幽灵记录。

用法

bash
arc space rm <app-did> <path> [options]
  • --app-did <did>(必填,也是第一个 positional):目标 app DID
  • --path <path>(必填,也是第二个 positional):app fragment 内的路径
  • --scope <instance|user>:哪个 DID Space(默认 user
  • --root-path <path> / --user-did <did>:跟 list 一样的覆盖项

两个 positional 都必填——只写 arc space rm <app-did>(不带 path)会被当成「非 option 参数不够」拒绝,跟上面 tree/path--app-did 撞上的是同一道 yargs 级守卫。

文件夹形态的 space

一个已 claim 的文件夹,是一个由普通、可手改的文件树加一个 .did-space/ 索引撑起来的 DID Space,跟 layout=cas(内容寻址)形态相对。这五条命令负责 claim 一个文件夹、检查它的索引是否还跟磁盘一致、不一致时重建索引、以及把一个 CAS space 转成文件夹形态。

arc space init

把一个文件夹初始化成可写的 DID Space——只创建 .did-space/

用法

bash
arc space init <dir> [options]
  • dir(必填):要 claim 成 DID Space 的文件夹(设了 --source 时是一个 ledger)
  • --external-writes <none|possible>(默认 possible):新 space 的查询新鲜度声明。possible——人手初始化的文件夹随时可能被手改,所以每次查询都重新扫盘。none——只有当别的东西(一个 daemon)会独占管理这个文件夹时才传这个;之后查询直接从索引里答。外部 ledger 形态(--source)要求是 possible
  • --source <path>:外部 ledger 形态的只读真相层;ledger(.did-space/config + space.db)建在 dir,source 本身永远不会被写
bash
$ arc space init ~/notes
Initialized DID Space at ~/notes (layout=files, externalWrites=possible)

init 的默认视图就是这同一行文字;--json 给的是 { "dir", "layout", "externalWrites" }

claim 一个已经有文件的文件夹不会把这些文件建进索引——init 只创建 .did-space/。之后跑 arc space repair --folder <dir>,或者先跑 arc space check --folder <dir> 看看索引缺了什么。

arc space check

审计本地 DID Space:layout(已迁移 / 需要迁移 / 读不了)以及索引跟磁盘是否一致。只读——它从不写索引或 config。有需要迁移的、读不了的、或者漂移了的,就以非零码退出。

用法

bash
arc space check [options]
  • --folder <dir>:只检查这一个已 claim 的文件夹的新鲜度,完全跳过 layout 扫描
  • --root <dir>(可重复):layout 扫描的 root 目录。默认:这个实例的 space root 加上它的兄弟目录 <home>/.afs/work——host 的 work ledger 就住在那里,在 root_path 之外,扫描漏了它会报出一个假的「全部干净」
  • --root-path <path>:扫描用的 root path 覆盖项

check 的默认(非 TTY)视图是 JSON,不是下面这段文字——这是重抓这一页才发现的新情况,之前那版把这段文字写得像是默认视图。下面每条例子都加了 --view human 才拿到可读形式;不加就拿到跟 --json 一样的数据,形状是 { "folder", "layout", "externalWrites", "directoriesChecked", "fresh", "dirty" }(不管你是走默认视图还是显式 --json,payload 都一样)。

漂移按目录报告,附带说明修法:

bash
$ arc space check --folder ~/notes --view human
Check freshness: ~/notes (layout=files, externalWrites=possible)
Directories checked: 2
Result: DRIFT DETECTED — 2 of 2 directories disagree with disk

    /
      ! b.txt [missing-in-index]
      ! notes [missing-in-index]
    /notes
      ! a.md [missing-in-index]

Run `arc space repair --folder ~/notes` to repair (review the entries above first — it rebuilds the WHOLE index from disk).

干净的 space 会照实说,退出码 0

bash
$ arc space check --folder ~/notes --view human
Check freshness: ~/notes (layout=files, externalWrites=possible)
Directories checked: 2
Result: CLEAN — index agrees with disk

arc space repair

通过从真实文件树重建索引来修复一个已 claim 的文件夹 space。用在手改过已 claim 文件夹下的文件之后,或者在里面直接 rm -rf 之后。

用法

bash
arc space repair --folder <dir> [options]
  • --folder <dir>(必填):要修复的已 claim 文件夹(必须是 layout=files
  • --index(默认 true):从磁盘重建 folder-backend 索引——这是这条命令今天唯一会做的修复
bash
$ arc space repair --folder ~/notes
Repaired index for ~/notes (layout=files)
Added:     2
Modified:  0
Deleted:   0
Unchanged: 0
Duration:  43ms

repair 的默认视图就是这同一段文字;--json 给的是 { "folder", "layout", "added", "modified", "unchanged", "deleted", "elapsed" }(注意是 elapsed,不是 Duration——两种视图对同一个数字用了不同的键名)。

arc space set

在一个已存在layout=files DID Space 上设置一条声明。

用法

bash
arc space set <folder> <assignment>
  • --folder <dir>(必填,也是第一个 positional):要更新的已 claim 文件夹(必须已经是 layout=files——见 arc space init
  • --assignment <key>=<value>(必填,也是第二个 positional):要设置的声明。external-writes=none——这个文件夹处在独占(比如 daemon)管理下,查询直接从索引里答,不扫盘。external-writes=possible——文件夹仍可能被手改,每次查询前都重新扫盘。两个方向都是幂等且可逆的;.did-space/config 里其它已有的键都原样保留
bash
$ arc space set ~/notes external-writes=none
Set externalWrites=none for ~/notes (layout=files)

arc space migrate

把一个 layout=cas 的 DID Space 原地迁移成 layout=files(copy → verify → 原子替换;旧的 CAS 目录留着以便回滚)。

用法

bash
arc space migrate <dir> --to-layout files [options]
  • dir(必填):要迁移的已 claim DID Space 文件夹(必须是 layout=cas
  • --to-layout <files>(必填):目标 layout——只支持 files
  • --dry-run:报告会发生什么(space 数、文件数、字节数、预期结果)——磁盘上零改动
  • --external-writes <none|possible>(默认 none):迁移结果 layout=files space 的查询新鲜度声明。默认 none,因为 layout=cas space 的真相层(objects/<cid> + _metadata.db)本来就不是人会手改的东西,所以「迁移时没有外部写入者」是一个可以断定的事实;只有当这个 space 确实被当成普通可手改文件夹用过,才传 possible

migrate 的默认(非 TTY)视图也是 JSON,跟 check 一样;--view human 给的是短句形式的摘要。

对一个已经layout=files 的文件夹跑 migrate 不是错误——它是一条真实的、幂等的 accept path,退出码 0

bash
$ arc space migrate ~/notes --to-layout files --view human
Migrate ~/notes (layout=cas -> layout=files)
Already layout=files — nothing to migrate.
External writes: none

对一个完全没有 .did-space/ 的文件夹跑 arc space migrate 一个错误——跟上面「已经迁移过」不是同一个(退出码 5):

bash
$ arc space migrate ~/plain-dir --to-layout files
ERROR: arc space migrate: ~/plain-dir is not a claimed DID Space (no .did-space/). Run `arc space init ~/plain-dir` first.

要撤销一次迁移,回滚步骤:

bash
rm -rf "<dir>"
mv "<parent-of-dir>/.cas-backups/<dir-name>" "<dir>"

迁移前的 layout=cas 目录留在 <parent-of-dir>/.cas-backups/<dir-name>,从不自动删除。它故意放在 space 自己 parent 的下一级,一个本身不是已 claim space 的容器里,这样 space 枚举——以及因此 daemon 启动——就永远不会重新发现这个备份。

sync-bench(内部)

这个构建上存在第十一个子命令:arc space sync-bench,一个跑在 afs-rpcafs-rpc-batch P6 driver)上的 SyncEngine 吞吐量 benchmark。它要求 --server <url>--token <token>——一个真实的远程端点,不是本地文件夹——并在 --prefix(默认 e2e-batch/bench)下播种/跑 benchmark/清理一批一次性文件。它不像另外十个那样在 space 的数据模型里承担读/写/检查的角色;flag(--blocklet--files--size--single-op--header--cleanup)见 arc space sync-bench --help。本页不再进一步展开。