@alvera-ai/platform-sdk 0.7.3 → 0.9.0

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
@@ -10,6 +10,8 @@ tables, action status updaters, AI agents — with full type safety.
10
10
  ```bash
11
11
  npm install @alvera-ai/platform-sdk
12
12
  # or
13
+ bun add @alvera-ai/platform-sdk
14
+ # or
13
15
  pnpm add @alvera-ai/platform-sdk
14
16
  ```
15
17
 
@@ -141,25 +143,30 @@ The CLI resolves a base URL with this precedence (highest first):
141
143
 
142
144
  Adding, renaming, or removing environments happens in the platform repo (the
143
145
  `servers/0` function in `lib/platform_api/api_spec.ex`); rerun
144
- `pnpm regen` in the SDK to pick up the change.
146
+ `bun run regen` in the SDK to pick up the change.
145
147
 
146
148
  ## Resources
147
149
 
148
- | Resource | Operations |
149
- |-------------------------|---------------------------------------------|
150
- | `ping` | health check |
151
- | `sessions` | `verify` |
152
- | `datasets` | `search` |
153
- | `datalakes` | `list`, `get`, `create` |
154
- | `dataSources` | `list`, `create`, `update` |
155
- | `tools` | `list`, `get`, `create`, `update`, `delete` |
156
- | `genericTables` | `list`, `create` |
157
- | `actionStatusUpdaters` | `list`, `create`, `update` |
158
- | `aiAgents` | `list`, `get`, `create`, `update`, `delete` |
159
- | `connectedApps` | `list`, `get`, `create`, `update`, `syncRoutes`, `resolvePage`, `updateMessageTracking` |
160
- | `dataActivationClients` | `ingest`, `ingestFile`, `createUploadLink` |
161
- | `mdm` | `verify` |
162
- | `workflows` | `execute` |
150
+ | Resource | Operations |
151
+ |------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
152
+ | `ping` | health check |
153
+ | `sessions` | `verify` |
154
+ | `auth` | `signUp` |
155
+ | `admin` | `confirmUser` |
156
+ | `tenants` | `list`, `create` |
157
+ | `invitations` | `list`, `create`, `accept` |
158
+ | `datasets` | `search`, `metadata`, `createUserSearch` |
159
+ | `datalakes` | `list`, `get`, `create`, `metadata`, `migrate`, `createUploadLink`, `createDownloadLink` |
160
+ | `dataSources` | `list`, `create`, `update` |
161
+ | `tools` | `list`, `get`, `create`, `update`, `delete`, `testInvocation` |
162
+ | `genericTables` | `list`, `create` |
163
+ | `actionStatusUpdaters` | `list`, `create`, `update` |
164
+ | `aiAgents` | `list`, `get`, `create`, `update`, `delete` |
165
+ | `connectedApps` | `list`, `get`, `create`, `update`, `syncRoutes`, `resolvePage`, `updateMessageTracking` |
166
+ | `dataActivationClients` | `list`, `get`, `create`, `update`, `delete`, `metadata`, `runManually`, `ingest`, `ingestFile`, `logs.list`, `logs.get` |
167
+ | `interoperabilityContracts` | `list`, `get`, `create`, `update`, `delete`, `metadata`, `run` |
168
+ | `mdm` | `verify` |
169
+ | `workflows` | `list`, `get`, `create`, `update`, `delete`, `metadata`, `execute`, `run`, `workflowLogs.list/get/download`, `batchLogs.list/get/start/stop/refresh` |
163
170
 
164
171
  Tenant and datalake provisioning are performed by Alvera admins — contact your
165
172
  representative to onboard a new tenant.
@@ -290,12 +297,11 @@ git commit -am "feat(api): …"
290
297
 
291
298
  # in this repo
292
299
  cp ../platform/openapi.yaml spec/openapi.yaml
293
- pnpm regen # gen-environments + codegen + check-coverage
294
- pnpm build # compile to dist/
300
+ bun run regen # gen-environments + codegen + check-coverage
295
301
  git commit -am "chore: sync openapi spec"
296
302
  ```
297
303
 
298
- `pnpm regen` runs `gen-environments` (rebuilds
304
+ `bun run regen` runs `gen-environments` (rebuilds
299
305
  `src/environments.generated.ts` from `servers[]`), then `codegen`, then
300
306
  `check-coverage`.
301
307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alvera-ai/platform-sdk",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "Typed SDK for the Alvera platform API — manage data sources, tools, generic tables, AI agents, and action status updaters.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -24,15 +24,14 @@
24
24
  "engines": {
25
25
  "node": ">=20"
26
26
  },
27
- "packageManager": "pnpm@10.20.0",
28
27
  "scripts": {
29
28
  "gen-environments": "tsx scripts/gen-environments.ts",
30
29
  "codegen": "openapi-ts && tsx scripts/patch-generated.ts",
31
30
  "check-coverage": "tsx scripts/check-sdk-coverage.ts",
32
- "regen": "pnpm gen-environments && pnpm codegen && pnpm check-coverage",
31
+ "regen": "bun run gen-environments && bun run codegen && bun run check-coverage",
33
32
  "typecheck": "tsc --noEmit && tsc --noEmit -p scripts/tsconfig.json",
34
33
  "clean": "rm -rf src/generated src/environments.generated.ts",
35
- "prepare": "pnpm gen-environments && pnpm codegen && pnpm check-coverage"
34
+ "prepare": "bun run gen-environments && bun run codegen && bun run check-coverage"
36
35
  },
37
36
  "dependencies": {
38
37
  "commander": "14.0.3",
@@ -0,0 +1,174 @@
1
+ import { Command } from 'commander';
2
+ import { createSession, createUnvalidatedPlatformApi, revokeSession } from '../client.js';
3
+ import {
4
+ CONFIG_PATHS,
5
+ ENVIRONMENTS,
6
+ clearProfileCreds,
7
+ getProfileName,
8
+ readProfileConfig,
9
+ resolveProfile,
10
+ writeProfileConfig,
11
+ writeProfileCreds,
12
+ } from '../config.js';
13
+ import { type GlobalOpts, authedApi, die, out, prompt, run } from './helpers.js';
14
+
15
+ const ENVIRONMENT_NAMES = Object.keys(ENVIRONMENTS);
16
+
17
+ export function register(program: Command): void {
18
+ program
19
+ .command('configure')
20
+ .description('Interactively set defaults (environment, tenant, email) for a profile')
21
+ .action(async () => {
22
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
23
+ const current = resolveProfile(profile);
24
+ const currentCfg = readProfileConfig(profile);
25
+
26
+ const options = ENVIRONMENT_NAMES.map(
27
+ (name) => `${name} (${ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].base_url})`,
28
+ ).join(', ');
29
+ const defaultChoice = currentCfg.environment ?? current.environment;
30
+ const envInput =
31
+ (await prompt(`Environment [${defaultChoice}] — one of: ${options}, or a custom URL: `)) ||
32
+ defaultChoice;
33
+
34
+ const patch: { environment?: string; base_url?: string; tenant_slug?: string; email?: string } = {};
35
+ const unset: Array<'environment' | 'base_url'> = [];
36
+ if (ENVIRONMENT_NAMES.includes(envInput)) {
37
+ patch.environment = envInput;
38
+ unset.push('base_url');
39
+ } else if (/^https?:\/\//.test(envInput)) {
40
+ patch.base_url = envInput;
41
+ unset.push('environment');
42
+ } else {
43
+ die(
44
+ `"${envInput}" is not a known environment or a URL. ` +
45
+ `Valid: ${ENVIRONMENT_NAMES.join(', ')} or http(s)://…`,
46
+ );
47
+ }
48
+
49
+ patch.tenant_slug =
50
+ (await prompt(`Default tenant slug [${current.tenantSlug ?? ''}]: `)) || current.tenantSlug || '';
51
+ patch.email = (await prompt(`Email [${current.email ?? ''}]: `)) || current.email || '';
52
+ writeProfileConfig(profile, patch, unset);
53
+ process.stderr.write(`Saved profile "${profile}" → ${CONFIG_PATHS.config}\n`);
54
+ });
55
+
56
+ program
57
+ .command('login')
58
+ .description('Exchange credentials for a session token and store it')
59
+ .option('--email <email>')
60
+ .option('--password <password>')
61
+ .option('--tenant <slug>')
62
+ .option('--base-url <url>')
63
+ .option('--expires-in <seconds>', 'session duration (default 86400, max 2592000)')
64
+ .action(async (opts: Record<string, string>) => {
65
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
66
+ const current = resolveProfile(profile);
67
+ const baseUrl = opts.baseUrl ?? current.baseUrl;
68
+ const email = opts.email ?? current.email ?? (await prompt('Email: '));
69
+ const password = opts.password ?? process.env.ALVERA_PASSWORD ?? (await prompt('Password: ', { hidden: true }));
70
+ const tenant = opts.tenant ?? current.tenantSlug ?? (await prompt('Tenant slug: '));
71
+ if (!email || !password || !tenant) die('email, password, and tenant are required');
72
+
73
+ await run(async () => {
74
+ const session = await createSession({
75
+ baseUrl,
76
+ email,
77
+ password,
78
+ tenantSlug: tenant,
79
+ expiresIn: opts.expiresIn ? Number(opts.expiresIn) : undefined,
80
+ });
81
+ writeProfileConfig(profile, {
82
+ ...(opts.baseUrl ? { base_url: baseUrl } : {}),
83
+ tenant_slug: tenant,
84
+ email,
85
+ });
86
+ clearProfileCreds(profile);
87
+ writeProfileCreds(profile, {
88
+ session_token: session.sessionToken,
89
+ expires_at: session.expiresAt ?? '',
90
+ });
91
+ const tenantLabel = session.tenant ? `tenant "${session.tenant.slug}"` : 'no tenant';
92
+ process.stderr.write(
93
+ `Logged in as ${email} → ${tenantLabel} (profile "${profile}").\n` +
94
+ `Token stored in ${CONFIG_PATHS.credentials}\n` +
95
+ (session.expiresAt ? `Expires at ${session.expiresAt}\n` : ''),
96
+ );
97
+ return undefined;
98
+ });
99
+ });
100
+
101
+ program
102
+ .command('logout')
103
+ .description('Revoke the current session and clear stored credentials')
104
+ .action(async () => {
105
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
106
+ const resolved = resolveProfile(profile);
107
+ if (resolved.sessionToken) {
108
+ createUnvalidatedPlatformApi({ baseUrl: resolved.baseUrl, sessionToken: resolved.sessionToken });
109
+ try {
110
+ await revokeSession();
111
+ } catch {
112
+ // Token may already be invalid/expired — clear local state anyway.
113
+ }
114
+ }
115
+ clearProfileCreds(profile);
116
+ process.stderr.write(`Cleared credentials for profile "${profile}".\n`);
117
+ });
118
+
119
+ program
120
+ .command('whoami')
121
+ .description('Print the current profile configuration')
122
+ .action(() => {
123
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
124
+ const resolved = resolveProfile(profile);
125
+ out({
126
+ profile: resolved.profile,
127
+ environment: resolved.environment,
128
+ baseUrl: resolved.baseUrl,
129
+ tenantSlug: resolved.tenantSlug,
130
+ email: resolved.email,
131
+ hasSessionToken: Boolean(resolved.sessionToken),
132
+ expiresAt: resolved.expiresAt,
133
+ hasApiKey: Boolean(resolved.apiKey),
134
+ });
135
+ });
136
+
137
+ program
138
+ .command('set-api-key')
139
+ .description('Store an API key for the current profile (used instead of session token)')
140
+ .argument('[key]', 'API key (omit to read from prompt or ALVERA_API_KEY)')
141
+ .action(async (key?: string) => {
142
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
143
+ const apiKey = key ?? process.env.ALVERA_API_KEY ?? (await prompt('API key: ', { hidden: true }));
144
+ if (!apiKey) die('API key is required');
145
+ clearProfileCreds(profile);
146
+ writeProfileCreds(profile, { api_key: apiKey });
147
+ process.stderr.write(
148
+ `API key stored for profile "${profile}" → ${CONFIG_PATHS.credentials}\n` +
149
+ `(session token cleared)\n`,
150
+ );
151
+ });
152
+
153
+ program
154
+ .command('ping')
155
+ .description('Health check')
156
+ .action(async () => {
157
+ await run(async () => {
158
+ const { api } = authedApi(program.opts<GlobalOpts>());
159
+ const { data } = await api.ping();
160
+ return data;
161
+ });
162
+ });
163
+
164
+ program
165
+ .command('sessions-verify')
166
+ .description('Verify the current session token via the API')
167
+ .action(async () => {
168
+ await run(async () => {
169
+ const { api } = authedApi(program.opts<GlobalOpts>());
170
+ const { data } = await api.sessions.verify();
171
+ return data;
172
+ });
173
+ });
174
+ }
package/src/cli/env.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { Command } from 'commander';
2
+ import {
3
+ DEFAULT_ENVIRONMENT,
4
+ ENVIRONMENTS,
5
+ getProfileName,
6
+ resolveProfile,
7
+ writeProfileConfig,
8
+ } from '../config.js';
9
+ import { type GlobalOpts, die, out } from './helpers.js';
10
+
11
+ const ENVIRONMENT_NAMES = Object.keys(ENVIRONMENTS);
12
+
13
+ export function register(program: Command): void {
14
+ const envCmd = program
15
+ .command('env')
16
+ .description('List and switch Alvera API environments');
17
+
18
+ envCmd
19
+ .command('list')
20
+ .description('List available environments (from spec/openapi.yaml)')
21
+ .action(() => {
22
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
23
+ const resolved = resolveProfile(profile);
24
+ out(
25
+ ENVIRONMENT_NAMES.map((name) => ({
26
+ name,
27
+ baseUrl: ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].base_url,
28
+ description: ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].description,
29
+ default: name === DEFAULT_ENVIRONMENT,
30
+ active: name === resolved.environment,
31
+ })),
32
+ );
33
+ });
34
+
35
+ envCmd
36
+ .command('use <name>')
37
+ .description('Persist the selected environment to the profile (clears any custom base_url)')
38
+ .action((name: string) => {
39
+ if (!ENVIRONMENT_NAMES.includes(name)) {
40
+ die(`unknown environment "${name}". Valid: ${ENVIRONMENT_NAMES.join(', ')}`);
41
+ }
42
+ const profile = getProfileName(program.opts<GlobalOpts>().profile);
43
+ writeProfileConfig(profile, { environment: name }, ['base_url']);
44
+ process.stderr.write(
45
+ `Profile "${profile}" now uses environment "${name}" ` +
46
+ `(${ENVIRONMENTS[name as keyof typeof ENVIRONMENTS].base_url}).\n`,
47
+ );
48
+ });
49
+ }
@@ -0,0 +1,128 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import { Command } from 'commander';
4
+ import { ValiError } from 'valibot';
5
+ import { createUnvalidatedPlatformApi } from '../client.js';
6
+ import { getProfileName, resolveProfile } from '../config.js';
7
+
8
+ export interface GlobalOpts {
9
+ profile?: string;
10
+ env?: string;
11
+ }
12
+
13
+ export function out(value: unknown): void {
14
+ process.stdout.write(JSON.stringify(value, null, 2) + '\n');
15
+ }
16
+
17
+ export function die(message: string, code = 1): never {
18
+ process.stderr.write(`alvera: ${message}\n`);
19
+ process.exit(code);
20
+ }
21
+
22
+ export async function prompt(question: string, { hidden = false } = {}): Promise<string> {
23
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
24
+ if (!hidden) {
25
+ return new Promise((resolve) => rl.question(question, (ans) => {
26
+ rl.close();
27
+ resolve(ans);
28
+ }));
29
+ }
30
+ const anyRl = rl as unknown as { _writeToOutput: (s: string) => void };
31
+ const originalWrite = anyRl._writeToOutput.bind(rl);
32
+ anyRl._writeToOutput = (s: string) => {
33
+ if (s.includes(question)) originalWrite(s);
34
+ else originalWrite('');
35
+ };
36
+ return new Promise((resolve) => rl.question(question, (ans) => {
37
+ rl.close();
38
+ process.stderr.write('\n');
39
+ resolve(ans);
40
+ }));
41
+ }
42
+
43
+ export function readBody(body: string | undefined, bodyFile: string | undefined): Record<string, unknown> {
44
+ if (body && bodyFile) die('use only one of --body or --body-file');
45
+ let raw: string;
46
+ if (body) raw = body;
47
+ else if (bodyFile === '-') raw = readFileSync(0, 'utf8');
48
+ else if (bodyFile) raw = readFileSync(bodyFile, 'utf8');
49
+ else die('missing request body (pass --body <json> or --body-file <path>)');
50
+ try {
51
+ const parsed = JSON.parse(raw);
52
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
53
+ die('body must be a JSON object');
54
+ }
55
+ return parsed;
56
+ } catch (err) {
57
+ die(`invalid JSON in body: ${(err as Error).message}`);
58
+ }
59
+ }
60
+
61
+ export function resolveTenant(explicit: string | undefined, profileTenant: string | null): string {
62
+ const tenant = explicit ?? profileTenant;
63
+ if (!tenant) die('tenant slug required (pass as argument or set `tenant_slug` in the profile)');
64
+ return tenant;
65
+ }
66
+
67
+ export function authedApi(opts: GlobalOpts) {
68
+ const profile = getProfileName(opts.profile);
69
+ const resolved = resolveProfile(profile);
70
+
71
+ if (resolved.apiKey) {
72
+ return {
73
+ api: createUnvalidatedPlatformApi({ baseUrl: resolved.baseUrl, apiKey: resolved.apiKey }),
74
+ resolved,
75
+ };
76
+ }
77
+
78
+ if (!resolved.sessionToken) {
79
+ die(
80
+ `no credentials for profile "${profile}". ` +
81
+ `Run \`alvera login --profile ${profile}\`, set ALVERA_SESSION_TOKEN, or set ALVERA_API_KEY.`,
82
+ );
83
+ }
84
+ if (resolved.expiresAt && new Date(resolved.expiresAt) < new Date()) {
85
+ die(
86
+ `session for profile "${profile}" expired at ${resolved.expiresAt}. ` +
87
+ `Run \`alvera login --profile ${profile}\` to refresh.`,
88
+ );
89
+ }
90
+ return {
91
+ api: createUnvalidatedPlatformApi({ baseUrl: resolved.baseUrl, sessionToken: resolved.sessionToken }),
92
+ resolved,
93
+ };
94
+ }
95
+
96
+ export async function run(fn: () => Promise<unknown>): Promise<void> {
97
+ try {
98
+ const result = await fn();
99
+ if (result !== undefined) out(result);
100
+ } catch (err) {
101
+ die(formatError(err));
102
+ }
103
+ }
104
+
105
+ export function formatError(err: unknown): string {
106
+ if (err instanceof ValiError) {
107
+ const lines = err.issues.map((issue) => {
108
+ const path = (issue.path ?? [])
109
+ .map((p: { key?: unknown }) => String(p.key ?? '?'))
110
+ .join('.') || '(root)';
111
+ return ` ${path}: ${issue.message}`;
112
+ });
113
+ return `validation failed:\n${lines.join('\n')}`;
114
+ }
115
+ if (err instanceof Error) return err.message;
116
+ if (err && typeof err === 'object') {
117
+ const detail = (err as { errors?: { detail?: unknown } }).errors?.detail;
118
+ if (typeof detail === 'string') return detail;
119
+ return JSON.stringify(err, null, 2);
120
+ }
121
+ return String(err);
122
+ }
123
+
124
+ export function bodyOption(cmd: Command): Command {
125
+ return cmd
126
+ .option('--body <json>', 'request body as a JSON string')
127
+ .option('--body-file <path>', 'path to a JSON file (or "-" for stdin)');
128
+ }
@@ -0,0 +1,123 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import { resolve } from 'node:path';
4
+ import { Command } from 'commander';
5
+
6
+ interface InitField {
7
+ key: string;
8
+ label: string;
9
+ default?: string;
10
+ }
11
+ type InitSection = { header: string; fields: InitField[] }
12
+
13
+ const DB_KEYS = ['HOST', 'PORT', 'NAME', 'SCHEMA', 'AUTH_METHOD', 'USER', 'PASS', 'ENABLE_SSL'] as const;
14
+ const DB_LABELS = ['host', 'port', 'database name', 'schema', 'auth method', 'user', 'password', 'enable SSL'] as const;
15
+ const DB_DEFAULTS: Record<string, string> = { PORT: '5432', SCHEMA: 'public', AUTH_METHOD: 'password', ENABLE_SSL: 'false' };
16
+
17
+ function dbSection(label: string, prefix: string): InitSection {
18
+ return {
19
+ header: label,
20
+ fields: DB_KEYS.map((k, i) => ({
21
+ key: `${prefix}_${k}`,
22
+ label: `${label} — ${DB_LABELS[i]}`,
23
+ default: DB_DEFAULTS[k],
24
+ })),
25
+ };
26
+ }
27
+
28
+ const STORAGE_KEYS = ['TYPE', 'REGION', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT', 'BUCKET'] as const;
29
+ const STORAGE_LABELS = ['type (aws/r2)', 'region', 'access key ID', 'secret access key', 'endpoint', 'bucket'] as const;
30
+
31
+ function storageSection(label: string, prefix: string): InitSection {
32
+ return {
33
+ header: label,
34
+ fields: STORAGE_KEYS.map((k, i) => ({
35
+ key: `${prefix}_${k}`,
36
+ label: `${label} — ${STORAGE_LABELS[i]}`,
37
+ default: k === 'TYPE' ? 'aws' : undefined,
38
+ })),
39
+ };
40
+ }
41
+
42
+ async function collectAndWrite(sections: InitSection[], outPath: string): Promise<void> {
43
+ const allFields = sections.flatMap((s) => s.fields);
44
+
45
+ let answers: string[];
46
+ if (process.stdin.isTTY) {
47
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
48
+ const ask = (q: string): Promise<string> =>
49
+ new Promise((res) => rl.question(q, (ans) => res(ans)));
50
+ answers = [];
51
+ for (const field of allFields) {
52
+ const dflt = field.default ?? '';
53
+ const suffix = dflt ? ` [${dflt}]` : '';
54
+ answers.push(await ask(`${field.label}${suffix}: `));
55
+ }
56
+ rl.close();
57
+ } else {
58
+ const raw = readFileSync(0, 'utf8');
59
+ answers = raw.split('\n');
60
+ }
61
+
62
+ const lines: string[] = [];
63
+ let idx = 0;
64
+ for (const section of sections) {
65
+ if (lines.length > 0) lines.push('');
66
+ lines.push(`# ${section.header}`);
67
+ for (const field of section.fields) {
68
+ const dflt = field.default ?? '';
69
+ const answer = (answers[idx++] ?? '').trim();
70
+ lines.push(`${field.key}=${answer || dflt}`);
71
+ }
72
+ }
73
+
74
+ const abs = resolve(process.cwd(), outPath);
75
+ writeFileSync(abs, lines.join('\n') + '\n');
76
+ process.stderr.write(`Wrote ${abs}\n`);
77
+ }
78
+
79
+ export function register(program: Command): void {
80
+ const initCmd = program.command('init').description('Generate a .env configuration file');
81
+
82
+ initCmd
83
+ .command('connected-app')
84
+ .description('Generate .env for SDK / connected-app integration')
85
+ .option('-o, --output <path>', 'output file path', '.env')
86
+ .action(async (opts: { output: string }) => {
87
+ await collectAndWrite([
88
+ {
89
+ header: 'Alvera connected-app configuration',
90
+ fields: [
91
+ { key: 'ALVERA_BASE_URL', label: 'Base URL', default: 'https://api.alvera.ai' },
92
+ { key: 'ALVERA_TENANT', label: 'Tenant slug' },
93
+ { key: 'ALVERA_DATALAKE', label: 'Datalake slug' },
94
+ { key: 'ALVERA_CONNECTED_APP', label: 'Connected app slug' },
95
+ ],
96
+ },
97
+ ], opts.output);
98
+ });
99
+
100
+ initCmd
101
+ .command('infra-setup')
102
+ .description('Generate .env for datalake infrastructure (databases + object storage)')
103
+ .option('-o, --output <path>', 'output file path', '.env')
104
+ .action(async (opts: { output: string }) => {
105
+ await collectAndWrite([
106
+ {
107
+ header: 'Datalake',
108
+ fields: [
109
+ { key: 'ALVERA_DATALAKE_NAME', label: 'Datalake name' },
110
+ { key: 'ALVERA_DATALAKE_DATA_DOMAIN', label: 'Data domain' },
111
+ { key: 'ALVERA_DATALAKE_TIMEZONE', label: 'Timezone', default: 'UTC' },
112
+ { key: 'ALVERA_DATALAKE_POOL_SIZE', label: 'Pool size', default: '10' },
113
+ ],
114
+ },
115
+ dbSection('Database — Unregulated Reader', 'ALVERA_DB_UNREG_READER'),
116
+ dbSection('Database — Unregulated Writer', 'ALVERA_DB_UNREG_WRITER'),
117
+ dbSection('Database — Regulated Reader', 'ALVERA_DB_REG_READER'),
118
+ dbSection('Database — Regulated Writer', 'ALVERA_DB_REG_WRITER'),
119
+ storageSection('Storage — Unregulated', 'ALVERA_STORAGE_UNREG'),
120
+ storageSection('Storage — Regulated', 'ALVERA_STORAGE_REG'),
121
+ ], opts.output);
122
+ });
123
+ }
package/src/cli/raw.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { Command } from 'commander';
2
+ import { getProfileName, resolveProfile } from '../config.js';
3
+ import { type GlobalOpts, bodyOption, die, out, readBody } from './helpers.js';
4
+
5
+ const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
6
+
7
+ export function register(program: Command): void {
8
+ bodyOption(
9
+ program
10
+ .command('raw <method> <path>')
11
+ .description('Send an authenticated HTTP request bypassing SDK validation')
12
+ .option('--no-parse', 'print raw response text instead of pretty-printing JSON'),
13
+ )
14
+ .action(
15
+ async (
16
+ method: string,
17
+ path: string,
18
+ opts: { body?: string; bodyFile?: string; parse?: boolean },
19
+ ) => {
20
+ const upper = method.toUpperCase();
21
+ if (!ALLOWED_METHODS.includes(upper as (typeof ALLOWED_METHODS)[number])) {
22
+ die(`invalid method "${method}". Allowed: ${ALLOWED_METHODS.join(', ')}`);
23
+ }
24
+
25
+ const globalOpts = program.opts<GlobalOpts>();
26
+ const profile = getProfileName(globalOpts.profile);
27
+ const resolved = resolveProfile(profile);
28
+ if (!resolved.sessionToken && !resolved.apiKey) {
29
+ die(
30
+ `no credentials for profile "${profile}". ` +
31
+ `Run \`alvera login\`, set ALVERA_SESSION_TOKEN, or set ALVERA_API_KEY.`,
32
+ );
33
+ }
34
+
35
+ const url = `${resolved.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
36
+ const headers: Record<string, string> = resolved.apiKey
37
+ ? { 'X-API-Key': resolved.apiKey }
38
+ : { Authorization: `Bearer ${resolved.sessionToken!}` };
39
+
40
+ let fetchBody: string | undefined;
41
+ if (opts.body || opts.bodyFile) {
42
+ const parsed = readBody(opts.body, opts.bodyFile);
43
+ fetchBody = JSON.stringify(parsed);
44
+ headers['Content-Type'] = 'application/json';
45
+ }
46
+
47
+ const resp = await fetch(url, { method: upper, headers, body: fetchBody });
48
+ const text = await resp.text();
49
+
50
+ if (!resp.ok) {
51
+ die(`${upper} ${path} → ${resp.status} ${resp.statusText}: ${text}`);
52
+ }
53
+
54
+ if (opts.parse === false) {
55
+ process.stdout.write(text + '\n');
56
+ } else {
57
+ try {
58
+ out(JSON.parse(text));
59
+ } catch {
60
+ process.stdout.write(text + '\n');
61
+ }
62
+ }
63
+ },
64
+ );
65
+ }