@cuna_labs/cli 0.1.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +169 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/capability-evidence.d.ts +19 -0
- package/dist/api/capability-evidence.d.ts.map +1 -0
- package/dist/api/capability-evidence.js +38 -0
- package/dist/api/capability-evidence.js.map +1 -0
- package/dist/api/client.d.ts +167 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +937 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/contracts.d.ts +345 -0
- package/dist/api/contracts.d.ts.map +1 -0
- package/dist/api/contracts.js +976 -0
- package/dist/api/contracts.js.map +1 -0
- package/dist/api/execution-workspaces.d.ts +17 -0
- package/dist/api/execution-workspaces.d.ts.map +1 -0
- package/dist/api/execution-workspaces.js +46 -0
- package/dist/api/execution-workspaces.js.map +1 -0
- package/dist/api/http.d.ts +76 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +838 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/managed-executions.d.ts +37 -0
- package/dist/api/managed-executions.d.ts.map +1 -0
- package/dist/api/managed-executions.js +80 -0
- package/dist/api/managed-executions.js.map +1 -0
- package/dist/api/observer-v2-schema.d.ts +206 -0
- package/dist/api/observer-v2-schema.d.ts.map +1 -0
- package/dist/api/observer-v2-schema.js +264 -0
- package/dist/api/observer-v2-schema.js.map +1 -0
- package/dist/api/observer-v2.d.ts +33 -0
- package/dist/api/observer-v2.d.ts.map +1 -0
- package/dist/api/observer-v2.js +60 -0
- package/dist/api/observer-v2.js.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts +678 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.js +887 -0
- package/dist/api/owner-observe-grants-v2-schema.js.map +1 -0
- package/dist/api/owner-observe-grants-v2.d.ts +268 -0
- package/dist/api/owner-observe-grants-v2.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2.js +604 -0
- package/dist/api/owner-observe-grants-v2.js.map +1 -0
- package/dist/api/provider-v2-schema.d.ts +293 -0
- package/dist/api/provider-v2-schema.d.ts.map +1 -0
- package/dist/api/provider-v2-schema.js +381 -0
- package/dist/api/provider-v2-schema.js.map +1 -0
- package/dist/api/provider-v2.d.ts +55 -0
- package/dist/api/provider-v2.d.ts.map +1 -0
- package/dist/api/provider-v2.js +54 -0
- package/dist/api/provider-v2.js.map +1 -0
- package/dist/api/remote-workspace.d.ts +29 -0
- package/dist/api/remote-workspace.d.ts.map +1 -0
- package/dist/api/remote-workspace.js +62 -0
- package/dist/api/remote-workspace.js.map +1 -0
- package/dist/api/supervisor-live-update.d.ts +248 -0
- package/dist/api/supervisor-live-update.d.ts.map +1 -0
- package/dist/api/supervisor-live-update.js +434 -0
- package/dist/api/supervisor-live-update.js.map +1 -0
- package/dist/api/terminal-reason.d.ts +5 -0
- package/dist/api/terminal-reason.d.ts.map +1 -0
- package/dist/api/terminal-reason.js +12 -0
- package/dist/api/terminal-reason.js.map +1 -0
- package/dist/auth/browser-handoff.d.ts +59 -0
- package/dist/auth/browser-handoff.d.ts.map +1 -0
- package/dist/auth/browser-handoff.js +33 -0
- package/dist/auth/browser-handoff.js.map +1 -0
- package/dist/auth/browser.d.ts +11 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +47 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/human-client.d.ts +39 -0
- package/dist/auth/human-client.d.ts.map +1 -0
- package/dist/auth/human-client.js +66 -0
- package/dist/auth/human-client.js.map +1 -0
- package/dist/auth/human-contracts.d.ts +52 -0
- package/dist/auth/human-contracts.d.ts.map +1 -0
- package/dist/auth/human-contracts.js +229 -0
- package/dist/auth/human-contracts.js.map +1 -0
- package/dist/auth/human-session.d.ts +48 -0
- package/dist/auth/human-session.d.ts.map +1 -0
- package/dist/auth/human-session.js +902 -0
- package/dist/auth/human-session.js.map +1 -0
- package/dist/auth/onboarding-state.d.ts +31 -0
- package/dist/auth/onboarding-state.d.ts.map +1 -0
- package/dist/auth/onboarding-state.js +52 -0
- package/dist/auth/onboarding-state.js.map +1 -0
- package/dist/auth/pkce.d.ts +15 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +19 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/bin/cuna.d.ts +3 -0
- package/dist/bin/cuna.d.ts.map +1 -0
- package/dist/bin/cuna.js +4 -0
- package/dist/bin/cuna.js.map +1 -0
- package/dist/build-identity.d.ts +20 -0
- package/dist/build-identity.d.ts.map +1 -0
- package/dist/build-identity.js +77 -0
- package/dist/build-identity.js.map +1 -0
- package/dist/cli/command-help.d.ts +9 -0
- package/dist/cli/command-help.d.ts.map +1 -0
- package/dist/cli/command-help.js +324 -0
- package/dist/cli/command-help.js.map +1 -0
- package/dist/cli/help.d.ts +40 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +256 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +95 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/parser.d.ts +65 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +294 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/process-entrypoint.d.ts +18 -0
- package/dist/cli/process-entrypoint.d.ts.map +1 -0
- package/dist/cli/process-entrypoint.js +26 -0
- package/dist/cli/process-entrypoint.js.map +1 -0
- package/dist/cli/run.d.ts +114 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +1969 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/commands/commands.d.ts +74 -0
- package/dist/commands/commands.d.ts.map +1 -0
- package/dist/commands/commands.js +2814 -0
- package/dist/commands/commands.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +323 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/infra-contract-witness.d.ts +29 -0
- package/dist/config/infra-contract-witness.d.ts.map +1 -0
- package/dist/config/infra-contract-witness.js +30 -0
- package/dist/config/infra-contract-witness.js.map +1 -0
- package/dist/core/browser-url.d.ts +2 -0
- package/dist/core/browser-url.d.ts.map +1 -0
- package/dist/core/browser-url.js +20 -0
- package/dist/core/browser-url.js.map +1 -0
- package/dist/core/deployed-wire-compatibility.d.ts +15 -0
- package/dist/core/deployed-wire-compatibility.d.ts.map +1 -0
- package/dist/core/deployed-wire-compatibility.js +15 -0
- package/dist/core/deployed-wire-compatibility.js.map +1 -0
- package/dist/core/errors.d.ts +59 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +82 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/exit-codes.d.ts +68 -0
- package/dist/core/exit-codes.d.ts.map +1 -0
- package/dist/core/exit-codes.js +84 -0
- package/dist/core/exit-codes.js.map +1 -0
- package/dist/core/instant.d.ts +60 -0
- package/dist/core/instant.d.ts.map +1 -0
- package/dist/core/instant.js +90 -0
- package/dist/core/instant.js.map +1 -0
- package/dist/core/namespace.d.ts +171 -0
- package/dist/core/namespace.d.ts.map +1 -0
- package/dist/core/namespace.js +227 -0
- package/dist/core/namespace.js.map +1 -0
- package/dist/core/observation-budget.d.ts +185 -0
- package/dist/core/observation-budget.d.ts.map +1 -0
- package/dist/core/observation-budget.js +209 -0
- package/dist/core/observation-budget.js.map +1 -0
- package/dist/core/product-web.d.ts +81 -0
- package/dist/core/product-web.d.ts.map +1 -0
- package/dist/core/product-web.js +87 -0
- package/dist/core/product-web.js.map +1 -0
- package/dist/core/validation.d.ts +90 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +199 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/credentials/contracts.d.ts +82 -0
- package/dist/credentials/contracts.d.ts.map +1 -0
- package/dist/credentials/contracts.js +2 -0
- package/dist/credentials/contracts.js.map +1 -0
- package/dist/credentials/errors.d.ts +19 -0
- package/dist/credentials/errors.d.ts.map +1 -0
- package/dist/credentials/errors.js +16 -0
- package/dist/credentials/errors.js.map +1 -0
- package/dist/credentials/index.d.ts +6 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +6 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/local-session.d.ts +101 -0
- package/dist/credentials/local-session.d.ts.map +1 -0
- package/dist/credentials/local-session.js +839 -0
- package/dist/credentials/local-session.js.map +1 -0
- package/dist/credentials/secret-material.d.ts +17 -0
- package/dist/credentials/secret-material.d.ts.map +1 -0
- package/dist/credentials/secret-material.js +49 -0
- package/dist/credentials/secret-material.js.map +1 -0
- package/dist/credentials/vault.d.ts +41 -0
- package/dist/credentials/vault.d.ts.map +1 -0
- package/dist/credentials/vault.js +719 -0
- package/dist/credentials/vault.js.map +1 -0
- package/dist/daemon/endpoint.d.ts +29 -0
- package/dist/daemon/endpoint.d.ts.map +1 -0
- package/dist/daemon/endpoint.js +66 -0
- package/dist/daemon/endpoint.js.map +1 -0
- package/dist/daemon/journal.d.ts +62 -0
- package/dist/daemon/journal.d.ts.map +1 -0
- package/dist/daemon/journal.js +299 -0
- package/dist/daemon/journal.js.map +1 -0
- package/dist/daemon/lease.d.ts +25 -0
- package/dist/daemon/lease.d.ts.map +1 -0
- package/dist/daemon/lease.js +94 -0
- package/dist/daemon/lease.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +14 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +29 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/protocol.d.ts +65 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +194 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +29 -0
- package/dist/daemon/supervisor.d.ts.map +1 -0
- package/dist/daemon/supervisor.js +87 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/daemon/views.d.ts +31 -0
- package/dist/daemon/views.d.ts.map +1 -0
- package/dist/daemon/views.js +86 -0
- package/dist/daemon/views.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/journey/api-effects.d.ts +59 -0
- package/dist/journey/api-effects.d.ts.map +1 -0
- package/dist/journey/api-effects.js +386 -0
- package/dist/journey/api-effects.js.map +1 -0
- package/dist/journey/derived-identity.d.ts +91 -0
- package/dist/journey/derived-identity.d.ts.map +1 -0
- package/dist/journey/derived-identity.js +123 -0
- package/dist/journey/derived-identity.js.map +1 -0
- package/dist/journey/index.d.ts +6 -0
- package/dist/journey/index.d.ts.map +1 -0
- package/dist/journey/index.js +6 -0
- package/dist/journey/index.js.map +1 -0
- package/dist/journey/intent.d.ts +36 -0
- package/dist/journey/intent.d.ts.map +1 -0
- package/dist/journey/intent.js +191 -0
- package/dist/journey/intent.js.map +1 -0
- package/dist/journey/orchestrator.d.ts +140 -0
- package/dist/journey/orchestrator.d.ts.map +1 -0
- package/dist/journey/orchestrator.js +415 -0
- package/dist/journey/orchestrator.js.map +1 -0
- package/dist/journey/provider-launch-intent.d.ts +12 -0
- package/dist/journey/provider-launch-intent.d.ts.map +1 -0
- package/dist/journey/provider-launch-intent.js +40 -0
- package/dist/journey/provider-launch-intent.js.map +1 -0
- package/dist/journey/remote-workspace.d.ts +34 -0
- package/dist/journey/remote-workspace.d.ts.map +1 -0
- package/dist/journey/remote-workspace.js +91 -0
- package/dist/journey/remote-workspace.js.map +1 -0
- package/dist/journey/root-entry.d.ts +56 -0
- package/dist/journey/root-entry.d.ts.map +1 -0
- package/dist/journey/root-entry.js +19 -0
- package/dist/journey/root-entry.js.map +1 -0
- package/dist/journey/selection.d.ts +165 -0
- package/dist/journey/selection.d.ts.map +1 -0
- package/dist/journey/selection.js +565 -0
- package/dist/journey/selection.js.map +1 -0
- package/dist/journey/session-failure.d.ts +4 -0
- package/dist/journey/session-failure.d.ts.map +1 -0
- package/dist/journey/session-failure.js +28 -0
- package/dist/journey/session-failure.js.map +1 -0
- package/dist/journey/workspace-effects.d.ts +30 -0
- package/dist/journey/workspace-effects.d.ts.map +1 -0
- package/dist/journey/workspace-effects.js +348 -0
- package/dist/journey/workspace-effects.js.map +1 -0
- package/dist/local-actions/broker.d.ts +32 -0
- package/dist/local-actions/broker.d.ts.map +1 -0
- package/dist/local-actions/broker.js +400 -0
- package/dist/local-actions/broker.js.map +1 -0
- package/dist/local-actions/browser-action.d.ts +78 -0
- package/dist/local-actions/browser-action.d.ts.map +1 -0
- package/dist/local-actions/browser-action.js +307 -0
- package/dist/local-actions/browser-action.js.map +1 -0
- package/dist/local-actions/callback-relay.d.ts +45 -0
- package/dist/local-actions/callback-relay.d.ts.map +1 -0
- package/dist/local-actions/callback-relay.js +265 -0
- package/dist/local-actions/callback-relay.js.map +1 -0
- package/dist/local-actions/clipboard.d.ts +3 -0
- package/dist/local-actions/clipboard.d.ts.map +1 -0
- package/dist/local-actions/clipboard.js +18 -0
- package/dist/local-actions/clipboard.js.map +1 -0
- package/dist/local-actions/contracts.d.ts +63 -0
- package/dist/local-actions/contracts.d.ts.map +1 -0
- package/dist/local-actions/contracts.js +18 -0
- package/dist/local-actions/contracts.js.map +1 -0
- package/dist/local-actions/desktop-presentation.d.ts +141 -0
- package/dist/local-actions/desktop-presentation.d.ts.map +1 -0
- package/dist/local-actions/desktop-presentation.js +253 -0
- package/dist/local-actions/desktop-presentation.js.map +1 -0
- package/dist/local-actions/device-selection.d.ts +43 -0
- package/dist/local-actions/device-selection.d.ts.map +1 -0
- package/dist/local-actions/device-selection.js +154 -0
- package/dist/local-actions/device-selection.js.map +1 -0
- package/dist/local-actions/executor.d.ts +49 -0
- package/dist/local-actions/executor.d.ts.map +1 -0
- package/dist/local-actions/executor.js +329 -0
- package/dist/local-actions/executor.js.map +1 -0
- package/dist/local-actions/file-transfers.d.ts +141 -0
- package/dist/local-actions/file-transfers.d.ts.map +1 -0
- package/dist/local-actions/file-transfers.js +446 -0
- package/dist/local-actions/file-transfers.js.map +1 -0
- package/dist/local-actions/foreground-notifications.d.ts +45 -0
- package/dist/local-actions/foreground-notifications.d.ts.map +1 -0
- package/dist/local-actions/foreground-notifications.js +163 -0
- package/dist/local-actions/foreground-notifications.js.map +1 -0
- package/dist/local-actions/git-signing.d.ts +39 -0
- package/dist/local-actions/git-signing.d.ts.map +1 -0
- package/dist/local-actions/git-signing.js +135 -0
- package/dist/local-actions/git-signing.js.map +1 -0
- package/dist/local-actions/index.d.ts +19 -0
- package/dist/local-actions/index.d.ts.map +1 -0
- package/dist/local-actions/index.js +19 -0
- package/dist/local-actions/index.js.map +1 -0
- package/dist/local-actions/local-service-actions.d.ts +87 -0
- package/dist/local-actions/local-service-actions.d.ts.map +1 -0
- package/dist/local-actions/local-service-actions.js +347 -0
- package/dist/local-actions/local-service-actions.js.map +1 -0
- package/dist/local-actions/loopback.d.ts +5 -0
- package/dist/local-actions/loopback.d.ts.map +1 -0
- package/dist/local-actions/loopback.js +15 -0
- package/dist/local-actions/loopback.js.map +1 -0
- package/dist/local-actions/policy-store.d.ts +7 -0
- package/dist/local-actions/policy-store.d.ts.map +1 -0
- package/dist/local-actions/policy-store.js +70 -0
- package/dist/local-actions/policy-store.js.map +1 -0
- package/dist/local-actions/policy.d.ts +21 -0
- package/dist/local-actions/policy.d.ts.map +1 -0
- package/dist/local-actions/policy.js +43 -0
- package/dist/local-actions/policy.js.map +1 -0
- package/dist/local-actions/private-port-forward.d.ts +36 -0
- package/dist/local-actions/private-port-forward.d.ts.map +1 -0
- package/dist/local-actions/private-port-forward.js +154 -0
- package/dist/local-actions/private-port-forward.js.map +1 -0
- package/dist/local-actions/provider-mcp-config.d.ts +101 -0
- package/dist/local-actions/provider-mcp-config.d.ts.map +1 -0
- package/dist/local-actions/provider-mcp-config.js +166 -0
- package/dist/local-actions/provider-mcp-config.js.map +1 -0
- package/dist/local-actions/providers.d.ts +12 -0
- package/dist/local-actions/providers.d.ts.map +1 -0
- package/dist/local-actions/providers.js +42 -0
- package/dist/local-actions/providers.js.map +1 -0
- package/dist/local-actions/schemas.d.ts +4 -0
- package/dist/local-actions/schemas.d.ts.map +1 -0
- package/dist/local-actions/schemas.js +314 -0
- package/dist/local-actions/schemas.js.map +1 -0
- package/dist/local-actions/sensitive-consent.d.ts +32 -0
- package/dist/local-actions/sensitive-consent.d.ts.map +1 -0
- package/dist/local-actions/sensitive-consent.js +47 -0
- package/dist/local-actions/sensitive-consent.js.map +1 -0
- package/dist/machines/command-launch.d.ts +17 -0
- package/dist/machines/command-launch.d.ts.map +1 -0
- package/dist/machines/command-launch.js +52 -0
- package/dist/machines/command-launch.js.map +1 -0
- package/dist/machines/execution-receipt.d.ts +18 -0
- package/dist/machines/execution-receipt.d.ts.map +1 -0
- package/dist/machines/execution-receipt.js +87 -0
- package/dist/machines/execution-receipt.js.map +1 -0
- package/dist/machines/executions-screen.d.ts +6 -0
- package/dist/machines/executions-screen.d.ts.map +1 -0
- package/dist/machines/executions-screen.js +410 -0
- package/dist/machines/executions-screen.js.map +1 -0
- package/dist/machines/explorer.d.ts +75 -0
- package/dist/machines/explorer.d.ts.map +1 -0
- package/dist/machines/explorer.js +1726 -0
- package/dist/machines/explorer.js.map +1 -0
- package/dist/machines/live-supervisor-update.d.ts +325 -0
- package/dist/machines/live-supervisor-update.d.ts.map +1 -0
- package/dist/machines/live-supervisor-update.js +513 -0
- package/dist/machines/live-supervisor-update.js.map +1 -0
- package/dist/machines/machine-first.d.ts +123 -0
- package/dist/machines/machine-first.d.ts.map +1 -0
- package/dist/machines/machine-first.js +119 -0
- package/dist/machines/machine-first.js.map +1 -0
- package/dist/machines/opencode-supervisor.d.ts +51 -0
- package/dist/machines/opencode-supervisor.d.ts.map +1 -0
- package/dist/machines/opencode-supervisor.js +124 -0
- package/dist/machines/opencode-supervisor.js.map +1 -0
- package/dist/machines/pagination.d.ts +4 -0
- package/dist/machines/pagination.d.ts.map +1 -0
- package/dist/machines/pagination.js +7 -0
- package/dist/machines/pagination.js.map +1 -0
- package/dist/machines/provider-availability.d.ts +42 -0
- package/dist/machines/provider-availability.d.ts.map +1 -0
- package/dist/machines/provider-availability.js +82 -0
- package/dist/machines/provider-availability.js.map +1 -0
- package/dist/machines/provider-screen.d.ts +12 -0
- package/dist/machines/provider-screen.d.ts.map +1 -0
- package/dist/machines/provider-screen.js +129 -0
- package/dist/machines/provider-screen.js.map +1 -0
- package/dist/machines/session-actionability.d.ts +86 -0
- package/dist/machines/session-actionability.d.ts.map +1 -0
- package/dist/machines/session-actionability.js +125 -0
- package/dist/machines/session-actionability.js.map +1 -0
- package/dist/machines/session-visibility.d.ts +139 -0
- package/dist/machines/session-visibility.d.ts.map +1 -0
- package/dist/machines/session-visibility.js +109 -0
- package/dist/machines/session-visibility.js.map +1 -0
- package/dist/platform/adapter.d.ts +62 -0
- package/dist/platform/adapter.d.ts.map +1 -0
- package/dist/platform/adapter.js +240 -0
- package/dist/platform/adapter.js.map +1 -0
- package/dist/platform/support.d.ts +12 -0
- package/dist/platform/support.d.ts.map +1 -0
- package/dist/platform/support.js +25 -0
- package/dist/platform/support.js.map +1 -0
- package/dist/pty/contracts.d.ts +45 -0
- package/dist/pty/contracts.d.ts.map +1 -0
- package/dist/pty/contracts.js +2 -0
- package/dist/pty/contracts.js.map +1 -0
- package/dist/pty/evidence-gate.d.ts +14 -0
- package/dist/pty/evidence-gate.d.ts.map +1 -0
- package/dist/pty/evidence-gate.js +35 -0
- package/dist/pty/evidence-gate.js.map +1 -0
- package/dist/pty/index.d.ts +5 -0
- package/dist/pty/index.d.ts.map +1 -0
- package/dist/pty/index.js +5 -0
- package/dist/pty/index.js.map +1 -0
- package/dist/pty/node-host-terminal.d.ts +14 -0
- package/dist/pty/node-host-terminal.d.ts.map +1 -0
- package/dist/pty/node-host-terminal.js +173 -0
- package/dist/pty/node-host-terminal.js.map +1 -0
- package/dist/pty/node-process.d.ts +26 -0
- package/dist/pty/node-process.d.ts.map +1 -0
- package/dist/pty/node-process.js +102 -0
- package/dist/pty/node-process.js.map +1 -0
- package/dist/runtime/api-terminal-control-plane.d.ts +7 -0
- package/dist/runtime/api-terminal-control-plane.d.ts.map +1 -0
- package/dist/runtime/api-terminal-control-plane.js +95 -0
- package/dist/runtime/api-terminal-control-plane.js.map +1 -0
- package/dist/runtime/backoff.d.ts +17 -0
- package/dist/runtime/backoff.d.ts.map +1 -0
- package/dist/runtime/backoff.js +61 -0
- package/dist/runtime/backoff.js.map +1 -0
- package/dist/runtime/boundary.d.ts +171 -0
- package/dist/runtime/boundary.d.ts.map +1 -0
- package/dist/runtime/boundary.js +1858 -0
- package/dist/runtime/boundary.js.map +1 -0
- package/dist/runtime/capability-gate.d.ts +25 -0
- package/dist/runtime/capability-gate.d.ts.map +1 -0
- package/dist/runtime/capability-gate.js +77 -0
- package/dist/runtime/capability-gate.js.map +1 -0
- package/dist/runtime/contracts.d.ts +25 -0
- package/dist/runtime/contracts.d.ts.map +1 -0
- package/dist/runtime/contracts.js +66 -0
- package/dist/runtime/contracts.js.map +1 -0
- package/dist/runtime/errors.d.ts +20 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +22 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node-foreground-session.d.ts +50 -0
- package/dist/runtime/node-foreground-session.d.ts.map +1 -0
- package/dist/runtime/node-foreground-session.js +431 -0
- package/dist/runtime/node-foreground-session.js.map +1 -0
- package/dist/runtime/node-websocket-connector.d.ts +6 -0
- package/dist/runtime/node-websocket-connector.d.ts.map +1 -0
- package/dist/runtime/node-websocket-connector.js +344 -0
- package/dist/runtime/node-websocket-connector.js.map +1 -0
- package/dist/runtime/observer-screen.d.ts +6 -0
- package/dist/runtime/observer-screen.d.ts.map +1 -0
- package/dist/runtime/observer-screen.js +144 -0
- package/dist/runtime/observer-screen.js.map +1 -0
- package/dist/runtime/owner-grant-operations.d.ts +136 -0
- package/dist/runtime/owner-grant-operations.d.ts.map +1 -0
- package/dist/runtime/owner-grant-operations.js +216 -0
- package/dist/runtime/owner-grant-operations.js.map +1 -0
- package/dist/runtime/owner-grants-screen.d.ts +40 -0
- package/dist/runtime/owner-grants-screen.d.ts.map +1 -0
- package/dist/runtime/owner-grants-screen.js +987 -0
- package/dist/runtime/owner-grants-screen.js.map +1 -0
- package/dist/runtime/terminal-transport.d.ts +85 -0
- package/dist/runtime/terminal-transport.d.ts.map +1 -0
- package/dist/runtime/terminal-transport.js +152 -0
- package/dist/runtime/terminal-transport.js.map +1 -0
- package/dist/sync/continuous-sync-supervisor.d.ts +107 -0
- package/dist/sync/continuous-sync-supervisor.d.ts.map +1 -0
- package/dist/sync/continuous-sync-supervisor.js +1117 -0
- package/dist/sync/continuous-sync-supervisor.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/journal.d.ts +57 -0
- package/dist/sync/journal.d.ts.map +1 -0
- package/dist/sync/journal.js +685 -0
- package/dist/sync/journal.js.map +1 -0
- package/dist/sync/revisions.d.ts +118 -0
- package/dist/sync/revisions.d.ts.map +1 -0
- package/dist/sync/revisions.js +364 -0
- package/dist/sync/revisions.js.map +1 -0
- package/dist/sync/supervisor.d.ts +107 -0
- package/dist/sync/supervisor.d.ts.map +1 -0
- package/dist/sync/supervisor.js +246 -0
- package/dist/sync/supervisor.js.map +1 -0
- package/dist/sync/workspace-sync-client.d.ts +18 -0
- package/dist/sync/workspace-sync-client.d.ts.map +1 -0
- package/dist/sync/workspace-sync-client.js +150 -0
- package/dist/sync/workspace-sync-client.js.map +1 -0
- package/dist/sync/workspace-sync-coordinator.d.ts +76 -0
- package/dist/sync/workspace-sync-coordinator.d.ts.map +1 -0
- package/dist/sync/workspace-sync-coordinator.js +621 -0
- package/dist/sync/workspace-sync-coordinator.js.map +1 -0
- package/dist/sync/workspace-sync-product-service.d.ts +79 -0
- package/dist/sync/workspace-sync-product-service.d.ts.map +1 -0
- package/dist/sync/workspace-sync-product-service.js +398 -0
- package/dist/sync/workspace-sync-product-service.js.map +1 -0
- package/dist/sync/workspace-sync-protocol.d.ts +145 -0
- package/dist/sync/workspace-sync-protocol.d.ts.map +1 -0
- package/dist/sync/workspace-sync-protocol.js +347 -0
- package/dist/sync/workspace-sync-protocol.js.map +1 -0
- package/dist/terminal/appbar.d.ts +40 -0
- package/dist/terminal/appbar.d.ts.map +1 -0
- package/dist/terminal/appbar.js +55 -0
- package/dist/terminal/appbar.js.map +1 -0
- package/dist/terminal/cell-width.d.ts +3 -0
- package/dist/terminal/cell-width.d.ts.map +1 -0
- package/dist/terminal/cell-width.js +53 -0
- package/dist/terminal/cell-width.js.map +1 -0
- package/dist/terminal/codec.d.ts +107 -0
- package/dist/terminal/codec.d.ts.map +1 -0
- package/dist/terminal/codec.js +538 -0
- package/dist/terminal/codec.js.map +1 -0
- package/dist/terminal/foreground.d.ts +135 -0
- package/dist/terminal/foreground.d.ts.map +1 -0
- package/dist/terminal/foreground.js +2097 -0
- package/dist/terminal/foreground.js.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts +26 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.js +27 -0
- package/dist/terminal/generated/terminal-wire-v1.js.map +1 -0
- package/dist/terminal/mode.d.ts +33 -0
- package/dist/terminal/mode.d.ts.map +1 -0
- package/dist/terminal/mode.js +99 -0
- package/dist/terminal/mode.js.map +1 -0
- package/dist/terminal/passthrough.d.ts +47 -0
- package/dist/terminal/passthrough.d.ts.map +1 -0
- package/dist/terminal/passthrough.js +666 -0
- package/dist/terminal/passthrough.js.map +1 -0
- package/dist/terminal/resume.d.ts +58 -0
- package/dist/terminal/resume.d.ts.map +1 -0
- package/dist/terminal/resume.js +185 -0
- package/dist/terminal/resume.js.map +1 -0
- package/dist/terminal/viewport.d.ts +93 -0
- package/dist/terminal/viewport.d.ts.map +1 -0
- package/dist/terminal/viewport.js +266 -0
- package/dist/terminal/viewport.js.map +1 -0
- package/dist/terminal/workbench.d.ts +32 -0
- package/dist/terminal/workbench.d.ts.map +1 -0
- package/dist/terminal/workbench.js +316 -0
- package/dist/terminal/workbench.js.map +1 -0
- package/dist/terminal/xterm-vte.d.ts +49 -0
- package/dist/terminal/xterm-vte.d.ts.map +1 -0
- package/dist/terminal/xterm-vte.js +602 -0
- package/dist/terminal/xterm-vte.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace/binding-path.d.ts +17 -0
- package/dist/workspace/binding-path.d.ts.map +1 -0
- package/dist/workspace/binding-path.js +91 -0
- package/dist/workspace/binding-path.js.map +1 -0
- package/dist/workspace/binding-store.d.ts +107 -0
- package/dist/workspace/binding-store.d.ts.map +1 -0
- package/dist/workspace/binding-store.js +783 -0
- package/dist/workspace/binding-store.js.map +1 -0
- package/dist/workspace/binding.d.ts +35 -0
- package/dist/workspace/binding.d.ts.map +1 -0
- package/dist/workspace/binding.js +70 -0
- package/dist/workspace/binding.js.map +1 -0
- package/dist/workspace/errors.d.ts +4 -0
- package/dist/workspace/errors.d.ts.map +1 -0
- package/dist/workspace/errors.js +19 -0
- package/dist/workspace/errors.js.map +1 -0
- package/dist/workspace/exclusion.d.ts +21 -0
- package/dist/workspace/exclusion.d.ts.map +1 -0
- package/dist/workspace/exclusion.js +243 -0
- package/dist/workspace/exclusion.js.map +1 -0
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +8 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/lifecycle.d.ts +56 -0
- package/dist/workspace/lifecycle.d.ts.map +1 -0
- package/dist/workspace/lifecycle.js +94 -0
- package/dist/workspace/lifecycle.js.map +1 -0
- package/dist/workspace/manifest.d.ts +47 -0
- package/dist/workspace/manifest.d.ts.map +1 -0
- package/dist/workspace/manifest.js +243 -0
- package/dist/workspace/manifest.js.map +1 -0
- package/dist/workspace/paths.d.ts +14 -0
- package/dist/workspace/paths.d.ts.map +1 -0
- package/dist/workspace/paths.js +67 -0
- package/dist/workspace/paths.js.map +1 -0
- package/dist/workspace/schema.d.ts +15 -0
- package/dist/workspace/schema.d.ts.map +1 -0
- package/dist/workspace/schema.js +33 -0
- package/dist/workspace/schema.js.map +1 -0
- package/dist/workspace/selection-screen.d.ts +4 -0
- package/dist/workspace/selection-screen.d.ts.map +1 -0
- package/dist/workspace/selection-screen.js +216 -0
- package/dist/workspace/selection-screen.js.map +1 -0
- package/dist/workspace/selection-service.d.ts +16 -0
- package/dist/workspace/selection-service.d.ts.map +1 -0
- package/dist/workspace/selection-service.js +65 -0
- package/dist/workspace/selection-service.js.map +1 -0
- package/node_modules/@xterm/headless/README.md +31 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js.map +1 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs.map +7 -0
- package/node_modules/@xterm/headless/package.json +29 -0
- package/node_modules/@xterm/headless/typings/xterm-headless.d.ts +1385 -0
- package/package.json +75 -0
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,1969 @@
|
|
|
1
|
+
import { observerApi } from "../api/observer-v2.js";
|
|
2
|
+
import { runObserverScreen } from "../runtime/observer-screen.js";
|
|
3
|
+
import { ownerObserveGrantsApi } from "../api/owner-observe-grants-v2.js";
|
|
4
|
+
import { runOwnerGrantsScreen } from "../runtime/owner-grants-screen.js";
|
|
5
|
+
import { ownerGrantOperationStore } from "../runtime/owner-grant-operations.js";
|
|
6
|
+
import { runProviderScreen } from "../machines/provider-screen.js";
|
|
7
|
+
import { Writable } from "node:stream";
|
|
8
|
+
import { terminalCellWidth, truncateTerminalLine } from "../terminal/cell-width.js";
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { launchRemoteWorkspaceSession } from "../journey/remote-workspace.js";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { createCunaApiClient } from "../api/client.js";
|
|
15
|
+
import { createHttpTransport } from "../api/http.js";
|
|
16
|
+
import { createBrowserOpener } from "../auth/browser.js";
|
|
17
|
+
import { createHumanAuthClient } from "../auth/human-client.js";
|
|
18
|
+
import { createHumanAuthService } from "../auth/human-session.js";
|
|
19
|
+
import { ARTIFACT_CHANNEL, packageBuildDigest, PROTOCOL_RANGE } from "../build-identity.js";
|
|
20
|
+
import { assertApiKeyUsable, ensureProfileRecorded, resolveConfig } from "../config/config.js";
|
|
21
|
+
import { executeCommand, preflightInvocation, } from "../commands/commands.js";
|
|
22
|
+
import { EXIT_CODES, normalizeError, CunaError, unsupportedError, usageError } from "../core/errors.js";
|
|
23
|
+
import { DEFAULT_REQUEST_BUDGET_MS } from "../core/observation-budget.js";
|
|
24
|
+
import { CONSOLE_ORIGIN, INTERNAL_DEFECT_HINT, automationCredentialHint, } from "../core/product-web.js";
|
|
25
|
+
import { integerArgument } from "../core/validation.js";
|
|
26
|
+
import { CredentialBoundaryError } from "../credentials/errors.js";
|
|
27
|
+
import { CredentialVault } from "../credentials/vault.js";
|
|
28
|
+
import { LocalEncryptedSessionBackend, localEncryptedSessionPaths } from "../credentials/local-session.js";
|
|
29
|
+
import { conservativeFilesystemCapabilities, createApiAgentJourneyEffects, createWorkspaceJourneyEffects, orchestrateAgentJourney, preflightAgentJourneyInvocation, } from "../journey/index.js";
|
|
30
|
+
import { rootJourneyArgv, runNodeRootJourney, } from "../journey/root-entry.js";
|
|
31
|
+
import { createPlatformAdapter } from "../platform/adapter.js";
|
|
32
|
+
import { CLI_VERSION, OUTPUT_SCHEMA_VERSION } from "../version.js";
|
|
33
|
+
import { runtimeFeatureGates } from "../runtime/contracts.js";
|
|
34
|
+
import { RuntimeBoundaryError } from "../runtime/errors.js";
|
|
35
|
+
import { runNodeForegroundSessions, selectNodeForegroundPresentation, } from "../runtime/node-foreground-session.js";
|
|
36
|
+
import { runNodeMachinesExplorer } from "../machines/explorer.js";
|
|
37
|
+
import { runWorkspaceSelectionScreen } from "../workspace/selection-screen.js";
|
|
38
|
+
import { runExecutionsScreen } from "../machines/executions-screen.js";
|
|
39
|
+
import { isOpenCodeSupervisorUpgradeReason } from "../machines/opencode-supervisor.js";
|
|
40
|
+
import { commandHelp, helpTopicName } from "./command-help.js";
|
|
41
|
+
import { FULL_HELP, ROOT_HELP } from "./help.js";
|
|
42
|
+
import { createOutputWriter, sanitizeHumanTerminalOutput } from "./output.js";
|
|
43
|
+
import { booleanOption, parseArgv, stringOption } from "./parser.js";
|
|
44
|
+
import { rejectUnknownOptions } from "./parser.js";
|
|
45
|
+
async function confirmMachineCreate(agent, signal) {
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
return false;
|
|
48
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
49
|
+
try {
|
|
50
|
+
const answer = await prompt.question(`No compatible machine is available. Create one for ${agent}? [y/N] `, { signal });
|
|
51
|
+
return answer.trim().toLocaleLowerCase("en-US") === "y" || answer.trim().toLocaleLowerCase("en-US") === "yes";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
prompt.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render the browser handoff onto the interactive terminal.
|
|
62
|
+
*
|
|
63
|
+
* Everything here goes to **stderr**, deliberately and for two reasons.
|
|
64
|
+
*
|
|
65
|
+
* It is prompt text, not a command result, so `stdout` stays clean for the
|
|
66
|
+
* structured result the command still owes its caller. And the continuation
|
|
67
|
+
* fragment is a bearer proof: on stderr it stays out of `$(cuna login)`,
|
|
68
|
+
* pipelines and redirected logs, while `run` already refuses the whole command
|
|
69
|
+
* unless stdin, stdout **and** stderr are all TTYs — so this print cannot reach
|
|
70
|
+
* a file or a pipe without that refusal firing first.
|
|
71
|
+
*
|
|
72
|
+
* Every line is passed through the same human-output sanitizer as the rest of
|
|
73
|
+
* the CLI. The URL is already constrained by `decodeCliContinuationIssued` and
|
|
74
|
+
* by `isBoundedHttpsBrowserUrl`, so this is defence in depth rather than the
|
|
75
|
+
* only control, but a print sink for service-controlled bytes is held to the
|
|
76
|
+
* same rule everywhere else in this file.
|
|
77
|
+
*/
|
|
78
|
+
export function createTerminalBrowserHandoffReporter(output) {
|
|
79
|
+
const line = (value) => {
|
|
80
|
+
output.write(`${sanitizeHumanTerminalOutput(value)}\n`);
|
|
81
|
+
};
|
|
82
|
+
// Three facts, one line each: where to go, what to do there, and — only when
|
|
83
|
+
// it happened — what went wrong. Everything the longer copy added was
|
|
84
|
+
// description of the link ("single-use") or of the mechanism ("in your
|
|
85
|
+
// browser", "automatically"), which the person is not deciding anything with
|
|
86
|
+
// while a terminal waits on them.
|
|
87
|
+
const reporter = {
|
|
88
|
+
continuationUrl(url) {
|
|
89
|
+
line("");
|
|
90
|
+
line("Sign in to Cuna:");
|
|
91
|
+
line("");
|
|
92
|
+
line(` ${url}`);
|
|
93
|
+
line("");
|
|
94
|
+
},
|
|
95
|
+
browserOpened() {
|
|
96
|
+
line("Opened your browser. Approve there, then paste the code below.");
|
|
97
|
+
},
|
|
98
|
+
browserOpenFailed() {
|
|
99
|
+
line("Could not open a browser. Open the link above, then paste the code below.");
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
return Object.freeze(reporter);
|
|
103
|
+
}
|
|
104
|
+
function loginCodeInputError(code, message) {
|
|
105
|
+
return new CunaError({
|
|
106
|
+
code: `cuna.auth.login_code_input_${code}`,
|
|
107
|
+
message,
|
|
108
|
+
exitCode: EXIT_CODES.auth,
|
|
109
|
+
hint: code === "unavailable"
|
|
110
|
+
? "Run `cuna login` from an interactive terminal that supports hidden input."
|
|
111
|
+
: "Run `cuna login` again and paste the complete cuna_login_ code.",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The control sequences several terminals wrap a paste in while raw mode is
|
|
116
|
+
* active. They are input framing, not pasted content: `finish` strips them from
|
|
117
|
+
* the value and `maskedLength` excludes them from the echo, so the number of
|
|
118
|
+
* mask characters on screen equals the number of credential bytes accepted.
|
|
119
|
+
*/
|
|
120
|
+
const BRACKETED_PASTE_START = `${String.fromCharCode(0x1b)}[200~`;
|
|
121
|
+
const BRACKETED_PASTE_END = `${String.fromCharCode(0x1b)}[201~`;
|
|
122
|
+
/** One mask character stands for one accepted byte. */
|
|
123
|
+
const LOGIN_CODE_MASK = "*";
|
|
124
|
+
/**
|
|
125
|
+
* How many accepted bytes the person should currently see masked — the buffer
|
|
126
|
+
* length minus whichever bracketed-paste markers are present. Derived from the
|
|
127
|
+
* same buffer `finish` reads, so the count on screen cannot drift from the
|
|
128
|
+
* value that will be submitted.
|
|
129
|
+
*/
|
|
130
|
+
function maskedLength(bytes) {
|
|
131
|
+
const raw = Buffer.from(bytes).toString("binary");
|
|
132
|
+
let length = bytes.length;
|
|
133
|
+
if (raw.startsWith(BRACKETED_PASTE_START))
|
|
134
|
+
length -= BRACKETED_PASTE_START.length;
|
|
135
|
+
if (raw.endsWith(BRACKETED_PASTE_END))
|
|
136
|
+
length -= BRACKETED_PASTE_END.length;
|
|
137
|
+
return length < 0 ? 0 : length;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Read the reusable browser login code without writing its bytes to terminal
|
|
141
|
+
* output. `readline.question` echoes pasted text, which is unacceptable for a
|
|
142
|
+
* credential retained in the encrypted local session store. Raw TTY input is
|
|
143
|
+
* the narrowest portable Node primitive that gives this command that boundary.
|
|
144
|
+
*
|
|
145
|
+
* This intentionally refuses pipes and terminals without `setRawMode`: a
|
|
146
|
+
* process that cannot suppress echo must not accept the durable code at all.
|
|
147
|
+
*
|
|
148
|
+
* Suppressing the bytes is not the same as suppressing all feedback. For a
|
|
149
|
+
* TYPED password an empty prompt is correct, because the person knows what they
|
|
150
|
+
* pressed. For a PASTED high-entropy code it is a dead end: nothing on screen
|
|
151
|
+
* distinguishes "the clipboard was empty", "the paste arrived", and "the paste
|
|
152
|
+
* arrived twice", so the only way to find out is to submit and read the error.
|
|
153
|
+
* So each accepted byte echoes one mask character and a backspace erases one,
|
|
154
|
+
* which keeps the display a truthful count of what will be submitted. That
|
|
155
|
+
* reveals the code's LENGTH and nothing else, and the length is already fixed
|
|
156
|
+
* and public in the `cuna_login_` format.
|
|
157
|
+
*/
|
|
158
|
+
export async function readHiddenLoginCode(input, output, signal) {
|
|
159
|
+
if (input.isTTY !== true || typeof input.setRawMode !== "function") {
|
|
160
|
+
throw loginCodeInputError("unavailable", "Cuna cannot safely accept a login code from this input.");
|
|
161
|
+
}
|
|
162
|
+
if (signal?.aborted)
|
|
163
|
+
throw loginCodeInputError("cancelled", "Cuna sign-in was cancelled.");
|
|
164
|
+
const wasRaw = input.isRaw === true;
|
|
165
|
+
// `resume()` below refs the terminal handle. Remember whether another
|
|
166
|
+
// consumer was already flowing so a completed login does not leave stdin
|
|
167
|
+
// keeping the whole CLI process alive after the success message.
|
|
168
|
+
const wasFlowing = input.readableFlowing === true;
|
|
169
|
+
const bytes = [];
|
|
170
|
+
const maxBytes = 256;
|
|
171
|
+
let masked = 0;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
let settled = false;
|
|
174
|
+
/**
|
|
175
|
+
* Reconcile the echo with the buffer instead of emitting per keystroke. A
|
|
176
|
+
* paste can be split across chunks and its closing marker only becomes
|
|
177
|
+
* recognizable on the last one, so the mask count is recomputed from the
|
|
178
|
+
* whole buffer rather than incremented as bytes arrive.
|
|
179
|
+
*/
|
|
180
|
+
const renderMask = () => {
|
|
181
|
+
const target = maskedLength(bytes);
|
|
182
|
+
if (target > masked)
|
|
183
|
+
output.write(LOGIN_CODE_MASK.repeat(target - masked));
|
|
184
|
+
else if (target < masked)
|
|
185
|
+
output.write("\b \b".repeat(masked - target));
|
|
186
|
+
masked = target;
|
|
187
|
+
};
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
input.off("data", onData);
|
|
190
|
+
input.off("error", onError);
|
|
191
|
+
input.off("end", onEnd);
|
|
192
|
+
signal?.removeEventListener("abort", onAbort);
|
|
193
|
+
try {
|
|
194
|
+
input.setRawMode?.(wasRaw);
|
|
195
|
+
}
|
|
196
|
+
catch { /* best-effort terminal restoration */ }
|
|
197
|
+
if (!wasFlowing)
|
|
198
|
+
input.pause();
|
|
199
|
+
output.write("\n");
|
|
200
|
+
bytes.fill(0);
|
|
201
|
+
};
|
|
202
|
+
const settle = (outcome) => {
|
|
203
|
+
if (settled)
|
|
204
|
+
return;
|
|
205
|
+
settled = true;
|
|
206
|
+
cleanup();
|
|
207
|
+
if ("error" in outcome)
|
|
208
|
+
reject(outcome.error);
|
|
209
|
+
else
|
|
210
|
+
resolve(outcome.value);
|
|
211
|
+
};
|
|
212
|
+
const finish = () => {
|
|
213
|
+
const raw = Buffer.from(bytes).toString("utf8");
|
|
214
|
+
// Accept exactly the bracketed-paste pair, never arbitrary escapes.
|
|
215
|
+
const withoutPasteStart = raw.startsWith(BRACKETED_PASTE_START)
|
|
216
|
+
? raw.slice(BRACKETED_PASTE_START.length)
|
|
217
|
+
: raw;
|
|
218
|
+
const value = withoutPasteStart.endsWith(BRACKETED_PASTE_END)
|
|
219
|
+
? withoutPasteStart.slice(0, -BRACKETED_PASTE_END.length)
|
|
220
|
+
: withoutPasteStart;
|
|
221
|
+
settle({ value });
|
|
222
|
+
};
|
|
223
|
+
const onAbort = () => settle({ error: loginCodeInputError("cancelled", "Cuna sign-in was cancelled.") });
|
|
224
|
+
const onError = () => settle({ error: loginCodeInputError("unavailable", "Cuna could not safely read the login code.") });
|
|
225
|
+
const onEnd = () => settle({ error: loginCodeInputError("cancelled", "Cuna did not receive a login code.") });
|
|
226
|
+
const onData = (chunk) => {
|
|
227
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
228
|
+
for (const byte of data) {
|
|
229
|
+
if (byte === 0x03)
|
|
230
|
+
return onAbort(); // Ctrl+C
|
|
231
|
+
if (byte === 0x04)
|
|
232
|
+
return onEnd(); // Ctrl+D
|
|
233
|
+
// Render before finishing. A clipboard whose contents end in a newline
|
|
234
|
+
// delivers the code and the Enter in ONE chunk, so returning straight
|
|
235
|
+
// into `finish` here would submit a paste that was never drawn — the
|
|
236
|
+
// exact no-feedback case this mask exists to remove.
|
|
237
|
+
if (byte === 0x0d || byte === 0x0a) {
|
|
238
|
+
renderMask();
|
|
239
|
+
return finish();
|
|
240
|
+
}
|
|
241
|
+
if (byte === 0x08 || byte === 0x7f) {
|
|
242
|
+
bytes.pop();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (bytes.length >= maxBytes) {
|
|
246
|
+
return settle({ error: loginCodeInputError("too_long", "The pasted Cuna login code is too long.") });
|
|
247
|
+
}
|
|
248
|
+
bytes.push(byte);
|
|
249
|
+
}
|
|
250
|
+
renderMask();
|
|
251
|
+
};
|
|
252
|
+
try {
|
|
253
|
+
input.setRawMode(true);
|
|
254
|
+
// The host and the `cuna_login_` prefix were both already on screen —
|
|
255
|
+
// the host in the link three lines up, the prefix in the code the person
|
|
256
|
+
// is holding in their clipboard. "(input hidden)" stays because it is
|
|
257
|
+
// still true and still the thing worth saying: the mask below shows the
|
|
258
|
+
// length, never the bytes.
|
|
259
|
+
output.write("Paste the login code (input hidden): ");
|
|
260
|
+
input.on("data", onData);
|
|
261
|
+
input.once("error", onError);
|
|
262
|
+
input.once("end", onEnd);
|
|
263
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
264
|
+
// AbortSignal does not replay an abort that happened immediately before
|
|
265
|
+
// listener registration (for example while raw mode is being enabled).
|
|
266
|
+
// Recheck before resuming input so hidden-code input never waits for a
|
|
267
|
+
// second user action after cancellation.
|
|
268
|
+
if (signal?.aborted === true)
|
|
269
|
+
return onAbort();
|
|
270
|
+
input.resume();
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
settle({ error: loginCodeInputError("unavailable", "Cuna cannot safely prepare hidden login-code input.") });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function promptLoginCode(signal) {
|
|
278
|
+
return readHiddenLoginCode(process.stdin, process.stderr, signal);
|
|
279
|
+
}
|
|
280
|
+
function defaultStreams() {
|
|
281
|
+
return Object.freeze({
|
|
282
|
+
stdout: process.stdout,
|
|
283
|
+
stderr: process.stderr,
|
|
284
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
285
|
+
stdinIsTTY: process.stdin.isTTY === true,
|
|
286
|
+
stderrIsTTY: process.stderr.isTTY === true,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The caller's explicit per-request budget, or `undefined` when they gave none.
|
|
291
|
+
*
|
|
292
|
+
* This used to return the literal `15_000` for an absent flag, which erased the
|
|
293
|
+
* difference between "the user chose 15 seconds" and "the user chose nothing" —
|
|
294
|
+
* and with it any possibility of an operation declaring a budget of its own.
|
|
295
|
+
* `machines create` waits ~50 s on the producer and was cut off at 15 s by a
|
|
296
|
+
* default nobody had asked for, then told the user the network had timed out.
|
|
297
|
+
* The default now lives in `core/observation-budget.ts` and is applied by the
|
|
298
|
+
* transport, after a per-operation budget has had its chance.
|
|
299
|
+
*/
|
|
300
|
+
function parseTimeout(raw) {
|
|
301
|
+
if (raw === undefined)
|
|
302
|
+
return undefined;
|
|
303
|
+
return integerArgument(raw, "timeout-ms", 100, 120_000);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The `command` field of every output record, including error records.
|
|
307
|
+
*
|
|
308
|
+
* This used to be "the first argv token that does not start with `-`", which is
|
|
309
|
+
* an option VALUE whenever a global option precedes the command:
|
|
310
|
+
* `cuna --config-file /home/me/.cuna/config.toml machines list` reported the
|
|
311
|
+
* command as `/home/me/.cuna/config.toml`, putting an absolute filesystem path
|
|
312
|
+
* into a JSON record and into whatever consumes it. The parser already knows
|
|
313
|
+
* which token is the command, so ask it; if the argv is too malformed to parse,
|
|
314
|
+
* there is no command to name.
|
|
315
|
+
*/
|
|
316
|
+
function commandLabel(argv) {
|
|
317
|
+
try {
|
|
318
|
+
return parseArgv(argv).command ?? "root";
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return "root";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function menuInvocationOptions(parsed) {
|
|
325
|
+
const args = [];
|
|
326
|
+
for (const name of ["profile", "base-url", "config-file", "timeout-ms"]) {
|
|
327
|
+
const value = stringOption(parsed, name);
|
|
328
|
+
if (value !== undefined)
|
|
329
|
+
args.push(`--${name}`, value);
|
|
330
|
+
}
|
|
331
|
+
if (booleanOption(parsed, "no-color"))
|
|
332
|
+
args.push("--no-color");
|
|
333
|
+
return args;
|
|
334
|
+
}
|
|
335
|
+
function humanResult(result) {
|
|
336
|
+
return Object.freeze({
|
|
337
|
+
profile: result.profile,
|
|
338
|
+
session_id: result.sessionId,
|
|
339
|
+
required_terms_version: result.context.requiredTermsVersion,
|
|
340
|
+
identity: result.context.identity,
|
|
341
|
+
admission: result.context.admission,
|
|
342
|
+
workspace: {
|
|
343
|
+
state: result.context.workspace.state,
|
|
344
|
+
...(result.context.workspace.id === undefined ? {} : { id: result.context.workspace.id }),
|
|
345
|
+
},
|
|
346
|
+
...(result.storageMode === undefined ? {} : { storage_mode: result.storageMode }),
|
|
347
|
+
...(result.context.waitlistPosition === undefined ? {} : { waitlist_position: result.context.waitlistPosition }),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function needsRemoteCredential(command, foreground) {
|
|
351
|
+
return command === "observe" || command === "share" || command === "capabilities" || command === "machines" || command === "agent-sessions" ||
|
|
352
|
+
command === "agent" || command === "executions" ||
|
|
353
|
+
command === "records" || command === "authorizations" || command === "api-keys" ||
|
|
354
|
+
command === "account" || command === "workspace" || command === "usage" ||
|
|
355
|
+
command === "claude" || command === "codex" || command === "opencode" || foreground !== undefined;
|
|
356
|
+
}
|
|
357
|
+
function managesInteractiveSession(command) {
|
|
358
|
+
return command === "signup" || command === "login" || command === "logout" ||
|
|
359
|
+
command === "whoami" || command === "access";
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Whether this invocation selects a credential authority — either by presenting
|
|
363
|
+
* a credential to the API, or because the presence of an automation credential
|
|
364
|
+
* changes what the command does.
|
|
365
|
+
*
|
|
366
|
+
* This is the blast radius of a broken environment credential. Everything
|
|
367
|
+
* outside it — `doctor`, `self-test --offline`, `config get`, `version`,
|
|
368
|
+
* `help`, and the fail-closed reserved commands — reads no credential, so an
|
|
369
|
+
* unusable one must not stop it. It used to, because the refusal lived inside
|
|
370
|
+
* `resolveConfig`, which runs before dispatch: a failed
|
|
371
|
+
* `export CUNA_API_KEY=$(fetch-secret)` disabled the two commands whose entire
|
|
372
|
+
* purpose is diagnosing a broken environment.
|
|
373
|
+
*/
|
|
374
|
+
function usesCredentialAuthority(command, foreground) {
|
|
375
|
+
return managesInteractiveSession(command) || needsRemoteCredential(command, foreground);
|
|
376
|
+
}
|
|
377
|
+
function foregroundSelection(parsed) {
|
|
378
|
+
if (parsed.command === "connect")
|
|
379
|
+
return Object.freeze({ agentSessionIds: parsed.operands });
|
|
380
|
+
if (parsed.command === "agent-sessions" && parsed.operands[0] === "attach") {
|
|
381
|
+
return Object.freeze({ agentSessionIds: parsed.operands.slice(1) });
|
|
382
|
+
}
|
|
383
|
+
const expectedAgent = parsed.command === "claude"
|
|
384
|
+
? "claude-code"
|
|
385
|
+
: parsed.command === "codex"
|
|
386
|
+
? "codex"
|
|
387
|
+
: parsed.command === "opencode"
|
|
388
|
+
? "opencode"
|
|
389
|
+
: undefined;
|
|
390
|
+
const agentSessionId = stringOption(parsed, "agent-session");
|
|
391
|
+
if (expectedAgent !== undefined && agentSessionId !== undefined) {
|
|
392
|
+
return Object.freeze({
|
|
393
|
+
agentSessionIds: Object.freeze([agentSessionId]),
|
|
394
|
+
expectedAgentKinds: Object.freeze([expectedAgent]),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
function nodePlatform(kind) {
|
|
400
|
+
return kind === "windows" ? "win32" : kind === "macos" ? "darwin" : "linux";
|
|
401
|
+
}
|
|
402
|
+
function platformHomeDirectory(environment, platform) {
|
|
403
|
+
const candidate = platform === "win32" ? environment.USERPROFILE : environment.HOME;
|
|
404
|
+
return typeof candidate === "string" && candidate.trim() !== "" ? candidate : undefined;
|
|
405
|
+
}
|
|
406
|
+
function sameHostPath(left, right, platform) {
|
|
407
|
+
const canonicalLeft = resolve(left);
|
|
408
|
+
const canonicalRight = resolve(right);
|
|
409
|
+
return platform === "win32"
|
|
410
|
+
? canonicalLeft.toLocaleLowerCase("en-US") === canonicalRight.toLocaleLowerCase("en-US")
|
|
411
|
+
: canonicalLeft === canonicalRight;
|
|
412
|
+
}
|
|
413
|
+
function managedWorkspaceScope(intent, machineId) {
|
|
414
|
+
if (machineId !== undefined) {
|
|
415
|
+
const digest = createHash("sha256").update(machineId, "utf8").digest("hex").slice(0, 16);
|
|
416
|
+
return `machine-${digest}`;
|
|
417
|
+
}
|
|
418
|
+
if (intent.machine.kind === "exact-name") {
|
|
419
|
+
const digest = createHash("sha256").update(intent.machine.name, "utf8").digest("hex").slice(0, 16);
|
|
420
|
+
return `machine-${digest}`;
|
|
421
|
+
}
|
|
422
|
+
return `${intent.agent}-${intent.machine.kind}`;
|
|
423
|
+
}
|
|
424
|
+
function agentDisplayName(agent) {
|
|
425
|
+
return agent === "claude-code" ? "Claude Code"
|
|
426
|
+
: agent === "codex" ? "Codex"
|
|
427
|
+
: agent === "opencode" ? "OpenCode"
|
|
428
|
+
: "OpenClaw";
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* `doctor` must distinguish two independent facts: whether this host can
|
|
432
|
+
* protect a durable login-code envelope, and whether the selected deployment
|
|
433
|
+
* currently serves the anonymous browser-login bootstrap. The latter is an
|
|
434
|
+
* opt-in network check so the normal diagnostic stays safe and offline.
|
|
435
|
+
*/
|
|
436
|
+
async function probeBrowserLoginRemote(input) {
|
|
437
|
+
const transportOptions = {
|
|
438
|
+
baseUrl: input.config.baseUrl,
|
|
439
|
+
timeoutMs: input.timeoutMs,
|
|
440
|
+
...(input.fetch === undefined ? {} : { fetch: input.fetch }),
|
|
441
|
+
};
|
|
442
|
+
try {
|
|
443
|
+
const bootstrap = await createHumanAuthClient({
|
|
444
|
+
anonymous: createHttpTransport(transportOptions),
|
|
445
|
+
authenticated: (accessToken) => createHttpTransport({ ...transportOptions, bearerToken: accessToken }),
|
|
446
|
+
}).bootstrap(input.signal);
|
|
447
|
+
return bootstrap.enabled
|
|
448
|
+
? Object.freeze({ status: "verified", reason: "remote_browser_login_bootstrap_verified" })
|
|
449
|
+
: Object.freeze({ status: "unavailable", reason: "remote_browser_login_disabled" });
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// The exact transport/decoder error is intentionally not copied into a
|
|
453
|
+
// public diagnostic. It might contain an untrusted endpoint response, and
|
|
454
|
+
// this gate only needs to say that a fresh bootstrap could not be proven.
|
|
455
|
+
return Object.freeze({ status: "unknown", reason: "remote_browser_login_probe_failed" });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function runtimeError(error) {
|
|
459
|
+
const exitCode = error.code === "session_conflict"
|
|
460
|
+
? EXIT_CODES.conflict
|
|
461
|
+
: error.code === "capability_unsupported" || error.code === "control_plane_unavailable" || error.code === "pty_unavailable"
|
|
462
|
+
? EXIT_CODES.unsupported
|
|
463
|
+
: error.code === "capability_unavailable" || error.code === "terminal_disconnected" || error.code === "terminal_timeout"
|
|
464
|
+
? EXIT_CODES.network
|
|
465
|
+
: error.code.startsWith("capability_") || error.code.startsWith("grant_") || error.code === "remote_state_unproven"
|
|
466
|
+
? EXIT_CODES.policy
|
|
467
|
+
: EXIT_CODES.remote;
|
|
468
|
+
return new CunaError({
|
|
469
|
+
code: `cuna.runtime.${error.code}`,
|
|
470
|
+
message: error.message,
|
|
471
|
+
exitCode,
|
|
472
|
+
...(error.code === "terminal_history_gap" && typeof error.safeDetails?.agent_session_id === "string" &&
|
|
473
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(error.safeDetails.agent_session_id)
|
|
474
|
+
? { hint: `Inspect this session with \`cuna agent-sessions get ${error.safeDetails.agent_session_id}\`.` }
|
|
475
|
+
: {}),
|
|
476
|
+
retryable: error.retryable,
|
|
477
|
+
...(error.safeDetails === undefined ? {} : { details: error.safeDetails }),
|
|
478
|
+
cause: error,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* A credential refresh that failed because the request did — a 429, a 5xx, a
|
|
483
|
+
* timeout — is not an auth failure. Reporting it as one exits `auth`, which
|
|
484
|
+
* this CLI documents as "no usable credential", and sends a person to
|
|
485
|
+
* `cuna login` to fix something that clears on its own. The exit-code contract
|
|
486
|
+
* already promises that "HTTP 429 and 5xx arrive as `cuna.network.rate_limited`
|
|
487
|
+
* and `cuna.network.service_unavailable`"; this keeps that promise across the
|
|
488
|
+
* credential boundary, where the class used to be overwritten.
|
|
489
|
+
*
|
|
490
|
+
* Every authenticated command re-exchanges the stored login code, and the
|
|
491
|
+
* server allows ten exchanges per rolling minute, so the eleventh command in a
|
|
492
|
+
* minute lands here. Say that, rather than doubting the credential.
|
|
493
|
+
*/
|
|
494
|
+
function credentialError(error) {
|
|
495
|
+
const reason = error.safeDetails?.["reason"];
|
|
496
|
+
const transport = typeof reason === "string" &&
|
|
497
|
+
(reason.startsWith("cuna.network.") || reason.startsWith("cuna.client."))
|
|
498
|
+
? reason
|
|
499
|
+
: undefined;
|
|
500
|
+
if (transport !== undefined) {
|
|
501
|
+
return new CunaError({
|
|
502
|
+
code: transport,
|
|
503
|
+
message: "Cuna could not renew this session because the request did not complete.",
|
|
504
|
+
exitCode: EXIT_CODES.network,
|
|
505
|
+
retryable: true,
|
|
506
|
+
hint: transport === "cuna.network.rate_limited"
|
|
507
|
+
? "This account exchanged its sign-in too many times in the last minute. Wait a minute and run the command again. The stored session is unchanged and `cuna login` is not needed."
|
|
508
|
+
: "Run the command again. The stored session is unchanged.",
|
|
509
|
+
details: { reason: transport },
|
|
510
|
+
cause: error,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return new CunaError({
|
|
514
|
+
code: `cuna.auth.${error.code}`,
|
|
515
|
+
message: error.message,
|
|
516
|
+
exitCode: EXIT_CODES.auth,
|
|
517
|
+
retryable: error.retryable,
|
|
518
|
+
// `RuntimeBoundaryError` already forwards its safe details; this arm
|
|
519
|
+
// dropped them, so the credential backend's reason died here even
|
|
520
|
+
// when the vault had populated it.
|
|
521
|
+
...(error.safeDetails === undefined ? {} : { details: error.safeDetails }),
|
|
522
|
+
cause: error,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const INLINE_CLOSE_FRAME_MS = 90;
|
|
526
|
+
const INLINE_CLOSE_FRAMES = Object.freeze(["✦ Closing Cuna...", "✧ Closing Cuna...", "✓ Closed."]);
|
|
527
|
+
function waitForUiFrame(milliseconds) {
|
|
528
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
529
|
+
}
|
|
530
|
+
async function animateInlineClose(stream, color) {
|
|
531
|
+
for (const [index, frame] of INLINE_CLOSE_FRAMES.entries()) {
|
|
532
|
+
const styled = color
|
|
533
|
+
? index === INLINE_CLOSE_FRAMES.length - 1
|
|
534
|
+
? `\u001b[38;5;42m\u001b[1m${frame}\u001b[0m`
|
|
535
|
+
: `\u001b[38;5;202m${frame}\u001b[0m`
|
|
536
|
+
: frame;
|
|
537
|
+
stream.write(`\r\u001b[2K${styled}`);
|
|
538
|
+
await waitForUiFrame(INLINE_CLOSE_FRAME_MS);
|
|
539
|
+
}
|
|
540
|
+
stream.write("\n");
|
|
541
|
+
}
|
|
542
|
+
function isTerminalResumeHandleConflict(error) {
|
|
543
|
+
return error.code === "cuna.remote.conflict" &&
|
|
544
|
+
error.details?.reason === "terminal_connection_resume_handle_conflict";
|
|
545
|
+
}
|
|
546
|
+
function terminalSupervisorReadiness(error) {
|
|
547
|
+
if (error.code !== "cuna.runtime.capability_unknown" &&
|
|
548
|
+
error.code !== "cuna.runtime.capability_unavailable")
|
|
549
|
+
return undefined;
|
|
550
|
+
const reason = error.details?.reason_code;
|
|
551
|
+
// The process this AgentSession named is gone for good (the Machine
|
|
552
|
+
// restarted, or the owner could not be recovered). Nothing can still
|
|
553
|
+
// arrive, so this is BLOCKED with a route, never a "try again in a moment".
|
|
554
|
+
if (reason === "terminal_owner_unrecoverable")
|
|
555
|
+
return "ended";
|
|
556
|
+
if (isOpenCodeSupervisorUpgradeReason(reason))
|
|
557
|
+
return "upgrade_required";
|
|
558
|
+
if (reason === "runtime_lease_expired")
|
|
559
|
+
return "lease_expired";
|
|
560
|
+
if (reason === "supervisor_registry_unavailable")
|
|
561
|
+
return "waiting";
|
|
562
|
+
// A capability-abstention at this exact boundary is not an actionable CLI
|
|
563
|
+
// error for a person. The server has declined to prove that it can mint an
|
|
564
|
+
// attached terminal; Cuna must leave the AgentSession alone and explain the
|
|
565
|
+
// transient state without leaking the internal capability vocabulary.
|
|
566
|
+
if (error.details?.capability_id === "terminal_connections.create")
|
|
567
|
+
return "unverified";
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
function writeTerminalReconnectConflict(stream, color) {
|
|
571
|
+
const accent = (value) => color ? `\u001b[38;5;202m\u001b[1m${value}\u001b[0m` : value;
|
|
572
|
+
const success = (value) => color ? `\u001b[38;5;42m${value}\u001b[0m` : value;
|
|
573
|
+
stream.write(`${accent("◆ CUNA")} Terminal connection changed\n`);
|
|
574
|
+
stream.write("The previous terminal link was already replaced.\n");
|
|
575
|
+
stream.write(`${success("Cuna did not stop the remote AgentSession.")}\n`);
|
|
576
|
+
stream.write("Run `cuna` again to reconnect.\n");
|
|
577
|
+
}
|
|
578
|
+
function writeTerminalSupervisorReadiness(stream, color, state, sessionIds) {
|
|
579
|
+
const accent = (value) => color ? `\u001b[38;5;202m\u001b[1m${value}\u001b[0m` : value;
|
|
580
|
+
const inspection = () => {
|
|
581
|
+
for (const id of sessionIds.slice(0, 4)) {
|
|
582
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(id)) {
|
|
583
|
+
stream.write(`Inspect: cuna agent-sessions get ${id}\n`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
if (state === "ended") {
|
|
588
|
+
stream.write(`${accent("◆ CUNA")} This AgentSession's terminal cannot be recovered\n`);
|
|
589
|
+
stream.write("The exact process or retained terminal is no longer available.\n");
|
|
590
|
+
inspection();
|
|
591
|
+
stream.write("Start a fresh one with `cuna <claude|codex|opencode> --new-session`.\n");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (state === "upgrade_required") {
|
|
595
|
+
stream.write(`${accent("◆ CUNA")} Machine terminal update needed\n`);
|
|
596
|
+
stream.write("This machine needs its terminal supervisor updated before it can attach.\n");
|
|
597
|
+
}
|
|
598
|
+
else if (state === "lease_expired") {
|
|
599
|
+
stream.write(`${accent("◆ CUNA")} AgentSession needs a fresh runtime check\n`);
|
|
600
|
+
stream.write("The machine has not recently confirmed that this selected AgentSession is still running.\n");
|
|
601
|
+
}
|
|
602
|
+
else if (state === "unverified") {
|
|
603
|
+
stream.write(`${accent("◆ CUNA")} Terminal connection not ready\n`);
|
|
604
|
+
stream.write("Cuna could not verify this machine's terminal authority yet.\n");
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
stream.write(`${accent("◆ CUNA")} Machine terminal supervisor unavailable\n`);
|
|
608
|
+
stream.write("Cuna could not verify live terminal control for this AgentSession.\n");
|
|
609
|
+
}
|
|
610
|
+
// This error can follow a redeemed connection and an automatic retry. Its
|
|
611
|
+
// capability reason cannot prove that no earlier connection or effect exists.
|
|
612
|
+
stream.write("Cuna could not complete this terminal attachment.\n");
|
|
613
|
+
inspection();
|
|
614
|
+
if (state === "unverified" || state === "waiting") {
|
|
615
|
+
stream.write("Check this AgentSession's current state before retrying; it may have ended.\n");
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
stream.write(state === "lease_expired"
|
|
619
|
+
? "Wait for a fresh runtime observation, then open this same AgentSession again.\n"
|
|
620
|
+
: "After the stopped-machine supervisor update, open this same AgentSession again.\n");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function inlineProgressColumns(stream) {
|
|
624
|
+
const tty = stream;
|
|
625
|
+
// Node 24 on Windows can retain stale public columns after ConPTY resize.
|
|
626
|
+
// Read this stream's native TTY observation without changing its prototype
|
|
627
|
+
// or cached fields. This guarded private API depends on the supported Node
|
|
628
|
+
// engine; absent/failed/malformed observations retain the ordinary fallback.
|
|
629
|
+
if (process.platform === "win32" && typeof tty._handle?.getWindowSize === "function") {
|
|
630
|
+
try {
|
|
631
|
+
const size = [];
|
|
632
|
+
if (tty._handle.getWindowSize(size) === 0 && size.length === 2 &&
|
|
633
|
+
Number.isSafeInteger(size[0]) && size[0] >= 2 && size[0] <= 4096 &&
|
|
634
|
+
Number.isSafeInteger(size[1]) && size[1] >= 1 && size[1] <= 4096)
|
|
635
|
+
return size[0];
|
|
636
|
+
}
|
|
637
|
+
catch { /* An unavailable native observation does not break progress. */ }
|
|
638
|
+
}
|
|
639
|
+
return Number.isSafeInteger(tty.columns) && tty.columns >= 2 && tty.columns <= 4096 ? tty.columns : 80;
|
|
640
|
+
}
|
|
641
|
+
function startInlineProgress(stream, color, initialLabel = "Loading machines") {
|
|
642
|
+
const frames = ["◐", "◓", "◑", "◒"];
|
|
643
|
+
const bars = ["━╺━━━━", "━━╺━━━", "━━━╺━━", "━━━━╺━", "━━━━━╺", "━━━━╸━", "━━━╸━━", "━━╸━━━"];
|
|
644
|
+
const startedAt = Date.now();
|
|
645
|
+
let frame = 0;
|
|
646
|
+
let label = initialLabel;
|
|
647
|
+
let stopped = false;
|
|
648
|
+
let lastColumns = 0;
|
|
649
|
+
let lastCells = 0;
|
|
650
|
+
const paint = () => {
|
|
651
|
+
const columns = inlineProgressColumns(stream);
|
|
652
|
+
const elapsed = Date.now() - startedAt;
|
|
653
|
+
// A spinner alone is too easy to mistake for a frozen cursor on slower
|
|
654
|
+
// Windows terminals. Keep the phase honest, then add a small, actionable
|
|
655
|
+
// acknowledgement while an authenticated read is still in flight.
|
|
656
|
+
const slowHint = elapsed >= 12_000
|
|
657
|
+
? " · still working — Ctrl-C cancels"
|
|
658
|
+
: elapsed >= 4_000
|
|
659
|
+
? " · still working"
|
|
660
|
+
: "";
|
|
661
|
+
const text = `◆ CUNA ${frames[frame % frames.length]} ${label}${slowHint} ${bars[frame % bars.length]}`;
|
|
662
|
+
const fitted = truncateTerminalLine(text, columns - 1);
|
|
663
|
+
const styled = color && fitted === text
|
|
664
|
+
? `\u001b[38;5;202m\u001b[1m◆ CUNA\u001b[0m \u001b[38;5;202m${frames[frame % frames.length]}\u001b[0m \u001b[38;5;255m\u001b[1m${label}\u001b[0m\u001b[38;5;245m${slowHint}\u001b[0m \u001b[38;5;208m${bars[frame % bars.length]}\u001b[0m`
|
|
665
|
+
: color ? `\u001b[38;5;255m${fitted}\u001b[0m` : fitted;
|
|
666
|
+
if (lastColumns > columns && lastCells >= columns) {
|
|
667
|
+
// A terminal resize can reflow our previously single row before repaint.
|
|
668
|
+
for (let row = 0; row < Math.floor(lastCells / columns); row += 1)
|
|
669
|
+
stream.write("\r\u001b[2K\u001b[1A");
|
|
670
|
+
}
|
|
671
|
+
stream.write(`\r\u001b[2K${styled}`);
|
|
672
|
+
lastColumns = columns;
|
|
673
|
+
lastCells = terminalCellWidth(fitted);
|
|
674
|
+
frame += 1;
|
|
675
|
+
};
|
|
676
|
+
paint();
|
|
677
|
+
const timer = setInterval(paint, 90);
|
|
678
|
+
timer.unref();
|
|
679
|
+
return Object.freeze({
|
|
680
|
+
update(nextLabel) {
|
|
681
|
+
if (stopped || nextLabel === label)
|
|
682
|
+
return;
|
|
683
|
+
label = nextLabel;
|
|
684
|
+
paint();
|
|
685
|
+
},
|
|
686
|
+
note(line) {
|
|
687
|
+
if (stopped) {
|
|
688
|
+
stream.write(`${line}\n`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// Clear the spinner row, leave the line behind, resume spinning below it.
|
|
692
|
+
stream.write(`\r${String.fromCharCode(0x1b)}[2K${line}\n`);
|
|
693
|
+
// The previous row now belongs to the persistent note, not the spinner.
|
|
694
|
+
lastColumns = 0;
|
|
695
|
+
lastCells = 0;
|
|
696
|
+
paint();
|
|
697
|
+
},
|
|
698
|
+
stop() {
|
|
699
|
+
if (stopped)
|
|
700
|
+
return;
|
|
701
|
+
stopped = true;
|
|
702
|
+
clearInterval(timer);
|
|
703
|
+
stream.write("\r\u001b[2K");
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
// Which batch command deserves a progress line, and what it should say. The
|
|
708
|
+
// label names the task, never the transport (PRD-PM-008 D5-R18). Commands that
|
|
709
|
+
// answer from disk are excluded: a spinner that appears for three milliseconds
|
|
710
|
+
// is noise.
|
|
711
|
+
function batchProgressLabel(parsed) {
|
|
712
|
+
const action = parsed.operands[0];
|
|
713
|
+
switch (parsed.command) {
|
|
714
|
+
case "machines":
|
|
715
|
+
switch (action) {
|
|
716
|
+
case undefined:
|
|
717
|
+
case "overview":
|
|
718
|
+
case "list":
|
|
719
|
+
return "Loading machines";
|
|
720
|
+
case "create":
|
|
721
|
+
return "Creating machine";
|
|
722
|
+
case "start":
|
|
723
|
+
return "Starting machine";
|
|
724
|
+
case "stop":
|
|
725
|
+
return "Stopping machine";
|
|
726
|
+
case "pause":
|
|
727
|
+
return "Pausing machine";
|
|
728
|
+
case "resume":
|
|
729
|
+
return "Resuming machine";
|
|
730
|
+
case "delete":
|
|
731
|
+
return "Deleting machine";
|
|
732
|
+
case "update-supervisor":
|
|
733
|
+
return "Updating terminal supervisor";
|
|
734
|
+
case "live-update-supervisor":
|
|
735
|
+
// Deliberately an attempt, not an outcome. The earlier label read
|
|
736
|
+
// "keeping sessions", which promised the one thing the server has not
|
|
737
|
+
// decided yet: preservation is conditional on a preflight that has not
|
|
738
|
+
// run when this line is painted. It is bounded by the request budget
|
|
739
|
+
// behind it, and the spinner stops when the answer arrives or that
|
|
740
|
+
// budget elapses -- never on its own.
|
|
741
|
+
return "Checking this machine and attempting an in-place supervisor update";
|
|
742
|
+
case "live-update-status":
|
|
743
|
+
// A read, and the label says so. It dispatches no installer and
|
|
744
|
+
// changes nothing, which is the whole reason it is safe to run after
|
|
745
|
+
// an interruption.
|
|
746
|
+
return "Reading what this machine's in-place supervisor update did";
|
|
747
|
+
default:
|
|
748
|
+
return "Reading your machines";
|
|
749
|
+
}
|
|
750
|
+
case "agent-sessions":
|
|
751
|
+
case "agent":
|
|
752
|
+
switch (action) {
|
|
753
|
+
case "create":
|
|
754
|
+
return "Creating AgentSession";
|
|
755
|
+
case "terminate":
|
|
756
|
+
return "Ending AgentSession";
|
|
757
|
+
case "rename":
|
|
758
|
+
return "Renaming AgentSession";
|
|
759
|
+
case "attach":
|
|
760
|
+
return "Attaching to the AgentSession terminal";
|
|
761
|
+
default:
|
|
762
|
+
return "Reading your AgentSessions";
|
|
763
|
+
}
|
|
764
|
+
case "api-keys":
|
|
765
|
+
return action === "create"
|
|
766
|
+
? "Creating API key"
|
|
767
|
+
: action === "revoke"
|
|
768
|
+
? "Revoking API key"
|
|
769
|
+
: "Reading your API keys";
|
|
770
|
+
case "capabilities":
|
|
771
|
+
return "Reading what this account can do";
|
|
772
|
+
case "records":
|
|
773
|
+
return "Reading your records";
|
|
774
|
+
case "authorizations":
|
|
775
|
+
return "Reading your authorizations";
|
|
776
|
+
case "account":
|
|
777
|
+
case "workspace":
|
|
778
|
+
return "Reading your workspace";
|
|
779
|
+
case "usage":
|
|
780
|
+
return "Reading your usage";
|
|
781
|
+
case "doctor":
|
|
782
|
+
return "Checking this installation";
|
|
783
|
+
case "self-test":
|
|
784
|
+
return "Running the self-test";
|
|
785
|
+
default:
|
|
786
|
+
// `config`, `version` and `help` answer from disk. Everything unlisted is
|
|
787
|
+
// routed elsewhere before reaching this dispatch.
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function writeJourneyDiscovery(stream, color) {
|
|
792
|
+
if (!color) {
|
|
793
|
+
stream.write("Cuna: finding a machine or AgentSession to open...\n");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
stream.write(`\u001b[38;5;202m\u001b[1m◆ CUNA\u001b[0m \u001b[38;5;255mFinding a machine or AgentSession\u001b[0m\n`);
|
|
797
|
+
}
|
|
798
|
+
function journeyPhaseLabel(phase, agent) {
|
|
799
|
+
const display = agentDisplayName(agent);
|
|
800
|
+
switch (phase) {
|
|
801
|
+
case "inspect-workspace": return "Inspecting workspace";
|
|
802
|
+
case "observe-machines": return "Finding a compatible machine";
|
|
803
|
+
case "create-machine": return "Creating machine";
|
|
804
|
+
case "reconcile-machine-create": return "Confirming machine creation";
|
|
805
|
+
case "ready-machine": return "Starting machine";
|
|
806
|
+
case "synchronize-workspace": return "Syncing workspace";
|
|
807
|
+
case "observe-agent-sessions": return `Finding ${/^[AEIOU]/u.test(display) ? "an" : "a"} ${display} session`;
|
|
808
|
+
case "create-agent-session": return `Creating ${display} session`;
|
|
809
|
+
case "ready-agent-session": return `Starting ${display}`;
|
|
810
|
+
case "attach": return agent === "opencode"
|
|
811
|
+
? "Opening OpenCode terminal — use /connect there"
|
|
812
|
+
: `Opening ${display}`;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function journeyPreparationLabel(agent) {
|
|
816
|
+
return agent === "opencode"
|
|
817
|
+
? "Preparing OpenCode — use /connect in its terminal"
|
|
818
|
+
: `Preparing ${agentDisplayName(agent)}`;
|
|
819
|
+
}
|
|
820
|
+
function foregroundAttachLabel(agent) {
|
|
821
|
+
return agent === "opencode"
|
|
822
|
+
? "Opening OpenCode terminal — use /connect there"
|
|
823
|
+
: `Attaching to ${agentDisplayName(agent)}`;
|
|
824
|
+
}
|
|
825
|
+
export async function runCli(argv, dependencies = {}) {
|
|
826
|
+
const streams = dependencies.streams ?? defaultStreams();
|
|
827
|
+
const writer = createOutputWriter({ streams, json: argv.includes("--json") });
|
|
828
|
+
const label = commandLabel(argv);
|
|
829
|
+
let inlineMachinesProgress;
|
|
830
|
+
let inlineJourneyProgress;
|
|
831
|
+
let inlineRootProgress;
|
|
832
|
+
let authProgress;
|
|
833
|
+
let stopAuthProgressCancellation;
|
|
834
|
+
let batchProgress;
|
|
835
|
+
let interactiveRootUi = false;
|
|
836
|
+
let interactiveRootColor = false;
|
|
837
|
+
// Root discovery, an explicit foreground attach, and an automatic provider
|
|
838
|
+
// journey all own the terminal interactively. They must share the same
|
|
839
|
+
// one-Ctrl-C close affordance; restricting it to bare `cuna` leaked a raw
|
|
840
|
+
// journey cancellation error from `cuna opencode` before it reached the PTY.
|
|
841
|
+
let interactiveCloseUi = false;
|
|
842
|
+
let interactiveCloseColor = false;
|
|
843
|
+
let terminalSessionIds = [];
|
|
844
|
+
const runForeground = async (input) => {
|
|
845
|
+
terminalSessionIds = [...input.agentSessionIds];
|
|
846
|
+
await (dependencies.foregroundTerminalRunner ?? runNodeForegroundSessions)(input);
|
|
847
|
+
};
|
|
848
|
+
try {
|
|
849
|
+
const parsed = parseArgv(argv);
|
|
850
|
+
if (!booleanOption(parsed, "help") && (booleanOption(parsed, "version") || parsed.command === "version")) {
|
|
851
|
+
rejectUnknownOptions(parsed, ["version"]);
|
|
852
|
+
if (parsed.command === "version" && parsed.operands.length !== 0) {
|
|
853
|
+
throw usageError("version accepts no operands.");
|
|
854
|
+
}
|
|
855
|
+
const identity = Object.freeze({
|
|
856
|
+
version: CLI_VERSION,
|
|
857
|
+
buildDigest: await packageBuildDigest(),
|
|
858
|
+
platform: process.platform,
|
|
859
|
+
architecture: process.arch,
|
|
860
|
+
updateChannel: ARTIFACT_CHANNEL,
|
|
861
|
+
artifactChannel: ARTIFACT_CHANNEL,
|
|
862
|
+
protocolRange: PROTOCOL_RANGE,
|
|
863
|
+
});
|
|
864
|
+
// Both `cuna version --json` and the help text promise version, build
|
|
865
|
+
// digest, platform and protocol range; the human branch printed the
|
|
866
|
+
// version alone. The digest is the one field that separates two
|
|
867
|
+
// installations reporting the same `0.1.0` — measured 2026-08-25, when
|
|
868
|
+
// the installed CLI was not the repo build and nothing printed said so.
|
|
869
|
+
// It is shown as a 12-hex prefix with the same `…` this CLI already uses
|
|
870
|
+
// for a truncated API-key prefix, so it can never read as the whole hash.
|
|
871
|
+
const humanIdentity = `${identity.version}\tbuild ${identity.buildDigest.slice(0, 12)}…` +
|
|
872
|
+
`\t${identity.platform}/${identity.architecture}` +
|
|
873
|
+
`\tprotocol ${identity.protocolRange.minimum}..${identity.protocolRange.maximum}`;
|
|
874
|
+
if (writer.structured) {
|
|
875
|
+
writer.success("version", identity, humanIdentity);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
writer.text(humanIdentity);
|
|
879
|
+
}
|
|
880
|
+
return EXIT_CODES.success;
|
|
881
|
+
}
|
|
882
|
+
// Help is answered before the root-option allowlist below. That allowlist
|
|
883
|
+
// exists to reject `cuna --profile x` with no command, but it ran first and
|
|
884
|
+
// so also rejected `cuna --help --profile x`: asking for help was refused
|
|
885
|
+
// because of the very option the user wanted help about. `--json` and
|
|
886
|
+
// `--no-color` happened to be on the allowlist and worked, which is what
|
|
887
|
+
// made the behaviour look arbitrary rather than wrong.
|
|
888
|
+
if (parsed.command === "help" || booleanOption(parsed, "help")) {
|
|
889
|
+
rejectUnknownOptions(parsed, ["help", "all"]);
|
|
890
|
+
if (parsed.command === "help" && parsed.operands.length !== 0)
|
|
891
|
+
throw usageError("help accepts no operands.");
|
|
892
|
+
const topic = parsed.command === "help" || parsed.command === undefined
|
|
893
|
+
? undefined
|
|
894
|
+
: parsed.command;
|
|
895
|
+
if (topic === "openclaw") {
|
|
896
|
+
throw usageError(`Unknown command ${topic}.`, "Run `cuna --help`.");
|
|
897
|
+
}
|
|
898
|
+
// `--all` widens the ROOT topic only. On a command topic the per-command
|
|
899
|
+
// help is already the complete surface for that command, so there is
|
|
900
|
+
// nothing to widen and the flag would promise something it cannot do.
|
|
901
|
+
const wantsFullSurface = booleanOption(parsed, "all");
|
|
902
|
+
if (wantsFullSurface && topic !== undefined) {
|
|
903
|
+
throw usageError("Option --all applies to `cuna help`, not to one command's help.", `Run \`cuna ${topic} --help\` for this command, or \`cuna help --all\` for the complete surface.`);
|
|
904
|
+
}
|
|
905
|
+
const help = wantsFullSurface ? FULL_HELP : commandHelp(topic, parsed.operands);
|
|
906
|
+
if (writer.structured) {
|
|
907
|
+
writer.success("help", {
|
|
908
|
+
version: CLI_VERSION,
|
|
909
|
+
output_schema_version: OUTPUT_SCHEMA_VERSION,
|
|
910
|
+
...(topic === undefined
|
|
911
|
+
? (wantsFullSurface ? { topic: "all" } : {})
|
|
912
|
+
: { topic: helpTopicName(topic, parsed.operands) }),
|
|
913
|
+
help,
|
|
914
|
+
}, help);
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
writer.text(help);
|
|
918
|
+
}
|
|
919
|
+
return EXIT_CODES.success;
|
|
920
|
+
}
|
|
921
|
+
if (parsed.command === undefined) {
|
|
922
|
+
const allowedRootOptions = new Set(["help", "version", "json", "no-color"]);
|
|
923
|
+
const invalidRootOption = Object.keys(parsed.options).find((name) => !allowedRootOptions.has(name));
|
|
924
|
+
if (invalidRootOption !== undefined)
|
|
925
|
+
throw usageError(`Option --${invalidRootOption} requires a command.`);
|
|
926
|
+
}
|
|
927
|
+
const interactiveRoot = parsed.command === undefined &&
|
|
928
|
+
!writer.structured && streams.stdinIsTTY && streams.stdoutIsTTY;
|
|
929
|
+
interactiveRootUi = interactiveRoot;
|
|
930
|
+
if (parsed.command === undefined && !interactiveRoot) {
|
|
931
|
+
if (writer.structured) {
|
|
932
|
+
writer.success("help", { version: CLI_VERSION, output_schema_version: OUTPUT_SCHEMA_VERSION, help: ROOT_HELP }, ROOT_HELP);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
writer.text(ROOT_HELP);
|
|
936
|
+
}
|
|
937
|
+
return EXIT_CODES.success;
|
|
938
|
+
}
|
|
939
|
+
// A bare interactive invocation must acknowledge input before any local
|
|
940
|
+
// credential-store, configuration, or network read. In particular, an
|
|
941
|
+
// access-token refresh can take several seconds; leaving the terminal
|
|
942
|
+
// blank during that work makes Cuna look stuck and encourages a duplicate
|
|
943
|
+
// invocation. The phase remains deliberately neutral until config and
|
|
944
|
+
// authentication tell us what is actually happening.
|
|
945
|
+
const effectiveEnvironment = { ...(dependencies.env ?? process.env) };
|
|
946
|
+
Object.freeze(effectiveEnvironment);
|
|
947
|
+
if (interactiveRoot) {
|
|
948
|
+
const color = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
949
|
+
interactiveRootColor = color;
|
|
950
|
+
interactiveCloseUi = true;
|
|
951
|
+
interactiveCloseColor = color;
|
|
952
|
+
if (streams.stderrIsTTY === true) {
|
|
953
|
+
inlineRootProgress = startInlineProgress(streams.stderr, color, "Starting Cuna");
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
streams.stderr.write("Cuna: starting...\n");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// Preflight gates and configuration must read the same invocation
|
|
960
|
+
// environment. This keeps credential and profile selection deterministic
|
|
961
|
+
// across embedded invocations.
|
|
962
|
+
if (parsed.command !== undefined)
|
|
963
|
+
preflightInvocation(parsed, (dependencies.now ?? Date.now)());
|
|
964
|
+
if (parsed.command === "observe" && (writer.structured || streams.stdinIsTTY !== true || streams.stdoutIsTTY !== true))
|
|
965
|
+
throw usageError("observe requires an interactive terminal; JSON and redirected output are unsupported.");
|
|
966
|
+
if (parsed.command === "share" && (writer.structured || streams.stdinIsTTY !== true || streams.stdoutIsTTY !== true))
|
|
967
|
+
throw usageError("share requires an interactive terminal; JSON and redirected output are unsupported.");
|
|
968
|
+
let journeyIntent = parsed.command === "claude" || parsed.command === "codex" || parsed.command === "opencode"
|
|
969
|
+
? preflightAgentJourneyInvocation(parsed)
|
|
970
|
+
: undefined;
|
|
971
|
+
const remoteMenuLaunch = journeyIntent?.target === "reconcile" && journeyIntent.newSession &&
|
|
972
|
+
dependencies.managedWorkspaceMachineId !== undefined && journeyIntent.localPath === undefined;
|
|
973
|
+
if (journeyIntent?.target === "reconcile" && journeyIntent.localPath === undefined && !remoteMenuLaunch) {
|
|
974
|
+
const homeDirectory = platformHomeDirectory(effectiveEnvironment, process.platform);
|
|
975
|
+
if (homeDirectory !== undefined && sameHostPath(process.cwd(), homeDirectory, process.platform)) {
|
|
976
|
+
// Keep HOME safe without forcing every machine to share one local
|
|
977
|
+
// binding. Exact machine selections get a stable private root; the
|
|
978
|
+
// automatic root remains stable so its committed binding can guide
|
|
979
|
+
// later automatic selection.
|
|
980
|
+
// Do not nest these roots under the former single-root `~/Cuna`.
|
|
981
|
+
// Workspace binding discovery intentionally walks ancestors, so a
|
|
982
|
+
// child below that already-bound root would inherit its record and
|
|
983
|
+
// then fail the child-root compare-and-swap.
|
|
984
|
+
const managedWorkspace = join(homeDirectory, "Cuna Workspaces", managedWorkspaceScope(journeyIntent, dependencies.managedWorkspaceMachineId));
|
|
985
|
+
await mkdir(managedWorkspace, { recursive: true });
|
|
986
|
+
journeyIntent = Object.freeze({ ...journeyIntent, localPath: managedWorkspace });
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const foreground = foregroundSelection(parsed);
|
|
990
|
+
if ((foreground !== undefined || journeyIntent?.target === "reconcile") &&
|
|
991
|
+
(writer.structured || !streams.stdinIsTTY || !streams.stdoutIsTTY)) {
|
|
992
|
+
throw usageError("Foreground AgentSession attachment requires an interactive terminal and does not support JSON output.", "Run this command directly in an interactive terminal without --json or output redirection.");
|
|
993
|
+
}
|
|
994
|
+
if ((journeyIntent !== undefined || foreground !== undefined) &&
|
|
995
|
+
!writer.structured && streams.stdinIsTTY && streams.stdoutIsTTY && streams.stderrIsTTY) {
|
|
996
|
+
interactiveCloseUi = true;
|
|
997
|
+
interactiveCloseColor = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
998
|
+
}
|
|
999
|
+
if (journeyIntent !== undefined) {
|
|
1000
|
+
// This is deliberately before config, credential, and network work: it
|
|
1001
|
+
// tells the truth immediately without claiming that attach has begun.
|
|
1002
|
+
const preparation = journeyPreparationLabel(journeyIntent.agent);
|
|
1003
|
+
const color = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1004
|
+
if (streams.stderrIsTTY === true)
|
|
1005
|
+
inlineJourneyProgress = startInlineProgress(streams.stderr, color, preparation);
|
|
1006
|
+
else
|
|
1007
|
+
streams.stderr.write(`Cuna: ${preparation.charAt(0).toLowerCase()}${preparation.slice(1)}...\n`);
|
|
1008
|
+
}
|
|
1009
|
+
const platform = dependencies.platform ?? createPlatformAdapter({ env: effectiveEnvironment });
|
|
1010
|
+
let foregroundPresentation;
|
|
1011
|
+
if (foreground !== undefined) {
|
|
1012
|
+
foregroundPresentation = selectNodeForegroundPresentation({
|
|
1013
|
+
platform: nodePlatform(platform.kind),
|
|
1014
|
+
environment: effectiveEnvironment,
|
|
1015
|
+
sessionCount: foreground.agentSessionIds.length,
|
|
1016
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1017
|
+
});
|
|
1018
|
+
if (foregroundPresentation === "plain" && foreground.agentSessionIds.length !== 1) {
|
|
1019
|
+
throw usageError("Plain passthrough mode attaches exactly one AgentSession.", "Select one AgentSession or use an admitted rich terminal for the multi-tab workbench.");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
let timeoutMs;
|
|
1023
|
+
try {
|
|
1024
|
+
timeoutMs = parseTimeout(stringOption(parsed, "timeout-ms"));
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
throw usageError("Option --timeout-ms must be an integer from 100 through 120000.");
|
|
1028
|
+
}
|
|
1029
|
+
// Seams and probes that never carry a per-operation budget still need one
|
|
1030
|
+
// number, and it is the same number, read from the same constant.
|
|
1031
|
+
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_REQUEST_BUDGET_MS;
|
|
1032
|
+
const profile = stringOption(parsed, "profile");
|
|
1033
|
+
const baseUrl = stringOption(parsed, "base-url");
|
|
1034
|
+
const configFile = stringOption(parsed, "config-file");
|
|
1035
|
+
// `cuna login --profile <name>` is the one invocation allowed to resolve a
|
|
1036
|
+
// profile the configuration file does not list yet, because it is the one
|
|
1037
|
+
// that creates it. The profile is written only after the sign-in succeeds.
|
|
1038
|
+
const creatingProfile = parsed.command === "login" && profile !== undefined;
|
|
1039
|
+
const config = await resolveConfig({
|
|
1040
|
+
platform,
|
|
1041
|
+
env: effectiveEnvironment,
|
|
1042
|
+
overrides: {
|
|
1043
|
+
...(profile === undefined ? {} : { profile }),
|
|
1044
|
+
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
1045
|
+
...(configFile === undefined ? {} : { configFile }),
|
|
1046
|
+
},
|
|
1047
|
+
...(creatingProfile ? { allowMissingProfile: true } : {}),
|
|
1048
|
+
});
|
|
1049
|
+
if (interactiveRoot) {
|
|
1050
|
+
inlineRootProgress?.update(config.apiKey === undefined ? "Checking your Cuna sign-in" : "Checking Cuna access");
|
|
1051
|
+
}
|
|
1052
|
+
// Fail closed before any authority is selected, and only for a command that
|
|
1053
|
+
// selects one. Empty or malformed still never means absent: an unusable
|
|
1054
|
+
// `*_API_KEY` refuses the command rather than silently demoting automation
|
|
1055
|
+
// mode to an interactive browser sign-in.
|
|
1056
|
+
if (interactiveRoot || usesCredentialAuthority(parsed.command, foreground))
|
|
1057
|
+
assertApiKeyUsable(config);
|
|
1058
|
+
const sessionPaths = localEncryptedSessionPaths(platform.paths.configDirectory, config.profile);
|
|
1059
|
+
if (config.apiKey !== undefined && (parsed.command === "login" || parsed.command === "signup")) {
|
|
1060
|
+
throw new CunaError({
|
|
1061
|
+
code: "cuna.auth.mode_conflict",
|
|
1062
|
+
message: `Encrypted browser authentication cannot be combined with ${config.apiKeyVariable ?? "CUNA_API_KEY"}.`,
|
|
1063
|
+
exitCode: EXIT_CODES.auth,
|
|
1064
|
+
hint: "Unset the automation credential before running `cuna login` or another interactive command.",
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const browserAuthUnavailable = (parsed.command === "login" || parsed.command === "signup") &&
|
|
1068
|
+
(writer.structured || !streams.stdinIsTTY || !streams.stdoutIsTTY || streams.stderrIsTTY !== true) &&
|
|
1069
|
+
dependencies.humanAuth === undefined &&
|
|
1070
|
+
dependencies.browser === undefined;
|
|
1071
|
+
const browserAuthUsageError = () => usageError("Browser authentication requires an interactive terminal and does not support JSON or redirected output.", "Run `cuna login` directly in a TTY; the one-time link is printed only to the terminal.");
|
|
1072
|
+
// `signup` always needs the browser. `login` decides after reading the
|
|
1073
|
+
// vault: a profile that is already signed in has nothing to print, and
|
|
1074
|
+
// "already signed in" is the answer, not a usage error (the one-time link
|
|
1075
|
+
// is still never written to redirected output — see the login branch).
|
|
1076
|
+
if (browserAuthUnavailable && parsed.command === "signup")
|
|
1077
|
+
throw browserAuthUsageError();
|
|
1078
|
+
let humanAuth = dependencies.humanAuth;
|
|
1079
|
+
const getHumanAuth = async () => {
|
|
1080
|
+
if (humanAuth !== undefined)
|
|
1081
|
+
return humanAuth;
|
|
1082
|
+
const transportOptions = {
|
|
1083
|
+
baseUrl: config.baseUrl,
|
|
1084
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
1085
|
+
...(dependencies.fetch === undefined ? {} : { fetch: dependencies.fetch }),
|
|
1086
|
+
};
|
|
1087
|
+
const humanClient = createHumanAuthClient({
|
|
1088
|
+
anonymous: createHttpTransport(transportOptions),
|
|
1089
|
+
authenticated: (accessToken) => createHttpTransport({ ...transportOptions, bearerToken: accessToken }),
|
|
1090
|
+
});
|
|
1091
|
+
humanAuth = createHumanAuthService({
|
|
1092
|
+
config,
|
|
1093
|
+
client: humanClient,
|
|
1094
|
+
vault: dependencies.credentialVault !== undefined
|
|
1095
|
+
? dependencies.credentialVault
|
|
1096
|
+
: new CredentialVault({
|
|
1097
|
+
backend: new LocalEncryptedSessionBackend({
|
|
1098
|
+
...sessionPaths,
|
|
1099
|
+
platform: nodePlatform(platform.kind),
|
|
1100
|
+
...(dependencies.now === undefined ? {} : { clock: dependencies.now }),
|
|
1101
|
+
}),
|
|
1102
|
+
platform: nodePlatform(platform.kind),
|
|
1103
|
+
...(dependencies.now === undefined ? {} : { clock: dependencies.now }),
|
|
1104
|
+
}),
|
|
1105
|
+
browser: dependencies.browser ?? createBrowserOpener(nodePlatform(platform.kind), effectiveEnvironment),
|
|
1106
|
+
// Bound to the real stderr the command was given, never to an injected
|
|
1107
|
+
// seam. A test that stubs `browser` still sees the URL it would have
|
|
1108
|
+
// printed to a user, so "the browser opened" and "the user was told
|
|
1109
|
+
// where to go" cannot be proven independently of each other.
|
|
1110
|
+
browserHandoff: createTerminalBrowserHandoffReporter(streams.stderr),
|
|
1111
|
+
readLoginCode: dependencies.readLoginCode ?? promptLoginCode,
|
|
1112
|
+
onLoginCodeAccepted: () => {
|
|
1113
|
+
// Keep the browser link and hidden reader still. Only validated input
|
|
1114
|
+
// starts completion feedback; remote exchange and storage may still fail.
|
|
1115
|
+
if (parsed.command === "login" && streams.stderrIsTTY === true &&
|
|
1116
|
+
!writer.structured && dependencies.signal?.aborted !== true && authProgress === undefined) {
|
|
1117
|
+
authProgress = startInlineProgress(streams.stderr, !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR"), "Completing Cuna sign-in");
|
|
1118
|
+
const signal = dependencies.signal;
|
|
1119
|
+
if (signal !== undefined) {
|
|
1120
|
+
const stopping = () => authProgress?.update("Stopping Cuna sign-in");
|
|
1121
|
+
signal.addEventListener("abort", stopping, { once: true });
|
|
1122
|
+
stopAuthProgressCancellation = () => signal.removeEventListener("abort", stopping);
|
|
1123
|
+
if (signal.aborted)
|
|
1124
|
+
stopping();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
...(dependencies.now === undefined ? {} : { clock: dependencies.now }),
|
|
1129
|
+
});
|
|
1130
|
+
return humanAuth;
|
|
1131
|
+
};
|
|
1132
|
+
// The primary human entry points own authentication as part of their
|
|
1133
|
+
// journey. A person who runs `cuna`, `cuna machines`, `cuna claude`, or
|
|
1134
|
+
// `cuna codex` should never see machine-discovery feedback followed by an
|
|
1135
|
+
// instruction to discover a separate login command. Establish or recover
|
|
1136
|
+
// the interactive session first, then begin the product action.
|
|
1137
|
+
const guidedInteractiveEntry = config.apiKey === undefined && (interactiveRoot ||
|
|
1138
|
+
(parsed.command === "machines" && parsed.operands.length === 0 &&
|
|
1139
|
+
!writer.structured && streams.stdinIsTTY && streams.stdoutIsTTY) ||
|
|
1140
|
+
journeyIntent?.target === "reconcile");
|
|
1141
|
+
if (guidedInteractiveEntry) {
|
|
1142
|
+
const guidedAuth = await getHumanAuth();
|
|
1143
|
+
try {
|
|
1144
|
+
await guidedAuth.acquireAccessToken(dependencies.signal);
|
|
1145
|
+
}
|
|
1146
|
+
catch (error) {
|
|
1147
|
+
if (!(error instanceof CunaError) ||
|
|
1148
|
+
(error.code !== "cuna.auth.required" && error.code !== "cuna.auth.reauthentication_required")) {
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
inlineRootProgress?.stop();
|
|
1152
|
+
inlineRootProgress = undefined;
|
|
1153
|
+
streams.stderr.write("Cuna: let's sign you in first...\n");
|
|
1154
|
+
await guidedAuth.login(dependencies.signal === undefined ? {} : { signal: dependencies.signal });
|
|
1155
|
+
streams.stderr.write("Cuna: signed in. Continuing...\n");
|
|
1156
|
+
if (interactiveRoot && streams.stderrIsTTY === true) {
|
|
1157
|
+
inlineRootProgress = startInlineProgress(streams.stderr, interactiveRootColor, "Finding a machine or AgentSession");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// Progress starts only after authentication is usable. This ordering is
|
|
1162
|
+
// observable UX: it must not claim to search machines while login is the
|
|
1163
|
+
// actual operation in progress.
|
|
1164
|
+
if (journeyIntent !== undefined) {
|
|
1165
|
+
const display = agentDisplayName(journeyIntent.agent);
|
|
1166
|
+
if (inlineJourneyProgress !== undefined)
|
|
1167
|
+
inlineJourneyProgress.update(`Connecting to ${display}`);
|
|
1168
|
+
else
|
|
1169
|
+
streams.stderr.write(`Cuna: connecting to ${display}...\n`);
|
|
1170
|
+
}
|
|
1171
|
+
if (parsed.command === "machines" && parsed.operands.length === 0 &&
|
|
1172
|
+
!writer.structured && streams.stdinIsTTY && streams.stdoutIsTTY) {
|
|
1173
|
+
const color = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1174
|
+
if (streams.stderrIsTTY === true)
|
|
1175
|
+
inlineMachinesProgress = startInlineProgress(streams.stderr, color);
|
|
1176
|
+
else
|
|
1177
|
+
streams.stderr.write("Cuna: loading machines...\n");
|
|
1178
|
+
}
|
|
1179
|
+
if (parsed.command === "signup" ||
|
|
1180
|
+
parsed.command === "login" ||
|
|
1181
|
+
parsed.command === "logout" ||
|
|
1182
|
+
parsed.command === "whoami" ||
|
|
1183
|
+
parsed.command === "access") {
|
|
1184
|
+
rejectUnknownOptions(parsed, []);
|
|
1185
|
+
if (parsed.command === "access") {
|
|
1186
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "status") {
|
|
1187
|
+
throw usageError("access requires the status action.");
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
else if (parsed.operands.length !== 0) {
|
|
1191
|
+
throw usageError(`${parsed.command} accepts no operands.`);
|
|
1192
|
+
}
|
|
1193
|
+
if (config.apiKey !== undefined) {
|
|
1194
|
+
throw new CunaError({
|
|
1195
|
+
code: "cuna.auth.mode_conflict",
|
|
1196
|
+
message: `Interactive authentication is disabled while ${config.apiKeyVariable ?? "CUNA_API_KEY"} selects automation mode.`,
|
|
1197
|
+
exitCode: EXIT_CODES.auth,
|
|
1198
|
+
hint: `Unset ${config.apiKeyVariable ?? "CUNA_API_KEY"} before managing the interactive session.`,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
// These commands unlock the encrypted local session and then wait on the
|
|
1202
|
+
// network. `login` is excluded: it prints a URL and blocks on the person,
|
|
1203
|
+
// and a spinner over a link they must read is worse than silence.
|
|
1204
|
+
// Progress exists only for a real terminal, never in structured output.
|
|
1205
|
+
if (parsed.command !== "login" &&
|
|
1206
|
+
streams.stderrIsTTY === true &&
|
|
1207
|
+
!writer.structured &&
|
|
1208
|
+
authProgress === undefined) {
|
|
1209
|
+
authProgress = startInlineProgress(streams.stderr, !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR"), parsed.command === "logout"
|
|
1210
|
+
? "Signing out of Cuna"
|
|
1211
|
+
: parsed.command === "signup"
|
|
1212
|
+
? "Creating your Cuna account"
|
|
1213
|
+
: "Checking your Cuna sign-in");
|
|
1214
|
+
}
|
|
1215
|
+
if (parsed.command === "signup") {
|
|
1216
|
+
const result = await (await getHumanAuth()).signup(dependencies.signal === undefined ? {} : { signal: dependencies.signal });
|
|
1217
|
+
const data = humanResult(result);
|
|
1218
|
+
authProgress?.stop();
|
|
1219
|
+
authProgress = undefined;
|
|
1220
|
+
writer.success("signup", data, result.context.admission === "waitlisted"
|
|
1221
|
+
? `Cuna saved your waitlist place for profile ${result.profile}.`
|
|
1222
|
+
: `Cuna completed signup for profile ${result.profile}.`);
|
|
1223
|
+
}
|
|
1224
|
+
else if (parsed.command === "login") {
|
|
1225
|
+
const auth = await getHumanAuth();
|
|
1226
|
+
let result;
|
|
1227
|
+
let alreadySignedIn = false;
|
|
1228
|
+
if (browserAuthUnavailable) {
|
|
1229
|
+
// No terminal to print a link to: the only login that can succeed
|
|
1230
|
+
// here is the one that already happened.
|
|
1231
|
+
try {
|
|
1232
|
+
result = await auth.whoami(dependencies.signal);
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
throw browserAuthUsageError();
|
|
1236
|
+
}
|
|
1237
|
+
alreadySignedIn = true;
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
try {
|
|
1241
|
+
result = await auth.login(dependencies.signal === undefined ? {} : { signal: dependencies.signal });
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
// Being signed in already is the outcome the user asked for, not a
|
|
1245
|
+
// failure: report who they are and how to switch, exit 0.
|
|
1246
|
+
if (!(error instanceof CunaError) || error.code !== "cuna.auth.already_signed_in")
|
|
1247
|
+
throw error;
|
|
1248
|
+
result = await auth.whoami(dependencies.signal);
|
|
1249
|
+
alreadySignedIn = true;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
// After the sign-in and before the result line, so the sentence the
|
|
1253
|
+
// person reads is already true on the next invocation.
|
|
1254
|
+
const profileCreated = creatingProfile
|
|
1255
|
+
? await ensureProfileRecorded({ platform, config })
|
|
1256
|
+
: false;
|
|
1257
|
+
const data = Object.freeze({
|
|
1258
|
+
...humanResult(result),
|
|
1259
|
+
storage_mode: "encrypted-local",
|
|
1260
|
+
already_signed_in: alreadySignedIn,
|
|
1261
|
+
...(creatingProfile ? { profile_created: profileCreated } : {}),
|
|
1262
|
+
});
|
|
1263
|
+
authProgress?.stop();
|
|
1264
|
+
authProgress = undefined;
|
|
1265
|
+
stopAuthProgressCancellation?.();
|
|
1266
|
+
stopAuthProgressCancellation = undefined;
|
|
1267
|
+
writer.success("login", data, alreadySignedIn
|
|
1268
|
+
? `Already signed in as ${result.context.identity}. Run \`cuna logout\` to switch accounts.`
|
|
1269
|
+
: "Signed in to Cuna.");
|
|
1270
|
+
}
|
|
1271
|
+
else if (parsed.command === "whoami" || parsed.command === "access") {
|
|
1272
|
+
const result = await (await getHumanAuth()).whoami(dependencies.signal);
|
|
1273
|
+
const data = humanResult(result);
|
|
1274
|
+
authProgress?.stop();
|
|
1275
|
+
authProgress = undefined;
|
|
1276
|
+
writer.success(parsed.command === "access" ? "access.status" : "whoami", data, `${result.context.identity}\t${result.context.admission}\t${result.context.workspace.state}`);
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
const result = await (await getHumanAuth()).logout(dependencies.signal);
|
|
1280
|
+
authProgress?.stop();
|
|
1281
|
+
authProgress = undefined;
|
|
1282
|
+
writer.success("logout", result, "Signed out of Cuna on this device.");
|
|
1283
|
+
}
|
|
1284
|
+
return EXIT_CODES.success;
|
|
1285
|
+
}
|
|
1286
|
+
let bearerTokenProvider;
|
|
1287
|
+
let credentialMode = config.apiKey === undefined ? undefined : "automation";
|
|
1288
|
+
if (config.apiKey === undefined && (interactiveRoot || needsRemoteCredential(parsed.command, foreground))) {
|
|
1289
|
+
if (dependencies.clientFactory === undefined || dependencies.humanAuth !== undefined) {
|
|
1290
|
+
const humanAuth = await getHumanAuth();
|
|
1291
|
+
if (dependencies.clientFactory === undefined) {
|
|
1292
|
+
bearerTokenProvider = (signal, refresh) => refresh === undefined
|
|
1293
|
+
? humanAuth.acquireAccessToken(signal)
|
|
1294
|
+
: humanAuth.refreshRejectedAccessToken(refresh.rejectedToken, signal);
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
await humanAuth.acquireAccessToken(dependencies.signal);
|
|
1298
|
+
}
|
|
1299
|
+
credentialMode = "interactive";
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const httpTransport = dependencies.clientFactory === undefined ? createHttpTransport({
|
|
1303
|
+
baseUrl: config.baseUrl,
|
|
1304
|
+
...(config.apiKey === undefined ? {} : { apiKey: config.apiKey }),
|
|
1305
|
+
...(bearerTokenProvider === undefined ? {} : { bearerTokenProvider }),
|
|
1306
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
1307
|
+
...(dependencies.fetch === undefined ? {} : { fetch: dependencies.fetch }),
|
|
1308
|
+
}) : undefined;
|
|
1309
|
+
const client = dependencies.clientFactory?.(config, effectiveTimeoutMs) ?? createCunaApiClient(httpTransport);
|
|
1310
|
+
if (parsed.command === "observe") {
|
|
1311
|
+
if (writer.structured || streams.stdinIsTTY !== true || streams.stdoutIsTTY !== true || credentialMode !== "interactive" || !httpTransport)
|
|
1312
|
+
throw usageError("observe requires an interactive terminal and human login.");
|
|
1313
|
+
// This process owns its initial principal. Checks around admission do not watch
|
|
1314
|
+
// another process changing local credentials; established streams rely on server authority.
|
|
1315
|
+
const identity = await client.getIdentity(dependencies.signal);
|
|
1316
|
+
await runObserverScreen(observerApi(httpTransport, identity.id, stringOption(parsed, "project"), async () => (await client.getIdentity(dependencies.signal)).id), config.baseUrl, undefined, dependencies.signal);
|
|
1317
|
+
return EXIT_CODES.success;
|
|
1318
|
+
}
|
|
1319
|
+
if (parsed.command === "share") {
|
|
1320
|
+
if (writer.structured || streams.stdinIsTTY !== true || streams.stdoutIsTTY !== true || credentialMode !== "interactive" || !httpTransport)
|
|
1321
|
+
throw usageError("share requires an interactive terminal and human login.");
|
|
1322
|
+
const project = stringOption(parsed, "project"), identity = await client.getIdentity(dependencies.signal);
|
|
1323
|
+
// Sessions come from the same Machine/AgentSession reads the explorer uses; the
|
|
1324
|
+
// server names each session's Project, and the grant receipt is bound to it again.
|
|
1325
|
+
// Both reads are bounded: at most 20 Machines, one AgentSession page each.
|
|
1326
|
+
// What those bounds cut off is counted and returned, because a screen that
|
|
1327
|
+
// shows a subset of the owner's own sessions without saying so is a false
|
|
1328
|
+
// statement about their account.
|
|
1329
|
+
const sessions = { async list(signal) { const all = (await client.listMachines(signal)).items; const machines = all.slice(0, 20); const rows = []; let machinesWithMoreSessions = 0; for (const machine of machines) {
|
|
1330
|
+
const page = await client.listAgentSessions(machine.id, {}, signal);
|
|
1331
|
+
if (page.nextCursor !== undefined)
|
|
1332
|
+
machinesWithMoreSessions += 1;
|
|
1333
|
+
for (const s of page.items) {
|
|
1334
|
+
if (s.processState === "terminated")
|
|
1335
|
+
continue;
|
|
1336
|
+
rows.push({ id: s.id, name: s.name, agent: s.agent, machineName: machine.name, state: s.processState, ...(s.projectId === undefined ? {} : { projectId: s.projectId }) });
|
|
1337
|
+
}
|
|
1338
|
+
} return { items: rows, omittedMachines: all.length - machines.length, machinesWithMoreSessions }; } };
|
|
1339
|
+
// The operation store outlives this process: an unanswered grant or
|
|
1340
|
+
// revocation keeps its exact operation ID on disk, so the next `cuna
|
|
1341
|
+
// share` opens on it instead of losing the authority it may have created.
|
|
1342
|
+
const operations = ownerGrantOperationStore(platform, { baseUrl: config.baseUrl, profile: config.profile, ownerPrincipalId: identity.id, projectId: project });
|
|
1343
|
+
await runOwnerGrantsScreen(ownerObserveGrantsApi(httpTransport, identity.id, project, async () => (await client.getIdentity(dependencies.signal)).id), sessions, operations, { project, owner: identity.id }, { ...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }), ...(stringOption(parsed, "grant") === undefined ? {} : { initialGrantId: stringOption(parsed, "grant") }) });
|
|
1344
|
+
return EXIT_CODES.success;
|
|
1345
|
+
}
|
|
1346
|
+
if (interactiveRoot) {
|
|
1347
|
+
const color = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1348
|
+
interactiveRootColor = color;
|
|
1349
|
+
interactiveCloseUi = true;
|
|
1350
|
+
interactiveCloseColor = color;
|
|
1351
|
+
if (streams.stderrIsTTY === true) {
|
|
1352
|
+
if (inlineRootProgress === undefined) {
|
|
1353
|
+
inlineRootProgress = startInlineProgress(streams.stderr, color, "Finding a machine or AgentSession");
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
inlineRootProgress.update("Finding a machine or AgentSession");
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
writeJourneyDiscovery(streams.stderr, false);
|
|
1361
|
+
}
|
|
1362
|
+
const rootRunner = dependencies.rootJourneyRunner ?? runNodeRootJourney;
|
|
1363
|
+
let selection;
|
|
1364
|
+
try {
|
|
1365
|
+
selection = await rootRunner({
|
|
1366
|
+
client,
|
|
1367
|
+
color,
|
|
1368
|
+
onBeforeTerminalOwnership: () => {
|
|
1369
|
+
inlineRootProgress?.stop();
|
|
1370
|
+
inlineRootProgress = undefined;
|
|
1371
|
+
},
|
|
1372
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1373
|
+
}, dependencies.now === undefined ? {} : { now: dependencies.now });
|
|
1374
|
+
}
|
|
1375
|
+
finally {
|
|
1376
|
+
inlineRootProgress?.stop();
|
|
1377
|
+
inlineRootProgress = undefined;
|
|
1378
|
+
}
|
|
1379
|
+
if (selection === undefined) {
|
|
1380
|
+
if (dependencies.signal?.aborted === true && streams.stderrIsTTY === true) {
|
|
1381
|
+
await animateInlineClose(streams.stderr, color);
|
|
1382
|
+
}
|
|
1383
|
+
return EXIT_CODES.success;
|
|
1384
|
+
}
|
|
1385
|
+
if (selection.kind === "provider-check") {
|
|
1386
|
+
await (dependencies.providerScreenRunner ?? runProviderScreen)(client, { kind: "check", sessionId: selection.agentSessionId }, undefined, dependencies.signal);
|
|
1387
|
+
return await runCli(["machines"], dependencies);
|
|
1388
|
+
}
|
|
1389
|
+
if (selection.kind === "executions") {
|
|
1390
|
+
const outcome = await runExecutionsScreen(client, selection.machineId, undefined, dependencies.signal, { platform, baseUrl: config.baseUrl, profile: config.profile });
|
|
1391
|
+
if (outcome === "cancelled")
|
|
1392
|
+
return EXIT_CODES.success;
|
|
1393
|
+
return await runCli(["machines"], dependencies);
|
|
1394
|
+
}
|
|
1395
|
+
if (selection.kind === "workspaces") {
|
|
1396
|
+
const identity = await client.getIdentity(dependencies.signal);
|
|
1397
|
+
if (identity.workspaceId === undefined)
|
|
1398
|
+
throw usageError("This account has no assigned workspace.");
|
|
1399
|
+
const outcome = await runWorkspaceSelectionScreen({ client, profileId: config.profile, userId: identity.id, workspaceId: identity.workspaceId, machineId: selection.machineId, stateDirectory: platform.paths.stateDirectory, platform: platform.kind }, dependencies.workspaceRoot ?? process.cwd(), undefined, dependencies.signal);
|
|
1400
|
+
if (outcome === "cancelled")
|
|
1401
|
+
return EXIT_CODES.success;
|
|
1402
|
+
return await runCli(["machines"], dependencies);
|
|
1403
|
+
}
|
|
1404
|
+
if (selection.kind === "attach") {
|
|
1405
|
+
const attachLabel = foregroundAttachLabel(selection.agent);
|
|
1406
|
+
if (streams.stderrIsTTY === true)
|
|
1407
|
+
inlineRootProgress = startInlineProgress(streams.stderr, color, attachLabel);
|
|
1408
|
+
else
|
|
1409
|
+
streams.stderr.write(`Cuna: ${attachLabel.charAt(0).toLowerCase()}${attachLabel.slice(1)}...\n`);
|
|
1410
|
+
const runner = runForeground;
|
|
1411
|
+
try {
|
|
1412
|
+
await runner({
|
|
1413
|
+
client,
|
|
1414
|
+
baseUrl: config.baseUrl,
|
|
1415
|
+
browser: dependencies.browser ?? createBrowserOpener(nodePlatform(platform.kind), effectiveEnvironment),
|
|
1416
|
+
agentSessionIds: [selection.agentSessionId],
|
|
1417
|
+
expectedAgentKinds: [selection.agent],
|
|
1418
|
+
color,
|
|
1419
|
+
hostPlatform: nodePlatform(platform.kind),
|
|
1420
|
+
presentationMode: selectNodeForegroundPresentation({
|
|
1421
|
+
platform: nodePlatform(platform.kind),
|
|
1422
|
+
environment: effectiveEnvironment,
|
|
1423
|
+
sessionCount: 1,
|
|
1424
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1425
|
+
}),
|
|
1426
|
+
onProgress: (nextLabel) => inlineRootProgress?.update(nextLabel),
|
|
1427
|
+
onBeforeTerminalOwnership: () => {
|
|
1428
|
+
inlineRootProgress?.stop();
|
|
1429
|
+
inlineRootProgress = undefined;
|
|
1430
|
+
},
|
|
1431
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1432
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
finally {
|
|
1436
|
+
inlineRootProgress?.stop();
|
|
1437
|
+
inlineRootProgress = undefined;
|
|
1438
|
+
}
|
|
1439
|
+
return EXIT_CODES.success;
|
|
1440
|
+
}
|
|
1441
|
+
if (selection.kind === "lifecycle") {
|
|
1442
|
+
const noColor = booleanOption(parsed, "no-color") || Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1443
|
+
const progress = startInlineProgress(streams.stderr, !noColor, selection.action === "start" ? "Starting machine" : "Stopping machine");
|
|
1444
|
+
const exit = await runCli([
|
|
1445
|
+
"machines", selection.action, selection.machineId, "--yes",
|
|
1446
|
+
...(noColor ? ["--no-color"] : []),
|
|
1447
|
+
], dependencies);
|
|
1448
|
+
progress.stop();
|
|
1449
|
+
return exit === EXIT_CODES.success
|
|
1450
|
+
? await runCli(noColor ? ["--no-color"] : [], dependencies)
|
|
1451
|
+
: exit;
|
|
1452
|
+
}
|
|
1453
|
+
if (selection.kind === "supervisor-update") {
|
|
1454
|
+
const noColor = booleanOption(parsed, "no-color") || Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1455
|
+
const progress = startInlineProgress(streams.stderr, !noColor, "Updating terminal supervisor");
|
|
1456
|
+
const exit = await runCli([
|
|
1457
|
+
"machines", "update-supervisor", selection.machineId, "--yes",
|
|
1458
|
+
...(noColor ? ["--no-color"] : []),
|
|
1459
|
+
], dependencies);
|
|
1460
|
+
progress.stop();
|
|
1461
|
+
return exit === EXIT_CODES.success
|
|
1462
|
+
? await runCli(noColor ? ["--no-color"] : [], dependencies)
|
|
1463
|
+
: exit;
|
|
1464
|
+
}
|
|
1465
|
+
if (selection.kind === "create") {
|
|
1466
|
+
// E13-R1/R6: the screen chose provider and name; the batch command
|
|
1467
|
+
// owns the `machines.create` gate, the idempotency key and the read.
|
|
1468
|
+
const noColor = booleanOption(parsed, "no-color") || Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1469
|
+
const progress = startInlineProgress(streams.stderr, !noColor, "Creating machine");
|
|
1470
|
+
const exit = await runCli([
|
|
1471
|
+
"machines", "create", "--name", selection.name, "--agent", selection.agent, "--yes",
|
|
1472
|
+
...(noColor ? ["--no-color"] : []),
|
|
1473
|
+
], dependencies);
|
|
1474
|
+
progress.stop();
|
|
1475
|
+
return exit === EXIT_CODES.success
|
|
1476
|
+
? await runCli(noColor ? ["--no-color"] : [], dependencies)
|
|
1477
|
+
: exit;
|
|
1478
|
+
}
|
|
1479
|
+
return await runCli([...rootJourneyArgv(selection), ...menuInvocationOptions(parsed)], {
|
|
1480
|
+
...dependencies,
|
|
1481
|
+
returnToMachinesOnCreateDeclined: true,
|
|
1482
|
+
...(selection.machineId === undefined ? {} : { managedWorkspaceMachineId: selection.machineId }),
|
|
1483
|
+
...(humanAuth === undefined ? {} : { humanAuth }),
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
if (journeyIntent?.target === "reconcile") {
|
|
1487
|
+
if (journeyIntent.agent === "openclaw") {
|
|
1488
|
+
throw unsupportedError("openclaw", "provider_route_unavailable");
|
|
1489
|
+
}
|
|
1490
|
+
const journeyAgent = journeyIntent.agent;
|
|
1491
|
+
if (credentialMode === undefined) {
|
|
1492
|
+
throw new CunaError({
|
|
1493
|
+
code: "cuna.auth.required",
|
|
1494
|
+
message: "The automatic Cuna journey requires authenticated account authority.",
|
|
1495
|
+
exitCode: EXIT_CODES.auth,
|
|
1496
|
+
hint: `Run \`cuna login\` for interactive use, or use an automation credential. ${automationCredentialHint()}`,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
let effects;
|
|
1500
|
+
let stopJourneyWorkspace;
|
|
1501
|
+
// Read before the effects branch, not inside it: the principal and the
|
|
1502
|
+
// workspace are half of the machine-create request identity, so every
|
|
1503
|
+
// journey needs them, including the one built from injected effects.
|
|
1504
|
+
const identity = await client.getIdentity(dependencies.signal);
|
|
1505
|
+
const workspaceId = identity.workspaceId;
|
|
1506
|
+
if (workspaceId === undefined) {
|
|
1507
|
+
throw new CunaError({
|
|
1508
|
+
code: "cuna.journey.workspace_identity_unavailable",
|
|
1509
|
+
message: "The signed-in account has no assigned workspace authority.",
|
|
1510
|
+
exitCode: EXIT_CODES.auth,
|
|
1511
|
+
hint: `Run \`cuna workspace show\` to see the current assignment or waitlist position. Workspace assignment happens at ${CONSOLE_ORIGIN}, not from the CLI.`,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
const journeyScope = Object.freeze({ userId: identity.id, workspaceId });
|
|
1515
|
+
if (remoteMenuLaunch && dependencies.managedWorkspaceMachineId !== undefined) {
|
|
1516
|
+
inlineJourneyProgress?.stop();
|
|
1517
|
+
inlineJourneyProgress = undefined;
|
|
1518
|
+
const preset = await (dependencies.providerScreenRunner ?? runProviderScreen)(client, { kind: "preset", agent: journeyAgent }, undefined, dependencies.signal);
|
|
1519
|
+
if (preset === undefined)
|
|
1520
|
+
return EXIT_CODES.success;
|
|
1521
|
+
const agentSessionId = await launchRemoteWorkspaceSession({
|
|
1522
|
+
preset,
|
|
1523
|
+
providerLaunchState: { stateDirectory: platform.paths.stateDirectory, ownerId: identity.id },
|
|
1524
|
+
confirmNew: async () => { const prompt = createInterface({ input: process.stdin, output: streams.stderr }); try {
|
|
1525
|
+
return /^y(?:es)?$/iu.test((await prompt.question("A previous launch is recorded. Create another session? [y/N; No resumes the recorded launch] ", { signal: dependencies.signal })).trim());
|
|
1526
|
+
}
|
|
1527
|
+
finally {
|
|
1528
|
+
prompt.close();
|
|
1529
|
+
} },
|
|
1530
|
+
client, machineId: dependencies.managedWorkspaceMachineId, workspaceId, agent: journeyAgent,
|
|
1531
|
+
onProgress: (label) => inlineJourneyProgress?.update(label),
|
|
1532
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1533
|
+
...(dependencies.now === undefined ? {} : { now: dependencies.now }),
|
|
1534
|
+
});
|
|
1535
|
+
return await runCli([
|
|
1536
|
+
journeyAgent,
|
|
1537
|
+
"--agent-session", agentSessionId,
|
|
1538
|
+
...(booleanOption(parsed, "no-color") ? ["--no-color"] : []),
|
|
1539
|
+
], { ...dependencies, ...(humanAuth === undefined ? {} : { humanAuth }) });
|
|
1540
|
+
}
|
|
1541
|
+
if (dependencies.automaticJourneyEffectsFactory !== undefined) {
|
|
1542
|
+
effects = dependencies.automaticJourneyEffectsFactory({
|
|
1543
|
+
client,
|
|
1544
|
+
intent: journeyIntent,
|
|
1545
|
+
config,
|
|
1546
|
+
platform,
|
|
1547
|
+
credentialMode,
|
|
1548
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
if (httpTransport === undefined) {
|
|
1553
|
+
throw new CunaError({
|
|
1554
|
+
code: "cuna.journey.workspace_transport_unavailable",
|
|
1555
|
+
message: "The injected API client did not provide authenticated workspace-sync transport authority.",
|
|
1556
|
+
exitCode: EXIT_CODES.unsupported,
|
|
1557
|
+
hint: INTERNAL_DEFECT_HINT,
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
const workspace = createWorkspaceJourneyEffects({
|
|
1561
|
+
client,
|
|
1562
|
+
transport: Object.freeze({
|
|
1563
|
+
request: (request) => httpTransport.request(request),
|
|
1564
|
+
authentication: "authenticated",
|
|
1565
|
+
credentialAuthority: credentialMode === "interactive" ? "interactive" : "api_key",
|
|
1566
|
+
}),
|
|
1567
|
+
profileId: config.profile,
|
|
1568
|
+
userId: identity.id,
|
|
1569
|
+
workspaceId,
|
|
1570
|
+
stateDirectory: platform.paths.stateDirectory,
|
|
1571
|
+
filesystemCapabilities: conservativeFilesystemCapabilities(platform.kind),
|
|
1572
|
+
// The journey never runs under structured output (fenced above), so
|
|
1573
|
+
// this line is always a human-facing note on stderr: above the
|
|
1574
|
+
// spinner while it runs, a plain line otherwise.
|
|
1575
|
+
onNotice: (line) => {
|
|
1576
|
+
if (inlineJourneyProgress !== undefined)
|
|
1577
|
+
inlineJourneyProgress.note(line);
|
|
1578
|
+
else
|
|
1579
|
+
streams.stderr.write(`${line}\n`);
|
|
1580
|
+
},
|
|
1581
|
+
});
|
|
1582
|
+
stopJourneyWorkspace = () => workspace.stopContinuousSync();
|
|
1583
|
+
const runner = runForeground;
|
|
1584
|
+
effects = createApiAgentJourneyEffects({
|
|
1585
|
+
client,
|
|
1586
|
+
requestedAgent: journeyAgent,
|
|
1587
|
+
confirmNewProviderLaunch: async (signal) => { inlineJourneyProgress?.stop(); inlineJourneyProgress = undefined; const prompt = createInterface({ input: process.stdin, output: streams.stderr }); try {
|
|
1588
|
+
return /^y(?:es)?$/iu.test((await prompt.question("A previous launch is recorded. Create another session? [y/N; No resumes the recorded launch] ", { signal })).trim());
|
|
1589
|
+
}
|
|
1590
|
+
finally {
|
|
1591
|
+
prompt.close();
|
|
1592
|
+
} },
|
|
1593
|
+
providerLaunchState: { stateDirectory: platform.paths.stateDirectory, ownerId: identity.id, workspaceId },
|
|
1594
|
+
selectProviderPreset: async (signal) => { inlineJourneyProgress?.stop(); inlineJourneyProgress = undefined; const preset = await (dependencies.providerScreenRunner ?? runProviderScreen)(client, { kind: "preset", agent: journeyAgent }, undefined, signal); if (!preset)
|
|
1595
|
+
throw new CunaError({ code: "cuna.provider.selection_cancelled", message: "Provider selection cancelled. The synchronized Workspace is preserved.", exitCode: EXIT_CODES.usage }); return preset; },
|
|
1596
|
+
inspectWorkspace: workspace.inspectWorkspace,
|
|
1597
|
+
synchronizeWorkspace: workspace.synchronizeWorkspace,
|
|
1598
|
+
// The spinner and the prompt write to the same row of the same
|
|
1599
|
+
// stream, and the spinner repaints every 90 ms, so a question asked
|
|
1600
|
+
// underneath it is erased before it can be read. What a person sees
|
|
1601
|
+
// is "Creating machine" forever, while the CLI waits for an answer to
|
|
1602
|
+
// a question it never showed. Give the row up, ask, take it back.
|
|
1603
|
+
authorizeMachineCreate: async ({ requestedAgent, signal }) => {
|
|
1604
|
+
const resume = inlineJourneyProgress;
|
|
1605
|
+
inlineJourneyProgress?.stop();
|
|
1606
|
+
inlineJourneyProgress = undefined;
|
|
1607
|
+
try {
|
|
1608
|
+
return await (dependencies.authorizeMachineCreate ?? confirmMachineCreate)(requestedAgent, signal);
|
|
1609
|
+
}
|
|
1610
|
+
finally {
|
|
1611
|
+
if (resume !== undefined && streams.stderrIsTTY === true) {
|
|
1612
|
+
inlineJourneyProgress = startInlineProgress(streams.stderr, !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR"), journeyPhaseLabel("create-machine", journeyAgent));
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
attach: async ({ agentSessionId, expectedAgent, signal }) => {
|
|
1617
|
+
const presentationMode = selectNodeForegroundPresentation({
|
|
1618
|
+
platform: nodePlatform(platform.kind),
|
|
1619
|
+
environment: effectiveEnvironment,
|
|
1620
|
+
sessionCount: 1,
|
|
1621
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1622
|
+
});
|
|
1623
|
+
await runner({
|
|
1624
|
+
client,
|
|
1625
|
+
baseUrl: config.baseUrl,
|
|
1626
|
+
browser: dependencies.browser ?? createBrowserOpener(nodePlatform(platform.kind), effectiveEnvironment),
|
|
1627
|
+
agentSessionIds: [agentSessionId],
|
|
1628
|
+
expectedAgentKinds: [expectedAgent],
|
|
1629
|
+
color: !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR"),
|
|
1630
|
+
hostPlatform: nodePlatform(platform.kind),
|
|
1631
|
+
presentationMode,
|
|
1632
|
+
onProgress: (nextLabel) => inlineJourneyProgress?.update(nextLabel),
|
|
1633
|
+
onBeforeTerminalOwnership: () => {
|
|
1634
|
+
inlineJourneyProgress?.stop();
|
|
1635
|
+
inlineJourneyProgress = undefined;
|
|
1636
|
+
},
|
|
1637
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1638
|
+
signal,
|
|
1639
|
+
});
|
|
1640
|
+
},
|
|
1641
|
+
...(dependencies.now === undefined ? {} : { now: dependencies.now }),
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const baseEffects = effects;
|
|
1645
|
+
effects = Object.freeze({
|
|
1646
|
+
...baseEffects,
|
|
1647
|
+
onPhase(phase) {
|
|
1648
|
+
baseEffects.onPhase?.(phase);
|
|
1649
|
+
inlineJourneyProgress?.update(journeyPhaseLabel(phase, journeyAgent));
|
|
1650
|
+
},
|
|
1651
|
+
async attach(input) {
|
|
1652
|
+
inlineJourneyProgress?.update(`Attaching to ${agentDisplayName(journeyAgent)}`);
|
|
1653
|
+
try {
|
|
1654
|
+
await baseEffects.attach(input);
|
|
1655
|
+
}
|
|
1656
|
+
finally {
|
|
1657
|
+
inlineJourneyProgress?.stop();
|
|
1658
|
+
inlineJourneyProgress = undefined;
|
|
1659
|
+
}
|
|
1660
|
+
},
|
|
1661
|
+
});
|
|
1662
|
+
let creationDeclined = false;
|
|
1663
|
+
try {
|
|
1664
|
+
await orchestrateAgentJourney({
|
|
1665
|
+
intent: journeyIntent,
|
|
1666
|
+
effects,
|
|
1667
|
+
scope: journeyScope,
|
|
1668
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
catch (error) {
|
|
1672
|
+
if (dependencies.returnToMachinesOnCreateDeclined === true && streams.stdinIsTTY === true &&
|
|
1673
|
+
dependencies.signal?.aborted !== true && error instanceof CunaError &&
|
|
1674
|
+
error.code === "cuna.journey.machine_create_not_authorized") {
|
|
1675
|
+
creationDeclined = true;
|
|
1676
|
+
}
|
|
1677
|
+
else
|
|
1678
|
+
throw error;
|
|
1679
|
+
}
|
|
1680
|
+
finally {
|
|
1681
|
+
inlineJourneyProgress?.stop();
|
|
1682
|
+
inlineJourneyProgress = undefined;
|
|
1683
|
+
await stopJourneyWorkspace?.();
|
|
1684
|
+
}
|
|
1685
|
+
if (creationDeclined) {
|
|
1686
|
+
return await runCli(["machines", ...menuInvocationOptions(parsed)], {
|
|
1687
|
+
...dependencies, returnToMachinesOnCreateDeclined: false,
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return EXIT_CODES.success;
|
|
1691
|
+
}
|
|
1692
|
+
if (foreground !== undefined) {
|
|
1693
|
+
const expectedAgent = foreground.expectedAgentKinds?.length === 1
|
|
1694
|
+
? foreground.expectedAgentKinds[0]
|
|
1695
|
+
: undefined;
|
|
1696
|
+
const attachLabel = expectedAgent === undefined
|
|
1697
|
+
? foreground.agentSessionIds.length === 1
|
|
1698
|
+
? "Attaching to AgentSession"
|
|
1699
|
+
: `Attaching to ${foreground.agentSessionIds.length} AgentSessions`
|
|
1700
|
+
: foregroundAttachLabel(expectedAgent);
|
|
1701
|
+
const color = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1702
|
+
if (inlineJourneyProgress !== undefined)
|
|
1703
|
+
inlineJourneyProgress.update(attachLabel);
|
|
1704
|
+
else if (streams.stderrIsTTY === true)
|
|
1705
|
+
inlineJourneyProgress = startInlineProgress(streams.stderr, color, attachLabel);
|
|
1706
|
+
else
|
|
1707
|
+
streams.stderr.write(`Cuna: ${attachLabel.toLowerCase()}...\n`);
|
|
1708
|
+
const runner = runForeground;
|
|
1709
|
+
try {
|
|
1710
|
+
await runner({
|
|
1711
|
+
client,
|
|
1712
|
+
baseUrl: config.baseUrl,
|
|
1713
|
+
browser: dependencies.browser ?? createBrowserOpener(nodePlatform(platform.kind), effectiveEnvironment),
|
|
1714
|
+
agentSessionIds: foreground.agentSessionIds,
|
|
1715
|
+
...(foreground.expectedAgentKinds === undefined
|
|
1716
|
+
? {}
|
|
1717
|
+
: { expectedAgentKinds: foreground.expectedAgentKinds }),
|
|
1718
|
+
color,
|
|
1719
|
+
hostPlatform: nodePlatform(platform.kind),
|
|
1720
|
+
...(foregroundPresentation === undefined ? {} : { presentationMode: foregroundPresentation }),
|
|
1721
|
+
onProgress: (nextLabel) => inlineJourneyProgress?.update(nextLabel),
|
|
1722
|
+
onBeforeTerminalOwnership: () => {
|
|
1723
|
+
inlineJourneyProgress?.stop();
|
|
1724
|
+
inlineJourneyProgress = undefined;
|
|
1725
|
+
},
|
|
1726
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1727
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
finally {
|
|
1731
|
+
inlineJourneyProgress?.stop();
|
|
1732
|
+
inlineJourneyProgress = undefined;
|
|
1733
|
+
}
|
|
1734
|
+
return EXIT_CODES.success;
|
|
1735
|
+
}
|
|
1736
|
+
let runtimeFeatures = dependencies.runtimeFeatures;
|
|
1737
|
+
if (parsed.command === "doctor" && runtimeFeatures === undefined) {
|
|
1738
|
+
const backend = dependencies.doctorCredentialBackend ?? new LocalEncryptedSessionBackend({
|
|
1739
|
+
...sessionPaths,
|
|
1740
|
+
platform: nodePlatform(platform.kind),
|
|
1741
|
+
...(dependencies.now === undefined ? {} : { clock: dependencies.now }),
|
|
1742
|
+
});
|
|
1743
|
+
let credentialBackendStatus = "unknown";
|
|
1744
|
+
let credentialBackendReason;
|
|
1745
|
+
try {
|
|
1746
|
+
const evidence = await backend.probe();
|
|
1747
|
+
credentialBackendStatus = evidence.status === "preview" ? "unavailable" : evidence.status;
|
|
1748
|
+
credentialBackendReason = evidence.reason;
|
|
1749
|
+
}
|
|
1750
|
+
catch {
|
|
1751
|
+
credentialBackendStatus = "unavailable";
|
|
1752
|
+
}
|
|
1753
|
+
const browserLoginRemote = booleanOption(parsed, "check-browser-login")
|
|
1754
|
+
? await probeBrowserLoginRemote({
|
|
1755
|
+
config,
|
|
1756
|
+
timeoutMs: effectiveTimeoutMs,
|
|
1757
|
+
...(dependencies.fetch === undefined ? {} : { fetch: dependencies.fetch }),
|
|
1758
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1759
|
+
})
|
|
1760
|
+
: Object.freeze({
|
|
1761
|
+
status: "not_checked",
|
|
1762
|
+
reason: "remote_browser_login_not_checked",
|
|
1763
|
+
});
|
|
1764
|
+
runtimeFeatures = runtimeFeatureGates({
|
|
1765
|
+
platform: platform.kind,
|
|
1766
|
+
credentialBackendStatus,
|
|
1767
|
+
credentialBackendId: backend.backendId,
|
|
1768
|
+
...(credentialBackendReason === undefined ? {} : { credentialBackendReason }),
|
|
1769
|
+
browserLoginRemoteStatus: browserLoginRemote.status,
|
|
1770
|
+
browserLoginRemoteReason: browserLoginRemote.reason,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
if (parsed.command === "machines" && parsed.operands.length === 0 &&
|
|
1774
|
+
!writer.structured && streams.stdinIsTTY && streams.stdoutIsTTY) {
|
|
1775
|
+
inlineMachinesProgress?.stop();
|
|
1776
|
+
inlineMachinesProgress = undefined;
|
|
1777
|
+
const runner = dependencies.machinesExplorerRunner ?? runNodeMachinesExplorer;
|
|
1778
|
+
const selection = await runner({
|
|
1779
|
+
client,
|
|
1780
|
+
color: !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR"),
|
|
1781
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1782
|
+
}, dependencies.now === undefined ? {} : { now: dependencies.now });
|
|
1783
|
+
if (selection !== undefined) {
|
|
1784
|
+
if (selection.kind === "provider-check") {
|
|
1785
|
+
await (dependencies.providerScreenRunner ?? runProviderScreen)(client, { kind: "check", sessionId: selection.agentSessionId }, undefined, dependencies.signal);
|
|
1786
|
+
return await runCli(["machines"], dependencies);
|
|
1787
|
+
}
|
|
1788
|
+
if (selection.kind === "executions") {
|
|
1789
|
+
const outcome = await runExecutionsScreen(client, selection.machineId, undefined, dependencies.signal, { platform, baseUrl: config.baseUrl, profile: config.profile });
|
|
1790
|
+
if (outcome === "cancelled")
|
|
1791
|
+
return EXIT_CODES.success;
|
|
1792
|
+
return await runCli(["machines"], dependencies);
|
|
1793
|
+
}
|
|
1794
|
+
else if (selection.kind === "workspaces") {
|
|
1795
|
+
const identity = await client.getIdentity(dependencies.signal);
|
|
1796
|
+
if (identity.workspaceId === undefined)
|
|
1797
|
+
throw usageError("This account has no assigned workspace.");
|
|
1798
|
+
const outcome = await runWorkspaceSelectionScreen({ client, profileId: config.profile, userId: identity.id, workspaceId: identity.workspaceId, machineId: selection.machineId, stateDirectory: platform.paths.stateDirectory, platform: platform.kind }, dependencies.workspaceRoot ?? process.cwd(), undefined, dependencies.signal);
|
|
1799
|
+
if (outcome === "cancelled")
|
|
1800
|
+
return EXIT_CODES.success;
|
|
1801
|
+
return await runCli(["machines"], dependencies);
|
|
1802
|
+
}
|
|
1803
|
+
else if (selection.kind === "attach") {
|
|
1804
|
+
const attachLabel = foregroundAttachLabel(selection.agent);
|
|
1805
|
+
const color = !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1806
|
+
if (streams.stderrIsTTY === true)
|
|
1807
|
+
inlineRootProgress = startInlineProgress(streams.stderr, color, attachLabel);
|
|
1808
|
+
else
|
|
1809
|
+
streams.stderr.write(`Cuna: ${attachLabel.charAt(0).toLowerCase()}${attachLabel.slice(1)}...\n`);
|
|
1810
|
+
const foregroundRunner = runForeground;
|
|
1811
|
+
try {
|
|
1812
|
+
await foregroundRunner({
|
|
1813
|
+
client,
|
|
1814
|
+
baseUrl: config.baseUrl,
|
|
1815
|
+
browser: dependencies.browser ?? createBrowserOpener(nodePlatform(platform.kind), effectiveEnvironment),
|
|
1816
|
+
agentSessionIds: [selection.agentSessionId],
|
|
1817
|
+
expectedAgentKinds: [selection.agent],
|
|
1818
|
+
color,
|
|
1819
|
+
hostPlatform: nodePlatform(platform.kind),
|
|
1820
|
+
presentationMode: selectNodeForegroundPresentation({
|
|
1821
|
+
platform: nodePlatform(platform.kind),
|
|
1822
|
+
environment: effectiveEnvironment,
|
|
1823
|
+
sessionCount: 1,
|
|
1824
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1825
|
+
}),
|
|
1826
|
+
onProgress: (nextLabel) => inlineRootProgress?.update(nextLabel),
|
|
1827
|
+
onBeforeTerminalOwnership: () => {
|
|
1828
|
+
inlineRootProgress?.stop();
|
|
1829
|
+
inlineRootProgress = undefined;
|
|
1830
|
+
},
|
|
1831
|
+
...(effectiveEnvironment.TERM === undefined ? {} : { terminalKind: effectiveEnvironment.TERM }),
|
|
1832
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
finally {
|
|
1836
|
+
inlineRootProgress?.stop();
|
|
1837
|
+
inlineRootProgress = undefined;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
else if (selection.kind === "launch") {
|
|
1841
|
+
return await runCli([...rootJourneyArgv(selection), ...menuInvocationOptions(parsed)], {
|
|
1842
|
+
...dependencies,
|
|
1843
|
+
returnToMachinesOnCreateDeclined: true,
|
|
1844
|
+
...(selection.machineId === undefined ? {} : { managedWorkspaceMachineId: selection.machineId }),
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
else if (selection.kind === "supervisor-update") {
|
|
1848
|
+
const noColor = booleanOption(parsed, "no-color") || Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1849
|
+
const progress = startInlineProgress(streams.stderr, !noColor, "Updating terminal supervisor");
|
|
1850
|
+
const exit = await runCli([
|
|
1851
|
+
"machines", "update-supervisor", selection.machineId, "--yes",
|
|
1852
|
+
...(noColor ? ["--no-color"] : []),
|
|
1853
|
+
], dependencies);
|
|
1854
|
+
progress.stop();
|
|
1855
|
+
return exit === EXIT_CODES.success
|
|
1856
|
+
? await runCli(["machines", ...(noColor ? ["--no-color"] : [])], dependencies)
|
|
1857
|
+
: exit;
|
|
1858
|
+
}
|
|
1859
|
+
else if (selection.kind === "create") {
|
|
1860
|
+
const noColor = booleanOption(parsed, "no-color") || Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1861
|
+
const progress = startInlineProgress(streams.stderr, !noColor, "Creating machine");
|
|
1862
|
+
const exit = await runCli([
|
|
1863
|
+
"machines", "create", "--name", selection.name, "--agent", selection.agent, "--yes",
|
|
1864
|
+
...(noColor ? ["--no-color"] : []),
|
|
1865
|
+
], dependencies);
|
|
1866
|
+
progress.stop();
|
|
1867
|
+
return exit === EXIT_CODES.success
|
|
1868
|
+
? await runCli(["machines", ...(noColor ? ["--no-color"] : [])], dependencies)
|
|
1869
|
+
: exit;
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
const noColor = booleanOption(parsed, "no-color") || Object.hasOwn(effectiveEnvironment, "NO_COLOR");
|
|
1873
|
+
const progress = startInlineProgress(streams.stderr, !noColor, selection.action === "start" ? "Starting machine" : "Stopping machine");
|
|
1874
|
+
const exit = await runCli([
|
|
1875
|
+
"machines", selection.action, selection.machineId, "--yes",
|
|
1876
|
+
...(noColor ? ["--no-color"] : []),
|
|
1877
|
+
], dependencies);
|
|
1878
|
+
progress.stop();
|
|
1879
|
+
return exit === EXIT_CODES.success
|
|
1880
|
+
? await runCli(["machines", ...(noColor ? ["--no-color"] : [])], dependencies)
|
|
1881
|
+
: exit;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return EXIT_CODES.success;
|
|
1885
|
+
}
|
|
1886
|
+
const commandClock = dependencies.now ?? Date.now;
|
|
1887
|
+
const batchLabel = batchProgressLabel(parsed);
|
|
1888
|
+
if (batchLabel !== undefined && streams.stderrIsTTY === true && !writer.structured) {
|
|
1889
|
+
batchProgress = startInlineProgress(streams.stderr, !booleanOption(parsed, "no-color") && !Object.hasOwn(effectiveEnvironment, "NO_COLOR"), batchLabel);
|
|
1890
|
+
}
|
|
1891
|
+
const result = await executeCommand({
|
|
1892
|
+
parsed,
|
|
1893
|
+
config,
|
|
1894
|
+
client,
|
|
1895
|
+
now: commandClock(),
|
|
1896
|
+
capabilityClock: commandClock,
|
|
1897
|
+
...(dependencies.convergencePoller === undefined
|
|
1898
|
+
? {}
|
|
1899
|
+
: { convergencePoller: dependencies.convergencePoller }),
|
|
1900
|
+
...(credentialMode === undefined ? {} : { credentialMode }),
|
|
1901
|
+
...(runtimeFeatures === undefined ? {} : { runtimeFeatures }),
|
|
1902
|
+
// Where `agent-sessions create` looks for `.cuna/workspace.json`. Passed
|
|
1903
|
+
// in rather than read inside the command so a test can point it at a
|
|
1904
|
+
// scratch folder.
|
|
1905
|
+
workspaceRoot: dependencies.workspaceRoot ?? process.cwd(),
|
|
1906
|
+
// The one batch command that keeps a durable local note across
|
|
1907
|
+
// invocations needs the same adapter every other on-disk state uses.
|
|
1908
|
+
platform,
|
|
1909
|
+
});
|
|
1910
|
+
batchProgress?.stop();
|
|
1911
|
+
batchProgress = undefined;
|
|
1912
|
+
writer.success(result.command, result.data, result.human);
|
|
1913
|
+
return EXIT_CODES.success;
|
|
1914
|
+
}
|
|
1915
|
+
catch (unknownError) {
|
|
1916
|
+
inlineMachinesProgress?.stop();
|
|
1917
|
+
inlineJourneyProgress?.stop();
|
|
1918
|
+
inlineRootProgress?.stop();
|
|
1919
|
+
authProgress?.stop();
|
|
1920
|
+
stopAuthProgressCancellation?.();
|
|
1921
|
+
batchProgress?.stop();
|
|
1922
|
+
const error = unknownError instanceof CredentialBoundaryError
|
|
1923
|
+
? credentialError(unknownError)
|
|
1924
|
+
: unknownError instanceof RuntimeBoundaryError
|
|
1925
|
+
? runtimeError(unknownError)
|
|
1926
|
+
: normalizeError(unknownError);
|
|
1927
|
+
if (interactiveCloseUi && streams.stderrIsTTY === true &&
|
|
1928
|
+
(dependencies.signal?.aborted === true || error.code === "cuna.journey.cancelled")) {
|
|
1929
|
+
await animateInlineClose(streams.stderr, interactiveCloseColor);
|
|
1930
|
+
return EXIT_CODES.success;
|
|
1931
|
+
}
|
|
1932
|
+
if (interactiveRootUi && streams.stderrIsTTY === true && isTerminalResumeHandleConflict(error)) {
|
|
1933
|
+
writeTerminalReconnectConflict(streams.stderr, interactiveRootColor);
|
|
1934
|
+
return error.exitCode;
|
|
1935
|
+
}
|
|
1936
|
+
const supervisorReadiness = terminalSupervisorReadiness(error);
|
|
1937
|
+
// The initial Cuna journey is interactive whenever stdin/stdout are TTYs.
|
|
1938
|
+
// Some Windows hosts expose stderr as a non-TTY even though it is visible
|
|
1939
|
+
// to the person. Never leak an internal capability name merely because
|
|
1940
|
+
// that host classification is conservative; render the same plain-language
|
|
1941
|
+
// recovery instead.
|
|
1942
|
+
if (interactiveCloseUi && supervisorReadiness !== undefined) {
|
|
1943
|
+
writeTerminalSupervisorReadiness(streams.stderr, interactiveCloseColor && streams.stderrIsTTY === true, supervisorReadiness, terminalSessionIds);
|
|
1944
|
+
// A process that is gone is a final refusal, not a network condition:
|
|
1945
|
+
// retrying cannot change it, so the exit code must not say "retry".
|
|
1946
|
+
return supervisorReadiness === "ended" ? EXIT_CODES.policy : error.exitCode;
|
|
1947
|
+
}
|
|
1948
|
+
writer.error(label, error);
|
|
1949
|
+
return error.exitCode;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
export function memoryStreams(input) {
|
|
1953
|
+
let stdout = "";
|
|
1954
|
+
let stderr = "";
|
|
1955
|
+
const stdoutStream = new Writable({ write(chunk, _encoding, callback) { stdout += chunk.toString(); callback(); } });
|
|
1956
|
+
const stderrStream = new Writable({ write(chunk, _encoding, callback) { stderr += chunk.toString(); callback(); } });
|
|
1957
|
+
return Object.freeze({
|
|
1958
|
+
streams: Object.freeze({
|
|
1959
|
+
stdout: stdoutStream,
|
|
1960
|
+
stderr: stderrStream,
|
|
1961
|
+
stdoutIsTTY: input?.stdoutIsTTY ?? false,
|
|
1962
|
+
stdinIsTTY: input?.stdinIsTTY ?? false,
|
|
1963
|
+
stderrIsTTY: input?.stderrIsTTY ?? input?.stdoutIsTTY ?? false,
|
|
1964
|
+
}),
|
|
1965
|
+
stdout: () => stdout,
|
|
1966
|
+
stderr: () => stderr,
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
//# sourceMappingURL=run.js.map
|