harnery 0.38.0 → 0.40.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/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +32 -8
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +97 -6
- package/dist/commands/backup.d.ts +16 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +239 -33
- package/dist/commands/browse.d.ts +8 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +466 -231
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +6 -3
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +20 -4
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +26 -3
- package/dist/commands/qa-status.d.ts.map +1 -1
- package/dist/commands/qa-status.js +7 -2
- package/dist/commands/resources.d.ts.map +1 -1
- package/dist/commands/resources.js +23 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +98 -23
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -2
- package/dist/core/adapters/profiles.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts +3 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -0
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +5 -3
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.d.ts +7 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +63 -8
- package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +2 -1
- package/dist/core/agents/state/names.d.ts +35 -2
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +80 -5
- package/dist/core/agents/state/stale-sweep.d.ts +2 -1
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +23 -6
- package/dist/core/agents/turn-elapsed.d.ts +51 -0
- package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
- package/dist/core/agents/turn-elapsed.js +91 -0
- package/dist/core/artifacts/activity.d.ts +14 -0
- package/dist/core/artifacts/activity.d.ts.map +1 -0
- package/dist/core/artifacts/activity.js +52 -0
- package/dist/core/artifacts/delivery-card.d.ts +39 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
- package/dist/core/artifacts/delivery-card.js +282 -0
- package/dist/core/artifacts/index.d.ts +42 -7
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +288 -76
- package/dist/core/artifacts/mutation-lock.d.ts +2 -0
- package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
- package/dist/core/artifacts/mutation-lock.js +91 -0
- package/dist/core/backup/host-snapshot.d.ts +18 -0
- package/dist/core/backup/host-snapshot.d.ts.map +1 -0
- package/dist/core/backup/host-snapshot.js +64 -0
- package/dist/core/config.d.ts +20 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +44 -10
- package/dist/core/diagnostics/advice.d.ts +38 -3
- package/dist/core/diagnostics/advice.d.ts.map +1 -1
- package/dist/core/diagnostics/advice.js +103 -99
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +111 -47
- package/dist/core/diagnostics/comparison.d.ts.map +1 -1
- package/dist/core/diagnostics/comparison.js +3 -2
- package/dist/core/diagnostics/contract.d.ts +15 -31
- package/dist/core/diagnostics/contract.d.ts.map +1 -1
- package/dist/core/diagnostics/contract.js +2 -5
- package/dist/core/diagnostics/index.d.ts +2 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -1
- package/dist/core/diagnostics/index.js +2 -0
- package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
- package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure-contract.js +70 -0
- package/dist/core/diagnostics/pressure.d.ts +25 -0
- package/dist/core/diagnostics/pressure.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure.js +1138 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -1
- package/dist/core/diagnostics/replay.js +90 -10
- package/dist/core/events/v3/adapter-id.d.ts +6 -0
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
- package/dist/core/events/v3/adapter-id.js +7 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
- package/dist/core/events/v3/archive-retention.js +3 -2
- package/dist/core/events/v3/bootstrap.d.ts +41 -4
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +120 -17
- package/dist/core/events/v3/capabilities.d.ts +16 -9
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +37 -2
- package/dist/core/events/v3/capability-drift.d.ts +2 -2
- package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.d.ts +48 -5
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +91 -36
- package/dist/core/events/v3/control.d.ts.map +1 -1
- package/dist/core/events/v3/control.js +22 -2
- package/dist/core/events/v3/index.d.ts +2 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +2 -1
- package/dist/core/events/v3/live-feed.d.ts +7 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +21 -3
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +10 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +2 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
- package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +15 -5
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +17 -4
- package/dist/core/events/v3/producers/intake.d.ts +4 -4
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +3 -2
- package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
- package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
- package/dist/core/events/v3/producers/recorder.d.ts +16 -5
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +31 -9
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +10 -13
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +3 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.d.ts +16 -14
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +36 -38
- package/dist/core/hooks/backup-schedule.d.ts +34 -0
- package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
- package/dist/core/hooks/backup-schedule.js +114 -0
- package/dist/core/hooks/cli.js +61 -18
- package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
- package/dist/core/hooks/codex-wsl-bridge.js +18 -4
- package/dist/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/contract.js +102 -0
- package/dist/core/hooks/prompt-context/index.d.ts +5 -0
- package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/index.js +2 -0
- package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
- package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/runner.js +249 -0
- package/dist/core/qa-artifacts.d.ts +3 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -1
- package/dist/core/qa-artifacts.js +25 -3
- package/dist/core/resources/contract.d.ts +69 -3
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/contract.js +1 -1
- package/dist/core/resources/index.d.ts +1 -0
- package/dist/core/resources/index.d.ts.map +1 -1
- package/dist/core/resources/index.js +1 -0
- package/dist/core/resources/native.d.ts +36 -0
- package/dist/core/resources/native.d.ts.map +1 -0
- package/dist/core/resources/native.js +418 -0
- package/dist/core/resources/sampler.d.ts +2 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +31 -1
- package/dist/core/resources/status.d.ts +61 -0
- package/dist/core/resources/status.d.ts.map +1 -0
- package/dist/core/resources/status.js +536 -0
- package/dist/core/resources/system.d.ts +27 -0
- package/dist/core/resources/system.d.ts.map +1 -0
- package/dist/core/resources/system.js +439 -0
- package/dist/core/semantic/contract.d.ts +13 -13
- package/dist/core/semantic/service.d.ts +2 -0
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +27 -4
- package/dist/core/storage/builtins.js +41 -0
- package/dist/core/supervisor/contract.d.ts +41 -3
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/contract.js +3 -2
- package/dist/core/supervisor/findings.d.ts +3 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +88 -0
- package/dist/core/supervisor/history.d.ts.map +1 -1
- package/dist/core/supervisor/history.js +14 -0
- package/dist/core/supervisor/ledger-state.d.ts +55 -0
- package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
- package/dist/core/supervisor/ledger-state.js +0 -0
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +120 -7
- package/dist/core/supervisor/storage.d.ts +3 -1
- package/dist/core/supervisor/storage.d.ts.map +1 -1
- package/dist/core/supervisor/storage.js +4 -0
- package/dist/core/workflow/admission.d.ts +3 -0
- package/dist/core/workflow/admission.d.ts.map +1 -1
- package/dist/core/workflow/admission.js +28 -16
- package/dist/core/workflow/engine.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +6 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -3
- package/dist/lib/browser/capture-fidelity.d.ts +7 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
- package/dist/lib/browser/capture-fidelity.js +17 -0
- package/dist/lib/browser/client.d.ts +9 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +85 -37
- package/dist/lib/browser/index.d.ts +3 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/page-review-budget.d.ts +13 -0
- package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
- package/dist/lib/browser/page-review-budget.js +251 -0
- package/dist/lib/browser/page-review-contracts.d.ts +127 -0
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
- package/dist/lib/browser/page-review-contracts.js +5 -0
- package/dist/lib/browser/page-review-evidence.d.ts +48 -0
- package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
- package/dist/lib/browser/page-review-evidence.js +340 -0
- package/dist/lib/browser/page-review-judge.d.ts +1 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +46 -38
- package/dist/lib/browser/page-review-pack.d.ts +44 -22
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +116 -36
- package/dist/lib/browser/page-review-source.d.ts +12 -0
- package/dist/lib/browser/page-review-source.d.ts.map +1 -0
- package/dist/lib/browser/page-review-source.js +22 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +48 -14
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +215 -68
- package/dist/lib/browser/qa-snapshot.d.ts +6 -0
- package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
- package/dist/lib/browser/qa-snapshot.js +259 -48
- package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
- package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
- package/dist/lib/browser/review-capture-transaction.js +79 -0
- package/dist/lib/browser/standalone-html.d.ts.map +1 -1
- package/dist/lib/browser/standalone-html.js +4 -2
- package/dist/lib/browser/thumbnail-association.d.ts +34 -0
- package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
- package/dist/lib/browser/thumbnail-association.js +158 -0
- package/dist/lib/browser/tiling.d.ts +30 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +38 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +4 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +7 -1
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +1 -0
- package/dist/lib/local-file-url.d.ts +12 -0
- package/dist/lib/local-file-url.d.ts.map +1 -1
- package/dist/lib/local-file-url.js +16 -2
- package/dist/lib/tunnel/state.d.ts +19 -4
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +53 -13
- package/package.json +7 -4
- package/schemas/config.schema.json +69 -2
- package/src/commands/agents.ts +42 -9
- package/src/commands/artifacts.ts +130 -6
- package/src/commands/backup.ts +287 -34
- package/src/commands/browse.ts +564 -267
- package/src/commands/diagnostics.ts +6 -3
- package/src/commands/ledger-v3.ts +20 -3
- package/src/commands/qa-run.ts +37 -3
- package/src/commands/qa-status.ts +11 -2
- package/src/commands/resources.ts +23 -3
- package/src/commands/review-pack.ts +165 -62
- package/src/commands/tunnel.ts +2 -1
- package/src/core/agents/canonical-emit.ts +4 -2
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/live-authority-v3.ts +7 -3
- package/src/core/agents/live-lifecycle-v3.ts +2 -2
- package/src/core/agents/render/prompt-context.ts +68 -14
- package/src/core/agents/session-finalizer-v3.ts +2 -2
- package/src/core/agents/state/live-coordination-view.ts +4 -4
- package/src/core/agents/state/names.ts +92 -7
- package/src/core/agents/state/stale-sweep.ts +22 -8
- package/src/core/agents/turn-elapsed.ts +135 -0
- package/src/core/artifacts/activity.ts +71 -0
- package/src/core/artifacts/delivery-card.ts +375 -0
- package/src/core/artifacts/index.ts +385 -91
- package/src/core/artifacts/mutation-lock.ts +106 -0
- package/src/core/backup/host-snapshot.ts +76 -0
- package/src/core/config.ts +66 -13
- package/src/core/diagnostics/advice.ts +148 -120
- package/src/core/diagnostics/bundle.ts +138 -52
- package/src/core/diagnostics/comparison.ts +6 -2
- package/src/core/diagnostics/contract.ts +39 -42
- package/src/core/diagnostics/index.ts +2 -0
- package/src/core/diagnostics/pressure-contract.ts +294 -0
- package/src/core/diagnostics/pressure.ts +1361 -0
- package/src/core/diagnostics/replay.ts +116 -11
- package/src/core/events/v3/adapter-id.ts +12 -0
- package/src/core/events/v3/archive-retention.ts +3 -6
- package/src/core/events/v3/bootstrap.ts +163 -16
- package/src/core/events/v3/capabilities.ts +75 -25
- package/src/core/events/v3/capability-drift.ts +2 -2
- package/src/core/events/v3/control-witness.ts +172 -41
- package/src/core/events/v3/control.ts +34 -2
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/live-feed.ts +23 -5
- package/src/core/events/v3/live-routing.ts +14 -2
- package/src/core/events/v3/producers/command-recorder.ts +4 -4
- package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
- package/src/core/events/v3/producers/hook-base.ts +21 -16
- package/src/core/events/v3/producers/hook.ts +21 -4
- package/src/core/events/v3/producers/intake.ts +6 -6
- package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +50 -21
- package/src/core/events/v3/reader.ts +14 -13
- package/src/core/events/v3/runtime-build.ts +3 -2
- package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
- package/src/core/events/v3/turn-telemetry.ts +4 -4
- package/src/core/hooks/adapter/output.ts +50 -36
- package/src/core/hooks/backup-schedule.ts +147 -0
- package/src/core/hooks/cli.ts +65 -14
- package/src/core/hooks/codex-wsl-bridge.ts +21 -5
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/prompt-context/contract.ts +177 -0
- package/src/core/hooks/prompt-context/index.ts +22 -0
- package/src/core/hooks/prompt-context/runner.ts +303 -0
- package/src/core/qa-artifacts.ts +29 -3
- package/src/core/resources/contract.ts +76 -3
- package/src/core/resources/index.ts +1 -0
- package/src/core/resources/native.ts +481 -0
- package/src/core/resources/sampler.ts +46 -1
- package/src/core/resources/status.ts +717 -0
- package/src/core/resources/system.ts +530 -0
- package/src/core/semantic/service.ts +37 -3
- package/src/core/storage/builtins.ts +41 -0
- package/src/core/supervisor/contract.ts +45 -3
- package/src/core/supervisor/findings.ts +159 -0
- package/src/core/supervisor/history.ts +15 -0
- package/src/core/supervisor/ledger-state.ts +0 -0
- package/src/core/supervisor/service.ts +133 -5
- package/src/core/supervisor/storage.ts +7 -0
- package/src/core/workflow/admission.ts +33 -17
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/live-session-v3.ts +6 -8
- package/src/core/workflow/proof.ts +4 -3
- package/src/lib/browser/capture-fidelity.ts +23 -0
- package/src/lib/browser/client.ts +96 -44
- package/src/lib/browser/index.ts +15 -1
- package/src/lib/browser/page-review-budget.ts +309 -0
- package/src/lib/browser/page-review-contracts.ts +129 -0
- package/src/lib/browser/page-review-evidence.ts +465 -0
- package/src/lib/browser/page-review-judge.ts +56 -40
- package/src/lib/browser/page-review-pack.ts +181 -43
- package/src/lib/browser/page-review-source.ts +30 -0
- package/src/lib/browser/qa-run-contracts.ts +62 -18
- package/src/lib/browser/qa-run.ts +259 -71
- package/src/lib/browser/qa-snapshot.ts +289 -48
- package/src/lib/browser/review-capture-transaction.ts +106 -0
- package/src/lib/browser/standalone-html.ts +4 -2
- package/src/lib/browser/thumbnail-association.ts +190 -0
- package/src/lib/browser/tiling.ts +63 -0
- package/src/lib/docs-metadata-managed.ts +4 -1
- package/src/lib/identities/assume.ts +12 -4
- package/src/lib/instructions/templates.ts +1 -0
- package/src/lib/local-file-url.ts +17 -2
- package/src/lib/tunnel/state.ts +70 -13
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ARTIFACT_MANIFEST } from "./constants.ts";
|
|
5
|
+
|
|
6
|
+
/** A checkpoint taken before an internal manifest write, never an expiry override. */
|
|
7
|
+
export interface ArtifactActivity {
|
|
8
|
+
last_changed_at: string;
|
|
9
|
+
root_entries_sha256: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validArtifactActivity(value: unknown): value is ArtifactActivity {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
14
|
+
const item = value as Partial<ArtifactActivity>;
|
|
15
|
+
return (
|
|
16
|
+
typeof item.last_changed_at === "string" &&
|
|
17
|
+
Number.isFinite(Date.parse(item.last_changed_at)) &&
|
|
18
|
+
typeof item.root_entries_sha256 === "string" &&
|
|
19
|
+
/^[a-f0-9]{64}$/.test(item.root_entries_sha256)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function artifactRootEntries(path: string): string {
|
|
24
|
+
return createHash("sha256")
|
|
25
|
+
.update(
|
|
26
|
+
JSON.stringify(
|
|
27
|
+
readdirSync(path)
|
|
28
|
+
.filter((name) => name !== ARTIFACT_MANIFEST)
|
|
29
|
+
.sort(),
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
.digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Metadata writes change both the manifest and its parent directory timestamps.
|
|
36
|
+
* Keep the prior root activity while its payload entries match the checkpoint.
|
|
37
|
+
* A root deletion or rename changes that fingerprint; nested changes retain
|
|
38
|
+
* their ordinary filesystem timestamps. Symlinks are inspected, never followed.
|
|
39
|
+
*/
|
|
40
|
+
export function readArtifactActivity(
|
|
41
|
+
path: string,
|
|
42
|
+
now: Date,
|
|
43
|
+
previous?: ArtifactActivity,
|
|
44
|
+
): ArtifactActivity {
|
|
45
|
+
const entries = artifactRootEntries(path);
|
|
46
|
+
const nowMs = now.getTime();
|
|
47
|
+
const accept = (timestamp: number): number =>
|
|
48
|
+
Number.isFinite(timestamp) && timestamp <= nowMs + 5 * 60 * 1000
|
|
49
|
+
? Math.min(timestamp, nowMs)
|
|
50
|
+
: 0;
|
|
51
|
+
const walk = (entry: string, root = false): number => {
|
|
52
|
+
const stat = lstatSync(entry);
|
|
53
|
+
let latest =
|
|
54
|
+
root && previous?.root_entries_sha256 === entries
|
|
55
|
+
? accept(Date.parse(previous.last_changed_at))
|
|
56
|
+
: Math.max(accept(stat.mtimeMs), accept(stat.ctimeMs));
|
|
57
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
58
|
+
for (const name of readdirSync(entry)) {
|
|
59
|
+
// Only this unit's own control manifest is metadata. Nested artifact
|
|
60
|
+
// manifests are payload from the containing workspace's point of view.
|
|
61
|
+
if (root && previous && name === ARTIFACT_MANIFEST) continue;
|
|
62
|
+
latest = Math.max(latest, walk(join(entry, name)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return latest;
|
|
66
|
+
};
|
|
67
|
+
const latest = Math.max(accept(Date.parse(previous?.last_changed_at ?? "")), walk(path, true));
|
|
68
|
+
if (artifactRootEntries(path) !== entries)
|
|
69
|
+
throw new Error("artifact entries changed during scan");
|
|
70
|
+
return { last_changed_at: new Date(latest).toISOString(), root_entries_sha256: entries };
|
|
71
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { encodeLinkSafeComponent } from "../../lib/local-file-url.ts";
|
|
15
|
+
import { findLiveTunnelForOrigin } from "../../lib/tunnel/state.ts";
|
|
16
|
+
import { resolveWebPort } from "../config.ts";
|
|
17
|
+
import { ARTIFACT_MANIFEST } from "./constants.ts";
|
|
18
|
+
import { resolveArtifactRef, showArtifact } from "./index.ts";
|
|
19
|
+
|
|
20
|
+
export const ARTIFACT_DELIVERY_MANIFEST = ".harnery-delivery.json";
|
|
21
|
+
export const ARTIFACT_DELIVERY_SCHEMA_VERSION = 1 as const;
|
|
22
|
+
export const ARTIFACT_DELIVERY_AUTO_ITEM_LIMIT = 5;
|
|
23
|
+
|
|
24
|
+
export interface ArtifactDeliveryUrl {
|
|
25
|
+
kind: "url";
|
|
26
|
+
label: string;
|
|
27
|
+
target: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ArtifactDeliveryPath {
|
|
31
|
+
kind: "path";
|
|
32
|
+
label: string;
|
|
33
|
+
path: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ArtifactDeliveryItem = ArtifactDeliveryUrl | ArtifactDeliveryPath;
|
|
37
|
+
|
|
38
|
+
export interface ArtifactDeliveryManifest {
|
|
39
|
+
schema_version: typeof ARTIFACT_DELIVERY_SCHEMA_VERSION;
|
|
40
|
+
title: string;
|
|
41
|
+
items: ArtifactDeliveryItem[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ArtifactDeliveryCard {
|
|
45
|
+
artifact_path: string;
|
|
46
|
+
manifest_path: string;
|
|
47
|
+
markdown: string;
|
|
48
|
+
auto_items: number;
|
|
49
|
+
omitted_auto_items: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DisplayEnvironment {
|
|
53
|
+
platform?: NodeJS.Platform;
|
|
54
|
+
wslDistroName?: string;
|
|
55
|
+
webPort?: number;
|
|
56
|
+
tunnelUrl?: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function writeArtifactDeliveryManifest(
|
|
60
|
+
repoRoot: string,
|
|
61
|
+
ref: string,
|
|
62
|
+
input: Omit<ArtifactDeliveryManifest, "schema_version">,
|
|
63
|
+
): ArtifactDeliveryManifest {
|
|
64
|
+
const artifactPath = managedArtifactPath(repoRoot, ref);
|
|
65
|
+
const manifest = validateManifest(artifactPath, {
|
|
66
|
+
schema_version: ARTIFACT_DELIVERY_SCHEMA_VERSION,
|
|
67
|
+
...input,
|
|
68
|
+
});
|
|
69
|
+
const target = resolve(artifactPath, ARTIFACT_DELIVERY_MANIFEST);
|
|
70
|
+
const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
|
|
71
|
+
try {
|
|
72
|
+
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
mode: 0o600,
|
|
75
|
+
flag: "wx",
|
|
76
|
+
});
|
|
77
|
+
renameSync(tmp, target);
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(tmp, { force: true });
|
|
80
|
+
}
|
|
81
|
+
return manifest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readArtifactDeliveryManifest(
|
|
85
|
+
repoRoot: string,
|
|
86
|
+
ref: string,
|
|
87
|
+
): ArtifactDeliveryManifest {
|
|
88
|
+
const artifactPath = managedArtifactPath(repoRoot, ref);
|
|
89
|
+
const path = resolve(artifactPath, ARTIFACT_DELIVERY_MANIFEST);
|
|
90
|
+
let parsed: unknown;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`cannot read ${ARTIFACT_DELIVERY_MANIFEST}: ${error instanceof Error ? error.message : String(error)}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return validateManifest(artifactPath, parsed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveArtifactDeliveryManifest(
|
|
102
|
+
repoRoot: string,
|
|
103
|
+
ref: string,
|
|
104
|
+
): ArtifactDeliveryManifest {
|
|
105
|
+
const artifactPath = managedArtifactPath(repoRoot, ref);
|
|
106
|
+
if (!existsSync(resolve(artifactPath, ARTIFACT_DELIVERY_MANIFEST))) {
|
|
107
|
+
return {
|
|
108
|
+
schema_version: ARTIFACT_DELIVERY_SCHEMA_VERSION,
|
|
109
|
+
title: "Artifact delivery",
|
|
110
|
+
items: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return readArtifactDeliveryManifest(repoRoot, ref);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderArtifactDeliveryCard(
|
|
117
|
+
repoRoot: string,
|
|
118
|
+
ref: string,
|
|
119
|
+
manifest = resolveArtifactDeliveryManifest(repoRoot, ref),
|
|
120
|
+
environment: DisplayEnvironment = {},
|
|
121
|
+
): ArtifactDeliveryCard {
|
|
122
|
+
const artifactPath = managedArtifactPath(repoRoot, ref);
|
|
123
|
+
const valid = validateManifest(artifactPath, manifest);
|
|
124
|
+
const linkBase = deliveryLinkBase(repoRoot, environment);
|
|
125
|
+
const rows: Array<{ label: string; target: string; href: string; icon: string }> = [];
|
|
126
|
+
|
|
127
|
+
for (const item of valid.items.filter((candidate) => candidate.kind === "url")) {
|
|
128
|
+
rows.push({ label: item.label, target: item.target, href: item.target, icon: "🌐" });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
rows.push({
|
|
132
|
+
label: "Artifact folder",
|
|
133
|
+
target: displayPath(artifactPath, environment),
|
|
134
|
+
href: artifactBrowserUrl(repoRoot, artifactPath, linkBase, true),
|
|
135
|
+
icon: "📁",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
for (const item of valid.items.filter((candidate) => candidate.kind === "path")) {
|
|
139
|
+
const absolute = resolveArtifactItem(artifactPath, item.path);
|
|
140
|
+
rows.push({
|
|
141
|
+
label: item.label,
|
|
142
|
+
target: displayPath(absolute, environment),
|
|
143
|
+
href: artifactBrowserUrl(repoRoot, absolute, linkBase, lstatSync(absolute).isDirectory()),
|
|
144
|
+
icon: iconForPath(absolute),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const discovered = valid.items.length === 0 ? discoverArtifactRootItems(artifactPath) : [];
|
|
149
|
+
const autoItems = discovered.slice(0, ARTIFACT_DELIVERY_AUTO_ITEM_LIMIT);
|
|
150
|
+
for (const item of autoItems) {
|
|
151
|
+
rows.push({
|
|
152
|
+
label: item.name,
|
|
153
|
+
target: displayPath(item.path, environment),
|
|
154
|
+
href: artifactBrowserUrl(repoRoot, item.path, linkBase, item.directory),
|
|
155
|
+
icon: iconForPath(item.path),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const omittedAutoItems = discovered.length - autoItems.length;
|
|
159
|
+
|
|
160
|
+
let links = rows
|
|
161
|
+
.map((row) => `- ${row.icon} [${escapeMarkdown(row.label)}](${markdownDestination(row.href)})`)
|
|
162
|
+
.join("\n");
|
|
163
|
+
if (omittedAutoItems > 0) {
|
|
164
|
+
links += `\n- 📁 **More root items:** ${omittedAutoItems} additional ${omittedAutoItems === 1 ? "entry" : "entries"}; open the artifact folder.`;
|
|
165
|
+
}
|
|
166
|
+
const plain = rows.map((row) => `${row.label.toUpperCase()}\n${row.target}`).join("\n\n");
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
artifact_path: artifactPath,
|
|
170
|
+
manifest_path: resolve(artifactPath, ARTIFACT_DELIVERY_MANIFEST),
|
|
171
|
+
markdown: `### ${escapeMarkdown(valid.title)}\n\n${links}\n\n\`\`\`text\n${plain}\n\`\`\``,
|
|
172
|
+
auto_items: autoItems.length,
|
|
173
|
+
omitted_auto_items: omittedAutoItems,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function parseArtifactDeliverySpec(
|
|
178
|
+
kind: "url" | "path",
|
|
179
|
+
spec: string,
|
|
180
|
+
): ArtifactDeliveryItem {
|
|
181
|
+
const delimiter = spec.indexOf("=");
|
|
182
|
+
if (delimiter < 1 || delimiter === spec.length - 1) {
|
|
183
|
+
throw new Error(`${kind} entries must use LABEL=VALUE`);
|
|
184
|
+
}
|
|
185
|
+
const label = spec.slice(0, delimiter).trim();
|
|
186
|
+
const value = spec.slice(delimiter + 1).trim();
|
|
187
|
+
return kind === "url" ? { kind, label, target: value } : { kind, label, path: value };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function managedArtifactPath(repoRoot: string, ref: string): string {
|
|
191
|
+
showArtifact(repoRoot, ref);
|
|
192
|
+
return realpathSync(resolveArtifactRef(repoRoot, ref));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateManifest(artifactPath: string, value: unknown): ArtifactDeliveryManifest {
|
|
196
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
197
|
+
throw new Error("delivery manifest must be an object");
|
|
198
|
+
}
|
|
199
|
+
const candidate = value as Record<string, unknown>;
|
|
200
|
+
if (candidate.schema_version !== ARTIFACT_DELIVERY_SCHEMA_VERSION) {
|
|
201
|
+
throw new Error(`delivery manifest schema_version must be ${ARTIFACT_DELIVERY_SCHEMA_VERSION}`);
|
|
202
|
+
}
|
|
203
|
+
const title = typeof candidate.title === "string" ? candidate.title.trim() : "";
|
|
204
|
+
if (!title) throw new Error("delivery title must not be empty");
|
|
205
|
+
if (!Array.isArray(candidate.items)) throw new Error("delivery manifest items must be an array");
|
|
206
|
+
|
|
207
|
+
const labels = new Set<string>();
|
|
208
|
+
const items = candidate.items.map((raw): ArtifactDeliveryItem => {
|
|
209
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
210
|
+
throw new Error("each delivery item must be an object");
|
|
211
|
+
}
|
|
212
|
+
const item = raw as Record<string, unknown>;
|
|
213
|
+
const label = typeof item.label === "string" ? item.label.trim() : "";
|
|
214
|
+
if (!label) throw new Error("delivery item labels must not be empty");
|
|
215
|
+
const normalizedLabel = label.toLowerCase();
|
|
216
|
+
if (normalizedLabel === "artifact folder") {
|
|
217
|
+
throw new Error('"Artifact folder" is reserved and included automatically');
|
|
218
|
+
}
|
|
219
|
+
if (labels.has(normalizedLabel)) throw new Error(`duplicate delivery label: ${label}`);
|
|
220
|
+
labels.add(normalizedLabel);
|
|
221
|
+
|
|
222
|
+
if (item.kind === "url") {
|
|
223
|
+
const target = typeof item.target === "string" ? item.target.trim() : "";
|
|
224
|
+
let url: URL;
|
|
225
|
+
try {
|
|
226
|
+
url = new URL(target);
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error(`invalid delivery URL for ${label}`);
|
|
229
|
+
}
|
|
230
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
231
|
+
throw new Error(`delivery URL for ${label} must use http or https`);
|
|
232
|
+
}
|
|
233
|
+
return { kind: "url", label, target: url.toString() };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (item.kind === "path") {
|
|
237
|
+
const path = typeof item.path === "string" ? item.path.trim() : "";
|
|
238
|
+
if (!path || isAbsolute(path)) {
|
|
239
|
+
throw new Error(`delivery path for ${label} must be relative to the artifact folder`);
|
|
240
|
+
}
|
|
241
|
+
const absolute = resolveArtifactItem(artifactPath, path);
|
|
242
|
+
const normalized = relative(artifactPath, absolute).split(sep).join("/");
|
|
243
|
+
return { kind: "path", label, path: normalized };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
throw new Error(`delivery item ${label} has an unsupported kind`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
schema_version: ARTIFACT_DELIVERY_SCHEMA_VERSION,
|
|
251
|
+
title,
|
|
252
|
+
items,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function resolveArtifactItem(artifactPath: string, itemPath: string): string {
|
|
257
|
+
const root = realpathSync(artifactPath);
|
|
258
|
+
const lexical = resolve(root, itemPath);
|
|
259
|
+
if (lexical === root || !lexical.startsWith(`${root}${sep}`)) {
|
|
260
|
+
throw new Error(`delivery path escapes the artifact folder: ${itemPath}`);
|
|
261
|
+
}
|
|
262
|
+
let target: string;
|
|
263
|
+
try {
|
|
264
|
+
target = realpathSync(lexical);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`delivery path does not exist: ${itemPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (!target.startsWith(`${root}${sep}`)) {
|
|
271
|
+
throw new Error(`delivery path escapes the artifact folder through a symlink: ${itemPath}`);
|
|
272
|
+
}
|
|
273
|
+
const stat = lstatSync(target);
|
|
274
|
+
if (!stat.isFile() && !stat.isDirectory()) {
|
|
275
|
+
throw new Error(`delivery path must be a file or directory: ${itemPath}`);
|
|
276
|
+
}
|
|
277
|
+
return target;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function discoverArtifactRootItems(
|
|
281
|
+
artifactPath: string,
|
|
282
|
+
): Array<{ name: string; path: string; directory: boolean }> {
|
|
283
|
+
return readdirSync(artifactPath, { withFileTypes: true })
|
|
284
|
+
.filter(
|
|
285
|
+
(entry) =>
|
|
286
|
+
!entry.name.startsWith(".") &&
|
|
287
|
+
entry.name !== ARTIFACT_MANIFEST &&
|
|
288
|
+
entry.name !== ARTIFACT_DELIVERY_MANIFEST &&
|
|
289
|
+
(entry.isFile() || entry.isDirectory()),
|
|
290
|
+
)
|
|
291
|
+
.map((entry) => ({
|
|
292
|
+
name: entry.name,
|
|
293
|
+
path: resolveArtifactItem(artifactPath, entry.name),
|
|
294
|
+
directory: entry.isDirectory(),
|
|
295
|
+
}))
|
|
296
|
+
.sort((left, right) => {
|
|
297
|
+
if (left.directory !== right.directory) return left.directory ? -1 : 1;
|
|
298
|
+
return left.name.localeCompare(right.name, "en");
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function displayPath(path: string, environment: DisplayEnvironment): string {
|
|
303
|
+
const distro = environment.wslDistroName ?? process.env.WSL_DISTRO_NAME;
|
|
304
|
+
const platform = environment.platform ?? process.platform;
|
|
305
|
+
if (platform === "linux" && distro) {
|
|
306
|
+
const suffix = resolve(path).replaceAll("/", "\\");
|
|
307
|
+
return `\\\\wsl.localhost\\${distro}${suffix}`;
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
platform === "linux" &&
|
|
311
|
+
(process.env.WSL_INTEROP || process.env.HARNERY_AGENT_COORD_BRIDGE === "codex-wsl")
|
|
312
|
+
) {
|
|
313
|
+
try {
|
|
314
|
+
return execFileSync("wslpath", ["-w", resolve(path)], { encoding: "utf8" }).trim();
|
|
315
|
+
} catch {
|
|
316
|
+
// Fall through to the native path when wslpath is unavailable.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return resolve(path);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function deliveryLinkBase(repoRoot: string, environment: DisplayEnvironment): string {
|
|
323
|
+
const webPort = environment.webPort ?? resolveWebPort(undefined, repoRoot);
|
|
324
|
+
// Every href below is a dashboard route, so only a tunnel that serves the
|
|
325
|
+
// dashboard's own origin will answer them. A tunnel sharing this upstream
|
|
326
|
+
// port under another Host serves a different site and would 400 on /browse
|
|
327
|
+
// and /files, so fall back to the local URL rather than publish that host.
|
|
328
|
+
const tunnelUrl =
|
|
329
|
+
environment.tunnelUrl === undefined
|
|
330
|
+
? findLiveTunnelForOrigin(webPort, `localhost:${webPort}`, repoRoot)?.url
|
|
331
|
+
: environment.tunnelUrl;
|
|
332
|
+
return (tunnelUrl ?? `http://localhost:${webPort}`).replace(/\/+$/, "");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function artifactBrowserUrl(
|
|
336
|
+
repoRoot: string,
|
|
337
|
+
target: string,
|
|
338
|
+
base: string,
|
|
339
|
+
directory: boolean,
|
|
340
|
+
): string {
|
|
341
|
+
const relPath = relative(realpathSync(repoRoot), realpathSync(target)).split(sep).join("/");
|
|
342
|
+
const query = directory
|
|
343
|
+
? `dir=${encodeLinkSafeComponent(relPath)}`
|
|
344
|
+
: `path=${encodeLinkSafeComponent(relPath)}`;
|
|
345
|
+
return `${base}/${directory ? "browse" : "files"}?${query}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Render a destination for a Markdown inline link.
|
|
350
|
+
*
|
|
351
|
+
* The angle-bracket form is valid CommonMark but not every renderer implements
|
|
352
|
+
* it, and one that does not can style the text as a link while dropping the
|
|
353
|
+
* href, leaving something that looks clickable and is not. So use the plain
|
|
354
|
+
* form, which every renderer handles, and reserve the brackets for a supplied
|
|
355
|
+
* URL that actually needs them.
|
|
356
|
+
*/
|
|
357
|
+
function markdownDestination(href: string): string {
|
|
358
|
+
return /[\s()<>]/.test(href) ? `<${href}>` : href;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function iconForPath(path: string): string {
|
|
362
|
+
if (lstatSync(path).isDirectory()) return "📁";
|
|
363
|
+
if (imagePath(path)) return "🖼️";
|
|
364
|
+
if (/\.(?:m4v|mkv|mov|mp4|webm)$/i.test(path)) return "🎬";
|
|
365
|
+
if (/\.(?:aac|flac|m4a|mp3|ogg|wav)$/i.test(path)) return "🎵";
|
|
366
|
+
return "📄";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function imagePath(path: string): boolean {
|
|
370
|
+
return /\.(?:avif|gif|jpe?g|png|svg|webp)$/i.test(path);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function escapeMarkdown(value: string): string {
|
|
374
|
+
return value.replaceAll("\\", "\\\\").replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
375
|
+
}
|