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,28 @@
|
|
|
1
|
+
import { CliError } from '../http.js';
|
|
2
|
+
import { terminalText } from '../prompts.js';
|
|
3
|
+
import { reportCommand } from '../campaign-reports.js';
|
|
4
|
+
import { identifier } from '../world-contracts.js';
|
|
5
|
+
import { nativeWatch } from './native-report.js';
|
|
6
|
+
/**
|
|
7
|
+
* Campaign progress. A native campaign shows its drill slots and execution
|
|
8
|
+
* states from the native read routes, and --follow keeps polling until the
|
|
9
|
+
* campaign settles, for a bounded time. A retained campaign from the older world commands (retired
|
|
10
|
+
* for new work in this release) is read once from its retained report reads
|
|
11
|
+
* (execution slots and latest activity); there is nothing to follow.
|
|
12
|
+
*/
|
|
13
|
+
export async function watchCommand(args, options, emit) {
|
|
14
|
+
// firedrill simulations lists only retained campaigns, never ones created by firedrill apply.
|
|
15
|
+
if (args.length !== 1)
|
|
16
|
+
throw new CliError('arguments_invalid', 'Use firedrill watch <campaign> [--follow] with one campaign id. '
|
|
17
|
+
+ 'A campaign from firedrill apply is named in its campaign_created output; firedrill apply <plan-id> --request <apply-request-key> '
|
|
18
|
+
+ 'prints the same campaign again, with no second approval or charge. firedrill simulations lists only campaigns from the older world commands.', 2);
|
|
19
|
+
const campaign = identifier(args[0]);
|
|
20
|
+
if (await nativeWatch(campaign, options, emit))
|
|
21
|
+
return;
|
|
22
|
+
const report = (type) => (value, readable) => emit(type, value, readable.split('\n').map(terminalText).join('\n'));
|
|
23
|
+
const common = { json: options.json, workspace: options.workspace, openBrowser: options.openBrowser && !options.json, signal: options.signal };
|
|
24
|
+
await reportCommand([campaign, 'assignments'], common, report('campaign_assignments'));
|
|
25
|
+
await reportCommand([campaign, 'activity'], common, report('campaign_activity'));
|
|
26
|
+
emit('watch_snapshot', { campaign_id: campaign, retained: true, follow: { state: 'not_applicable', reason: 'retained_campaign' } }, (options.follow ? 'Retained campaign: one snapshot. --follow applies to campaigns created by firedrill apply.'
|
|
27
|
+
: 'Retained campaign: one snapshot.') + ' Run firedrill watch again to refresh.');
|
|
28
|
+
}
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
// `firedrill world versions | version | recipients | grants | share |
|
|
2
|
+
// share-request | withdraw-share-request | revoke | fork | fork-request |
|
|
3
|
+
// withdraw-fork | dependents`:
|
|
4
|
+
// reusing a world definition across workspaces of one organization.
|
|
5
|
+
//
|
|
6
|
+
// - Sharing grants one exact world revision to another workspace, read only or
|
|
7
|
+
// read and fork. Only an administrator of the owner workspace shares, lists
|
|
8
|
+
// grants or revokes, and only after the human approves. It shares the closed
|
|
9
|
+
// declaration only: never the owner's descriptor, sources, configuration
|
|
10
|
+
// values, credentials, spending or execution, and never the tools or library
|
|
11
|
+
// versions the declaration references (each needs its own access).
|
|
12
|
+
// - Forking copies one usable revision into a new private world of this
|
|
13
|
+
// workspace, bound to this workspace's agent and descriptor revision, after
|
|
14
|
+
// the human approves. The declaration is the source as read, or the caller's
|
|
15
|
+
// remapped copy; the source is pinned by its exact digest. The fork is this
|
|
16
|
+
// workspace's own afterwards. Tool archive and state forks are different.
|
|
17
|
+
// - Request keys are printed before anything is sent. The same key returns the
|
|
18
|
+
// original; a lost answer is read back or withdrawn, never sent under a new key.
|
|
19
|
+
// Nothing here provisions, runs or spends anything.
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
21
|
+
import { CliError } from '../http.js';
|
|
22
|
+
import { selectOne, terminalText } from '../prompts.js';
|
|
23
|
+
import { selectedWorkspace } from '../world-access.js';
|
|
24
|
+
import { errorCode, retryTransient } from '../native/control-plane.js';
|
|
25
|
+
import { listLibraryRecipients } from '../native/library-sharing.js';
|
|
26
|
+
import { answerLost } from '../native/metadata-requests.js';
|
|
27
|
+
import { getNativeDescriptor, listNativeDescriptors } from '../native/registration.js';
|
|
28
|
+
import { answeredRefusal, checkFork, declarationNeeds, forkWorld, getReusableWorld, listReusableWorlds, listWorldDependents, listWorldGrants, recoverWorldFork, recoverWorldGrant, revokeWorldGrant, shareWorldVersion, withdrawWorldFork, withdrawWorldGrantRequest, WORLD_DEPENDENT_KINDS, worldReuseId, } from '../native/world-reuse.js';
|
|
29
|
+
import { readJsonObject } from './plan-apply.js';
|
|
30
|
+
/** The verbs this module owns under `firedrill world`. */
|
|
31
|
+
export const WORLD_REUSE_VERBS = new Set(['versions', 'version', 'recipients', 'grants', 'share', 'share-request', 'withdraw-share-request', 'revoke',
|
|
32
|
+
'fork', 'fork-request', 'withdraw-fork', 'dependents']);
|
|
33
|
+
const RECOVER_MS = 15_000;
|
|
34
|
+
const DEFINITION_BYTES = 2 * 1024 * 1024;
|
|
35
|
+
const tty = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
36
|
+
const scopeOf = (options) => selectedWorkspace({ workspace: options.workspace, signal: options.signal,
|
|
37
|
+
interactive: tty(options), openBrowser: options.openBrowser && !options.json });
|
|
38
|
+
const sharedHere = (scope, row) => row.owner_workspace_id !== scope.workspace.toLowerCase();
|
|
39
|
+
function pageNumber(value) {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (!/^[1-9][0-9]{0,6}$/.test(value))
|
|
43
|
+
throw new CliError('arguments_invalid', 'Use --page with a number of 1 or more.', 2);
|
|
44
|
+
return Number(value);
|
|
45
|
+
}
|
|
46
|
+
/** Who owns a world version and what this workspace may do with it. */
|
|
47
|
+
export function worldAccess(scope, row) {
|
|
48
|
+
if (sharedHere(scope, row)) {
|
|
49
|
+
return `shared from workspace ${row.owner_workspace_id}: `
|
|
50
|
+
+ `${row.can_use ? 'read and fork' : 'read only here (shared read only, or its world or agent is archived)'}; never editable here`;
|
|
51
|
+
}
|
|
52
|
+
return row.can_use ? 'owned by this workspace' : 'owned by this workspace; its world or agent is archived, so it cannot be forked until restored';
|
|
53
|
+
}
|
|
54
|
+
/** Why a world version cannot be forked here, with the one step that changes that. */
|
|
55
|
+
function unusableText(scope, row) {
|
|
56
|
+
if (sharedHere(scope, row)) {
|
|
57
|
+
return `Workspace ${row.owner_workspace_id} shared this world version read only, or its world or agent is archived, so it cannot be forked here. `
|
|
58
|
+
+ 'Ask its owner for read-and-fork access.';
|
|
59
|
+
}
|
|
60
|
+
return 'Its world or agent is archived. Restore it before forking this version.';
|
|
61
|
+
}
|
|
62
|
+
function versionLine(scope, row) {
|
|
63
|
+
return ` ${row.id} ${terminalText(row.name)} v${row.revision} ${worldAccess(scope, row)} ${terminalText(row.created_at)}`;
|
|
64
|
+
}
|
|
65
|
+
function grantLine(grant) {
|
|
66
|
+
return ` ${grant.id} workspace ${grant.recipient_workspace_id} ${grant.allow_use ? 'read and fork' : 'read only'} `
|
|
67
|
+
+ `shared ${terminalText(grant.created_at)}${grant.revoked_at ? ` revoked ${terminalText(grant.revoked_at)}` : ''}`;
|
|
68
|
+
}
|
|
69
|
+
/** A fork's lines. The agent is named only when this run sent it; a fork read back by key does not record it. */
|
|
70
|
+
function forkText(fork, lead, agentId) {
|
|
71
|
+
return [
|
|
72
|
+
`${lead} into a new private world ${fork.world_id}, revision ${fork.target_revision_id} (digest ${fork.target_digest}).`,
|
|
73
|
+
`Copied from world version ${fork.source_revision_id} at digest ${fork.source_digest}. Later changes to the source never reach it, `
|
|
74
|
+
+ 'and revoking the share does not remove it.',
|
|
75
|
+
`Review it with: firedrill plan --world ${fork.world_id}`,
|
|
76
|
+
`Change it with firedrill world save --agent ${agentId ?? '<the agent it was forked for>'} --world ${fork.world_id} --expected-revision <n> `
|
|
77
|
+
+ `--file <declaration.json|->, where <n> is its newest revision in firedrill world history ${fork.world_id}. `
|
|
78
|
+
+ `The forked declaration is the definition in firedrill world show ${fork.world_id} ${fork.target_revision_id} --json`,
|
|
79
|
+
`Request key (rerun with --request to replay): ${fork.request_key}`,
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
/** Root answers 404 for a page past the last one; that is not a missing version. */
|
|
83
|
+
const pastEnd = (page, otherwise) => (error) => {
|
|
84
|
+
if ((page ?? 1) > 1 && error instanceof CliError && error.httpStatus === 404) {
|
|
85
|
+
throw new CliError('page_not_found', `There is no page ${page}. Start again from --page 1.`, 2);
|
|
86
|
+
}
|
|
87
|
+
return otherwise(error);
|
|
88
|
+
};
|
|
89
|
+
const rethrow = (error) => { throw error; };
|
|
90
|
+
/** Only an organization administrator shares, lists grants, revokes or lists recipients; root says so with admin_required. */
|
|
91
|
+
const adminOnly = (error) => {
|
|
92
|
+
if (errorCode(error) === 'admin_required') {
|
|
93
|
+
throw new CliError('native_world_admin_required', 'Only an administrator of this organization can share world versions, list or revoke their '
|
|
94
|
+
+ 'sharing, or list recipient workspaces. Nothing was shared or changed.', 3, error instanceof CliError ? error.httpStatus : null);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
};
|
|
98
|
+
const notReadable = (error) => {
|
|
99
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
100
|
+
throw new CliError('native_world_version_not_found', 'That is not a world version this workspace can read. Find them with firedrill world versions.', 2);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
};
|
|
104
|
+
/** Grants are listed and revoked by an administrator of the owner workspace only; any other workspace gets 404. */
|
|
105
|
+
const ownerAdminOnly = (error) => {
|
|
106
|
+
if (errorCode(error) === 'admin_required')
|
|
107
|
+
return adminOnly(error);
|
|
108
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
109
|
+
throw new CliError('native_world_version_not_found', 'That is not a world version this workspace owns. Only the owner workspace lists or revokes '
|
|
110
|
+
+ 'its sharing grants; find this workspace\'s versions with firedrill world versions.', 2);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
};
|
|
114
|
+
/** Each verb's arguments and flags, checked before anything is read or sent. */
|
|
115
|
+
export function checkWorldReuseArguments(args, options) {
|
|
116
|
+
const [verb, first, second, third] = args;
|
|
117
|
+
const takes = {
|
|
118
|
+
versions: ['search', 'page'], version: [], recipients: ['search', 'page'], grants: ['page'], share: ['request', 'approve'], 'share-request': [],
|
|
119
|
+
'withdraw-share-request': ['approve'], revoke: ['approve'], fork: ['agent', 'descriptorRevision', 'file', 'request', 'approve'], 'fork-request': [], 'withdraw-fork': ['approve'],
|
|
120
|
+
dependents: ['kind', 'page'],
|
|
121
|
+
};
|
|
122
|
+
const flags = { kind: '--kind', file: '--file', search: '--search', archived: '--archived',
|
|
123
|
+
page: '--page', expectedRevision: '--expected-revision', request: '--request', agent: '--agent', world: '--world',
|
|
124
|
+
descriptorRevision: '--descriptor-revision', approve: '--approve-change', parameters: '--parameters', removeParameters: '--remove-parameters' };
|
|
125
|
+
const extra = Object.keys(flags)
|
|
126
|
+
.filter((flag) => options[flag] !== undefined && options[flag] !== false && !takes[verb]?.includes(flag));
|
|
127
|
+
if (extra.length)
|
|
128
|
+
throw new CliError('arguments_invalid', `firedrill world ${verb} does not take ${extra.map((flag) => flags[flag]).join(', ')}.`, 2);
|
|
129
|
+
const counts = { versions: 1, version: 2, recipients: 1, grants: 2, share: 4, 'share-request': 3, 'withdraw-share-request': 3,
|
|
130
|
+
revoke: 3, fork: 2,
|
|
131
|
+
'fork-request': 2, 'withdraw-fork': 2, dependents: 2 };
|
|
132
|
+
if (args.length !== counts[verb] || (verb === 'share' && third !== 'read' && third !== 'use') || (verb === 'fork' && options.agent === undefined)) {
|
|
133
|
+
throw new CliError('arguments_invalid', 'Use firedrill world versions [--search <prefix>], world version <version-id>, world recipients, '
|
|
134
|
+
+ 'world grants <version-id>, world share <version-id> <workspace-id> read|use, world share-request|withdraw-share-request <version-id> <request-key>, '
|
|
135
|
+
+ 'world revoke <version-id> <grant-id>, world fork <version-id> --agent <agent-id> [--descriptor-revision <id>] [--file <declaration.json|->], '
|
|
136
|
+
+ 'world fork-request <request-key>, world withdraw-fork <request-key> or world dependents <version-id> [--kind fork|campaign].', 2);
|
|
137
|
+
}
|
|
138
|
+
const keyed = verb === 'fork-request' || verb === 'withdraw-fork';
|
|
139
|
+
if (first !== undefined)
|
|
140
|
+
worldReuseId(first, keyed ? 'request key' : 'world version id');
|
|
141
|
+
if (second !== undefined)
|
|
142
|
+
worldReuseId(second, verb === 'share' ? 'workspace id' : verb === 'revoke' ? 'grant id' : 'request key');
|
|
143
|
+
if (options.request !== undefined)
|
|
144
|
+
worldReuseId(options.request, 'request key');
|
|
145
|
+
if (options.agent !== undefined)
|
|
146
|
+
worldReuseId(options.agent, 'agent id');
|
|
147
|
+
if (options.descriptorRevision !== undefined)
|
|
148
|
+
worldReuseId(options.descriptorRevision, 'descriptor revision id');
|
|
149
|
+
if (verb === 'dependents' && options.kind !== undefined && !WORLD_DEPENDENT_KINDS.includes(options.kind)) {
|
|
150
|
+
throw new CliError('arguments_invalid', `Use --kind ${WORLD_DEPENDENT_KINDS.join('|')} with world dependents.`, 2);
|
|
151
|
+
}
|
|
152
|
+
if (options.search !== undefined && [...options.search].length > 120) {
|
|
153
|
+
throw new CliError('arguments_invalid', 'Use a name prefix of up to 120 characters.', 2);
|
|
154
|
+
}
|
|
155
|
+
pageNumber(options.page);
|
|
156
|
+
}
|
|
157
|
+
/** `share <version-id> <workspace-id> read|use [--request <key>] [--approve-change]`. */
|
|
158
|
+
async function share(scope, args, options, emit) {
|
|
159
|
+
const [, version, recipientArg, mode] = args;
|
|
160
|
+
const signal = options.signal;
|
|
161
|
+
const recipient = worldReuseId(recipientArg, 'workspace id');
|
|
162
|
+
if (recipient === scope.workspace.toLowerCase()) {
|
|
163
|
+
throw new CliError('arguments_invalid', 'Share with another workspace of this organization; this one already owns the world. Nothing was sent.', 2);
|
|
164
|
+
}
|
|
165
|
+
const allowUse = mode === 'use';
|
|
166
|
+
const access = allowUse ? 'read and fork' : 'read only';
|
|
167
|
+
const requestKey = options.request === undefined ? randomUUID() : worldReuseId(options.request, 'request key');
|
|
168
|
+
const rerun = `firedrill world share ${version.toLowerCase()} ${recipient} ${mode} --request ${requestKey}`;
|
|
169
|
+
const read = `firedrill world share-request ${version.toLowerCase()} ${requestKey}`;
|
|
170
|
+
const row = await getReusableWorld(scope, { revisionId: version, signal }).catch(notReadable);
|
|
171
|
+
if (sharedHere(scope, row)) {
|
|
172
|
+
throw new CliError('native_world_share_owner_only', `Only workspace ${row.owner_workspace_id}, which owns this world, can share it. Nothing was sent.`, 2);
|
|
173
|
+
}
|
|
174
|
+
// A grant already made under this key is shown, never made again. The read
|
|
175
|
+
// is administrator-only, so a fresh key also confirms that before the human is asked.
|
|
176
|
+
const original = await recoverWorldGrant(scope, { revisionId: row.id, requestKey, signal }).catch(adminOnly);
|
|
177
|
+
if (original) {
|
|
178
|
+
if (original.recipient_workspace_id !== recipient || original.allow_use !== allowUse) {
|
|
179
|
+
throw new CliError('idempotency_conflict', `Request ${requestKey} shared this world version with workspace ${original.recipient_workspace_id} `
|
|
180
|
+
+ `(${original.allow_use ? 'read and fork' : 'read only'}). Nothing was changed; share differently under a new request.`, 2);
|
|
181
|
+
}
|
|
182
|
+
emit('world_grant', original, `Already shared under request ${requestKey}:\n${grantLine(original)}`
|
|
183
|
+
+ (original.revoked_at ? '\nThis grant was revoked. A request key is never renewed: to share again, start without --request.' : ''));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
emit('world_share_review', { version: row, recipient_workspace_id: recipient, allow_use: allowUse, request_key: requestKey }, [
|
|
187
|
+
`Share world ${terminalText(row.name)} v${row.revision} (version ${row.id}) with workspace ${recipient}: ${access}.`,
|
|
188
|
+
allowUse ? 'It can read this exact declaration and fork it into its own private worlds.'
|
|
189
|
+
: 'It can read this exact declaration, but not fork it.',
|
|
190
|
+
'The declaration holds references and configuration names only. That workspace never gets this workspace\'s descriptor, sources, configuration '
|
|
191
|
+
+ 'values or credentials, and no access to the tools or library versions it references: those need their own sharing.',
|
|
192
|
+
'It can never edit this world. Revoking later stops new reads and forks; forks it already made stay its own.',
|
|
193
|
+
row.can_use ? null : 'Its world or agent is archived now, so it cannot be forked until restored (an archived world cannot be shared at all).',
|
|
194
|
+
].filter((line) => line !== null).join('\n'));
|
|
195
|
+
let approved = options.approve === true;
|
|
196
|
+
if (!approved && tty(options)) {
|
|
197
|
+
approved = await selectOne(`Share this world version with workspace ${recipient} (${access})?`, [
|
|
198
|
+
{ value: false, label: 'Not now', detail: 'Nothing is shared.' },
|
|
199
|
+
{ value: true, label: 'Share it', detail: `Grants ${access} access to this exact declaration only.` },
|
|
200
|
+
], signal);
|
|
201
|
+
}
|
|
202
|
+
if (!approved)
|
|
203
|
+
throw new CliError('world_share_consent_required', `Nothing was shared. After the human approves: ${rerun} --approve-change`, 2);
|
|
204
|
+
if (options.json)
|
|
205
|
+
emit('request_prepared', { request_key: requestKey }, '');
|
|
206
|
+
else
|
|
207
|
+
process.stderr.write(`Request key: ${requestKey} (if interrupted, rerun ${rerun} --approve-change; ${read} reads it back)\n`);
|
|
208
|
+
let grant;
|
|
209
|
+
try {
|
|
210
|
+
// The same key and body replay the original grant, so a retry never shares twice.
|
|
211
|
+
grant = await retryTransient(() => shareWorldVersion(scope, { revisionId: row.id, recipientWorkspaceId: recipient, allowUse, requestKey, signal }), { signal, attempts: 3 });
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (errorCode(error) === 'admin_required')
|
|
215
|
+
adminOnly(error);
|
|
216
|
+
if (errorCode(error) === 'native_world_grant_withdrawn') {
|
|
217
|
+
throw new CliError('native_world_grant_withdrawn', `Sharing request ${requestKey} was withdrawn, so it will never land. Share again without --request `
|
|
218
|
+
+ 'only if the human still wants it.', 2);
|
|
219
|
+
}
|
|
220
|
+
// A refusal with the service's own code wrote nothing; only an answer that never arrived is recovered by its key.
|
|
221
|
+
if (signal.aborted || !answerLost(error) || answeredRefusal(error)) {
|
|
222
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
223
|
+
throw new CliError('native_world_recipient_unavailable', 'That workspace is not another active workspace of this organization, or this world '
|
|
224
|
+
+ 'is archived. Find recipients with firedrill world recipients. Nothing was shared.', 2);
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
const recovered = await recoverWorldGrant(scope, { revisionId: row.id, requestKey, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
|
|
229
|
+
if (!recovered) {
|
|
230
|
+
throw new CliError('native_world_share_unconfirmed', `The share was not confirmed and may still land. Read it with ${read}, or rerun ${rerun} `
|
|
231
|
+
+ '--approve-change, which returns the original instead of sharing twice. Before sharing differently, withdraw it: '
|
|
232
|
+
+ `firedrill world withdraw-share-request ${row.id} ${requestKey} --approve-change. Never share it again under a new key.`);
|
|
233
|
+
}
|
|
234
|
+
if (recovered.recipient_workspace_id !== recipient || recovered.allow_use !== allowUse) {
|
|
235
|
+
throw new CliError('native_response_unconfirmed', `Request ${requestKey} recorded a different grant, not this share. Nothing else was sent. `
|
|
236
|
+
+ `Read it with ${read}.`, 2);
|
|
237
|
+
}
|
|
238
|
+
grant = recovered;
|
|
239
|
+
}
|
|
240
|
+
emit('world_grant', grant, `Shared world ${terminalText(row.name)} v${row.revision} with workspace ${grant.recipient_workspace_id}: `
|
|
241
|
+
+ `${grant.allow_use ? 'read and fork' : 'read only'}.\n${grantLine(grant)}\nRevoke it with firedrill world revoke ${row.id} ${grant.id}`);
|
|
242
|
+
}
|
|
243
|
+
/** `revoke <version-id> <grant-id> [--approve-change]`. */
|
|
244
|
+
async function revoke(scope, args, options, emit) {
|
|
245
|
+
const [, version, grantArg] = args;
|
|
246
|
+
const signal = options.signal;
|
|
247
|
+
const revisionId = worldReuseId(version, 'world version id');
|
|
248
|
+
const grantId = worldReuseId(grantArg, 'grant id');
|
|
249
|
+
// Revoked by its exact id: root's revoke needs no listing, and grant history
|
|
250
|
+
// is never searched (an old grant may sit beyond any page bound). The
|
|
251
|
+
// original-request read is administrator-only, so reading a fresh key
|
|
252
|
+
// confirms that before the human is asked. It changes nothing.
|
|
253
|
+
await recoverWorldGrant(scope, { revisionId, requestKey: randomUUID(), signal }).catch(ownerAdminOnly);
|
|
254
|
+
const command = `firedrill world revoke ${revisionId} ${grantId}`;
|
|
255
|
+
let approved = options.approve === true;
|
|
256
|
+
if (!approved && tty(options)) {
|
|
257
|
+
approved = await selectOne(`Revoke sharing grant ${grantId} of world version ${revisionId}?`, [
|
|
258
|
+
{ value: false, label: 'Not now', detail: 'Nothing is changed.' },
|
|
259
|
+
{ value: true, label: 'Revoke', detail: 'Stops that workspace\'s new reads and forks. Forks it already made are its own and are not removed.' },
|
|
260
|
+
], signal);
|
|
261
|
+
}
|
|
262
|
+
if (!approved)
|
|
263
|
+
throw new CliError('world_share_consent_required', `Nothing was changed. After the human approves: ${command} --approve-change`, 2);
|
|
264
|
+
// Idempotent: a grant revoked earlier is returned as it is, with its original revocation time.
|
|
265
|
+
const revoked = await retryTransient(() => revokeWorldGrant(scope, { revisionId, grantId, signal }), { signal, attempts: 3 })
|
|
266
|
+
.catch((error) => {
|
|
267
|
+
if (errorCode(error) !== 'admin_required' && error instanceof CliError && error.httpStatus === 404) {
|
|
268
|
+
throw new CliError('native_world_grant_not_found', 'That is not a sharing grant of a world version this workspace owns. Nothing was changed. '
|
|
269
|
+
+ `List them with firedrill world grants ${revisionId}.`, 2);
|
|
270
|
+
}
|
|
271
|
+
return ownerAdminOnly(error);
|
|
272
|
+
});
|
|
273
|
+
emit('world_grant', revoked, `Grant ${revoked.id} is revoked (since ${terminalText(revoked.revoked_at)}).\n${grantLine(revoked)}\n`
|
|
274
|
+
+ `Workspace ${revoked.recipient_workspace_id} can no longer read or fork this world version. Forks it already made stay its own.`);
|
|
275
|
+
}
|
|
276
|
+
/** `fork <version-id> --agent <agent-id> [--descriptor-revision <id>] [--file <declaration.json|->] [--request <key>] [--approve-change]`. */
|
|
277
|
+
async function fork(scope, args, options, emit) {
|
|
278
|
+
const [, version] = args;
|
|
279
|
+
const signal = options.signal;
|
|
280
|
+
const sourceId = worldReuseId(version, 'world version id');
|
|
281
|
+
const agentId = worldReuseId(options.agent, 'agent id');
|
|
282
|
+
const remapped = options.file === undefined ? undefined
|
|
283
|
+
: await readJsonObject(options.file, 'the remapped world declaration', signal, DEFINITION_BYTES);
|
|
284
|
+
const requestKey = options.request === undefined ? randomUUID() : worldReuseId(options.request, 'request key');
|
|
285
|
+
// A fork already made under this key is shown, never made again. Root
|
|
286
|
+
// returns it before checking current access, so this works after a revoke.
|
|
287
|
+
if (options.request !== undefined) {
|
|
288
|
+
const original = await recoverWorldFork(scope, { requestKey, signal });
|
|
289
|
+
if (original.state === 'withdrawn') {
|
|
290
|
+
throw new CliError('native_world_fork_withdrawn', `Fork request ${requestKey} was withdrawn, so it will never land. Fork again without --request `
|
|
291
|
+
+ 'only if the human still wants it.', 2);
|
|
292
|
+
}
|
|
293
|
+
if (original.state === 'committed') {
|
|
294
|
+
if (original.fork.source_revision_id !== sourceId) {
|
|
295
|
+
throw new CliError('idempotency_conflict', `Request ${requestKey} forked world version ${original.fork.source_revision_id}, not ${sourceId}. `
|
|
296
|
+
+ 'Nothing was sent; fork this version without --request.', 2);
|
|
297
|
+
}
|
|
298
|
+
return recoveredOnly(original.fork, requestKey, emit);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const source = await getReusableWorld(scope, { revisionId: sourceId, signal }).catch(notReadable);
|
|
302
|
+
if (!source.can_use)
|
|
303
|
+
throw new CliError('native_world_use_not_granted', `${unusableText(scope, source)} Nothing was sent.`, 2);
|
|
304
|
+
const definition = remapped ?? source.definition;
|
|
305
|
+
const descriptorRevision = options.descriptorRevision !== undefined
|
|
306
|
+
? await agentDescriptor(scope, agentId, worldReuseId(options.descriptorRevision, 'descriptor revision id'), signal)
|
|
307
|
+
: await newestDescriptor(scope, agentId, signal);
|
|
308
|
+
const needs = declarationNeeds(definition);
|
|
309
|
+
const references = [
|
|
310
|
+
needs.simulatedTools ? `${needs.simulatedTools} simulated tool version${needs.simulatedTools === 1 ? '' : 's'}` : null,
|
|
311
|
+
needs.realTools ? `${needs.realTools} real tool${needs.realTools === 1 ? '' : 's'}` : null,
|
|
312
|
+
needs.libraryVersions ? `${needs.libraryVersions} library version${needs.libraryVersions === 1 ? '' : 's'}` : null,
|
|
313
|
+
needs.worldItems ? `${needs.worldItems} world item${needs.worldItems === 1 ? '' : 's'}` : null,
|
|
314
|
+
].filter((part) => part !== null);
|
|
315
|
+
const rerun = `firedrill world fork ${sourceId} --agent ${agentId} --descriptor-revision ${descriptorRevision}`
|
|
316
|
+
+ `${options.file === undefined ? '' : ' --file <the same declaration>'} --request ${requestKey}`;
|
|
317
|
+
emit('world_fork_review', { source, agent_id: agentId, native_descriptor_id: descriptorRevision, remapped: remapped !== undefined, needs,
|
|
318
|
+
request_key: requestKey }, [
|
|
319
|
+
`Fork world ${terminalText(source.name)} v${source.revision} (version ${source.id}`
|
|
320
|
+
+ `${sharedHere(scope, source) ? `, shared from workspace ${source.owner_workspace_id}` : ''}) into a new private world of this workspace,`,
|
|
321
|
+
`for agent ${agentId} with descriptor revision ${descriptorRevision}${options.descriptorRevision === undefined ? ' (its newest)' : ''}.`,
|
|
322
|
+
remapped ? `The declaration is your remapped file; the source is pinned by its digest ${source.definition_digest}.`
|
|
323
|
+
: `The declaration is copied as read (digest ${source.definition_digest}). To point it at other versions this workspace uses, `
|
|
324
|
+
+ `export its definition with firedrill world version ${source.id} --json, edit it and pass it with --file.`,
|
|
325
|
+
references.length ? `It references ${references.join(', ')}.` : null,
|
|
326
|
+
'Forking grants no access to anything it references: each must already be usable by this workspace, or the fork is refused.',
|
|
327
|
+
needs.configurationNames.length ? `Configuration names (values are never copied; this workspace sets its own): ${needs.configurationNames.map(terminalText).join(', ')}.` : null,
|
|
328
|
+
'The new world is this workspace\'s own. Nothing is provisioned, run or spent.',
|
|
329
|
+
].filter((line) => line !== null).join('\n'));
|
|
330
|
+
let approved = options.approve === true;
|
|
331
|
+
if (!approved && tty(options)) {
|
|
332
|
+
approved = await selectOne('Fork this world version into a new private world?', [
|
|
333
|
+
{ value: false, label: 'Not now', detail: 'Nothing is forked.' },
|
|
334
|
+
{ value: true, label: 'Fork it', detail: 'Creates a new world of this workspace from this exact declaration.' },
|
|
335
|
+
], signal);
|
|
336
|
+
}
|
|
337
|
+
if (!approved)
|
|
338
|
+
throw new CliError('world_fork_consent_required', `Nothing was forked. After the human approves: ${rerun} --approve-change`, 2);
|
|
339
|
+
if (options.json)
|
|
340
|
+
emit('request_prepared', { request_key: requestKey }, '');
|
|
341
|
+
else
|
|
342
|
+
process.stderr.write(`Request key: ${requestKey} (if interrupted, rerun ${rerun} --approve-change; firedrill world fork-request ${requestKey} reads it back)\n`);
|
|
343
|
+
let result;
|
|
344
|
+
try {
|
|
345
|
+
// The same key and body return the original fork, so a retry never forks twice.
|
|
346
|
+
result = await retryTransient(() => forkWorld(scope, { source, agentId, descriptorRevisionId: descriptorRevision, definition, requestKey, signal }), { signal, attempts: 3 });
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
// A refusal with the service's own code wrote nothing; only an answer that never arrived is recovered by its key.
|
|
350
|
+
if (signal.aborted || !answerLost(error) || answeredRefusal(error)) {
|
|
351
|
+
// The source was just read as usable, so a 404 most often means the
|
|
352
|
+
// destination: root also answers 404 for an archived or non-local agent.
|
|
353
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
354
|
+
throw new CliError('native_world_fork_not_found', `Nothing was forked. Agent ${agentId} may be archived or not a locally registered agent, `
|
|
355
|
+
+ `or descriptor revision ${descriptorRevision} may no longer be one of its revisions (firedrill descriptors --agent ${agentId}). `
|
|
356
|
+
+ `If both are right, the share was revoked or the source world was archived just now: check with firedrill world version ${sourceId}.`, 2);
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
// No answer: read the original back by its key. Never sent again here, never under a new key.
|
|
361
|
+
const recovered = await recoverWorldFork(scope, { requestKey, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
|
|
362
|
+
if (recovered?.state === 'withdrawn') {
|
|
363
|
+
throw new CliError('native_world_fork_withdrawn', `Fork request ${requestKey} was withdrawn, so it will never land.`, 2);
|
|
364
|
+
}
|
|
365
|
+
if (recovered?.state !== 'committed') {
|
|
366
|
+
throw new CliError('native_world_fork_unconfirmed', `The fork was not confirmed and may still land (request ${requestKey}). Read it with `
|
|
367
|
+
+ `firedrill world fork-request ${requestKey}, or rerun ${rerun} --approve-change, which returns the original instead of forking twice. `
|
|
368
|
+
+ `Before forking differently, withdraw it: firedrill world withdraw-fork ${requestKey}. Never fork it again under a new key.`);
|
|
369
|
+
}
|
|
370
|
+
result = checkFork(recovered.fork, { sourceId, sourceDigest: source.definition_digest, requestKey });
|
|
371
|
+
// A key this run created was never used before, so the recovered fork is
|
|
372
|
+
// this request. A key the caller supplied may hold an earlier original.
|
|
373
|
+
if (options.request !== undefined)
|
|
374
|
+
return recoveredOnly(result, requestKey, emit);
|
|
375
|
+
}
|
|
376
|
+
emit('world_fork', result, forkText(result, 'Forked', agentId));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* An original fork read back by a caller-supplied key. Root identifies it by
|
|
380
|
+
* the key, not by this invocation's flags, so it is shown as recorded: the
|
|
381
|
+
* agent, descriptor revision and declaration given now are never claimed.
|
|
382
|
+
*/
|
|
383
|
+
function recoveredOnly(fork, requestKey, emit) {
|
|
384
|
+
emit('native_request', { kind: 'world_fork', request_key: requestKey, state: 'committed', receipt: fork, submitted_change_confirmed: false }, [
|
|
385
|
+
forkText(fork, 'Recovered the original fork under this request key'),
|
|
386
|
+
'It keeps the agent, descriptor revision and declaration it was created with. The --agent, --descriptor-revision and --file given now are '
|
|
387
|
+
+ 'not applied and not confirmed to match.',
|
|
388
|
+
`Inspect it with firedrill world show ${fork.world_id} ${fork.target_revision_id} before continuing. To fork differently, run without --request.`,
|
|
389
|
+
].join('\n'));
|
|
390
|
+
}
|
|
391
|
+
/** Root answers a fork for a missing destination with a bare 404, so the destination is read before the human is asked. */
|
|
392
|
+
const destinationMissing = (agentId, descriptorId) => (error) => {
|
|
393
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
394
|
+
throw new CliError('native_world_fork_destination_not_found', descriptorId === undefined
|
|
395
|
+
? `Agent ${agentId} is not an agent of this workspace. Use the agent id from firedrill status. Nothing was sent.`
|
|
396
|
+
: `Agent ${agentId} has no descriptor revision ${descriptorId} in this workspace. List its revisions with `
|
|
397
|
+
+ `firedrill descriptors --agent ${agentId}. Nothing was sent.`, 2);
|
|
398
|
+
}
|
|
399
|
+
throw error;
|
|
400
|
+
};
|
|
401
|
+
/** This agent's newest registered descriptor revision: the default a fork binds to, which the review names. */
|
|
402
|
+
async function newestDescriptor(scope, agentId, signal) {
|
|
403
|
+
const newest = (await listNativeDescriptors(scope, { agentId, page: 1, signal }).catch(destinationMissing(agentId))).results[0];
|
|
404
|
+
if (!newest)
|
|
405
|
+
throw new CliError('native_descriptor_required', 'Register a descriptor for this agent first with firedrill register. Nothing was sent.', 2);
|
|
406
|
+
return newest.id.toLowerCase();
|
|
407
|
+
}
|
|
408
|
+
/** An explicit descriptor revision must be one of this agent's own. */
|
|
409
|
+
async function agentDescriptor(scope, agentId, descriptorId, signal) {
|
|
410
|
+
await getNativeDescriptor(scope, { agentId, descriptorId, signal }).catch(destinationMissing(agentId, descriptorId));
|
|
411
|
+
return descriptorId;
|
|
412
|
+
}
|
|
413
|
+
/** `withdraw-share-request <version-id> <request-key> [--approve-change]`. */
|
|
414
|
+
async function withdrawShareRequest(scope, revisionId, requestKey, options, emit) {
|
|
415
|
+
const signal = options.signal;
|
|
416
|
+
// Administrator-only, like the share itself; a grant that already landed is shown, not withdrawn.
|
|
417
|
+
const original = await recoverWorldGrant(scope, { revisionId, requestKey, signal }).catch((error) => ownerAdminOnly(error));
|
|
418
|
+
if (original) {
|
|
419
|
+
emit('world_grant', original, `This sharing request already landed, so nothing was withdrawn:\n${grantLine(original)}\n`
|
|
420
|
+
+ (original.revoked_at ? 'It is already revoked.' : `Revoke it instead: firedrill world revoke ${revisionId} ${original.id} --approve-change`));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const command = `firedrill world withdraw-share-request ${revisionId} ${requestKey}`;
|
|
424
|
+
let approved = options.approve === true;
|
|
425
|
+
if (!approved && tty(options)) {
|
|
426
|
+
approved = await selectOne(`Withdraw sharing request ${requestKey} so it can never land?`, [
|
|
427
|
+
{ value: false, label: 'Not now', detail: 'Nothing is changed.' },
|
|
428
|
+
{ value: true, label: 'Withdraw', detail: 'If it already landed, the grant is shown instead and kept.' },
|
|
429
|
+
], signal);
|
|
430
|
+
}
|
|
431
|
+
if (!approved)
|
|
432
|
+
throw new CliError('world_share_consent_required', `Nothing was withdrawn. After the human approves: ${command} --approve-change`, 2);
|
|
433
|
+
const outcome = await retryTransient(() => withdrawWorldGrantRequest(scope, { revisionId, requestKey, signal }), { signal, attempts: 3 })
|
|
434
|
+
.catch(ownerAdminOnly);
|
|
435
|
+
if (outcome.state === 'committed') {
|
|
436
|
+
emit('world_grant', outcome.grant, `This sharing request had already landed, so nothing was withdrawn:\n${grantLine(outcome.grant)}\n`
|
|
437
|
+
+ `Revoke it instead: firedrill world revoke ${revisionId} ${outcome.grant.id} --approve-change`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
emit('world_share_withdrawal', { state: 'withdrawn', revision_id: revisionId, request_key: requestKey }, `Withdrawn. Sharing request ${requestKey} will never land. Share again without --request only if the human still wants it.`);
|
|
441
|
+
}
|
|
442
|
+
/** `withdraw-fork <request-key> [--approve-change]`. */
|
|
443
|
+
async function withdrawFork(scope, requestKey, options, emit) {
|
|
444
|
+
const signal = options.signal;
|
|
445
|
+
const original = await recoverWorldFork(scope, { requestKey, signal });
|
|
446
|
+
if (original.state === 'committed') {
|
|
447
|
+
emit('world_fork', original.fork, forkText(original.fork, 'This request already landed, so nothing was withdrawn. It forked'));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (original.state === 'withdrawn') {
|
|
451
|
+
emit('world_fork_withdrawal', { state: 'withdrawn', request_key: requestKey }, `Fork request ${requestKey} is already withdrawn; it will never land.`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const command = `firedrill world withdraw-fork ${requestKey}`;
|
|
455
|
+
let approved = options.approve === true;
|
|
456
|
+
if (!approved && tty(options)) {
|
|
457
|
+
approved = await selectOne(`Withdraw fork request ${requestKey} so it can never land?`, [
|
|
458
|
+
{ value: false, label: 'Not now', detail: 'Nothing is changed.' },
|
|
459
|
+
{ value: true, label: 'Withdraw', detail: 'If it already landed, the fork is shown instead and kept.' },
|
|
460
|
+
], signal);
|
|
461
|
+
}
|
|
462
|
+
if (!approved)
|
|
463
|
+
throw new CliError('world_fork_consent_required', `Nothing was withdrawn. After the human approves: ${command} --approve-change`, 2);
|
|
464
|
+
const outcome = await retryTransient(() => withdrawWorldFork(scope, { requestKey, signal }), { signal, attempts: 3 });
|
|
465
|
+
if (outcome.state === 'committed') {
|
|
466
|
+
emit('world_fork', outcome.fork, forkText(outcome.fork, 'This request had already landed, so nothing was withdrawn. It forked'));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
emit('world_fork_withdrawal', { state: 'withdrawn', request_key: requestKey }, `Withdrawn. Fork request ${requestKey} will never land. Fork again without --request only if the human still wants it.`);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* `world versions [--search <prefix>] [--page <n>] | version <version-id> | recipients [--search <prefix>] [--page <n>]
|
|
473
|
+
* | grants <version-id> [--page <n>] | share <version-id> <workspace-id> read|use [--request <key>] [--approve-change]
|
|
474
|
+
* | share-request <version-id> <key> | withdraw-share-request <version-id> <key> [--approve-change]
|
|
475
|
+
* | revoke <version-id> <grant-id> [--approve-change]
|
|
476
|
+
* | fork <version-id> --agent <id> [--descriptor-revision <id>] [--file <declaration.json|->] [--request <key>] [--approve-change]
|
|
477
|
+
* | fork-request <key> | withdraw-fork <key> [--approve-change] | dependents <version-id> [--kind fork|campaign] [--page <n>]`.
|
|
478
|
+
*/
|
|
479
|
+
export async function worldReuseCommand(args, options, emit) {
|
|
480
|
+
checkWorldReuseArguments(args, options);
|
|
481
|
+
const [verb, first, second] = args;
|
|
482
|
+
const signal = options.signal;
|
|
483
|
+
const scope = await scopeOf(options);
|
|
484
|
+
if (verb === 'share')
|
|
485
|
+
return share(scope, args, options, emit);
|
|
486
|
+
if (verb === 'revoke')
|
|
487
|
+
return revoke(scope, args, options, emit);
|
|
488
|
+
if (verb === 'fork')
|
|
489
|
+
return fork(scope, args, options, emit);
|
|
490
|
+
if (verb === 'withdraw-fork')
|
|
491
|
+
return withdrawFork(scope, worldReuseId(first, 'request key'), options, emit);
|
|
492
|
+
if (verb === 'withdraw-share-request') {
|
|
493
|
+
return withdrawShareRequest(scope, worldReuseId(first, 'world version id'), worldReuseId(second, 'request key'), options, emit);
|
|
494
|
+
}
|
|
495
|
+
if (verb === 'versions') {
|
|
496
|
+
const number = pageNumber(options.page);
|
|
497
|
+
const page = await listReusableWorlds(scope, { search: options.search, page: number, signal }).catch(pastEnd(number, rethrow));
|
|
498
|
+
emit('world_versions', page, [
|
|
499
|
+
`World versions this workspace can read, newest first, page ${page.page} (${page.count} in all):`,
|
|
500
|
+
...page.results.map((row) => versionLine(scope, row)),
|
|
501
|
+
page.results.length ? null : ' none',
|
|
502
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
503
|
+
'Read one: firedrill world version <version-id>. Fork one: firedrill world fork <version-id> --agent <agent-id>',
|
|
504
|
+
].filter((line) => line !== null).join('\n'));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (verb === 'version') {
|
|
508
|
+
const row = await getReusableWorld(scope, { revisionId: worldReuseId(first, 'world version id'), signal }).catch(notReadable);
|
|
509
|
+
const needs = declarationNeeds(row.definition);
|
|
510
|
+
emit('world_version', row, [
|
|
511
|
+
`World ${terminalText(row.name)} v${row.revision}: version ${row.id} digest ${row.definition_digest} ${terminalText(row.created_at)}`,
|
|
512
|
+
`Access: ${worldAccess(scope, row)}`,
|
|
513
|
+
`References: ${needs.simulatedTools} simulated tool version(s), ${needs.realTools} real tool(s), ${needs.libraryVersions} library version(s), `
|
|
514
|
+
+ `${needs.worldItems} world item(s). Configuration names: ${needs.configurationNames.length ? needs.configurationNames.map(terminalText).join(', ') : 'none'}.`,
|
|
515
|
+
terminalText(JSON.stringify(row.definition, null, 2)),
|
|
516
|
+
row.can_use ? `Fork it: firedrill world fork ${row.id} --agent <agent-id>` : null,
|
|
517
|
+
sharedHere(scope, row) ? null : `Share it: firedrill world share ${row.id} <workspace-id> read|use`,
|
|
518
|
+
].filter((line) => line !== null).join('\n'));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (verb === 'recipients') {
|
|
522
|
+
// The organization's recipient chooser is shared with library sharing.
|
|
523
|
+
const number = pageNumber(options.page);
|
|
524
|
+
const page = await listLibraryRecipients(scope, { search: options.search, page: number, signal }).catch(pastEnd(number, adminOnly));
|
|
525
|
+
emit('world_recipients', page, [
|
|
526
|
+
`Workspaces of this organization a world version can be shared with, page ${page.page} (${page.count} in all):`,
|
|
527
|
+
...page.results.map((one) => ` ${one.id} ${terminalText(one.name)}`),
|
|
528
|
+
page.results.length ? null : ' none',
|
|
529
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
530
|
+
'Share a version: firedrill world share <version-id> <workspace-id> read|use',
|
|
531
|
+
].filter((line) => line !== null).join('\n'));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (verb === 'grants') {
|
|
535
|
+
const revisionId = worldReuseId(first, 'world version id');
|
|
536
|
+
const number = pageNumber(options.page);
|
|
537
|
+
const page = await listWorldGrants(scope, { revisionId, page: number, signal }).catch(pastEnd(number, ownerAdminOnly));
|
|
538
|
+
emit('world_grants', { revision_id: revisionId, ...page }, [
|
|
539
|
+
`Sharing grants of world version ${revisionId}, page ${page.page} (${page.count} in all, revoked ones included):`,
|
|
540
|
+
...page.results.map(grantLine),
|
|
541
|
+
page.results.length ? null : ' none',
|
|
542
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
543
|
+
`Share it: firedrill world share ${revisionId} <workspace-id> read|use`,
|
|
544
|
+
].filter((line) => line !== null).join('\n'));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (verb === 'share-request') {
|
|
548
|
+
const revisionId = worldReuseId(first, 'world version id');
|
|
549
|
+
const requestKey = worldReuseId(second, 'request key');
|
|
550
|
+
const grant = await recoverWorldGrant(scope, { revisionId, requestKey, signal }).catch(adminOnly);
|
|
551
|
+
emit('world_grant', grant ?? { revision_id: revisionId, request_key: requestKey, state: 'not_found' }, grant ? grantLine(grant).trim()
|
|
552
|
+
: `None of your sharing requests for this world version is recorded under ${requestKey}. An unanswered request may still arrive. `
|
|
553
|
+
+ 'Rerun the original share with the same --request key (it returns the original instead of sharing twice), or withdraw it first: '
|
|
554
|
+
+ `firedrill world withdraw-share-request ${revisionId} ${requestKey}. Never share it under a new key.`);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (verb === 'fork-request') {
|
|
558
|
+
const requestKey = worldReuseId(first, 'request key');
|
|
559
|
+
const original = await recoverWorldFork(scope, { requestKey, signal });
|
|
560
|
+
if (original.state === 'committed') {
|
|
561
|
+
emit('world_fork', original.fork, forkText(original.fork, 'Forked'));
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
emit('world_fork_request', { request_key: requestKey, state: original.state }, original.state === 'withdrawn'
|
|
565
|
+
? `Fork request ${requestKey} was withdrawn; it will never land.`
|
|
566
|
+
: `None of your fork requests is recorded under ${requestKey}. An unanswered request may still land. Rerun the original fork with `
|
|
567
|
+
+ `--request ${requestKey} (it returns the original instead of forking twice), or withdraw it first: firedrill world withdraw-fork ${requestKey}. `
|
|
568
|
+
+ 'Never fork it again under a new key.');
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const revisionId = worldReuseId(first, 'world version id');
|
|
573
|
+
const kind = (options.kind ?? 'fork');
|
|
574
|
+
const number = pageNumber(options.page);
|
|
575
|
+
const page = await listWorldDependents(scope, { revisionId, kind, page: number, signal }).catch(pastEnd(number, notReadable));
|
|
576
|
+
emit('world_dependents', { revision_id: revisionId, kind, ...page }, [
|
|
577
|
+
`${kind === 'fork' ? 'Forks' : 'Campaigns'} in this workspace from world version ${revisionId}, page ${page.page} (${page.count} in all):`,
|
|
578
|
+
...page.results.map((one) => 'world_id' in one
|
|
579
|
+
? ` fork ${one.id} world ${one.world_id} revision ${one.target_revision_id} ${terminalText(one.created_at)}`
|
|
580
|
+
: ` campaign ${one.id} ${terminalText(one.state)} ${terminalText(one.created_at)}`),
|
|
581
|
+
page.results.length ? null : ' none',
|
|
582
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
583
|
+
'Other workspaces\' forks and campaigns are never shown here.',
|
|
584
|
+
kind === 'fork' ? `Campaigns planned from it: firedrill world dependents ${revisionId} --kind campaign` : null,
|
|
585
|
+
].filter((line) => line !== null).join('\n'));
|
|
586
|
+
}
|