Skip to content

feat(cli): named environments (scope env, --env) - #1474

Open
Cedric Vidal (cedricvidal) wants to merge 6 commits into
mainfrom
cedricvidal-cli-named-environments
Open

Cedric Vidal (cedricvidal) wants to merge 6 commits into
mainfrom
cedricvidal-cli-named-environments

Conversation

@cedricvidal

@cedricvidal Cedric Vidal (cedricvidal) commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds named CLI environments: saved per-environment API URL, optional Scope token, and project preference.

  • Commands: scope env add|use|set|unset|list|show|remove. scope env use --clear deselects the active environment.
  • Selection: root --env <name> for a single command, or the saved selection from env use.
  • Precedence: an explicit -u/--url (or MCP --api-url) wins, then --env, then the saved selection, then the legacy SCOPE_API_URL / SCOPE_TOKEN / SCOPE_PROJECT configuration.
  • Pinning: ScopeCommand pins the connection in AsyncLocalStorage for the whole command, covering REST, SSE, polling, retries and downloads.
  • Storage: files are owner-only (0600) and tokens are redacted in list/show.
  • Behaviour changes in existing commands:
    • codebase commands now use apiFetch, so they send auth and --project/projectId. Main's API already requires projectId on codebase list/create.
    • run/report SSE now uses an auth-aware apiEventSource.

Extracted from #1470 as an independent PR (based on main). It overlaps lightly with #1473 in apps/cli/src/utils/api-client.ts and index.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 local and staging environments, select one, list projects with no SCOPE_API_URL set, save a project, use --env for a one-off, check 0600 files, then clear the selection.

scope-env-walkthrough.mp4

Testing

  • cd apps/cli && npx tsc --noEmit: passed
  • pnpm exec vitest run apps/cli/src: passed (156 tests, including the new environments.test.ts)
  • Manual run in a temporary XDG_CONFIG_HOME: env add, env use, env list, env show and --env local project list all 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" table
  • Website: new page "Configure the CLI" (getting-started/configure-cli.md, under Getting Started). It covers scope env, root --env, legacy SCOPE_* variables, precedence and where config is stored. install-cli.md and submitting-requests-cli.md now link to it.
  • Compatibility: with no environment selected, behaviour is unchanged. The snapshot adds env, root --env, and --project on codebase commands.

Checklist

  • If Portal features changed, keep CLI capabilities in sync. N/A
  • If Portal components changed, update their Storybook stories. N/A
  • If database changes require a migration, include up() / down() and keep it CosmosDB-compatible. N/A
  • If dependencies changed, update the lockfile and regenerate NOTICE / NOTICE-REVIEW.txt with pnpm notice as needed. N/A
  • Video showing the behavior before the suggested change. N/A
  • Video showing the behavior after the suggested change.

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]>
@github-actions github-actions Bot added type: documentation Documentation additions, corrections, and improvements. area: cli Scope command-line interface and terminal workflows. language: javascript Work involving JavaScript code, tooling, or dependencies. area: skills Skill registration, installation, management, resolution, and delivery. topic: testing Test coverage, test infrastructure, and validation quality. labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #212

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
3218 3218 0 0 0 0 0 1m13s

🎉 All tests passed!

Github Test Reporter

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]>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #214

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
3220 3220 0 0 0 0 0 1m4s

🎉 All tests passed!

Github Test Reporter

Documents scope env, root --env, legacy SCOPE_* configuration,
connection and project precedence, and the config directory layout.

Co-authored-by: Copilot App <[email protected]>
@github-actions github-actions Bot added the topic: ui Portal, visual presentation, layout, and interface components. label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #216

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
3220 3220 0 0 0 0 0 1m9s

🎉 All tests passed!

Github Test Reporter

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]>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #222

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
3220 3220 0 0 0 0 0 1m27s

🎉 All tests passed!

Github Test Reporter

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]>
@github-actions

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #229

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
3258 3258 0 0 0 0 0 1m29s

🎉 All tests passed!

Github Test Reporter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/cli/src/utils/connection.ts Outdated
);
if (apiOption && command.getOptionValueSource(apiOption.attributeName()) === "cli") return undefined;
const name = selectedEnvironmentName(command, store);
return name ? store.read(name) : undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/cli/src/commands/codebase.ts Outdated
const format = (options.output ?? "table") as OutputFormat;
const baseUrl = normalizeUrl(options.url);
const baseUrl = options.url;
const projectId = requireProjectId(options.project);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/cli/src/commands/env.ts Outdated
.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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@github-actions

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #254

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
3265 3265 0 0 0 0 0 1m28s

🎉 All tests passed!

Github Test Reporter

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Scope command-line interface and terminal workflows. area: skills Skill registration, installation, management, resolution, and delivery. language: javascript Work involving JavaScript code, tooling, or dependencies. topic: testing Test coverage, test infrastructure, and validation quality. topic: ui Portal, visual presentation, layout, and interface components. type: documentation Documentation additions, corrections, and improvements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants