Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/30871-os-deploy-dev-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/cli': patch
'create-objectstack': patch
---

feat(cli): `os deploy` — one command from a local checkout to your ObjectOS Cloud environment; installs pick the organization's only environment, keep it across `os cloud login`, and refuse before any write when none resolves

Clause-②: no
Metadata-protocol: none

**`os deploy`** (new). With no flags it builds the app (the same compile `os build` runs), publishes it as `<manifest.version>-dev.<YYYYMMDDHHMMSS>` (a UTC semver pre-release, so deploying again never needs a version bump and never collides with an existing version), installs exactly that version into your organization's environment, and waits until the environment serves it (it reads the control plane's served-version report for the package and, since an environment rebuilds only when it gets a request, sends the environment one on every poll). A failed local build prints its errors and exits 1 having published nothing; a build the environment reports as failed prints the environment's reason and exits 1. Flags only for the uncommon case: `--env <id>` when your account reaches several environments, `--no-wait`, `--wait-timeout <seconds>` (default 600: a failing build is reported only after three failed rounds in a row). Against a control plane that does not yet report which version an environment serves, the command says so and does not claim the version is live.

**`os package publish --install`** keeps its meaning (a release version, the manifest's own), and now resolves its environment like `os deploy`: `--env` / `$OS_ENVIRONMENT_ID`, then the environment `os environments switch` recorded for this control plane, then the organization's only environment, asked of the control plane and printed. With none, several, or an unreadable list it exits 1 **before registering the package or publishing anything**, naming how to pass one. It used to print "Skipping auto-install", publish anyway and exit 0.

**`os cloud login`** no longer drops the active environment: re-logging in to the same control plane as the same account keeps `activeEnvironmentId` in `~/.objectstack/cloud.json`. A different control plane or a different account still starts without one.

**`os dev`** offers the deploy on a key once the local server is up: press `d` to run `os deploy` in the same terminal; when not logged in to ObjectOS Cloud, the same key logs in first. There is no deploy on save. **`os build`** ends with the next step (`os deploy`), and bare `os` lists the everyday commands first. New projects from `create-objectstack` (and `os init`'s app template) ship `npm run deploy`.
68 changes: 65 additions & 3 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ Starts development mode. Three usage shapes:
2. **With a pre-built artifact** (`--artifact <path|url>`) — skips auto-compile and boots the artifact directly. No `objectstack.config.ts` needed in cwd. Useful for *trying out a published app in dev mode without cloning its source.*
3. **Monorepo root** (cwd has `pnpm-workspace.yaml`) — orchestrates `pnpm -r dev` across packages.

Once the local server is up, `os dev` in an interactive terminal offers
**`d` to deploy**: it runs [`os deploy`](#os-deploy) in the same terminal, and
when you are not logged in to ObjectOS Cloud the same key runs `os cloud login`
first. Saving a file never deploys.

```bash
os dev # Auto-compile cwd config, then start
os dev my-package # Workspace package (monorepo orchestration mode)
Expand Down Expand Up @@ -3063,7 +3068,9 @@ os cloud login --email me@acme.com --password secret # CI
Like `os login`, in an interactive terminal it uses the browser-based device
flow: it prints a one-time verification URL and polls until you approve. If
cloud credentials already exist it exits successfully with "Already logged in";
pass `--force` to re-authenticate.
pass `--force` to re-authenticate. Re-authenticating to the same control plane
as the same account keeps the active environment recorded in `cloud.json`; a
different control plane or account starts without one.

##### `os cloud login --json` is NDJSON — the same exception as `os login`

Expand Down Expand Up @@ -3139,6 +3146,53 @@ os environments bind <environment-id> --artifact ./dist/objectstack.json --build
is reserved for the server-side reseed endpoint; use it only when that endpoint
is available in your deployment.

### Deploy

#### `os deploy`

Puts the app in the current directory on your ObjectOS Cloud environment, with
no flags. It builds the app (the compile [`os build`](#os-compile) runs),
publishes it as `<manifest.version>-dev.<YYYYMMDDHHMMSS>` — a UTC semver
pre-release, so deploying again never needs a version bump — installs that
version into your organization's environment, and waits until the environment
serves it. Scaffolded projects run it as `npm run deploy`, and
[`os dev`](#os-dev) runs it when you press `d`.

```bash
os cloud login # once per machine
os deploy
os deploy --env env_abc123 # your account reaches several environments
os deploy --no-wait # return once the install is recorded
```

The environment is resolved exactly as
[`os package publish --install`](#os-package-publish) resolves it: `--env`,
then the active environment for this control plane, then your organization's
only environment. Every refusal comes before anything is written: not logged
in, no environment to name, a failed build (its errors are printed), or a
`manifest.version` that is not semver. A build the environment itself reports
as failed prints the environment's reason and exits `1`; so does a wait that
times out, naming what it waited for. An environment rebuilds only when it gets
a request, so while it waits the command sends it one on every poll. A failing
build is reported only after three failed rounds in a row, so it can read as
still building for a few minutes before the reason appears. On a control plane
that does not yet report which version an environment serves, the command says
so and does not claim the new version is live.

There is no deploy on save: on ObjectOS Cloud your organization's environment
is its production, so a deploy is always a deliberate act.

| Flag | Env equivalent | Purpose |
|---|---|---|
| `--env <id>` | `OS_ENVIRONMENT_ID` | Environment to deploy into (default: the active one for this control plane, then your organization's only one) |
| `--no-wait` | — | Return once the install is recorded |
| `--wait-timeout <seconds>` | — | How long to wait for the environment to serve the new version (default `600`) |
| `-s, --server <url>` | `OS_CLOUD_URL` | Control-plane URL. Default `https://cloud.objectos.ai`, or the URL recorded by `os cloud login` |
| `-t, --token <key>` | `OS_CLOUD_API_KEY` | Bearer token; `$OS_TOKEN` and `~/.objectstack/cloud.json` are the fallbacks |

`os package publish` stays the command for a **release**: it publishes the
version the manifest declares, with `--install` optional.

### Packages

The two commands that move a compiled app onto a platform. They target
Expand Down Expand Up @@ -3191,6 +3245,14 @@ resolves an install target by bare id, with no name or short-id rescue. A value
written by an older CLI into `credentials.json` is copied across once, and only
when both files' `url`s agree.

With neither `--env` nor an active environment, `--install` asks the control
plane for your organization's environments and uses the only one, printing
which; archived and platform environments are not counted. On ObjectOS Cloud
every organization has exactly one environment, so this is the usual case.
None, several, or a list that cannot be read is a refusal: the command exits
`1` before it registers the package or publishes a version, and lists the
candidates with how to pass `--env`.

**Options:**

| Flag | Env equivalent | Purpose |
Expand All @@ -3205,8 +3267,8 @@ when both files' `url`s agree.
| `--category <slug>` | — | Marketplace category slug (`crm`, `hr`, `devtools`, …) |
| `--visibility <level>` | — | `org` (installable across your organization) · `private` (explicit grants only) · `marketplace` (public after review). Omitted: not sent, so the control plane decides — an existing package keeps its visibility, a new one gets the control plane's default (`org` on ObjectOS Cloud) |
| `--org <id>` | `OS_ORG_ID` | `owner_org_id`. Required with a bearer key in service mode; ignored in user mode |
| `--env <id>` | `OS_ENVIRONMENT_ID` | Environment to install the new version into. Defaults to the environment [`os environments switch`](#cloud-environments) — or `os environments create --activate` — recorded **for this control plane** in `~/.objectstack/cloud.json` |
| `--install` | — | Auto-install into `--env` after publishing. With no `--env`, no `$OS_ENVIRONMENT_ID` and no active environment for this control plane, it reports that and publishes without installing |
| `--env <id>` | `OS_ENVIRONMENT_ID` | Environment to install the new version into. Defaults to the environment [`os environments switch`](#cloud-environments) — or `os environments create --activate` — recorded **for this control plane** in `~/.objectstack/cloud.json`, then to your organization's only environment |
| `--install` | — | Install the new version after publishing, into `--env` or the environment it defaults to. With no environment to name — none, several, or a list the control plane would not give — it exits `1` **before publishing anything**, naming how to pass one |
| `--seed-sample-data` | — | Include sample data in that auto-install |
| `--pre-release` | — | Mark the version as a pre-release (also inferred — see below) |
| `--submit` | — | Submit the new version for marketplace review. Needs the package's visibility to be `marketplace` (already stored, or set with `--visibility marketplace`) and a complete listing |
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"oclif": {
"bin": "os",
"dirname": "objectstack",
"helpClass": "./dist/help.js",
"commands": {
"strategy": "pattern",
"target": "./dist/commands",
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ export default class Build extends Compile {

/** `os build` is what a deployment runs, so it says what the artifact leaves behind. */
protected override reportsLocalCustomizations = true;

/** `os build` is the step before `os deploy`, so it says so (`utils/next-steps.ts`). */
protected override printsNextStep = true;
}
46 changes: 37 additions & 9 deletions packages/cli/src/commands/cloud/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,33 @@ import { Command, Flags } from '@oclif/core';
import type { CliExitCode } from '../../utils/format.js';
import { printHeader, printKV, printSuccess, printError, emitJson, errorCodeFields } from '../../utils/format.js';
import { loginWithBrowser, loginWithPassword } from '../../utils/auth-flows.js';
import { DEFAULT_CLOUD_URL, readCloudConfig, writeCloudConfig } from '../../utils/cloud-config.js';
import { DEFAULT_CLOUD_URL, readCloudConfig, tryReadCloudConfig, writeCloudConfig, type CloudConfig } from '../../utils/cloud-config.js';
import { isSameControlPlane } from '../../utils/active-environment.js';

/**
* The `cloud.json` a successful login writes: the new credential, plus the
* active environment the previous file recorded — when it is still about the
* same control plane and the same account.
*
* A login used to rewrite the file whole, so `activeEnvironmentId` (written by
* `os environments switch` / `os environments create --activate`, read back as
* the install target by `os package publish --install` and `os deploy`) was
* gone after every login and the next install refused for want of it.
*
* Kept only when both hold, because an environment id only resolves on the
* server it was chosen on (`active-environment.ts`) and only for an account
* that can reach it:
* - the previous file's url names the same control plane as this login's;
* - it is the same account — the user ids agree, or one side recorded none.
* Otherwise the id is dropped, exactly as before. Nothing else is carried
* over: every other key is the new login's.
*/
export function mergeCloudLogin(previous: CloudConfig | undefined, next: CloudConfig): CloudConfig {
if (!previous?.activeEnvironmentId) return next;
if (!isSameControlPlane(previous.url, next.url)) return next;
if (previous.userId && next.userId && previous.userId !== next.userId) return next;
return { ...next, activeEnvironmentId: previous.activeEnvironmentId };
}

/**
* Emit ONE NDJSON record on stdout — the only `--json` writer in this command.
Expand Down Expand Up @@ -219,13 +245,15 @@ export default class CloudLogin extends Command {
})
: await this.fallbackPasswordPrompt(url, flags.email);

await writeCloudConfig({
url,
token: result.token,
email: result.user?.email,
userId: result.user?.id,
createdAt: new Date().toISOString(),
});
await writeCloudConfig(
mergeCloudLogin(await tryReadCloudConfig(), {
url,
token: result.token,
email: result.user?.email,
userId: result.user?.id,
createdAt: new Date().toISOString(),
}),
);

if (flags.json) {
// Record 2 of 2 on the device path, and the only record on the
Expand All @@ -237,7 +265,7 @@ export default class CloudLogin extends Command {
if (result.user?.id) printKV('User ID', result.user.id);
console.log('');
console.log(' Credentials stored in ~/.objectstack/cloud.json');
console.log(' Next: `os package publish` to push a package, `os cloud whoami` to verify.');
console.log(' Next: `os deploy` to put this app on your cloud environment, `os cloud whoami` to verify.');
console.log('');
}
} catch (error: any) {
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { findExtensionFieldSuffixRefusals } from '../utils/extension-field-suffi
// 3a-vii runs, over the automation engine's own judge.
import { findFlowCycleRefusals } from '../utils/flow-undeclared-cycle.js';
import { localCustomizationNotice, readLocalCustomizations } from '../utils/local-customizations.js';
import { BUILD_NEXT_STEP } from '../utils/next-steps.js';

export default class Compile extends Command {
static override description = 'Compile ObjectStack configuration to JSON artifact';
Expand Down Expand Up @@ -129,6 +130,13 @@ export default class Compile extends Command {
*/
protected reportsLocalCustomizations = false;

/**
* Whether a successful text-face run ends with the next step
* (`BUILD_NEXT_STEP`). On for `os build` only: `os compile` is what `os dev`
* runs on every boot and rebuild, where a line per rebuild is noise.
*/
protected printsNextStep = false;

async run(): Promise<void> {
const { args, flags } = await this.parse(Compile);
const timer = createTimer();
Expand Down Expand Up @@ -1471,6 +1479,11 @@ export default class Compile extends Command {
}
}

if (this.printsNextStep) {
console.log(chalk.cyan(` ${BUILD_NEXT_STEP}`));
console.log('');
}

} catch (error: any) {
if (isExitSignal(error)) throw error;
if (flags.json) {
Expand Down
Loading
Loading