@sequenceholdings/studio-cli 0.1.10 → 0.1.12

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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Tiered environment discovery.
3
+ *
4
+ * The published package ships with only the `local` environment baked in.
5
+ * Every other deployment URL is *discovered* from the authenticated
6
+ * `GET /api/platform/environments` endpoint and cached on disk:
7
+ *
8
+ * - `anonymous` — no usable token (or the endpoint rejected it):
9
+ * `local` only, no catalog cached.
10
+ * - `sequence` — Sequence staff / trusted M2M: full catalog
11
+ * (local, staging, production, banksouth) + preview targeting.
12
+ * - `partner` — any other authenticated identity: local + their
13
+ * deployment(s).
14
+ *
15
+ * This is a visibility/UX layer, not access control — every deployment
16
+ * still enforces Auth0 + FGA per request. A user who knows a URL can
17
+ * always add it to config.toml; the server rejecting their token is the
18
+ * real gate.
19
+ *
20
+ * One bootstrap hostname is baked into the package for the discovery
21
+ * call itself. All deployments share the same Auth0 audience, so any
22
+ * identity's token validates there; the endpoint 401s anonymously.
23
+ */
24
+ export type EnvTier = 'anonymous' | 'sequence' | 'partner';
25
+ export interface CatalogEnvironment {
26
+ name: string;
27
+ url: string;
28
+ }
29
+ export interface EnvCatalog {
30
+ tier: Exclude<EnvTier, 'anonymous'>;
31
+ environments: CatalogEnvironment[];
32
+ /** Unix ms timestamp of the successful fetch. */
33
+ fetchedAt: number;
34
+ /**
35
+ * Auth0 `sub` of the identity that fetched this catalog. Reads ignore the
36
+ * cache when the current identity differs (logout, seqapi re-login as
37
+ * someone else), so one user's tier never carries over to another.
38
+ */
39
+ subject: string;
40
+ }
41
+ /** The host the discovery call goes to. Override for tests / self-hosting. */
42
+ export declare function bootstrapUrl(): string;
43
+ export declare function catalogPath(): string;
44
+ /**
45
+ * Read the cached catalog from disk. Returns null when missing, malformed,
46
+ * or fetched by a *different* identity than the one the CLI would
47
+ * authenticate as right now — callers treat all of those as tier
48
+ * `anonymous`. The subject check means a logout (token file removed) or a
49
+ * re-login as someone else invalidates the cache immediately, without
50
+ * waiting for the next discovery call.
51
+ */
52
+ export declare function readCachedCatalog(): Promise<EnvCatalog | null>;
53
+ export declare function clearCatalog(): Promise<void>;
54
+ /**
55
+ * Fetch the environment catalog from the discovery endpoint and cache it.
56
+ *
57
+ * Returns the fresh catalog, or null when the caller is effectively
58
+ * anonymous: no token available, or the endpoint rejected the token
59
+ * (401/403 — the stale cache is cleared so visibility downgrades too).
60
+ * Network/server errors leave any existing cache in place and rethrow so
61
+ * callers can distinguish "offline" from "not authorized".
62
+ */
63
+ export declare function fetchCatalog(): Promise<EnvCatalog | null>;
@@ -0,0 +1,111 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { currentIdentitySubject, decodeJwtSub, tryGetAccessToken } from './auth.js';
6
+ const DEFAULT_BOOTSTRAP_URL = 'https://atlas.seqholdings.com';
7
+ /** The host the discovery call goes to. Override for tests / self-hosting. */
8
+ export function bootstrapUrl() {
9
+ return (process.env['SEQ_STUDIO_DISCOVERY_URL']?.trim().replace(/\/+$/, '') ||
10
+ DEFAULT_BOOTSTRAP_URL);
11
+ }
12
+ export function catalogPath() {
13
+ return join(homedir(), '.config', 'lattice', 'environments.json');
14
+ }
15
+ function isValidCatalog(value) {
16
+ if (typeof value !== 'object' || value === null)
17
+ return false;
18
+ const candidate = value;
19
+ return ((candidate.tier === 'sequence' || candidate.tier === 'partner') &&
20
+ Array.isArray(candidate.environments) &&
21
+ candidate.environments.every((env) => typeof env === 'object' &&
22
+ env !== null &&
23
+ typeof env.name === 'string' &&
24
+ typeof env.url === 'string') &&
25
+ typeof candidate.fetchedAt === 'number' &&
26
+ typeof candidate.subject === 'string' &&
27
+ candidate.subject.length > 0);
28
+ }
29
+ /**
30
+ * Read the cached catalog from disk. Returns null when missing, malformed,
31
+ * or fetched by a *different* identity than the one the CLI would
32
+ * authenticate as right now — callers treat all of those as tier
33
+ * `anonymous`. The subject check means a logout (token file removed) or a
34
+ * re-login as someone else invalidates the cache immediately, without
35
+ * waiting for the next discovery call.
36
+ */
37
+ export async function readCachedCatalog() {
38
+ const path = catalogPath();
39
+ if (!existsSync(path))
40
+ return null;
41
+ let catalog;
42
+ try {
43
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
44
+ catalog = isValidCatalog(parsed) ? parsed : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ if (!catalog)
50
+ return null;
51
+ const subject = await currentIdentitySubject();
52
+ if (subject === null || subject !== catalog.subject)
53
+ return null;
54
+ return catalog;
55
+ }
56
+ async function saveCatalog(catalog) {
57
+ const path = catalogPath();
58
+ await mkdir(dirname(path), { recursive: true });
59
+ await writeFile(path, JSON.stringify(catalog, null, 2) + '\n', {
60
+ encoding: 'utf8',
61
+ mode: 0o600,
62
+ });
63
+ }
64
+ export async function clearCatalog() {
65
+ await rm(catalogPath(), { force: true });
66
+ }
67
+ /**
68
+ * Fetch the environment catalog from the discovery endpoint and cache it.
69
+ *
70
+ * Returns the fresh catalog, or null when the caller is effectively
71
+ * anonymous: no token available, or the endpoint rejected the token
72
+ * (401/403 — the stale cache is cleared so visibility downgrades too).
73
+ * Network/server errors leave any existing cache in place and rethrow so
74
+ * callers can distinguish "offline" from "not authorized".
75
+ */
76
+ export async function fetchCatalog() {
77
+ // Fail closed when an M2M secret is configured but minting fails, so
78
+ // headless callers (CI) see the real Auth0 error instead of a silent
79
+ // downgrade to the anonymous tier.
80
+ const token = await tryGetAccessToken({ failClosedForM2m: true });
81
+ if (!token) {
82
+ await clearCatalog();
83
+ return null;
84
+ }
85
+ const response = await fetch(`${bootstrapUrl()}/api/platform/environments`, {
86
+ headers: { Authorization: `Bearer ${token}` },
87
+ });
88
+ if (response.status === 401 || response.status === 403) {
89
+ await clearCatalog();
90
+ return null;
91
+ }
92
+ if (!response.ok) {
93
+ throw new Error(`Environment discovery failed (${response.status}) against ${bootstrapUrl()}.`);
94
+ }
95
+ const body = (await response.json());
96
+ if ((body.tier !== 'sequence' && body.tier !== 'partner') ||
97
+ !Array.isArray(body.environments)) {
98
+ throw new Error('Environment discovery returned an unexpected response shape.');
99
+ }
100
+ const catalog = {
101
+ tier: body.tier,
102
+ environments: body.environments,
103
+ fetchedAt: Date.now(),
104
+ // Bind the cache to the identity that fetched it. `unknown` never
105
+ // matches a real subject, so an undecodable token yields a catalog
106
+ // that works for this invocation but is not trusted from disk later.
107
+ subject: decodeJwtSub(token) ?? 'unknown',
108
+ };
109
+ await saveCatalog(catalog);
110
+ return catalog;
111
+ }
@@ -0,0 +1 @@
1
+ export declare function runEnvsCommand(sub: string | undefined, _rest: string[]): Promise<number>;
@@ -0,0 +1,74 @@
1
+ import { readConfig, configPath } from '../config.js';
2
+ import { bootstrapUrl, fetchCatalog, readCachedCatalog, } from '../env-catalog.js';
3
+ const ENVS_USAGE = `usage:
4
+ seq-studio envs list show the environments visible to your identity
5
+ seq-studio envs refresh re-fetch the environment catalog for your identity
6
+
7
+ The published CLI ships with only the "local" environment. After you
8
+ authenticate (seq-studio login, or AUTH0_M2M_CLIENT_SECRET for headless use),
9
+ "envs refresh" discovers the environments your identity may target; the
10
+ catalog is cached at ~/.config/lattice/environments.json and also refreshes
11
+ lazily the first time you pass an -e <env> that isn't cached yet.
12
+
13
+ Custom entries in ${configPath()} are always honored on top.
14
+ `;
15
+ export async function runEnvsCommand(sub, _rest) {
16
+ switch (sub) {
17
+ case 'list':
18
+ return listCommand();
19
+ case 'refresh':
20
+ return refreshCommand();
21
+ case undefined:
22
+ case 'help':
23
+ case '--help':
24
+ case '-h':
25
+ console.log(ENVS_USAGE);
26
+ return sub ? 0 : 1;
27
+ default:
28
+ console.error(`unknown envs command: ${sub}`);
29
+ console.error(ENVS_USAGE);
30
+ return 1;
31
+ }
32
+ }
33
+ async function listCommand() {
34
+ const [config, catalog] = await Promise.all([readConfig(), readCachedCatalog()]);
35
+ const discovered = new Set(catalog?.environments.map((env) => env.name) ?? []);
36
+ console.log(`tier: ${config.tier}`);
37
+ if (catalog) {
38
+ console.log(`catalog fetched: ${new Date(catalog.fetchedAt).toISOString()}`);
39
+ }
40
+ console.log('');
41
+ const width = Math.max(...Object.keys(config.envs).map((name) => name.length));
42
+ for (const [name, { url }] of Object.entries(config.envs)) {
43
+ const source = name === 'local' && !discovered.has(name)
44
+ ? 'built-in'
45
+ : discovered.has(name)
46
+ ? 'discovered'
47
+ : 'config.toml';
48
+ console.log(` ${name.padEnd(width)} ${url} (${source})`);
49
+ }
50
+ if (config.tier === 'anonymous') {
51
+ console.log('');
52
+ console.log('Not authenticated — only "local" is visible. Authenticate with ' +
53
+ '`seq-studio login` (or set AUTH0_M2M_CLIENT_SECRET), then run: seq-studio envs refresh');
54
+ }
55
+ return 0;
56
+ }
57
+ async function refreshCommand() {
58
+ let catalog;
59
+ try {
60
+ catalog = await fetchCatalog();
61
+ }
62
+ catch (err) {
63
+ console.error(`envs refresh: FAIL — ${err instanceof Error ? err.message : String(err)}`);
64
+ return 1;
65
+ }
66
+ if (!catalog) {
67
+ console.log(`Not authenticated against ${bootstrapUrl()} — catalog cleared; only ` +
68
+ '"local" (plus any config.toml entries) is visible.\n' +
69
+ 'Authenticate with `seq-studio login` (or set AUTH0_M2M_CLIENT_SECRET) and retry.');
70
+ return 0;
71
+ }
72
+ console.log(`tier: ${catalog.tier} — ${catalog.environments.length} environment(s) cached.`);
73
+ return listCommand();
74
+ }
@@ -82,5 +82,5 @@ export declare function functionsRollbackCommand(args: ParsedArgs): Promise<numb
82
82
  /** Minimal dotenv parser — KEY=VALUE lines, quotes stripped, comments skipped. */
83
83
  export declare function parseDotenv(content: string): Record<string, string>;
84
84
  export declare function functionsDeleteCommand(args: ParsedArgs): Promise<number>;
85
- export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <local|staging|production|banksouth|preview:<slug>> \u00B7 --fn <slug> \u00B7 --dir <path>\n --from-env-file <path> (default: .env) source file for secret values\n --no-wait \u00B7 --yes\n --no-provision (deploy) update-only: error instead of registering a new\n shell, writing secret values, or attaching secrets (CI sweep)\n\n Source for build/deploy: a local --dir (default .), a platform git-service\n repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a\n branch/tag/commit (default: the repo's default branch). Remote sources record\n the pinned commit as provenance (never dirty) and NEVER read a repo-committed\n .env for secret values \u2014 provision secrets server-side or pass a local\n --from-env-file (resolved against your cwd).\n\n --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT\n (repo:read scope \u2014 `seq-studio auth pat create --scopes repo:read`, or\n Atlas \u2192 Settings \u2192 Tokens). It's the same token you clone the repo with;\n --env + seq-studio login are still needed to resolve the repo and deploy.\n";
85
+ export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <env|preview:<slug>> (see: seq-studio envs list) \u00B7 --fn <slug> \u00B7 --dir <path>\n --from-env-file <path> (default: .env) source file for secret values\n --no-wait \u00B7 --yes\n --no-provision (deploy) update-only: error instead of registering a new\n shell, writing secret values, or attaching secrets (CI sweep)\n\n Source for build/deploy: a local --dir (default .), a platform git-service\n repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a\n branch/tag/commit (default: the repo's default branch). Remote sources record\n the pinned commit as provenance (never dirty) and NEVER read a repo-committed\n .env for secret values \u2014 provision secrets server-side or pass a local\n --from-env-file (resolved against your cwd).\n\n --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT\n (repo:read scope \u2014 `seq-studio auth pat create --scopes repo:read`, or\n Atlas \u2192 Settings \u2192 Tokens). It's the same token you clone the repo with;\n --env + seq-studio login are still needed to resolve the repo and deploy.\n";
86
86
  export declare function runFunctionsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline';
7
7
  import { Writable } from 'node:stream';
8
8
  import { load as parseYaml } from 'js-yaml';
9
9
  import { getAccessToken } from '../auth.js';
10
- import { readConfig, resolveEnv } from '../config.js';
10
+ import { resolveEnvWithDiscovery } from '../config.js';
11
11
  import { AtlasApiError, deleteJson, getJson, postJson } from '../atlas-client.js';
12
12
  import { clarifyApplyFailureReason, printCliError } from '../cli-errors.js';
13
13
  import { confirmYes } from '../prompt.js';
@@ -24,10 +24,9 @@ export function flagBool(flags, ...keys) {
24
24
  return keys.some((key) => flags[key] === true || flags[key] === 'true');
25
25
  }
26
26
  export async function buildContext(args) {
27
- const config = await readConfig();
28
27
  const requested = (typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
29
28
  (typeof args.flags.e === 'string' ? args.flags.e : undefined);
30
- const env = resolveEnv({ config, requested });
29
+ const env = await resolveEnvWithDiscovery({ requested });
31
30
  const token = await getAccessToken();
32
31
  return { env, token };
33
32
  }
@@ -309,7 +308,7 @@ export async function functionsInitCommand(args) {
309
308
  console.log(' # public npm instead (the deploy worker re-resolves server-side).');
310
309
  console.log(' seq-studio functions build # local pre-flight checks');
311
310
  console.log(' # Add secret names to managed-function.yml (secrets: [MY_SECRET]) and values to .env');
312
- console.log(' seq-studio functions deploy -e staging # upload, apply secrets, then deploy');
311
+ console.log(' seq-studio functions deploy -e <env> # upload, apply secrets, then deploy');
313
312
  return 0;
314
313
  }
315
314
  // ---------------------------------------------------------------------------
@@ -942,7 +941,7 @@ export const FUNCTIONS_USAGE = `usage:
942
941
  seq-studio functions delete [--yes] archive function + tear down GCP resources
943
942
  (version history is retained)
944
943
 
945
- Flags: -e/--env <local|staging|production|banksouth|preview:<slug>> · --fn <slug> · --dir <path>
944
+ Flags: -e/--env <env|preview:<slug>> (see: seq-studio envs list) · --fn <slug> · --dir <path>
946
945
  --from-env-file <path> (default: .env) source file for secret values
947
946
  --no-wait · --yes
948
947
  --no-provision (deploy) update-only: error instead of registering a new
package/dist/login.js CHANGED
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process';
5
5
  import { homedir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN, saveTokens, seqapiTokenPath, } from './auth.js';
8
+ import { clearCatalog, fetchCatalog } from './env-catalog.js';
8
9
  const DEFAULT_REDIRECT_PORT = 5099;
9
10
  const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
10
11
  function base64Url(input) {
@@ -195,6 +196,22 @@ export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBro
195
196
  export async function login() {
196
197
  await loginWithPkce({});
197
198
  console.log(`Authenticated. Tokens saved to ${seqapiTokenPath()}.`);
199
+ // Refresh the environment catalog for the new identity immediately, so a
200
+ // previously cached catalog from another identity (or tier) never lingers
201
+ // past a login. Drop the old cache first — if the refresh below fails, the
202
+ // prior identity's catalog must not survive the login either.
203
+ // Best-effort: discovery being unreachable must not fail login.
204
+ await clearCatalog();
205
+ try {
206
+ const catalog = await fetchCatalog();
207
+ if (catalog) {
208
+ console.log(`Environment catalog refreshed (tier: ${catalog.tier}, ` +
209
+ `${catalog.environments.length} environment(s)).`);
210
+ }
211
+ }
212
+ catch {
213
+ console.log('Could not refresh the environment catalog — run: seq-studio envs refresh');
214
+ }
198
215
  }
199
216
  /**
200
217
  * Pre-unification artifact-studio token file. `seq-studio artifact` commands
@@ -209,5 +226,8 @@ function legacyArtifactTokenPath() {
209
226
  export async function logout() {
210
227
  await rm(seqapiTokenPath(), { force: true });
211
228
  await rm(legacyArtifactTokenPath(), { force: true });
229
+ // Drop the cached environment catalog so visibility downgrades with the
230
+ // identity — a logged-out terminal must not keep the old tier's env list.
231
+ await clearCatalog();
212
232
  console.log('Logged out of seq-studio and seqapi.');
213
233
  }
package/dist/main.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * seq-studio secrets <sub> manage org-owned Managed Secrets
9
9
  * seq-studio repos <sub> manage platform git-service repos
10
10
  * seq-studio auth <sub> manage git-service PATs
11
+ * seq-studio envs <sub> list/refresh discovered environments
11
12
  * seq-studio login authenticate interactively with Auth0
12
13
  * seq-studio logout remove cached user tokens
13
14
  * seq-studio doctor check token + env + writer gate
package/dist/main.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * seq-studio secrets <sub> manage org-owned Managed Secrets
9
9
  * seq-studio repos <sub> manage platform git-service repos
10
10
  * seq-studio auth <sub> manage git-service PATs
11
+ * seq-studio envs <sub> list/refresh discovered environments
11
12
  * seq-studio login authenticate interactively with Auth0
12
13
  * seq-studio logout remove cached user tokens
13
14
  * seq-studio doctor check token + env + writer gate
@@ -25,13 +26,16 @@ const TOP_LEVEL_USAGE = `usage:
25
26
  seq-studio repos <sub> [args] list | namespaces | show | create | clone | pull | delete
26
27
  seq-studio auth <sub> [args] pat create | pat list | pat revoke
27
28
  seq-studio orm <sub> [args] init | validate | plan | apply
29
+ seq-studio envs <sub> list | refresh — environments visible to your identity
28
30
  seq-studio login authenticate in the browser
29
31
  seq-studio logout remove cached user tokens
30
32
  seq-studio doctor [-e <env>] diagnose config, auth, and writer gate
33
+ seq-studio version print the installed version
31
34
  seq-studio help show this message
32
35
 
33
36
  Authenticate with: seq-studio login
34
- Env URLs come from ~/.config/lattice/config.toml (built-ins: local, staging, production, banksouth).
37
+ Built-in env: local. Authenticated identities discover more via
38
+ \`seq-studio envs refresh\`; custom entries live in ~/.config/lattice/config.toml.
35
39
  `;
36
40
  const PROCESS_USAGE = `usage:
37
41
  seq-studio process init <dir>
@@ -82,9 +86,20 @@ export async function run(argv = process.argv.slice(2)) {
82
86
  const { runOrmCommand } = await import('./orm/delegate.js');
83
87
  return runOrmCommand(sub, rest);
84
88
  }
89
+ case 'envs': {
90
+ const { runEnvsCommand } = await import('./envs/commands.js');
91
+ return runEnvsCommand(sub, rest);
92
+ }
85
93
  case 'login':
86
94
  case 'logout':
87
95
  return runSessionCommand({ argument: sub, command: namespace });
96
+ case 'version':
97
+ case '--version':
98
+ case '-v': {
99
+ const { currentVersion } = await import('./update-check.js');
100
+ console.log(currentVersion());
101
+ return 0;
102
+ }
88
103
  case 'doctor':
89
104
  return doctorCommand(parseArgs([sub, ...rest].filter(Boolean)));
90
105
  default:
@@ -6,16 +6,17 @@
6
6
  * SEQUENCE_ORM_BASE_URL, share the seqapi token, forward argv.
7
7
  */
8
8
  import { getAccessToken, tryGetAccessToken } from '../auth.js';
9
- import { readConfig, resolveEnv } from '../config.js';
9
+ import { resolveEnvWithDiscovery } from '../config.js';
10
10
  import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
11
11
  const ORM_USAGE = `usage:
12
12
  seq-studio orm init <dir> scaffold a namespace directory
13
- seq-studio orm validate [dir] parse + validate definitions, print the content hash
13
+ seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
14
14
  seq-studio orm plan [dir] -e <env> compile definitions and diff against the registry
15
- seq-studio orm diff [dir] write the next committed migration (--check verifies, --allow-destructive consents)
16
- seq-studio orm apply [dir] -e <env> register the definitions and apply them to the env
15
+ seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh types.gen.ts (the everyday command)
16
+ seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents)
17
17
 
18
- Built-in envs: local, staging, production, banksouth.
18
+ Environments: see \`seq-studio envs list\` (built-in: local; more are
19
+ discovered after you authenticate).
19
20
  Authenticate with: seq-studio login
20
21
  `;
21
22
  export async function runOrmCommand(sub, rest) {
@@ -26,8 +27,7 @@ export async function runOrmCommand(sub, rest) {
26
27
  const normalized = normalizeShortEnvFlag(rest);
27
28
  const requested = readEnvFromArgv(normalized);
28
29
  if (requested) {
29
- const config = await readConfig();
30
- const resolved = resolveEnv({ config, requested });
30
+ const resolved = await resolveEnvWithDiscovery({ requested });
31
31
  process.env['SEQUENCE_ORM_BASE_URL'] = resolved.url;
32
32
  }
33
33
  const token = await tryGetAccessToken({ failClosedForM2m: true });
@@ -12,7 +12,7 @@ import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
12
12
  import { getJson, getJsonOr404, postJson, AtlasApiError } from '../atlas-client.js';
13
13
  import { generateProcessFiles } from './codegen.js';
14
14
  import { getAccessToken, NotLoggedInError } from '../auth.js';
15
- import { ENV_NAMES, readConfig, resolveEnv } from '../config.js';
15
+ import { readConfig, resolveEnvWithDiscovery } from '../config.js';
16
16
  import { buildBundleFromProcesses, summarizeBundle } from './build.js';
17
17
  import { buildResolveProcessPinFromEnv } from './resolve-process-pin.js';
18
18
  import { loadBundleForPublish } from './local-bundle.js';
@@ -92,9 +92,8 @@ const SHORT_FLAG_ALIASES = {
92
92
  '-o': 'out',
93
93
  };
94
94
  async function getEnvAndToken(args) {
95
- const config = await readConfig();
96
95
  const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
97
- const env = resolveEnv({ config, requested });
96
+ const env = await resolveEnvWithDiscovery({ requested });
98
97
  let token;
99
98
  try {
100
99
  token = await getAccessToken();
@@ -667,16 +666,16 @@ export async function doctorCommand(args) {
667
666
  }
668
667
  else {
669
668
  lines.push(`config: ok — default_env=${config.defaultEnv}, envs=${Object.keys(config.envs).join(',')}`);
669
+ lines.push(`tier: ${config.tier}` +
670
+ (config.tier === 'anonymous'
671
+ ? ' — authenticate and run `seq-studio envs refresh` to discover more environments'
672
+ : ''));
670
673
  }
671
674
  const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
672
- if (requested && !ENV_NAMES.includes(requested) && config && !(requested in config.envs)) {
673
- lines.push(`env "${requested}": FAIL — not in built-ins (${ENV_NAMES.join(',')}) or config`);
674
- ok = false;
675
- }
676
675
  let env = null;
677
676
  if (config) {
678
677
  try {
679
- env = resolveEnv({ config, requested });
678
+ env = await resolveEnvWithDiscovery({ requested });
680
679
  lines.push(`env: ok — ${env.name} (${env.url})`);
681
680
  }
682
681
  catch (err) {
@@ -45,5 +45,5 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
45
45
  */
46
46
  export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
47
47
  export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
48
- export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <local|staging|production|banksouth>\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
48
+ export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
49
49
  export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
@@ -24,7 +24,7 @@ import { buildContext, clientOptions, flagBool, LOG, } from '../functions/comman
24
24
  import { resolveGitPatFromEnv, runGitClone, } from './git-clone.js';
25
25
  import { formatPatSetupHint } from '../pat-hints.js';
26
26
  import { tryGetAccessToken } from '../auth.js';
27
- import { readConfig, resolveEnv } from '../config.js';
27
+ import { resolveEnvWithDiscovery } from '../config.js';
28
28
  const PAGE_SIZE = 200;
29
29
  // ---------------------------------------------------------------------------
30
30
  // Helpers
@@ -60,7 +60,7 @@ export function stringFlag(flags, key) {
60
60
  */
61
61
  async function reposContext(args) {
62
62
  if (args.flags.env === true || args.flags.e === true) {
63
- throw new Error('-e/--env requires a value (local|staging|production|banksouth|preview:<slug>).');
63
+ throw new Error('-e/--env requires a value — see: seq-studio envs list.');
64
64
  }
65
65
  const ctx = await buildContext(args);
66
66
  const secret = process.env.PREVIEW_ACCESS_HEADER?.trim();
@@ -414,12 +414,11 @@ export async function reposCloneCommand(args, deps = {}) {
414
414
  /** Resolve -e/--env without requiring Auth0 (for PAT-only --url/--id clones). */
415
415
  async function resolveEnvOnly(args) {
416
416
  if (args.flags.env === true || args.flags.e === true) {
417
- throw new Error('-e/--env requires a value (local|staging|production|banksouth|preview:<slug>).');
417
+ throw new Error('-e/--env requires a value — see: seq-studio envs list.');
418
418
  }
419
- const config = await readConfig();
420
419
  const requested = (typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
421
420
  (typeof args.flags.e === 'string' ? args.flags.e : undefined);
422
- return resolveEnv({ config, requested });
421
+ return resolveEnvWithDiscovery({ requested });
423
422
  }
424
423
  /**
425
424
  * Validate clone URL shape and that the origin matches the resolved Atlas env
@@ -502,7 +501,7 @@ export const REPOS_USAGE = `usage:
502
501
  materialize the tree at a ref (JSON API)
503
502
  seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)
504
503
 
505
- Flags: -e/--env <local|staging|production|banksouth>
504
+ Flags: -e/--env <env> (see: seq-studio envs list)
506
505
 
507
506
  clone prefers real git clone (PAT via askpass — never written into the remote
508
507
  URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints
@@ -20,5 +20,5 @@ export declare function secretsSetDefaultCommand(args: ParsedArgs): Promise<numb
20
20
  * UI: pin + redeploy the function's active version / pin only / cancel.
21
21
  */
22
22
  export declare function secretsPinCommand(args: ParsedArgs): Promise<number>;
23
- export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> set the shared default value (write-only)\n seq-studio secrets list [-e env] secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> remove attachment\n seq-studio secrets apply --from-env-file .env [-e env] push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> value version history (never values)\n seq-studio secrets set-default <NAME> [version] point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <local|staging|production|banksouth>\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
23
+ export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> set the shared default value (write-only)\n seq-studio secrets list [-e env] secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> remove attachment\n seq-studio secrets apply --from-env-file .env [-e env] push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> value version history (never values)\n seq-studio secrets set-default <NAME> [version] point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
24
24
  export declare function runSecretsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
@@ -658,7 +658,7 @@ export const SECRETS_USAGE = `usage:
658
658
  Use \`secrets\` commands for CI (no .env), bulk/multi-function ops, or write-only
659
659
  value changes without a redeploy.
660
660
 
661
- Flags: -e/--env <local|staging|production|banksouth>
661
+ Flags: -e/--env <env> (see: seq-studio envs list)
662
662
  --fn <slug> · --env-var <NAME> · --yes
663
663
  --functions <f1,f2> · --keys <K1,K2> · --all (override key selection)
664
664
  set-default: [version|version-row-id] (defaults to the most recent non-default) · --yes
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Detached background entry spawned by maybeNotifyUpdate() when the update
3
+ * cache is stale. Polls the npm registry once and persists the result so
4
+ * the *next* command can print the notice without any foreground network.
5
+ */
6
+ import { refreshUpdateCache } from './update-check.js';
7
+ refreshUpdateCache().catch(() => {
8
+ // Best-effort: nothing to report to — the parent process is long gone.
9
+ });
@@ -0,0 +1,27 @@
1
+ /** The version of this installed package (dist/../package.json). */
2
+ export declare function currentVersion(): string;
3
+ export declare function updateCheckCachePath(): string;
4
+ /** Exclusive-create marker that makes "who spawns the refresh?" atomic. */
5
+ export declare function updateCheckClaimPath(): string;
6
+ /**
7
+ * True when `candidate` is a strictly newer semver than `current`.
8
+ * Prerelease suffixes are ignored — the published channel is plain
9
+ * major.minor.patch and this only drives a notice, not resolution.
10
+ */
11
+ export declare function isNewerVersion(candidate: string, current: string): boolean;
12
+ /**
13
+ * Poll the registry and persist the result. Runs in the detached background
14
+ * process (see update-check-refresh.ts) — never on a command's exit path.
15
+ * Failed fetches still record `lastCheckedAt` (keeping any previously known
16
+ * version) so attempts are throttled to once per interval either way. The
17
+ * cache file is written *only* here, so a foreground path can never clobber
18
+ * a fresher result. Releases the spawn claim when done.
19
+ */
20
+ export declare function refreshUpdateCache(): Promise<void>;
21
+ /**
22
+ * Print a strong upgrade recommendation to stderr when the cached registry
23
+ * state says a newer version is published, and kick off a detached refresh
24
+ * when the cache is stale. Reads only local state — adds no network latency
25
+ * to the command. Never throws.
26
+ */
27
+ export declare function maybeNotifyUpdate(): Promise<void>;