@animalabs/agent-framework 0.26.2 → 0.28.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/README.md +99 -8
- package/dist/src/agent.d.ts +291 -23
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +596 -61
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +5 -4
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +34 -0
- package/dist/src/auto-continue.d.ts.map +1 -0
- package/dist/src/auto-continue.js +36 -0
- package/dist/src/auto-continue.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -1
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +1038 -193
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +5967 -1514
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +405 -46
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +2646 -399
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts +103 -0
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
- package/dist/src/gate/recipe-reconcile.js +325 -0
- package/dist/src/gate/recipe-reconcile.js.map +1 -0
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +229 -8
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +184 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +752 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +433 -113
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1154 -435
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +16 -8
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +29 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +14 -2
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +32 -7
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +26 -10
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +40 -69
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +4 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +14 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +73 -28
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +6 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +3 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +20 -2
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +33 -11
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +177 -145
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +380 -46
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +736 -77
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +93 -4
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +176 -11
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +21 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +311 -0
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
- package/dist/src/mcpl/tool-presence.js +697 -0
- package/dist/src/mcpl/tool-presence.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js +14 -6
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +46 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
- package/dist/src/mcpl/tool-signature.js +321 -0
- package/dist/src/mcpl/tool-signature.js.map +1 -0
- package/dist/src/mcpl/toolset-host.d.ts +251 -0
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
- package/dist/src/mcpl/toolset-host.js +720 -0
- package/dist/src/mcpl/toolset-host.js.map +1 -0
- package/dist/src/mcpl/toolsets-module.d.ts +83 -0
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
- package/dist/src/mcpl/toolsets-module.js +523 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +2 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +3 -2
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +24 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +7 -9
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +21 -9
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +27 -9
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +77 -17
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +2 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +73 -28
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +108 -6
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +273 -57
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/answers.d.ts +22 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +123 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +184 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
- package/dist/src/modules/workspace/direct-write.js +417 -0
- package/dist/src/modules/workspace/direct-write.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +27 -12
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +61 -29
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +10 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +21 -14
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +308 -78
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2103 -720
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +75 -63
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +366 -180
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts +41 -0
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
- package/dist/src/modules/workspace/read-page.js +219 -0
- package/dist/src/modules/workspace/read-page.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +26 -16
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +68 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/result-text.js +94 -0
- package/dist/src/modules/workspace/result-text.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/text.d.ts +29 -12
- package/dist/src/modules/workspace/text.d.ts.map +1 -1
- package/dist/src/modules/workspace/text.js +35 -33
- package/dist/src/modules/workspace/text.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +137 -60
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +50 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.d.ts +59 -0
- package/dist/src/sent-rows.d.ts.map +1 -0
- package/dist/src/sent-rows.js +145 -0
- package/dist/src/sent-rows.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +17 -10
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stable-hash.d.ts +3 -0
- package/dist/src/stable-hash.d.ts.map +1 -0
- package/dist/src/stable-hash.js +15 -0
- package/dist/src/stable-hash.js.map +1 -0
- package/dist/src/stored-injections.d.ts +99 -0
- package/dist/src/stored-injections.d.ts.map +1 -0
- package/dist/src/stored-injections.js +106 -0
- package/dist/src/stored-injections.js.map +1 -0
- package/dist/src/text-as-stored.d.ts +4 -3
- package/dist/src/text-as-stored.d.ts.map +1 -1
- package/dist/src/text-as-stored.js +4 -3
- package/dist/src/text-as-stored.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +74 -0
- package/dist/src/thinking-drops.d.ts.map +1 -0
- package/dist/src/thinking-drops.js +96 -0
- package/dist/src/thinking-drops.js.map +1 -0
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +35 -1
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts +9 -8
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +9 -8
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +3 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +44 -17
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +176 -24
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +411 -91
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +17 -9
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +84 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +217 -7
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +33 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +39 -3
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +95 -9
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +91 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/agent.ts +661 -67
- package/src/api/server.ts +5 -4
- package/src/auto-continue.ts +43 -0
- package/src/code-execution/isolate/scripts.ts +11 -3
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +6168 -1540
- package/src/gate/event-gate.ts +2895 -405
- package/src/gate/index.ts +10 -1
- package/src/gate/recipe-reconcile.ts +355 -0
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +234 -9
- package/src/heartbeat/heartbeat.ts +804 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/index.ts +11 -4
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +1222 -468
- package/src/mcpl/conversation-router.ts +34 -16
- package/src/mcpl/convert-block.ts +14 -1
- package/src/mcpl/hook-orchestrator.ts +33 -6
- package/src/mcpl/inbound-source.ts +42 -69
- package/src/mcpl/index.ts +6 -1
- package/src/mcpl/inference-router.ts +79 -34
- package/src/mcpl/prose-grammar.ts +7 -0
- package/src/mcpl/protocol-family.ts +2 -0
- package/src/mcpl/push-coalescer.ts +51 -13
- package/src/mcpl/reference-fetcher.ts +201 -162
- package/src/mcpl/references.ts +851 -77
- package/src/mcpl/server-connection.ts +189 -11
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-lifecycle.ts +21 -0
- package/src/mcpl/tool-presence.ts +716 -0
- package/src/mcpl/tool-result-normalize.ts +18 -6
- package/src/mcpl/tool-signature.ts +316 -0
- package/src/mcpl/toolset-host.ts +727 -0
- package/src/mcpl/toolsets-module.ts +545 -0
- package/src/mcpl/transport.ts +3 -2
- package/src/mcpl/types.ts +25 -2
- package/src/mcpl/visible-content.ts +22 -9
- package/src/module-registry.ts +85 -26
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +70 -30
- package/src/modules/history/semantic.ts +308 -54
- package/src/modules/workspace/answers.ts +133 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +460 -0
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +84 -35
- package/src/modules/workspace/effects.ts +22 -13
- package/src/modules/workspace/index.ts +2142 -723
- package/src/modules/workspace/observe.ts +381 -177
- package/src/modules/workspace/read-page.ts +228 -0
- package/src/modules/workspace/reconcile.ts +27 -15
- package/src/modules/workspace/result-text.ts +121 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/text.ts +58 -32
- package/src/modules/workspace/types.ts +137 -60
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +62 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +162 -0
- package/src/silent-heartbeat.ts +17 -10
- package/src/stable-hash.ts +14 -0
- package/src/stored-injections.ts +131 -0
- package/src/text-as-stored.ts +4 -3
- package/src/thinking-drops.ts +135 -0
- package/src/tool-args.ts +34 -1
- package/src/tool-image-ledger.ts +9 -8
- package/src/tool-presentation.ts +3 -1
- package/src/tool-result-guard.ts +45 -17
- package/src/tool-result-history.ts +471 -90
- package/src/tune-out/coordinator.ts +80 -37
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +220 -7
- package/src/types/events.ts +33 -9
- package/src/types/framework.ts +36 -3
- package/src/types/module.ts +91 -9
- package/src/types/trace.ts +94 -3
|
@@ -47,6 +47,7 @@ import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'n
|
|
|
47
47
|
import type { FileHandle } from 'node:fs/promises';
|
|
48
48
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
49
49
|
import { sameRootIdentity, type Fingerprint, type RootIdentity } from './disk-agreement.js';
|
|
50
|
+
import { errnoCode, systemWords } from './answers.js';
|
|
50
51
|
|
|
51
52
|
export interface MountView {
|
|
52
53
|
/**
|
|
@@ -84,6 +85,8 @@ export interface MountView {
|
|
|
84
85
|
nested?: readonly NestedMount[];
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
export type NestedDir = { dir: string; guessed?: true };
|
|
89
|
+
|
|
87
90
|
/**
|
|
88
91
|
* A mount nested in another: its directory in one of its spellings,
|
|
89
92
|
* mount-relative and `/`-separated, its name, its real root, which every
|
|
@@ -94,7 +97,7 @@ export interface MountView {
|
|
|
94
97
|
* it: the mount is nested by its configured path alone, since through its
|
|
95
98
|
* real root it would claim this mount's every path.
|
|
96
99
|
*/
|
|
97
|
-
export type NestedMount =
|
|
100
|
+
export type NestedMount = NestedDir & { name: string; real?: string; include?: readonly string[] };
|
|
98
101
|
|
|
99
102
|
export type StatFingerprint = Omit<Fingerprint, 'hashedAt'>;
|
|
100
103
|
|
|
@@ -145,11 +148,15 @@ export const RACY_WINDOW_MS = 2000;
|
|
|
145
148
|
|
|
146
149
|
export const OUTSIDE_PARENT = 'a parent directory resolves outside the mount';
|
|
147
150
|
export const NOT_FOLLOWED = 'a symlink, which this mount does not follow';
|
|
151
|
+
export const DIRECTORY = 'a directory';
|
|
152
|
+
export const NOT_REGULAR = 'not a regular file';
|
|
153
|
+
export const LEAVES_MOUNT = 'a symlink that leaves the mount';
|
|
154
|
+
export const NOT_A_DIRECTORY = 'a parent path is not a directory';
|
|
148
155
|
export const ROOT_UNAVAILABLE = 'the mount root is unavailable';
|
|
149
156
|
export const ROOT_REPLACED =
|
|
150
157
|
'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
|
|
151
158
|
'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
|
|
152
|
-
const CHANGED = 'it changed while it was observed';
|
|
159
|
+
export const CHANGED = 'it changed while it was observed';
|
|
153
160
|
const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
154
161
|
const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
|
|
155
162
|
|
|
@@ -279,21 +286,23 @@ export async function linkLeads(root: string, rootReal: string, link: string): P
|
|
|
279
286
|
* innermost where nested mounts nest, as #460 picks one; undefined when none
|
|
280
287
|
* does.
|
|
281
288
|
*/
|
|
282
|
-
export function nestedHolder<N extends
|
|
283
|
-
return nested
|
|
284
|
-
.filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
|
|
285
|
-
.sort((a, b) => b.dir.length - a.dir.length)[0];
|
|
289
|
+
export function nestedHolder<N extends NestedDir & { name: string }>(target: string, nested: readonly N[]): N | undefined {
|
|
290
|
+
return nestedHolders(target, nested)[0];
|
|
286
291
|
}
|
|
287
292
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return
|
|
293
|
+
function nestedHolders<N extends NestedDir>(target: string, nested: readonly N[]): N[] {
|
|
294
|
+
const segments = target.split('/');
|
|
295
|
+
return mayHold(nested.filter((n) => beneathNested(segments, n)), (n) => n.dir.split('/').length, (n) => !n.guessed && surelyBeneath(segments, n.dir.split('/')));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function mayHold<T>(holders: readonly T[], depth: (holder: T) => number, sure: (holder: T) => boolean): T[] {
|
|
299
|
+
const sorted = [...holders].sort((a, b) => depth(b) - depth(a) || Number(sure(b)) - Number(sure(a)));
|
|
300
|
+
const surely = sorted.findIndex(sure);
|
|
301
|
+
return surely === -1 ? sorted : sorted.slice(0, surely + 1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function namesHolder(leads: { target: string; directory: boolean }, nested: readonly NestedMount[]): boolean {
|
|
305
|
+
return nestedHolders(leads.target, nested).every((holder) => namesNestedPlace(restBeneath(leads.target, holder), holder.include, leads.directory));
|
|
297
306
|
}
|
|
298
307
|
|
|
299
308
|
/**
|
|
@@ -312,18 +321,12 @@ export function namesNestedPlace(rest: string, include: readonly string[] | unde
|
|
|
312
321
|
return directory || insideMount(rest, include);
|
|
313
322
|
}
|
|
314
323
|
|
|
315
|
-
/**
|
|
316
|
-
* Whether a path of a mount is one its passes decide and its listings may
|
|
317
|
-
* name: inside its include, and, in a nested mount's directory, a place
|
|
318
|
-
* that mount names (namesNestedPlace), its directory included. History the
|
|
319
|
-
* mount took in before another was nested there, which that mount's
|
|
320
|
-
* include leaves out, is no mount's, so no pass decides it, and no listing
|
|
321
|
-
* names it or a directory only it implies.
|
|
322
|
-
*/
|
|
323
324
|
export function withinIncludes(view: Pick<MountView, 'include' | 'nested'>, path: string): boolean {
|
|
324
325
|
if (!insideMount(path, view.include)) return false;
|
|
325
|
-
|
|
326
|
-
|
|
326
|
+
return nestedHolders(path, view.nested ?? []).every((holder) => {
|
|
327
|
+
const rest = restBeneath(path, holder);
|
|
328
|
+
return rest === '' || namesNestedPlace(rest, holder.include, false);
|
|
329
|
+
});
|
|
327
330
|
}
|
|
328
331
|
|
|
329
332
|
/**
|
|
@@ -414,16 +417,17 @@ export async function coversLanding(view: Pick<MountView, 'include'>, rootReal:
|
|
|
414
417
|
*/
|
|
415
418
|
export function linkWords(name: string, found: LinkFound, leads: LinkLeads | null, nested: readonly NestedMount[] = [], include?: readonly string[]): string {
|
|
416
419
|
const spell = (rel: string) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
417
|
-
if (!('link' in found)) return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
|
|
420
|
+
if (!('link' in found)) return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(errnoCode({ code: found.code }), found.code)})`;
|
|
418
421
|
const link = `${spell(found.link)} is a symlink`;
|
|
419
422
|
if (leads === null || leads.kind === 'nowhere') return `${link}, which this mount does not follow`;
|
|
420
423
|
if (leads.kind === 'outside') return `${link} leading outside the mount, which this mount does not follow`;
|
|
421
424
|
const holder = nestedHolder(leads.target, nested);
|
|
422
425
|
if (holder) {
|
|
423
|
-
if (!namesHolder(leads,
|
|
424
|
-
const rest = leads.target
|
|
425
|
-
|
|
426
|
-
|
|
426
|
+
if (!namesHolder(leads, nested)) return `${link}, which this mount does not follow`;
|
|
427
|
+
const rest = restBeneath(leads.target, holder);
|
|
428
|
+
const guess = nestedGuess(name, leads.target.split('/'), holder);
|
|
429
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
|
|
430
|
+
`"${holder.name}", nested in mount "${name}": ${nestedStep({ name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, guess }, { after: ` in place of ${spell(found.link)}` })}`;
|
|
427
431
|
}
|
|
428
432
|
if (!leads.directory && !insideMount(leads.target, include)) return `${link}, which this mount does not follow`;
|
|
429
433
|
// Following it serves only what the include covers where it leads (judgedAt).
|
|
@@ -450,22 +454,81 @@ export async function linkReason(view: Pick<MountView, 'root' | 'name' | 'nested
|
|
|
450
454
|
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
451
455
|
}
|
|
452
456
|
|
|
457
|
+
export function realSegments(path: string): string[] {
|
|
458
|
+
return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function beneathRoot(path: string, root: string): string | undefined {
|
|
462
|
+
const segments = realSegments(path);
|
|
463
|
+
const top = realSegments(root);
|
|
464
|
+
return segments.length > top.length && beginsWith(segments, top) ? segments.slice(top.length).join('/') : undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function sameRoot(path: string, root: string): boolean {
|
|
468
|
+
const segments = realSegments(path);
|
|
469
|
+
const top = realSegments(root);
|
|
470
|
+
return segments.length === top.length && beginsWith(segments, top);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function surelyAtRoot(path: string, root: string): boolean {
|
|
474
|
+
return surelyBeneath(realSegments(path), realSegments(root));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function nestedRootAtReal(real: string, nested: readonly NestedMount[] = []): { mount: NestedMount; depth: number } | undefined {
|
|
478
|
+
return nestedRootsAtReal(real, nested)[0];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function nestedRootsAtReal(real: string, nested: readonly NestedMount[]): Array<{ mount: NestedMount; depth: number }> {
|
|
482
|
+
// realpath keeps the spelling it's given where disk folds it on Linux, as
|
|
483
|
+
// from a link's target, so a real root is compared as loosely as a path.
|
|
484
|
+
const segments = realSegments(real);
|
|
485
|
+
const holders = nested.flatMap((n) => {
|
|
486
|
+
if (n.real === undefined) return [];
|
|
487
|
+
const root = realSegments(n.real);
|
|
488
|
+
return beginsWith(segments, root) ? [{ mount: n, depth: root.length, sure: surelyBeneath(segments, root) }] : [];
|
|
489
|
+
});
|
|
490
|
+
return mayHold(holders, (h) => h.depth, (h) => h.sure);
|
|
491
|
+
}
|
|
492
|
+
|
|
453
493
|
/**
|
|
454
494
|
* The nested mount whose root holds `real`, a canonical path, the innermost,
|
|
455
|
-
* with the path to use there
|
|
456
|
-
*
|
|
495
|
+
* with the path to use there and the path beneath its root (`rest`);
|
|
496
|
+
* undefined when none does. A path the lexical checks pass
|
|
457
497
|
* (inOtherMount, the tools' holderOf) reaches another mount's directory this
|
|
458
498
|
* way through a symlink the mount follows, so each check that resolves a
|
|
459
|
-
* path canonically asks this too
|
|
460
|
-
* namesNestedPlace says the place is that mount's.
|
|
499
|
+
* path canonically asks this too.
|
|
461
500
|
*/
|
|
462
|
-
export function nestedAtReal(real: string, nested: readonly NestedMount[] = []): { name: string; use: string; rest: string;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
.sort((a, b) => b.real.length - a.real.length)[0];
|
|
501
|
+
export async function nestedAtReal(real: string, nested: readonly NestedMount[] = [], at?: { name: string; rootReal: string }): Promise<{ name: string; use: string; rest: string; judgedByInclude: boolean; names: (directory: boolean) => boolean; guess?: NestedGuess } | undefined> {
|
|
502
|
+
const holders = nestedRootsAtReal(real, nested);
|
|
503
|
+
const holder = holders[0];
|
|
466
504
|
if (!holder) return undefined;
|
|
467
|
-
const
|
|
468
|
-
|
|
505
|
+
const segments = realSegments(real);
|
|
506
|
+
// realpath keeps a link target's spelling beneath the root too, so the
|
|
507
|
+
// rest is read as disk spells it, for each mount's include to judge.
|
|
508
|
+
const rests = await Promise.all(holders.map(async (h) => {
|
|
509
|
+
const given = segments.slice(h.depth).join('/');
|
|
510
|
+
return given === '' ? '' : await diskSpelling(h.mount.real!, given);
|
|
511
|
+
}));
|
|
512
|
+
const rest = rests[0]!;
|
|
513
|
+
const { name } = holder.mount;
|
|
514
|
+
const root = realSegments(holder.mount.real!);
|
|
515
|
+
const found = {
|
|
516
|
+
name,
|
|
517
|
+
use: rest ? `${name}/${rest}` : name,
|
|
518
|
+
rest,
|
|
519
|
+
judgedByInclude: holders.some((h) => h.mount.include !== undefined),
|
|
520
|
+
names: (directory: boolean) => holders.every((h, i) => namesNestedPlace(rests[i]!, h.mount.include, directory)),
|
|
521
|
+
};
|
|
522
|
+
if (surelyBeneath(segments, root)) return found;
|
|
523
|
+
if (at === undefined) return { ...found, guess: null };
|
|
524
|
+
const top = realSegments(at.rootReal);
|
|
525
|
+
if (segments.length <= top.length || root.length <= top.length || !top.every((t, i) => segments[i] === t)) return { ...found, guess: null };
|
|
526
|
+
const within = segments.slice(top.length);
|
|
527
|
+
const below = root.slice(top.length);
|
|
528
|
+
const guess = guessFor(at.name, within, below);
|
|
529
|
+
const leads = guess === undefined ? [at.name, ...within].join('/') : [guess.given, ...within.slice(below.length)].join('/');
|
|
530
|
+
if (!surelyBeneath(root.slice(0, top.length), top)) return { ...found, guess: { ...placedGuess(at.name, within.slice(0, below.length), guess), leads } };
|
|
531
|
+
return { ...found, guess: guess === undefined ? null : { ...guess, leads } };
|
|
469
532
|
}
|
|
470
533
|
|
|
471
534
|
/**
|
|
@@ -473,8 +536,9 @@ export function nestedAtReal(real: string, nested: readonly NestedMount[] = []):
|
|
|
473
536
|
* refused (nestedAtReal): what it leads into, and the path to use in its
|
|
474
537
|
* place. Spelled as the tools take paths, from the mount's name.
|
|
475
538
|
*/
|
|
476
|
-
export function leadsInto(name: string, rel: string, holder: { name: string; use: string }): string {
|
|
477
|
-
|
|
539
|
+
export function leadsInto(name: string, rel: string, holder: { name: string; use: string; guess?: NestedGuess }): string {
|
|
540
|
+
const verb = holder.guess === undefined ? 'leads' : 'may lead';
|
|
541
|
+
return `"${rel === '' ? name : `${name}/${rel}`}" ${verb} into mount "${holder.name}", nested in mount "${name}": ${nestedStep(holder, { after: ' in its place' })}`;
|
|
478
542
|
}
|
|
479
543
|
|
|
480
544
|
// ===========================================================================
|
|
@@ -488,9 +552,8 @@ const otherCase = (c: string): string => (c === c.toLowerCase() ? c.toUpperCase(
|
|
|
488
552
|
|
|
489
553
|
/**
|
|
490
554
|
* The name with each letter's case flipped, where its other case is one
|
|
491
|
-
* letter whose own other case is the letter again
|
|
492
|
-
*
|
|
493
|
-
* and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
555
|
+
* letter whose own other case is the letter again. `ß`, whose upper case is
|
|
556
|
+
* `SS`, and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
494
557
|
*/
|
|
495
558
|
const flippedCase = (name: string): string =>
|
|
496
559
|
[...name].map((c) => {
|
|
@@ -509,6 +572,17 @@ const folded = (name: string): string => name.normalize('NFC').toLowerCase();
|
|
|
509
572
|
*/
|
|
510
573
|
export const foldedLoosely = (name: string): string => name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase();
|
|
511
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Linux's casefold, which ext4 and f2fs set per directory, drops
|
|
577
|
+
* default-ignorable code points (on every kernel but 6.12 to 6.12.4) and
|
|
578
|
+
* compares names decomposed, in full case folding, and HFS+ passes over some
|
|
579
|
+
* of those code points too. Case mapped on the decomposed name joins marks
|
|
580
|
+
* that come in another order around a combining ypogegrammeni (U+0345),
|
|
581
|
+
* whose upper case is a letter.
|
|
582
|
+
*/
|
|
583
|
+
const foldedFully = (name: string): string =>
|
|
584
|
+
name.replace(/\p{Default_Ignorable_Code_Point}/gu, '').normalize('NFD').toLowerCase().toUpperCase().toLowerCase().normalize('NFD');
|
|
585
|
+
|
|
512
586
|
/** What the workspace holds of a mount, as the spelling step reads it (diskSpelling). */
|
|
513
587
|
export interface WorkspaceEntries {
|
|
514
588
|
/**
|
|
@@ -616,18 +690,81 @@ function entryName(paths: readonly string[], prefix: string, segment: string): s
|
|
|
616
690
|
return found;
|
|
617
691
|
}
|
|
618
692
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
693
|
+
function sameLevel(given: string, name: string): boolean {
|
|
694
|
+
return given === name || (given !== '' && foldedFully(given) === foldedFully(name));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function beginsWith(segments: readonly string[], dir: readonly string[]): boolean {
|
|
698
|
+
return segments.length >= dir.length && dir.every((d, i) => sameLevel(segments[i]!, d));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function beneathNested(segments: readonly string[], nested: NestedDir): boolean {
|
|
702
|
+
return beginsWith(segments, nested.dir.split('/'));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function restBeneath(path: string, nested: NestedDir): string {
|
|
706
|
+
// By segments, since a name that matches can be another length, as `SS`
|
|
707
|
+
// is beside `ß`.
|
|
708
|
+
return path.split('/').slice(nested.dir.split('/').length).join('/');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function surelyBeneath(segments: readonly string[], dir: readonly string[]): boolean {
|
|
712
|
+
return segments.length >= dir.length && dir.every((d, i) => segments[i] === d);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function shownByCodePoints(name: string): string {
|
|
716
|
+
return [...name].map((c) => {
|
|
717
|
+
const code = c.codePointAt(0)!;
|
|
718
|
+
if (c === '\\') return '\\\\';
|
|
719
|
+
return code < 0x80 ? c : code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
|
|
720
|
+
}).join('');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export type NestedGuess = { given: string; dir: string; leads?: string } | { placed: string; dir?: string; leads?: string } | null;
|
|
724
|
+
|
|
725
|
+
function guessFor(mount: string, given: readonly string[], dir: readonly string[]): Extract<NestedGuess, { given: string }> | undefined {
|
|
726
|
+
if (surelyBeneath(given, dir)) return undefined;
|
|
727
|
+
const levels = given.slice(0, dir.length);
|
|
728
|
+
const plain = (a: string, b: string) => a === b || !/[^\x00-\x7f]/.test(a + b);
|
|
729
|
+
const shown = (name: string, other: string) => (plain(name, other) ? name : shownByCodePoints(name));
|
|
730
|
+
return {
|
|
731
|
+
given: [mount, ...levels.map((g, i) => shown(g, dir[i]!))].join('/'),
|
|
732
|
+
dir: [mount, ...dir.map((d, i) => shown(d, levels[i]!))].join('/'),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function nestedGuess(mount: string, segments: readonly string[], nested: NestedDir): NestedGuess | undefined {
|
|
737
|
+
const dir = nested.dir.split('/');
|
|
738
|
+
const guess = guessFor(mount, segments, dir);
|
|
739
|
+
return nested.guessed ? placedGuess(mount, segments.slice(0, dir.length), guess) : guess;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function placedGuess(mount: string, levels: readonly string[], guess: ReturnType<typeof guessFor>): Extract<NestedGuess, { placed: string }> {
|
|
743
|
+
return guess === undefined ? { placed: [mount, ...levels].join('/') } : { placed: guess.given, dir: guess.dir };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function nestedStep(holder: { name: string; use: string; guess?: NestedGuess }, opts: { after?: string; named?: boolean } = {}): string {
|
|
747
|
+
const { guess } = holder;
|
|
748
|
+
const after = opts.after ?? '';
|
|
749
|
+
if (guess === undefined) return `use "${holder.use}"${after}`;
|
|
750
|
+
if (guess === null) {
|
|
751
|
+
return `this workspace can't tell whether where it leads is that mount's directory. ` +
|
|
752
|
+
`If you meant that mount's directory, use "${holder.use}"${after}; if not, ask whoever runs this workspace`;
|
|
753
|
+
}
|
|
754
|
+
const leads = guess.leads === undefined ? '' : `it leads to "${guess.leads}", and `;
|
|
755
|
+
if ('placed' in guess) {
|
|
756
|
+
const check = 'ask whoever runs this workspace to check where the two mounts are configured';
|
|
757
|
+
const outside = guess.dir === undefined ? `"${guess.placed}"` : `"${guess.placed}" and "${guess.dir}"`;
|
|
758
|
+
return `${leads}this workspace can't tell whether "${guess.placed}" is mount "${holder.name}"'s directory, since the two mounts' roots are configured in ` +
|
|
759
|
+
`spellings it can't tell apart. If you meant that mount's directory, use "${holder.use}"${after}. ` +
|
|
760
|
+
(opts.named ? `If you meant one of this mount's own, for a new file use a path outside ${outside}, and for one already there ${check}` : `If not, ${check}`);
|
|
761
|
+
}
|
|
762
|
+
const remedy = `rename one of "${guess.given}" and "${guess.dir}"`;
|
|
763
|
+
const own = opts.named
|
|
764
|
+
? `If you meant one of this mount's own, for a new file use a path outside "${guess.given}" and "${guess.dir}", and for one already there ask whoever runs this workspace to ${remedy}`
|
|
765
|
+
: `If not, ask whoever runs this workspace to ${remedy}`;
|
|
766
|
+
return `${leads}this workspace can't tell "${guess.given}" from "${guess.dir}", ` +
|
|
767
|
+
`mount "${holder.name}"'s directory. If you meant that mount's directory, use "${holder.use}"${after}. ${own}`;
|
|
631
768
|
}
|
|
632
769
|
|
|
633
770
|
/**
|
|
@@ -825,8 +962,8 @@ export type DirCheck =
|
|
|
825
962
|
* holds the non-directory in its place.
|
|
826
963
|
*/
|
|
827
964
|
| { kind: 'absent'; by: string }
|
|
828
|
-
/** Unproven either way; `outside` when it resolves outside the mount. */
|
|
829
|
-
| { kind: 'unobserved'; reason: string; outside?: true };
|
|
965
|
+
/** Unproven either way; `outside` when it resolves outside the mount, `errno` when a system call failed. */
|
|
966
|
+
| { kind: 'unobserved'; reason: string; outside?: true; errno?: string };
|
|
830
967
|
|
|
831
968
|
/** Errors meaning a directory can't be synced on this filesystem, not that syncing it failed. */
|
|
832
969
|
const DIRECTORY_SYNC_UNSUPPORTED = new Set(['EINVAL', 'ENOTSUP']);
|
|
@@ -846,11 +983,16 @@ export class CheckedDir {
|
|
|
846
983
|
private readonly lexical: string,
|
|
847
984
|
private readonly dev: bigint,
|
|
848
985
|
private readonly ino: bigint,
|
|
986
|
+
private readonly born: bigint,
|
|
849
987
|
) {}
|
|
850
988
|
|
|
851
989
|
/** Which directory it is. */
|
|
852
990
|
identity(): RootIdentity {
|
|
853
|
-
return {
|
|
991
|
+
return {
|
|
992
|
+
dev: String(this.dev),
|
|
993
|
+
ino: String(this.ino),
|
|
994
|
+
...(this.born > 0n ? { born: String(this.born) } : {}),
|
|
995
|
+
};
|
|
854
996
|
}
|
|
855
997
|
|
|
856
998
|
/** A path that reaches `name` in this directory. */
|
|
@@ -892,11 +1034,25 @@ export class CheckedDir {
|
|
|
892
1034
|
}
|
|
893
1035
|
}
|
|
894
1036
|
|
|
1037
|
+
/**
|
|
1038
|
+
* What is at a mount root's path now, followed as the root is: for a
|
|
1039
|
+
* directory, undefined; for anything else, ENOTDIR; and where the path
|
|
1040
|
+
* can't be followed to anything, the errno its stat failed with, ENOENT
|
|
1041
|
+
* when nothing is there.
|
|
1042
|
+
*/
|
|
1043
|
+
export async function rootAbsence(root: string): Promise<string | undefined> {
|
|
1044
|
+
try {
|
|
1045
|
+
return (await stat(root)).isDirectory() ? undefined : 'ENOTDIR';
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
return errno(err);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
895
1051
|
/** Check the directory at a canonical location, if one is there. */
|
|
896
1052
|
async function checkedAt(view: MountView, rel: string, real: string): Promise<CheckedDir | null> {
|
|
897
1053
|
const info = await lstat(real, { bigint: true }).catch(() => null);
|
|
898
1054
|
if (!info?.isDirectory()) return null;
|
|
899
|
-
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino);
|
|
1055
|
+
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs);
|
|
900
1056
|
}
|
|
901
1057
|
|
|
902
1058
|
/**
|
|
@@ -910,12 +1066,12 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
910
1066
|
real = await realpath(lexical);
|
|
911
1067
|
} catch (err) {
|
|
912
1068
|
const code = errno(err);
|
|
913
|
-
if (code === 'ENOENT' || code === 'ENOTDIR') return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE } : { kind: 'missing' };
|
|
1069
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE, errno: code } : { kind: 'missing' };
|
|
914
1070
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
915
1071
|
// link this mount doesn't follow.
|
|
916
1072
|
const found = rel === '' ? null : await linkOnTheWay(view, rel);
|
|
917
1073
|
if (found !== null) return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
|
|
918
|
-
return { kind: 'unobserved', reason: `cannot resolve the directory (${code
|
|
1074
|
+
return { kind: 'unobserved', reason: `cannot resolve the directory (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
919
1075
|
}
|
|
920
1076
|
// A mount that doesn't follow symlinks goes through none on the way to a
|
|
921
1077
|
// directory, inside the mount or out of it.
|
|
@@ -932,10 +1088,10 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
932
1088
|
// mount's, which the mount neither reads nor writes. A place there that
|
|
933
1089
|
// mount's include leaves out is no mount's, and holds nothing beneath it,
|
|
934
1090
|
// as at any file (namesNestedPlace).
|
|
935
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1091
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
936
1092
|
if (holder) {
|
|
937
|
-
const directory = holder.
|
|
938
|
-
return
|
|
1093
|
+
const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
1094
|
+
return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
|
|
939
1095
|
}
|
|
940
1096
|
}
|
|
941
1097
|
const checked = await checkedAt(view, rel, real);
|
|
@@ -961,10 +1117,10 @@ export async function locateRoot(view: MountView, rootReal: string): Promise<{ i
|
|
|
961
1117
|
}
|
|
962
1118
|
|
|
963
1119
|
/** The mount root as locateRoot finds it, checked, so a caller can ask later whether it's still there. */
|
|
964
|
-
export async function checkRoot(view: MountView, rootReal: string): Promise<{ dir: CheckedDir } | { reason: string }> {
|
|
1120
|
+
export async function checkRoot(view: MountView, rootReal: string): Promise<{ dir: CheckedDir } | { reason: string; errno?: string }> {
|
|
965
1121
|
const found = await locateDir(view, rootReal, '');
|
|
966
1122
|
if (found.kind === 'checked') return { dir: found.dir };
|
|
967
|
-
return
|
|
1123
|
+
return found.kind === 'unobserved' ? { reason: found.reason, ...(found.errno !== undefined ? { errno: found.errno } : {}) } : { reason: ROOT_UNAVAILABLE };
|
|
968
1124
|
}
|
|
969
1125
|
|
|
970
1126
|
/**
|
|
@@ -999,13 +1155,13 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
999
1155
|
const childRel = parts.slice(0, i + 1).join('/');
|
|
1000
1156
|
// Nothing is made in a nested mount's directory, its root included where
|
|
1001
1157
|
// it isn't made yet, by a path that reaches it through a link.
|
|
1002
|
-
const holder = nestedAtReal(base.at(parts[i]!), view.nested);
|
|
1158
|
+
const holder = await nestedAtReal(base.at(parts[i]!), view.nested, { name: view.name, rootReal });
|
|
1003
1159
|
if (holder) return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
|
|
1004
1160
|
try {
|
|
1005
1161
|
await mkdir(base.at(parts[i]!));
|
|
1006
1162
|
} catch (err) {
|
|
1007
1163
|
const code = errno(err);
|
|
1008
|
-
if (code !== 'EEXIST') return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${code
|
|
1164
|
+
if (code !== 'EEXIST') return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
1009
1165
|
}
|
|
1010
1166
|
// Made, or there already (made meanwhile, or not a directory): it must
|
|
1011
1167
|
// be a real directory now, never a symlink or file in its place.
|
|
@@ -1015,7 +1171,7 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1015
1171
|
// doesn't follow symlinks, is named as one.
|
|
1016
1172
|
const there = view.followSymlinks ? null : await lstat(base.at(parts[i]!)).catch(() => null);
|
|
1017
1173
|
if (there?.isSymbolicLink()) return { kind: 'unobserved', reason: await linkReason(view, rootReal, { link: childRel }) };
|
|
1018
|
-
return { kind: 'unobserved', reason:
|
|
1174
|
+
return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
|
|
1019
1175
|
}
|
|
1020
1176
|
base = checked;
|
|
1021
1177
|
}
|
|
@@ -1058,13 +1214,13 @@ export async function confirmAbsent(view: MountView, rootReal: string, rel: stri
|
|
|
1058
1214
|
} catch (err) {
|
|
1059
1215
|
const code = errno(err);
|
|
1060
1216
|
if (code === 'ENOTDIR') return CHANGED;
|
|
1061
|
-
if (code !== 'ENOENT') return `cannot stat (${code
|
|
1217
|
+
if (code !== 'ENOENT') return `cannot stat (${systemWords(code, err)})`;
|
|
1062
1218
|
// Absent from a directory that, like each above it, is still the one its path names.
|
|
1063
1219
|
return proven();
|
|
1064
1220
|
}
|
|
1065
1221
|
if (info.isDirectory()) {
|
|
1066
1222
|
if (last) return CHANGED; // present after all
|
|
1067
|
-
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino);
|
|
1223
|
+
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
|
|
1068
1224
|
walked.push(cur);
|
|
1069
1225
|
continue;
|
|
1070
1226
|
}
|
|
@@ -1173,12 +1329,12 @@ async function parentPlacement(view: MountView, rootReal: string, relativePath:
|
|
|
1173
1329
|
const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
|
|
1174
1330
|
if (found !== null) return { reason: await linkReason(view, rootReal, found) };
|
|
1175
1331
|
if (!contained(rootReal, parent)) return { reason: OUTSIDE_PARENT };
|
|
1176
|
-
const holder = nestedAtReal(parent, view.nested);
|
|
1332
|
+
const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
|
|
1177
1333
|
if (!holder) return { inside: parent };
|
|
1178
1334
|
// A place there that the nested mount's include leaves out is no mount's,
|
|
1179
1335
|
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1180
|
-
const directory = holder.
|
|
1181
|
-
return
|
|
1336
|
+
const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1337
|
+
return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
|
|
1182
1338
|
} catch (err) {
|
|
1183
1339
|
const code = errno(err);
|
|
1184
1340
|
if (code === 'ENOENT' || code === 'ENOTDIR') return 'gone';
|
|
@@ -1186,7 +1342,7 @@ async function parentPlacement(view: MountView, rootReal: string, relativePath:
|
|
|
1186
1342
|
// link this mount doesn't follow.
|
|
1187
1343
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1188
1344
|
if (found !== null) return { reason: await linkReason(view, rootReal, found) };
|
|
1189
|
-
return { reason: `cannot resolve the parent directory (${code
|
|
1345
|
+
return { reason: `cannot resolve the parent directory (${systemWords(code, err)})` };
|
|
1190
1346
|
}
|
|
1191
1347
|
}
|
|
1192
1348
|
|
|
@@ -1207,7 +1363,7 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1207
1363
|
} catch (err) {
|
|
1208
1364
|
const code = errno(err);
|
|
1209
1365
|
if (code === 'ENOENT' || code === 'ENOTDIR') return absence(view, rootReal, relativePath);
|
|
1210
|
-
return { kind: 'unobserved', reason: `cannot stat (${code
|
|
1366
|
+
return { kind: 'unobserved', reason: `cannot stat (${systemWords(code, err)})` };
|
|
1211
1367
|
}
|
|
1212
1368
|
if (info.isSymbolicLink()) {
|
|
1213
1369
|
if (!view.followSymlinks) return { kind: 'unobserved', reason: NOT_FOLLOWED };
|
|
@@ -1215,37 +1371,37 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1215
1371
|
try {
|
|
1216
1372
|
target = await stat(lexical, { bigint: true });
|
|
1217
1373
|
} catch (err) {
|
|
1218
|
-
return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${errno(err)
|
|
1374
|
+
return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${systemWords(errno(err), err)})` };
|
|
1219
1375
|
}
|
|
1220
1376
|
let real: string;
|
|
1221
1377
|
try {
|
|
1222
1378
|
real = await realpath(lexical);
|
|
1223
1379
|
} catch (err) {
|
|
1224
|
-
return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${errno(err)
|
|
1380
|
+
return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${systemWords(errno(err), err)})` };
|
|
1225
1381
|
}
|
|
1226
|
-
if (!contained(rootReal, real)) return { kind: 'unobserved', reason:
|
|
1227
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1382
|
+
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1383
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1228
1384
|
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1229
1385
|
// leaves this one (namesNestedPlace).
|
|
1230
1386
|
if (holder) {
|
|
1231
|
-
return { kind: 'unobserved', reason:
|
|
1387
|
+
return { kind: 'unobserved', reason: holder.names(target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : LEAVES_MOUNT };
|
|
1232
1388
|
}
|
|
1233
1389
|
if (!target.isFile()) {
|
|
1234
|
-
return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' :
|
|
1390
|
+
return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : NOT_REGULAR };
|
|
1235
1391
|
}
|
|
1236
1392
|
// A file this mount's own include leaves out where the link lands is no
|
|
1237
1393
|
// file of this mount's (coversLanding). It's judged here, and not only
|
|
1238
1394
|
// where the file is opened, since a pass takes a copy whose fingerprint
|
|
1239
1395
|
// the stat vouches for without opening it.
|
|
1240
|
-
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason:
|
|
1396
|
+
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1241
1397
|
info = target;
|
|
1242
1398
|
} else if (info.isDirectory()) {
|
|
1243
|
-
return { kind: 'unobserved', reason:
|
|
1399
|
+
return { kind: 'unobserved', reason: DIRECTORY };
|
|
1244
1400
|
} else if (!info.isFile()) {
|
|
1245
|
-
return { kind: 'unobserved', reason:
|
|
1401
|
+
return { kind: 'unobserved', reason: NOT_REGULAR };
|
|
1246
1402
|
} else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
|
|
1247
1403
|
// So is one reached through a link among its directories.
|
|
1248
|
-
return { kind: 'unobserved', reason:
|
|
1404
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1249
1405
|
}
|
|
1250
1406
|
return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 0o777n), fp: statFingerprint(info) };
|
|
1251
1407
|
}
|
|
@@ -1282,8 +1438,8 @@ async function openContained(
|
|
|
1282
1438
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1283
1439
|
return { kind: 'unobserved', reason: found === null ? NOT_FOLLOWED : await linkReason(view, rootReal, found) };
|
|
1284
1440
|
}
|
|
1285
|
-
if (code === 'EISDIR') return { kind: 'unobserved', reason:
|
|
1286
|
-
return { kind: 'unobserved', reason: `cannot open (${code
|
|
1441
|
+
if (code === 'EISDIR') return { kind: 'unobserved', reason: DIRECTORY };
|
|
1442
|
+
return { kind: 'unobserved', reason: `cannot open (${systemWords(code, err)})` };
|
|
1287
1443
|
}
|
|
1288
1444
|
let keep = false;
|
|
1289
1445
|
try {
|
|
@@ -1297,13 +1453,13 @@ async function openContained(
|
|
|
1297
1453
|
// descriptor holds, so a link gets its own answer whatever it leads to.
|
|
1298
1454
|
const found = await linkRefusal(view, rootReal, relativePath, real);
|
|
1299
1455
|
if (found !== null) return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
|
|
1300
|
-
if (!info.isFile()) return { kind: 'unobserved', reason: info.isDirectory() ?
|
|
1301
|
-
const leaves = view.followSymlinks ?
|
|
1456
|
+
if (!info.isFile()) return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
|
|
1457
|
+
const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
|
|
1302
1458
|
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: leaves };
|
|
1303
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1459
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1304
1460
|
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1305
1461
|
// through a link this mount doesn't follow, the link is what's refused.
|
|
1306
|
-
if (holder) return { kind: 'unobserved', reason:
|
|
1462
|
+
if (holder) return { kind: 'unobserved', reason: holder.names(false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1307
1463
|
// So is a file this mount's own include leaves out (coversLanding).
|
|
1308
1464
|
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1309
1465
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
@@ -1325,7 +1481,7 @@ async function openContained(
|
|
|
1325
1481
|
} catch (err) {
|
|
1326
1482
|
const code = errno(err);
|
|
1327
1483
|
if (code === 'ENOENT') return absence(view, rootReal, relativePath);
|
|
1328
|
-
return { kind: 'unobserved', reason: `cannot read (${code
|
|
1484
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
|
|
1329
1485
|
} finally {
|
|
1330
1486
|
if (!keep) await handle.close();
|
|
1331
1487
|
}
|
|
@@ -1423,7 +1579,7 @@ export async function readPath(
|
|
|
1423
1579
|
} catch (err) {
|
|
1424
1580
|
const code = errno(err);
|
|
1425
1581
|
if (code === 'ENOENT') return absence(view, rootReal, relativePath);
|
|
1426
|
-
return { kind: 'unobserved', reason: `cannot read (${code
|
|
1582
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
|
|
1427
1583
|
} finally {
|
|
1428
1584
|
await handle.close();
|
|
1429
1585
|
}
|
|
@@ -1483,7 +1639,7 @@ export async function readBytes(
|
|
|
1483
1639
|
if (more) return { kind: 'bytes', size: Math.max(Number((await handle.stat({ bigint: true })).size), bytes.length + 1), bytes: null, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1484
1640
|
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1485
1641
|
} catch (err) {
|
|
1486
|
-
return { kind: 'unobserved', reason: `cannot read (${errno(err)
|
|
1642
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
|
|
1487
1643
|
} finally {
|
|
1488
1644
|
await handle.close();
|
|
1489
1645
|
}
|
|
@@ -1507,7 +1663,7 @@ export async function readHead(
|
|
|
1507
1663
|
const { bytesRead } = await handle.read(head, 0, HEAD_BYTES, 0);
|
|
1508
1664
|
return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
|
|
1509
1665
|
} catch (err) {
|
|
1510
|
-
return { kind: 'unobserved', reason: `cannot read (${errno(err)
|
|
1666
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
|
|
1511
1667
|
} finally {
|
|
1512
1668
|
await handle.close();
|
|
1513
1669
|
}
|
|
@@ -1636,14 +1792,14 @@ function subtreeAgainst(dir: string, at: string): 'before' | 'holds' | 'after' {
|
|
|
1636
1792
|
}
|
|
1637
1793
|
|
|
1638
1794
|
/**
|
|
1639
|
-
* Whether a mount's ignore list covers a mount-relative path: the path itself
|
|
1640
|
-
* or any directory above it, as the walk's own test reads
|
|
1641
|
-
* path and
|
|
1795
|
+
* Whether a mount's ignore list covers a mount-relative path: the path itself,
|
|
1796
|
+
* a directory or not, or any directory above it, as the walk's own test reads
|
|
1797
|
+
* each entry by its path and kind.
|
|
1642
1798
|
*/
|
|
1643
|
-
export function coveredByIgnore(relativePath: string, patterns: string[]): boolean {
|
|
1799
|
+
export function coveredByIgnore(relativePath: string, patterns: readonly string[], directory: boolean): boolean {
|
|
1644
1800
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1645
1801
|
for (let i = 0; i < segments.length; i++) {
|
|
1646
|
-
if (isIgnored(segments.slice(0, i + 1).join('/'),
|
|
1802
|
+
if (isIgnored(segments.slice(0, i + 1).join('/'), patterns, i < segments.length - 1 || directory)) return true;
|
|
1647
1803
|
}
|
|
1648
1804
|
return false;
|
|
1649
1805
|
}
|
|
@@ -1688,42 +1844,89 @@ export function insideMount(relativePath: string, include: readonly string[] | u
|
|
|
1688
1844
|
}
|
|
1689
1845
|
|
|
1690
1846
|
/**
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
* (pathSegments). A pattern the matcher can't read would match nothing, and
|
|
1697
|
-
* take every file out of the mount unsaid, so the module refuses it.
|
|
1847
|
+
* A pattern of `ignore` or `include`, read as gitignore reads it: its
|
|
1848
|
+
* segments, each a name or a glob with `*` and `?` within it; whether it's a
|
|
1849
|
+
* path from the mount's root (a leading or inner `/`, or a trailing `/**`)
|
|
1850
|
+
* rather than a name at any depth; and whether it matches only directories (a
|
|
1851
|
+
* trailing `/` or `/**`).
|
|
1698
1852
|
*/
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1853
|
+
type Pattern = { segments: Array<string | RegExp>; anchored: boolean; directory: boolean };
|
|
1854
|
+
|
|
1855
|
+
const readings = new Map<string, Pattern | string>();
|
|
1856
|
+
|
|
1857
|
+
function readPattern(pattern: string): Pattern | string {
|
|
1858
|
+
let reading = readings.get(pattern);
|
|
1859
|
+
if (reading === undefined) {
|
|
1860
|
+
reading = parsePattern(pattern);
|
|
1861
|
+
readings.set(pattern, reading);
|
|
1862
|
+
}
|
|
1863
|
+
return reading;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function parsePattern(pattern: string): Pattern | string {
|
|
1867
|
+
if (pattern.startsWith('!')) return 'a leading "!" makes an exception in a .gitignore, and this list has none, so leave it out';
|
|
1868
|
+
if (pattern.startsWith('#')) return 'a leading "#" marks a comment in a .gitignore, so leave comments out of the list';
|
|
1869
|
+
if (pattern.includes('\\')) return '"\\" is an escape in a .gitignore and a separator on Windows, so no pattern here holds one, and "?" stands for one in a name';
|
|
1870
|
+
if (pattern.endsWith(' ')) return 'a .gitignore drops trailing spaces, so remove them';
|
|
1871
|
+
const anyDepth = pattern.startsWith('**/');
|
|
1872
|
+
let body = anyDepth ? pattern.slice(3) : pattern;
|
|
1873
|
+
const beneath = body.endsWith('/**');
|
|
1874
|
+
const directory = beneath || body.endsWith('/');
|
|
1875
|
+
body = body.slice(0, beneath ? -3 : directory ? -1 : undefined);
|
|
1876
|
+
const anchored = (beneath && !anyDepth) || body.includes('/');
|
|
1877
|
+
if (body.startsWith('/') && !anyDepth) body = body.slice(1);
|
|
1878
|
+
const segments = body.split('/');
|
|
1879
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..')) return 'it has an empty, "." or ".." segment, which names no entry';
|
|
1880
|
+
if (body.includes('**') || (anyDepth && anchored)) return '"**" is read only as a leading "**/" before one name, or a trailing "/**"';
|
|
1881
|
+
const glob = (s: string): string | RegExp => /[*?]/.test(s)
|
|
1882
|
+
? new RegExp(`^${s.replace(/[.+^${}()|[\]*?]/g, (c) => (c === '*' ? '.*' : c === '?' ? '.' : `\\${c}`))}$`, 'su')
|
|
1883
|
+
: s;
|
|
1884
|
+
return { segments: segments.map(glob), anchored, directory };
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function nameMatches(segment: string | RegExp, name: string): boolean {
|
|
1888
|
+
return typeof segment === 'string' ? segment === name : segment.test(name);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function matchesEntry(pattern: Pattern, segments: readonly string[], directory: boolean): boolean {
|
|
1892
|
+
if (pattern.directory && !directory) return false;
|
|
1893
|
+
if (!pattern.anchored) return nameMatches(pattern.segments[0]!, segments[segments.length - 1]!);
|
|
1894
|
+
return segments.length === pattern.segments.length && pattern.segments.every((s, i) => nameMatches(s, segments[i]!));
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
export function patternRefusal(pattern: unknown, list: 'ignore' | 'include'): string | undefined {
|
|
1898
|
+
if (typeof pattern !== 'string') return "it isn't a string";
|
|
1899
|
+
const reading = readPattern(pattern);
|
|
1900
|
+
if (typeof reading === 'string') return reading;
|
|
1901
|
+
if (list === 'ignore' && pattern.includes('[')) {
|
|
1902
|
+
return '"[" starts a character class in a .gitignore, which this list doesn\'t read, so write each name out, with "?" for a "[" in it';
|
|
1903
|
+
}
|
|
1904
|
+
return undefined;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
export function directoryAt(path: string, followSymlinks: boolean): Promise<boolean | undefined> {
|
|
1908
|
+
if (!followSymlinks) return lstat(path).then((s) => s.isDirectory(), () => undefined);
|
|
1909
|
+
return stat(path).then((s) => s.isDirectory(), () => lstat(path).then(() => false, () => undefined));
|
|
1705
1910
|
}
|
|
1706
1911
|
|
|
1707
1912
|
/**
|
|
1708
|
-
* Whether a mount's include covers a mount-relative path
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
* no covering: where one is a scope, refusesAsOutside (index.ts) decides.
|
|
1913
|
+
* Whether a mount's include covers a mount-relative path, read as a file: a
|
|
1914
|
+
* pattern that matches only directories covers what lies beneath a directory
|
|
1915
|
+
* it matches, and any other covers a file by its own name or path, and
|
|
1916
|
+
* nothing beneath a directory it matches. So `gate.json` covers `x/gate.json`
|
|
1917
|
+
* and not `x/gate.json/run.sh`, and a subtree is named with `dir/` or
|
|
1918
|
+
* `dir/**`. A directory is no file, so it needs no covering: where one is a
|
|
1919
|
+
* scope, refusesAsOutside (index.ts) decides.
|
|
1716
1920
|
*/
|
|
1717
1921
|
function coveredByInclude(relativePath: string, include: readonly string[]): boolean {
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}
|
|
1726
|
-
return false;
|
|
1922
|
+
const segments = relativePath.split('/');
|
|
1923
|
+
return include.some((p) => {
|
|
1924
|
+
const pattern = readPattern(p);
|
|
1925
|
+
if (typeof pattern === 'string') return false;
|
|
1926
|
+
if (!pattern.directory) return matchesEntry(pattern, segments, false);
|
|
1927
|
+
for (let k = 1; k < segments.length; k++) if (matchesEntry(pattern, segments.slice(0, k), true)) return true;
|
|
1928
|
+
return false;
|
|
1929
|
+
});
|
|
1727
1930
|
}
|
|
1728
1931
|
|
|
1729
1932
|
/**
|
|
@@ -1732,7 +1935,7 @@ function coveredByInclude(relativePath: string, include: readonly string[]): boo
|
|
|
1732
1935
|
* it (#276); one covering only something inside it does.
|
|
1733
1936
|
*/
|
|
1734
1937
|
export function ignoreBeneath(scope: string, patterns: string[]): string[] {
|
|
1735
|
-
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern]));
|
|
1938
|
+
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern], true));
|
|
1736
1939
|
}
|
|
1737
1940
|
|
|
1738
1941
|
/**
|
|
@@ -1781,34 +1984,34 @@ export const OTHER_MOUNT = "another mount's directory";
|
|
|
1781
1984
|
* Every nested-mount check (a pass's, a walk's, a push's, grep's) asks this.
|
|
1782
1985
|
*/
|
|
1783
1986
|
export function inOtherMount(relativePath: string, others: readonly string[]): boolean {
|
|
1784
|
-
const
|
|
1785
|
-
return others.some((dir) =>
|
|
1987
|
+
const segments = posix.normalize(relativePath).replace(/\/+$/, '').split('/');
|
|
1988
|
+
return others.some((dir) => beneathNested(segments, { dir }));
|
|
1786
1989
|
}
|
|
1787
1990
|
|
|
1788
1991
|
/**
|
|
1789
|
-
* Why a walk passes over an entry by design, by its mount-relative path
|
|
1790
|
-
*
|
|
1791
|
-
* name. Undefined when none covers it.
|
|
1992
|
+
* Why a walk passes over an entry by design, by its mount-relative path, its
|
|
1993
|
+
* name and whether it's a directory: another mount's directory, the ignore
|
|
1994
|
+
* list, or an editor's file name. Undefined when none covers it.
|
|
1792
1995
|
*/
|
|
1793
|
-
function passedOverBecause(relativePath: string, name: string, patterns: string[], others: readonly string[]): string | undefined {
|
|
1996
|
+
function passedOverBecause(relativePath: string, name: string, patterns: string[], others: readonly string[], directory: boolean): string | undefined {
|
|
1794
1997
|
if (inOtherMount(relativePath, others)) return OTHER_MOUNT;
|
|
1795
|
-
if (isIgnored(relativePath,
|
|
1998
|
+
if (isIgnored(relativePath, patterns, directory)) return IGNORED;
|
|
1796
1999
|
if (EDITOR_FILE.test(name)) return EDITOR_FILE_REASON;
|
|
1797
2000
|
return undefined;
|
|
1798
2001
|
}
|
|
1799
2002
|
|
|
1800
2003
|
/**
|
|
1801
2004
|
* Why a sync without a path, and the watcher, pass over a mount-relative
|
|
1802
|
-
* path: the path itself or a directory above it is passed
|
|
1803
|
-
* the walk reads each entry by its path and
|
|
1804
|
-
* directory is that mount's, whatever else covers a
|
|
1805
|
-
* Undefined when nothing covers it.
|
|
2005
|
+
* path, a directory or not: the path itself or a directory above it is passed
|
|
2006
|
+
* over by design, as the walk reads each entry by its path, name and kind. A
|
|
2007
|
+
* path in another mount's directory is that mount's, whatever else covers a
|
|
2008
|
+
* directory above it. Undefined when nothing covers it.
|
|
1806
2009
|
*/
|
|
1807
|
-
export function passedOverAt(relativePath: string, patterns: string[], others: readonly string[]): string | undefined {
|
|
2010
|
+
export function passedOverAt(relativePath: string, patterns: string[], others: readonly string[], directory: boolean): string | undefined {
|
|
1808
2011
|
if (inOtherMount(relativePath, others)) return OTHER_MOUNT;
|
|
1809
2012
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1810
2013
|
for (let i = 0; i < segments.length; i++) {
|
|
1811
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others);
|
|
2014
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others, i < segments.length - 1 || directory);
|
|
1812
2015
|
if (why !== undefined) return why;
|
|
1813
2016
|
}
|
|
1814
2017
|
return undefined;
|
|
@@ -1824,24 +2027,19 @@ export function passedOverBetween(scope: string, relativePath: string, patterns:
|
|
|
1824
2027
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1825
2028
|
const from = scope === '' ? 0 : scope.split('/').filter(Boolean).length;
|
|
1826
2029
|
for (let i = from; i < segments.length - 1; i++) {
|
|
1827
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others);
|
|
2030
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others, true);
|
|
1828
2031
|
if (why !== undefined) return why;
|
|
1829
2032
|
}
|
|
1830
2033
|
return undefined;
|
|
1831
2034
|
}
|
|
1832
2035
|
|
|
1833
|
-
/**
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1841
|
-
if (!pattern.includes('/') && relativePath === pattern) return true;
|
|
1842
|
-
if (pattern.startsWith('*.') && name.endsWith(pattern.slice(1))) return true;
|
|
1843
|
-
}
|
|
1844
|
-
return false;
|
|
2036
|
+
/** Whether an ignore pattern matches the entry at a mount-relative path, a directory or not, itself. */
|
|
2037
|
+
function isIgnored(relativePath: string, patterns: readonly string[], directory: boolean): boolean {
|
|
2038
|
+
const segments = relativePath.split('/');
|
|
2039
|
+
return patterns.some((p) => {
|
|
2040
|
+
const pattern = readPattern(p);
|
|
2041
|
+
return typeof pattern !== 'string' && matchesEntry(pattern, segments, directory);
|
|
2042
|
+
});
|
|
1845
2043
|
}
|
|
1846
2044
|
|
|
1847
2045
|
/**
|
|
@@ -1855,12 +2053,12 @@ async function checkChild(view: MountView, rootReal: string, parent: CheckedDir,
|
|
|
1855
2053
|
try {
|
|
1856
2054
|
info = await lstat(real, { bigint: true });
|
|
1857
2055
|
} catch (err) {
|
|
1858
|
-
if (errno(err) !== 'ENOENT') return { kind: 'unobserved', reason: `cannot stat (${errno(err)
|
|
2056
|
+
if (errno(err) !== 'ENOENT') return { kind: 'unobserved', reason: `cannot stat (${systemWords(errno(err), err)})` };
|
|
1859
2057
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
1860
2058
|
return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
|
|
1861
2059
|
}
|
|
1862
2060
|
if (!info.isDirectory()) return { kind: 'unobserved', reason: CHANGED_WALK };
|
|
1863
|
-
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino) };
|
|
2061
|
+
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
|
|
1864
2062
|
}
|
|
1865
2063
|
|
|
1866
2064
|
/**
|
|
@@ -1893,12 +2091,11 @@ export async function walkScope(
|
|
|
1893
2091
|
};
|
|
1894
2092
|
// A walk that isn't named passes over a scope the mount passes over by
|
|
1895
2093
|
// design, at it or a directory above it, as its parent's listing would pass
|
|
1896
|
-
// over its entry
|
|
1897
|
-
//
|
|
1898
|
-
//
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
// sync that names it, whose scope is walked (#276), below.
|
|
2094
|
+
// over its entry. So a listing, glob or grep there never starts tracking
|
|
2095
|
+
// what the mount leaves out, and a stored copy beneath it is unobserved, as
|
|
2096
|
+
// beneath any region passed over by design. Its region says how to take
|
|
2097
|
+
// what's there in: a sync that names it, whose scope is walked (#276),
|
|
2098
|
+
// below.
|
|
1902
2099
|
// Another mount's directory is passed over by every walk, named or not,
|
|
1903
2100
|
// and no sync of this mount takes it in, so its region gives no advice.
|
|
1904
2101
|
if (scope !== '' && inOtherMount(scope, view.others)) {
|
|
@@ -1907,7 +2104,10 @@ export async function walkScope(
|
|
|
1907
2104
|
return walk;
|
|
1908
2105
|
}
|
|
1909
2106
|
if (!opts.named && scope !== '') {
|
|
1910
|
-
|
|
2107
|
+
let why = passedOverAt(scope, view.ignore, view.others, true);
|
|
2108
|
+
if (why !== undefined && passedOverAt(scope, view.ignore, view.others, false) === undefined) {
|
|
2109
|
+
if ((await directoryAt(join(rootReal, scope), view.followSymlinks)) === false) why = undefined;
|
|
2110
|
+
}
|
|
1911
2111
|
if (why !== undefined) {
|
|
1912
2112
|
walk.others.set(scope, why);
|
|
1913
2113
|
walk.incomplete.push({ path: scope, reason: `${why}: a sync that names this path takes in what's there, editors' files aside`, kind: 'ignored' });
|
|
@@ -2040,9 +2240,13 @@ export async function walkScope(
|
|
|
2040
2240
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
2041
2241
|
// Another mount's directory is passed over by its real path too, so a
|
|
2042
2242
|
// walk that reaches one through a link to a directory above it lists
|
|
2043
|
-
// none of that mount's names (
|
|
2044
|
-
|
|
2045
|
-
?? (entry.
|
|
2243
|
+
// none of that mount's names (nestedRootAtReal).
|
|
2244
|
+
let passedOver = passedOverBecause(childRel, entry.name, ignore, view.others, entry.isDirectory())
|
|
2245
|
+
?? (!entry.isSymbolicLink() && nestedRootAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
|
|
2246
|
+
if (passedOver === undefined && view.followSymlinks && entry.isSymbolicLink()) {
|
|
2247
|
+
const asDirectory = passedOverBecause(childRel, entry.name, ignore, view.others, true);
|
|
2248
|
+
if (asDirectory !== undefined && (await directoryAt(dir.at(entry.name), true)) === true) passedOver = asDirectory;
|
|
2249
|
+
}
|
|
2046
2250
|
if (passedOver !== undefined) {
|
|
2047
2251
|
others.push([childRel, passedOver]);
|
|
2048
2252
|
regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
|
|
@@ -2082,7 +2286,7 @@ export async function walkScope(
|
|
|
2082
2286
|
} else {
|
|
2083
2287
|
const symlink = entry.isSymbolicLink();
|
|
2084
2288
|
leaves.push([childRel, false]);
|
|
2085
|
-
others.push([childRel, symlink ? 'a symlink' :
|
|
2289
|
+
others.push([childRel, symlink ? 'a symlink' : NOT_REGULAR]);
|
|
2086
2290
|
if (symlink) regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
|
|
2087
2291
|
}
|
|
2088
2292
|
}
|
|
@@ -2113,10 +2317,10 @@ export async function walkScope(
|
|
|
2113
2317
|
};
|
|
2114
2318
|
|
|
2115
2319
|
// Each nested root by which directory it is, read once, when the walk
|
|
2116
|
-
// first descends: a directory the walk reaches
|
|
2117
|
-
//
|
|
2118
|
-
// a
|
|
2119
|
-
// mount's directory.
|
|
2320
|
+
// first descends: a directory the walk reaches by a path that no
|
|
2321
|
+
// comparison of names places in the nested mount's directory, as through a
|
|
2322
|
+
// bind mount, or on a disk whose folding goes past the comparison's, is
|
|
2323
|
+
// still another mount's directory.
|
|
2120
2324
|
let nestedRoots: Array<{ dev: string; ino: string }> | undefined;
|
|
2121
2325
|
const isNestedRoot = async (dir: CheckedDir): Promise<boolean> => {
|
|
2122
2326
|
if (!view.nested?.length) return false;
|