firedrill-run 2.2.4 → 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,277 @@
|
|
|
1
|
+
import { APP_ORIGIN } from './http.js';
|
|
2
|
+
import { api, openBrowser } from './auth.js';
|
|
3
|
+
import { CliError } from './http.js';
|
|
4
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
5
|
+
import { selectedWorkspace } from './world-access.js';
|
|
6
|
+
import { identifier } from './world-contracts.js';
|
|
7
|
+
import { ciConfigurationRead, ciConfigurationMatches, ciConfigurationsMatch, ciRevisionPageMatches, ciScenariosMatch } from './ci-configuration-contracts.js';
|
|
8
|
+
import { ciRunRead, ciRunMatches, ciRunsMatch } from './ci-run-contracts.js';
|
|
9
|
+
import { ciAccessRead, ciApprovalsMatch, ciActivationsMatch } from './ci-access-contracts.js';
|
|
10
|
+
import { ciResultRead, ciResultMatches, ciScoresMatch, ciCriteriaMatches } from './ci-result-contracts.js';
|
|
11
|
+
import { ciDeliveryRead, ciDeliveryMatches, ciDeliveriesMatch, ciDeliveryStates } from './ci-delivery-contracts.js';
|
|
12
|
+
import { reviewCISetupProposal } from './ci-setup-proposals.js';
|
|
13
|
+
import { ciCoverage } from './ci-coverage.js';
|
|
14
|
+
function configuredPlan(row) {
|
|
15
|
+
return row.baseline ? `Baseline: ${terminalText(row.baseline.world_name)} · Saved plan ${row.baseline.plan_revision} · ${row.baseline.scenario_count.toLocaleString()} scenarios · ${row.baseline.planned_trials.toLocaleString()} drills`
|
|
16
|
+
: 'Saved world unavailable';
|
|
17
|
+
}
|
|
18
|
+
function scenarioPolicy(row) {
|
|
19
|
+
const policy = row.head?.document.scenario_policy;
|
|
20
|
+
return policy?.mode === 'change_relevant' ? `PR selection: change-relevant, with ${policy.mandatory_scenario_ids.length.toLocaleString()} additional mandatory cases. Nightly keeps all baseline cases.`
|
|
21
|
+
: 'PR selection: all planned scenarios.';
|
|
22
|
+
}
|
|
23
|
+
function configuredTriggers(row) {
|
|
24
|
+
return row.head?.document.triggers.map(value => value === 'pull_request' ? 'Pull requests' : value === 'nightly'
|
|
25
|
+
? `Nightly: ${String(row.head.document.nightly_hour_utc).padStart(2, '0')}:00 UTC`
|
|
26
|
+
: `Branch: ${terminalText(row.head.document.default_branch)}`).join(' · ') || 'No triggers saved';
|
|
27
|
+
}
|
|
28
|
+
const states = new Set(['queued', 'acquiring_source', 'preparing_world', 'configuration_required', 'validating', 'planning', 'running',
|
|
29
|
+
'report_pending', 'attention_required', 'canceling', 'completed', 'failed', 'canceled', 'superseded']);
|
|
30
|
+
const recordedOutcomes = { met: 'exact checks met', unmet: 'exact checks unmet', incomplete: 'incomplete coverage', report_only: 'report only', no_report: 'no available report' };
|
|
31
|
+
function pageNumber(value) {
|
|
32
|
+
if (value !== undefined && !/^[1-9][0-9]{0,8}$/.test(value))
|
|
33
|
+
throw new CliError('page_invalid', 'Use a positive page number.', 2);
|
|
34
|
+
return value ?? '1';
|
|
35
|
+
}
|
|
36
|
+
function label(value) { return { rerun_review: 'Rerun review', check_run: 'Reviewed rerun', action_required: 'Review required' }[value] ?? value.replaceAll('_', ' '); }
|
|
37
|
+
function link(workspace, path) {
|
|
38
|
+
const url = new URL(path, APP_ORIGIN);
|
|
39
|
+
url.searchParams.set('workspace', identifier(workspace));
|
|
40
|
+
return url.href;
|
|
41
|
+
}
|
|
42
|
+
export async function ciCommand(args, options, output) {
|
|
43
|
+
const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
44
|
+
const context = await selectedWorkspace({ workspace: options.workspace, interactive, signal: options.signal, openBrowser: options.openBrowser });
|
|
45
|
+
const prefix = `/api/workspaces/${context.workspace}/ci`;
|
|
46
|
+
const read = (path) => api(path, { actor: context.owner, signal: options.signal });
|
|
47
|
+
const configurationRow = async (id) => {
|
|
48
|
+
const value = await read(`${prefix}/configurations/${identifier(id)}`);
|
|
49
|
+
return ciConfigurationRead(value, ciConfigurationMatches(value, context.workspace, id));
|
|
50
|
+
};
|
|
51
|
+
const configurationRows = async (number, search = '', state = '') => {
|
|
52
|
+
const value = await read(`${prefix}/configurations?${new URLSearchParams({ page: String(number), q: search, state })}`);
|
|
53
|
+
return ciConfigurationRead(value, ciConfigurationsMatch(value, context.workspace, search, state, number));
|
|
54
|
+
};
|
|
55
|
+
const runRow = async (id) => {
|
|
56
|
+
const value = await read(`${prefix}/runs/${identifier(id)}`);
|
|
57
|
+
return ciRunRead(value, ciRunMatches(value, context.workspace, { id }));
|
|
58
|
+
};
|
|
59
|
+
const runRows = async (configuration, number, filters = {}) => {
|
|
60
|
+
const value = await read(`${prefix}/configurations/${configuration}/runs?${new URLSearchParams({ page: String(number),
|
|
61
|
+
sha: filters.sha ?? '', state: filters.state ?? '', trigger: filters.trigger ?? '', world: filters.world ?? '', reference: filters.reference ?? '', outcome: filters.outcome ?? '' })}`);
|
|
62
|
+
return ciRunRead(value, ciRunsMatch(value, context.workspace, configuration, filters, number));
|
|
63
|
+
};
|
|
64
|
+
// Managed CI setup is retired (FIR-504); retained configurations and run history stay readable.
|
|
65
|
+
const action = args[0] ?? 'list';
|
|
66
|
+
const known = ['list', 'show', 'scenarios', 'revisions', 'automatic-history', 'approvals', 'runs', 'run', 'result', 'criteria', 'scores', 'coverage', 'delivery', 'delivery-history', 'configure', 'access', 'inputs', 'open', 'setup-proposal'];
|
|
67
|
+
if (!known.includes(action))
|
|
68
|
+
throw new CliError('arguments_invalid', 'Use ci list, show, scenarios, revisions, automatic-history, approvals, runs, run, result, criteria, scores, coverage, delivery, delivery-history, configure, setup-proposal, access, inputs or open.', 2);
|
|
69
|
+
if (options.search && !['list', 'scenarios', 'criteria', 'scores', 'coverage'].includes(action))
|
|
70
|
+
throw new CliError('arguments_invalid', 'Use --search to find a CI configuration, scenario, coverage path or criterion by name.', 2);
|
|
71
|
+
if (action !== 'runs' && (options.world !== undefined && action !== 'setup-proposal' || [options.trigger, options.reference, options.outcome].some(value => value !== undefined)))
|
|
72
|
+
throw new CliError('arguments_invalid', 'Use --world with ci runs or setup-proposal. Trigger, reference and outcome filters apply to ci runs.', 2);
|
|
73
|
+
if (options.page && !['list', 'scenarios', 'runs', 'revisions', 'automatic-history', 'approvals', 'criteria', 'scores', 'coverage', 'delivery-history'].includes(action))
|
|
74
|
+
throw new CliError('arguments_invalid', 'Use --page with CI directories and history.', 2);
|
|
75
|
+
if (options.status && !['list', 'runs', 'automatic-history', 'approvals', 'criteria', 'coverage', 'delivery-history'].includes(action))
|
|
76
|
+
throw new CliError('arguments_invalid', 'Use --status with CI directories, coverage and history.', 2);
|
|
77
|
+
if (args.length > (['scenarios', 'runs', 'revisions', 'automatic-history', 'approvals', 'delivery', 'delivery-history', 'coverage'].includes(action) ? 3 : action === 'list' ? 1 : 2))
|
|
78
|
+
throw new CliError('arguments_invalid', 'Use one configuration or run ID for this CI action.', 2);
|
|
79
|
+
const page = pageNumber(options.page);
|
|
80
|
+
if (action === 'setup-proposal') {
|
|
81
|
+
// Reviewing a CI setup draft hands off to the retired assistant/CI setup (FIR-504); nothing is read or sent.
|
|
82
|
+
throw new CliError('legacy_launch_retired', 'Reviewing a CI setup draft is retired with managed CI setup; retained CI configurations and run history stay readable. Nothing was opened.', 2);
|
|
83
|
+
}
|
|
84
|
+
async function configuration(value) {
|
|
85
|
+
if (value)
|
|
86
|
+
return configurationRow(value);
|
|
87
|
+
if (!interactive)
|
|
88
|
+
throw new CliError('configuration_required', 'Choose a configuration ID with firedrill ci list.', 2);
|
|
89
|
+
let number = 1;
|
|
90
|
+
while (true) {
|
|
91
|
+
const rows = await configurationRows(number);
|
|
92
|
+
const choices = rows.results.map(row => ({ value: row.id, label: row.name, detail: `${row.repository} · ${configuredPlan(row)} · ${label(row.state)}` }));
|
|
93
|
+
if (number > 1)
|
|
94
|
+
choices.unshift({ value: 'previous', label: 'Previous page', detail: '' });
|
|
95
|
+
if (rows.next)
|
|
96
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
97
|
+
if (!choices.length)
|
|
98
|
+
throw new CliError('configuration_required', 'No CI configurations are saved.', 2);
|
|
99
|
+
const picked = await selectOne('Repository configuration', choices, options.signal);
|
|
100
|
+
if (picked === 'previous') {
|
|
101
|
+
number -= 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (picked === 'next') {
|
|
105
|
+
number += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
return configurationRow(picked);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function run(value) {
|
|
112
|
+
if (value)
|
|
113
|
+
return runRow(value);
|
|
114
|
+
if (!interactive)
|
|
115
|
+
throw new CliError('run_required', 'Choose a CI run ID with firedrill ci runs.', 2);
|
|
116
|
+
const config = await configuration();
|
|
117
|
+
let number = 1;
|
|
118
|
+
while (true) {
|
|
119
|
+
const rows = await runRows(config.id, number);
|
|
120
|
+
const choices = rows.results.map(row => ({ value: row.id, label: `${row.head_sha.slice(0, 12)}${row.pull_number ? ` · PR #${row.pull_number}` : ''}`,
|
|
121
|
+
detail: `${label(row.state)} · ${row.reference}` }));
|
|
122
|
+
if (number > 1)
|
|
123
|
+
choices.unshift({ value: 'previous', label: 'Previous page', detail: '' });
|
|
124
|
+
if (rows.next)
|
|
125
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
126
|
+
if (!choices.length)
|
|
127
|
+
throw new CliError('run_required', 'This configuration has no saved CI runs. Open its history in the browser to review setup.', 2);
|
|
128
|
+
const picked = await selectOne('CI run', choices, options.signal);
|
|
129
|
+
if (picked === 'previous') {
|
|
130
|
+
number -= 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (picked === 'next') {
|
|
134
|
+
number += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
return runRow(picked);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function browser(path) {
|
|
141
|
+
const url = link(context.workspace, path);
|
|
142
|
+
const opened = !options.json && options.openBrowser ? await openBrowser(url) : false;
|
|
143
|
+
output('browser_handoff', { url, opened, workspace_id: context.workspace, local_execution_started: false }, `${opened ? 'Opened CI in your browser.' : 'Open CI in your browser:'}\n${url}`);
|
|
144
|
+
}
|
|
145
|
+
if (action === 'list') {
|
|
146
|
+
if (options.status && !['draft', 'active', 'disconnected'].includes(options.status))
|
|
147
|
+
throw new CliError('status_invalid', 'Choose draft, active or disconnected.', 2);
|
|
148
|
+
if ((options.search?.length ?? 0) > 200)
|
|
149
|
+
throw new CliError('search_invalid', 'Use a configuration name prefix of 200 characters or fewer.', 2);
|
|
150
|
+
const rows = await configurationRows(Number(page), options.search ?? '', options.status ?? '');
|
|
151
|
+
output('ci_configurations', rows, rows.results.map(row => `${row.id} ${terminalText(row.name)} ${terminalText(row.repository)} ${label(row.state)}\n${configuredPlan(row)}\n${configuredTriggers(row)}`).join('\n\n') || 'No CI configurations match.');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (action === 'configure') {
|
|
155
|
+
throw new CliError('legacy_launch_retired', 'Managed CI setup is retired; retained CI configurations and run history stay readable. Nothing was opened.', 2);
|
|
156
|
+
}
|
|
157
|
+
if (action === 'scenarios') {
|
|
158
|
+
const row = await configuration(args[1]), revision = args[2] ?? String(row.revision);
|
|
159
|
+
if (!/^[1-9][0-9]{0,18}$/.test(revision) || BigInt(revision) >= 2n ** 63n || (options.search?.length ?? 0) > 200)
|
|
160
|
+
throw new CliError('selection_invalid', 'Choose a positive configuration revision and a scenario name prefix of 200 characters or fewer.', 2);
|
|
161
|
+
const value = await read(`${prefix}/configurations/${row.id}/scenarios?${new URLSearchParams({ revision, page, q: options.search ?? '' })}`);
|
|
162
|
+
ciConfigurationRead(value, ciScenariosMatch(value, context.workspace, row.id, revision, options.search ?? '', Number(page)));
|
|
163
|
+
output('ci_configured_scenarios', value, [`Configuration ${value.configuration_revision} · ${terminalText(value.world_name)} · Saved plan ${value.plan_revision}`,
|
|
164
|
+
...value.results.map(one => one.available ? `${terminalText(one.name ?? '')} · Scenario revision ${one.revision}\n${one.detail_path ? link(context.workspace, one.detail_path) : ''}` : `Saved selection ${one.position + 1}: Scenario unavailable`),
|
|
165
|
+
...(!value.count ? ['No saved scenarios match this search.'] : []), `Page ${value.page}; ${value.count.toLocaleString()} scenarios.`,
|
|
166
|
+
...(value.next ? [`Continue with --page ${value.page + 1} and the same revision/search.`] : [])].join('\n\n'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (['revisions', 'automatic-history', 'approvals'].includes(action)) {
|
|
170
|
+
const row = await configuration(args[1]), revision = args[2] ?? '';
|
|
171
|
+
if (revision && (!/^[1-9][0-9]{0,18}$/.test(revision) || BigInt(revision) >= 2n ** 63n))
|
|
172
|
+
throw new CliError('revision_invalid', 'Use a positive configuration revision after its ID.', 2);
|
|
173
|
+
if (action === 'revisions') {
|
|
174
|
+
const rows = await read(`${prefix}/configurations/${row.id}/revisions?${new URLSearchParams({ page, revision })}`);
|
|
175
|
+
ciConfigurationRead(rows, ciRevisionPageMatches(rows, context.workspace, row.id, revision, '', Number(page)));
|
|
176
|
+
output('ci_configuration_revisions', rows, rows.results.map(one => `Revision ${one.revision} ${terminalText(one.document.name)} ${label(one.document.mode)} ${one.document.baseline_sha.slice(0, 12)} ${one.created_at}\nPR selection: ${one.document.scenario_policy.mode === 'change_relevant' ? `change-relevant, ${one.document.scenario_policy.mandatory_scenario_ids.length} additional mandatory cases` : 'all planned scenarios'}`).join('\n') || 'No configuration revisions match.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (action === 'automatic-history') {
|
|
180
|
+
if (options.status && !['enabled', 'paused'].includes(options.status))
|
|
181
|
+
throw new CliError('status_invalid', 'Choose enabled or paused.', 2);
|
|
182
|
+
const rows = await read(`${prefix}/configurations/${row.id}/activations?${new URLSearchParams({ page, revision, state: options.status ?? '' })}`);
|
|
183
|
+
ciAccessRead(rows, ciActivationsMatch(rows, context.workspace, row.id, revision, options.status ?? '', Number(page)));
|
|
184
|
+
output('ci_automatic_history', rows, rows.results.map(one => `${one.id} Revision ${one.revision} ${label(one.state)} ${one.created_at}`).join('\n') || 'No automatic-run settings match.');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (options.status && !['recorded', 'revoked'].includes(options.status))
|
|
188
|
+
throw new CliError('status_invalid', 'Choose recorded or revoked.', 2);
|
|
189
|
+
const rows = await read(`${prefix}/configurations/${row.id}/input-approvals?${new URLSearchParams({ page, revision, state: options.status ?? '' })}`);
|
|
190
|
+
ciAccessRead(rows, ciApprovalsMatch(rows, context.workspace, row.id, { revision, state: options.status ?? '' }, Number(page)));
|
|
191
|
+
output('ci_approval_history', rows, rows.results.map(one => `${one.id} Revision ${one.revision} ${label(one.scope)}${one.head_sha ? ` ${one.head_sha.slice(0, 12)}` : ''} ${label(one.state)} ${one.created_at}`).join('\n') || 'No input approvals match.');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (action === 'show' || action === 'access' || action === 'runs') {
|
|
195
|
+
const row = await configuration(args[1]);
|
|
196
|
+
if (action === 'access') {
|
|
197
|
+
await browser(`/ci/configurations/${row.id}/access`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (action === 'show') {
|
|
201
|
+
output('ci_configuration', { ...row, url: link(context.workspace, `/ci/configurations/${row.id}`) }, `${terminalText(row.name)}\n${terminalText(row.repository)}\n${configuredPlan(row)}\n${configuredTriggers(row)}\n${scenarioPolicy(row)}\n${label(row.state)} · revision ${row.revision}\n${link(context.workspace, `/ci/configurations/${row.id}`)}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (options.status && !states.has(options.status))
|
|
205
|
+
throw new CliError('status_invalid', 'Choose a retained CI run state.', 2);
|
|
206
|
+
if (args[2] && !/^[0-9a-f]{40}$/i.test(args[2]))
|
|
207
|
+
throw new CliError('commit_invalid', 'Use the full commit SHA after the configuration ID.', 2);
|
|
208
|
+
if (options.trigger !== undefined && !['pull_request', 'push', 'scheduled', 'check_run'].includes(options.trigger))
|
|
209
|
+
throw new CliError('trigger_invalid', 'Choose pull_request, push, scheduled or check_run (reviewed reruns).', 2);
|
|
210
|
+
if (options.outcome !== undefined && !Object.hasOwn(recordedOutcomes, options.outcome))
|
|
211
|
+
throw new CliError('outcome_invalid', 'Choose met, unmet, incomplete, report_only or no_report.', 2);
|
|
212
|
+
if (options.reference !== undefined && (options.reference.length > 1024 || /[\u0000-\u001f\u007f]/.test(options.reference)))
|
|
213
|
+
throw new CliError('reference_invalid', 'Use a case-sensitive Git reference prefix of 1024 characters or fewer.', 2);
|
|
214
|
+
const rows = await runRows(row.id, Number(page), { state: options.status ?? '', sha: args[2]?.toLowerCase() ?? '',
|
|
215
|
+
world: options.world ? identifier(options.world) : '', trigger: options.trigger ?? '', reference: options.reference ?? '', outcome: options.outcome ?? '',
|
|
216
|
+
});
|
|
217
|
+
output('ci_runs', rows, rows.results.map(one => `${one.id} ${one.head_sha.slice(0, 12)} ${one.pull_number ? `PR #${one.pull_number}` : terminalText(one.reference)} ${label(one.state)} ${one.recorded_report ? `Recorded: ${recordedOutcomes[one.recorded_report.status] ?? 'unavailable'} at ${one.recorded_report.observed_at}${one.recorded_report.complete ? '' : ' (coverage incomplete)'}` : 'No available report'}`).join('\n') || 'No CI runs match. CI runs on GitHub events independently of your terminal.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const selected = await run(args[1]);
|
|
221
|
+
if (action === 'coverage') {
|
|
222
|
+
await ciCoverage(selected.id, args[2], context, options, output);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (action === 'inputs' || action === 'open') {
|
|
226
|
+
await browser(`/ci/runs/${selected.id}${action === 'inputs' ? '/inputs' : ''}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (action === 'run') {
|
|
230
|
+
output('ci_run', { ...selected, url: link(context.workspace, `/ci/runs/${selected.id}`) }, `${selected.head_sha}\n${label(selected.state)} · configuration ${selected.configuration_revision}${selected.trigger === 'check_run' ? '\nReviewed rerun. Execution permissions are specific to this run.' : ''}\n${link(context.workspace, `/ci/runs/${selected.id}`)}`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (action === 'result') {
|
|
234
|
+
const value = await read(`${prefix}/runs/${selected.id}/result`);
|
|
235
|
+
ciResultRead(value, ciResultMatches(value, context.workspace, selected.id, { sha: selected.head_sha, campaign: selected.campaign_id }));
|
|
236
|
+
const summary = value.result;
|
|
237
|
+
output('ci_result', value, summary ? `${summary.scenarios} scenarios · ${summary.planned_trials} planned drills\nCompleted: ${summary.trials.completed}. Agent failures: ${summary.trials.failed}. Platform failures: ${summary.trials.platform_failed}. Timed out: ${summary.trials.timed_out}. Unfinished: ${summary.trials.unfinished}.\n${label(summary.decision.status)}. ${value.published ? `GitHub result: ${value.ci_verdict}` : 'GitHub delivery is not confirmed for this result.'}`
|
|
238
|
+
: `Report: ${label(value.state)}. No result is implied yet.`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (action === 'scores') {
|
|
242
|
+
if ((options.search?.length ?? 0) > 200 || /[\u0000-\u001f\u007f]/.test(options.search ?? ''))
|
|
243
|
+
throw new CliError('search_invalid', 'Use a criterion name prefix of 200 characters or fewer.', 2);
|
|
244
|
+
const value = await read(`${prefix}/runs/${selected.id}/result/scores?${new URLSearchParams({ page, q: options.search ?? '' })}`);
|
|
245
|
+
ciResultRead(value, ciScoresMatch(value, context.workspace, selected.id, options.search ?? '', Number(page)));
|
|
246
|
+
const score = (number) => number === null ? 'not assessed' : number.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
247
|
+
output('ci_scores', value, value.state !== 'ready' || value.count === null ? `Scores: ${label(value.state)}. Refresh the original result before inspecting scores.`
|
|
248
|
+
: value.results.map(one => `${terminalText(one.cohort.scenario_name)} · ${terminalText(one.cohort.criterion)}\nMean ${score(one.summary.mean)}${one.summary.mean === null ? '' : ` / ${one.cohort.levels.length - 1}`} (${one.cohort.direction === 'lower' ? 'lower' : 'higher'} is better). ${one.summary.assessed} assessed; ${one.summary.not_assessable} not assessable; ${one.summary.not_applicable} not applicable.\nEvaluator: ${terminalText(one.cohort.model)}`).join('\n\n') || 'No retained CI score groups match. Missing evaluations remain in report coverage.');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (action === 'criteria') {
|
|
252
|
+
if ((options.search?.length ?? 0) > 200 || /[\u0000-\u001f\u007f]/.test(options.search ?? ''))
|
|
253
|
+
throw new CliError('search_invalid', 'Use a criterion name prefix of 200 characters or fewer.', 2);
|
|
254
|
+
if (options.status && !['complete', 'incomplete', 'not_assessable'].includes(options.status))
|
|
255
|
+
throw new CliError('status_invalid', 'Choose complete, incomplete or not_assessable coverage.', 2);
|
|
256
|
+
const value = await read(`${prefix}/runs/${selected.id}/result/criteria?${new URLSearchParams({ page, q: options.search ?? '', status: options.status ?? '' })}`);
|
|
257
|
+
ciResultRead(value, ciCriteriaMatches(value, context.workspace, selected.id, options.search ?? '', options.status ?? '', Number(page)));
|
|
258
|
+
output('ci_criteria', value, value.state !== 'ready' || value.count === null ? `Evaluation coverage: ${label(value.state)}. Refresh the original result before inspecting coverage.`
|
|
259
|
+
: value.results.map(one => `${terminalText(one.name ?? 'Unavailable criterion')} · ${one.counts.expected === 0 ? 'no applicable drills' : label(one.coverage_state)}\n${one.counts.assessed} of ${one.counts.expected} expected assessments scored; ${one.counts.not_applicable} not applicable; ${one.counts.not_assessable} not assessable; ${one.counts.unavailable} unavailable.${one.definition_url ? `\n${link(context.workspace, one.definition_url)}` : '\nSaved definition unavailable.'}`).join('\n\n') || 'No CI evaluation criteria match.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const channel = args[2] ?? 'check';
|
|
263
|
+
if (!['check', 'comment'].includes(channel))
|
|
264
|
+
throw new CliError('channel_invalid', 'Choose check or comment.', 2);
|
|
265
|
+
const path = `${prefix}/runs/${selected.id}/${channel}`;
|
|
266
|
+
if (action === 'delivery') {
|
|
267
|
+
const value = await read(path);
|
|
268
|
+
ciDeliveryRead(value, ciDeliveryMatches(value, context.workspace, selected.id, channel));
|
|
269
|
+
output('ci_delivery', value, `${channel}: ${label(value.state)}${value.head?.purpose === 'rerun_review' && value.head.rerun_event_id ? `\nRerun requested. Review before starting: ${link(context.workspace, `/ci/configurations/${selected.configuration_id}?${new URLSearchParams({ events: 'check_run', event: value.head.rerun_event_id })}`)}` : ''}\n${link(context.workspace, `/ci/runs/${selected.id}`)}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (options.status && !ciDeliveryStates.includes(options.status))
|
|
273
|
+
throw new CliError('status_invalid', 'Choose a retained GitHub delivery state.', 2);
|
|
274
|
+
const rows = await read(`${path}/history?${new URLSearchParams({ page, status: options.status ?? '' })}`);
|
|
275
|
+
ciDeliveryRead(rows, ciDeliveriesMatch(rows, context.workspace, selected.id, channel, options.status ?? '', Number(page)));
|
|
276
|
+
output('ci_delivery_history', rows, rows.results.map(row => `${row.intent_id} ${label(row.purpose)} ${label(row.state)} ${row.created_at}`).join('\n') || 'No delivery records match.');
|
|
277
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { unlink } from 'node:fs/promises';
|
|
4
|
+
import { api } from './auth.js';
|
|
5
|
+
import { privateDirectory } from './credentials.js';
|
|
6
|
+
import { CliError } from './http.js';
|
|
7
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
8
|
+
import { selectedWorld } from './world-access.js';
|
|
9
|
+
import { identifier } from './world-contracts.js';
|
|
10
|
+
import { ownedSourceMetadataFile, readSourceMetadata, writeSourceMetadata } from './source-connection.js';
|
|
11
|
+
import { withBootstrapProcessLock } from './bootstrap-process-lock.js';
|
|
12
|
+
import { worldSessionMatches } from '../../contracts/session-contracts.js';
|
|
13
|
+
import { resourcePageMatches } from '../../contracts/world-lifetime-contracts.js';
|
|
14
|
+
import { coordinatedCheckpointMatches, coordinatedCheckpointDetailMatches } from '../../contracts/coordinated-checkpoint-contracts.js';
|
|
15
|
+
export const COORDINATED_ACTIONS = ['coordinated-checkpoints', 'coordinated-checkpoint', 'save-world-and-agent'];
|
|
16
|
+
const problem = () => new CliError('coordinated_checkpoint_unconfirmed', 'Inspect the original world and agent capture. No replacement capture was submitted.');
|
|
17
|
+
const label = (value) => `${terminalText(value.name)} · ${value.available ? 'Saved' : value.state}\n`
|
|
18
|
+
+ `${value.application_component_count} application components; ${value.excluded_count} excluded. `
|
|
19
|
+
+ (value.consistency === 'not_observed' ? 'Capture not observed.' : value.consistency === 'application_consistent'
|
|
20
|
+
? 'Source-attested application consistency with supported native data.' : 'Capture includes crash-consistent components.')
|
|
21
|
+
+ (value.problem ? '\n' + terminalText(value.problem) : '');
|
|
22
|
+
export async function coordinatedCheckpointsCommand(args, options, output) {
|
|
23
|
+
const action = args[0], writing = action === 'save-world-and-agent';
|
|
24
|
+
if (!COORDINATED_ACTIONS.includes(action) || (action === 'coordinated-checkpoints' ? args.length > 2 : args.length !== 2)
|
|
25
|
+
|| options.page !== undefined && (!/^[1-9][0-9]{0,8}$/.test(options.page) || action !== 'coordinated-checkpoints')
|
|
26
|
+
|| options.search !== undefined && (action !== 'coordinated-checkpoints' || options.search.length > 200)
|
|
27
|
+
|| !writing && (options.name !== undefined || options.run !== undefined || options.request !== undefined))
|
|
28
|
+
throw new CliError('arguments_invalid', 'Use sessions coordinated-checkpoints [session], coordinated-checkpoint <id>, or save-world-and-agent <session> --name <name>.', 2);
|
|
29
|
+
const tty = !options.json && Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
30
|
+
const scope = await selectedWorld({ ...options, interactive: tty });
|
|
31
|
+
const auth = (signal = options.signal) => ({ actor: scope.owner, workspace: scope.workspace, signal });
|
|
32
|
+
const read = async (key, session, input, signal = options.signal) => {
|
|
33
|
+
const value = await api(`${scope.prefix}/coordinated-checkpoints/${identifier(key)}`, auth(signal));
|
|
34
|
+
if (!coordinatedCheckpointDetailMatches(value, scope.world, { ...(session ? { session } : {}), ...(input ? { key: input.key, name: input.input.name } : {}) })
|
|
35
|
+
|| !input && value.id !== key && value.request_key !== key)
|
|
36
|
+
throw problem();
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
const show = (value) => output('coordinated_checkpoint', value, label(value)
|
|
40
|
+
+ `\n${value.record_count === null ? 'World data is unavailable.' : `${value.record_count} saved records in ${value.collection_count} collections.`}`
|
|
41
|
+
+ (value.excluded.length ? `\nNot included: ${value.excluded.map(terminalText).join(', ')}.` : '')
|
|
42
|
+
+ '\nNew-process continuation only. External accounts and elapsed wall time are not restored.');
|
|
43
|
+
if (action === 'coordinated-checkpoint') {
|
|
44
|
+
show(await read(identifier(args[1])));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (action === 'coordinated-checkpoints') {
|
|
48
|
+
const page = Number(options.page ?? 1), session = args[1] ? identifier(args[1]) : undefined;
|
|
49
|
+
const base = session ? `${scope.prefix}/runtime-sessions/${session}` : scope.prefix;
|
|
50
|
+
const value = await api(`${base}/coordinated-checkpoints?page=${page}&q=${encodeURIComponent(options.search ?? '')}`, auth());
|
|
51
|
+
if (!resourcePageMatches(value, page, row => coordinatedCheckpointMatches(row, scope.world, session ? { session } : {})))
|
|
52
|
+
throw problem();
|
|
53
|
+
output('coordinated_checkpoints', value, (value.results.map(label).join('\n\n') || 'No saved world and agent checkpoints.')
|
|
54
|
+
+ `\nPage ${value.page}; ${value.count} matching captures.${value.next ? ` Continue with --page ${value.page + 1}.` : ''}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const sessionId = identifier(args[1]);
|
|
58
|
+
const path = join(await privateDirectory(), `coordinated-capture-${scope.owner}-${scope.workspace}-${scope.world}-${sessionId}.json`);
|
|
59
|
+
await withBootstrapProcessLock(path + '.lock', options.signal, async (signal) => {
|
|
60
|
+
let intent = null;
|
|
61
|
+
try {
|
|
62
|
+
const file = await ownedSourceMetadataFile(path, 4096);
|
|
63
|
+
try {
|
|
64
|
+
intent = await readSourceMetadata(file, 4096);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await file.close();
|
|
68
|
+
}
|
|
69
|
+
if (!intent || intent.schema !== 'firedrill.coordinated-capture-intent.v1' || intent.owner !== scope.owner || intent.workspace !== scope.workspace
|
|
70
|
+
|| intent.world !== scope.world || intent.session !== sessionId || Object.keys(intent).sort().join(',') !== 'input,key,owner,schema,session,workspace,world'
|
|
71
|
+
|| identifier(intent.key) !== intent.key || !intent.input || Object.keys(intent.input).sort().join(',') !== 'capture_consent,expected_reset_count,name'
|
|
72
|
+
|| intent.input.capture_consent !== true || typeof intent.input.name !== 'string' || intent.input.name.length < 1 || intent.input.name.length > 120
|
|
73
|
+
|| !/^(0|[1-9][0-9]{0,18})$/.test(intent.input.expected_reset_count))
|
|
74
|
+
throw problem();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code !== 'ENOENT')
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (intent && (options.request !== undefined && identifier(options.request) !== intent.key
|
|
81
|
+
|| options.name !== undefined && options.name.trim() !== intent.input.name))
|
|
82
|
+
throw problem();
|
|
83
|
+
const key = intent?.key ?? identifier(options.request ?? randomUUID());
|
|
84
|
+
try {
|
|
85
|
+
const prior = await read(key, sessionId, intent ?? undefined, signal);
|
|
86
|
+
show(prior);
|
|
87
|
+
if (intent && prior.state !== 'pending')
|
|
88
|
+
await unlink(path);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!(error instanceof CliError) || error.httpStatus !== 404)
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const row = await api(`${scope.prefix}/runtime-sessions/${sessionId}`, auth(signal));
|
|
96
|
+
if (!worldSessionMatches(row, { actor: scope.owner, workspace: scope.workspace, world: scope.world }, { id: sessionId })
|
|
97
|
+
|| !row.coordinated_checkpoint.available || !row.observation_current || !row.can_control || row.reset_pending || row.external_effects || row.terminal_at
|
|
98
|
+
|| intent && row.reset_count !== intent.input.expected_reset_count)
|
|
99
|
+
throw new CliError('coordinated_checkpoint_not_ready', 'The original capture needs the same current synthetic session and reset state. Its saved status remains available; no capture was submitted.', 2);
|
|
100
|
+
if (!intent) {
|
|
101
|
+
const name = options.name?.trim();
|
|
102
|
+
if (!name || name.length > 120 || /[\u0000-\u001f\u007f]/.test(name))
|
|
103
|
+
throw new CliError('arguments_invalid', 'Use --name with a plain checkpoint name up to 120 characters.', 2);
|
|
104
|
+
intent = { schema: 'firedrill.coordinated-capture-intent.v1', owner: scope.owner, workspace: scope.workspace, world: scope.world,
|
|
105
|
+
session: sessionId, key, input: { name, expected_reset_count: row.reset_count, capture_consent: true } };
|
|
106
|
+
}
|
|
107
|
+
if (!options.run && !(tty && await selectOne('Capture supported world and application state?', [
|
|
108
|
+
{ value: false, label: 'Cancel' }, { value: true, label: 'Capture state', detail: 'Uses the source-declared quiescence API and supported native stores. Open tool views close. External accounts, live processes and elapsed wall time are not captured.' }
|
|
109
|
+
], signal)))
|
|
110
|
+
throw new CliError('coordinated_checkpoint_consent_required', 'Use --run only after reviewing this exact capture.', 2);
|
|
111
|
+
await writeSourceMetadata(path, intent);
|
|
112
|
+
output('coordinated_checkpoint_intent', { request_key: intent.key, session_id: sessionId }, `Request: ${intent.key}\nRecovery: firedrill sessions coordinated-checkpoint ${intent.key} --world ${scope.world} --workspace ${scope.workspace}. A missing receipt does not authorize a replacement capture.`);
|
|
113
|
+
let accepted;
|
|
114
|
+
try {
|
|
115
|
+
accepted = await api(`${scope.prefix}/runtime-sessions/${sessionId}/coordinated-checkpoints`, {
|
|
116
|
+
...auth(signal), method: 'POST', idempotencyKey: intent.key, body: intent.input
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error instanceof CliError && error.httpStatus === 400 && error.code === 'coordinated_checkpoint_input_invalid')
|
|
121
|
+
await unlink(path);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
if (!coordinatedCheckpointDetailMatches(accepted, scope.world, { session: sessionId, key: intent.key, name: intent.input.name }))
|
|
125
|
+
throw problem();
|
|
126
|
+
const observed = await read(intent.key, sessionId, intent, signal);
|
|
127
|
+
if (observed.id !== accepted.id)
|
|
128
|
+
throw problem();
|
|
129
|
+
show(observed);
|
|
130
|
+
if (observed.state !== 'pending')
|
|
131
|
+
await unlink(path);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential store availability and per-process credential mode selection
|
|
3
|
+
* (Plan20 section 3.6, W09/W13C).
|
|
4
|
+
*
|
|
5
|
+
* Platform record (source behavior, runtime unverified):
|
|
6
|
+
*
|
|
7
|
+
* | Platform | Persistent store | Typical unavailability | Session-only companion |
|
|
8
|
+
* | --- | --- | --- | --- |
|
|
9
|
+
* | macOS | Keychain (login keychain) | locked keychain, SSH session without unlock | supported |
|
|
10
|
+
* | Windows | Credential Manager | service account, non-interactive/restricted logon | supported (named pipe) |
|
|
11
|
+
* | Desktop Linux | Secret Service (GNOME Keyring, KWallet, KeePassXC) | locked collection, no provider | supported |
|
|
12
|
+
* | Headless/remote Linux | usually no Secret Service on a D-Bus session bus | common | supported, recommended |
|
|
13
|
+
* | Other / no native binding | none | always | supported |
|
|
14
|
+
*
|
|
15
|
+
* - No store never implies plaintext fallback, and headless Linux without a
|
|
16
|
+
* keyring is not "unsupported": the session-only companion is the supported path.
|
|
17
|
+
* - Refresh: refresh_pending is recorded (OS store or memory) before the token
|
|
18
|
+
* exchange; an unconfirmed refresh is never retried blindly, in either mode.
|
|
19
|
+
* - Concurrent login: persistent mode serializes through the OS-owned auth lock;
|
|
20
|
+
* session mode serializes in the companion process, and only one companion
|
|
21
|
+
* may serve a platform origin per user (companion.lock).
|
|
22
|
+
* - Cancellation: an interrupted device poll leaves the attempt marked uncertain
|
|
23
|
+
* (persistent) or discards it with the process (session); the single-use
|
|
24
|
+
* device code is never exchanged twice.
|
|
25
|
+
* - Private files: the state directory is 0700 and owned by the user; the
|
|
26
|
+
* companion nonce is 0600 (exclusive create) and removed on shutdown; the
|
|
27
|
+
* socket is 0600 inside a 0700 directory. On Windows these rely on the
|
|
28
|
+
* user-profile ACL. No credential is ever written to a file.
|
|
29
|
+
*/
|
|
30
|
+
import { CliError } from './http.js';
|
|
31
|
+
import { probeCredentialStore, setCredentialMode } from './credentials.js';
|
|
32
|
+
import { companionPresent, companionRequest } from './native/companion-ipc.js';
|
|
33
|
+
const SESSION_ONLY = 'Use `firedrill init --session-only` to keep the sign-in only in memory for one companion process';
|
|
34
|
+
const OR_SESSION_ONLY = 'Or use `firedrill init --session-only` to keep the sign-in only in memory for one companion process.';
|
|
35
|
+
const NO_FILE = 'Credentials are never saved to a plaintext file.';
|
|
36
|
+
function platformBackend() {
|
|
37
|
+
if (process.platform === 'darwin')
|
|
38
|
+
return 'macos-keychain';
|
|
39
|
+
if (process.platform === 'win32')
|
|
40
|
+
return 'windows-credential-manager';
|
|
41
|
+
if (process.platform === 'linux')
|
|
42
|
+
return 'linux-secret-service';
|
|
43
|
+
return 'none';
|
|
44
|
+
}
|
|
45
|
+
/** Linux session without a desktop or session bus: SSH, container, CI or server. */
|
|
46
|
+
export function headlessLinuxSession(env = process.env) {
|
|
47
|
+
if (process.platform !== 'linux')
|
|
48
|
+
return false;
|
|
49
|
+
const desktop = !!(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
50
|
+
const bus = !!env.DBUS_SESSION_BUS_ADDRESS;
|
|
51
|
+
return !bus && !desktop || !desktop && !!(env.SSH_CONNECTION || env.SSH_TTY);
|
|
52
|
+
}
|
|
53
|
+
function remediation(backend, reason, headless) {
|
|
54
|
+
if (reason === 'unsupported_platform' || reason === 'binding_unavailable')
|
|
55
|
+
return `No supported OS credential store is available for this platform build. ${SESSION_ONLY}. ${NO_FILE}`;
|
|
56
|
+
if (reason === 'ambiguous_entry')
|
|
57
|
+
return `More than one OS credential store item matches Firedrill's sign-in entry. Remove the duplicate "run.firedrill.simulation" items in your credential store, then sign in again. ${OR_SESSION_ONLY} ${NO_FILE}`;
|
|
58
|
+
if (backend === 'macos-keychain')
|
|
59
|
+
return reason === 'store_locked'
|
|
60
|
+
? `Unlock the macOS login keychain (Keychain Access, or \`security unlock-keychain\` over SSH) and allow Firedrill access when asked, then sign in again. ${OR_SESSION_ONLY} ${NO_FILE}`
|
|
61
|
+
: `The macOS Keychain could not be used from this session. Sign in from a logged-in user session or unlock it with \`security unlock-keychain\`. ${OR_SESSION_ONLY} ${NO_FILE}`;
|
|
62
|
+
if (backend === 'windows-credential-manager')
|
|
63
|
+
return `Windows Credential Manager is unavailable in this session (for example a service account, a restricted or non-interactive logon, or a stopped Credential Manager service). Sign in from an interactive Windows user session. ${OR_SESSION_ONLY} ${NO_FILE}`;
|
|
64
|
+
if (headless)
|
|
65
|
+
return `No Secret Service is reachable in this headless or remote Linux session. ${SESSION_ONLY}, or start and unlock a Secret Service provider (for example gnome-keyring-daemon or KeePassXC with Secret Service integration) on a D-Bus session bus. ${NO_FILE}`;
|
|
66
|
+
return reason === 'store_locked'
|
|
67
|
+
? `Unlock your login keyring (Secret Service), then sign in again. ${OR_SESSION_ONLY} ${NO_FILE}`
|
|
68
|
+
: `No Secret Service provider answered on this desktop session. Install or enable one (GNOME Keyring, KWallet with Secret Service, or KeePassXC), then sign in again. ${OR_SESSION_ONLY} ${NO_FILE}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Probe the persistent store without saving any secret. verifyWrite also stores
|
|
72
|
+
* and deletes a fixed non-secret marker (use before login so a store that cannot
|
|
73
|
+
* be written is reported before the browser ceremony starts).
|
|
74
|
+
*/
|
|
75
|
+
export async function credentialStoreStatus(options = {}) {
|
|
76
|
+
const backend = platformBackend(), headless = headlessLinuxSession();
|
|
77
|
+
if (backend === 'none')
|
|
78
|
+
return { available: false, backend, reason: 'unsupported_platform', headless, remediation: remediation(backend, 'unsupported_platform', headless) };
|
|
79
|
+
const probe = await probeCredentialStore({ write: options.verifyWrite === true, signal: options.signal });
|
|
80
|
+
if (probe.ok)
|
|
81
|
+
return { available: true, backend, remediation: null, reason: null, headless };
|
|
82
|
+
const effective = probe.failure === 'binding_unavailable' ? 'none' : backend;
|
|
83
|
+
return { available: false, backend: effective, reason: probe.failure, headless, remediation: remediation(effective, probe.failure, headless) };
|
|
84
|
+
}
|
|
85
|
+
async function runningCompanion(signal) {
|
|
86
|
+
if (!await companionPresent())
|
|
87
|
+
return null;
|
|
88
|
+
try {
|
|
89
|
+
return await companionRequest('status', signal);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// A crashed, suspended, unverifiable or unsafe companion is not usable for
|
|
93
|
+
// mode selection. Treating it as absent keeps persistent-mode commands
|
|
94
|
+
// working; any later companion request still refuses with its own code.
|
|
95
|
+
if (error instanceof CliError && error.code.startsWith('companion_'))
|
|
96
|
+
return null;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve and set this process's credential mode, once, before any credential access.
|
|
102
|
+
* Precedence: --session-only / --persistent flags, then FIREDRILL_CREDENTIAL_MODE,
|
|
103
|
+
* then a running session-only companion, else persistent.
|
|
104
|
+
* companion: 'require' (this process becomes the companion), 'if-session' (init: require when the
|
|
105
|
+
* resolved mode is session-only),
|
|
106
|
+
* 'if-absent' (run: become the companion unless one already serves), 'never' (all other commands).
|
|
107
|
+
*/
|
|
108
|
+
export async function configureCredentialMode(options = {}) {
|
|
109
|
+
if (options.sessionOnly && options.persistent)
|
|
110
|
+
throw new CliError('credential_mode_conflict', 'Choose either --session-only or --persistent. No sign-in was started.', 2);
|
|
111
|
+
const env = process.env.FIREDRILL_CREDENTIAL_MODE;
|
|
112
|
+
if (env !== undefined && env !== 'session' && env !== 'persistent')
|
|
113
|
+
throw new CliError('credential_mode_invalid', 'Set FIREDRILL_CREDENTIAL_MODE to session or persistent, or unset it. No credential mode was selected.', 2);
|
|
114
|
+
const fromEnv = env;
|
|
115
|
+
const explicit = options.sessionOnly ? 'session' : options.persistent ? 'persistent' : fromEnv ?? null;
|
|
116
|
+
const requested = options.companion ?? 'never';
|
|
117
|
+
// An explicit persistent selection for a non-companion command needs nothing
|
|
118
|
+
// from a running companion, so it never waits on one.
|
|
119
|
+
const running = explicit === 'persistent' && requested === 'never' ? null : await runningCompanion(options.signal);
|
|
120
|
+
const mode = explicit ?? (running?.mode === 'session' ? 'session' : 'persistent');
|
|
121
|
+
// 'if-session' (init): become the companion exactly when the resolved mode is
|
|
122
|
+
// session-only, whether it came from --session-only or FIREDRILL_CREDENTIAL_MODE.
|
|
123
|
+
const role = requested === 'if-session' ? (mode === 'session' ? 'require' : 'never') : requested;
|
|
124
|
+
let companion = false;
|
|
125
|
+
if (role === 'require') {
|
|
126
|
+
if (running)
|
|
127
|
+
throw new CliError('companion_already_running', running.mode === 'session'
|
|
128
|
+
? 'A session-only companion is already signed in for this platform. Use it from other terminals, or stop it with `firedrill logout` first. No second sign-in was started.'
|
|
129
|
+
: 'A Firedrill companion is already running for this platform. Stop it before starting another. No second sign-in was started.', 2);
|
|
130
|
+
companion = true;
|
|
131
|
+
}
|
|
132
|
+
else if (role === 'if-absent') {
|
|
133
|
+
if (running && running.mode !== mode)
|
|
134
|
+
throw new CliError('companion_mode_conflict', `A ${running.mode === 'session' ? 'session-only' : 'persistent'} companion is already running. Stop it or use the same credential mode. No work was started.`, 2);
|
|
135
|
+
companion = !running;
|
|
136
|
+
}
|
|
137
|
+
setCredentialMode(mode, { companion });
|
|
138
|
+
return { mode, companion, running };
|
|
139
|
+
}
|