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。本頁不再進一步展開。