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。
arc space <subcommand> [options]全局 flag(见总览):--json、--view、--instance / -i(哪个本地 ARC 实例;省略即 default)、--home(实例根;要指定作用在哪个实例,用 --instance)。
检查类子命令(list、tree、path、rm)接受 --scope 来指定作用在哪个 DID Space,外加 --root-path/--user-did 覆盖数据实际存在哪里。只有 list 接受 --scope all;tree、path、rm 只接受 instance|user。文件夹类子命令(init、check、repair、set、migrate、sync)直接对着一个目录操作——一个路径本身就说明了它属于哪个 space。
每个子命令的默认(非 TTY)视图各不相同——这是重抓这一页时发现的本页自身的漂移:list 的默认视图已经就是下面这种完整的人类可读块;check 和 migrate 的默认是 JSON,不是文字;sync 的默认是压成一行的拒绝理由摘要,不是长版解释。--view human 是唯一一个在所有子命令上都稳定一致的 view,本页往下的每一条示例命令都用它——显式加上它,别指望默认视图,因为默认按子命令各不相同,也不是本页的契约。--json 不受这一切影响——它永远是完整的结构化 payload。
下面的例子里,demo space 路径简化成了 ~/notes;其余部分都是真实运行的原样。
查看存了什么
arc space list
列出 app space,带它们的文件数和 root。
用法
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 Space 和 User DID Space),各说一句 (no app spaces)——没有单行的「这里什么都没有」空态;每个响应都是按 scope 分块的:
$ 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):
$ 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 数组,每个元素带 role、userDid、rootPath、apps、total。
arc space tree
显示一个 app space 的文件树。
用法
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 表和 --help 的 Options: 列表里。tree 对着一个完全没有 space 的 app DID 跑,是一次真实的拒绝(退出码 5,App space not found: <did>)——accept-path 的对应项是下面的 arc space path,它对着同一个缺失的 DID 不会报错,因为它只是计算数据会存在哪里。
arc space path
打印一个 app DID Space fragment 对应的 AFS 路径,方便直接喂给 arc afs 命令用。
用法
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:
$ 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 或 --scope:layout=files 的 space 在磁盘上就是真实目录,一个路径本身已经说清楚了你指的是哪个 space、哪个 fragment。(--scope 仍然作用于按 DID 定位 space 的 list/tree/path/rm。)全局的 --instance / -i 仍然是选这条命令跟哪个本地 ARC 实例说话;它不是 DID Space 的 id。
用法
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,见上面 查看存了什么 那条说明:
$ 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 反映的是一次真实的删除,不是编出来的数字:
$ 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 退出:
$ 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 才能清掉的幽灵记录。
用法
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/。
用法
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 本身永远不会被写
$ 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。有需要迁移的、读不了的、或者漂移了的,就以非零码退出。
用法
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 都一样)。
漂移按目录报告,附带说明修法:
$ 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:
$ arc space check --folder ~/notes --view human
Check freshness: ~/notes (layout=files, externalWrites=possible)
Directories checked: 2
Result: CLEAN — index agrees with diskarc space repair
通过从真实文件树重建索引来修复一个已 claim 的文件夹 space。用在手改过已 claim 文件夹下的文件之后,或者在里面直接 rm -rf 之后。
用法
arc space repair --folder <dir> [options]--folder <dir>(必填):要修复的已 claim 文件夹(必须是layout=files)--index(默认true):从磁盘重建 folder-backend 索引——这是这条命令今天唯一会做的修复
$ arc space repair --folder ~/notes
Repaired index for ~/notes (layout=files)
Added: 2
Modified: 0
Deleted: 0
Unchanged: 0
Duration: 43msrepair 的默认视图就是这同一段文字;--json 给的是 { "folder", "layout", "added", "modified", "unchanged", "deleted", "elapsed" }(注意是 elapsed,不是 Duration——两种视图对同一个数字用了不同的键名)。
arc space set
在一个已存在的 layout=files DID Space 上设置一条声明。
用法
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里其它已有的键都原样保留
$ 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 目录留着以便回滚)。
用法
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=filesspace 的查询新鲜度声明。默认none,因为layout=casspace 的真相层(objects/<cid>+_metadata.db)本来就不是人会手改的东西,所以「迁移时没有外部写入者」是一个可以断定的事实;只有当这个 space 确实被当成普通可手改文件夹用过,才传possible
migrate 的默认(非 TTY)视图也是 JSON,跟 check 一样;--view human 给的是短句形式的摘要。
对一个已经是 layout=files 的文件夹跑 migrate 不是错误——它是一条真实的、幂等的 accept path,退出码 0:
$ 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):
$ 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.要撤销一次迁移,回滚步骤:
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-rpc(afs-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。本页不再进一步展开。