Repository navigation
feat(cli): named environments (scope env, --env) - #1474
Cedric Vidal (cedricvidal) wants to merge 6 commits into
Conversation
Add saved per-environment API URL, token and project connections, selected with root --env or 'scope env use'. Extracted from #1470. Co-authored-by: Copilot App <[email protected]>
Test Results (Node.js 22)test: Run #212
🎉 All tests passed! |
Use environmentConfigDir() (XDG_CONFIG_HOME / ~/.config / %LOCALAPPDATA%) for config.json and update-check.json so all CLI state lives together. An existing ~/.config/scope/config.json is still read until rewritten. Co-authored-by: Copilot App <[email protected]>
Test Results (Node.js 22)test: Run #214
🎉 All tests passed! |
Documents scope env, root --env, legacy SCOPE_* configuration, connection and project precedence, and the config directory layout. Co-authored-by: Copilot App <[email protected]>
Test Results (Node.js 22)test: Run #216
🎉 All tests passed! |
The update-check cache now follows XDG_CONFIG_HOME. GitHub runners set it, so the tests shared one cache and later cases hit the cooldown. Point it at the per-test temp dir alongside HOME. Co-authored-by: Copilot App <[email protected]>
Test Results (Node.js 22)test: Run #222
🎉 All tests passed! |
Preserve both command registrations and sensitive-body redaction; verify named connection selection, credential isolation and URL overrides for secret operations. Co-authored-by: Copilot App <[email protected]>
Test Results (Node.js 22)test: Run #229
🎉 All tests passed! |
Wassim Chegham (manekinekko)
left a comment
There was a problem hiding this comment.
The action-scoped connection design and token-redaction coverage look good. I found four reproducible correctness issues in the connection resolution, existing command compatibility, and profile replacement paths; details are inline.
| ); | ||
| if (apiOption && command.getOptionValueSource(apiOption.attributeName()) === "cli") return undefined; | ||
| const name = selectedEnvironmentName(command, store); | ||
| return name ? store.read(name) : undefined; |
There was a problem hiding this comment.
[P2] Reject an explicitly empty environment selector
Commander accepts --env "", but this truthiness check treats it as no named connection and uses the legacy URL, token, and project. With local active and ambient SCOPE_API_URL/SCOPE_PROJECT configured, scope --env "$TARGET_ENV" run submit ... sends a POST to the ambient deployment when TARGET_ENV is empty, rather than rejecting the invalid name. This can create runs on the wrong deployment despite an explicit selector. Treat only undefined as absent and validate every explicitly supplied string before running the action.
There was a problem hiding this comment.
Fixed. An explicit root --env is now treated as selected whenever its value is not undefined, so an empty string goes through environment-name validation and fails before the command action instead of falling back to the active or legacy connection. I added a regression test with both an active environment and legacy configuration present that verifies no request is sent.
| export async function apiFetch(baseUrl: string | undefined, path: string, init?: ApiFetchInit): Promise<Response> { | ||
| const url = `${normalizeUrl(baseUrl)}${withProjectId(resolveApiPath(path), init?.projectId)}`; | ||
| const environment = currentEnvironment(); | ||
| const url = `${normalizeUrl(resolveApiUrl(baseUrl))}${withProjectId(resolveApiPath(path), init?.projectId)}`; |
There was a problem hiding this comment.
[P2] Resolve the named URL before upload preflight output too
Resolving the named URL only inside the transport leaves run upload and run upload-batch calling normalizeUrl(options.url) first (run.ts:938,948,1022,1026). With a valid named environment and no ambient SCOPE_API_URL, both uploads and both --dry-run paths fail with No API URL configured before reaching apiFetch. If an ambient legacy URL exists, the dry run instead advertises that unrelated server. Route these preflight/display calls through the named URL resolver as well, and cover both upload commands with named-only configuration.
There was a problem hiding this comment.
Fixed. Both run upload and run upload-batch now use the same named URL resolver as the transport for their dry-run destination and pre-upload status output. Coverage exercises both commands in dry-run and real multipart-upload modes with no ambient SCOPE_API_URL, and verifies the named URL, project, and token are used.
| const format = (options.output ?? "table") as OutputFormat; | ||
| const baseUrl = normalizeUrl(options.url); | ||
| const baseUrl = options.url; | ||
| const projectId = requireProjectId(options.project); |
There was a problem hiding this comment.
[P2] Preserve project-free codebase operations by UUID
This new unconditional guard breaks existing legacy invocations such as scope codebase revisions <uuid> --url <api> when no project is selected: they now fail before making any request. The API's direct /codebases/:id lookup and its revision/delete/upload operations identify the parent by its globally unique _id and do not require projectId. The same guard is repeated in resolve, upload, and delete. Keep the project optional for the direct ID lookup and require it only if resolution falls back to the project-scoped slug list; list/create should still require a project.
There was a problem hiding this comment.
Fixed. Direct codebase lookup and the revisions, resolve, upload, and delete operations by resolved ID no longer require or append projectId. A project is required only after direct lookup returns 404 and resolution falls back to the project-scoped slug list. Tests cover every direct by-ID operation without a project, plus the scoped slug fallback and missing-project error.
| .option("--force", "Replace an existing environment of the same name") | ||
| .action((name: string, options: { url: string; token?: string; force?: boolean }) => { | ||
| const store = new EnvironmentStore(); | ||
| const replaced = options.force && store.list().some((entry) => entry.name === name); |
There was a problem hiding this comment.
[P2] Allow --force to replace a malformed profile
store.list() parses every saved environment before store.add() can replace the requested one. If any .env lacks SCOPE_API_URL or has an invalid URL, scope env add <name> --url <valid-url> --force throws, including when <name> is the broken profile being replaced. An unrelated malformed profile also prevents forced creation of a new name. Determine whether the requested file exists without parsing all profiles, or have add() report whether it replaced an entry, so --force can restore valid configuration.
There was a problem hiding this comment.
Fixed. EnvironmentStore.add now checks only the requested target file and returns whether it replaced an existing entry, so env add --force no longer calls store.list() or parses unrelated profiles. Regression coverage verifies that --force can repair a malformed target profile and create a new profile while another malformed profile exists.
Co-authored-by: Copilot App <[email protected]> Copilot-Session: d981480a-1bde-49a6-858a-89eafad54ff7
Test Results (Node.js 22)test: Run #254
🎉 All tests passed! |
Summary
Adds named CLI environments: saved per-environment API URL, optional Scope token, and project preference.
scope env add|use|set|unset|list|show|remove.scope env use --cleardeselects the active environment.--env <name>for a single command, or the saved selection fromenv use.-u/--url(or MCP--api-url) wins, then--env, then the saved selection, then the legacySCOPE_API_URL/SCOPE_TOKEN/SCOPE_PROJECTconfiguration.ScopeCommandpins the connection inAsyncLocalStoragefor the whole command, covering REST, SSE, polling, retries and downloads.list/show.codebasecommands now useapiFetch, so they send auth and--project/projectId. Main's API already requiresprojectIdon codebase list/create.run/reportSSE now uses an auth-awareapiEventSource.Extracted from #1470 as an independent PR (based on
main). It overlaps lightly with #1473 inapps/cli/src/utils/api-client.tsandindex.ts; whichever merges second needs a trivial rebase.Demos
CLI walkthrough recorded against a local Scope stack (API + Token Manager from
main, CLI from this branch).Before
N/A (new command)
After
Add
localandstagingenvironments, select one, list projects with noSCOPE_API_URLset, save a project, use--envfor a one-off, check 0600 files, then clear the selection.scope-env-walkthrough.mp4
Testing
cd apps/cli && npx tsc --noEmit: passedpnpm exec vitest run apps/cli/src: passed (156 tests, including the newenvironments.test.ts)XDG_CONFIG_HOME:env add,env use,env list,env showand--env local project listall work. The files are created 0600 and the token is shown as[REDACTED].Documentation and compatibility
apps/cli/README.md: "Named environments" section, with the existing content moved under "Legacy configuration"docs/architecture/cli-distribution.md: "Named connections" section and the project precedence for named mode.agents/skills/scope-cli/SKILL.md: "Named Environments" tablegetting-started/configure-cli.md, under Getting Started). It coversscope env, root--env, legacySCOPE_*variables, precedence and where config is stored.install-cli.mdandsubmitting-requests-cli.mdnow link to it.env, root--env, and--projecton codebase commands.Checklist
up()/down()and keep it CosmosDB-compatible. N/ANOTICE/NOTICE-REVIEW.txtwithpnpm noticeas needed. N/A