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,227 @@
|
|
|
1
|
+
import { api, openBrowser } from './auth.js';
|
|
2
|
+
import { APP_ORIGIN, CliError } from './http.js';
|
|
3
|
+
import { selectedWorkspace } from './world-access.js';
|
|
4
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
5
|
+
import { creditAmount as credits } from './credit-format.js';
|
|
6
|
+
const actions = new Set(['credits', 'credit-help', 'usage', 'credit-ledger', 'credit-allocations', 'credit-work', 'credit-purchases', 'credit-purchase', 'recharge', 'credit-settings']);
|
|
7
|
+
const exact = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
8
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
const invalid = () => new CliError('billing_response_unconfirmed', 'The credit response could not be confirmed. Reopen Billing or retry this read.');
|
|
10
|
+
const uid = (value) => typeof value === 'string' && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value);
|
|
11
|
+
const boundedText = (value, limit) => typeof value === 'string' && value.length <= limit && !/[\u0000-\u001f\u007f]/.test(value);
|
|
12
|
+
const retainedCreditUnit = (row) => typeof row.denomination === 'string'
|
|
13
|
+
&& /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,31}$/.test(row.denomination) && exact(row.credit_scale)
|
|
14
|
+
&& row.credit_scale >= 1 && /^10*$/.test(String(row.credit_scale));
|
|
15
|
+
const retainedCreditAmount = (row) => retainedCreditUnit(row)
|
|
16
|
+
? `${credits(row.units, row.credit_scale)} credits` : 'Credit amount unavailable';
|
|
17
|
+
function creditGuideUrl(scope) {
|
|
18
|
+
if (!uid(scope.workspace))
|
|
19
|
+
throw invalid();
|
|
20
|
+
const url = new URL('/settings/billing/credits', APP_ORIGIN);
|
|
21
|
+
url.searchParams.set('workspace', scope.workspace);
|
|
22
|
+
return url.href;
|
|
23
|
+
}
|
|
24
|
+
async function showCreditGuide(scope, options, output, open = false) {
|
|
25
|
+
options.signal.throwIfAborted();
|
|
26
|
+
const help_url = creditGuideUrl(scope);
|
|
27
|
+
const opened = open && options.openBrowser && !options.json ? await openBrowser(help_url) : false;
|
|
28
|
+
options.signal.throwIfAborted();
|
|
29
|
+
output('billing_credit_help', { workspace_id: scope.workspace, help_url, opened }, `${opened ? 'Opened How credits work' : 'How credits work'}\n${help_url}`);
|
|
30
|
+
}
|
|
31
|
+
function scoped(value, scope) {
|
|
32
|
+
if (!object(value) || value.actor_id !== scope.owner || value.workspace_id !== scope.workspace
|
|
33
|
+
|| !uid(value.organization_id) || !uid(value.account_id))
|
|
34
|
+
throw invalid();
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
async function summary(scope, options) {
|
|
38
|
+
const raw = scoped(await api(`/api/workspaces/${scope.workspace}/billing/credits`, {
|
|
39
|
+
actor: scope.owner, workspace: scope.workspace, signal: options.signal
|
|
40
|
+
}), scope);
|
|
41
|
+
if (raw.schema !== 'firedrill.credit-summary.v1' || !object(raw.policy) || typeof raw.policy.enabled !== 'boolean'
|
|
42
|
+
|| raw.policy.help_path !== '/settings/billing/credits' || typeof raw.can_manage !== 'boolean')
|
|
43
|
+
throw invalid();
|
|
44
|
+
if (raw.policy.enabled && (!object(raw.balance) || !exact(raw.balance.available_units) || !exact(raw.balance.reserved_units)
|
|
45
|
+
|| !exact(raw.balance.credit_scale) || !object(raw.balance.by_source)
|
|
46
|
+
|| Object.values(raw.balance.by_source).some(one => !exact(one))))
|
|
47
|
+
throw invalid();
|
|
48
|
+
return raw;
|
|
49
|
+
}
|
|
50
|
+
async function summaryWithGuide(scope, options, output) {
|
|
51
|
+
try {
|
|
52
|
+
return await summary(scope, options);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!options.signal.aborted)
|
|
56
|
+
await showCreditGuide(scope, options, output);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function isCreditBillingAction(action) { return !!action && actions.has(action); }
|
|
61
|
+
export async function creditBillingCommand(args, options, output, selected) {
|
|
62
|
+
const action = args[0] ?? 'credits';
|
|
63
|
+
const detail = ['credit-work', 'credit-purchase'].includes(action);
|
|
64
|
+
if (!actions.has(action) || args.length > 2 || detail && !uid(args[1]) || !detail && args[1]
|
|
65
|
+
|| options.page !== undefined && (!/^[1-9][0-9]{0,8}$/.test(options.page) || !['usage', 'credit-ledger', 'credit-allocations', 'credit-purchases'].includes(action))
|
|
66
|
+
|| options.search !== undefined || options.kind !== undefined && !['credit-ledger', 'credit-allocations'].includes(action)
|
|
67
|
+
|| options.status !== undefined && !['usage', 'credit-purchases'].includes(action)
|
|
68
|
+
|| (options.from !== undefined || options.to !== undefined) && !['usage', 'credit-ledger'].includes(action))
|
|
69
|
+
throw new CliError('arguments_invalid', 'Use billing credits, credit-help, usage, credit-ledger, credit-allocations, credit-work <id>, credit-purchases, credit-purchase <id> or recharge.', 2);
|
|
70
|
+
const scope = selected ?? await selectedWorkspace({ ...options, interactive: !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY });
|
|
71
|
+
if (action === 'credit-help') {
|
|
72
|
+
await showCreditGuide(scope, options, output, true);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (action === 'recharge' || action === 'credit-settings') {
|
|
76
|
+
const url = new URL('/settings/billing', APP_ORIGIN);
|
|
77
|
+
url.searchParams.set('workspace', scope.workspace);
|
|
78
|
+
const result = { action: 'browser_approval_required', url: url.href, workspace_id: scope.workspace };
|
|
79
|
+
output('billing_browser_handoff', result, 'Open Billing to review credits, event codes, subscriptions and payment settings. Automatic top-ups require the billing owner to approve the pack, threshold, monthly cash cap and saved card.\n' + url.href);
|
|
80
|
+
if (options.openBrowser && !options.json)
|
|
81
|
+
await openBrowser(url.href);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Historical work/payments carry their own frozen identity and scale. A
|
|
85
|
+
// current tariff read is not a prerequisite for those independent reads or
|
|
86
|
+
// for the explicit browser-owned financial recovery ceremony above.
|
|
87
|
+
const row = ['credits', 'credit-ledger', 'credit-allocations'].includes(action) ? await summaryWithGuide(scope, options, output) : null;
|
|
88
|
+
if (row && (!row.policy.enabled || !row.balance)) {
|
|
89
|
+
const help_url = creditGuideUrl(scope);
|
|
90
|
+
output('billing_credit_policy_pending', { workspace_id: scope.workspace, configured: false,
|
|
91
|
+
help_url }, 'Resource-credit rates are not configured for this platform. No free-runtime or fixed-cost assumption was made.\nHow credits work\n' + help_url);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (action === 'credits') {
|
|
95
|
+
if (!row?.balance)
|
|
96
|
+
throw invalid();
|
|
97
|
+
output('billing_credits', row, `${credits(row.balance.available_units, row.balance.credit_scale)} credits available\n`
|
|
98
|
+
+ `${credits(row.balance.reserved_units, row.balance.credit_scale)} reserved\n`
|
|
99
|
+
+ Object.entries(row.balance.by_source).map(([source, value]) => `${terminalText(source)}: ${credits(value, row.balance.credit_scale)}`).join('\n')
|
|
100
|
+
+ '\nCredits cover preparation, actual authorized execution resources and standalone tool runtime.');
|
|
101
|
+
await showCreditGuide(scope, options, output);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (action === 'credit-purchases' || action === 'credit-purchase') {
|
|
105
|
+
const states = ['pending', 'preparing', 'awaiting_payment', 'paid', 'failed', 'uncertain', 'reconciliation_required', 'expired', 'reversed', 'requires_action', 'requires_confirmation', 'canceled'];
|
|
106
|
+
if (options.status !== undefined && !states.includes(options.status))
|
|
107
|
+
throw new CliError('arguments_invalid', 'Choose a supported credit purchase status.', 2);
|
|
108
|
+
const suffix = action === 'credit-purchase' ? `/${args[1]}` : `?${new URLSearchParams({ page: options.page ?? '1', state: options.status ?? '' })}`;
|
|
109
|
+
const value = scoped(await api(`/api/workspaces/${scope.workspace}/billing/credits/purchases${suffix}`, {
|
|
110
|
+
actor: scope.owner, workspace: scope.workspace, signal: options.signal
|
|
111
|
+
}), scope);
|
|
112
|
+
const read = (raw) => {
|
|
113
|
+
if (!object(raw) || !uid(raw.id) || !['topup', 'automatic_topup'].includes(String(raw.kind)) || !boundedText(raw.state, 48) || !raw.state
|
|
114
|
+
|| raw.request_state !== null && !boundedText(raw.request_state, 48)
|
|
115
|
+
|| !exact(raw.units) || !exact(raw.amount_cents) || raw.currency !== 'usd'
|
|
116
|
+
|| !(raw.denomination === null || typeof raw.denomination === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,31}$/.test(raw.denomination))
|
|
117
|
+
|| !(raw.credit_scale === null ? raw.url === null : retainedCreditUnit(raw))
|
|
118
|
+
|| raw.message !== undefined && !boundedText(raw.message, 512))
|
|
119
|
+
throw invalid();
|
|
120
|
+
return raw;
|
|
121
|
+
};
|
|
122
|
+
const state = (row) => !['paid', 'reversed'].includes(String(row.state))
|
|
123
|
+
&& ['uncertain', 'failed', 'reconciliation_required'].includes(String(row.request_state)) ? String(row.request_state) : String(row.state);
|
|
124
|
+
const money = (cents) => new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(cents / 100);
|
|
125
|
+
if (action === 'credit-purchase') {
|
|
126
|
+
const purchase = read(value.purchase);
|
|
127
|
+
if (purchase.id !== args[1])
|
|
128
|
+
throw invalid();
|
|
129
|
+
const url = new URL('/settings/billing', APP_ORIGIN);
|
|
130
|
+
url.searchParams.set('workspace', scope.workspace);
|
|
131
|
+
url.searchParams.set('credit_history_purchase', purchase.id);
|
|
132
|
+
// Hosted payment capabilities stay in the explicit browser ceremony.
|
|
133
|
+
const { url: _hosted, ...metadata } = purchase;
|
|
134
|
+
output('billing_credit_purchase', { ...value, purchase: metadata, browser_url: url.href }, `${retainedCreditAmount(purchase)} · ${money(purchase.amount_cents)}\n${terminalText(state(purchase)).replaceAll('_', ' ')}`
|
|
135
|
+
+ (purchase.message ? `\n${terminalText(purchase.message)}` : '') + `\nReview this original payment in Billing:\n${url.href}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!exact(value.count) || !exact(value.page) || value.page !== Number(options.page ?? '1') || !exact(value.page_size)
|
|
139
|
+
|| value.page_size < 1 || value.page_size > 100 || !Array.isArray(value.results) || value.results.length > value.page_size)
|
|
140
|
+
throw invalid();
|
|
141
|
+
const purchases = value.results.map(read);
|
|
142
|
+
if (purchases.some(row => row.url !== null || options.status && state(row) !== options.status))
|
|
143
|
+
throw invalid();
|
|
144
|
+
output('billing_credit_purchases', value, (purchases.map(purchase => `${purchase.id}\t${retainedCreditAmount(purchase)}\t${money(purchase.amount_cents)}\t${terminalText(state(purchase)).replaceAll('_', ' ')}`).join('\n') || 'No credit purchases.')
|
|
145
|
+
+ `\nPage ${value.page}; ${value.count} purchases.` + (value.page * value.page_size < value.count ? `\nNext: firedrill billing credit-purchases --page ${value.page + 1}` : ''));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const path = action === 'credit-work' ? `/usage/works/${args[1]}` : action === 'usage' ? '/usage/works'
|
|
149
|
+
: action === 'credit-ledger' ? '/credits/ledger' : '/credits/allocations';
|
|
150
|
+
const params = new URLSearchParams();
|
|
151
|
+
if (options.page)
|
|
152
|
+
params.set('page', options.page);
|
|
153
|
+
if (options.kind) {
|
|
154
|
+
const kinds = action === 'credit-ledger' ? ['grant', 'reserved', 'consumed', 'released', 'expired', 'revoked', 'refunded']
|
|
155
|
+
: ['included', 'purchased', 'sponsored', 'adjustment'];
|
|
156
|
+
if (!kinds.includes(options.kind))
|
|
157
|
+
throw new CliError('arguments_invalid', 'Choose a supported credit history kind.', 2);
|
|
158
|
+
params.set('kind', options.kind);
|
|
159
|
+
}
|
|
160
|
+
if (options.status) {
|
|
161
|
+
if (!['reserved', 'measured', 'budget_exhausted', 'usage_pending', 'settled', 'released'].includes(options.status))
|
|
162
|
+
throw new CliError('arguments_invalid', 'Choose a supported work usage status.', 2);
|
|
163
|
+
params.set('state', options.status);
|
|
164
|
+
}
|
|
165
|
+
for (const [key, value] of [['from', options.from], ['to', options.to]])
|
|
166
|
+
if (value !== undefined) {
|
|
167
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) || !Number.isFinite(Date.parse(value)))
|
|
168
|
+
throw new CliError('arguments_invalid', 'Use an ISO timestamp with an explicit timezone for --from or --to.', 2);
|
|
169
|
+
params.set(key, new Date(value).toISOString());
|
|
170
|
+
}
|
|
171
|
+
const raw = await api(`/api/workspaces/${scope.workspace}/billing${path}${params.size ? '?' + params : ''}`, {
|
|
172
|
+
actor: scope.owner, workspace: scope.workspace, signal: options.signal
|
|
173
|
+
});
|
|
174
|
+
if (action === 'credit-work') {
|
|
175
|
+
const work = scoped(raw, scope);
|
|
176
|
+
if (work.id !== args[1]
|
|
177
|
+
|| !exact(work.estimated_units) || !exact(work.maximum_units) || !exact(work.consumed_units) || !exact(work.credit_scale)
|
|
178
|
+
|| work.measured_units !== null && !exact(work.measured_units)
|
|
179
|
+
|| typeof work.state !== 'string' || typeof work.measurement_complete !== 'boolean')
|
|
180
|
+
throw invalid();
|
|
181
|
+
output('billing_credit_work', work, `Work: ${terminalText(String(work.id))}\n${terminalText(work.state)}\n`
|
|
182
|
+
+ `Estimate: ${credits(work.estimated_units, work.credit_scale)} credits\nBudget: ${credits(work.maximum_units, work.credit_scale)} credits\n`
|
|
183
|
+
+ `Recorded use: ${work.measured_units === null ? 'Not yet observed' : credits(work.measured_units, work.credit_scale) + ' credits' + (work.measurement_complete ? '' : ' so far')}\n`
|
|
184
|
+
+ (['settled', 'released'].includes(work.state) ? `Charged: ${credits(work.consumed_units, work.credit_scale)} credits` : 'Final charge pending'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const page = scoped(raw, scope);
|
|
188
|
+
if (!exact(page.count) || !exact(page.page_size) || page.page_size < 1 || !Array.isArray(page.results) || page.results.length > 100)
|
|
189
|
+
throw invalid();
|
|
190
|
+
for (const entry of page.results)
|
|
191
|
+
if (!object(entry) || !uid(entry.id))
|
|
192
|
+
throw invalid();
|
|
193
|
+
output('billing_credit_history', page, `${page.count} records, page ${options.page ?? '1'}\n`
|
|
194
|
+
+ (page.results.length ? page.results.map((entry) => {
|
|
195
|
+
const label = typeof entry.description === 'string' ? entry.description : `${entry.activity ?? entry.kind ?? 'Allocation'} ${entry.state ?? ''}`;
|
|
196
|
+
// Work history can include a previous tariff/denomination. Never apply
|
|
197
|
+
// today's display scale to another retained quote's actual charge.
|
|
198
|
+
const scale = action === 'usage' ? entry.credit_scale : row.balance.credit_scale;
|
|
199
|
+
if (!exact(scale) || scale < 1 || !/^10*$/.test(String(scale)))
|
|
200
|
+
throw invalid();
|
|
201
|
+
if (action === 'usage' && entry.measured_units !== null && !exact(entry.measured_units))
|
|
202
|
+
throw invalid();
|
|
203
|
+
const amount = action === 'usage' ? entry.measured_units : entry.units ?? entry.available_units;
|
|
204
|
+
if (amount !== undefined && amount !== null && (typeof amount !== 'number' || !Number.isSafeInteger(amount)))
|
|
205
|
+
throw invalid();
|
|
206
|
+
const formatted = amount === null ? 'Not yet observed' : amount === undefined ? '' : amount < 0 ? '-' + credits(-amount, scale) : credits(amount, scale);
|
|
207
|
+
return `${entry.id}\t${terminalText(label)}\t${formatted}`;
|
|
208
|
+
}).join('\n') : 'No matching records.')
|
|
209
|
+
+ (Number(options.page ?? 1) * page.page_size < page.count ? `\nNext page: firedrill billing ${action} --page ${Number(options.page ?? 1) + 1}` : ''));
|
|
210
|
+
}
|
|
211
|
+
/** Preserve a single organization scope when upgrading the normal billing flow. */
|
|
212
|
+
export async function useResourceCreditSummary(scope, options, output) {
|
|
213
|
+
const row = await summaryWithGuide(scope, options, output);
|
|
214
|
+
if (!row.policy.enabled) {
|
|
215
|
+
await creditBillingCommand(['credits'], options, output, scope);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
await creditBillingCommand(['credits'], options, output, scope);
|
|
219
|
+
if (!options.json && process.stdin.isTTY && process.stderr.isTTY) {
|
|
220
|
+
const choice = await selectOne('Billing', [{ value: 'usage', label: 'Work usage' }, { value: 'credit-ledger', label: 'Credit history' },
|
|
221
|
+
{ value: 'credit-allocations', label: 'Allocations and expiry' }, { value: 'credit-help', label: 'How credits work' }, ...(row.can_manage ? [{ value: 'credit-purchases', label: 'Credit purchase history' }, { value: 'recharge', label: 'Payment and top-up settings in browser' }] : []),
|
|
222
|
+
{ value: 'done', label: 'Back' }], options.signal);
|
|
223
|
+
if (choice !== 'done')
|
|
224
|
+
await creditBillingCommand([choice], options, output, scope);
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/billing-wallet-contracts.js';
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { api, openBrowser } from './auth.js';
|
|
2
|
+
import { CliError } from './http.js';
|
|
3
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
4
|
+
import { selectedWorkspace } from './world-access.js';
|
|
5
|
+
import { identifier } from './world-contracts.js';
|
|
6
|
+
import { creditBillingCommand, isCreditBillingAction, useResourceCreditSummary } from './billing-usage.js';
|
|
7
|
+
import { billingWalletRead, billingSummaryMatches } from './billing-wallet-contracts.js';
|
|
8
|
+
import { billingCommerceRead, billingOptionsMatch, billingPurchaseMatches, billingPurchasesMatch, billingInvoiceMatches, billingInvoicesMatch, billingInvoiceLinesMatch } from './billing-commerce-contracts.js';
|
|
9
|
+
const interactive = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
10
|
+
const auth = (scope, options) => ({ actor: scope.owner, workspace: scope.workspace, signal: options.signal });
|
|
11
|
+
const prefix = (scope) => `/api/workspaces/${scope.workspace}/billing`;
|
|
12
|
+
const money = (cents) => Number.isSafeInteger(cents)
|
|
13
|
+
? new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(cents / 100) : 'Amount unavailable';
|
|
14
|
+
const purchaseStates = { paid: 'Payment recorded', pending: 'Preparing hosted page', preparing: 'Preparing hosted page',
|
|
15
|
+
awaiting_payment: 'Awaiting payment', portal_ready: 'Hosted management ready', uncertain: 'Request status unconfirmed',
|
|
16
|
+
reconciliation_required: 'Payment needs review', failed: 'Request not completed', expired: 'Request expired', reversed: 'Payment adjusted' };
|
|
17
|
+
const effectiveState = (row) => !['paid', 'reversed'].includes(row.state)
|
|
18
|
+
&& ['uncertain', 'failed', 'reconciliation_required'].includes(row.request_state ?? '') ? row.request_state : row.state;
|
|
19
|
+
const purchaseName = (row) => row.kind === 'portal' ? row.offer ? `Change to ${row.offer} (${row.interval})` : 'Payment details and cancellation'
|
|
20
|
+
: `${row.offer} subscription (${row.interval})`;
|
|
21
|
+
const purchaseUnitUnavailable = (row) => !(row.kind === 'portal' && row.units === 0 && row.amount_cents === 0
|
|
22
|
+
&& row.denomination === null && !row.offer && !row.interval)
|
|
23
|
+
&& !(typeof row.denomination === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,31}$/.test(row.denomination)
|
|
24
|
+
&& typeof row.credit_scale === 'number' && Number.isSafeInteger(row.credit_scale) && row.credit_scale >= 1 && /^10*$/.test(String(row.credit_scale)));
|
|
25
|
+
const invoiceId = (value) => {
|
|
26
|
+
if (!/^in_[A-Za-z0-9_]{1,120}$/.test(value))
|
|
27
|
+
throw new CliError('invoice_required', 'Choose an invoice from billing invoices.', 2);
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
30
|
+
function showPurchase(row, output) {
|
|
31
|
+
const { url: _url, ...publicRow } = row;
|
|
32
|
+
output('billing_purchase', publicRow, `${purchaseName(row)}\n${purchaseStates[effectiveState(row)] ?? 'Status unavailable'}\n`
|
|
33
|
+
+ (row.kind === 'portal' ? '' : `${money(row.amount_cents)} before any Checkout discount. Recorded payment details are in invoices.\n`)
|
|
34
|
+
+ (row.kind === 'portal' && row.offer ? 'Confirm the selected change and any proration in Stripe. Opening this page does not confirm a change or payment.\n' : '')
|
|
35
|
+
+ `Request: ${row.id}\nRecorded: ${row.created_at}` + (row.message ? `\n${row.message}` : '')
|
|
36
|
+
+ (row.url && !purchaseUnitUnavailable(row) ? `\nOpen the hosted page: firedrill billing open ${row.id}` : ''));
|
|
37
|
+
}
|
|
38
|
+
async function purchaseAction(action, id, scope, options, output) {
|
|
39
|
+
const selected = identifier(id), path = `${prefix(scope)}/purchases/${selected}`;
|
|
40
|
+
const row = await api(path, action === 'refresh-purchase'
|
|
41
|
+
? { ...auth(scope, options), method: 'POST', body: {} } : auth(scope, options));
|
|
42
|
+
billingCommerceRead(row, billingPurchaseMatches(row, scope.workspace, { id: selected }));
|
|
43
|
+
if (action === 'refresh-purchase') {
|
|
44
|
+
showPurchase(row, output);
|
|
45
|
+
output('billing_refresh', { id: selected, scheduled: true }, 'A read of this original payment or subscription was requested. No new payment or plan change was created.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (action !== 'open') {
|
|
49
|
+
showPurchase(row, output);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let link;
|
|
53
|
+
try {
|
|
54
|
+
if (purchaseUnitUnavailable(row) || !row.url || !row.expires_at || !['awaiting_payment', 'portal_ready'].includes(row.state)
|
|
55
|
+
|| !Number.isFinite(Date.parse(row.expires_at)) || Date.parse(row.expires_at) <= Date.now())
|
|
56
|
+
throw new Error();
|
|
57
|
+
link = new URL(row.url);
|
|
58
|
+
if (link.protocol !== 'https:' || link.host !== (row.kind === 'portal' ? 'billing.stripe.com' : 'checkout.stripe.com')
|
|
59
|
+
|| link.username || link.password)
|
|
60
|
+
throw new Error();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new CliError('billing_link_unavailable', 'No current hosted link is available. Inspect this original payment request before starting another.', 2);
|
|
64
|
+
}
|
|
65
|
+
const opened = !options.json && options.openBrowser && await openBrowser(link.href);
|
|
66
|
+
output('billing_hosted_page', { id: row.id, opened, ...(opened ? {} : { url: link.href }), expires_at: row.expires_at }, opened ? 'Opened the hosted payment page. No payment is implied.' : `Open the hosted payment page: ${link.href}`);
|
|
67
|
+
}
|
|
68
|
+
async function invoiceAction(action, id, scope, options, output) {
|
|
69
|
+
const row = await api(`${prefix(scope)}/invoices/${invoiceId(id)}`, action === 'refresh-invoice'
|
|
70
|
+
? { ...auth(scope, options), method: 'POST', body: {} } : auth(scope, options));
|
|
71
|
+
billingCommerceRead(row, billingInvoiceMatches(row, scope.workspace, id));
|
|
72
|
+
output('billing_invoice', row, `${row.id}\n${row.status} · ${money(row.total_cents)} total · ${money(row.paid_cents)} paid\n`
|
|
73
|
+
+ `${money(row.discount_cents)} discount\nRecorded: ${row.created_at}\nReconciliation: ${row.reconciliation}`
|
|
74
|
+
+ (row.service_starts_at ? `\nService period: ${row.service_starts_at} to ${row.service_ends_at}` : '')
|
|
75
|
+
+ (action === 'refresh-invoice' ? '\nA current invoice read was requested. No payment was changed.' : `\nInspect line items: firedrill billing lines ${row.id}`));
|
|
76
|
+
}
|
|
77
|
+
async function history(action, id, scope, options, output) {
|
|
78
|
+
let page = Number(options.page ?? '1');
|
|
79
|
+
while (true) {
|
|
80
|
+
const query = new URLSearchParams({ page: String(page) });
|
|
81
|
+
if (options.status)
|
|
82
|
+
query.set('status', options.status);
|
|
83
|
+
const path = action === 'lines' ? `/invoices/${invoiceId(id)}/lines` : `/${action}`;
|
|
84
|
+
let rows, labels;
|
|
85
|
+
if (action === 'purchases') {
|
|
86
|
+
const value = await api(`${prefix(scope)}${path}?${query}`, auth(scope, options));
|
|
87
|
+
rows = billingCommerceRead(value, billingPurchasesMatch(value, scope.workspace, options.status ?? '', page));
|
|
88
|
+
labels = value.results.map(item => `${purchaseName(item)} · ${purchaseStates[effectiveState(item)] ?? 'Status unavailable'} · ${item.id}`);
|
|
89
|
+
}
|
|
90
|
+
else if (action === 'invoices') {
|
|
91
|
+
const value = await api(`${prefix(scope)}${path}?${query}`, auth(scope, options));
|
|
92
|
+
rows = billingCommerceRead(value, billingInvoicesMatch(value, scope.workspace, options.status ?? '', page));
|
|
93
|
+
labels = value.results.map(item => `${money(item.total_cents)} · ${item.status} · ${item.id}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const value = await api(`${prefix(scope)}${path}?${query}`, auth(scope, options));
|
|
97
|
+
rows = billingCommerceRead(value, billingInvoiceLinesMatch(value, scope.workspace, id, page));
|
|
98
|
+
labels = value.results.map(item => `${item.plan}, ${item.interval} · ${money(item.amount_cents)}${item.prorated ? ' · Prorated' : ''} · ${item.service_starts_at} to ${item.service_ends_at}`);
|
|
99
|
+
}
|
|
100
|
+
output(`billing_${action}`, rows, (labels.join('\n') || 'No matching records.') + `\nPage ${rows.page} · ${rows.count} records`
|
|
101
|
+
+ (action === 'lines' && 'collection_complete' in rows && !rows.collection_complete ? '\nInvoice line collection is incomplete. These are the items recorded so far.' : '')
|
|
102
|
+
+ (rows.next ? `\nNext: firedrill billing ${action}${id ? ` ${id}` : ''} --workspace ${scope.workspace} --page ${page + 1}${options.status ? ` --status ${options.status}` : ''}` : ''));
|
|
103
|
+
if (!interactive(options))
|
|
104
|
+
return;
|
|
105
|
+
const selected = await selectOne('Billing history', [
|
|
106
|
+
...(['purchases', 'invoices'].includes(action) ? rows.results.map((item, index) => ({ value: item.id, label: terminalText(labels[index]) })) : []),
|
|
107
|
+
...(page > 1 ? [{ value: 'previous', label: 'Previous page' }] : []),
|
|
108
|
+
...(rows.next ? [{ value: 'next', label: 'Next page' }] : []), { value: 'done', label: 'Back' }
|
|
109
|
+
], options.signal);
|
|
110
|
+
if (selected === 'done')
|
|
111
|
+
return;
|
|
112
|
+
if (selected === 'previous') {
|
|
113
|
+
page -= 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (selected === 'next') {
|
|
117
|
+
page += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (action === 'purchases')
|
|
121
|
+
await purchaseAction('purchase', selected, scope, options, output);
|
|
122
|
+
else
|
|
123
|
+
await invoiceAction('invoice', selected, scope, options, output);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function billingCommand(args, options, output) {
|
|
127
|
+
const action = args[0] ?? 'summary';
|
|
128
|
+
if (isCreditBillingAction(action)) {
|
|
129
|
+
await creditBillingCommand(args, options, output);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const financial = ['subscribe', 'change', 'topup', 'portal', 'redeem'];
|
|
133
|
+
const withId = ['purchase', 'open', 'refresh-purchase', 'invoice', 'lines', 'refresh-invoice'];
|
|
134
|
+
const listing = ['purchases', 'invoices', 'lines'];
|
|
135
|
+
if (!['summary', 'options', ...financial, ...withId, ...listing].includes(action) || args.length > 2
|
|
136
|
+
|| withId.includes(action) && !args[1] || args[1] && !withId.includes(action)
|
|
137
|
+
|| options.from !== undefined || options.to !== undefined || options.kind !== undefined || options.search !== undefined
|
|
138
|
+
|| options.page !== undefined && (!listing.includes(action) || !/^[1-9][0-9]{0,8}$/.test(options.page))
|
|
139
|
+
|| options.status !== undefined && (!['purchases', 'invoices'].includes(action)
|
|
140
|
+
|| !(action === 'purchases' ? Object.keys(purchaseStates) : ['draft', 'open', 'paid', 'void', 'uncollectible']).includes(options.status)))
|
|
141
|
+
throw new CliError('arguments_invalid', 'Use billing credits, usage, credit-ledger, credit-allocations, options, purchases or invoices. Review subscription, payment and event-code changes in Billing.', 2);
|
|
142
|
+
const scope = await selectedWorkspace({ ...options, interactive: interactive(options) });
|
|
143
|
+
if (action === 'summary') {
|
|
144
|
+
await useResourceCreditSummary(scope, options, output);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (financial.includes(action)) {
|
|
148
|
+
await creditBillingCommand(['credit-settings'], options, output, scope);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (withId.includes(action)) {
|
|
152
|
+
if (action === 'lines')
|
|
153
|
+
await history('lines', args[1], scope, options, output);
|
|
154
|
+
else if (action === 'invoice' || action === 'refresh-invoice')
|
|
155
|
+
await invoiceAction(action, args[1], scope, options, output);
|
|
156
|
+
else
|
|
157
|
+
await purchaseAction(action, args[1], scope, options, output);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (action === 'purchases' || action === 'invoices') {
|
|
161
|
+
await history(action, undefined, scope, options, output);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const summary = await api(prefix(scope), auth(scope, options));
|
|
165
|
+
billingWalletRead(summary, billingSummaryMatches(summary, scope.workspace, scope.owner));
|
|
166
|
+
const offers = summary.can_manage ? await api(`${prefix(scope)}/options`, auth(scope, options)) : null;
|
|
167
|
+
if (offers)
|
|
168
|
+
billingCommerceRead(offers, billingOptionsMatch(offers, scope.workspace, scope.owner, summary.organization_id));
|
|
169
|
+
output('billing_options', { catalog: summary.catalog, offers, can_manage: summary.can_manage }, JSON.stringify({ catalog: summary.catalog, offers, can_manage: summary.can_manage }, null, 2));
|
|
170
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { CliError } from './http.js';
|
|
7
|
+
const unavailable = () => new CliError('bootstrap_operation_owned', 'This sign-in or helper setup is already owned, a prior lock needs inspection, or local coordination is unavailable. No pending request was repeated.');
|
|
8
|
+
/** Before a trusted Python exists, Node's exclusive loopback binding supplies
|
|
9
|
+
* process-lifetime coordination only. No protocol, credentials or requests are
|
|
10
|
+
* served. A deterministic port collision is a refusal, never another lock slot.
|
|
11
|
+
* This is not proof of account identity or a security boundary against local
|
|
12
|
+
* denial of service. All product/refresh journals retain their original fences.
|
|
13
|
+
*/
|
|
14
|
+
class BindCollision extends Error {
|
|
15
|
+
}
|
|
16
|
+
export async function bootstrapProcessLock(path, options = {}) {
|
|
17
|
+
const waitMs = options.waitMs ?? 0;
|
|
18
|
+
if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > 30000)
|
|
19
|
+
throw unavailable();
|
|
20
|
+
const deadline = performance.now() + waitMs;
|
|
21
|
+
for (;;) {
|
|
22
|
+
options.signal?.throwIfAborted();
|
|
23
|
+
try {
|
|
24
|
+
return await acquireProcessLock(path);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Only an OS bind collision may wait. No old directory lock, failed
|
|
28
|
+
// ownership check, provider request or persisted refresh is retried.
|
|
29
|
+
if (!(error instanceof BindCollision))
|
|
30
|
+
throw error;
|
|
31
|
+
const remaining = deadline - performance.now();
|
|
32
|
+
if (remaining <= 0)
|
|
33
|
+
throw unavailable();
|
|
34
|
+
await delay(Math.min(remaining, 100 + Math.floor(Math.random() * 150)), undefined, { signal: options.signal });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function acquireProcessLock(path) {
|
|
39
|
+
if (!isAbsolute(path))
|
|
40
|
+
throw unavailable();
|
|
41
|
+
const directory = await lstat(dirname(path));
|
|
42
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
43
|
+
|| process.getuid && directory.uid !== process.getuid()
|
|
44
|
+
|| process.platform !== 'win32' && directory.mode & 0o077)
|
|
45
|
+
throw unavailable();
|
|
46
|
+
// Preserve any old directory-based owner, including an ambiguous dead owner.
|
|
47
|
+
// Never infer safety from its PID or unlink it to get a different lock.
|
|
48
|
+
try {
|
|
49
|
+
await lstat(path);
|
|
50
|
+
throw unavailable();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== 'ENOENT')
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
const port = 16384 + createHash('sha256').update('firedrill.bootstrap-lock.v1\0').update(path).digest().readUInt32BE(0) % 49152;
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const server = createServer(socket => socket.destroy());
|
|
59
|
+
server.maxConnections = 1;
|
|
60
|
+
let held = false, closing = false, closePromise;
|
|
61
|
+
const lost = () => { held = false; if (!closing)
|
|
62
|
+
controller.abort(); };
|
|
63
|
+
server.on('error', lost);
|
|
64
|
+
server.on('close', lost);
|
|
65
|
+
server.on('listening', () => { if (closing)
|
|
66
|
+
server.close(); });
|
|
67
|
+
try {
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
let finished = false;
|
|
70
|
+
const fail = (error) => finish(error?.code === 'EADDRINUSE' ? new BindCollision() : unavailable());
|
|
71
|
+
const finish = (error) => {
|
|
72
|
+
if (finished)
|
|
73
|
+
return;
|
|
74
|
+
finished = true;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
server.off('error', fail);
|
|
77
|
+
server.off('listening', ready);
|
|
78
|
+
if (error)
|
|
79
|
+
reject(error);
|
|
80
|
+
else
|
|
81
|
+
resolve();
|
|
82
|
+
};
|
|
83
|
+
const ready = () => { held = true; finish(); };
|
|
84
|
+
const timer = setTimeout(fail, 10000);
|
|
85
|
+
server.once('error', fail);
|
|
86
|
+
server.once('listening', ready);
|
|
87
|
+
try {
|
|
88
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true, backlog: 1 });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
finish(unavailable());
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
closing = true;
|
|
97
|
+
try {
|
|
98
|
+
server.close();
|
|
99
|
+
}
|
|
100
|
+
catch { /* A failed bind may never have opened. */ }
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
const close = () => closePromise ??= new Promise((resolve, reject) => {
|
|
104
|
+
closing = true;
|
|
105
|
+
held = false;
|
|
106
|
+
const timer = setTimeout(() => { controller.abort(); reject(unavailable()); }, 5000);
|
|
107
|
+
server.close(error => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
if (error) {
|
|
110
|
+
controller.abort();
|
|
111
|
+
reject(unavailable());
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return { signal: controller.signal, assertHeld: () => {
|
|
118
|
+
if (!held || !server.listening || controller.signal.aborted || closing)
|
|
119
|
+
throw unavailable();
|
|
120
|
+
}, close };
|
|
121
|
+
}
|
|
122
|
+
export async function withBootstrapProcessLock(path, signal, work) {
|
|
123
|
+
signal.throwIfAborted();
|
|
124
|
+
const lock = await bootstrapProcessLock(path), active = AbortSignal.any([signal, lock.signal]);
|
|
125
|
+
try {
|
|
126
|
+
lock.assertHeld();
|
|
127
|
+
active.throwIfAborted();
|
|
128
|
+
const value = await work(active, lock.signal);
|
|
129
|
+
lock.assertHeld();
|
|
130
|
+
active.throwIfAborted();
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await lock.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { APP_ORIGIN, CliError } from './http.js';
|
|
2
|
+
import { identifier } from './world-contracts.js';
|
|
3
|
+
/** Browser and native workspace selections are independent authenticated state. */
|
|
4
|
+
export function workspaceLink(workspace, path, parameters = {}) {
|
|
5
|
+
if (!path.startsWith('/') || path.startsWith('//'))
|
|
6
|
+
throw new CliError('browser_link_invalid', 'Open this record from its workspace.', 2);
|
|
7
|
+
const url = new URL(path, APP_ORIGIN);
|
|
8
|
+
if (url.origin !== new URL(APP_ORIGIN).origin)
|
|
9
|
+
throw new CliError('browser_link_invalid', 'Open this record from its workspace.', 2);
|
|
10
|
+
for (const [key, value] of Object.entries(parameters))
|
|
11
|
+
url.searchParams.set(key, value);
|
|
12
|
+
url.searchParams.set('workspace', identifier(workspace));
|
|
13
|
+
return url.href;
|
|
14
|
+
}
|