Skip to content

Commit dda6991

Browse files
committed
fix(cli): os environments runs on the os cloud login session through one resolver
createControlPlaneApiClient (utils/api-client.ts) chooses the stored session for list, show, create, bind and switch: credentials.json's session where it targets the server (with no --url it names it), else cloud.json's on the same terms; an explicit url neither file names keeps credentials.json's session as before and never gets the cloud token. The active environment comes from the chosen file, and switch / create --activate no longer write a cloud environment id into credentials.json when they ran on the cloud session. The refusal with no session names os cloud login too. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
1 parent eb9dcda commit dda6991

11 files changed

Lines changed: 223 additions & 46 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
`os environments list | show | create | bind | switch` run on the `os cloud login` session
6+
7+
Clause-②: no
8+
9+
The documented hosted flow is `os cloud login`, then `os environments create`. The five
10+
`os environments` subcommands read only `~/.objectstack/credentials.json` (the `os login`
11+
session), so with only `~/.objectstack/cloud.json` they exited 1 with
12+
`Authentication required` before sending any request, while `os login --help` sends hosted
13+
users to `os cloud login`.
14+
15+
All five now choose their session in one shared resolver:
16+
17+
- With no `--url` / `OS_CLOUD_URL`, they use the `os login` session when there is one, which
18+
is the same behaviour as before. Otherwise they use the `os cloud login` session and the URL
19+
it recorded.
20+
- With a `--url`, they use the session whose file names that server, `credentials.json` first.
21+
When neither file names it, they use `credentials.json`'s session as before. The cloud token
22+
is never sent to a URL other than its own.
23+
- The active environment sent with each request comes from the chosen session's file.
24+
`os environments switch` and `create --activate` no longer write a cloud environment id into
25+
`credentials.json` when they ran on the cloud session.
26+
27+
With no session at all, the `Authentication required` message now names `os cloud login` as
28+
well as `os login`. `os package publish` is unchanged: it still reads only `cloud.json`.

‎packages/cli/README.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,19 @@ Typical flow (build → publish → install into an environment, seeding sample
9696

9797
```bash
9898
os compile # → dist/objectstack.json
99-
os cloud login # one-time; the session os package publish reads
100-
os environments create --org "$ORG" --name "Dev" --activate # does NOT read that session — see below
99+
os cloud login # one-time; the session os package publish and os environments read
100+
os environments create --org "$ORG" --name "Dev" --activate
101101
os package publish --env <env-id> --install --seed-sample-data
102102
```
103103

104-
`os environments create` does not use the session `os cloud login` stored: give
105-
it `--url` and `--token` (or `OS_CLOUD_URL` / `OS_TOKEN`), or an `os login`
106-
session. Without either it exits 1 with `Authentication required`.
104+
`os environments` runs on either stored session. With no `--url` it talks to
105+
the server of the `os login` session (`credentials.json`) when there is one,
106+
else to the server of the `os cloud login` session (`cloud.json`), with that
107+
session's token. A `--url` (or `OS_CLOUD_URL`) picks the session that names
108+
that server, `credentials.json`'s first — so with both stored, a `--url` naming
109+
the cloud uses the cloud session. A `--url` neither file names gets
110+
`credentials.json`'s token as before, never `cloud.json`'s. With no session and
111+
no `--token` / `OS_TOKEN`, it exits 1 with `Authentication required`.
107112

108113
`os package publish` registers a `sys_package` (keyed by a reverse-domain
109114
`--manifest-id`, derived from the artifact when omitted), snapshots the
@@ -124,7 +129,7 @@ Two stored sessions exist, and each command authenticates with one of them:
124129
| `os cloud login` | `-u, --url` (env `OS_CLOUD_URL`, default `https://cloud.objectos.ai`) | none — `-e, --email` / `-p, --password`, or the browser device flow | writes `~/.objectstack/cloud.json` |
125130
| `os cloud whoami` / `os cloud logout` | — | — | reads / deletes `~/.objectstack/cloud.json` |
126131
| `os package publish`, `os plugin publish` | `-s, --server` (env `OS_CLOUD_URL`); else the URL in `cloud.json`; else `https://cloud.objectos.ai` | `-t, --token` (env `OS_CLOUD_API_KEY`, then `OS_TOKEN`) | `~/.objectstack/cloud.json` — the `os cloud login` session |
127-
| `os environments list` / `show` / `create` / `bind` / `switch` | `-u, --url` (env `OS_CLOUD_URL`); else the URL in `credentials.json`; else `http://localhost:3000` | `-t, --token` (env `OS_TOKEN`) | `~/.objectstack/credentials.json` — the `os login` session, **not** `os cloud login`'s |
132+
| `os environments list` / `show` / `create` / `bind` / `switch` | `-u, --url` (env `OS_CLOUD_URL`); else the URL of the stored session it uses; else `http://localhost:3000` | `-t, --token` (env `OS_TOKEN`) | No `--url`: `~/.objectstack/credentials.json` (the `os login` session), else `~/.objectstack/cloud.json` (the `os cloud login` session). With `--url`: the file that names that server, `credentials.json` first; when neither does, `credentials.json` as before. `cloud.json`'s token is never sent to another URL |
128133

129134
`os package install` is not a cloud command: it installs into a running runtime
130135
(`-r, --runtime`, env `OS_RUNTIME_URL`, default `http://localhost:3000`) and signs

‎packages/cli/src/commands/environments/bind.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from 'node:path';
55
import fs from 'node:fs/promises';
66
import { spawnSync } from 'node:child_process';
77
import { printError, printStep, printKV, emitJson, isExitSignal, errorCodeFields } from '../../utils/format.js';
8-
import { createApiClient, requireAuth } from '../../utils/api-client.js';
8+
import { createControlPlaneApiClient, requireControlPlaneAuth } from '../../utils/api-client.js';
99
import { formatOutput } from '../../utils/output-formatter.js';
1010

1111
/**
@@ -99,8 +99,8 @@ export default class EnvironmentsBind extends Command {
9999
this.exit(1);
100100
}
101101

102-
const { client, token } = await createApiClient({ url: flags.url, token: flags.token });
103-
requireAuth(token);
102+
const { client, token } = await createControlPlaneApiClient({ url: flags.url, token: flags.token });
103+
requireControlPlaneAuth(token);
104104

105105
// Fetch existing metadata so we don't blow it away.
106106
const current = await client.environments.get(args.environmentId);

‎packages/cli/src/commands/environments/create-clone-from.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ vi.mock('../../utils/api-client.js', async (importOriginal) => {
5252
const actual = await importOriginal<typeof import('../../utils/api-client.js')>();
5353
return {
5454
...actual,
55-
createApiClient: async () => ({ client: stub.client, token: stub.token, baseUrl: 'https://door.test' }),
55+
createControlPlaneApiClient: async () => ({ client: stub.client, token: stub.token, baseUrl: 'https://door.test' }),
5656
};
5757
});
5858

‎packages/cli/src/commands/environments/create.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Command, Flags } from '@oclif/core';
44
import { printError, emitJson, isExitSignal, errorCodeFields } from '../../utils/format.js';
5-
import { createApiClient, requireAuth } from '../../utils/api-client.js';
5+
import { createControlPlaneApiClient, requireControlPlaneAuth } from '../../utils/api-client.js';
66
import { formatOutput } from '../../utils/output-formatter.js';
77
import { readAuthConfig, writeAuthConfig } from '../../utils/auth-config.js';
88
import { recordCloudActiveEnvironmentId } from '../../utils/active-environment.js';
@@ -18,12 +18,18 @@ import { recordCloudActiveEnvironmentId } from '../../utils/active-environment.j
1818
* lives in a repo this one never compiles against, so a class name here rots
1919
* with nothing to catch it.
2020
*
21+
* Authenticates through `createControlPlaneApiClient` — `credentials.json`'s
22+
* session where it targets the server, else `cloud.json`'s (the order is
23+
* written once, there).
24+
*
2125
* On success, optionally activates the new environment for the current session
2226
* and persists `activeEnvironmentId` into `~/.objectstack/credentials.json`
23-
* (unless `--no-activate` is passed). When the control plane it just talked
24-
* to IS the one `~/.objectstack/cloud.json` records, the same id is written
25-
* there as well, so `os package publish --install` can install into the
26-
* environment you just created without repeating the uuid.
27+
* (unless `--no-activate` is passed, or the create ran on `cloud.json`'s
28+
* session, whose server is not the one `credentials.json` names). When the
29+
* control plane it just talked to IS the one `~/.objectstack/cloud.json`
30+
* records, the same id is written there as well, so `os package publish
31+
* --install` can install into the environment you just created without
32+
* repeating the uuid.
2733
*
2834
* `os environments switch` records it through the SAME helper. An environment
2935
* id is only meaningful against the server that issued it, and that gate is
@@ -79,8 +85,8 @@ export default class EnvironmentsCreate extends Command {
7985
const { flags } = await this.parse(EnvironmentsCreate);
8086

8187
try {
82-
const { client, token, baseUrl } = await createApiClient({ url: flags.url, token: flags.token });
83-
requireAuth(token);
88+
const { client, token, baseUrl, session } = await createControlPlaneApiClient({ url: flags.url, token: flags.token });
89+
requireControlPlaneAuth(token);
8490

8591
// Resolve the artifact to an absolute path so the server can read it
8692
// regardless of its CWD. Bail early if the file is missing — better
@@ -123,7 +129,9 @@ export default class EnvironmentsCreate extends Command {
123129
// a second copy of it.
124130
recordedForCloud = await recordCloudActiveEnvironmentId(res.environment.id, baseUrl);
125131

126-
const cfg = await readAuthConfig().catch(() => null);
132+
// Runtime store — skipped on `cloud.json`'s session, for the reason
133+
// `os environments switch` gives: that server is not credentials.json's.
134+
const cfg = session === 'cloud' ? null : await readAuthConfig().catch(() => null);
127135
if (cfg) {
128136
cfg.activeEnvironmentId = res.environment.id;
129137
cfg.lastUsedAt = new Date().toISOString();

‎packages/cli/src/commands/environments/list.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Command, Flags } from '@oclif/core';
44
import { printError, emitJson, errorCodeFields } from '../../utils/format.js';
5-
import { createApiClient, requireAuth } from '../../utils/api-client.js';
5+
import { createControlPlaneApiClient, requireControlPlaneAuth } from '../../utils/api-client.js';
66
import { formatOutput } from '../../utils/output-formatter.js';
77

88
/**
@@ -38,12 +38,12 @@ export default class EnvironmentsList extends Command {
3838
const { flags } = await this.parse(EnvironmentsList);
3939

4040
try {
41-
const { client, token, environmentId: activeId } = await createApiClient({
41+
const { client, token, environmentId: activeId } = await createControlPlaneApiClient({
4242
url: flags.url,
4343
token: flags.token,
4444
});
4545

46-
requireAuth(token);
46+
requireControlPlaneAuth(token);
4747

4848
const res = await client.environments.list({
4949
organization_id: flags.org,

‎packages/cli/src/commands/environments/show.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Args, Command, Flags } from '@oclif/core';
44
import { printError, emitJson, errorCodeFields } from '../../utils/format.js';
5-
import { createApiClient, requireAuth } from '../../utils/api-client.js';
5+
import { createControlPlaneApiClient, requireControlPlaneAuth } from '../../utils/api-client.js';
66
import { formatOutput } from '../../utils/output-formatter.js';
77

88
/**
@@ -38,8 +38,8 @@ export default class EnvironmentsShow extends Command {
3838
const { args, flags } = await this.parse(EnvironmentsShow);
3939

4040
try {
41-
const { client, token } = await createApiClient({ url: flags.url, token: flags.token });
42-
requireAuth(token);
41+
const { client, token } = await createControlPlaneApiClient({ url: flags.url, token: flags.token });
42+
requireControlPlaneAuth(token);
4343

4444
const res = await client.environments.get(args.id);
4545

‎packages/cli/src/commands/environments/switch.ts‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22

33
import { Args, Command, Flags } from '@oclif/core';
44
import { printError } from '../../utils/format.js';
5-
import { createApiClient, requireAuth } from '../../utils/api-client.js';
5+
import { createControlPlaneApiClient, requireControlPlaneAuth } from '../../utils/api-client.js';
66
import { readAuthConfig, writeAuthConfig } from '../../utils/auth-config.js';
77
import { recordCloudActiveEnvironmentId } from '../../utils/active-environment.js';
88

99
/**
1010
* `os environments switch <id>` — set the active environment for this CLI session.
1111
*
12-
* Calls `POST /api/v1/cloud/environments/:id/activate` to update the
13-
* server-side session, then persists `activeEnvironmentId` into
12+
* Authenticates through `createControlPlaneApiClient` — `credentials.json`'s
13+
* session where it targets the server, else `cloud.json`'s (the order is
14+
* written once, there). Calls `POST /api/v1/cloud/environments/:id/activate`
15+
* to update the server-side session, then persists `activeEnvironmentId` into
1416
* `~/.objectstack/credentials.json` so subsequent CLI commands (and any
1517
* client they create via `createApiClient`) automatically target this
16-
* environment.
18+
* environment — unless the switch ran on `cloud.json`'s session, whose server
19+
* is not the one `credentials.json` names.
1720
*
1821
* When the control plane it just talked to IS the one `~/.objectstack/cloud.json`
1922
* records, the same id is written there as well, so `os package publish --install`
@@ -48,8 +51,8 @@ export default class EnvironmentsSwitch extends Command {
4851
const { args, flags } = await this.parse(EnvironmentsSwitch);
4952

5053
try {
51-
const { client, token, baseUrl } = await createApiClient({ url: flags.url, token: flags.token });
52-
requireAuth(token);
54+
const { client, token, baseUrl, session } = await createControlPlaneApiClient({ url: flags.url, token: flags.token });
55+
requireControlPlaneAuth(token);
5356

5457
// Sanity-check the id resolves — fail fast before writing the cred file
5558
const lookup = await client.environments.get(args.id);
@@ -68,10 +71,12 @@ export default class EnvironmentsSwitch extends Command {
6871
// `credentials.json` at all).
6972
const recordedForCloud = await recordCloudActiveEnvironmentId(environment.id, baseUrl);
7073

71-
// Runtime store: unchanged behaviour. This is the copy `createApiClient`
72-
// reads, so the `data` / `meta` / `environments` families keep targeting
73-
// the environment you just switched to.
74-
const cfg = await readAuthConfig().catch(() => null);
74+
// Runtime store: the copy `createApiClient` reads, so the `data` / `meta`
75+
// families keep targeting the environment you just switched to. Skipped
76+
// when this switch ran on `cloud.json`'s session: the server it talked to
77+
// is cloud.json's, and the environment would not resolve on the server
78+
// `credentials.json` names.
79+
const cfg = session === 'cloud' ? null : await readAuthConfig().catch(() => null);
7580
if (cfg) {
7681
cfg.activeEnvironmentId = environment.id;
7782
cfg.lastUsedAt = new Date().toISOString();

0 commit comments

Comments
 (0)