firedrill-run 2.2.5 → 3.0.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 +178 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +465 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1622 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +61 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, realpath, rename, unlink } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { api } from './auth.js';
|
|
9
|
+
import { privateDirectory } from './credentials.js';
|
|
10
|
+
import { CliError, releaseClosed } from './http.js';
|
|
11
|
+
import { localSource } from './local-source.js';
|
|
12
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
13
|
+
import { identifier } from './world-contracts.js';
|
|
14
|
+
import { selectedWorkspace } from './world-access.js';
|
|
15
|
+
import { withBootstrapProcessLock } from './bootstrap-process-lock.js';
|
|
16
|
+
import { localSourceConnectionMatches, localSourceLookupMatches, localSourceOriginalMatches, localSourceUploadMatches, localSourcePageMatches, nativeMetadataCanonical } from '../../contracts/local-value-source-contracts.js';
|
|
17
|
+
const prefix = (scope, key) => `/api/workspaces/${identifier(scope.workspace)}/local-sources/${identifier(key)}`;
|
|
18
|
+
const auth = (scope, signal) => ({ actor: scope.owner, workspace: scope.workspace, signal });
|
|
19
|
+
const projectKey = (root) => createHash('sha256').update(root).digest('hex');
|
|
20
|
+
const requestDigest = (body) => createHash('sha256').update(nativeMetadataCanonical(body)).digest('hex');
|
|
21
|
+
const invalidReceipt = () => new CliError('source_receipt_invalid', 'The source response did not match this account, workspace and original capture. No source was substituted or target started.');
|
|
22
|
+
function original(intent, value) {
|
|
23
|
+
if (value.state === 'not_retained' || !localSourceOriginalMatches(value, intent, intent.key, intent.body, requestDigest(intent.body)))
|
|
24
|
+
throw invalidReceipt();
|
|
25
|
+
}
|
|
26
|
+
export async function ownedSourceMetadataFile(path, maximum) {
|
|
27
|
+
const previous = await lstat(path);
|
|
28
|
+
if (!previous.isFile() || previous.isSymbolicLink() || previous.nlink !== 1 || previous.size > maximum
|
|
29
|
+
|| process.getuid && previous.uid !== process.getuid() || process.platform !== 'win32' && (previous.mode & 0o077) !== 0) {
|
|
30
|
+
throw new CliError('source_journal_unsafe', 'Use private source metadata owned by this OS user. No file was transferred.');
|
|
31
|
+
}
|
|
32
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
33
|
+
const actual = await file.stat();
|
|
34
|
+
if (actual.ino !== previous.ino || actual.dev !== previous.dev || actual.size !== previous.size
|
|
35
|
+
|| actual.nlink !== 1 || actual.mtimeMs !== previous.mtimeMs || actual.ctimeMs !== previous.ctimeMs) {
|
|
36
|
+
await file.close();
|
|
37
|
+
throw new CliError('source_journal_changed', 'The local source metadata changed. No file was transferred.');
|
|
38
|
+
}
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
const ownedFile = ownedSourceMetadataFile;
|
|
42
|
+
export async function readSourceMetadata(file, maximum) {
|
|
43
|
+
const before = await file.stat();
|
|
44
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
45
|
+
throw new CliError('source_journal_unsafe', 'The original source metadata exceeds its private read capacity.');
|
|
46
|
+
const bytes = Buffer.alloc(before.size + 1);
|
|
47
|
+
let size = 0;
|
|
48
|
+
while (size < bytes.length) {
|
|
49
|
+
const part = await file.read(bytes, size, bytes.length - size, size);
|
|
50
|
+
if (!part.bytesRead)
|
|
51
|
+
break;
|
|
52
|
+
size += part.bytesRead;
|
|
53
|
+
}
|
|
54
|
+
const after = await file.stat();
|
|
55
|
+
if (size !== before.size || after.size !== before.size || after.nlink !== 1
|
|
56
|
+
|| after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
57
|
+
throw new CliError('source_journal_changed', 'The original source metadata changed while it was read. Nothing was transferred.');
|
|
58
|
+
}
|
|
59
|
+
return JSON.parse(new TextDecoder('utf8', { fatal: true }).decode(bytes.subarray(0, size)));
|
|
60
|
+
}
|
|
61
|
+
export async function writeSourceMetadata(path, value) {
|
|
62
|
+
const temporary = path + '.' + randomUUID() + '.tmp';
|
|
63
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
64
|
+
try {
|
|
65
|
+
await file.writeFile(JSON.stringify(value));
|
|
66
|
+
await file.sync();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await file.close();
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await rename(temporary, path);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
await unlink(temporary).catch(() => undefined);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
if (process.platform !== 'win32') {
|
|
79
|
+
const directory = await open(dirname(path), constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
80
|
+
try {
|
|
81
|
+
await directory.sync();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await directory.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const writePrivate = writeSourceMetadata;
|
|
89
|
+
async function journalPath(scope, key) {
|
|
90
|
+
return join(await privateDirectory(), `source-connect-${identifier(scope.owner)}-${identifier(scope.workspace)}-${identifier(key)}.json`);
|
|
91
|
+
}
|
|
92
|
+
async function journal(scope, key) {
|
|
93
|
+
const file = await ownedFile(await journalPath(scope, key), 16384);
|
|
94
|
+
try {
|
|
95
|
+
const value = await readSourceMetadata(file, 16384);
|
|
96
|
+
if (value.schema !== 'firedrill.source-connect-intent.v1' || value.key !== identifier(key)
|
|
97
|
+
|| value.owner !== scope.owner || value.workspace !== scope.workspace || !/^[a-f0-9]{64}$/.test(value.project_key)
|
|
98
|
+
|| value.body?.schema !== 'firedrill.local-source-connect.v1' || !/^[a-f0-9]{64}$/.test(value.body.archive?.sha256)
|
|
99
|
+
|| !/^[a-f0-9]{64}$/.test(value.body.capture?.content_digest)
|
|
100
|
+
|| value.body.consent?.policy !== 'firedrill.local-source-transfer.v1'
|
|
101
|
+
|| value.body.consent.source_upload !== true || value.body.consent.source_processing !== true
|
|
102
|
+
|| !['request_dispatched', 'registered', 'upload_dispatched', 'uploaded', 'complete_requested'].includes(value.stage))
|
|
103
|
+
throw new Error();
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new CliError('source_journal_invalid', 'The source request journal could not be read safely. It was not discarded.');
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await file.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function requestLock(scope, key, signal, work) {
|
|
114
|
+
const directory = join(await privateDirectory(), `source-lock-${identifier(scope.owner)}-${identifier(scope.workspace)}-${identifier(key)}`);
|
|
115
|
+
return withBootstrapProcessLock(directory, signal, work);
|
|
116
|
+
}
|
|
117
|
+
function readable(value) {
|
|
118
|
+
const key = `Source request ${value.request_key}`;
|
|
119
|
+
if (value.state === 'not_retained')
|
|
120
|
+
return `${key}: no retained receipt yet. This does not prove the original request cannot arrive.`;
|
|
121
|
+
if (value.state === 'connected')
|
|
122
|
+
return value.defer_world
|
|
123
|
+
? `${key}: component source connected.\nAgent: ${value.agent_id}\nNo world, build or simulation has been started.`
|
|
124
|
+
: `${key}: source connected.\nWorld: ${value.world_id}\nNo build or simulation has been started.`;
|
|
125
|
+
if (value.state === 'awaiting_upload')
|
|
126
|
+
return `${key}: waiting for the approved source archive.`;
|
|
127
|
+
if (value.state === 'verifying')
|
|
128
|
+
return `${key}: source processing is accepted and still in progress.`;
|
|
129
|
+
if (value.state === 'upload_missing')
|
|
130
|
+
return `${key}: upload is not visible yet and may still arrive. Use firedrill connect resume ${value.request_key} to check processing, or connect upload ${value.request_key} to explicitly attempt the same archive.`;
|
|
131
|
+
if (value.state === 'withdrawn')
|
|
132
|
+
return `${key}: withdrawn. A delayed request with this key cannot create source.`;
|
|
133
|
+
return `${key}: ${value.state}. ${value.problem ?? 'Inspect this request before continuing.'}`;
|
|
134
|
+
}
|
|
135
|
+
async function lookup(scope, key, signal) {
|
|
136
|
+
const result = await api(prefix(scope, key), auth(scope, signal));
|
|
137
|
+
if (!localSourceLookupMatches(result, scope, key))
|
|
138
|
+
throw invalidReceipt();
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
async function bindProject(intent, receipt) {
|
|
142
|
+
original(intent, receipt);
|
|
143
|
+
if (receipt.state !== 'connected' || !receipt.world_id || !receipt.source_id || !receipt.agent_id
|
|
144
|
+
|| receipt.capture?.content_digest !== intent.body.capture.content_digest
|
|
145
|
+
|| receipt.archive?.sha256 !== intent.body.archive.sha256)
|
|
146
|
+
throw new CliError('source_receipt_invalid', 'The connected source did not match the selected capture.');
|
|
147
|
+
await writePrivate(join(await privateDirectory(), `project-${intent.owner}-${intent.project_key}.json`), {
|
|
148
|
+
schema: 'firedrill.project-binding.v1', owner: intent.owner, workspace: intent.workspace, world: identifier(receipt.world_id),
|
|
149
|
+
agent: identifier(receipt.agent_id), source: identifier(receipt.source_id), content_digest: intent.body.capture.content_digest
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export async function bindSavedProject(scope, root, value) {
|
|
153
|
+
const physical = await realpath(resolve(root));
|
|
154
|
+
if (!/^[a-f0-9]{64}$/.test(value.content_digest))
|
|
155
|
+
throw new CliError('source_receipt_invalid', 'The source digest cannot be confirmed.');
|
|
156
|
+
await writePrivate(join(await privateDirectory(), `project-${identifier(scope.owner)}-${projectKey(physical)}.json`), {
|
|
157
|
+
schema: 'firedrill.project-binding.v1', owner: scope.owner, workspace: scope.workspace, world: identifier(value.world),
|
|
158
|
+
agent: identifier(value.agent), source: identifier(value.source), content_digest: value.content_digest,
|
|
159
|
+
...(value.batch_key ? { batch_key: identifier(value.batch_key) } : {})
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function savedProject(scope, root) {
|
|
163
|
+
const physical = await realpath(resolve(root));
|
|
164
|
+
const path = join(await privateDirectory(), `project-${identifier(scope.owner)}-${projectKey(physical)}.json`);
|
|
165
|
+
let file;
|
|
166
|
+
try {
|
|
167
|
+
file = await ownedFile(path, 4096);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.code === 'ENOENT')
|
|
171
|
+
return null;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const value = await readSourceMetadata(file, 4096);
|
|
176
|
+
if (value.schema !== 'firedrill.project-binding.v1' || value.owner !== scope.owner || typeof value.world !== 'string')
|
|
177
|
+
throw new Error();
|
|
178
|
+
if (value.workspace !== scope.workspace)
|
|
179
|
+
return null;
|
|
180
|
+
return { world: identifier(value.world), source: value.source === undefined ? null : identifier(String(value.source)),
|
|
181
|
+
batch_key: value.batch_key === undefined ? null : identifier(String(value.batch_key)) };
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
throw new CliError('project_binding_invalid', 'The saved project binding needs attention. Choose an explicit world; it was not discarded.');
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await file.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function archiveFile(intent) {
|
|
191
|
+
const directory = await privateDirectory();
|
|
192
|
+
if (dirname(intent.snapshot) !== directory || !/^source-[A-Za-z0-9_-]+$/.test(basename(intent.snapshot)))
|
|
193
|
+
throw new CliError('source_snapshot_invalid', 'Choose the private captured source for this request.');
|
|
194
|
+
const info = await lstat(intent.snapshot);
|
|
195
|
+
if (!info.isDirectory() || info.isSymbolicLink() || process.getuid && info.uid !== process.getuid()
|
|
196
|
+
|| process.platform !== 'win32' && (info.mode & 0o077) !== 0)
|
|
197
|
+
throw new CliError('source_snapshot_invalid', 'The source snapshot is not private.');
|
|
198
|
+
const file = await ownedFile(join(intent.snapshot, 'source.tar.gz'), 5 * 1024 ** 3);
|
|
199
|
+
try {
|
|
200
|
+
const digest = createHash('sha256');
|
|
201
|
+
const stream = file.createReadStream({ autoClose: false });
|
|
202
|
+
for await (const chunk of stream)
|
|
203
|
+
digest.update(chunk);
|
|
204
|
+
const actual = await file.stat();
|
|
205
|
+
if (actual.size !== intent.body.archive.size || digest.digest('hex') !== intent.body.archive.sha256)
|
|
206
|
+
throw new Error();
|
|
207
|
+
return file;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
await file.close();
|
|
211
|
+
throw new CliError('source_snapshot_changed', 'The archive no longer matches the approved capture. No file was transferred.');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function upload(intent, signal) {
|
|
215
|
+
const file = await archiveFile(intent);
|
|
216
|
+
try {
|
|
217
|
+
const access = await api(`${prefix(intent, intent.key)}/upload-access`, { ...auth(intent, signal), method: 'POST', body: {} });
|
|
218
|
+
let destination;
|
|
219
|
+
try {
|
|
220
|
+
destination = new URL(access.upload_url);
|
|
221
|
+
if (!localSourceUploadMatches(access, intent, intent.key, intent.body, requestDigest(intent.body)) || destination.origin !== 'https://storage.googleapis.com'
|
|
222
|
+
|| destination.username || destination.password || destination.hash || access.archive.sha256 !== intent.body.archive.sha256
|
|
223
|
+
|| access.archive.size !== intent.body.archive.size)
|
|
224
|
+
throw new Error();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
throw new CliError('source_upload_access_invalid', 'The scoped upload response was not accepted. No file was transferred.');
|
|
228
|
+
}
|
|
229
|
+
intent.stage = 'upload_dispatched';
|
|
230
|
+
await writePrivate(await journalPath(intent, intent.key), intent);
|
|
231
|
+
signal.throwIfAborted();
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(destination, { method: 'PUT', headers: { ...access.headers, 'Content-Length': String(intent.body.archive.size) },
|
|
234
|
+
body: Readable.toWeb(file.createReadStream({ autoClose: false, start: 0 })), duplex: 'half', redirect: 'error', credentials: 'omit',
|
|
235
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(12 * 60 * 1000)]) });
|
|
236
|
+
await response.body?.cancel();
|
|
237
|
+
// 412 can mean the write-once object already exists after a lost reply.
|
|
238
|
+
// Completion verifies exact GCS bytes; it is not treated as upload success.
|
|
239
|
+
if (![200, 201, 412].includes(response.status))
|
|
240
|
+
throw new CliError('source_upload_uncertain', 'The source upload was not confirmed. Inspect or complete this exact request before choosing to upload it again.');
|
|
241
|
+
if (response.status !== 412) {
|
|
242
|
+
intent.stage = 'uploaded';
|
|
243
|
+
await writePrivate(await journalPath(intent, intent.key), intent);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (signal.aborted)
|
|
248
|
+
throw error;
|
|
249
|
+
throw error instanceof CliError ? error : new CliError('source_upload_uncertain', 'The upload reply was not received. Use connect resume for the exact request; source is not uploaded again automatically.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
await file.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function complete(intent, signal, output) {
|
|
257
|
+
signal.throwIfAborted();
|
|
258
|
+
intent.stage = 'complete_requested';
|
|
259
|
+
await writePrivate(await journalPath(intent, intent.key), intent);
|
|
260
|
+
let value;
|
|
261
|
+
try {
|
|
262
|
+
value = await api(`${prefix(intent, intent.key)}/complete`, { ...auth(intent, signal), method: 'POST', body: {} });
|
|
263
|
+
original(intent, value);
|
|
264
|
+
value = await lookup(intent, intent.key, signal);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (signal.aborted || releaseClosed(error))
|
|
268
|
+
throw error;
|
|
269
|
+
value = await lookup(intent, intent.key, signal);
|
|
270
|
+
}
|
|
271
|
+
let last = '';
|
|
272
|
+
const deadline = performance.now() + 30000;
|
|
273
|
+
while (true) {
|
|
274
|
+
original(intent, value);
|
|
275
|
+
if (last !== value.state) {
|
|
276
|
+
output('source_connection', value, readable(value));
|
|
277
|
+
last = value.state;
|
|
278
|
+
}
|
|
279
|
+
if (value.state !== 'verifying' || performance.now() >= deadline)
|
|
280
|
+
break;
|
|
281
|
+
await sleep(1500, undefined, { signal });
|
|
282
|
+
value = await lookup(intent, intent.key, signal);
|
|
283
|
+
}
|
|
284
|
+
if (value.state === 'connected' && !value.defer_world)
|
|
285
|
+
await bindProject(intent, value);
|
|
286
|
+
if (['refused', 'rejected'].includes(value.state))
|
|
287
|
+
throw new CliError('source_connection_refused', value.problem ?? 'Source connection was refused.');
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
export async function connectSource(options, output) {
|
|
291
|
+
const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
292
|
+
if (!interactive && !options.upload)
|
|
293
|
+
throw new CliError('source_transfer_consent_required', 'Use --upload-source to explicitly approve selected source upload and cloud source processing. No capture was transferred.', 2);
|
|
294
|
+
const root = await realpath(resolve(options.root ?? process.cwd()));
|
|
295
|
+
const capture = await localSource('snapshot', root, options.signal, interactive);
|
|
296
|
+
output('source_capture', { files: capture.files.length, bytes: capture.expanded_bytes, content_digest: capture.content_digest }, `${capture.files.length} selected files, ${capture.expanded_bytes} bytes. Environment files and values excluded.\n${capture.disclosure}`);
|
|
297
|
+
const approved = options.upload || await selectOne('Source use', [
|
|
298
|
+
{ value: true, label: 'Upload selected source for cloud preparation', detail: 'Store this capture in the workspace and let Firedrill process its source. Does not start a target or simulation.' },
|
|
299
|
+
{ value: false, label: 'Keep this capture local', detail: 'Do not connect or transfer source.' },
|
|
300
|
+
], options.signal);
|
|
301
|
+
if (!approved) {
|
|
302
|
+
output('source_local', { uploaded: false, snapshot: capture.local_snapshot_path }, 'Capture kept locally. No cloud connection was created.');
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (!capture.archive || !capture.local_snapshot_path)
|
|
306
|
+
throw new CliError('source_snapshot_invalid', 'The private source capture is incomplete.');
|
|
307
|
+
const scope = await selectedWorkspace({ workspace: options.workspace, expectedOwner: options.expectedOwner,
|
|
308
|
+
interactive, signal: options.signal, openBrowser: options.openBrowser });
|
|
309
|
+
if (options.expectedOwner && scope.owner !== options.expectedOwner)
|
|
310
|
+
throw new CliError('native_account_changed', 'The terminal account changed. Reopen this project before transferring source.', 2);
|
|
311
|
+
const name = (options.name ?? basename(root)).trim();
|
|
312
|
+
if (!name || name.length > 120 || /[\u0000-\u001f\u007f]/.test(name))
|
|
313
|
+
throw new CliError('agent_name_invalid', 'Use --name with an agent name of up to 120 characters.', 2);
|
|
314
|
+
const intent = capturedSourceIntent(scope, root, name, capture);
|
|
315
|
+
return connectPreparedSource(intent, options.signal, output);
|
|
316
|
+
}
|
|
317
|
+
/** Reuse the immutable captured bytes, not a new scan after partial transfer. */
|
|
318
|
+
export function capturedSourceIntent(scope, root, name, capture, options = {}) {
|
|
319
|
+
if (!capture.archive || !capture.local_snapshot_path)
|
|
320
|
+
throw new CliError('source_snapshot_invalid', 'The private source capture is incomplete.');
|
|
321
|
+
return { ...scope, schema: 'firedrill.source-connect-intent.v1', key: options.key ? identifier(options.key) : randomUUID(), snapshot: capture.local_snapshot_path,
|
|
322
|
+
project_key: projectKey(root), stage: 'request_dispatched', created_at: new Date().toISOString(), body: {
|
|
323
|
+
schema: 'firedrill.local-source-connect.v1', name, archive: { size: capture.archive.size, sha256: capture.archive.sha256 },
|
|
324
|
+
capture: { schema: 'firedrill.local-source.v1', content_digest: capture.content_digest,
|
|
325
|
+
base_commit: capture.repository?.base_commit ?? null, project_root: capture.repository?.project_root ?? '',
|
|
326
|
+
file_count: capture.files.length, expanded_bytes: capture.expanded_bytes },
|
|
327
|
+
consent: { policy: 'firedrill.local-source-transfer.v1', source_upload: true, source_processing: true },
|
|
328
|
+
...(options.deferWorld ? { defer_world: true } : {}),
|
|
329
|
+
...(options.componentAgent ? { component_agent_id: identifier(options.componentAgent) } : {})
|
|
330
|
+
} };
|
|
331
|
+
}
|
|
332
|
+
export async function connectPreparedSource(intent, signal, output) {
|
|
333
|
+
return requestLock(intent, intent.key, signal, async (active) => {
|
|
334
|
+
signal = active;
|
|
335
|
+
// A recoverable metadata-only intent precedes the first network mutation.
|
|
336
|
+
await writePrivate(await journalPath(intent, intent.key), intent);
|
|
337
|
+
output('source_request', { request_key: intent.key, state: 'not_retained' }, `Source request ${intent.key}.`);
|
|
338
|
+
let receipt;
|
|
339
|
+
try {
|
|
340
|
+
receipt = await api(prefix(intent, intent.key), { ...auth(intent, signal), method: 'PUT', body: intent.body });
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (signal.aborted || releaseClosed(error))
|
|
344
|
+
throw error;
|
|
345
|
+
receipt = await lookup(intent, intent.key, signal);
|
|
346
|
+
if (receipt.state === 'not_retained')
|
|
347
|
+
throw new CliError('source_request_uncertain', `Inspect or withdraw source request ${intent.key}. It was not resubmitted. No source upload was attempted.`);
|
|
348
|
+
}
|
|
349
|
+
original(intent, receipt);
|
|
350
|
+
receipt = await lookup(intent, intent.key, signal);
|
|
351
|
+
original(intent, receipt);
|
|
352
|
+
if (receipt.state !== 'awaiting_upload')
|
|
353
|
+
throw new CliError('source_request_changed', 'The saved source request changed. Inspect its exact key.');
|
|
354
|
+
intent.stage = 'registered';
|
|
355
|
+
await writePrivate(await journalPath(intent, intent.key), intent);
|
|
356
|
+
await upload(intent, signal);
|
|
357
|
+
return complete(intent, signal, output);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
export async function retainPreparedSource(intent) {
|
|
361
|
+
await writePrivate(await journalPath(intent, intent.key), intent);
|
|
362
|
+
}
|
|
363
|
+
export async function resumePreparedSource(scope, key, signal, output) {
|
|
364
|
+
return requestLock(scope, key, signal, async (active) => {
|
|
365
|
+
const intent = await journal(scope, key);
|
|
366
|
+
const value = await lookup(scope, key, active);
|
|
367
|
+
original(intent, value);
|
|
368
|
+
if (value.state === 'connected') {
|
|
369
|
+
if (!value.defer_world)
|
|
370
|
+
await bindProject(intent, value);
|
|
371
|
+
output('source_connection', value, readable(value));
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
if (!['awaiting_upload', 'upload_missing', 'verifying'].includes(value.state)) {
|
|
375
|
+
output('source_connection', value, readable(value));
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
// Only a first upload that never reached its dispatch boundary is allowed
|
|
379
|
+
// automatically. Uncertain writes require the explicit same-key upload
|
|
380
|
+
// command, not recapture or implicit retries by a batch coordinator.
|
|
381
|
+
if (value.state === 'awaiting_upload' && ['request_dispatched', 'registered'].includes(intent.stage))
|
|
382
|
+
await upload(intent, active);
|
|
383
|
+
return complete(intent, active, output);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
export async function sourceRequests(options, output) {
|
|
387
|
+
const scope = await selectedWorkspace({ workspace: options.workspace, interactive: !options.json && !!process.stdin.isTTY,
|
|
388
|
+
signal: options.signal, openBrowser: options.openBrowser });
|
|
389
|
+
if (options.action === 'list') {
|
|
390
|
+
const search = (options.search ?? '').trim(), status = options.status ?? 'all', after = options.after ?? '';
|
|
391
|
+
if (search.length > 200 || /[\u0000-\u001f\u007f]/.test(search)
|
|
392
|
+
|| !['all', 'awaiting_upload', 'verifying', 'upload_missing', 'connected', 'withdrawn', 'refused', 'rejected'].includes(status)
|
|
393
|
+
|| after.length > 2048 || after && !/^[A-Za-z0-9_.:-]+$/.test(after)) {
|
|
394
|
+
throw new CliError('source_history_filters_invalid', 'Use the available source request status, search and returned continuation.', 2);
|
|
395
|
+
}
|
|
396
|
+
const query = new URLSearchParams({ q: search, state: status });
|
|
397
|
+
if (after)
|
|
398
|
+
query.set('after', after);
|
|
399
|
+
const value = await api(`/api/workspaces/${identifier(scope.workspace)}/local-sources?${query}`, auth(scope, options.signal));
|
|
400
|
+
if (!localSourcePageMatches(value, scope, { query: search, state: status, after }))
|
|
401
|
+
throw invalidReceipt();
|
|
402
|
+
const lines = value.results.map(one => `${one.name || 'Withdrawn source request'}\n${readable(one)}`);
|
|
403
|
+
lines.push(`${value.results.length} retained source requests on this page.`);
|
|
404
|
+
if (value.next_after)
|
|
405
|
+
lines.push(`Continue with --after '${value.next_after}' and the same --search and --status filters.`);
|
|
406
|
+
lines.push('This is cloud-retained history. A pending local request may not appear yet. Use connect status, resume, upload or withdraw with its original request key; no local journal was discarded.');
|
|
407
|
+
output('source_connection_page', value, lines.join('\n'));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const key = identifier(options.key ?? '');
|
|
411
|
+
if (options.action === 'status') {
|
|
412
|
+
const value = await lookup(scope, key, options.signal);
|
|
413
|
+
output('source_connection', value, readable(value));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (options.action === 'withdraw') {
|
|
417
|
+
const value = await api(prefix(scope, key), { ...auth(scope, options.signal), method: 'DELETE' });
|
|
418
|
+
const retained = await lookup(scope, key, options.signal);
|
|
419
|
+
if (!localSourceConnectionMatches(value, scope, key) || value.state !== 'withdrawn' || retained.state !== 'withdrawn'
|
|
420
|
+
|| nativeMetadataCanonical(value) !== nativeMetadataCanonical(retained))
|
|
421
|
+
throw invalidReceipt();
|
|
422
|
+
output('source_connection', retained, readable(retained));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
await requestLock(scope, key, options.signal, async (active) => {
|
|
426
|
+
options = { ...options, signal: active };
|
|
427
|
+
const intent = await journal(scope, key);
|
|
428
|
+
const value = await lookup(scope, key, options.signal);
|
|
429
|
+
if (value.state === 'connected') {
|
|
430
|
+
if (!value.defer_world)
|
|
431
|
+
await bindProject(intent, value);
|
|
432
|
+
output('source_connection', value, readable(value));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (value.state === 'not_retained')
|
|
436
|
+
throw new CliError('source_request_uncertain', 'The original request may still arrive. Withdraw this exact key before making a new connection; it was not resubmitted.');
|
|
437
|
+
if (!['awaiting_upload', 'upload_missing', 'verifying'].includes(value.state)) {
|
|
438
|
+
output('source_connection', value, readable(value));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
original(intent, value);
|
|
442
|
+
if (options.action === 'upload') {
|
|
443
|
+
if (value.state === 'verifying')
|
|
444
|
+
throw new CliError('source_processing_active', 'Inspect or resume source processing. An accepted upload is not repeated.');
|
|
445
|
+
// This command is an explicit same-archive write attempt, never a hidden
|
|
446
|
+
// resume retry or a different source/target action.
|
|
447
|
+
await upload(intent, options.signal);
|
|
448
|
+
}
|
|
449
|
+
await complete(intent, options.signal, output);
|
|
450
|
+
});
|
|
451
|
+
}
|