@sequenceholdings/studio-cli 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -18
- package/dist/artifact/delegate.js +46 -5
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +21 -7
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +42 -2
- package/dist/bin.js +8 -2
- package/dist/config.d.ts +27 -11
- package/dist/config.js +72 -22
- package/dist/env-catalog.d.ts +63 -0
- package/dist/env-catalog.js +111 -0
- package/dist/envs/commands.d.ts +1 -0
- package/dist/envs/commands.js +74 -0
- package/dist/functions/commands.d.ts +1 -1
- package/dist/functions/commands.js +4 -5
- package/dist/login.js +20 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +16 -1
- package/dist/orm/delegate.js +7 -7
- package/dist/process/commands.js +7 -8
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +5 -6
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +1 -1
- package/dist/update-check-refresh.d.ts +1 -0
- package/dist/update-check-refresh.js +9 -0
- package/dist/update-check.d.ts +27 -0
- package/dist/update-check.js +200 -0
- package/package.json +5 -5
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiered environment discovery.
|
|
3
|
+
*
|
|
4
|
+
* The published package ships with only the `local` environment baked in.
|
|
5
|
+
* Every other deployment URL is *discovered* from the authenticated
|
|
6
|
+
* `GET /api/platform/environments` endpoint and cached on disk:
|
|
7
|
+
*
|
|
8
|
+
* - `anonymous` — no usable token (or the endpoint rejected it):
|
|
9
|
+
* `local` only, no catalog cached.
|
|
10
|
+
* - `sequence` — Sequence staff / trusted M2M: full catalog
|
|
11
|
+
* (local, staging, production, banksouth) + preview targeting.
|
|
12
|
+
* - `partner` — any other authenticated identity: local + their
|
|
13
|
+
* deployment(s).
|
|
14
|
+
*
|
|
15
|
+
* This is a visibility/UX layer, not access control — every deployment
|
|
16
|
+
* still enforces Auth0 + FGA per request. A user who knows a URL can
|
|
17
|
+
* always add it to config.toml; the server rejecting their token is the
|
|
18
|
+
* real gate.
|
|
19
|
+
*
|
|
20
|
+
* One bootstrap hostname is baked into the package for the discovery
|
|
21
|
+
* call itself. All deployments share the same Auth0 audience, so any
|
|
22
|
+
* identity's token validates there; the endpoint 401s anonymously.
|
|
23
|
+
*/
|
|
24
|
+
export type EnvTier = 'anonymous' | 'sequence' | 'partner';
|
|
25
|
+
export interface CatalogEnvironment {
|
|
26
|
+
name: string;
|
|
27
|
+
url: string;
|
|
28
|
+
}
|
|
29
|
+
export interface EnvCatalog {
|
|
30
|
+
tier: Exclude<EnvTier, 'anonymous'>;
|
|
31
|
+
environments: CatalogEnvironment[];
|
|
32
|
+
/** Unix ms timestamp of the successful fetch. */
|
|
33
|
+
fetchedAt: number;
|
|
34
|
+
/**
|
|
35
|
+
* Auth0 `sub` of the identity that fetched this catalog. Reads ignore the
|
|
36
|
+
* cache when the current identity differs (logout, seqapi re-login as
|
|
37
|
+
* someone else), so one user's tier never carries over to another.
|
|
38
|
+
*/
|
|
39
|
+
subject: string;
|
|
40
|
+
}
|
|
41
|
+
/** The host the discovery call goes to. Override for tests / self-hosting. */
|
|
42
|
+
export declare function bootstrapUrl(): string;
|
|
43
|
+
export declare function catalogPath(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Read the cached catalog from disk. Returns null when missing, malformed,
|
|
46
|
+
* or fetched by a *different* identity than the one the CLI would
|
|
47
|
+
* authenticate as right now — callers treat all of those as tier
|
|
48
|
+
* `anonymous`. The subject check means a logout (token file removed) or a
|
|
49
|
+
* re-login as someone else invalidates the cache immediately, without
|
|
50
|
+
* waiting for the next discovery call.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readCachedCatalog(): Promise<EnvCatalog | null>;
|
|
53
|
+
export declare function clearCatalog(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Fetch the environment catalog from the discovery endpoint and cache it.
|
|
56
|
+
*
|
|
57
|
+
* Returns the fresh catalog, or null when the caller is effectively
|
|
58
|
+
* anonymous: no token available, or the endpoint rejected the token
|
|
59
|
+
* (401/403 — the stale cache is cleared so visibility downgrades too).
|
|
60
|
+
* Network/server errors leave any existing cache in place and rethrow so
|
|
61
|
+
* callers can distinguish "offline" from "not authorized".
|
|
62
|
+
*/
|
|
63
|
+
export declare function fetchCatalog(): Promise<EnvCatalog | null>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { currentIdentitySubject, decodeJwtSub, tryGetAccessToken } from './auth.js';
|
|
6
|
+
const DEFAULT_BOOTSTRAP_URL = 'https://atlas.seqholdings.com';
|
|
7
|
+
/** The host the discovery call goes to. Override for tests / self-hosting. */
|
|
8
|
+
export function bootstrapUrl() {
|
|
9
|
+
return (process.env['SEQ_STUDIO_DISCOVERY_URL']?.trim().replace(/\/+$/, '') ||
|
|
10
|
+
DEFAULT_BOOTSTRAP_URL);
|
|
11
|
+
}
|
|
12
|
+
export function catalogPath() {
|
|
13
|
+
return join(homedir(), '.config', 'lattice', 'environments.json');
|
|
14
|
+
}
|
|
15
|
+
function isValidCatalog(value) {
|
|
16
|
+
if (typeof value !== 'object' || value === null)
|
|
17
|
+
return false;
|
|
18
|
+
const candidate = value;
|
|
19
|
+
return ((candidate.tier === 'sequence' || candidate.tier === 'partner') &&
|
|
20
|
+
Array.isArray(candidate.environments) &&
|
|
21
|
+
candidate.environments.every((env) => typeof env === 'object' &&
|
|
22
|
+
env !== null &&
|
|
23
|
+
typeof env.name === 'string' &&
|
|
24
|
+
typeof env.url === 'string') &&
|
|
25
|
+
typeof candidate.fetchedAt === 'number' &&
|
|
26
|
+
typeof candidate.subject === 'string' &&
|
|
27
|
+
candidate.subject.length > 0);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read the cached catalog from disk. Returns null when missing, malformed,
|
|
31
|
+
* or fetched by a *different* identity than the one the CLI would
|
|
32
|
+
* authenticate as right now — callers treat all of those as tier
|
|
33
|
+
* `anonymous`. The subject check means a logout (token file removed) or a
|
|
34
|
+
* re-login as someone else invalidates the cache immediately, without
|
|
35
|
+
* waiting for the next discovery call.
|
|
36
|
+
*/
|
|
37
|
+
export async function readCachedCatalog() {
|
|
38
|
+
const path = catalogPath();
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return null;
|
|
41
|
+
let catalog;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
44
|
+
catalog = isValidCatalog(parsed) ? parsed : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!catalog)
|
|
50
|
+
return null;
|
|
51
|
+
const subject = await currentIdentitySubject();
|
|
52
|
+
if (subject === null || subject !== catalog.subject)
|
|
53
|
+
return null;
|
|
54
|
+
return catalog;
|
|
55
|
+
}
|
|
56
|
+
async function saveCatalog(catalog) {
|
|
57
|
+
const path = catalogPath();
|
|
58
|
+
await mkdir(dirname(path), { recursive: true });
|
|
59
|
+
await writeFile(path, JSON.stringify(catalog, null, 2) + '\n', {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
mode: 0o600,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function clearCatalog() {
|
|
65
|
+
await rm(catalogPath(), { force: true });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetch the environment catalog from the discovery endpoint and cache it.
|
|
69
|
+
*
|
|
70
|
+
* Returns the fresh catalog, or null when the caller is effectively
|
|
71
|
+
* anonymous: no token available, or the endpoint rejected the token
|
|
72
|
+
* (401/403 — the stale cache is cleared so visibility downgrades too).
|
|
73
|
+
* Network/server errors leave any existing cache in place and rethrow so
|
|
74
|
+
* callers can distinguish "offline" from "not authorized".
|
|
75
|
+
*/
|
|
76
|
+
export async function fetchCatalog() {
|
|
77
|
+
// Fail closed when an M2M secret is configured but minting fails, so
|
|
78
|
+
// headless callers (CI) see the real Auth0 error instead of a silent
|
|
79
|
+
// downgrade to the anonymous tier.
|
|
80
|
+
const token = await tryGetAccessToken({ failClosedForM2m: true });
|
|
81
|
+
if (!token) {
|
|
82
|
+
await clearCatalog();
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const response = await fetch(`${bootstrapUrl()}/api/platform/environments`, {
|
|
86
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
87
|
+
});
|
|
88
|
+
if (response.status === 401 || response.status === 403) {
|
|
89
|
+
await clearCatalog();
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Environment discovery failed (${response.status}) against ${bootstrapUrl()}.`);
|
|
94
|
+
}
|
|
95
|
+
const body = (await response.json());
|
|
96
|
+
if ((body.tier !== 'sequence' && body.tier !== 'partner') ||
|
|
97
|
+
!Array.isArray(body.environments)) {
|
|
98
|
+
throw new Error('Environment discovery returned an unexpected response shape.');
|
|
99
|
+
}
|
|
100
|
+
const catalog = {
|
|
101
|
+
tier: body.tier,
|
|
102
|
+
environments: body.environments,
|
|
103
|
+
fetchedAt: Date.now(),
|
|
104
|
+
// Bind the cache to the identity that fetched it. `unknown` never
|
|
105
|
+
// matches a real subject, so an undecodable token yields a catalog
|
|
106
|
+
// that works for this invocation but is not trusted from disk later.
|
|
107
|
+
subject: decodeJwtSub(token) ?? 'unknown',
|
|
108
|
+
};
|
|
109
|
+
await saveCatalog(catalog);
|
|
110
|
+
return catalog;
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runEnvsCommand(sub: string | undefined, _rest: string[]): Promise<number>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readConfig, configPath } from '../config.js';
|
|
2
|
+
import { bootstrapUrl, fetchCatalog, readCachedCatalog, } from '../env-catalog.js';
|
|
3
|
+
const ENVS_USAGE = `usage:
|
|
4
|
+
seq-studio envs list show the environments visible to your identity
|
|
5
|
+
seq-studio envs refresh re-fetch the environment catalog for your identity
|
|
6
|
+
|
|
7
|
+
The published CLI ships with only the "local" environment. After you
|
|
8
|
+
authenticate (seq-studio login, or AUTH0_M2M_CLIENT_SECRET for headless use),
|
|
9
|
+
"envs refresh" discovers the environments your identity may target; the
|
|
10
|
+
catalog is cached at ~/.config/lattice/environments.json and also refreshes
|
|
11
|
+
lazily the first time you pass an -e <env> that isn't cached yet.
|
|
12
|
+
|
|
13
|
+
Custom entries in ${configPath()} are always honored on top.
|
|
14
|
+
`;
|
|
15
|
+
export async function runEnvsCommand(sub, _rest) {
|
|
16
|
+
switch (sub) {
|
|
17
|
+
case 'list':
|
|
18
|
+
return listCommand();
|
|
19
|
+
case 'refresh':
|
|
20
|
+
return refreshCommand();
|
|
21
|
+
case undefined:
|
|
22
|
+
case 'help':
|
|
23
|
+
case '--help':
|
|
24
|
+
case '-h':
|
|
25
|
+
console.log(ENVS_USAGE);
|
|
26
|
+
return sub ? 0 : 1;
|
|
27
|
+
default:
|
|
28
|
+
console.error(`unknown envs command: ${sub}`);
|
|
29
|
+
console.error(ENVS_USAGE);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function listCommand() {
|
|
34
|
+
const [config, catalog] = await Promise.all([readConfig(), readCachedCatalog()]);
|
|
35
|
+
const discovered = new Set(catalog?.environments.map((env) => env.name) ?? []);
|
|
36
|
+
console.log(`tier: ${config.tier}`);
|
|
37
|
+
if (catalog) {
|
|
38
|
+
console.log(`catalog fetched: ${new Date(catalog.fetchedAt).toISOString()}`);
|
|
39
|
+
}
|
|
40
|
+
console.log('');
|
|
41
|
+
const width = Math.max(...Object.keys(config.envs).map((name) => name.length));
|
|
42
|
+
for (const [name, { url }] of Object.entries(config.envs)) {
|
|
43
|
+
const source = name === 'local' && !discovered.has(name)
|
|
44
|
+
? 'built-in'
|
|
45
|
+
: discovered.has(name)
|
|
46
|
+
? 'discovered'
|
|
47
|
+
: 'config.toml';
|
|
48
|
+
console.log(` ${name.padEnd(width)} ${url} (${source})`);
|
|
49
|
+
}
|
|
50
|
+
if (config.tier === 'anonymous') {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log('Not authenticated — only "local" is visible. Authenticate with ' +
|
|
53
|
+
'`seq-studio login` (or set AUTH0_M2M_CLIENT_SECRET), then run: seq-studio envs refresh');
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
async function refreshCommand() {
|
|
58
|
+
let catalog;
|
|
59
|
+
try {
|
|
60
|
+
catalog = await fetchCatalog();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`envs refresh: FAIL — ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
if (!catalog) {
|
|
67
|
+
console.log(`Not authenticated against ${bootstrapUrl()} — catalog cleared; only ` +
|
|
68
|
+
'"local" (plus any config.toml entries) is visible.\n' +
|
|
69
|
+
'Authenticate with `seq-studio login` (or set AUTH0_M2M_CLIENT_SECRET) and retry.');
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
console.log(`tier: ${catalog.tier} — ${catalog.environments.length} environment(s) cached.`);
|
|
73
|
+
return listCommand();
|
|
74
|
+
}
|
|
@@ -82,5 +82,5 @@ export declare function functionsRollbackCommand(args: ParsedArgs): Promise<numb
|
|
|
82
82
|
/** Minimal dotenv parser — KEY=VALUE lines, quotes stripped, comments skipped. */
|
|
83
83
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
84
84
|
export declare function functionsDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
85
|
-
export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <
|
|
85
|
+
export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy [-e env] [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list [-e env] [--match-local] functions visible on the environment\n seq-studio functions show [-e env] [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs [-e env] [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> [-e env] make a version live\n seq-studio functions rollback [<version>] [-e env] redeploy a prior version\n seq-studio functions delete [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <env|preview:<slug>> (see: seq-studio envs list) \u00B7 --fn <slug> \u00B7 --dir <path>\n --from-env-file <path> (default: .env) source file for secret values\n --no-wait \u00B7 --yes\n --no-provision (deploy) update-only: error instead of registering a new\n shell, writing secret values, or attaching secrets (CI sweep)\n\n Source for build/deploy: a local --dir (default .), a platform git-service\n repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a\n branch/tag/commit (default: the repo's default branch). Remote sources record\n the pinned commit as provenance (never dirty) and NEVER read a repo-committed\n .env for secret values \u2014 provision secrets server-side or pass a local\n --from-env-file (resolved against your cwd).\n\n --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT\n (repo:read scope \u2014 `seq-studio auth pat create --scopes repo:read`, or\n Atlas \u2192 Settings \u2192 Tokens). It's the same token you clone the repo with;\n --env + seq-studio login are still needed to resolve the repo and deploy.\n";
|
|
86
86
|
export declare function runFunctionsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline';
|
|
|
7
7
|
import { Writable } from 'node:stream';
|
|
8
8
|
import { load as parseYaml } from 'js-yaml';
|
|
9
9
|
import { getAccessToken } from '../auth.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveEnvWithDiscovery } from '../config.js';
|
|
11
11
|
import { AtlasApiError, deleteJson, getJson, postJson } from '../atlas-client.js';
|
|
12
12
|
import { clarifyApplyFailureReason, printCliError } from '../cli-errors.js';
|
|
13
13
|
import { confirmYes } from '../prompt.js';
|
|
@@ -24,10 +24,9 @@ export function flagBool(flags, ...keys) {
|
|
|
24
24
|
return keys.some((key) => flags[key] === true || flags[key] === 'true');
|
|
25
25
|
}
|
|
26
26
|
export async function buildContext(args) {
|
|
27
|
-
const config = await readConfig();
|
|
28
27
|
const requested = (typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
|
|
29
28
|
(typeof args.flags.e === 'string' ? args.flags.e : undefined);
|
|
30
|
-
const env =
|
|
29
|
+
const env = await resolveEnvWithDiscovery({ requested });
|
|
31
30
|
const token = await getAccessToken();
|
|
32
31
|
return { env, token };
|
|
33
32
|
}
|
|
@@ -309,7 +308,7 @@ export async function functionsInitCommand(args) {
|
|
|
309
308
|
console.log(' # public npm instead (the deploy worker re-resolves server-side).');
|
|
310
309
|
console.log(' seq-studio functions build # local pre-flight checks');
|
|
311
310
|
console.log(' # Add secret names to managed-function.yml (secrets: [MY_SECRET]) and values to .env');
|
|
312
|
-
console.log(' seq-studio functions deploy -e
|
|
311
|
+
console.log(' seq-studio functions deploy -e <env> # upload, apply secrets, then deploy');
|
|
313
312
|
return 0;
|
|
314
313
|
}
|
|
315
314
|
// ---------------------------------------------------------------------------
|
|
@@ -942,7 +941,7 @@ export const FUNCTIONS_USAGE = `usage:
|
|
|
942
941
|
seq-studio functions delete [--yes] archive function + tear down GCP resources
|
|
943
942
|
(version history is retained)
|
|
944
943
|
|
|
945
|
-
Flags: -e/--env <
|
|
944
|
+
Flags: -e/--env <env|preview:<slug>> (see: seq-studio envs list) · --fn <slug> · --dir <path>
|
|
946
945
|
--from-env-file <path> (default: .env) source file for secret values
|
|
947
946
|
--no-wait · --yes
|
|
948
947
|
--no-provision (deploy) update-only: error instead of registering a new
|
package/dist/login.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN, saveTokens, seqapiTokenPath, } from './auth.js';
|
|
8
|
+
import { clearCatalog, fetchCatalog } from './env-catalog.js';
|
|
8
9
|
const DEFAULT_REDIRECT_PORT = 5099;
|
|
9
10
|
const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
|
|
10
11
|
function base64Url(input) {
|
|
@@ -195,6 +196,22 @@ export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBro
|
|
|
195
196
|
export async function login() {
|
|
196
197
|
await loginWithPkce({});
|
|
197
198
|
console.log(`Authenticated. Tokens saved to ${seqapiTokenPath()}.`);
|
|
199
|
+
// Refresh the environment catalog for the new identity immediately, so a
|
|
200
|
+
// previously cached catalog from another identity (or tier) never lingers
|
|
201
|
+
// past a login. Drop the old cache first — if the refresh below fails, the
|
|
202
|
+
// prior identity's catalog must not survive the login either.
|
|
203
|
+
// Best-effort: discovery being unreachable must not fail login.
|
|
204
|
+
await clearCatalog();
|
|
205
|
+
try {
|
|
206
|
+
const catalog = await fetchCatalog();
|
|
207
|
+
if (catalog) {
|
|
208
|
+
console.log(`Environment catalog refreshed (tier: ${catalog.tier}, ` +
|
|
209
|
+
`${catalog.environments.length} environment(s)).`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
console.log('Could not refresh the environment catalog — run: seq-studio envs refresh');
|
|
214
|
+
}
|
|
198
215
|
}
|
|
199
216
|
/**
|
|
200
217
|
* Pre-unification artifact-studio token file. `seq-studio artifact` commands
|
|
@@ -209,5 +226,8 @@ function legacyArtifactTokenPath() {
|
|
|
209
226
|
export async function logout() {
|
|
210
227
|
await rm(seqapiTokenPath(), { force: true });
|
|
211
228
|
await rm(legacyArtifactTokenPath(), { force: true });
|
|
229
|
+
// Drop the cached environment catalog so visibility downgrades with the
|
|
230
|
+
// identity — a logged-out terminal must not keep the old tier's env list.
|
|
231
|
+
await clearCatalog();
|
|
212
232
|
console.log('Logged out of seq-studio and seqapi.');
|
|
213
233
|
}
|
package/dist/main.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* seq-studio secrets <sub> manage org-owned Managed Secrets
|
|
9
9
|
* seq-studio repos <sub> manage platform git-service repos
|
|
10
10
|
* seq-studio auth <sub> manage git-service PATs
|
|
11
|
+
* seq-studio envs <sub> list/refresh discovered environments
|
|
11
12
|
* seq-studio login authenticate interactively with Auth0
|
|
12
13
|
* seq-studio logout remove cached user tokens
|
|
13
14
|
* seq-studio doctor check token + env + writer gate
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* seq-studio secrets <sub> manage org-owned Managed Secrets
|
|
9
9
|
* seq-studio repos <sub> manage platform git-service repos
|
|
10
10
|
* seq-studio auth <sub> manage git-service PATs
|
|
11
|
+
* seq-studio envs <sub> list/refresh discovered environments
|
|
11
12
|
* seq-studio login authenticate interactively with Auth0
|
|
12
13
|
* seq-studio logout remove cached user tokens
|
|
13
14
|
* seq-studio doctor check token + env + writer gate
|
|
@@ -25,13 +26,16 @@ const TOP_LEVEL_USAGE = `usage:
|
|
|
25
26
|
seq-studio repos <sub> [args] list | namespaces | show | create | clone | pull | delete
|
|
26
27
|
seq-studio auth <sub> [args] pat create | pat list | pat revoke
|
|
27
28
|
seq-studio orm <sub> [args] init | validate | plan | apply
|
|
29
|
+
seq-studio envs <sub> list | refresh — environments visible to your identity
|
|
28
30
|
seq-studio login authenticate in the browser
|
|
29
31
|
seq-studio logout remove cached user tokens
|
|
30
32
|
seq-studio doctor [-e <env>] diagnose config, auth, and writer gate
|
|
33
|
+
seq-studio version print the installed version
|
|
31
34
|
seq-studio help show this message
|
|
32
35
|
|
|
33
36
|
Authenticate with: seq-studio login
|
|
34
|
-
|
|
37
|
+
Built-in env: local. Authenticated identities discover more via
|
|
38
|
+
\`seq-studio envs refresh\`; custom entries live in ~/.config/lattice/config.toml.
|
|
35
39
|
`;
|
|
36
40
|
const PROCESS_USAGE = `usage:
|
|
37
41
|
seq-studio process init <dir>
|
|
@@ -82,9 +86,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
82
86
|
const { runOrmCommand } = await import('./orm/delegate.js');
|
|
83
87
|
return runOrmCommand(sub, rest);
|
|
84
88
|
}
|
|
89
|
+
case 'envs': {
|
|
90
|
+
const { runEnvsCommand } = await import('./envs/commands.js');
|
|
91
|
+
return runEnvsCommand(sub, rest);
|
|
92
|
+
}
|
|
85
93
|
case 'login':
|
|
86
94
|
case 'logout':
|
|
87
95
|
return runSessionCommand({ argument: sub, command: namespace });
|
|
96
|
+
case 'version':
|
|
97
|
+
case '--version':
|
|
98
|
+
case '-v': {
|
|
99
|
+
const { currentVersion } = await import('./update-check.js');
|
|
100
|
+
console.log(currentVersion());
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
88
103
|
case 'doctor':
|
|
89
104
|
return doctorCommand(parseArgs([sub, ...rest].filter(Boolean)));
|
|
90
105
|
default:
|
package/dist/orm/delegate.js
CHANGED
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
* SEQUENCE_ORM_BASE_URL, share the seqapi token, forward argv.
|
|
7
7
|
*/
|
|
8
8
|
import { getAccessToken, tryGetAccessToken } from '../auth.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveEnvWithDiscovery } from '../config.js';
|
|
10
10
|
import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
|
|
11
11
|
const ORM_USAGE = `usage:
|
|
12
12
|
seq-studio orm init <dir> scaffold a namespace directory
|
|
13
|
-
seq-studio orm validate [dir] parse + validate definitions
|
|
13
|
+
seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
|
|
14
14
|
seq-studio orm plan [dir] -e <env> compile definitions and diff against the registry
|
|
15
|
-
seq-studio orm
|
|
16
|
-
seq-studio orm
|
|
15
|
+
seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh types.gen.ts (the everyday command)
|
|
16
|
+
seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Environments: see \`seq-studio envs list\` (built-in: local; more are
|
|
19
|
+
discovered after you authenticate).
|
|
19
20
|
Authenticate with: seq-studio login
|
|
20
21
|
`;
|
|
21
22
|
export async function runOrmCommand(sub, rest) {
|
|
@@ -26,8 +27,7 @@ export async function runOrmCommand(sub, rest) {
|
|
|
26
27
|
const normalized = normalizeShortEnvFlag(rest);
|
|
27
28
|
const requested = readEnvFromArgv(normalized);
|
|
28
29
|
if (requested) {
|
|
29
|
-
const
|
|
30
|
-
const resolved = resolveEnv({ config, requested });
|
|
30
|
+
const resolved = await resolveEnvWithDiscovery({ requested });
|
|
31
31
|
process.env['SEQUENCE_ORM_BASE_URL'] = resolved.url;
|
|
32
32
|
}
|
|
33
33
|
const token = await tryGetAccessToken({ failClosedForM2m: true });
|
package/dist/process/commands.js
CHANGED
|
@@ -12,7 +12,7 @@ import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
|
|
|
12
12
|
import { getJson, getJsonOr404, postJson, AtlasApiError } from '../atlas-client.js';
|
|
13
13
|
import { generateProcessFiles } from './codegen.js';
|
|
14
14
|
import { getAccessToken, NotLoggedInError } from '../auth.js';
|
|
15
|
-
import {
|
|
15
|
+
import { readConfig, resolveEnvWithDiscovery } from '../config.js';
|
|
16
16
|
import { buildBundleFromProcesses, summarizeBundle } from './build.js';
|
|
17
17
|
import { buildResolveProcessPinFromEnv } from './resolve-process-pin.js';
|
|
18
18
|
import { loadBundleForPublish } from './local-bundle.js';
|
|
@@ -92,9 +92,8 @@ const SHORT_FLAG_ALIASES = {
|
|
|
92
92
|
'-o': 'out',
|
|
93
93
|
};
|
|
94
94
|
async function getEnvAndToken(args) {
|
|
95
|
-
const config = await readConfig();
|
|
96
95
|
const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
97
|
-
const env =
|
|
96
|
+
const env = await resolveEnvWithDiscovery({ requested });
|
|
98
97
|
let token;
|
|
99
98
|
try {
|
|
100
99
|
token = await getAccessToken();
|
|
@@ -667,16 +666,16 @@ export async function doctorCommand(args) {
|
|
|
667
666
|
}
|
|
668
667
|
else {
|
|
669
668
|
lines.push(`config: ok — default_env=${config.defaultEnv}, envs=${Object.keys(config.envs).join(',')}`);
|
|
669
|
+
lines.push(`tier: ${config.tier}` +
|
|
670
|
+
(config.tier === 'anonymous'
|
|
671
|
+
? ' — authenticate and run `seq-studio envs refresh` to discover more environments'
|
|
672
|
+
: ''));
|
|
670
673
|
}
|
|
671
674
|
const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
672
|
-
if (requested && !ENV_NAMES.includes(requested) && config && !(requested in config.envs)) {
|
|
673
|
-
lines.push(`env "${requested}": FAIL — not in built-ins (${ENV_NAMES.join(',')}) or config`);
|
|
674
|
-
ok = false;
|
|
675
|
-
}
|
|
676
675
|
let env = null;
|
|
677
676
|
if (config) {
|
|
678
677
|
try {
|
|
679
|
-
env =
|
|
678
|
+
env = await resolveEnvWithDiscovery({ requested });
|
|
680
679
|
lines.push(`env: ok — ${env.name} (${env.url})`);
|
|
681
680
|
}
|
|
682
681
|
catch (err) {
|
package/dist/repos/commands.d.ts
CHANGED
|
@@ -45,5 +45,5 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
|
|
47
47
|
export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
48
|
-
export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <
|
|
48
|
+
export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
|
|
49
49
|
export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
package/dist/repos/commands.js
CHANGED
|
@@ -24,7 +24,7 @@ import { buildContext, clientOptions, flagBool, LOG, } from '../functions/comman
|
|
|
24
24
|
import { resolveGitPatFromEnv, runGitClone, } from './git-clone.js';
|
|
25
25
|
import { formatPatSetupHint } from '../pat-hints.js';
|
|
26
26
|
import { tryGetAccessToken } from '../auth.js';
|
|
27
|
-
import {
|
|
27
|
+
import { resolveEnvWithDiscovery } from '../config.js';
|
|
28
28
|
const PAGE_SIZE = 200;
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Helpers
|
|
@@ -60,7 +60,7 @@ export function stringFlag(flags, key) {
|
|
|
60
60
|
*/
|
|
61
61
|
async function reposContext(args) {
|
|
62
62
|
if (args.flags.env === true || args.flags.e === true) {
|
|
63
|
-
throw new Error('-e/--env requires a value
|
|
63
|
+
throw new Error('-e/--env requires a value — see: seq-studio envs list.');
|
|
64
64
|
}
|
|
65
65
|
const ctx = await buildContext(args);
|
|
66
66
|
const secret = process.env.PREVIEW_ACCESS_HEADER?.trim();
|
|
@@ -414,12 +414,11 @@ export async function reposCloneCommand(args, deps = {}) {
|
|
|
414
414
|
/** Resolve -e/--env without requiring Auth0 (for PAT-only --url/--id clones). */
|
|
415
415
|
async function resolveEnvOnly(args) {
|
|
416
416
|
if (args.flags.env === true || args.flags.e === true) {
|
|
417
|
-
throw new Error('-e/--env requires a value
|
|
417
|
+
throw new Error('-e/--env requires a value — see: seq-studio envs list.');
|
|
418
418
|
}
|
|
419
|
-
const config = await readConfig();
|
|
420
419
|
const requested = (typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
|
|
421
420
|
(typeof args.flags.e === 'string' ? args.flags.e : undefined);
|
|
422
|
-
return
|
|
421
|
+
return resolveEnvWithDiscovery({ requested });
|
|
423
422
|
}
|
|
424
423
|
/**
|
|
425
424
|
* Validate clone URL shape and that the origin matches the resolved Atlas env
|
|
@@ -502,7 +501,7 @@ export const REPOS_USAGE = `usage:
|
|
|
502
501
|
materialize the tree at a ref (JSON API)
|
|
503
502
|
seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)
|
|
504
503
|
|
|
505
|
-
Flags: -e/--env <
|
|
504
|
+
Flags: -e/--env <env> (see: seq-studio envs list)
|
|
506
505
|
|
|
507
506
|
clone prefers real git clone (PAT via askpass — never written into the remote
|
|
508
507
|
URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints
|
|
@@ -20,5 +20,5 @@ export declare function secretsSetDefaultCommand(args: ParsedArgs): Promise<numb
|
|
|
20
20
|
* UI: pin + redeploy the function's active version / pin only / cancel.
|
|
21
21
|
*/
|
|
22
22
|
export declare function secretsPinCommand(args: ParsedArgs): Promise<number>;
|
|
23
|
-
export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> set the shared default value (write-only)\n seq-studio secrets list [-e env] secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> remove attachment\n seq-studio secrets apply --from-env-file .env [-e env] push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> value version history (never values)\n seq-studio secrets set-default <NAME> [version] point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <
|
|
23
|
+
export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> set the shared default value (write-only)\n seq-studio secrets list [-e env] secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> remove attachment\n seq-studio secrets apply --from-env-file .env [-e env] push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> value version history (never values)\n seq-studio secrets set-default <NAME> [version] point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
|
|
24
24
|
export declare function runSecretsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
package/dist/secrets/commands.js
CHANGED
|
@@ -658,7 +658,7 @@ export const SECRETS_USAGE = `usage:
|
|
|
658
658
|
Use \`secrets\` commands for CI (no .env), bulk/multi-function ops, or write-only
|
|
659
659
|
value changes without a redeploy.
|
|
660
660
|
|
|
661
|
-
Flags: -e/--env <
|
|
661
|
+
Flags: -e/--env <env> (see: seq-studio envs list)
|
|
662
662
|
--fn <slug> · --env-var <NAME> · --yes
|
|
663
663
|
--functions <f1,f2> · --keys <K1,K2> · --all (override key selection)
|
|
664
664
|
set-default: [version|version-row-id] (defaults to the most recent non-default) · --yes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached background entry spawned by maybeNotifyUpdate() when the update
|
|
3
|
+
* cache is stale. Polls the npm registry once and persists the result so
|
|
4
|
+
* the *next* command can print the notice without any foreground network.
|
|
5
|
+
*/
|
|
6
|
+
import { refreshUpdateCache } from './update-check.js';
|
|
7
|
+
refreshUpdateCache().catch(() => {
|
|
8
|
+
// Best-effort: nothing to report to — the parent process is long gone.
|
|
9
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The version of this installed package (dist/../package.json). */
|
|
2
|
+
export declare function currentVersion(): string;
|
|
3
|
+
export declare function updateCheckCachePath(): string;
|
|
4
|
+
/** Exclusive-create marker that makes "who spawns the refresh?" atomic. */
|
|
5
|
+
export declare function updateCheckClaimPath(): string;
|
|
6
|
+
/**
|
|
7
|
+
* True when `candidate` is a strictly newer semver than `current`.
|
|
8
|
+
* Prerelease suffixes are ignored — the published channel is plain
|
|
9
|
+
* major.minor.patch and this only drives a notice, not resolution.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isNewerVersion(candidate: string, current: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Poll the registry and persist the result. Runs in the detached background
|
|
14
|
+
* process (see update-check-refresh.ts) — never on a command's exit path.
|
|
15
|
+
* Failed fetches still record `lastCheckedAt` (keeping any previously known
|
|
16
|
+
* version) so attempts are throttled to once per interval either way. The
|
|
17
|
+
* cache file is written *only* here, so a foreground path can never clobber
|
|
18
|
+
* a fresher result. Releases the spawn claim when done.
|
|
19
|
+
*/
|
|
20
|
+
export declare function refreshUpdateCache(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Print a strong upgrade recommendation to stderr when the cached registry
|
|
23
|
+
* state says a newer version is published, and kick off a detached refresh
|
|
24
|
+
* when the cache is stale. Reads only local state — adds no network latency
|
|
25
|
+
* to the command. Never throws.
|
|
26
|
+
*/
|
|
27
|
+
export declare function maybeNotifyUpdate(): Promise<void>;
|