@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 CHANGED
@@ -43,16 +43,17 @@ releases install immediately.)
43
43
 
44
44
  ## Authenticate
45
45
 
46
- `seq-studio` is a **read-only consumer of seqapi tokens** it does not
47
- have its own login flow. Run `seqapi login` once to mint tokens at
48
- `~/.config/sequence-api/tokens.json`; `seqapi`, `seq-studio process`, and
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
- seqapi login
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
- ### Sequence staff — CLI mint (optional)
210
+ ### CLI mint (optional)
210
211
 
211
- Requires `seqapi login` (Auth0). Same tokens as the UI:
212
+ Requires Auth0 login. Same identity as the UI:
212
213
 
213
214
  ```bash
214
- seqapi login
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: seqapi login
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
- // Share the seqapi token. `tryGetAccessToken` resolves an M2M
100
- // service-account token when AUTH0_M2M_CLIENT_SECRET is set (headless /
101
- // CI / cloud-agent path) and otherwise the cached `seqapi login` user
102
- // token (see ../auth.ts). If neither is available, artifact-studio
103
- // commands that need a token surface their own error we don't force
104
- // `seqapi login` here because some commands (init, validate, build) work
105
- // offline. Manual escape hatch: pass `--token <jwt>` (artifact-studio's
106
- // CLI accepts it and it wins over everything).
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
- // The ARTIFACT_STUDIO_TOKEN env var above is captured once and never
115
- // refreshes, so long-running commands (notably `artifact dev`) would start
116
- // failing with "Authentication failed" once the initial token's TTL elapses.
117
- // Hand artifact-studio a refreshing source getAccessToken() mints a fresh
118
- // access token via the Auth0 refresh grant when the cached one is near
119
- // expiry so a watch session survives indefinitely.
120
- setTokenProvider(async () => {
121
- if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
122
- // Fail closed for configured M2M failures so headless deploys never
123
- // silently fall back to another cached identity.
124
- return await getAccessToken();
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
- catch {
130
- return null;
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 Most users (no seqapi): open Atlas \u2192 Settings \u2192 Tokens, or:\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 Sequence staff: `seqapi login` then `auth pat create` (same Auth0 session\n as the Atlas UI). The CLI cannot mint a PAT without that login.\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";
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
- * (`seqapi login` Sequence staff). Third-party / OpCo developers without
6
- * seqapi should mint tokens in the Atlas UI at `/settings/tokens` instead;
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
- Most users (no seqapi): open Atlas Settings → Tokens, or:
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
- // Prefer the Atlas UI path third-party / OpCo developers do not have seqapi.
59
- // buildContext failed before we know the env URL; point at the common hosts.
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
- ` No seqapi? Mint a PAT in the Atlas UI (Settings → Tokens), then:\n` +
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\n` +
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 is a read-only consumer of the seqapi token file. Tokens
7
- * are written by `seqapi login` (Python, in `shared/seqapi/seqapi/auth.py`)
8
- * and shared between every Sequence CLI: same Auth0 tenant, same client
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 `seqapi login` can still push.
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
- * Refresh is the only write path: when the cached access token is
23
- * expired and we have a refresh token, we hit Auth0 and update the file
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: seqapi login\n' +
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: seqapi login');
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
- await import('node:fs/promises').then((fs) => fs.rename(tmp, path));
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
  }
@@ -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 `seqapi login` and retry.';
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 + seqapi login are still needed to resolve the repo and deploy.\n";
85
+ export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <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 (seqapi login) to register it`);
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 + seqapi login are still needed to resolve the repo and deploy.
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 {
@@ -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: seqapi login
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);
@@ -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: seqapi login
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') {
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * User-facing hints for minting a git-service PAT.
3
3
  *
4
- * Third-party / OpCo developers typically do not have `seqapi`. Their path is
5
- * the Atlas UI at `/settings/tokens`. Sequence staff can also use
6
- * `seq-studio auth pat create` after `seqapi login`.
4
+ * Users can mint a PAT with seq-studio after interactive login, or through
5
+ * the Atlas UI at `/settings/tokens`.
7
6
  */
8
7
  export declare function settingsTokensUrl(envUrl: string): string;
9
8
  /**
10
- * Multi-line setup instructions. Prefer the Atlas UI first (works for anyone
11
- * with Atlas access); mention the CLI mint path as a staff convenience.
9
+ * Multi-line setup instructions covering both the CLI and Atlas UI paths.
12
10
  */
13
11
  export declare function formatPatSetupHint({ envUrl, envName, indent, }: {
14
12
  envUrl: string;
package/dist/pat-hints.js CHANGED
@@ -1,28 +1,27 @@
1
1
  /**
2
2
  * User-facing hints for minting a git-service PAT.
3
3
  *
4
- * Third-party / OpCo developers typically do not have `seqapi`. Their path is
5
- * the Atlas UI at `/settings/tokens`. Sequence staff can also use
6
- * `seq-studio auth pat create` after `seqapi login`.
4
+ * Users can mint a PAT with seq-studio after interactive login, or through
5
+ * the Atlas UI at `/settings/tokens`.
7
6
  */
8
7
  export function settingsTokensUrl(envUrl) {
9
8
  return `${envUrl.replace(/\/$/, '')}/settings/tokens`;
10
9
  }
11
10
  /**
12
- * Multi-line setup instructions. Prefer the Atlas UI first (works for anyone
13
- * with Atlas access); mention the CLI mint path as a staff convenience.
11
+ * Multi-line setup instructions covering both the CLI and Atlas UI paths.
14
12
  */
15
13
  export function formatPatSetupHint({ envUrl, envName, indent = ' ', }) {
16
14
  const tokensUrl = settingsTokensUrl(envUrl);
17
15
  return [
18
- `${indent}Get a PAT (no seqapi required):`,
19
- `${indent} 1. Open ${tokensUrl} and sign in to Atlas`,
16
+ `${indent}Get a PAT with seq-studio:`,
17
+ `${indent} seq-studio login`,
18
+ `${indent} seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e ${envName}`,
19
+ `${indent}Or create one in Atlas:`,
20
+ `${indent} 1. Open ${tokensUrl} and sign in`,
20
21
  `${indent} 2. New token → scopes repo:read (add repo:write for push) → copy once`,
21
22
  `${indent} 3. export ATLAS_GIT_PAT=<token>`,
22
23
  `${indent} 4. Copy the clone URL from Repositories → Clone, then:`,
23
24
  `${indent} ATLAS_GIT_PAT=<token> seq-studio repos clone --url <https://…/repos/<id>/git>`,
24
- `${indent} (or with seqapi: seq-studio repos clone <ns>/<name>)`,
25
- `${indent}Sequence staff with seqapi can instead:`,
26
- `${indent} seqapi login && seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e ${envName}`,
25
+ `${indent} (or: seq-studio repos clone <ns>/<name>)`,
27
26
  ];
28
27
  }
@@ -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(' seqapi login');
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 `seqapi login` to resolve child process versions', { cause: err });
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 `seqapi login`)');
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 `seqapi login`.');
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 {
@@ -45,5 +45,5 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
45
45
  */
46
46
  export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
47
47
  export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
48
- export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <local|staging|production|banksouth>\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (Atlas UI /settings/tokens \u2014 no seqapi required).\n No seqapi + PAT: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seqapi login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
48
+ export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <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>;
@@ -2,7 +2,7 @@
2
2
  * `seq-studio repos <sub>` — basic management of platform git-service repos
3
3
  * over the JSON API.
4
4
  *
5
- * Everything here is API-driven (Auth0 bearer via seqapi login), matching the
5
+ * Everything here is API-driven (Auth0 bearer via seq-studio login), matching the
6
6
  * rest of seq-studio. `pull` materializes a tree over the JSON API; `clone`
7
7
  * prefers a real `git clone` when `ATLAS_GIT_PAT` is set (smart-HTTP URL from
8
8
  * `show`), otherwise falls back to the same JSON materialize path and hints
@@ -367,14 +367,15 @@ export async function reposCloneCommand(args, deps = {}) {
367
367
  const env = await resolveEnvOnly(args);
368
368
  const lines = pat
369
369
  ? [
370
- `ATLAS_GIT_PAT is set, but resolving ${namespace}/${name} needs Auth0 (seqapi login).`,
371
- ` No seqapi? Copy the clone URL from Repositories → Clone, then:`,
370
+ `ATLAS_GIT_PAT is set, but resolving ${namespace}/${name} needs Auth0 (` +
371
+ `seq-studio login).`,
372
+ ` Or copy the clone URL from Repositories → Clone, then:`,
372
373
  ` ATLAS_GIT_PAT=<token> seq-studio repos clone --url <https://…/repos/<id>/git> -e ${env.name}`,
373
374
  ]
374
375
  : [
375
- `${LOG} no ATLAS_GIT_PAT and not logged in (seqapi).`,
376
+ `${LOG} no ATLAS_GIT_PAT and not logged in.`,
376
377
  ...formatPatSetupHint({ envUrl: env.url, envName: env.name }),
377
- ` Or: seqapi login && seq-studio repos clone ${namespace}/${name} -e ${env.name}`,
378
+ ` Then: seq-studio repos clone ${namespace}/${name} -e ${env.name}`,
378
379
  ];
379
380
  throw new Error(lines.join('\n'));
380
381
  }
@@ -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 — no seqapi required).
509
- No seqapi + PAT: use --url from Repositories → Clone (or --id <uuid>).
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: seqapi login
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.9",
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). Reuses ~/.config/sequence-api/tokens.json for Auth0.",
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/lattice": "0.1.0",
44
- "@sequenceholdings/artifact-studio": "0.1.9"
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"