@sequenceholdings/studio-cli 0.1.9 → 0.1.10
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 +9 -8
- package/dist/artifact/delegate.js +75 -31
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +10 -13
- package/dist/auth.d.ts +29 -0
- package/dist/auth.js +17 -18
- package/dist/cli-errors.js +1 -1
- package/dist/functions/commands.d.ts +1 -1
- package/dist/functions/commands.js +3 -2
- package/dist/login.d.ts +12 -0
- package/dist/login.js +213 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +21 -1
- package/dist/orm/delegate.js +1 -1
- package/dist/pat-hints.d.ts +3 -5
- package/dist/pat-hints.js +9 -10
- package/dist/process/commands.js +4 -4
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +9 -8
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -43,16 +43,17 @@ releases install immediately.)
|
|
|
43
43
|
|
|
44
44
|
## Authenticate
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`seq-studio artifact` all share that file.
|
|
46
|
+
Run the built-in browser login once. `seq-studio` and `seqapi` share the
|
|
47
|
+
resulting tokens at `~/.config/sequence-api/tokens.json`, so logging in with
|
|
48
|
+
either CLI authenticates both.
|
|
50
49
|
|
|
51
50
|
```bash
|
|
52
|
-
|
|
51
|
+
seq-studio login
|
|
53
52
|
seq-studio doctor # confirms config + auth + authorization
|
|
54
53
|
```
|
|
55
54
|
|
|
55
|
+
Use `seq-studio logout` to remove the shared cached tokens.
|
|
56
|
+
|
|
56
57
|
### Headless auth (CI) — M2M
|
|
57
58
|
|
|
58
59
|
When there's no interactive login (CI, automation), set the service-account
|
|
@@ -206,12 +207,12 @@ seq-studio repos clone artifacts/ai-fluency -e staging
|
|
|
206
207
|
You can also open **Repositories → Access tokens** / the clone popover’s
|
|
207
208
|
**Manage tokens** link.
|
|
208
209
|
|
|
209
|
-
###
|
|
210
|
+
### CLI mint (optional)
|
|
210
211
|
|
|
211
|
-
Requires
|
|
212
|
+
Requires Auth0 login. Same identity as the UI:
|
|
212
213
|
|
|
213
214
|
```bash
|
|
214
|
-
|
|
215
|
+
seq-studio login
|
|
215
216
|
seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e staging
|
|
216
217
|
# optional: --expires 7d|30d|90d|1y|never (default 30d)
|
|
217
218
|
# optional: --store-credentials # git credential approve for the env host
|
|
@@ -48,9 +48,32 @@ const ARTIFACT_USAGE = `usage:
|
|
|
48
48
|
Preview deploys sit behind a Cloudflare WAF gate. Off the company network/VPN,
|
|
49
49
|
set PREVIEW_ACCESS_HEADER=<secret> and the CLI sends it as x-preview-access.
|
|
50
50
|
|
|
51
|
-
Authenticate with:
|
|
51
|
+
Authenticate with: seq-studio login
|
|
52
52
|
`;
|
|
53
53
|
export async function runArtifactCommand(sub, rest) {
|
|
54
|
+
// Older artifact-studio versions exposed nested login/logout commands backed
|
|
55
|
+
// by a separate token file. Keep those commands working, but route them to
|
|
56
|
+
// seq-studio's shared token so every namespace uses the same identity.
|
|
57
|
+
if (sub === 'login' || sub === 'logout') {
|
|
58
|
+
if (rest.length > 0) {
|
|
59
|
+
if (sub === 'login' && rest.includes('--token')) {
|
|
60
|
+
// The legacy `artifact login --token <jwt>` persisted a bearer to
|
|
61
|
+
// artifact-studio's own token file — a store this unification retires.
|
|
62
|
+
console.error('seq-studio artifact login no longer stores a bearer token.\n' +
|
|
63
|
+
'Scripted/headless options:\n' +
|
|
64
|
+
' - pass --token <jwt> directly to the artifact command (deploy/plan/whoami/...)\n' +
|
|
65
|
+
' - export ARTIFACT_STUDIO_TOKEN=<jwt> for the session\n' +
|
|
66
|
+
' - set AUTH0_M2M_CLIENT_SECRET for service-account (M2M) auth in CI');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.error(`seq-studio artifact ${sub} does not accept arguments.`);
|
|
70
|
+
}
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const auth = await import('../login.js');
|
|
74
|
+
await auth[sub]();
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
54
77
|
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
55
78
|
console.log(ARTIFACT_USAGE);
|
|
56
79
|
return sub ? 0 : 1;
|
|
@@ -96,40 +119,49 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
96
119
|
if (isPreviewUrl(resolved.url))
|
|
97
120
|
applyPreviewAccessHeader();
|
|
98
121
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
const token = await tryGetAccessToken({ failClosedForM2m: true });
|
|
108
|
-
if (token) {
|
|
109
|
-
process.env['ARTIFACT_STUDIO_TOKEN'] = token;
|
|
110
|
-
}
|
|
122
|
+
// An explicit `--token <jwt>` is the manual escape hatch and must win over
|
|
123
|
+
// everything, including a configured-but-failing M2M credential (which
|
|
124
|
+
// `tryGetAccessToken({ failClosedForM2m: true })` would otherwise turn into
|
|
125
|
+
// an abort before argv ever reaches artifact-studio). artifact-studio's
|
|
126
|
+
// `getOptionalToken` checks `flags.token` first, so when it's present we
|
|
127
|
+
// skip shared-token resolution entirely. Only the two-token `--token <jwt>`
|
|
128
|
+
// form counts: artifact-studio's parser does not split `--token=<jwt>`.
|
|
129
|
+
const hasExplicitToken = hasTokenFlag(argvForCli);
|
|
111
130
|
// Lazy import so `process` / `doctor` commands don't pull in
|
|
112
131
|
// artifact-studio's vite/react/tailwind dependency graph.
|
|
113
132
|
const { runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
return await getAccessToken();
|
|
133
|
+
if (!hasExplicitToken) {
|
|
134
|
+
// Share the seqapi token. `tryGetAccessToken` resolves an M2M
|
|
135
|
+
// service-account token when AUTH0_M2M_CLIENT_SECRET is set (headless /
|
|
136
|
+
// CI / cloud-agent path) and otherwise the cached interactive user
|
|
137
|
+
// token (see ../auth.ts). If neither is available, artifact-studio
|
|
138
|
+
// commands that need a token surface their own error — we don't force
|
|
139
|
+
// `seq-studio login` here because some commands (init, validate, build)
|
|
140
|
+
// work offline.
|
|
141
|
+
const token = await tryGetAccessToken({ failClosedForM2m: true });
|
|
142
|
+
if (token) {
|
|
143
|
+
process.env['ARTIFACT_STUDIO_TOKEN'] = token;
|
|
128
144
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
// The ARTIFACT_STUDIO_TOKEN env var above is captured once and never
|
|
146
|
+
// refreshes, so long-running commands (notably `artifact dev`) would start
|
|
147
|
+
// failing with "Authentication failed" once the initial token's TTL
|
|
148
|
+
// elapses. Hand artifact-studio a refreshing source — getAccessToken()
|
|
149
|
+
// mints a fresh access token via the Auth0 refresh grant when the cached
|
|
150
|
+
// one is near expiry — so a watch session survives indefinitely.
|
|
151
|
+
setTokenProvider(async () => {
|
|
152
|
+
if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
|
|
153
|
+
// Fail closed for configured M2M failures so headless deploys never
|
|
154
|
+
// silently fall back to another cached identity.
|
|
155
|
+
return await getAccessToken();
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
return await getAccessToken();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
133
165
|
return runArtifactStudio([sub, ...argvForCli]);
|
|
134
166
|
}
|
|
135
167
|
/**
|
|
@@ -167,6 +199,18 @@ export function extractPreviewFlags(argv) {
|
|
|
167
199
|
}
|
|
168
200
|
return { rest, prNumber, envUrl };
|
|
169
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* True when argv carries an explicit two-token `--token <jwt>` that
|
|
204
|
+
* artifact-studio's parser will bind to `flags.token`. `--token=<jwt>` is
|
|
205
|
+
* excluded because that parser stores it as a stray flag and never binds it.
|
|
206
|
+
*/
|
|
207
|
+
function hasTokenFlag(argv) {
|
|
208
|
+
const index = argv.indexOf('--token');
|
|
209
|
+
if (index === -1)
|
|
210
|
+
return false;
|
|
211
|
+
const value = argv[index + 1];
|
|
212
|
+
return value !== undefined && !value.startsWith('--');
|
|
213
|
+
}
|
|
170
214
|
function splitInlineValue(arg) {
|
|
171
215
|
const eq = arg.indexOf('=');
|
|
172
216
|
if (arg.startsWith('--') && eq !== -1)
|
|
@@ -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
|
|
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 <local|staging|production|banksouth>\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
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `seq-studio auth <sub>` — PAT management for the platform git service.
|
|
3
3
|
*
|
|
4
|
-
* Issues / lists / revokes Personal Access Tokens via Auth0 bearer auth
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* help text and error hints lead with that path.
|
|
4
|
+
* Issues / lists / revokes Personal Access Tokens via Auth0 bearer auth.
|
|
5
|
+
* Users authenticate with `seq-studio login`; the Atlas UI at
|
|
6
|
+
* `/settings/tokens` remains an alternative.
|
|
8
7
|
*
|
|
9
8
|
* The raw token is printed exactly once on create; use it as the Basic-auth
|
|
10
9
|
* password for `git clone` / `git push` (username is ignored).
|
|
@@ -33,14 +32,12 @@ export const AUTH_USAGE = `usage:
|
|
|
33
32
|
Issue a personal access token for git clone / git push against the platform
|
|
34
33
|
git service.
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
Authenticate with \`seq-studio login\`, then run \`auth pat create\`.
|
|
36
|
+
Alternatively, open Atlas → Settings → Tokens:
|
|
37
37
|
https://<atlas-host>/settings/tokens
|
|
38
38
|
Sign in, create a token (repo:read / repo:write), copy once, then:
|
|
39
39
|
export ATLAS_GIT_PAT=<token>
|
|
40
40
|
|
|
41
|
-
Sequence staff: \`seqapi login\` then \`auth pat create\` (same Auth0 session
|
|
42
|
-
as the Atlas UI). The CLI cannot mint a PAT without that login.
|
|
43
|
-
|
|
44
41
|
On create the raw token is printed ONCE — store it; Atlas cannot re-show it.
|
|
45
42
|
Git Basic auth: any username (e.g. git), PAT as the password.
|
|
46
43
|
|
|
@@ -55,15 +52,15 @@ async function authContext(args) {
|
|
|
55
52
|
}
|
|
56
53
|
catch (err) {
|
|
57
54
|
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
-
//
|
|
59
|
-
//
|
|
55
|
+
// buildContext failed before we know the env URL; offer CLI login and point
|
|
56
|
+
// at the common Atlas hosts as an alternative.
|
|
60
57
|
throw new Error(`${message}\n` +
|
|
61
|
-
`
|
|
58
|
+
` Run \`seq-studio login\`, then retry \`auth pat create\`.\n` +
|
|
59
|
+
` Or mint a PAT in the Atlas UI (Settings → Tokens), then:\n` +
|
|
62
60
|
` export ATLAS_GIT_PAT=<token>\n` +
|
|
63
61
|
` Staging: https://staging.atlas.seqholdings.com/settings/tokens\n` +
|
|
64
62
|
` Production: https://atlas.seqholdings.com/settings/tokens\n` +
|
|
65
|
-
` BankSouth: https://banksouth.seqholdings.com/settings/tokens
|
|
66
|
-
` Sequence staff: run \`seqapi login\`, then retry \`auth pat create\`.`);
|
|
63
|
+
` BankSouth: https://banksouth.seqholdings.com/settings/tokens`);
|
|
67
64
|
}
|
|
68
65
|
}
|
|
69
66
|
export function parsePatScopes(raw) {
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seq-studio and seqapi share one token file and one Auth0 application:
|
|
3
|
+
* same tenant, client id, audience, and token shape. Either CLI can log in,
|
|
4
|
+
* and the resulting access token validates against every Atlas /api/* route.
|
|
5
|
+
*
|
|
6
|
+
* Two token sources, in the SAME precedence order as seqapi's
|
|
7
|
+
* `get_access_token` (`shared/seqapi/seqapi/auth.py`):
|
|
8
|
+
* 1. M2M service account — Auth0 client-credentials grant, used when
|
|
9
|
+
* `AUTH0_M2M_CLIENT_SECRET` is set. This is the headless path: CI /
|
|
10
|
+
* cloud agents with no interactive login can still push.
|
|
11
|
+
* (M2M carries app scopes but NO user identity / workspace membership
|
|
12
|
+
* — see the `atlas-test-access` rule.)
|
|
13
|
+
* 2. Cached user token — read from the seqapi token file and refreshed
|
|
14
|
+
* via the Auth0 refresh-token grant when near expiry.
|
|
15
|
+
*
|
|
16
|
+
* Login and refresh both write the shared file. This mirrors
|
|
17
|
+
* `seqapi._save_tokens` exactly:
|
|
18
|
+
* same fields, same shape, same 0o600 permissions, atomic write via
|
|
19
|
+
* tmpfile + rename. The M2M token is in-memory only (never persisted).
|
|
20
|
+
*/
|
|
21
|
+
export declare const AUTH0_DOMAIN = "dev-n1t8ts403fp8oyxp.us.auth0.com";
|
|
22
|
+
export declare const AUTH0_CLIENT_ID = "GD9riCDWocfc66odpWBjwBiX43qqAX8r";
|
|
23
|
+
export declare const AUTH0_AUDIENCE = "https://api.studio.com";
|
|
24
|
+
export interface SeqapiTokens {
|
|
25
|
+
refresh_token?: string;
|
|
26
|
+
access_token?: string;
|
|
27
|
+
expires_at?: number;
|
|
28
|
+
}
|
|
1
29
|
export declare function seqapiTokenDir(): string;
|
|
2
30
|
export declare function seqapiTokenPath(): string;
|
|
3
31
|
export declare class NotLoggedInError extends Error {
|
|
@@ -24,3 +52,4 @@ export declare function tryGetAccessToken(options?: {
|
|
|
24
52
|
*/
|
|
25
53
|
failClosedForM2m?: boolean;
|
|
26
54
|
}): Promise<string | null>;
|
|
55
|
+
export declare function saveTokens(tokens: SeqapiTokens): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
/**
|
|
6
|
-
* seq-studio
|
|
7
|
-
*
|
|
8
|
-
* and
|
|
9
|
-
* id, same audience — so the same access token validates against every
|
|
10
|
-
* Atlas /api/* route.
|
|
6
|
+
* seq-studio and seqapi share one token file and one Auth0 application:
|
|
7
|
+
* same tenant, client id, audience, and token shape. Either CLI can log in,
|
|
8
|
+
* and the resulting access token validates against every Atlas /api/* route.
|
|
11
9
|
*
|
|
12
10
|
* Two token sources, in the SAME precedence order as seqapi's
|
|
13
11
|
* `get_access_token` (`shared/seqapi/seqapi/auth.py`):
|
|
14
12
|
* 1. M2M service account — Auth0 client-credentials grant, used when
|
|
15
13
|
* `AUTH0_M2M_CLIENT_SECRET` is set. This is the headless path: CI /
|
|
16
|
-
* cloud agents with no interactive
|
|
14
|
+
* cloud agents with no interactive login can still push.
|
|
17
15
|
* (M2M carries app scopes but NO user identity / workspace membership
|
|
18
16
|
* — see the `atlas-test-access` rule.)
|
|
19
17
|
* 2. Cached user token — read from the seqapi token file and refreshed
|
|
20
18
|
* via the Auth0 refresh-token grant when near expiry.
|
|
21
19
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* with the rotated tokens. This mirrors `seqapi._save_tokens` exactly:
|
|
20
|
+
* Login and refresh both write the shared file. This mirrors
|
|
21
|
+
* `seqapi._save_tokens` exactly:
|
|
25
22
|
* same fields, same shape, same 0o600 permissions, atomic write via
|
|
26
23
|
* tmpfile + rename. The M2M token is in-memory only (never persisted).
|
|
27
24
|
*/
|
|
28
25
|
// Match `shared/seqapi/seqapi/config.py`. Hard-coded because the seqapi
|
|
29
26
|
// CLI also hard-codes them — there's a single Sequence Auth0 tenant for
|
|
30
27
|
// all Sequence CLIs.
|
|
31
|
-
const AUTH0_DOMAIN = 'dev-n1t8ts403fp8oyxp.us.auth0.com';
|
|
32
|
-
const AUTH0_CLIENT_ID = 'GD9riCDWocfc66odpWBjwBiX43qqAX8r';
|
|
33
|
-
const AUTH0_AUDIENCE = 'https://api.studio.com';
|
|
28
|
+
export const AUTH0_DOMAIN = 'dev-n1t8ts403fp8oyxp.us.auth0.com';
|
|
29
|
+
export const AUTH0_CLIENT_ID = 'GD9riCDWocfc66odpWBjwBiX43qqAX8r';
|
|
30
|
+
export const AUTH0_AUDIENCE = 'https://api.studio.com';
|
|
34
31
|
const AUTH0_M2M_CLIENT_ID = '5TLffZqvLq4ztLDjZhwKVJCpB5VNrWj0';
|
|
35
32
|
// In-memory cache for the M2M token (seconds-based, mirrors seqapi's
|
|
36
33
|
// `_m2m_cache`). Reused while > 60s from expiry to avoid re-minting on every
|
|
@@ -81,8 +78,7 @@ export function seqapiTokenPath() {
|
|
|
81
78
|
export class NotLoggedInError extends Error {
|
|
82
79
|
name = 'NotLoggedInError';
|
|
83
80
|
constructor() {
|
|
84
|
-
super('Not logged in. Run:
|
|
85
|
-
'seq-studio does not have its own login flow — it shares tokens with seqapi.\n' +
|
|
81
|
+
super('Not logged in. Run: seq-studio login\n' +
|
|
86
82
|
'For headless contexts (CI / cloud agents), set AUTH0_M2M_CLIENT_SECRET for ' +
|
|
87
83
|
'service-account (M2M) access instead.');
|
|
88
84
|
}
|
|
@@ -117,7 +113,7 @@ export async function getAccessToken() {
|
|
|
117
113
|
}),
|
|
118
114
|
});
|
|
119
115
|
if (response.status === 401 || response.status === 403) {
|
|
120
|
-
throw new Error('Refresh token expired or revoked. Re-authenticate with:
|
|
116
|
+
throw new Error('Refresh token expired or revoked. Re-authenticate with: seq-studio login');
|
|
121
117
|
}
|
|
122
118
|
if (!response.ok) {
|
|
123
119
|
throw new Error(`Auth0 token refresh failed (${response.status}): ${await response.text()}`);
|
|
@@ -161,11 +157,14 @@ async function loadTokens() {
|
|
|
161
157
|
return null;
|
|
162
158
|
}
|
|
163
159
|
}
|
|
164
|
-
async function saveTokens(tokens) {
|
|
160
|
+
export async function saveTokens(tokens) {
|
|
165
161
|
const path = seqapiTokenPath();
|
|
166
162
|
await mkdir(dirname(path), { recursive: true });
|
|
167
163
|
// Mirror seqapi's atomic-write pattern: write to tmpfile then rename.
|
|
168
164
|
const tmp = path + '.tmp';
|
|
169
165
|
await writeFile(tmp, JSON.stringify(tokens, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
170
|
-
|
|
166
|
+
// writeFile's mode only applies when creating a file. Reset it explicitly in
|
|
167
|
+
// case a prior interrupted login left a permissive tmp file behind.
|
|
168
|
+
await chmod(tmp, 0o600);
|
|
169
|
+
await rename(tmp, path);
|
|
171
170
|
}
|
package/dist/cli-errors.js
CHANGED
|
@@ -37,7 +37,7 @@ export function clarifyApplyFailureReason(reason) {
|
|
|
37
37
|
function nextStepForAtlasError(error) {
|
|
38
38
|
const msg = error.message.toLowerCase();
|
|
39
39
|
if (error.status === 401) {
|
|
40
|
-
return 'Next step: run `
|
|
40
|
+
return 'Next step: run `seq-studio login` and retry.';
|
|
41
41
|
}
|
|
42
42
|
if (error.status === 403) {
|
|
43
43
|
return 'Next step: confirm `-e` targets the right environment and you have the required access grant.';
|
|
@@ -82,5 +82,5 @@ export declare function functionsRollbackCommand(args: ParsedArgs): Promise<numb
|
|
|
82
82
|
/** Minimal dotenv parser — KEY=VALUE lines, quotes stripped, comments skipped. */
|
|
83
83
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
84
84
|
export declare function functionsDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
85
|
-
export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <local|staging|production|banksouth|preview:<slug>> \u00B7 --fn <slug> \u00B7 --dir <path>\n --from-env-file <path> (default: .env) source file for secret values\n --no-wait \u00B7 --yes\n --no-provision (deploy) update-only: error instead of registering a new\n shell, writing secret values, or attaching secrets (CI sweep)\n\n Source for build/deploy: a local --dir (default .), a platform git-service\n repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a\n branch/tag/commit (default: the repo's default branch). Remote sources record\n the pinned commit as provenance (never dirty) and NEVER read a repo-committed\n .env for secret values \u2014 provision secrets server-side or pass a local\n --from-env-file (resolved against your cwd).\n\n --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT\n (repo:read scope \u2014 `seq-studio auth pat create --scopes repo:read`, or\n Atlas \u2192 Settings \u2192 Tokens). It's the same token you clone the repo with;\n --env +
|
|
85
|
+
export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <local|staging|production|banksouth|preview:<slug>> \u00B7 --fn <slug> \u00B7 --dir <path>\n --from-env-file <path> (default: .env) source file for secret values\n --no-wait \u00B7 --yes\n --no-provision (deploy) update-only: error instead of registering a new\n shell, writing secret values, or attaching secrets (CI sweep)\n\n Source for build/deploy: a local --dir (default .), a platform git-service\n repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a\n branch/tag/commit (default: the repo's default branch). Remote sources record\n the pinned commit as provenance (never dirty) and NEVER read a repo-committed\n .env for secret values \u2014 provision secrets server-side or pass a local\n --from-env-file (resolved against your cwd).\n\n --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT\n (repo:read scope \u2014 `seq-studio auth pat create --scopes repo:read`, or\n Atlas \u2192 Settings \u2192 Tokens). It's the same token you clone the repo with;\n --env + seq-studio login are still needed to resolve the repo and deploy.\n";
|
|
86
86
|
export declare function runFunctionsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
@@ -544,7 +544,8 @@ async function deployFromResolvedSource({ args, spec, ctx: earlyCtx, source, })
|
|
|
544
544
|
const problems = [];
|
|
545
545
|
if (!existingShell) {
|
|
546
546
|
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 (
|
|
547
|
+
`\`seq-studio functions deploy -e ${ctx.env.name}\` once with your own login (` +
|
|
548
|
+
`\`seq-studio login\`) to register it`);
|
|
548
549
|
}
|
|
549
550
|
for (const c of classification?.secrets ?? []) {
|
|
550
551
|
if (c.category === 'UPLOAD_NEW' || c.category === 'OVERWRITE') {
|
|
@@ -957,7 +958,7 @@ export const FUNCTIONS_USAGE = `usage:
|
|
|
957
958
|
--repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT
|
|
958
959
|
(repo:read scope — \`seq-studio auth pat create --scopes repo:read\`, or
|
|
959
960
|
Atlas → Settings → Tokens). It's the same token you clone the repo with;
|
|
960
|
-
--env +
|
|
961
|
+
--env + seq-studio login are still needed to resolve the repo and deploy.
|
|
961
962
|
`;
|
|
962
963
|
export async function runFunctionsCommand(sub, args) {
|
|
963
964
|
try {
|
package/dist/login.d.ts
ADDED
|
@@ -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,213 @@
|
|
|
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
|
+
const DEFAULT_REDIRECT_PORT = 5099;
|
|
9
|
+
const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
|
|
10
|
+
function base64Url(input) {
|
|
11
|
+
return input.toString('base64url');
|
|
12
|
+
}
|
|
13
|
+
function configuredPort() {
|
|
14
|
+
const raw = process.env.SEQAPI_PORT?.trim();
|
|
15
|
+
if (!raw)
|
|
16
|
+
return DEFAULT_REDIRECT_PORT;
|
|
17
|
+
const port = Number(raw);
|
|
18
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
19
|
+
throw new Error(`SEQAPI_PORT must be an integer from 1 to 65535 (got "${raw}").`);
|
|
20
|
+
}
|
|
21
|
+
return port;
|
|
22
|
+
}
|
|
23
|
+
function authorizationUrl({ challenge, redirectUri, state, }) {
|
|
24
|
+
const url = new URL(`https://${AUTH0_DOMAIN}/authorize`);
|
|
25
|
+
url.searchParams.set('response_type', 'code');
|
|
26
|
+
url.searchParams.set('client_id', AUTH0_CLIENT_ID);
|
|
27
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
28
|
+
url.searchParams.set('scope', 'openid profile email offline_access');
|
|
29
|
+
url.searchParams.set('audience', AUTH0_AUDIENCE);
|
|
30
|
+
url.searchParams.set('code_challenge', challenge);
|
|
31
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
32
|
+
url.searchParams.set('state', state);
|
|
33
|
+
return url.toString();
|
|
34
|
+
}
|
|
35
|
+
export async function openSystemBrowser(authorizationUrl) {
|
|
36
|
+
const windows = process.platform === 'win32';
|
|
37
|
+
const command = process.platform === 'darwin' ? 'open' : windows ? 'cmd' : 'xdg-open';
|
|
38
|
+
// On Windows the URL must be quoted manually (with verbatim arguments so
|
|
39
|
+
// Node does not re-escape): cmd otherwise splits the unquoted URL on `&`,
|
|
40
|
+
// truncating the query string and executing the tail as commands. The empty
|
|
41
|
+
// `""` is `start`'s window-title slot — without it, start would treat the
|
|
42
|
+
// quoted URL as the title.
|
|
43
|
+
const args = windows
|
|
44
|
+
? ['/c', 'start', '""', `"${authorizationUrl}"`]
|
|
45
|
+
: [authorizationUrl];
|
|
46
|
+
const child = spawn(command, args, {
|
|
47
|
+
stdio: 'ignore',
|
|
48
|
+
detached: true,
|
|
49
|
+
...(windows ? { windowsVerbatimArguments: true } : {}),
|
|
50
|
+
});
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
child.once('spawn', resolve);
|
|
53
|
+
child.once('error', reject);
|
|
54
|
+
});
|
|
55
|
+
child.unref();
|
|
56
|
+
}
|
|
57
|
+
function callbackPort(server) {
|
|
58
|
+
const address = server.address();
|
|
59
|
+
if (!address || typeof address === 'string') {
|
|
60
|
+
throw new Error('Auth0 callback server did not expose a TCP port.');
|
|
61
|
+
}
|
|
62
|
+
return address.port;
|
|
63
|
+
}
|
|
64
|
+
function sendHtml({ body, response, status, }) {
|
|
65
|
+
response.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
|
|
66
|
+
response.end(body);
|
|
67
|
+
}
|
|
68
|
+
function waitForAuthorizationCode({ expectedState, onListening, port, timeoutMs, }) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
let redirectUri = '';
|
|
71
|
+
let settled = false;
|
|
72
|
+
let timer;
|
|
73
|
+
const finish = ({ code, error }) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
server.close();
|
|
79
|
+
if (error)
|
|
80
|
+
reject(error);
|
|
81
|
+
else if (code)
|
|
82
|
+
resolve({ code, redirectUri });
|
|
83
|
+
else
|
|
84
|
+
reject(new Error('Auth0 callback completed without an authorization code.'));
|
|
85
|
+
};
|
|
86
|
+
const server = createServer((request, response) => {
|
|
87
|
+
const url = new URL(request.url ?? '/', redirectUri);
|
|
88
|
+
const code = url.searchParams.get('code');
|
|
89
|
+
const authError = url.searchParams.get('error');
|
|
90
|
+
if (!code && !authError) {
|
|
91
|
+
response.writeHead(204);
|
|
92
|
+
response.end();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (url.searchParams.get('state') !== expectedState) {
|
|
96
|
+
sendHtml({
|
|
97
|
+
body: '<h1>Sequence login failed</h1><p>Authorization state mismatch.</p>',
|
|
98
|
+
response,
|
|
99
|
+
status: 400,
|
|
100
|
+
});
|
|
101
|
+
finish({ error: new Error('Auth0 callback state mismatch.') });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (authError) {
|
|
105
|
+
const description = url.searchParams.get('error_description') ?? authError;
|
|
106
|
+
sendHtml({
|
|
107
|
+
body: '<h1>Sequence login failed</h1><p>Return to the terminal for details.</p>',
|
|
108
|
+
response,
|
|
109
|
+
status: 400,
|
|
110
|
+
});
|
|
111
|
+
finish({ error: new Error(`Auth0 login failed: ${description}`) });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
sendHtml({
|
|
115
|
+
body: '<h1>Sequence login complete</h1><p>You may close this tab.</p>',
|
|
116
|
+
response,
|
|
117
|
+
status: 200,
|
|
118
|
+
});
|
|
119
|
+
finish({ code: code ?? undefined });
|
|
120
|
+
});
|
|
121
|
+
server.once('error', (error) => {
|
|
122
|
+
const message = error.code === 'EADDRINUSE'
|
|
123
|
+
? `Port ${port} is in use. Stop the process using it, or set SEQAPI_PORT to a registered Auth0 callback port.`
|
|
124
|
+
: `Could not start the Auth0 callback server: ${error.message}`;
|
|
125
|
+
finish({ error: new Error(message) });
|
|
126
|
+
});
|
|
127
|
+
server.listen(port, 'localhost', () => {
|
|
128
|
+
redirectUri = `http://localhost:${callbackPort(server)}`;
|
|
129
|
+
onListening(redirectUri);
|
|
130
|
+
});
|
|
131
|
+
timer = setTimeout(() => {
|
|
132
|
+
finish({
|
|
133
|
+
error: new Error(`Login timed out after ${Math.ceil(timeoutMs / 1_000)}s waiting for the browser callback.`),
|
|
134
|
+
});
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function parseTokenResponse(value) {
|
|
139
|
+
if (!value || typeof value !== 'object') {
|
|
140
|
+
throw new Error('Auth0 token response was not an object.');
|
|
141
|
+
}
|
|
142
|
+
const accessToken = Reflect.get(value, 'access_token');
|
|
143
|
+
const refreshToken = Reflect.get(value, 'refresh_token');
|
|
144
|
+
const expiresIn = Reflect.get(value, 'expires_in');
|
|
145
|
+
if (typeof accessToken !== 'string' || !accessToken) {
|
|
146
|
+
throw new Error('Auth0 token response missing access_token.');
|
|
147
|
+
}
|
|
148
|
+
if (typeof refreshToken !== 'string' || !refreshToken) {
|
|
149
|
+
throw new Error('No refresh token returned. Ensure Auth0 offline access is enabled.');
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
accessToken,
|
|
153
|
+
refreshToken,
|
|
154
|
+
expiresIn: typeof expiresIn === 'number' ? expiresIn : 86_400,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBrowser = openSystemBrowser, port = configuredPort(), timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS, }) {
|
|
158
|
+
const verifier = base64Url(randomBytes(32));
|
|
159
|
+
const challenge = base64Url(createHash('sha256').update(verifier).digest());
|
|
160
|
+
const state = base64Url(randomBytes(32));
|
|
161
|
+
const callback = await waitForAuthorizationCode({
|
|
162
|
+
expectedState: state,
|
|
163
|
+
port,
|
|
164
|
+
timeoutMs,
|
|
165
|
+
onListening: (redirectUri) => {
|
|
166
|
+
const url = authorizationUrl({ challenge, redirectUri, state });
|
|
167
|
+
console.error(`Opening browser for Sequence login. If it does not open, visit:\n${url}`);
|
|
168
|
+
void Promise.resolve(openBrowser(url)).catch((error) => {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
console.error(`Could not open a browser automatically: ${message}`);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
const tokenResponse = await fetchImpl(`https://${AUTH0_DOMAIN}/oauth/token`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
grant_type: 'authorization_code',
|
|
179
|
+
client_id: AUTH0_CLIENT_ID,
|
|
180
|
+
code: callback.code,
|
|
181
|
+
redirect_uri: callback.redirectUri,
|
|
182
|
+
code_verifier: verifier,
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
if (!tokenResponse.ok) {
|
|
186
|
+
throw new Error(`Auth0 token exchange failed (${tokenResponse.status}): ${await tokenResponse.text()}`);
|
|
187
|
+
}
|
|
188
|
+
const tokens = parseTokenResponse(await tokenResponse.json());
|
|
189
|
+
await saveTokens({
|
|
190
|
+
access_token: tokens.accessToken,
|
|
191
|
+
refresh_token: tokens.refreshToken,
|
|
192
|
+
expires_at: now() / 1_000 + tokens.expiresIn,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export async function login() {
|
|
196
|
+
await loginWithPkce({});
|
|
197
|
+
console.log(`Authenticated. Tokens saved to ${seqapiTokenPath()}.`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Pre-unification artifact-studio token file. `seq-studio artifact` commands
|
|
201
|
+
* still fall back to it (artifact-studio's `getAccessToken` →
|
|
202
|
+
* `readTokenConfig`, see shared/services/artifact-studio/src/config.ts), so
|
|
203
|
+
* logout must clear it too or artifact commands would stay authenticated
|
|
204
|
+
* after a successful logout.
|
|
205
|
+
*/
|
|
206
|
+
function legacyArtifactTokenPath() {
|
|
207
|
+
return join(homedir(), '.config', 'sequence-artifact-studio', 'tokens.json');
|
|
208
|
+
}
|
|
209
|
+
export async function logout() {
|
|
210
|
+
await rm(seqapiTokenPath(), { force: true });
|
|
211
|
+
await rm(legacyArtifactTokenPath(), { force: true });
|
|
212
|
+
console.log('Logged out of seq-studio and seqapi.');
|
|
213
|
+
}
|
package/dist/main.d.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
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 login authenticate interactively with Auth0
|
|
12
|
+
* seq-studio logout remove cached user tokens
|
|
11
13
|
* seq-studio doctor check token + env + writer gate
|
|
12
14
|
* seq-studio help show usage
|
|
13
15
|
*/
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
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 login authenticate interactively with Auth0
|
|
12
|
+
* seq-studio logout remove cached user tokens
|
|
11
13
|
* seq-studio doctor check token + env + writer gate
|
|
12
14
|
* seq-studio help show usage
|
|
13
15
|
*/
|
|
@@ -23,10 +25,12 @@ const TOP_LEVEL_USAGE = `usage:
|
|
|
23
25
|
seq-studio repos <sub> [args] list | namespaces | show | create | clone | pull | delete
|
|
24
26
|
seq-studio auth <sub> [args] pat create | pat list | pat revoke
|
|
25
27
|
seq-studio orm <sub> [args] init | validate | plan | apply
|
|
28
|
+
seq-studio login authenticate in the browser
|
|
29
|
+
seq-studio logout remove cached user tokens
|
|
26
30
|
seq-studio doctor [-e <env>] diagnose config, auth, and writer gate
|
|
27
31
|
seq-studio help show this message
|
|
28
32
|
|
|
29
|
-
Authenticate with:
|
|
33
|
+
Authenticate with: seq-studio login
|
|
30
34
|
Env URLs come from ~/.config/lattice/config.toml (built-ins: local, staging, production, banksouth).
|
|
31
35
|
`;
|
|
32
36
|
const PROCESS_USAGE = `usage:
|
|
@@ -78,6 +82,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
78
82
|
const { runOrmCommand } = await import('./orm/delegate.js');
|
|
79
83
|
return runOrmCommand(sub, rest);
|
|
80
84
|
}
|
|
85
|
+
case 'login':
|
|
86
|
+
case 'logout':
|
|
87
|
+
return runSessionCommand({ argument: sub, command: namespace });
|
|
81
88
|
case 'doctor':
|
|
82
89
|
return doctorCommand(parseArgs([sub, ...rest].filter(Boolean)));
|
|
83
90
|
default:
|
|
@@ -86,6 +93,19 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
86
93
|
return 1;
|
|
87
94
|
}
|
|
88
95
|
}
|
|
96
|
+
async function runSessionCommand({ argument, command, }) {
|
|
97
|
+
if (argument === 'help' || argument === '--help' || argument === '-h') {
|
|
98
|
+
console.log(`usage: seq-studio ${command}`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (argument) {
|
|
102
|
+
console.error(`seq-studio ${command} does not accept arguments.`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const auth = await import('./login.js');
|
|
106
|
+
await auth[command]();
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
89
109
|
async function runProcessNamespace(sub, rest) {
|
|
90
110
|
if (!sub) {
|
|
91
111
|
console.error(PROCESS_USAGE);
|
package/dist/orm/delegate.js
CHANGED
|
@@ -16,7 +16,7 @@ const ORM_USAGE = `usage:
|
|
|
16
16
|
seq-studio orm apply [dir] -e <env> register the definitions and apply them to the env
|
|
17
17
|
|
|
18
18
|
Built-in envs: local, staging, production, banksouth.
|
|
19
|
-
Authenticate with:
|
|
19
|
+
Authenticate with: seq-studio login
|
|
20
20
|
`;
|
|
21
21
|
export async function runOrmCommand(sub, rest) {
|
|
22
22
|
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
package/dist/pat-hints.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User-facing hints for minting a git-service PAT.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the Atlas UI at `/settings/tokens`.
|
|
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
|
|
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
|
-
*
|
|
5
|
-
* the Atlas UI at `/settings/tokens`.
|
|
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
|
|
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
|
|
19
|
-
`${indent}
|
|
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
|
|
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
|
}
|
package/dist/process/commands.js
CHANGED
|
@@ -161,7 +161,7 @@ export async function initCommand(args) {
|
|
|
161
161
|
// pnpm-workspace.yaml's `minimumReleaseAge: 10080` supply-chain
|
|
162
162
|
// gate actually takes effect (npm has no equivalent setting).
|
|
163
163
|
console.log(` cd ${relative(process.cwd(), dir) || '.'} && pnpm install`);
|
|
164
|
-
console.log('
|
|
164
|
+
console.log(' seq-studio login');
|
|
165
165
|
console.log(' seq-studio process plan -e local');
|
|
166
166
|
return 0;
|
|
167
167
|
}
|
|
@@ -209,7 +209,7 @@ async function buildBundleForPublish(args, defs) {
|
|
|
209
209
|
}
|
|
210
210
|
catch (err) {
|
|
211
211
|
if (hasSubprocess) {
|
|
212
|
-
throw new Error('subprocess nodes require -e <env> and `
|
|
212
|
+
throw new Error('subprocess nodes require -e <env> and `seq-studio login` to resolve child process versions', { cause: err });
|
|
213
213
|
}
|
|
214
214
|
return await buildBundleFromProcesses(defs);
|
|
215
215
|
}
|
|
@@ -221,7 +221,7 @@ export async function planCommand(args) {
|
|
|
221
221
|
console.log(JSON.stringify(summary, null, 2));
|
|
222
222
|
const loadActiveProcess = await tryBuildActiveProcessLoader(args);
|
|
223
223
|
if (!loadActiveProcess) {
|
|
224
|
-
console.log('\n(no diff — env or auth unavailable; pass --env <name> and run `
|
|
224
|
+
console.log('\n(no diff — env or auth unavailable; pass --env <name> and run `seq-studio login`)');
|
|
225
225
|
return 0;
|
|
226
226
|
}
|
|
227
227
|
const diff = await diffBundleAgainstActive({ newBundle: bundle, loadActiveProcess });
|
|
@@ -727,7 +727,7 @@ export async function doctorCommand(args) {
|
|
|
727
727
|
ok = false;
|
|
728
728
|
}
|
|
729
729
|
else if (err.status === 401) {
|
|
730
|
-
lines.push('writer gate: FAIL — 401. Token is rejected by Atlas — run `
|
|
730
|
+
lines.push('writer gate: FAIL — 401. Token is rejected by Atlas — run `seq-studio login`.');
|
|
731
731
|
ok = false;
|
|
732
732
|
}
|
|
733
733
|
else {
|
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 <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
|
|
48
|
+
export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <local|staging|production|banksouth>\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
|
|
49
49
|
export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
package/dist/repos/commands.js
CHANGED
|
@@ -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
|
|
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
|
|
@@ -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 (
|
|
371
|
-
|
|
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
|
|
376
|
+
`${LOG} no ATLAS_GIT_PAT and not logged in.`,
|
|
376
377
|
...formatPatSetupHint({ envUrl: env.url, envName: env.name }),
|
|
377
|
-
`
|
|
378
|
+
` Then: seq-studio repos clone ${namespace}/${name} -e ${env.name}`,
|
|
378
379
|
];
|
|
379
380
|
throw new Error(lines.join('\n'));
|
|
380
381
|
}
|
|
@@ -505,11 +506,11 @@ export const REPOS_USAGE = `usage:
|
|
|
505
506
|
|
|
506
507
|
clone prefers real git clone (PAT via askpass — never written into the remote
|
|
507
508
|
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
|
|
509
|
-
|
|
509
|
+
how to get a PAT (seq-studio or Atlas UI /settings/tokens).
|
|
510
|
+
PAT without Auth0 login: use --url from Repositories → Clone (or --id <uuid>).
|
|
510
511
|
--ref accepts a branch, tag, or commit SHA (SHA → clone then checkout).
|
|
511
512
|
|
|
512
|
-
Authenticate JSON API calls with:
|
|
513
|
+
Authenticate JSON API calls with: seq-studio login
|
|
513
514
|
Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings → Tokens)
|
|
514
515
|
`;
|
|
515
516
|
export async function runReposCommand(sub, args) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequenceholdings/studio-cli",
|
|
3
|
-
"version": "0.1.
|
|
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).
|
|
3
|
+
"version": "0.1.10",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"smol-toml": "^1.4.2",
|
|
41
41
|
"tsx": "^4.20.3",
|
|
42
42
|
"zod": "^4.1.13",
|
|
43
|
-
"@sequenceholdings/
|
|
44
|
-
"@sequenceholdings/
|
|
43
|
+
"@sequenceholdings/artifact-studio": "0.1.10",
|
|
44
|
+
"@sequenceholdings/lattice": "0.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@sequenceholdings/orm": "0.1.0"
|