@sequenceholdings/studio-cli 0.1.9 → 0.1.11

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.
@@ -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
  // ---------------------------------------------------------------------------
@@ -544,7 +543,8 @@ async function deployFromResolvedSource({ args, spec, ctx: earlyCtx, source, })
544
543
  const problems = [];
545
544
  if (!existingShell) {
546
545
  problems.push(`function "${slug}" is not registered on ${ctx.env.name} — run ` +
547
- `\`seq-studio functions deploy -e ${ctx.env.name}\` once with your own login (seqapi login) to register it`);
546
+ `\`seq-studio functions deploy -e ${ctx.env.name}\` once with your own login (` +
547
+ `\`seq-studio login\`) to register it`);
548
548
  }
549
549
  for (const c of classification?.secrets ?? []) {
550
550
  if (c.category === 'UPLOAD_NEW' || c.category === 'OVERWRITE') {
@@ -941,7 +941,7 @@ export const FUNCTIONS_USAGE = `usage:
941
941
  seq-studio functions delete [--yes] archive function + tear down GCP resources
942
942
  (version history is retained)
943
943
 
944
- 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>
945
945
  --from-env-file <path> (default: .env) source file for secret values
946
946
  --no-wait · --yes
947
947
  --no-provision (deploy) update-only: error instead of registering a new
@@ -957,7 +957,7 @@ export const FUNCTIONS_USAGE = `usage:
957
957
  --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT
958
958
  (repo:read scope — \`seq-studio auth pat create --scopes repo:read\`, or
959
959
  Atlas → Settings → Tokens). It's the same token you clone the repo with;
960
- --env + seqapi login are still needed to resolve the repo and deploy.
960
+ --env + seq-studio login are still needed to resolve the repo and deploy.
961
961
  `;
962
962
  export async function runFunctionsCommand(sub, args) {
963
963
  try {
@@ -0,0 +1,12 @@
1
+ interface LoginOptions {
2
+ port?: number;
3
+ timeoutMs?: number;
4
+ fetchImpl?: typeof fetch;
5
+ now?: () => number;
6
+ openBrowser?: (authorizationUrl: string) => void | Promise<void>;
7
+ }
8
+ export declare function openSystemBrowser(authorizationUrl: string): Promise<void>;
9
+ export declare function loginWithPkce({ fetchImpl, now, openBrowser, port, timeoutMs, }: LoginOptions): Promise<void>;
10
+ export declare function login(): Promise<void>;
11
+ export declare function logout(): Promise<void>;
12
+ export {};
package/dist/login.js ADDED
@@ -0,0 +1,233 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { rm } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { spawn } from 'node:child_process';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN, saveTokens, seqapiTokenPath, } from './auth.js';
8
+ import { clearCatalog, fetchCatalog } from './env-catalog.js';
9
+ const DEFAULT_REDIRECT_PORT = 5099;
10
+ const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
11
+ function base64Url(input) {
12
+ return input.toString('base64url');
13
+ }
14
+ function configuredPort() {
15
+ const raw = process.env.SEQAPI_PORT?.trim();
16
+ if (!raw)
17
+ return DEFAULT_REDIRECT_PORT;
18
+ const port = Number(raw);
19
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
20
+ throw new Error(`SEQAPI_PORT must be an integer from 1 to 65535 (got "${raw}").`);
21
+ }
22
+ return port;
23
+ }
24
+ function authorizationUrl({ challenge, redirectUri, state, }) {
25
+ const url = new URL(`https://${AUTH0_DOMAIN}/authorize`);
26
+ url.searchParams.set('response_type', 'code');
27
+ url.searchParams.set('client_id', AUTH0_CLIENT_ID);
28
+ url.searchParams.set('redirect_uri', redirectUri);
29
+ url.searchParams.set('scope', 'openid profile email offline_access');
30
+ url.searchParams.set('audience', AUTH0_AUDIENCE);
31
+ url.searchParams.set('code_challenge', challenge);
32
+ url.searchParams.set('code_challenge_method', 'S256');
33
+ url.searchParams.set('state', state);
34
+ return url.toString();
35
+ }
36
+ export async function openSystemBrowser(authorizationUrl) {
37
+ const windows = process.platform === 'win32';
38
+ const command = process.platform === 'darwin' ? 'open' : windows ? 'cmd' : 'xdg-open';
39
+ // On Windows the URL must be quoted manually (with verbatim arguments so
40
+ // Node does not re-escape): cmd otherwise splits the unquoted URL on `&`,
41
+ // truncating the query string and executing the tail as commands. The empty
42
+ // `""` is `start`'s window-title slot — without it, start would treat the
43
+ // quoted URL as the title.
44
+ const args = windows
45
+ ? ['/c', 'start', '""', `"${authorizationUrl}"`]
46
+ : [authorizationUrl];
47
+ const child = spawn(command, args, {
48
+ stdio: 'ignore',
49
+ detached: true,
50
+ ...(windows ? { windowsVerbatimArguments: true } : {}),
51
+ });
52
+ await new Promise((resolve, reject) => {
53
+ child.once('spawn', resolve);
54
+ child.once('error', reject);
55
+ });
56
+ child.unref();
57
+ }
58
+ function callbackPort(server) {
59
+ const address = server.address();
60
+ if (!address || typeof address === 'string') {
61
+ throw new Error('Auth0 callback server did not expose a TCP port.');
62
+ }
63
+ return address.port;
64
+ }
65
+ function sendHtml({ body, response, status, }) {
66
+ response.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
67
+ response.end(body);
68
+ }
69
+ function waitForAuthorizationCode({ expectedState, onListening, port, timeoutMs, }) {
70
+ return new Promise((resolve, reject) => {
71
+ let redirectUri = '';
72
+ let settled = false;
73
+ let timer;
74
+ const finish = ({ code, error }) => {
75
+ if (settled)
76
+ return;
77
+ settled = true;
78
+ clearTimeout(timer);
79
+ server.close();
80
+ if (error)
81
+ reject(error);
82
+ else if (code)
83
+ resolve({ code, redirectUri });
84
+ else
85
+ reject(new Error('Auth0 callback completed without an authorization code.'));
86
+ };
87
+ const server = createServer((request, response) => {
88
+ const url = new URL(request.url ?? '/', redirectUri);
89
+ const code = url.searchParams.get('code');
90
+ const authError = url.searchParams.get('error');
91
+ if (!code && !authError) {
92
+ response.writeHead(204);
93
+ response.end();
94
+ return;
95
+ }
96
+ if (url.searchParams.get('state') !== expectedState) {
97
+ sendHtml({
98
+ body: '<h1>Sequence login failed</h1><p>Authorization state mismatch.</p>',
99
+ response,
100
+ status: 400,
101
+ });
102
+ finish({ error: new Error('Auth0 callback state mismatch.') });
103
+ return;
104
+ }
105
+ if (authError) {
106
+ const description = url.searchParams.get('error_description') ?? authError;
107
+ sendHtml({
108
+ body: '<h1>Sequence login failed</h1><p>Return to the terminal for details.</p>',
109
+ response,
110
+ status: 400,
111
+ });
112
+ finish({ error: new Error(`Auth0 login failed: ${description}`) });
113
+ return;
114
+ }
115
+ sendHtml({
116
+ body: '<h1>Sequence login complete</h1><p>You may close this tab.</p>',
117
+ response,
118
+ status: 200,
119
+ });
120
+ finish({ code: code ?? undefined });
121
+ });
122
+ server.once('error', (error) => {
123
+ const message = error.code === 'EADDRINUSE'
124
+ ? `Port ${port} is in use. Stop the process using it, or set SEQAPI_PORT to a registered Auth0 callback port.`
125
+ : `Could not start the Auth0 callback server: ${error.message}`;
126
+ finish({ error: new Error(message) });
127
+ });
128
+ server.listen(port, 'localhost', () => {
129
+ redirectUri = `http://localhost:${callbackPort(server)}`;
130
+ onListening(redirectUri);
131
+ });
132
+ timer = setTimeout(() => {
133
+ finish({
134
+ error: new Error(`Login timed out after ${Math.ceil(timeoutMs / 1_000)}s waiting for the browser callback.`),
135
+ });
136
+ }, timeoutMs);
137
+ });
138
+ }
139
+ function parseTokenResponse(value) {
140
+ if (!value || typeof value !== 'object') {
141
+ throw new Error('Auth0 token response was not an object.');
142
+ }
143
+ const accessToken = Reflect.get(value, 'access_token');
144
+ const refreshToken = Reflect.get(value, 'refresh_token');
145
+ const expiresIn = Reflect.get(value, 'expires_in');
146
+ if (typeof accessToken !== 'string' || !accessToken) {
147
+ throw new Error('Auth0 token response missing access_token.');
148
+ }
149
+ if (typeof refreshToken !== 'string' || !refreshToken) {
150
+ throw new Error('No refresh token returned. Ensure Auth0 offline access is enabled.');
151
+ }
152
+ return {
153
+ accessToken,
154
+ refreshToken,
155
+ expiresIn: typeof expiresIn === 'number' ? expiresIn : 86_400,
156
+ };
157
+ }
158
+ export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBrowser = openSystemBrowser, port = configuredPort(), timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS, }) {
159
+ const verifier = base64Url(randomBytes(32));
160
+ const challenge = base64Url(createHash('sha256').update(verifier).digest());
161
+ const state = base64Url(randomBytes(32));
162
+ const callback = await waitForAuthorizationCode({
163
+ expectedState: state,
164
+ port,
165
+ timeoutMs,
166
+ onListening: (redirectUri) => {
167
+ const url = authorizationUrl({ challenge, redirectUri, state });
168
+ console.error(`Opening browser for Sequence login. If it does not open, visit:\n${url}`);
169
+ void Promise.resolve(openBrowser(url)).catch((error) => {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ console.error(`Could not open a browser automatically: ${message}`);
172
+ });
173
+ },
174
+ });
175
+ const tokenResponse = await fetchImpl(`https://${AUTH0_DOMAIN}/oauth/token`, {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({
179
+ grant_type: 'authorization_code',
180
+ client_id: AUTH0_CLIENT_ID,
181
+ code: callback.code,
182
+ redirect_uri: callback.redirectUri,
183
+ code_verifier: verifier,
184
+ }),
185
+ });
186
+ if (!tokenResponse.ok) {
187
+ throw new Error(`Auth0 token exchange failed (${tokenResponse.status}): ${await tokenResponse.text()}`);
188
+ }
189
+ const tokens = parseTokenResponse(await tokenResponse.json());
190
+ await saveTokens({
191
+ access_token: tokens.accessToken,
192
+ refresh_token: tokens.refreshToken,
193
+ expires_at: now() / 1_000 + tokens.expiresIn,
194
+ });
195
+ }
196
+ export async function login() {
197
+ await loginWithPkce({});
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
+ }
215
+ }
216
+ /**
217
+ * Pre-unification artifact-studio token file. `seq-studio artifact` commands
218
+ * still fall back to it (artifact-studio's `getAccessToken` →
219
+ * `readTokenConfig`, see shared/services/artifact-studio/src/config.ts), so
220
+ * logout must clear it too or artifact commands would stay authenticated
221
+ * after a successful logout.
222
+ */
223
+ function legacyArtifactTokenPath() {
224
+ return join(homedir(), '.config', 'sequence-artifact-studio', 'tokens.json');
225
+ }
226
+ export async function logout() {
227
+ await rm(seqapiTokenPath(), { force: true });
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();
232
+ console.log('Logged out of seq-studio and seqapi.');
233
+ }
package/dist/main.d.ts CHANGED
@@ -8,6 +8,9 @@
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
12
+ * seq-studio login authenticate interactively with Auth0
13
+ * seq-studio logout remove cached user tokens
11
14
  * seq-studio doctor check token + env + writer gate
12
15
  * seq-studio help show usage
13
16
  */
package/dist/main.js CHANGED
@@ -8,6 +8,9 @@
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
12
+ * seq-studio login authenticate interactively with Auth0
13
+ * seq-studio logout remove cached user tokens
11
14
  * seq-studio doctor check token + env + writer gate
12
15
  * seq-studio help show usage
13
16
  */
@@ -23,11 +26,15 @@ const TOP_LEVEL_USAGE = `usage:
23
26
  seq-studio repos <sub> [args] list | namespaces | show | create | clone | pull | delete
24
27
  seq-studio auth <sub> [args] pat create | pat list | pat revoke
25
28
  seq-studio orm <sub> [args] init | validate | plan | apply
29
+ seq-studio envs <sub> list | refresh — environments visible to your identity
30
+ seq-studio login authenticate in the browser
31
+ seq-studio logout remove cached user tokens
26
32
  seq-studio doctor [-e <env>] diagnose config, auth, and writer gate
27
33
  seq-studio help show this message
28
34
 
29
- Authenticate with: seqapi login
30
- Env URLs come from ~/.config/lattice/config.toml (built-ins: local, staging, production, banksouth).
35
+ Authenticate with: seq-studio login
36
+ Built-in env: local. Authenticated identities discover more via
37
+ \`seq-studio envs refresh\`; custom entries live in ~/.config/lattice/config.toml.
31
38
  `;
32
39
  const PROCESS_USAGE = `usage:
33
40
  seq-studio process init <dir>
@@ -78,6 +85,13 @@ export async function run(argv = process.argv.slice(2)) {
78
85
  const { runOrmCommand } = await import('./orm/delegate.js');
79
86
  return runOrmCommand(sub, rest);
80
87
  }
88
+ case 'envs': {
89
+ const { runEnvsCommand } = await import('./envs/commands.js');
90
+ return runEnvsCommand(sub, rest);
91
+ }
92
+ case 'login':
93
+ case 'logout':
94
+ return runSessionCommand({ argument: sub, command: namespace });
81
95
  case 'doctor':
82
96
  return doctorCommand(parseArgs([sub, ...rest].filter(Boolean)));
83
97
  default:
@@ -86,6 +100,19 @@ export async function run(argv = process.argv.slice(2)) {
86
100
  return 1;
87
101
  }
88
102
  }
103
+ async function runSessionCommand({ argument, command, }) {
104
+ if (argument === 'help' || argument === '--help' || argument === '-h') {
105
+ console.log(`usage: seq-studio ${command}`);
106
+ return 0;
107
+ }
108
+ if (argument) {
109
+ console.error(`seq-studio ${command} does not accept arguments.`);
110
+ return 1;
111
+ }
112
+ const auth = await import('./login.js');
113
+ await auth[command]();
114
+ return 0;
115
+ }
89
116
  async function runProcessNamespace(sub, rest) {
90
117
  if (!sub) {
91
118
  console.error(PROCESS_USAGE);
@@ -6,17 +6,18 @@
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.
19
- Authenticate with: seqapi login
18
+ Environments: see \`seq-studio envs list\` (built-in: local; more are
19
+ discovered after you authenticate).
20
+ Authenticate with: seq-studio login
20
21
  `;
21
22
  export async function runOrmCommand(sub, rest) {
22
23
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
@@ -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 });
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * User-facing hints for minting a git-service PAT.
3
3
  *
4
- * Third-party / OpCo developers typically do not have `seqapi`. Their path is
5
- * the Atlas UI at `/settings/tokens`. Sequence staff can also use
6
- * `seq-studio auth pat create` after `seqapi login`.
4
+ * Users can mint a PAT with seq-studio after interactive login, or through
5
+ * the Atlas UI at `/settings/tokens`.
7
6
  */
8
7
  export declare function settingsTokensUrl(envUrl: string): string;
9
8
  /**
10
- * Multi-line setup instructions. Prefer the Atlas UI first (works for anyone
11
- * with Atlas access); mention the CLI mint path as a staff convenience.
9
+ * Multi-line setup instructions covering both the CLI and Atlas UI paths.
12
10
  */
13
11
  export declare function formatPatSetupHint({ envUrl, envName, indent, }: {
14
12
  envUrl: string;
package/dist/pat-hints.js CHANGED
@@ -1,28 +1,27 @@
1
1
  /**
2
2
  * User-facing hints for minting a git-service PAT.
3
3
  *
4
- * Third-party / OpCo developers typically do not have `seqapi`. Their path is
5
- * the Atlas UI at `/settings/tokens`. Sequence staff can also use
6
- * `seq-studio auth pat create` after `seqapi login`.
4
+ * Users can mint a PAT with seq-studio after interactive login, or through
5
+ * the Atlas UI at `/settings/tokens`.
7
6
  */
8
7
  export function settingsTokensUrl(envUrl) {
9
8
  return `${envUrl.replace(/\/$/, '')}/settings/tokens`;
10
9
  }
11
10
  /**
12
- * Multi-line setup instructions. Prefer the Atlas UI first (works for anyone
13
- * with Atlas access); mention the CLI mint path as a staff convenience.
11
+ * Multi-line setup instructions covering both the CLI and Atlas UI paths.
14
12
  */
15
13
  export function formatPatSetupHint({ envUrl, envName, indent = ' ', }) {
16
14
  const tokensUrl = settingsTokensUrl(envUrl);
17
15
  return [
18
- `${indent}Get a PAT (no seqapi required):`,
19
- `${indent} 1. Open ${tokensUrl} and sign in to Atlas`,
16
+ `${indent}Get a PAT with seq-studio:`,
17
+ `${indent} seq-studio login`,
18
+ `${indent} seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e ${envName}`,
19
+ `${indent}Or create one in Atlas:`,
20
+ `${indent} 1. Open ${tokensUrl} and sign in`,
20
21
  `${indent} 2. New token → scopes repo:read (add repo:write for push) → copy once`,
21
22
  `${indent} 3. export ATLAS_GIT_PAT=<token>`,
22
23
  `${indent} 4. Copy the clone URL from Repositories → Clone, then:`,
23
24
  `${indent} ATLAS_GIT_PAT=<token> seq-studio repos clone --url <https://…/repos/<id>/git>`,
24
- `${indent} (or with seqapi: seq-studio repos clone <ns>/<name>)`,
25
- `${indent}Sequence staff with seqapi can instead:`,
26
- `${indent} seqapi login && seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e ${envName}`,
25
+ `${indent} (or: seq-studio repos clone <ns>/<name>)`,
27
26
  ];
28
27
  }
@@ -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();
@@ -161,7 +160,7 @@ export async function initCommand(args) {
161
160
  // pnpm-workspace.yaml's `minimumReleaseAge: 10080` supply-chain
162
161
  // gate actually takes effect (npm has no equivalent setting).
163
162
  console.log(` cd ${relative(process.cwd(), dir) || '.'} && pnpm install`);
164
- console.log(' seqapi login');
163
+ console.log(' seq-studio login');
165
164
  console.log(' seq-studio process plan -e local');
166
165
  return 0;
167
166
  }
@@ -209,7 +208,7 @@ async function buildBundleForPublish(args, defs) {
209
208
  }
210
209
  catch (err) {
211
210
  if (hasSubprocess) {
212
- throw new Error('subprocess nodes require -e <env> and `seqapi login` to resolve child process versions', { cause: err });
211
+ throw new Error('subprocess nodes require -e <env> and `seq-studio login` to resolve child process versions', { cause: err });
213
212
  }
214
213
  return await buildBundleFromProcesses(defs);
215
214
  }
@@ -221,7 +220,7 @@ export async function planCommand(args) {
221
220
  console.log(JSON.stringify(summary, null, 2));
222
221
  const loadActiveProcess = await tryBuildActiveProcessLoader(args);
223
222
  if (!loadActiveProcess) {
224
- console.log('\n(no diff — env or auth unavailable; pass --env <name> and run `seqapi login`)');
223
+ console.log('\n(no diff — env or auth unavailable; pass --env <name> and run `seq-studio login`)');
225
224
  return 0;
226
225
  }
227
226
  const diff = await diffBundleAgainstActive({ newBundle: bundle, loadActiveProcess });
@@ -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) {
@@ -727,7 +726,7 @@ export async function doctorCommand(args) {
727
726
  ok = false;
728
727
  }
729
728
  else if (err.status === 401) {
730
- lines.push('writer gate: FAIL — 401. Token is rejected by Atlas — run `seqapi login`.');
729
+ lines.push('writer gate: FAIL — 401. Token is rejected by Atlas — run `seq-studio login`.');
731
730
  ok = false;
732
731
  }
733
732
  else {
@@ -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 (Atlas UI /settings/tokens \u2014 no seqapi required).\n No seqapi + PAT: 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: seqapi 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>;
@@ -2,7 +2,7 @@
2
2
  * `seq-studio repos <sub>` — basic management of platform git-service repos
3
3
  * over the JSON API.
4
4
  *
5
- * Everything here is API-driven (Auth0 bearer via seqapi login), matching the
5
+ * Everything here is API-driven (Auth0 bearer via seq-studio login), matching the
6
6
  * rest of seq-studio. `pull` materializes a tree over the JSON API; `clone`
7
7
  * prefers a real `git clone` when `ATLAS_GIT_PAT` is set (smart-HTTP URL from
8
8
  * `show`), otherwise falls back to the same JSON materialize path and hints
@@ -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();
@@ -367,14 +367,15 @@ export async function reposCloneCommand(args, deps = {}) {
367
367
  const env = await resolveEnvOnly(args);
368
368
  const lines = pat
369
369
  ? [
370
- `ATLAS_GIT_PAT is set, but resolving ${namespace}/${name} needs Auth0 (seqapi login).`,
371
- ` No seqapi? Copy the clone URL from Repositories → Clone, then:`,
370
+ `ATLAS_GIT_PAT is set, but resolving ${namespace}/${name} needs Auth0 (` +
371
+ `seq-studio login).`,
372
+ ` Or copy the clone URL from Repositories → Clone, then:`,
372
373
  ` ATLAS_GIT_PAT=<token> seq-studio repos clone --url <https://…/repos/<id>/git> -e ${env.name}`,
373
374
  ]
374
375
  : [
375
- `${LOG} no ATLAS_GIT_PAT and not logged in (seqapi).`,
376
+ `${LOG} no ATLAS_GIT_PAT and not logged in.`,
376
377
  ...formatPatSetupHint({ envUrl: env.url, envName: env.name }),
377
- ` Or: seqapi login && seq-studio repos clone ${namespace}/${name} -e ${env.name}`,
378
+ ` Then: seq-studio repos clone ${namespace}/${name} -e ${env.name}`,
378
379
  ];
379
380
  throw new Error(lines.join('\n'));
380
381
  }
@@ -413,12 +414,11 @@ export async function reposCloneCommand(args, deps = {}) {
413
414
  /** Resolve -e/--env without requiring Auth0 (for PAT-only --url/--id clones). */
414
415
  async function resolveEnvOnly(args) {
415
416
  if (args.flags.env === true || args.flags.e === true) {
416
- 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.');
417
418
  }
418
- const config = await readConfig();
419
419
  const requested = (typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
420
420
  (typeof args.flags.e === 'string' ? args.flags.e : undefined);
421
- return resolveEnv({ config, requested });
421
+ return resolveEnvWithDiscovery({ requested });
422
422
  }
423
423
  /**
424
424
  * Validate clone URL shape and that the origin matches the resolved Atlas env
@@ -501,15 +501,15 @@ export const REPOS_USAGE = `usage:
501
501
  materialize the tree at a ref (JSON API)
502
502
  seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)
503
503
 
504
- Flags: -e/--env <local|staging|production|banksouth>
504
+ Flags: -e/--env <env> (see: seq-studio envs list)
505
505
 
506
506
  clone prefers real git clone (PAT via askpass — never written into the remote
507
507
  URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints
508
- how to get a PAT (Atlas UI /settings/tokens — no seqapi required).
509
- No seqapi + PAT: use --url from Repositories → Clone (or --id <uuid>).
508
+ how to get a PAT (seq-studio or Atlas UI /settings/tokens).
509
+ PAT without Auth0 login: use --url from Repositories → Clone (or --id <uuid>).
510
510
  --ref accepts a branch, tag, or commit SHA (SHA → clone then checkout).
511
511
 
512
- Authenticate JSON API calls with: seqapi login
512
+ Authenticate JSON API calls with: seq-studio login
513
513
  Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings → Tokens)
514
514
  `;
515
515
  export async function runReposCommand(sub, args) {