@sonarsource/marketing-cli 1.0.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.
@@ -0,0 +1,111 @@
1
+ import { confirm } from '@clack/prompts';
2
+ import { runMigrations } from '@kontent-ai/data-ops';
3
+ import { execSync } from 'node:child_process';
4
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { detectBranch } from '../branch.js';
8
+ import { MIGRATIONS_FOLDER, PROTECTED_BRANCHES } from '../branches.js';
9
+ import { isCI, resolveManagementKey } from '../ci.js';
10
+ import { loadConfig } from '../config.js';
11
+ import { readEnvValue } from '../dotenv.js';
12
+ import { assertNotCancelled, CliError } from '../errors.js';
13
+ import { ensureMigrationsPackageJson, requireMigrationsEsm } from '../project.js';
14
+ const TEMPLATE_PATH = join(fileURLToPath(import.meta.url), '..', '..', 'migration.template');
15
+ /**
16
+ * `marketing migration create <name>` — scaffold a new timestamp-ordered
17
+ * TypeScript migration file.
18
+ */
19
+ export async function migrationCreate(name, folder = MIGRATIONS_FOLDER) {
20
+ const now = new Date();
21
+ const template = await readFile(TEMPLATE_PATH, 'utf-8');
22
+ const content = template.replace('{{ORDER}}', `new Date('${now.toISOString()}')`);
23
+ const timestamp = [
24
+ now.getUTCFullYear(),
25
+ pad(now.getUTCMonth() + 1),
26
+ pad(now.getUTCDate()),
27
+ pad(now.getUTCHours()),
28
+ pad(now.getUTCMinutes()),
29
+ pad(now.getUTCSeconds()),
30
+ ].join('-');
31
+ const fileName = `${timestamp}-${name}.ts`;
32
+ await mkdir(folder, { recursive: true });
33
+ if (ensureMigrationsPackageJson(folder)) {
34
+ console.log(`Created ${folder}/package.json`);
35
+ }
36
+ await writeFile(join(folder, fileName), content, 'utf-8');
37
+ console.log(`Created migration: ${fileName}`);
38
+ }
39
+ function pad(n) {
40
+ return n.toString().padStart(2, '0');
41
+ }
42
+ /**
43
+ * `marketing migration run` — compile and execute all pending migrations
44
+ * against the environment specified in `.env.development`.
45
+ *
46
+ * Two-layer production guard:
47
+ * 1. Refuses on protected branches (`master`/`main`/`develop`)
48
+ * 2. Refuses when the resolved environment ID matches production
49
+ *
50
+ * Both guards are bypassed by `allowProduction: true` (used internally by
51
+ * `environment finish`).
52
+ */
53
+ export async function migrationRun(opts) {
54
+ const cwd = process.cwd();
55
+ requireMigrationsEsm(join(cwd, MIGRATIONS_FOLDER));
56
+ const config = loadConfig(cwd);
57
+ // --- Resolve environment ID ---
58
+ const envFilePath = join(cwd, '.env.development');
59
+ const environmentId = opts.environmentId ?? readEnvValue(envFilePath, config.envVars.environmentId);
60
+ if (!environmentId) {
61
+ throw new CliError(`No environment ID found in .env.development — run \`marketing setup\` first.`);
62
+ }
63
+ // --- Production guards + confirmation ---
64
+ if (!opts.allowProduction) {
65
+ const branch = detectBranch(opts);
66
+ if (branch && PROTECTED_BRANCHES.includes(branch)) {
67
+ throw new CliError(`Refusing to run migrations on protected branch "${branch}". ` +
68
+ `Switch to a feature branch, or use \`environment finish\` to migrate production.`);
69
+ }
70
+ if (environmentId === config.kontent.productionEnvironmentId) {
71
+ throw new CliError(`Refusing to run migrations against the production environment (${environmentId}). ` +
72
+ `Use \`environment finish\` to migrate production.`);
73
+ }
74
+ if (!opts.yes) {
75
+ if (!process.stdin.isTTY) {
76
+ throw new CliError(`No interactive terminal detected. Pass --yes to skip confirmation, ` +
77
+ `or run in an interactive shell.`);
78
+ }
79
+ const confirmed = assertNotCancelled(await confirm({
80
+ message: branch
81
+ ? `Run all migrations against environment for branch "${branch}" (${environmentId})?`
82
+ : `Run all migrations against environment ${environmentId}?`,
83
+ }));
84
+ if (confirmed !== true) {
85
+ throw new CliError('Migration aborted by user.');
86
+ }
87
+ }
88
+ }
89
+ // --- Resolve management key ---
90
+ const ci = isCI(opts);
91
+ const apiKey = resolveManagementKey({
92
+ ci,
93
+ netlifyValue: undefined,
94
+ envFilePath,
95
+ managementKeyName: config.envVars.managementKey,
96
+ });
97
+ // --- Compile → run → clean ---
98
+ try {
99
+ execSync('tsc --build tsconfig.migrations.json', { stdio: 'inherit' });
100
+ await runMigrations({
101
+ environmentId,
102
+ apiKey,
103
+ migrationsFolder: MIGRATIONS_FOLDER,
104
+ all: true,
105
+ });
106
+ console.log('Migrations completed successfully.');
107
+ }
108
+ finally {
109
+ execSync('tsc --build tsconfig.migrations.json --clean', { stdio: 'inherit' });
110
+ }
111
+ }
@@ -0,0 +1,9 @@
1
+ export interface SetupOptions {
2
+ branch?: string;
3
+ ci?: boolean;
4
+ }
5
+ /**
6
+ * `marketing setup` — assemble `.env.development` and `.env.production` from
7
+ * Netlify env vars and local config. Fully headless, zero prompts.
8
+ */
9
+ export declare function setup(opts: SetupOptions): Promise<void>;
@@ -0,0 +1,73 @@
1
+ import { join } from 'node:path';
2
+ import { detectBranch } from '../branch.js';
3
+ import { isCI, resolveManagementKey } from '../ci.js';
4
+ import { loadConfig } from '../config.js';
5
+ import { upsertEnvFile } from '../dotenv.js';
6
+ import { CliError } from '../errors.js';
7
+ import { fetchBranchVars, fetchProductionVars, resolveNetlifyToken } from '../netlify.js';
8
+ /**
9
+ * `marketing setup` — assemble `.env.development` and `.env.production` from
10
+ * Netlify env vars and local config. Fully headless, zero prompts.
11
+ */
12
+ export async function setup(opts) {
13
+ const cwd = process.cwd();
14
+ // 1. Load & validate config
15
+ const config = loadConfig(cwd);
16
+ console.log('Loaded config');
17
+ // 2. Resolve Netlify auth token
18
+ const token = resolveNetlifyToken();
19
+ // 3. Detect branch
20
+ const ci = isCI(opts);
21
+ const branch = detectBranch(opts);
22
+ if (!branch && ci) {
23
+ console.log('No branch detected — using global/production values');
24
+ }
25
+ else if (branch) {
26
+ console.log(`Branch: ${branch}`);
27
+ }
28
+ // 4. Fetch production vars
29
+ const prodVars = await fetchProductionVars(token, config.netlify.productionSiteId, config.envVars);
30
+ console.log('Fetched vars from production site');
31
+ // 5. Validate production environment ID
32
+ if (prodVars.productionEnvironmentId !== config.kontent.productionEnvironmentId) {
33
+ throw new CliError(`Netlify's production environment ID does not match config — ` +
34
+ `got "${prodVars.productionEnvironmentId}", expected "${config.kontent.productionEnvironmentId}".`);
35
+ }
36
+ // 6. Fetch preview vars (single GET, cached in BranchVars)
37
+ const branchVars = await fetchBranchVars(token, config.netlify.previewSiteId);
38
+ console.log('Fetched vars from preview site');
39
+ // 7. Resolve each env var from preview site
40
+ const resolvedEnvId = branchVars.resolve(config.envVars.environmentId, branch);
41
+ const resolvedPreviewKey = branchVars.resolve(config.envVars.previewKey, branch);
42
+ const resolvedHomepage = branchVars.resolve(config.envVars.homepage, branch);
43
+ const hasBranchVars = [resolvedEnvId, resolvedPreviewKey, resolvedHomepage].some((r) => r?.isBranchSpecific);
44
+ if (branch) {
45
+ console.log(`Branch: ${branch} (branch-scoped vars ${hasBranchVars ? 'found' : 'not found'})`);
46
+ }
47
+ // 8. Resolve management key
48
+ const managementKey = resolveManagementKey({
49
+ ci,
50
+ netlifyValue: branchVars.resolve(config.envVars.managementKey, branch)?.value,
51
+ envFilePath: join(cwd, '.env.development'),
52
+ managementKeyName: config.envVars.managementKey,
53
+ });
54
+ // 9. Build the var sets
55
+ const sharedVars = {
56
+ [config.envVars.secureKey]: prodVars.secureKey,
57
+ [config.envVars.managementKey]: managementKey,
58
+ };
59
+ if (resolvedEnvId)
60
+ sharedVars[config.envVars.environmentId] = resolvedEnvId.value;
61
+ if (resolvedPreviewKey)
62
+ sharedVars[config.envVars.previewKey] = resolvedPreviewKey.value;
63
+ if (resolvedHomepage)
64
+ sharedVars[config.envVars.homepage] = resolvedHomepage.value;
65
+ // 10. Write .env.development (preview mode on)
66
+ const devVars = { ...sharedVars, [config.envVars.previewMode]: 'true' };
67
+ await upsertEnvFile(join(cwd, '.env.development'), devVars);
68
+ console.log('Wrote .env.development');
69
+ // 11. Write .env.production (preview mode off)
70
+ const prodEnvVars = { ...sharedVars, [config.envVars.previewMode]: 'false' };
71
+ await upsertEnvFile(join(cwd, '.env.production'), prodEnvVars);
72
+ console.log('Wrote .env.production');
73
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod/v4';
2
+ export declare const longLivedBranchSchema: z.ZodObject<{
3
+ environmentId: z.ZodOptional<z.ZodString>;
4
+ children: z.ZodArray<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ export declare const configSchema: z.ZodObject<{
7
+ kontent: z.ZodObject<{
8
+ productionEnvironmentName: z.ZodString;
9
+ productionEnvironmentId: z.ZodString;
10
+ roles: z.ZodArray<z.ZodString>;
11
+ longLivedBranches: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
12
+ environmentId: z.ZodOptional<z.ZodString>;
13
+ children: z.ZodArray<z.ZodString>;
14
+ }, z.core.$strip>>>>;
15
+ }, z.core.$strict>;
16
+ netlify: z.ZodObject<{
17
+ previewSiteId: z.ZodString;
18
+ productionSiteId: z.ZodString;
19
+ }, z.core.$strict>;
20
+ envVars: z.ZodObject<{
21
+ environmentId: z.ZodString;
22
+ secureKey: z.ZodString;
23
+ previewKey: z.ZodString;
24
+ homepage: z.ZodString;
25
+ previewMode: z.ZodString;
26
+ managementKey: z.ZodString;
27
+ }, z.core.$strict>;
28
+ }, z.core.$strict>;
29
+ export type MarketingConfig = z.infer<typeof configSchema>;
30
+ /**
31
+ * Load and validate `marketing.config.json` from the given directory.
32
+ * Throws {@link CliError} on missing file, parse errors, or schema violations.
33
+ */
34
+ export declare function loadConfig(cwd?: string): MarketingConfig;
35
+ /**
36
+ * Write a mutated config back to `marketing.config.json`. Used by
37
+ * `environment create --stack` and `environment finish` to persist
38
+ * long-lived branch state.
39
+ */
40
+ export declare function updateConfig(config: MarketingConfig, cwd?: string): void;
package/dist/config.js ADDED
@@ -0,0 +1,69 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod/v4';
4
+ import { CliError } from './errors.js';
5
+ const CONFIG_FILENAME = 'marketing.config.json';
6
+ export const longLivedBranchSchema = z.object({
7
+ environmentId: z.string().min(1).optional(),
8
+ children: z.array(z.string()),
9
+ });
10
+ export const configSchema = z.strictObject({
11
+ kontent: z.strictObject({
12
+ productionEnvironmentName: z.string().min(1),
13
+ productionEnvironmentId: z.string().min(1),
14
+ roles: z.array(z.string().min(1)),
15
+ longLivedBranches: z.record(z.string(), longLivedBranchSchema).optional().default({}),
16
+ }),
17
+ netlify: z.strictObject({
18
+ previewSiteId: z.string().min(1),
19
+ productionSiteId: z.string().min(1),
20
+ }),
21
+ envVars: z.strictObject({
22
+ environmentId: z.string().min(1),
23
+ secureKey: z.string().min(1),
24
+ previewKey: z.string().min(1),
25
+ homepage: z.string().min(1),
26
+ previewMode: z.string().min(1),
27
+ managementKey: z.string().min(1),
28
+ }),
29
+ });
30
+ /**
31
+ * Load and validate `marketing.config.json` from the given directory.
32
+ * Throws {@link CliError} on missing file, parse errors, or schema violations.
33
+ */
34
+ export function loadConfig(cwd = process.cwd()) {
35
+ const filePath = join(cwd, CONFIG_FILENAME);
36
+ let raw;
37
+ try {
38
+ raw = readFileSync(filePath, 'utf8');
39
+ }
40
+ catch (err) {
41
+ if (err.code === 'ENOENT') {
42
+ throw new CliError(`Could not read ${CONFIG_FILENAME} in ${cwd}. ` +
43
+ `Make sure the file exists and you are running from the project root.`);
44
+ }
45
+ throw new CliError(`Could not read ${CONFIG_FILENAME}: ${err.message}`);
46
+ }
47
+ let json;
48
+ try {
49
+ json = JSON.parse(raw);
50
+ }
51
+ catch {
52
+ throw new CliError(`${CONFIG_FILENAME} contains invalid JSON.`);
53
+ }
54
+ const result = configSchema.safeParse(json);
55
+ if (!result.success) {
56
+ const issues = z.prettifyError(result.error);
57
+ throw new CliError(`Invalid ${CONFIG_FILENAME}:\n${issues}`);
58
+ }
59
+ return result.data;
60
+ }
61
+ /**
62
+ * Write a mutated config back to `marketing.config.json`. Used by
63
+ * `environment create --stack` and `environment finish` to persist
64
+ * long-lived branch state.
65
+ */
66
+ export function updateConfig(config, cwd = process.cwd()) {
67
+ const filePath = join(cwd, CONFIG_FILENAME);
68
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
69
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Upsert key-value pairs into a marker-delimited managed section of a dotenv
3
+ * file. Keys outside the markers are never touched. If the file does not exist
4
+ * or has no markers yet, the managed block is appended.
5
+ */
6
+ export declare function upsertEnvFile(filePath: string, vars: Record<string, string>): Promise<void>;
7
+ /**
8
+ * Read a single key's value from a dotenv file. Returns `undefined` if the
9
+ * file does not exist or the key is not found. Not marker-aware — scans the
10
+ * entire file.
11
+ */
12
+ export declare function readEnvValue(filePath: string, key: string): string | undefined;
package/dist/dotenv.js ADDED
@@ -0,0 +1,113 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { CliError } from './errors.js';
4
+ const START_MARKER = '# --- managed by @sonarsource/marketing-cli start ---';
5
+ const END_MARKER = '# --- managed by @sonarsource/marketing-cli end ---';
6
+ /**
7
+ * Upsert key-value pairs into a marker-delimited managed section of a dotenv
8
+ * file. Keys outside the markers are never touched. If the file does not exist
9
+ * or has no markers yet, the managed block is appended.
10
+ */
11
+ export async function upsertEnvFile(filePath, vars) {
12
+ let existing = '';
13
+ try {
14
+ existing = readFileSync(filePath, 'utf8');
15
+ }
16
+ catch {
17
+ // file doesn't exist yet — fine
18
+ }
19
+ const startIdx = existing.indexOf(START_MARKER);
20
+ const endIdx = existing.indexOf(END_MARKER);
21
+ if (startIdx === -1 || endIdx === -1) {
22
+ // No managed section yet — append one
23
+ const block = buildManagedBlock(vars);
24
+ const sep = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
25
+ const content = existing + sep + block + '\n';
26
+ ensureWrite(filePath, content);
27
+ return;
28
+ }
29
+ // Extract the managed section lines
30
+ const before = existing.slice(0, startIdx);
31
+ const after = existing.slice(endIdx + END_MARKER.length);
32
+ const sectionContent = existing.slice(startIdx + START_MARKER.length, endIdx);
33
+ const sectionLines = sectionContent.split('\n');
34
+ // Upsert within the section: update existing keys, track which are new
35
+ const remaining = new Map(Object.entries(vars));
36
+ const updatedLines = [];
37
+ for (const line of sectionLines) {
38
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
39
+ if (match?.[1] && remaining.has(match[1])) {
40
+ updatedLines.push(`${match[1]}=${remaining.get(match[1])}`);
41
+ remaining.delete(match[1]);
42
+ }
43
+ else {
44
+ updatedLines.push(line);
45
+ }
46
+ }
47
+ // Strip trailing blank lines so new keys sit right before END_MARKER
48
+ while (updatedLines.length && updatedLines[updatedLines.length - 1] === '') {
49
+ updatedLines.pop();
50
+ }
51
+ for (const [key, value] of remaining) {
52
+ updatedLines.push(`${key}=${quoteValue(value)}`);
53
+ }
54
+ const newSection = updatedLines.join('\n');
55
+ const content = before + START_MARKER + '\n' + newSection + '\n' + END_MARKER + after;
56
+ ensureWrite(filePath, content);
57
+ }
58
+ /**
59
+ * Quote a value for safe dotenv writing. Wraps in double quotes and escapes
60
+ * embedded double-quotes and backslashes. Rejects embedded newlines since
61
+ * they would break the marker-delimited section.
62
+ */
63
+ function quoteValue(value) {
64
+ if (value.includes('\n') || value.includes('\r')) {
65
+ throw new CliError(`Cannot write env value containing newlines — this would corrupt the managed section.`);
66
+ }
67
+ // Always quote to handle spaces, #, and other special chars
68
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
69
+ return `"${escaped}"`;
70
+ }
71
+ function buildManagedBlock(vars) {
72
+ const lines = Object.entries(vars).map(([k, v]) => `${k}=${quoteValue(v)}`);
73
+ return [START_MARKER, ...lines, END_MARKER].join('\n');
74
+ }
75
+ function ensureWrite(filePath, content) {
76
+ mkdirSync(dirname(filePath), { recursive: true });
77
+ writeFileSync(filePath, content, 'utf8');
78
+ }
79
+ /**
80
+ * Read a single key's value from a dotenv file. Returns `undefined` if the
81
+ * file does not exist or the key is not found. Not marker-aware — scans the
82
+ * entire file.
83
+ */
84
+ export function readEnvValue(filePath, key) {
85
+ let content;
86
+ try {
87
+ content = readFileSync(filePath, 'utf8');
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ let value;
93
+ for (const line of content.split('\n')) {
94
+ if (line.startsWith(`${key}=`)) {
95
+ value = line.slice(key.length + 1);
96
+ }
97
+ }
98
+ return value !== undefined ? stripQuotes(value) : value;
99
+ }
100
+ /**
101
+ * Strip matching surrounding quotes (single or double) from a dotenv value,
102
+ * mirroring the behaviour of standard dotenv parsers.
103
+ */
104
+ function stripQuotes(raw) {
105
+ if (raw.length >= 2) {
106
+ const first = raw[0];
107
+ const last = raw[raw.length - 1];
108
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
109
+ return raw.slice(1, -1);
110
+ }
111
+ }
112
+ return raw;
113
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * User-facing CLI error. Printed without a stack trace because the failure is
3
+ * expected (bad config, missing token, API mismatch, etc.).
4
+ */
5
+ export declare class CliError extends Error {
6
+ name: string;
7
+ }
8
+ /**
9
+ * Guard for `@clack/prompts` cancel symbols.
10
+ * Returns the value unchanged if it is not a cancel symbol;
11
+ * throws {@link CliError} otherwise.
12
+ */
13
+ export declare function assertNotCancelled<T>(value: T | symbol): T;
package/dist/errors.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * User-facing CLI error. Printed without a stack trace because the failure is
3
+ * expected (bad config, missing token, API mismatch, etc.).
4
+ */
5
+ export class CliError extends Error {
6
+ name = 'CliError';
7
+ }
8
+ /**
9
+ * Guard for `@clack/prompts` cancel symbols.
10
+ * Returns the value unchanged if it is not a cancel symbol;
11
+ * throws {@link CliError} otherwise.
12
+ */
13
+ export function assertNotCancelled(value) {
14
+ if (typeof value === 'symbol') {
15
+ throw new CliError('Aborted by user.');
16
+ }
17
+ return value;
18
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { buildProgram } from './cli.js';
3
+ buildProgram().parse();
@@ -0,0 +1,8 @@
1
+ import { ManagementClient } from '@kontent-ai/management-sdk';
2
+ /**
3
+ * Create a Kontent.ai Management SDK client scoped to a specific environment.
4
+ *
5
+ * Single factory centralises the constructor so the entire SDK surface can be
6
+ * mocked with one `vi.mock('./kontent.js', …)` target.
7
+ */
8
+ export declare function createKontentClient(environmentId: string, apiKey: string): ManagementClient;
@@ -0,0 +1,10 @@
1
+ import { ManagementClient } from '@kontent-ai/management-sdk';
2
+ /**
3
+ * Create a Kontent.ai Management SDK client scoped to a specific environment.
4
+ *
5
+ * Single factory centralises the constructor so the entire SDK surface can be
6
+ * mocked with one `vi.mock('./kontent.js', …)` target.
7
+ */
8
+ export function createKontentClient(environmentId, apiKey) {
9
+ return new ManagementClient({ environmentId, apiKey });
10
+ }
@@ -0,0 +1,9 @@
1
+ import type { MigrationModule } from "@sonarsource/marketing-cli";
2
+
3
+ const migration: MigrationModule = {
4
+ order: {{ORDER}},
5
+ run: async _apiClient => {},
6
+ rollback: async _apiClient => {},
7
+ };
8
+
9
+ export default migration;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Re-exports for consumer migration files. Migrations import from
3
+ * `@sonarsource/marketing-cli` — they never need to depend on `data-ops` or
4
+ * `management-sdk` directly.
5
+ */
6
+ export type { MigrationModule, MigrationOrder } from '@kontent-ai/data-ops';
7
+ export * from '@kontent-ai/management-sdk';
@@ -0,0 +1 @@
1
+ export * from '@kontent-ai/management-sdk';
@@ -0,0 +1,49 @@
1
+ import type { MarketingConfig } from './config.js';
2
+ /**
3
+ * Shape of an env var value entry in the Netlify API response.
4
+ */
5
+ export interface NetlifyEnvValue {
6
+ id: string;
7
+ value: string;
8
+ context: string;
9
+ context_parameter?: string;
10
+ }
11
+ export interface NetlifyEnvVar {
12
+ key: string;
13
+ scopes: string[];
14
+ values: NetlifyEnvValue[];
15
+ }
16
+ export interface ProductionVars {
17
+ secureKey: string;
18
+ productionEnvironmentId: string;
19
+ }
20
+ /**
21
+ * Fetch env vars from the production Netlify site and extract:
22
+ * - `secureKey` (the delivery secure key)
23
+ * - `productionEnvironmentId` (for validation against config)
24
+ */
25
+ export declare function fetchProductionVars(token: string, siteId: string, envVarNames: MarketingConfig['envVars']): Promise<ProductionVars>;
26
+ export interface BranchVars {
27
+ /** Whether a branch-scoped value exists for `key` on `branch`. Reads from cache. */
28
+ has(key: string, branch: string): boolean;
29
+ /** Resolve a var for a branch (branch-specific → global fallback). Reads from cache. */
30
+ resolve(key: string, branch: string | undefined): {
31
+ value: string;
32
+ isBranchSpecific: boolean;
33
+ } | undefined;
34
+ /** Set (or update) a branch-scoped value. Hits the API, then updates the cache. */
35
+ set(key: string, branch: string, value: string): Promise<void>;
36
+ /** Delete a branch-scoped value. Hits the API, then updates the cache. */
37
+ delete(key: string, branch: string): Promise<void>;
38
+ }
39
+ /**
40
+ * Fetch all env vars for a Netlify site once and return a {@link BranchVars}
41
+ * object whose read methods operate on the cached snapshot and whose write
42
+ * methods hit the API then optimistically update the cache.
43
+ */
44
+ export declare function fetchBranchVars(token: string, siteId: string): Promise<BranchVars>;
45
+ /**
46
+ * Resolve the Netlify auth token from the environment.
47
+ * Throws with actionable guidance when missing.
48
+ */
49
+ export declare function resolveNetlifyToken(): string;