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