@sequenceholdings/studio-cli 0.1.9

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.
Files changed (74) hide show
  1. package/README.md +258 -0
  2. package/dist/artifact/delegate.d.ts +25 -0
  3. package/dist/artifact/delegate.js +263 -0
  4. package/dist/atlas-client.d.ts +44 -0
  5. package/dist/atlas-client.js +173 -0
  6. package/dist/auth-cmds/commands.d.ts +15 -0
  7. package/dist/auth-cmds/commands.js +249 -0
  8. package/dist/auth.d.ts +26 -0
  9. package/dist/auth.js +171 -0
  10. package/dist/bin.d.ts +2 -0
  11. package/dist/bin.js +8 -0
  12. package/dist/cli-errors.d.ts +5 -0
  13. package/dist/cli-errors.js +78 -0
  14. package/dist/config.d.ts +44 -0
  15. package/dist/config.js +103 -0
  16. package/dist/env-flags.d.ts +8 -0
  17. package/dist/env-flags.js +47 -0
  18. package/dist/functions/bundle.d.ts +30 -0
  19. package/dist/functions/bundle.js +137 -0
  20. package/dist/functions/commands.d.ts +86 -0
  21. package/dist/functions/commands.js +999 -0
  22. package/dist/functions/egress-preview.d.ts +32 -0
  23. package/dist/functions/egress-preview.js +54 -0
  24. package/dist/functions/lockfile-origin.d.ts +16 -0
  25. package/dist/functions/lockfile-origin.js +45 -0
  26. package/dist/functions/manifest.d.ts +89 -0
  27. package/dist/functions/manifest.js +586 -0
  28. package/dist/functions/secret-reconcile.d.ts +79 -0
  29. package/dist/functions/secret-reconcile.js +86 -0
  30. package/dist/main.d.ts +14 -0
  31. package/dist/main.js +129 -0
  32. package/dist/orm/delegate.d.ts +8 -0
  33. package/dist/orm/delegate.js +61 -0
  34. package/dist/pat-hints.d.ts +17 -0
  35. package/dist/pat-hints.js +28 -0
  36. package/dist/preview.d.ts +89 -0
  37. package/dist/preview.js +291 -0
  38. package/dist/process/agent-loader.d.ts +24 -0
  39. package/dist/process/agent-loader.js +57 -0
  40. package/dist/process/build.d.ts +14 -0
  41. package/dist/process/build.js +368 -0
  42. package/dist/process/codegen.d.ts +18 -0
  43. package/dist/process/codegen.js +270 -0
  44. package/dist/process/commands.d.ts +47 -0
  45. package/dist/process/commands.js +786 -0
  46. package/dist/process/discover.d.ts +32 -0
  47. package/dist/process/discover.js +131 -0
  48. package/dist/process/lint.d.ts +39 -0
  49. package/dist/process/lint.js +485 -0
  50. package/dist/process/local-bundle.d.ts +17 -0
  51. package/dist/process/local-bundle.js +65 -0
  52. package/dist/process/plan-diff.d.ts +82 -0
  53. package/dist/process/plan-diff.js +333 -0
  54. package/dist/process/resolve-process-pin.d.ts +11 -0
  55. package/dist/process/resolve-process-pin.js +63 -0
  56. package/dist/process/simulate.d.ts +50 -0
  57. package/dist/process/simulate.js +328 -0
  58. package/dist/prompt.d.ts +35 -0
  59. package/dist/prompt.js +65 -0
  60. package/dist/repos/commands.d.ts +49 -0
  61. package/dist/repos/commands.js +548 -0
  62. package/dist/repos/git-clone.d.ts +10 -0
  63. package/dist/repos/git-clone.js +49 -0
  64. package/dist/secrets/commands.d.ts +24 -0
  65. package/dist/secrets/commands.js +704 -0
  66. package/dist/templates/process/example-process/process.ts +43 -0
  67. package/dist/templates/process/package.json +23 -0
  68. package/dist/templates/process/pnpm-workspace.yaml +21 -0
  69. package/dist/templates/process/tsconfig.json +17 -0
  70. package/package.json +78 -0
  71. package/templates/process/example-process/process.ts +43 -0
  72. package/templates/process/package.json +23 -0
  73. package/templates/process/pnpm-workspace.yaml +21 -0
  74. package/templates/process/tsconfig.json +17 -0
@@ -0,0 +1,78 @@
1
+ import { AtlasApiError } from './atlas-client.js';
2
+ export const LOG = '[seq-studio]';
3
+ /** Map common apply failure reasons to clearer copy (original reason is preserved). */
4
+ export function clarifyApplyFailureReason(reason) {
5
+ const hints = [
6
+ [
7
+ /^Function not found$/,
8
+ 'Function shell not registered — run secrets apply from a function directory or deploy first',
9
+ ],
10
+ [
11
+ /^acknowledgeSharedDefault required/,
12
+ 'Secret already has a shared default — re-run and confirm at the prompt',
13
+ ],
14
+ [
15
+ /^Admin access required on function$/,
16
+ 'You need admin access on the target function',
17
+ ],
18
+ [
19
+ /^Editor access required on secret$/,
20
+ 'You need editor access on the secret (or create permission for new secrets)',
21
+ ],
22
+ [
23
+ /secret_creator required/,
24
+ 'Ask your org admin for secret_creator on the managed scope, or use secrets create first',
25
+ ],
26
+ [
27
+ /function_creator required/,
28
+ 'Ask your org admin for function_creator on the managed scope before creating a function',
29
+ ],
30
+ ];
31
+ for (const [pattern, hint] of hints) {
32
+ if (pattern.test(reason))
33
+ return `${reason} — ${hint}`;
34
+ }
35
+ return reason;
36
+ }
37
+ function nextStepForAtlasError(error) {
38
+ const msg = error.message.toLowerCase();
39
+ if (error.status === 401) {
40
+ return 'Next step: run `seqapi login` and retry.';
41
+ }
42
+ if (error.status === 403) {
43
+ return 'Next step: confirm `-e` targets the right environment and you have the required access grant.';
44
+ }
45
+ if (error.status === 404 && msg.includes('function')) {
46
+ return 'Next step: run `seq-studio functions list -e <env>` and confirm `managed-function.yml` `function.id` matches a remote slug.';
47
+ }
48
+ if (error.status === 409 && msg.includes('version')) {
49
+ return 'Next step: rerun deploy — another upload may have taken the version slot.';
50
+ }
51
+ if (error.status === 413 || msg.includes('too large')) {
52
+ return 'Next step: run `seq-studio functions build` and reduce bundle size or dependencies.';
53
+ }
54
+ if (error.status === 503) {
55
+ return 'Next step: wait a few seconds and retry (backend may still be warming up).';
56
+ }
57
+ if (error.status === 524 || msg.includes('timeout') || msg.includes('timed out')) {
58
+ return 'Next step: retry; if it persists check VPN/network, then `seq-studio functions list` or logs to see whether the operation completed.';
59
+ }
60
+ return null;
61
+ }
62
+ export function formatCliError(error) {
63
+ if (error instanceof AtlasApiError) {
64
+ const lines = [`${LOG} ${error.status}${error.statusText ? ` ${error.statusText}` : ''}: ${error.message}`];
65
+ if (error.path)
66
+ lines.push(`${LOG} request: ${error.path}`);
67
+ const next = nextStepForAtlasError(error);
68
+ if (next)
69
+ lines.push(`${LOG} ${next}`);
70
+ return lines;
71
+ }
72
+ return [`${LOG} ${error instanceof Error ? error.message : String(error)}`];
73
+ }
74
+ export function printCliError(error) {
75
+ for (const line of formatCliError(error)) {
76
+ console.error(line);
77
+ }
78
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `--env preview:<branch-or-slug>` targets a per-PR Vercel preview deployment
3
+ * without a config.toml entry. The slug after the prefix is run through the
4
+ * canonical branch→slug transform (see `preview.ts`). PR-number resolution and
5
+ * the Cloudflare WAF bypass header live in `artifact/delegate.ts`.
6
+ */
7
+ export declare const PREVIEW_ENV_PREFIX = "preview:";
8
+ export declare const ENV_NAMES: string[];
9
+ export interface LatticeConfig {
10
+ envs: Record<string, {
11
+ url: string;
12
+ }>;
13
+ defaultEnv: string;
14
+ }
15
+ export declare function globalConfigDir(): string;
16
+ export declare function configPath(): string;
17
+ export declare function defaultConfig(): LatticeConfig;
18
+ /**
19
+ * Read the config from disk. If the file does not exist, return defaults
20
+ * — no auto-write, since seq-studio should work out of the box without
21
+ * any state on disk. The user can run `seq-studio config init` (future)
22
+ * if they want a customizable file.
23
+ *
24
+ * User-supplied envs are merged on top of the built-ins, so an override
25
+ * for `local` (say, pointing at a different port) takes effect, and net
26
+ * new envs (e.g. `staging-2`) are picked up.
27
+ */
28
+ export declare function readConfig(): Promise<LatticeConfig>;
29
+ /** Write the config to disk, creating the dir if missing. */
30
+ export declare function writeConfig(config: LatticeConfig): Promise<void>;
31
+ export interface ResolvedEnv {
32
+ name: string;
33
+ url: string;
34
+ }
35
+ /**
36
+ * Resolve `--env <name>` against the user's config. Falls back to the
37
+ * config's `defaultEnv` when no name is passed. Throws a clear error
38
+ * when the name does not match any known env so users see a list of
39
+ * what's available rather than a confusing 404 later.
40
+ */
41
+ export declare function resolveEnv({ config, requested, }: {
42
+ config: LatticeConfig;
43
+ requested?: string;
44
+ }): ResolvedEnv;
package/dist/config.js ADDED
@@ -0,0 +1,103 @@
1
+ import { mkdir, readFile, 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 { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
6
+ import { resolvePreviewFromSlug } from './preview.js';
7
+ /**
8
+ * `--env preview:<branch-or-slug>` targets a per-PR Vercel preview deployment
9
+ * without a config.toml entry. The slug after the prefix is run through the
10
+ * canonical branch→slug transform (see `preview.ts`). PR-number resolution and
11
+ * the Cloudflare WAF bypass header live in `artifact/delegate.ts`.
12
+ */
13
+ export const PREVIEW_ENV_PREFIX = 'preview:';
14
+ /**
15
+ * Environment URLs match the existing seqapi + artifact-studio mapping so
16
+ * an engineer's mental model is the same across all three tools. `local`
17
+ * is the dev / `make dev-all` URL — kept named `local` (not `dev`) so it
18
+ * lines up with `seqapi -e local` and `artifact-studio --env local`.
19
+ */
20
+ const BUILT_IN_ENV_URLS = {
21
+ local: 'http://localhost:5001',
22
+ staging: 'https://staging.atlas.seqholdings.com',
23
+ production: 'https://atlas.seqholdings.com',
24
+ banksouth: 'https://banksouth.seqholdings.com',
25
+ };
26
+ export const ENV_NAMES = Object.keys(BUILT_IN_ENV_URLS);
27
+ export function globalConfigDir() {
28
+ return join(homedir(), '.config', 'lattice');
29
+ }
30
+ export function configPath() {
31
+ return join(globalConfigDir(), 'config.toml');
32
+ }
33
+ export function defaultConfig() {
34
+ const envs = {};
35
+ for (const [name, url] of Object.entries(BUILT_IN_ENV_URLS)) {
36
+ envs[name] = { url };
37
+ }
38
+ return { envs, defaultEnv: 'local' };
39
+ }
40
+ /**
41
+ * Read the config from disk. If the file does not exist, return defaults
42
+ * — no auto-write, since seq-studio should work out of the box without
43
+ * any state on disk. The user can run `seq-studio config init` (future)
44
+ * if they want a customizable file.
45
+ *
46
+ * User-supplied envs are merged on top of the built-ins, so an override
47
+ * for `local` (say, pointing at a different port) takes effect, and net
48
+ * new envs (e.g. `staging-2`) are picked up.
49
+ */
50
+ export async function readConfig() {
51
+ const path = configPath();
52
+ if (!existsSync(path))
53
+ return defaultConfig();
54
+ const raw = parseToml(await readFile(path, 'utf8'));
55
+ const merged = defaultConfig();
56
+ if (raw.env) {
57
+ for (const [name, value] of Object.entries(raw.env)) {
58
+ if (typeof value?.url === 'string') {
59
+ merged.envs[name] = { url: value.url };
60
+ }
61
+ }
62
+ }
63
+ if (typeof raw.default_env === 'string') {
64
+ merged.defaultEnv = raw.default_env;
65
+ }
66
+ return merged;
67
+ }
68
+ /** Write the config to disk, creating the dir if missing. */
69
+ export async function writeConfig(config) {
70
+ const path = configPath();
71
+ await mkdir(dirname(path), { recursive: true });
72
+ // smol-toml's stringify can't accept arbitrary nested keys via the
73
+ // `env.<name>` syntax directly; we build it manually for stable
74
+ // ordering and clean output.
75
+ const lines = [];
76
+ for (const [name, value] of Object.entries(config.envs)) {
77
+ lines.push(`[env.${name}]`);
78
+ lines.push(`url = ${stringifyToml({ url: value.url }).trim().replace(/^url = /, '')}`);
79
+ lines.push('');
80
+ }
81
+ lines.push(`default_env = "${config.defaultEnv}"`);
82
+ await writeFile(path, lines.join('\n') + '\n', { encoding: 'utf8', mode: 0o600 });
83
+ }
84
+ /**
85
+ * Resolve `--env <name>` against the user's config. Falls back to the
86
+ * config's `defaultEnv` when no name is passed. Throws a clear error
87
+ * when the name does not match any known env so users see a list of
88
+ * what's available rather than a confusing 404 later.
89
+ */
90
+ export function resolveEnv({ config, requested, }) {
91
+ const name = requested ?? config.defaultEnv;
92
+ if (name.startsWith(PREVIEW_ENV_PREFIX)) {
93
+ const { slug, url } = resolvePreviewFromSlug(name.slice(PREVIEW_ENV_PREFIX.length));
94
+ return { name: `${PREVIEW_ENV_PREFIX}${slug}`, url };
95
+ }
96
+ const env = config.envs[name];
97
+ if (!env) {
98
+ const known = Object.keys(config.envs).join(', ');
99
+ throw new Error(`Unknown env "${name}". Known envs: ${known}. ` +
100
+ `Pass --env <name> or set default_env in ${configPath()}.`);
101
+ }
102
+ return { name, url: env.url };
103
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared argv env-flag handling for delegate namespaces. Delegated package
3
+ * CLIs only recognize the two-token `--env <value>` form, so every `-e value`
4
+ * / `-e=value` / `--env=value` spelling is rewritten before forwarding.
5
+ */
6
+ export declare function normalizeShortEnvFlag(argv: readonly string[]): string[];
7
+ /** Only valid AFTER normalizeShortEnvFlag — recognizes the two-token form. */
8
+ export declare function readEnvFromArgv(rest: readonly string[]): string | undefined;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared argv env-flag handling for delegate namespaces. Delegated package
3
+ * CLIs only recognize the two-token `--env <value>` form, so every `-e value`
4
+ * / `-e=value` / `--env=value` spelling is rewritten before forwarding.
5
+ */
6
+ export function normalizeShortEnvFlag(argv) {
7
+ const out = [];
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const arg = argv[i];
10
+ if (arg === undefined)
11
+ continue;
12
+ if (arg === '-e' || arg === '--env') {
13
+ const next = argv[i + 1];
14
+ if (next !== undefined && !next.startsWith('-')) {
15
+ out.push('--env', next);
16
+ i += 1;
17
+ }
18
+ else {
19
+ out.push('--env');
20
+ }
21
+ }
22
+ else if (arg.startsWith('-e=')) {
23
+ out.push('--env', arg.slice('-e='.length));
24
+ }
25
+ else if (arg.startsWith('--env=')) {
26
+ out.push('--env', arg.slice('--env='.length));
27
+ }
28
+ else {
29
+ out.push(arg);
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+ /** Only valid AFTER normalizeShortEnvFlag — recognizes the two-token form. */
35
+ export function readEnvFromArgv(rest) {
36
+ for (let i = 0; i < rest.length; i++) {
37
+ const arg = rest[i];
38
+ if (!arg)
39
+ continue;
40
+ if (arg === '--env') {
41
+ const next = rest[i + 1];
42
+ if (next && !next.startsWith('-'))
43
+ return next;
44
+ }
45
+ }
46
+ return undefined;
47
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Local bundle collection for `seq-studio functions deploy`. The CLI
3
+ * ALWAYS excludes dotenv files (the server rejects them as a backstop),
4
+ * plus node_modules (vendored server-side), VCS metadata, and .npmrc
5
+ * (the registry is pinned server-side). Pnpm hook files are kept in the
6
+ * collected file list so local validation can hard-fail before upload.
7
+ */
8
+ export interface LocalBundleFile {
9
+ path: string;
10
+ content: string;
11
+ encoding: 'utf8' | 'base64';
12
+ }
13
+ export interface CollectResult {
14
+ files: LocalBundleFile[];
15
+ excludedEnvFiles: string[];
16
+ }
17
+ export declare function collectBundleFiles(rootDir: string): Promise<CollectResult>;
18
+ export interface LocalValidationIssue {
19
+ level: 'error' | 'warning' | 'info';
20
+ message: string;
21
+ }
22
+ /**
23
+ * Pre-flight checks mirroring the server's bundle validation so engineers
24
+ * get fast local feedback instead of a 400 after uploading.
25
+ */
26
+ export declare function validateLocalBundle({ rootDir, files, }: {
27
+ rootDir: string;
28
+ files: LocalBundleFile[];
29
+ }): Promise<LocalValidationIssue[]>;
30
+ export declare function isDirectory(path: string): Promise<boolean>;
@@ -0,0 +1,137 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+ import { classifyLockfileOrigin } from './lockfile-origin.js';
5
+ const ENV_FILE_RE = /^\.env(\..*)?$/;
6
+ const PNPM_HOOK_FILE_RE = /^\.?pnpmfile\.cjs$/;
7
+ // Mirrors the server: a pnpm-workspace.yaml defines catalog/workspace
8
+ // indirection that can resolve deps outside Chainguard on the no-lockfile path.
9
+ const PNPM_WORKSPACE_FILE_RE = /^pnpm-workspace\.ya?ml$/;
10
+ const EXCLUDED_DIRS = new Set(['node_modules', '.git', '.idea', '.vscode', 'dist', 'build']);
11
+ const EXCLUDED_FILES = new Set(['.npmrc', '.DS_Store']);
12
+ function isProbablyBinary(buffer) {
13
+ const probe = buffer.subarray(0, 8192);
14
+ return probe.includes(0);
15
+ }
16
+ export async function collectBundleFiles(rootDir) {
17
+ const files = [];
18
+ const excludedEnvFiles = [];
19
+ const walk = async (dir) => {
20
+ const entries = await readdir(dir, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const full = join(dir, entry.name);
23
+ const rel = relative(rootDir, full).replace(/\\/g, '/');
24
+ if (entry.isDirectory()) {
25
+ if (EXCLUDED_DIRS.has(entry.name))
26
+ continue;
27
+ await walk(full);
28
+ continue;
29
+ }
30
+ if (!entry.isFile())
31
+ continue; // symlinks etc. are skipped
32
+ if (ENV_FILE_RE.test(entry.name)) {
33
+ excludedEnvFiles.push(rel);
34
+ continue;
35
+ }
36
+ if (EXCLUDED_FILES.has(entry.name) || entry.name.endsWith('.log'))
37
+ continue;
38
+ const buffer = await readFile(full);
39
+ if (isProbablyBinary(buffer)) {
40
+ files.push({ path: rel, content: buffer.toString('base64'), encoding: 'base64' });
41
+ }
42
+ else {
43
+ files.push({ path: rel, content: buffer.toString('utf8'), encoding: 'utf8' });
44
+ }
45
+ }
46
+ };
47
+ await walk(rootDir);
48
+ return { files, excludedEnvFiles };
49
+ }
50
+ /**
51
+ * Pre-flight checks mirroring the server's bundle validation so engineers
52
+ * get fast local feedback instead of a 400 after uploading.
53
+ */
54
+ export async function validateLocalBundle({ rootDir, files, }) {
55
+ const issues = [];
56
+ const paths = new Set(files.map((f) => f.path));
57
+ const lockfile = files.find((f) => f.path === 'pnpm-lock.yaml');
58
+ if (!lockfile) {
59
+ if (existsSync(join(rootDir, 'package-lock.json')) || existsSync(join(rootDir, 'yarn.lock'))) {
60
+ issues.push({
61
+ level: 'warning',
62
+ message: 'Found a non-pnpm lockfile (package-lock.json / yarn.lock). It is ignored — the deploy worker resolves dependencies against Chainguard, Sequence\'s internal registry. With Chainguard credentials, `pnpm install` produces a pinned lockfile for a faster deploy; without them this is fine as-is.',
63
+ });
64
+ }
65
+ else {
66
+ issues.push({
67
+ level: 'info',
68
+ message: 'No pnpm-lock.yaml — the deploy worker will resolve dependencies against Chainguard, Sequence\'s internal registry. With Chainguard credentials, `pnpm install` produces a pinned lockfile for a faster deploy; without them this is fine as-is.',
69
+ });
70
+ }
71
+ }
72
+ else {
73
+ const lockfileText = lockfile.encoding === 'base64'
74
+ ? Buffer.from(lockfile.content, 'base64').toString('utf8')
75
+ : lockfile.content;
76
+ if (classifyLockfileOrigin(lockfileText) !== 'chainguard') {
77
+ issues.push({
78
+ level: 'warning',
79
+ message: 'pnpm-lock.yaml was not resolved against Chainguard — the deploy worker will discard it and re-resolve (slower, and versions may differ). This is expected without Chainguard credentials; with them, regenerate against Chainguard (registry=https://libraries.cgr.dev/javascript/) for a fast, pinned deploy.',
80
+ });
81
+ }
82
+ }
83
+ if (!paths.has('package.json')) {
84
+ issues.push({ level: 'error', message: 'Missing package.json.' });
85
+ }
86
+ else {
87
+ try {
88
+ const raw = await readFile(join(rootDir, 'package.json'), 'utf8');
89
+ const pkg = JSON.parse(raw);
90
+ if (!pkg.dependencies?.['@google-cloud/functions-framework']) {
91
+ issues.push({
92
+ level: 'error',
93
+ message: 'package.json must declare @google-cloud/functions-framework as a dependency (pnpm add @google-cloud/functions-framework).',
94
+ });
95
+ }
96
+ }
97
+ catch {
98
+ issues.push({ level: 'error', message: 'package.json is not valid JSON.' });
99
+ }
100
+ }
101
+ const hookFiles = files.filter((file) => {
102
+ const segments = file.path.split('/');
103
+ return PNPM_HOOK_FILE_RE.test(segments[segments.length - 1] ?? '');
104
+ });
105
+ for (const file of hookFiles) {
106
+ issues.push({
107
+ level: 'error',
108
+ message: `${file.path} is not allowed — pnpm hook files execute during install in the deploy worker.`,
109
+ });
110
+ }
111
+ const workspaceFiles = files.filter((file) => {
112
+ const segments = file.path.split('/');
113
+ return PNPM_WORKSPACE_FILE_RE.test(segments[segments.length - 1] ?? '');
114
+ });
115
+ for (const file of workspaceFiles) {
116
+ issues.push({
117
+ level: 'error',
118
+ message: `${file.path} is not allowed — pnpm-workspace.yaml defines catalog/workspace indirection that can resolve dependencies outside Chainguard.`,
119
+ });
120
+ }
121
+ const totalBytes = files.reduce((sum, f) => sum + Buffer.byteLength(f.content, f.encoding === 'base64' ? 'base64' : 'utf8'), 0);
122
+ if (totalBytes > 25 * 1024 * 1024) {
123
+ issues.push({
124
+ level: 'error',
125
+ message: `Bundle is ${(totalBytes / 1024 / 1024).toFixed(1)} MiB unpacked — the limit is 25 MiB.`,
126
+ });
127
+ }
128
+ return issues;
129
+ }
130
+ export async function isDirectory(path) {
131
+ try {
132
+ return (await stat(path)).isDirectory();
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
@@ -0,0 +1,86 @@
1
+ import { type ResolvedEnv } from '../config.js';
2
+ import type { ParsedArgs } from '../process/commands.js';
3
+ import { type ManagedFunctionManifest } from './manifest.js';
4
+ import { type SourceSpec } from '@sequenceholdings/artifact-studio/source-resolver';
5
+ export declare const LOG = "[seq-studio]";
6
+ export interface FunctionSummary {
7
+ id: string;
8
+ slug: string;
9
+ title: string;
10
+ description: string | null;
11
+ runtime: string;
12
+ activeVersionId?: string | null;
13
+ deployed: boolean;
14
+ secretsNeedDeploy?: boolean;
15
+ currentUserAccess: string | null;
16
+ canInvoke: boolean;
17
+ updatedAt?: string;
18
+ egressHosts?: string[];
19
+ egressIpRanges?: string[];
20
+ egressEnforced?: boolean;
21
+ }
22
+ export interface VersionSummary {
23
+ id: string;
24
+ functionId: string;
25
+ version: string;
26
+ status: string;
27
+ statusDetail: string | null;
28
+ deployMessage: string | null;
29
+ createdAt: string;
30
+ deployedAt: string | null;
31
+ isActive: boolean;
32
+ }
33
+ export declare function flagBool(flags: ParsedArgs['flags'], ...keys: string[]): boolean;
34
+ export interface CommandContext {
35
+ env: ResolvedEnv;
36
+ token: string;
37
+ }
38
+ export declare function buildContext(args: ParsedArgs): Promise<CommandContext>;
39
+ export declare function clientOptions(ctx: CommandContext): {
40
+ baseUrl: string;
41
+ token: string;
42
+ };
43
+ export declare function readManifestOptional(dir: string): Promise<ManagedFunctionManifest | null>;
44
+ export declare function workDir(args: ParsedArgs): string;
45
+ /**
46
+ * Source selection for build/deploy: --dir (local, default '.'), a platform
47
+ * git-service repo (--repo <ns>/<name>), or any git URL (--git-url <url>);
48
+ * --ref picks a branch/tag/commit. Reuses the artifact-studio resolver —
49
+ * functions name their local dir with --dir rather than a positional, so map
50
+ * it onto the spec parser's positional slot.
51
+ */
52
+ export declare function parseFunctionsSourceSpec(args: ParsedArgs): SourceSpec;
53
+ export declare function resolveOrRegisterFunction({ ctx, slug, title, description, }: {
54
+ ctx: CommandContext;
55
+ slug: string;
56
+ title: string;
57
+ description?: string | null;
58
+ }): Promise<{
59
+ fn: FunctionSummary;
60
+ created: boolean;
61
+ }>;
62
+ export declare function predictNextVersion(latest: string | undefined): string;
63
+ /** Prompt on the TTY with echo suppressed — secret values are never displayed. */
64
+ export declare function promptHidden(question: string): Promise<string>;
65
+ export declare function printError(error: unknown): void;
66
+ export declare function functionsInitCommand(args: ParsedArgs): Promise<number>;
67
+ export declare function functionsBuildCommand(args: ParsedArgs): Promise<number>;
68
+ export declare function functionsDeployCommand(args: ParsedArgs): Promise<number>;
69
+ export declare function pollVersion({ ctx, functionId, versionId, egressNote, }: {
70
+ ctx: CommandContext;
71
+ functionId: string;
72
+ versionId: string;
73
+ /** Print the egress-propagation hint once the version is live. */
74
+ egressNote?: boolean;
75
+ }): Promise<number>;
76
+ export declare function functionsListCommand(args: ParsedArgs): Promise<number>;
77
+ export declare function functionsShowCommand(args: ParsedArgs): Promise<number>;
78
+ export declare function functionsLogsCommand(args: ParsedArgs): Promise<number>;
79
+ export declare function resolveLiveActiveVersionId(ctx: CommandContext, functionId: string): Promise<string | null>;
80
+ export declare function functionsPromoteCommand(args: ParsedArgs): Promise<number>;
81
+ export declare function functionsRollbackCommand(args: ParsedArgs): Promise<number>;
82
+ /** Minimal dotenv parser — KEY=VALUE lines, quotes stripped, comments skipped. */
83
+ export declare function parseDotenv(content: string): Record<string, string>;
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";
86
+ export declare function runFunctionsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;