@sequenceholdings/studio-cli 0.1.10 → 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.
- package/README.md +36 -18
- package/dist/artifact/delegate.js +46 -5
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +21 -7
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +42 -2
- package/dist/config.d.ts +27 -11
- package/dist/config.js +72 -22
- package/dist/env-catalog.d.ts +63 -0
- package/dist/env-catalog.js +111 -0
- package/dist/envs/commands.d.ts +1 -0
- package/dist/envs/commands.js +74 -0
- package/dist/functions/commands.d.ts +1 -1
- package/dist/functions/commands.js +4 -5
- package/dist/login.js +20 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +8 -1
- package/dist/orm/delegate.js +7 -7
- package/dist/process/commands.js +7 -8
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +5 -6
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,9 +6,10 @@ any repo against the platform over HTTP — no monorepo checkout required.
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
seq-studio process lint
|
|
9
|
-
seq-studio process plan -e
|
|
10
|
-
seq-studio process apply -e
|
|
11
|
-
seq-studio artifact deploy -e
|
|
9
|
+
seq-studio process plan -e <env>
|
|
10
|
+
seq-studio process apply -e <env>
|
|
11
|
+
seq-studio artifact deploy -e <env>
|
|
12
|
+
seq-studio envs list
|
|
12
13
|
seq-studio doctor
|
|
13
14
|
```
|
|
14
15
|
|
|
@@ -62,7 +63,7 @@ secret and `seq-studio` mints a token via the Auth0 client-credentials grant
|
|
|
62
63
|
|
|
63
64
|
```bash
|
|
64
65
|
export AUTH0_M2M_CLIENT_SECRET=... # provided by your platform administrator
|
|
65
|
-
seq-studio artifact deploy -e
|
|
66
|
+
seq-studio artifact deploy -e <env>
|
|
66
67
|
```
|
|
67
68
|
|
|
68
69
|
The secret is read at runtime — never commit it. M2M carries app scopes but
|
|
@@ -72,17 +73,35 @@ user-scoped/private resources.
|
|
|
72
73
|
**Manual escape hatch:** any `artifact` command also accepts an explicit
|
|
73
74
|
`--token <jwt>`, which wins over both the M2M and cached-user paths.
|
|
74
75
|
|
|
75
|
-
## Environments
|
|
76
|
+
## Environments
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
The CLI ships with a single built-in environment, `local`
|
|
79
|
+
(`http://localhost:5001`). The other environments your identity may target
|
|
80
|
+
are **discovered** after you authenticate: the CLI calls the platform's
|
|
81
|
+
environment-discovery endpoint and caches the result at
|
|
82
|
+
`~/.config/lattice/environments.json`.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
seq-studio login # or: export AUTH0_M2M_CLIENT_SECRET=...
|
|
86
|
+
seq-studio envs refresh # fetch the environments visible to your identity
|
|
87
|
+
seq-studio envs list # show them (name, URL, source)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Discovery also happens lazily: the first time you pass an `-e <env>` that
|
|
91
|
+
isn't cached yet, the CLI refreshes the catalog before failing. What you can
|
|
92
|
+
see depends on who you are — unauthenticated installs get `local` only, and
|
|
93
|
+
authenticated identities get the deployments they're entitled to. Visibility
|
|
94
|
+
is not access control: every request is still authorized server-side.
|
|
95
|
+
|
|
96
|
+
You can always add or override environments yourself in
|
|
97
|
+
`~/.config/lattice/config.toml` (user entries win over discovered ones):
|
|
79
98
|
|
|
80
99
|
```toml
|
|
81
100
|
[env.local]
|
|
82
101
|
url = "http://localhost:5001"
|
|
83
102
|
|
|
84
|
-
[env.
|
|
85
|
-
url = "https://
|
|
103
|
+
[env.my-atlas]
|
|
104
|
+
url = "https://atlas.example.com"
|
|
86
105
|
|
|
87
106
|
default_env = "local"
|
|
88
107
|
```
|
|
@@ -191,17 +210,16 @@ any username, PAT as password). **You do not need `seqapi`.**
|
|
|
191
210
|
|
|
192
211
|
### Everyone (recommended) — Atlas UI
|
|
193
212
|
|
|
194
|
-
1. Open **Settings → Tokens** in Atlas for your environment
|
|
195
|
-
-
|
|
196
|
-
|
|
197
|
-
- BankSouth: https://banksouth.seqholdings.com/settings/tokens
|
|
213
|
+
1. Open **Settings → Tokens** in Atlas for your environment
|
|
214
|
+
(`<your-atlas-url>/settings/tokens` — run `seq-studio envs list` for the
|
|
215
|
+
URLs visible to your identity)
|
|
198
216
|
2. **New token** → scopes `repo:read` (add `repo:write` for push) → copy once
|
|
199
217
|
3. Export and clone:
|
|
200
218
|
|
|
201
219
|
```bash
|
|
202
220
|
export ATLAS_GIT_PAT=atlas_git_…
|
|
203
|
-
seq-studio repos clone
|
|
204
|
-
# or: git clone https://git:$ATLAS_GIT_PAT
|
|
221
|
+
seq-studio repos clone <namespace>/<repo> -e <env>
|
|
222
|
+
# or: git clone https://git:$ATLAS_GIT_PAT@<your-atlas-host>/api/git-service/repos/<id>/git
|
|
205
223
|
```
|
|
206
224
|
|
|
207
225
|
You can also open **Repositories → Access tokens** / the clone popover’s
|
|
@@ -213,12 +231,12 @@ Requires Auth0 login. Same identity as the UI:
|
|
|
213
231
|
|
|
214
232
|
```bash
|
|
215
233
|
seq-studio login
|
|
216
|
-
seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e
|
|
234
|
+
seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e <env>
|
|
217
235
|
# optional: --expires 7d|30d|90d|1y|never (default 30d)
|
|
218
236
|
# optional: --store-credentials # git credential approve for the env host
|
|
219
237
|
|
|
220
|
-
seq-studio auth pat list -e
|
|
221
|
-
seq-studio auth pat revoke <id> -e
|
|
238
|
+
seq-studio auth pat list -e <env>
|
|
239
|
+
seq-studio auth pat revoke <id> -e <env> --yes
|
|
222
240
|
```
|
|
223
241
|
|
|
224
242
|
The raw token is printed **once** on create.
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* is shared with the rest of `seq-studio`.
|
|
12
12
|
* 4. Forward all remaining argv to `runCli`.
|
|
13
13
|
*/
|
|
14
|
-
import { getAccessToken, tryGetAccessToken } from '../auth.js';
|
|
15
|
-
import { readConfig,
|
|
14
|
+
import { getAccessToken, M2mTokenError, tryGetAccessToken } from '../auth.js';
|
|
15
|
+
import { readConfig, resolveEnvWithDiscovery } from '../config.js';
|
|
16
|
+
import { fetchCatalog, readCachedCatalog } from '../env-catalog.js';
|
|
16
17
|
import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
|
|
17
18
|
import { PREVIEW_DOMAIN, PREVIEW_PROJECT, resolvePreviewByPr } from '../preview.js';
|
|
18
19
|
const ARTIFACT_USAGE = `usage:
|
|
@@ -31,7 +32,8 @@ const ARTIFACT_USAGE = `usage:
|
|
|
31
32
|
seq-studio artifact whoami -e <env>
|
|
32
33
|
seq-studio artifact env list
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Environments: see \`seq-studio envs list\` (built-in: local; more are
|
|
36
|
+
discovered after you authenticate).
|
|
35
37
|
|
|
36
38
|
Source for build/plan/deploy: a local [dir] (default), a platform git-service
|
|
37
39
|
repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a
|
|
@@ -78,6 +80,12 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
78
80
|
console.log(ARTIFACT_USAGE);
|
|
79
81
|
return sub ? 0 : 1;
|
|
80
82
|
}
|
|
83
|
+
// `artifact env list` renders the tier-aware discovered catalog instead of
|
|
84
|
+
// artifact-studio's built-in (local-only) list.
|
|
85
|
+
if (sub === 'env' && rest[0] === 'list') {
|
|
86
|
+
const { runEnvsCommand } = await import('../envs/commands.js');
|
|
87
|
+
return runEnvsCommand('list', []);
|
|
88
|
+
}
|
|
81
89
|
// Normalize `-e <env>` / `-e=<env>` to `--env <env>` because
|
|
82
90
|
// artifact-studio's argv parser (`shared/services/artifact-studio/src/cli.ts`)
|
|
83
91
|
// only recognizes long flags. Without this rewrite `seq-studio
|
|
@@ -93,6 +101,7 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
93
101
|
let resolved;
|
|
94
102
|
let argvForCli = forwardRest;
|
|
95
103
|
if (envUrl !== undefined) {
|
|
104
|
+
await requireSequenceTier('--env-url');
|
|
96
105
|
const validated = validatePreviewEnvUrl(envUrl);
|
|
97
106
|
// Explicit override wins over everything. Keep the user's --env name if
|
|
98
107
|
// they gave one, else label it `preview`.
|
|
@@ -100,13 +109,13 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
100
109
|
argvForCli = ensureEnvFlag(forwardRest, resolved.name);
|
|
101
110
|
}
|
|
102
111
|
else if (prNumber !== undefined) {
|
|
112
|
+
await requireSequenceTier('--pr');
|
|
103
113
|
const preview = await resolvePreviewByPr({ pr: prNumber });
|
|
104
114
|
resolved = { name: `preview:${preview.slug}`, url: preview.url };
|
|
105
115
|
argvForCli = ensureEnvFlag(forwardRest, resolved.name);
|
|
106
116
|
}
|
|
107
117
|
else if (requested) {
|
|
108
|
-
|
|
109
|
-
resolved = resolveEnv({ config, requested });
|
|
118
|
+
resolved = await resolveEnvWithDiscovery({ requested });
|
|
110
119
|
// For `preview:<slug>` normalize the forwarded --env to the canonical name.
|
|
111
120
|
if (resolved.name !== requested)
|
|
112
121
|
argvForCli = ensureEnvFlag(forwardRest, resolved.name);
|
|
@@ -119,6 +128,13 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
119
128
|
if (isPreviewUrl(resolved.url))
|
|
120
129
|
applyPreviewAccessHeader();
|
|
121
130
|
}
|
|
131
|
+
// Always hand artifact-studio the env map visible to this identity
|
|
132
|
+
// (discovered catalog + config.toml). Commands that resolve an env *name*
|
|
133
|
+
// without an explicit --env flag — the stored `.artifact-studio/config.json`
|
|
134
|
+
// defaultEnv, `artifact env set <name>` — would otherwise fall back to
|
|
135
|
+
// artifact-studio's built-in local-only map and fail on deployed envs.
|
|
136
|
+
const { envs } = await readConfig();
|
|
137
|
+
process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(envs).map(([name, { url }]) => [name, url])));
|
|
122
138
|
// An explicit `--token <jwt>` is the manual escape hatch and must win over
|
|
123
139
|
// everything, including a configured-but-failing M2M credential (which
|
|
124
140
|
// `tryGetAccessToken({ failClosedForM2m: true })` would otherwise turn into
|
|
@@ -164,6 +180,31 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
164
180
|
}
|
|
165
181
|
return runArtifactStudio([sub, ...argvForCli]);
|
|
166
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Preview targeting (`preview:<slug>`, `--pr`, `--env-url`) is a
|
|
185
|
+
* Sequence-staff surface. The tier comes from the cached discovery catalog;
|
|
186
|
+
* refresh it lazily so a just-logged-in engineer isn't blocked on a stale
|
|
187
|
+
* anonymous cache.
|
|
188
|
+
*/
|
|
189
|
+
async function requireSequenceTier(flag) {
|
|
190
|
+
let { tier } = await readConfig();
|
|
191
|
+
if (tier !== 'sequence') {
|
|
192
|
+
try {
|
|
193
|
+
tier = (await fetchCatalog())?.tier ?? 'anonymous';
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
// Broken CI credentials must surface as the Auth0 error, not as a
|
|
197
|
+
// misleading "Sequence-staff surface" rejection.
|
|
198
|
+
if (err instanceof M2mTokenError)
|
|
199
|
+
throw err;
|
|
200
|
+
tier = (await readCachedCatalog())?.tier ?? 'anonymous';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (tier !== 'sequence') {
|
|
204
|
+
throw new Error(`${flag} targets per-PR preview environments, a Sequence-staff surface. ` +
|
|
205
|
+
'Authenticate with `seq-studio login` and run `seq-studio envs refresh`.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
167
208
|
/**
|
|
168
209
|
* Strip the seq-studio-only `--pr <number>` and `--env-url <url>` flags from
|
|
169
210
|
* argv and return the rest (forwarded to artifact-studio) plus the parsed
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParsedArgs } from '../process/commands.js';
|
|
2
2
|
declare const PAT_SCOPES: readonly ["repo:read", "repo:write", "repo:admin"];
|
|
3
3
|
type PatScope = (typeof PAT_SCOPES)[number];
|
|
4
|
-
export declare const AUTH_USAGE = "usage:\n seq-studio auth pat create --name <n> [--scopes repo:read,repo:write] [-e env]\n [--expires 7d|30d|90d|1y|never] [--store-credentials]\n seq-studio auth pat list [-e env]\n seq-studio auth pat revoke <id> [-e env] [--yes]\n\n Issue a personal access token for git clone / git push against the platform\n git service.\n\n Authenticate with `seq-studio login`, then run `auth pat create`.\n Alternatively, open Atlas \u2192 Settings \u2192 Tokens:\n https://<atlas-host>/settings/tokens\n Sign in, create a token (repo:read / repo:write), copy once, then:\n export ATLAS_GIT_PAT=<token>\n\n On create the raw token is printed ONCE \u2014 store it; Atlas cannot re-show it.\n Git Basic auth: any username (e.g. git), PAT as the password.\n\n Flags: -e/--env <
|
|
4
|
+
export declare const AUTH_USAGE = "usage:\n seq-studio auth pat create --name <n> [--scopes repo:read,repo:write] [-e env]\n [--expires 7d|30d|90d|1y|never] [--store-credentials]\n seq-studio auth pat list [-e env]\n seq-studio auth pat revoke <id> [-e env] [--yes]\n\n Issue a personal access token for git clone / git push against the platform\n git service.\n\n Authenticate with `seq-studio login`, then run `auth pat create`.\n Alternatively, open Atlas \u2192 Settings \u2192 Tokens:\n https://<atlas-host>/settings/tokens\n Sign in, create a token (repo:read / repo:write), copy once, then:\n export ATLAS_GIT_PAT=<token>\n\n On create the raw token is printed ONCE \u2014 store it; Atlas cannot re-show it.\n Git Basic auth: any username (e.g. git), PAT as the password.\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n";
|
|
5
5
|
export declare function parsePatScopes(raw: string | undefined): PatScope[];
|
|
6
6
|
/**
|
|
7
7
|
* Map UI-style duration choices to an absolute ISO-8601 expiresAt, or undefined
|
|
@@ -11,9 +11,23 @@
|
|
|
11
11
|
import { deleteNoContent, getJson, postJson } from '../atlas-client.js';
|
|
12
12
|
import { printCliError } from '../cli-errors.js';
|
|
13
13
|
import { buildContext, clientOptions, flagBool, LOG, } from '../functions/commands.js';
|
|
14
|
+
import { readConfig } from '../config.js';
|
|
14
15
|
import { confirmYes } from '../prompt.js';
|
|
15
16
|
import { storeGitCredentials } from '../repos/git-clone.js';
|
|
16
17
|
import { formatPatSetupHint, settingsTokensUrl } from '../pat-hints.js';
|
|
18
|
+
/**
|
|
19
|
+
* Tokens-page URLs rendered from the environments visible to this identity
|
|
20
|
+
* (discovered catalog + config.toml). Anonymous users get a generic pointer
|
|
21
|
+
* instead of a hardcoded list of internal hosts.
|
|
22
|
+
*/
|
|
23
|
+
async function settingsTokensLines() {
|
|
24
|
+
const config = await readConfig().catch(() => null);
|
|
25
|
+
const remote = Object.entries(config?.envs ?? {}).filter(([name]) => name !== 'local');
|
|
26
|
+
if (remote.length === 0) {
|
|
27
|
+
return ['Tokens page: <your Atlas deployment URL>/settings/tokens'];
|
|
28
|
+
}
|
|
29
|
+
return remote.map(([name, { url }]) => `${name}: ${settingsTokensUrl(url)}`);
|
|
30
|
+
}
|
|
17
31
|
const PAT_SCOPES = ['repo:read', 'repo:write', 'repo:admin'];
|
|
18
32
|
function stringFlag(flags, key) {
|
|
19
33
|
const value = flags[key];
|
|
@@ -41,26 +55,26 @@ export const AUTH_USAGE = `usage:
|
|
|
41
55
|
On create the raw token is printed ONCE — store it; Atlas cannot re-show it.
|
|
42
56
|
Git Basic auth: any username (e.g. git), PAT as the password.
|
|
43
57
|
|
|
44
|
-
Flags: -e/--env <
|
|
58
|
+
Flags: -e/--env <env> (see: seq-studio envs list)
|
|
45
59
|
`;
|
|
46
60
|
async function authContext(args) {
|
|
47
61
|
if (args.flags.env === true || args.flags.e === true) {
|
|
48
|
-
throw new Error('-e/--env requires a value
|
|
62
|
+
throw new Error('-e/--env requires a value — see: seq-studio envs list.');
|
|
49
63
|
}
|
|
50
64
|
try {
|
|
51
65
|
return await buildContext(args);
|
|
52
66
|
}
|
|
53
67
|
catch (err) {
|
|
54
68
|
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
-
// buildContext failed before we know the env URL; offer CLI login and
|
|
56
|
-
//
|
|
69
|
+
// buildContext failed before we know the env URL; offer CLI login and
|
|
70
|
+
// render the tokens-page URLs from the environments visible to this
|
|
71
|
+
// identity (third-party / OpCo developers only see their own hosts).
|
|
72
|
+
const tokenUrls = await settingsTokensLines();
|
|
57
73
|
throw new Error(`${message}\n` +
|
|
58
74
|
` Run \`seq-studio login\`, then retry \`auth pat create\`.\n` +
|
|
59
75
|
` Or mint a PAT in the Atlas UI (Settings → Tokens), then:\n` +
|
|
60
76
|
` export ATLAS_GIT_PAT=<token>\n` +
|
|
61
|
-
`
|
|
62
|
-
` Production: https://atlas.seqholdings.com/settings/tokens\n` +
|
|
63
|
-
` BankSouth: https://banksouth.seqholdings.com/settings/tokens`);
|
|
77
|
+
tokenUrls.map((line) => ` ${line}`).join('\n'));
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
80
|
export function parsePatScopes(raw) {
|
package/dist/auth.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface SeqapiTokens {
|
|
|
26
26
|
access_token?: string;
|
|
27
27
|
expires_at?: number;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A configured M2M credential failed to mint a token. Typed so callers that
|
|
31
|
+
* normally swallow discovery errors (lazy catalog refresh) can still surface
|
|
32
|
+
* broken CI credentials instead of a misleading "unknown env" message.
|
|
33
|
+
*/
|
|
34
|
+
export declare class M2mTokenError extends Error {
|
|
35
|
+
readonly name = "M2mTokenError";
|
|
36
|
+
}
|
|
29
37
|
export declare function seqapiTokenDir(): string;
|
|
30
38
|
export declare function seqapiTokenPath(): string;
|
|
31
39
|
export declare class NotLoggedInError extends Error {
|
|
@@ -52,4 +60,16 @@ export declare function tryGetAccessToken(options?: {
|
|
|
52
60
|
*/
|
|
53
61
|
failClosedForM2m?: boolean;
|
|
54
62
|
}): Promise<string | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Decode the `sub` claim from a JWT without verification. Verification is the
|
|
65
|
+
* server's job — this is only used to compare identities locally (e.g. to
|
|
66
|
+
* bind the cached environment catalog to the identity that fetched it).
|
|
67
|
+
*/
|
|
68
|
+
export declare function decodeJwtSub(token: string): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* The Auth0 subject the CLI would authenticate as right now, without any
|
|
71
|
+
* network call: the fixed M2M client subject when the secret is configured,
|
|
72
|
+
* else the `sub` of the cached user token, else null (anonymous).
|
|
73
|
+
*/
|
|
74
|
+
export declare function currentIdentitySubject(): Promise<string | null>;
|
|
55
75
|
export declare function saveTokens(tokens: SeqapiTokens): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -33,6 +33,14 @@ const AUTH0_M2M_CLIENT_ID = '5TLffZqvLq4ztLDjZhwKVJCpB5VNrWj0';
|
|
|
33
33
|
// `_m2m_cache`). Reused while > 60s from expiry to avoid re-minting on every
|
|
34
34
|
// call within a single process (e.g. a long `artifact dev` watch).
|
|
35
35
|
let m2mCache = null;
|
|
36
|
+
/**
|
|
37
|
+
* A configured M2M credential failed to mint a token. Typed so callers that
|
|
38
|
+
* normally swallow discovery errors (lazy catalog refresh) can still surface
|
|
39
|
+
* broken CI credentials instead of a misleading "unknown env" message.
|
|
40
|
+
*/
|
|
41
|
+
export class M2mTokenError extends Error {
|
|
42
|
+
name = 'M2mTokenError';
|
|
43
|
+
}
|
|
36
44
|
/**
|
|
37
45
|
* Mint an M2M access token via the Auth0 client-credentials grant when
|
|
38
46
|
* `AUTH0_M2M_CLIENT_SECRET` is set. Returns null when the secret is unset
|
|
@@ -57,11 +65,11 @@ async function getM2mToken() {
|
|
|
57
65
|
}),
|
|
58
66
|
});
|
|
59
67
|
if (!response.ok) {
|
|
60
|
-
throw new
|
|
68
|
+
throw new M2mTokenError(`Auth0 M2M token request failed (${response.status}): ${await response.text()}`);
|
|
61
69
|
}
|
|
62
70
|
const data = (await response.json());
|
|
63
71
|
if (!data.access_token) {
|
|
64
|
-
throw new
|
|
72
|
+
throw new M2mTokenError('Auth0 M2M response missing access_token');
|
|
65
73
|
}
|
|
66
74
|
m2mCache = {
|
|
67
75
|
accessToken: data.access_token,
|
|
@@ -146,6 +154,38 @@ export async function tryGetAccessToken(options) {
|
|
|
146
154
|
return null;
|
|
147
155
|
}
|
|
148
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Decode the `sub` claim from a JWT without verification. Verification is the
|
|
159
|
+
* server's job — this is only used to compare identities locally (e.g. to
|
|
160
|
+
* bind the cached environment catalog to the identity that fetched it).
|
|
161
|
+
*/
|
|
162
|
+
export function decodeJwtSub(token) {
|
|
163
|
+
const payload = token.split('.')[1];
|
|
164
|
+
if (!payload)
|
|
165
|
+
return null;
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
168
|
+
const sub = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'sub') : null;
|
|
169
|
+
return typeof sub === 'string' && sub ? sub : null;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The Auth0 subject the CLI would authenticate as right now, without any
|
|
177
|
+
* network call: the fixed M2M client subject when the secret is configured,
|
|
178
|
+
* else the `sub` of the cached user token, else null (anonymous).
|
|
179
|
+
*/
|
|
180
|
+
export async function currentIdentitySubject() {
|
|
181
|
+
if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
|
|
182
|
+
return `${AUTH0_M2M_CLIENT_ID}@clients`;
|
|
183
|
+
}
|
|
184
|
+
const tokens = await loadTokens();
|
|
185
|
+
if (!tokens?.access_token)
|
|
186
|
+
return null;
|
|
187
|
+
return decodeJwtSub(tokens.access_token);
|
|
188
|
+
}
|
|
149
189
|
async function loadTokens() {
|
|
150
190
|
const path = seqapiTokenPath();
|
|
151
191
|
if (!existsSync(path))
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type EnvTier } from './env-catalog.js';
|
|
1
2
|
/**
|
|
2
3
|
* `--env preview:<branch-or-slug>` targets a per-PR Vercel preview deployment
|
|
3
4
|
* without a config.toml entry. The slug after the prefix is run through the
|
|
4
5
|
* canonical branch→slug transform (see `preview.ts`). PR-number resolution and
|
|
5
6
|
* the Cloudflare WAF bypass header live in `artifact/delegate.ts`.
|
|
7
|
+
* Preview targeting is a Sequence-staff surface — resolveEnv gates it on the
|
|
8
|
+
* discovered tier.
|
|
6
9
|
*/
|
|
7
10
|
export declare const PREVIEW_ENV_PREFIX = "preview:";
|
|
8
11
|
export declare const ENV_NAMES: string[];
|
|
@@ -11,19 +14,22 @@ export interface LatticeConfig {
|
|
|
11
14
|
url: string;
|
|
12
15
|
}>;
|
|
13
16
|
defaultEnv: string;
|
|
17
|
+
/** Discovered identity tier; `anonymous` when no catalog is cached. */
|
|
18
|
+
tier: EnvTier;
|
|
14
19
|
}
|
|
15
20
|
export declare function globalConfigDir(): string;
|
|
16
21
|
export declare function configPath(): string;
|
|
17
22
|
export declare function defaultConfig(): LatticeConfig;
|
|
18
23
|
/**
|
|
19
|
-
* Read the config
|
|
20
|
-
* — no auto-write, since seq-studio should work out of the box without
|
|
21
|
-
* any state on disk. The user can run `seq-studio config init` (future)
|
|
22
|
-
* if they want a customizable file.
|
|
24
|
+
* Read the effective config. Merge precedence (later wins):
|
|
23
25
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* 1. built-in `local`
|
|
27
|
+
* 2. the cached discovered catalog (`~/.config/lattice/environments.json`)
|
|
28
|
+
* 3. user entries in config.toml (an override for `local`, or net-new envs)
|
|
29
|
+
*
|
|
30
|
+
* If the config.toml file does not exist, defaults are returned — no
|
|
31
|
+
* auto-write, since seq-studio should work out of the box without any
|
|
32
|
+
* state on disk.
|
|
27
33
|
*/
|
|
28
34
|
export declare function readConfig(): Promise<LatticeConfig>;
|
|
29
35
|
/** Write the config to disk, creating the dir if missing. */
|
|
@@ -33,12 +39,22 @@ export interface ResolvedEnv {
|
|
|
33
39
|
url: string;
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
|
-
* Resolve `--env <name>` against the
|
|
37
|
-
* config's `defaultEnv` when no name is passed. Throws a clear error
|
|
38
|
-
*
|
|
39
|
-
*
|
|
42
|
+
* Resolve `--env <name>` against the effective config. Falls back to the
|
|
43
|
+
* config's `defaultEnv` when no name is passed. Throws a clear error when
|
|
44
|
+
* the name does not resolve at the caller's tier, listing only the envs
|
|
45
|
+
* that are actually visible.
|
|
40
46
|
*/
|
|
41
47
|
export declare function resolveEnv({ config, requested, }: {
|
|
42
48
|
config: LatticeConfig;
|
|
43
49
|
requested?: string;
|
|
44
50
|
}): ResolvedEnv;
|
|
51
|
+
/**
|
|
52
|
+
* Async resolveEnv with lazy discovery: when the requested name doesn't
|
|
53
|
+
* resolve from the current config (built-ins + cached catalog + toml),
|
|
54
|
+
* refresh the catalog once — a logged-in identity that never ran
|
|
55
|
+
* `seq-studio envs refresh` (or fresh CI with AUTH0_M2M_CLIENT_SECRET)
|
|
56
|
+
* gets its environments on first use — then retry.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveEnvWithDiscovery({ requested, }: {
|
|
59
|
+
requested?: string;
|
|
60
|
+
}): Promise<ResolvedEnv>;
|
package/dist/config.js
CHANGED
|
@@ -3,25 +3,27 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
6
|
+
import { M2mTokenError } from './auth.js';
|
|
7
|
+
import { fetchCatalog, readCachedCatalog, } from './env-catalog.js';
|
|
6
8
|
import { resolvePreviewFromSlug } from './preview.js';
|
|
7
9
|
/**
|
|
8
10
|
* `--env preview:<branch-or-slug>` targets a per-PR Vercel preview deployment
|
|
9
11
|
* without a config.toml entry. The slug after the prefix is run through the
|
|
10
12
|
* canonical branch→slug transform (see `preview.ts`). PR-number resolution and
|
|
11
13
|
* the Cloudflare WAF bypass header live in `artifact/delegate.ts`.
|
|
14
|
+
* Preview targeting is a Sequence-staff surface — resolveEnv gates it on the
|
|
15
|
+
* discovered tier.
|
|
12
16
|
*/
|
|
13
17
|
export const PREVIEW_ENV_PREFIX = 'preview:';
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Only `local` ships in the published package. Additional environments are
|
|
20
|
+
* discovered per-identity from the authenticated catalog endpoint (see
|
|
21
|
+
* `env-catalog.ts`) or added by the user in config.toml. `local` is the
|
|
22
|
+
* dev / `make dev-all` URL — kept named `local` (not `dev`) so it lines up
|
|
23
|
+
* with `seqapi -e local` and `artifact-studio --env local`.
|
|
19
24
|
*/
|
|
20
25
|
const BUILT_IN_ENV_URLS = {
|
|
21
26
|
local: 'http://localhost:5001',
|
|
22
|
-
staging: 'https://staging.atlas.seqholdings.com',
|
|
23
|
-
production: 'https://atlas.seqholdings.com',
|
|
24
|
-
banksouth: 'https://banksouth.seqholdings.com',
|
|
25
27
|
};
|
|
26
28
|
export const ENV_NAMES = Object.keys(BUILT_IN_ENV_URLS);
|
|
27
29
|
export function globalConfigDir() {
|
|
@@ -35,24 +37,32 @@ export function defaultConfig() {
|
|
|
35
37
|
for (const [name, url] of Object.entries(BUILT_IN_ENV_URLS)) {
|
|
36
38
|
envs[name] = { url };
|
|
37
39
|
}
|
|
38
|
-
return { envs, defaultEnv: 'local' };
|
|
40
|
+
return { envs, defaultEnv: 'local', tier: 'anonymous' };
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
|
-
* Read the config
|
|
42
|
-
* — no auto-write, since seq-studio should work out of the box without
|
|
43
|
-
* any state on disk. The user can run `seq-studio config init` (future)
|
|
44
|
-
* if they want a customizable file.
|
|
43
|
+
* Read the effective config. Merge precedence (later wins):
|
|
45
44
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* 1. built-in `local`
|
|
46
|
+
* 2. the cached discovered catalog (`~/.config/lattice/environments.json`)
|
|
47
|
+
* 3. user entries in config.toml (an override for `local`, or net-new envs)
|
|
48
|
+
*
|
|
49
|
+
* If the config.toml file does not exist, defaults are returned — no
|
|
50
|
+
* auto-write, since seq-studio should work out of the box without any
|
|
51
|
+
* state on disk.
|
|
49
52
|
*/
|
|
50
53
|
export async function readConfig() {
|
|
54
|
+
const merged = defaultConfig();
|
|
55
|
+
const catalog = await readCachedCatalog();
|
|
56
|
+
if (catalog) {
|
|
57
|
+
merged.tier = catalog.tier;
|
|
58
|
+
for (const env of catalog.environments) {
|
|
59
|
+
merged.envs[env.name] = { url: env.url };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
const path = configPath();
|
|
52
63
|
if (!existsSync(path))
|
|
53
|
-
return
|
|
64
|
+
return merged;
|
|
54
65
|
const raw = parseToml(await readFile(path, 'utf8'));
|
|
55
|
-
const merged = defaultConfig();
|
|
56
66
|
if (raw.env) {
|
|
57
67
|
for (const [name, value] of Object.entries(raw.env)) {
|
|
58
68
|
if (typeof value?.url === 'string') {
|
|
@@ -81,15 +91,22 @@ export async function writeConfig(config) {
|
|
|
81
91
|
lines.push(`default_env = "${config.defaultEnv}"`);
|
|
82
92
|
await writeFile(path, lines.join('\n') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
83
93
|
}
|
|
94
|
+
/** Shared hint appended to visibility errors at the anonymous/partner tiers. */
|
|
95
|
+
const DISCOVERY_HINT = 'If you expect more environments, authenticate (seq-studio login, or set ' +
|
|
96
|
+
'AUTH0_M2M_CLIENT_SECRET) and run: seq-studio envs refresh';
|
|
84
97
|
/**
|
|
85
|
-
* Resolve `--env <name>` against the
|
|
86
|
-
* config's `defaultEnv` when no name is passed. Throws a clear error
|
|
87
|
-
*
|
|
88
|
-
*
|
|
98
|
+
* Resolve `--env <name>` against the effective config. Falls back to the
|
|
99
|
+
* config's `defaultEnv` when no name is passed. Throws a clear error when
|
|
100
|
+
* the name does not resolve at the caller's tier, listing only the envs
|
|
101
|
+
* that are actually visible.
|
|
89
102
|
*/
|
|
90
103
|
export function resolveEnv({ config, requested, }) {
|
|
91
104
|
const name = requested ?? config.defaultEnv;
|
|
92
105
|
if (name.startsWith(PREVIEW_ENV_PREFIX)) {
|
|
106
|
+
if (config.tier !== 'sequence') {
|
|
107
|
+
throw new Error(`Preview environments (${PREVIEW_ENV_PREFIX}<branch>) are a Sequence-staff ` +
|
|
108
|
+
`surface. ${DISCOVERY_HINT}`);
|
|
109
|
+
}
|
|
93
110
|
const { slug, url } = resolvePreviewFromSlug(name.slice(PREVIEW_ENV_PREFIX.length));
|
|
94
111
|
return { name: `${PREVIEW_ENV_PREFIX}${slug}`, url };
|
|
95
112
|
}
|
|
@@ -97,7 +114,40 @@ export function resolveEnv({ config, requested, }) {
|
|
|
97
114
|
if (!env) {
|
|
98
115
|
const known = Object.keys(config.envs).join(', ');
|
|
99
116
|
throw new Error(`Unknown env "${name}". Known envs: ${known}. ` +
|
|
100
|
-
`Pass --env <name> or set default_env in ${configPath()}
|
|
117
|
+
`Pass --env <name> or set default_env in ${configPath()}. ${DISCOVERY_HINT}`);
|
|
101
118
|
}
|
|
102
119
|
return { name, url: env.url };
|
|
103
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Async resolveEnv with lazy discovery: when the requested name doesn't
|
|
123
|
+
* resolve from the current config (built-ins + cached catalog + toml),
|
|
124
|
+
* refresh the catalog once — a logged-in identity that never ran
|
|
125
|
+
* `seq-studio envs refresh` (or fresh CI with AUTH0_M2M_CLIENT_SECRET)
|
|
126
|
+
* gets its environments on first use — then retry.
|
|
127
|
+
*/
|
|
128
|
+
export async function resolveEnvWithDiscovery({ requested, }) {
|
|
129
|
+
const config = await readConfig();
|
|
130
|
+
try {
|
|
131
|
+
return resolveEnv({ config, requested });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
let catalog;
|
|
135
|
+
try {
|
|
136
|
+
catalog = await fetchCatalog();
|
|
137
|
+
}
|
|
138
|
+
catch (fetchErr) {
|
|
139
|
+
// A configured-but-broken M2M credential is the real failure — CI must
|
|
140
|
+
// see the Auth0 error, not a misleading "unknown env" message.
|
|
141
|
+
if (fetchErr instanceof M2mTokenError)
|
|
142
|
+
throw fetchErr;
|
|
143
|
+
// Network/server errors: fall back to whatever is cached, else
|
|
144
|
+
// surface the original resolve error.
|
|
145
|
+
catalog = await readCachedCatalog();
|
|
146
|
+
}
|
|
147
|
+
if (!catalog)
|
|
148
|
+
throw err;
|
|
149
|
+
// Retry against the refreshed catalog; if the name still doesn't
|
|
150
|
+
// resolve, the retry throws an error rendered from the fresh state.
|
|
151
|
+
return resolveEnv({ config: await readConfig(), requested });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -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 <
|
|
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 {
|
|
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 =
|
|
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
|
|
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 <
|
|
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,15 @@ 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
|
|
31
33
|
seq-studio help show this message
|
|
32
34
|
|
|
33
35
|
Authenticate with: seq-studio login
|
|
34
|
-
|
|
36
|
+
Built-in env: local. Authenticated identities discover more via
|
|
37
|
+
\`seq-studio envs refresh\`; custom entries live in ~/.config/lattice/config.toml.
|
|
35
38
|
`;
|
|
36
39
|
const PROCESS_USAGE = `usage:
|
|
37
40
|
seq-studio process init <dir>
|
|
@@ -82,6 +85,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
82
85
|
const { runOrmCommand } = await import('./orm/delegate.js');
|
|
83
86
|
return runOrmCommand(sub, rest);
|
|
84
87
|
}
|
|
88
|
+
case 'envs': {
|
|
89
|
+
const { runEnvsCommand } = await import('./envs/commands.js');
|
|
90
|
+
return runEnvsCommand(sub, rest);
|
|
91
|
+
}
|
|
85
92
|
case 'login':
|
|
86
93
|
case 'logout':
|
|
87
94
|
return runSessionCommand({ argument: sub, command: namespace });
|
package/dist/orm/delegate.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
16
|
-
seq-studio orm
|
|
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
|
-
|
|
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
|
|
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 });
|
package/dist/process/commands.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
678
|
+
env = await resolveEnvWithDiscovery({ requested });
|
|
680
679
|
lines.push(`env: ok — ${env.name} (${env.url})`);
|
|
681
680
|
}
|
|
682
681
|
catch (err) {
|
package/dist/repos/commands.d.ts
CHANGED
|
@@ -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 <
|
|
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>;
|
package/dist/repos/commands.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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 <
|
|
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 <
|
|
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>;
|
package/dist/secrets/commands.js
CHANGED
|
@@ -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 <
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequenceholdings/studio-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Unified Sequence Studio CLI — `seq-studio process` (Lattice), `seq-studio artifact` (Artifact Studio), `seq-studio functions` / `secrets`, `seq-studio repos` (platform git-service), and `seq-studio auth pat` (git-service PATs). Includes Auth0 browser login shared with seqapi.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"smol-toml": "^1.4.2",
|
|
41
41
|
"tsx": "^4.20.3",
|
|
42
42
|
"zod": "^4.1.13",
|
|
43
|
-
"@sequenceholdings/artifact-studio": "0.1.
|
|
44
|
-
"@sequenceholdings/lattice": "0.1.
|
|
43
|
+
"@sequenceholdings/artifact-studio": "0.1.11",
|
|
44
|
+
"@sequenceholdings/lattice": "0.1.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@sequenceholdings/orm": "0.1.
|
|
47
|
+
"@sequenceholdings/orm": "0.1.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@sequenceholdings/orm": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@types/node": "^22.0.0",
|
|
57
57
|
"typescript": "^5.6.0",
|
|
58
58
|
"vitest": "^4.1.5",
|
|
59
|
-
"@sequenceholdings/orm": "0.1.
|
|
59
|
+
"@sequenceholdings/orm": "0.1.1"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
62
62
|
"node": ">=20"
|