@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.
- package/README.md +180 -0
- package/dist/branch.d.ts +14 -0
- package/dist/branch.js +27 -0
- package/dist/branches.d.ts +8 -0
- package/dist/branches.js +8 -0
- package/dist/ci.d.ts +25 -0
- package/dist/ci.js +37 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +102 -0
- package/dist/commands/environment.d.ts +36 -0
- package/dist/commands/environment.js +454 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +184 -0
- package/dist/commands/migration.d.ts +31 -0
- package/dist/commands/migration.js +111 -0
- package/dist/commands/setup.d.ts +9 -0
- package/dist/commands/setup.js +73 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +69 -0
- package/dist/dotenv.d.ts +12 -0
- package/dist/dotenv.js +113 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/kontent.d.ts +8 -0
- package/dist/kontent.js +10 -0
- package/dist/migration.template +9 -0
- package/dist/migrations.d.ts +7 -0
- package/dist/migrations.js +1 -0
- package/dist/netlify.d.ts +49 -0
- package/dist/netlify.js +199 -0
- package/dist/project.d.ts +21 -0
- package/dist/project.js +57 -0
- package/dist/status.d.ts +8 -0
- package/dist/status.js +24 -0
- package/dist/test/handlers.d.ts +18 -0
- package/dist/test/handlers.js +102 -0
- package/dist/test/server.d.ts +1 -0
- package/dist/test/server.js +2 -0
- package/package.json +54 -0
package/dist/netlify.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { CliError } from './errors.js';
|
|
2
|
+
const NETLIFY_API_BASE = 'https://api.netlify.com/api/v1';
|
|
3
|
+
/**
|
|
4
|
+
* Fetch env vars for a Netlify site using raw `fetch`.
|
|
5
|
+
*
|
|
6
|
+
* We bypass `@netlify/api`'s `getSiteEnvVars` because of a bug in the
|
|
7
|
+
* `@netlify/open-api` spec that doubles the `/api/v1` path prefix,
|
|
8
|
+
* producing a 404. See ADR-0002 and https://github.com/netlify/open-api/issues/579
|
|
9
|
+
*/
|
|
10
|
+
async function fetchSiteEnvVars(token, siteId) {
|
|
11
|
+
const url = `${NETLIFY_API_BASE}/sites/${encodeURIComponent(siteId)}/env`;
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `Bearer ${token}`,
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
assertOk(response, 'GET', url);
|
|
19
|
+
return (await response.json());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Fetch the `account_id` for a Netlify site.
|
|
23
|
+
*
|
|
24
|
+
* Write operations on env vars (PATCH, DELETE) must go through the
|
|
25
|
+
* account-level endpoint `/accounts/{account_id}/env/…`, not the
|
|
26
|
+
* site-level endpoint. See ADR-0002.
|
|
27
|
+
*/
|
|
28
|
+
async function fetchSiteAccountId(token, siteId) {
|
|
29
|
+
const url = `${NETLIFY_API_BASE}/sites/${encodeURIComponent(siteId)}`;
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
assertOk(response, 'GET', url);
|
|
37
|
+
const body = (await response.json());
|
|
38
|
+
if (!body.account_id) {
|
|
39
|
+
throw new CliError(`Could not resolve account_id for Netlify site "${siteId}".`);
|
|
40
|
+
}
|
|
41
|
+
return body.account_id;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fetch env vars from the production Netlify site and extract:
|
|
45
|
+
* - `secureKey` (the delivery secure key)
|
|
46
|
+
* - `productionEnvironmentId` (for validation against config)
|
|
47
|
+
*/
|
|
48
|
+
export async function fetchProductionVars(token, siteId, envVarNames) {
|
|
49
|
+
const vars = await fetchSiteEnvVars(token, siteId);
|
|
50
|
+
const secureKey = requireProductionVar(vars, envVarNames.secureKey);
|
|
51
|
+
const productionEnvironmentId = requireProductionVar(vars, envVarNames.environmentId);
|
|
52
|
+
return { secureKey, productionEnvironmentId };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Find a var on the production site and return its production/all value,
|
|
56
|
+
* or throw with a clear message.
|
|
57
|
+
*/
|
|
58
|
+
function requireProductionVar(vars, varName) {
|
|
59
|
+
const envVar = vars.find((v) => v.key === varName);
|
|
60
|
+
if (!envVar) {
|
|
61
|
+
throw new CliError(`Env var "${varName}" not found on the production Netlify site.`);
|
|
62
|
+
}
|
|
63
|
+
const value = getProductionValue(envVar);
|
|
64
|
+
if (!value) {
|
|
65
|
+
throw new CliError(`Env var "${varName}" has no production/all value on the production Netlify site.`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Branch-context guard: resolve a var's value for a specific branch.
|
|
71
|
+
*
|
|
72
|
+
* 1. Look for `context: "branch"` with `context_parameter === branch`.
|
|
73
|
+
* 2. If found → use that value (branch-specific).
|
|
74
|
+
* 3. If not found → use the `context: "all"` value (global fallback).
|
|
75
|
+
*
|
|
76
|
+
* A global fallback is **never** treated as branch-specific.
|
|
77
|
+
*/
|
|
78
|
+
function resolveVarForBranch(envVar, branch) {
|
|
79
|
+
if (branch) {
|
|
80
|
+
const branchEntry = envVar.values.find((v) => v.context === 'branch' && v.context_parameter === branch);
|
|
81
|
+
if (branchEntry) {
|
|
82
|
+
return { value: branchEntry.value, isBranchSpecific: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Fall back to global/all
|
|
86
|
+
const allEntry = envVar.values.find((v) => v.context === 'all');
|
|
87
|
+
if (allEntry) {
|
|
88
|
+
return { value: allEntry.value, isBranchSpecific: false };
|
|
89
|
+
}
|
|
90
|
+
// Try production context as fallback
|
|
91
|
+
const prodEntry = envVar.values.find((v) => v.context === 'production');
|
|
92
|
+
if (prodEntry) {
|
|
93
|
+
return { value: prodEntry.value, isBranchSpecific: false };
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Assert a Netlify API response is successful; throw {@link CliError} on
|
|
99
|
+
* auth failures or unexpected status codes.
|
|
100
|
+
*/
|
|
101
|
+
function assertOk(response, method, url) {
|
|
102
|
+
if (response.status === 401 || response.status === 403) {
|
|
103
|
+
throw new CliError('Netlify authentication failed. Run `netlify login` or set NETLIFY_AUTH_TOKEN.');
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new CliError(`Netlify API error: ${response.status} ${response.statusText} (${method} ${url})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the production or "all" context value from a Netlify env var.
|
|
111
|
+
*/
|
|
112
|
+
function getProductionValue(envVar) {
|
|
113
|
+
const prod = envVar.values.find((v) => v.context === 'production');
|
|
114
|
+
if (prod)
|
|
115
|
+
return prod.value;
|
|
116
|
+
const all = envVar.values.find((v) => v.context === 'all');
|
|
117
|
+
return all?.value;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Fetch all env vars for a Netlify site once and return a {@link BranchVars}
|
|
121
|
+
* object whose read methods operate on the cached snapshot and whose write
|
|
122
|
+
* methods hit the API then optimistically update the cache.
|
|
123
|
+
*/
|
|
124
|
+
export async function fetchBranchVars(token, siteId) {
|
|
125
|
+
// Fetch env vars and account_id in parallel — the account_id is needed
|
|
126
|
+
// because write operations use the account-level API endpoint.
|
|
127
|
+
const [vars, accountId] = await Promise.all([
|
|
128
|
+
fetchSiteEnvVars(token, siteId),
|
|
129
|
+
fetchSiteAccountId(token, siteId),
|
|
130
|
+
]);
|
|
131
|
+
return {
|
|
132
|
+
has(key, branch) {
|
|
133
|
+
const envVar = vars.find((v) => v.key === key);
|
|
134
|
+
if (!envVar)
|
|
135
|
+
return false;
|
|
136
|
+
return envVar.values.some((v) => v.context === 'branch' && v.context_parameter === branch);
|
|
137
|
+
},
|
|
138
|
+
resolve(key, branch) {
|
|
139
|
+
const envVar = vars.find((v) => v.key === key);
|
|
140
|
+
if (!envVar)
|
|
141
|
+
return undefined;
|
|
142
|
+
return resolveVarForBranch(envVar, branch);
|
|
143
|
+
},
|
|
144
|
+
async set(key, branch, value) {
|
|
145
|
+
const url = `${NETLIFY_API_BASE}/accounts/${encodeURIComponent(accountId)}/env/${encodeURIComponent(key)}?site_id=${encodeURIComponent(siteId)}`;
|
|
146
|
+
const response = await fetch(url, {
|
|
147
|
+
method: 'PATCH',
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${token}`,
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
context: 'branch',
|
|
154
|
+
context_parameter: branch,
|
|
155
|
+
value,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
assertOk(response, 'PATCH', url);
|
|
159
|
+
// Optimistic cache update — replace the cached var with the API response
|
|
160
|
+
const updated = (await response.json());
|
|
161
|
+
const idx = vars.findIndex((v) => v.key === key);
|
|
162
|
+
if (idx >= 0) {
|
|
163
|
+
vars[idx] = updated;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
vars.push(updated);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
async delete(key, branch) {
|
|
170
|
+
const envVar = vars.find((v) => v.key === key);
|
|
171
|
+
if (!envVar)
|
|
172
|
+
return;
|
|
173
|
+
const branchValue = envVar.values.find((v) => v.context === 'branch' && v.context_parameter === branch);
|
|
174
|
+
if (!branchValue)
|
|
175
|
+
return;
|
|
176
|
+
const url = `${NETLIFY_API_BASE}/accounts/${encodeURIComponent(accountId)}/env/${encodeURIComponent(key)}/value/${encodeURIComponent(branchValue.id)}?site_id=${encodeURIComponent(siteId)}`;
|
|
177
|
+
const response = await fetch(url, {
|
|
178
|
+
method: 'DELETE',
|
|
179
|
+
headers: {
|
|
180
|
+
Authorization: `Bearer ${token}`,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
assertOk(response, 'DELETE', url);
|
|
184
|
+
// Optimistic cache update — remove the deleted value
|
|
185
|
+
envVar.values = envVar.values.filter((v) => v.id !== branchValue.id);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the Netlify auth token from the environment.
|
|
191
|
+
* Throws with actionable guidance when missing.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveNetlifyToken() {
|
|
194
|
+
const token = process.env['NETLIFY_AUTH_TOKEN'];
|
|
195
|
+
if (!token) {
|
|
196
|
+
throw new CliError('Netlify authentication failed. Run `netlify login` or set NETLIFY_AUTH_TOKEN.');
|
|
197
|
+
}
|
|
198
|
+
return token;
|
|
199
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Canonical content for `Migrations/package.json`. */
|
|
2
|
+
export declare const MIGRATIONS_PACKAGE_JSON: {
|
|
3
|
+
readonly name: "kontent-migrations";
|
|
4
|
+
readonly private: true;
|
|
5
|
+
readonly type: "module";
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Assert that `<migrationsFolder>/package.json` sets `"type": "module"`.
|
|
9
|
+
*
|
|
10
|
+
* Compiled migration `.js` files live in the migrations folder. Without a
|
|
11
|
+
* neighbouring `package.json` that sets `"type": "module"`, Node treats them
|
|
12
|
+
* as CommonJS, and `@kontent-ai/data-ops` cannot load them via dynamic
|
|
13
|
+
* `import()`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requireMigrationsEsm(migrationsFolder: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Write `Migrations/package.json` if it does not already exist.
|
|
18
|
+
*
|
|
19
|
+
* Returns `true` when the file was created, `false` when it already existed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureMigrationsPackageJson(migrationsFolder: string): boolean;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { accessSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { CliError } from './errors.js';
|
|
4
|
+
/** Canonical content for `Migrations/package.json`. */
|
|
5
|
+
export const MIGRATIONS_PACKAGE_JSON = {
|
|
6
|
+
name: 'kontent-migrations',
|
|
7
|
+
private: true,
|
|
8
|
+
type: 'module',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Assert that `<migrationsFolder>/package.json` sets `"type": "module"`.
|
|
12
|
+
*
|
|
13
|
+
* Compiled migration `.js` files live in the migrations folder. Without a
|
|
14
|
+
* neighbouring `package.json` that sets `"type": "module"`, Node treats them
|
|
15
|
+
* as CommonJS, and `@kontent-ai/data-ops` cannot load them via dynamic
|
|
16
|
+
* `import()`.
|
|
17
|
+
*/
|
|
18
|
+
export function requireMigrationsEsm(migrationsFolder) {
|
|
19
|
+
const pkgPath = join(migrationsFolder, 'package.json');
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = readFileSync(pkgPath, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new CliError(`No Migrations/package.json found in ${migrationsFolder}. ` +
|
|
26
|
+
`Run \`marketing init\` to scaffold the required project files.`);
|
|
27
|
+
}
|
|
28
|
+
let pkg;
|
|
29
|
+
try {
|
|
30
|
+
pkg = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new CliError(`Migrations/package.json in ${migrationsFolder} contains invalid JSON.`);
|
|
34
|
+
}
|
|
35
|
+
if (typeof pkg !== 'object' ||
|
|
36
|
+
pkg === null ||
|
|
37
|
+
pkg['type'] !== 'module') {
|
|
38
|
+
throw new CliError(`Migrations/package.json must set "type": "module" for compiled migrations to load correctly. ` +
|
|
39
|
+
`Run \`marketing init\` to fix this.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write `Migrations/package.json` if it does not already exist.
|
|
44
|
+
*
|
|
45
|
+
* Returns `true` when the file was created, `false` when it already existed.
|
|
46
|
+
*/
|
|
47
|
+
export function ensureMigrationsPackageJson(migrationsFolder) {
|
|
48
|
+
const pkgPath = join(migrationsFolder, 'package.json');
|
|
49
|
+
try {
|
|
50
|
+
accessSync(pkgPath);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
writeFileSync(pkgPath, JSON.stringify(MIGRATIONS_PACKAGE_JSON, null, 2) + '\n');
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load `Migrations/status.json` (or return empty `{}`).
|
|
3
|
+
*/
|
|
4
|
+
export declare function loadStatusJson(cwd: string): Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Write `Migrations/status.json`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeStatusJson(cwd: string, status: Record<string, unknown>): void;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { MIGRATIONS_FOLDER } from './branches.js';
|
|
4
|
+
/**
|
|
5
|
+
* Load `Migrations/status.json` (or return empty `{}`).
|
|
6
|
+
*/
|
|
7
|
+
export function loadStatusJson(cwd) {
|
|
8
|
+
const filePath = join(cwd, MIGRATIONS_FOLDER, 'status.json');
|
|
9
|
+
try {
|
|
10
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Write `Migrations/status.json`.
|
|
19
|
+
*/
|
|
20
|
+
export function writeStatusJson(cwd, status) {
|
|
21
|
+
const filePath = join(cwd, MIGRATIONS_FOLDER, 'status.json');
|
|
22
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
23
|
+
writeFileSync(filePath, JSON.stringify(status, null, 2) + '\n', 'utf8');
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NetlifyEnvVar } from '../netlify.js';
|
|
2
|
+
export interface NetlifyFixture {
|
|
3
|
+
/** site_id → env vars mapping */
|
|
4
|
+
sites: Record<string, NetlifyEnvVar[]>;
|
|
5
|
+
/** site_id → account_id mapping */
|
|
6
|
+
siteAccounts: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Factory that creates MSW handlers for the Netlify env-var endpoints.
|
|
10
|
+
*
|
|
11
|
+
* Read (list) uses the site-level endpoint:
|
|
12
|
+
* GET /api/v1/sites/{site_id}/env
|
|
13
|
+
*
|
|
14
|
+
* Write operations use the account-level endpoints (per the Netlify API spec):
|
|
15
|
+
* PATCH /api/v1/accounts/{account_id}/env/{key}?site_id=…
|
|
16
|
+
* DELETE /api/v1/accounts/{account_id}/env/{key}/value/{id}?site_id=…
|
|
17
|
+
*/
|
|
18
|
+
export declare function createNetlifyHandlers(fixture: NetlifyFixture): import("msw").HttpHandler[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { http, HttpResponse } from 'msw';
|
|
2
|
+
/**
|
|
3
|
+
* Factory that creates MSW handlers for the Netlify env-var endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Read (list) uses the site-level endpoint:
|
|
6
|
+
* GET /api/v1/sites/{site_id}/env
|
|
7
|
+
*
|
|
8
|
+
* Write operations use the account-level endpoints (per the Netlify API spec):
|
|
9
|
+
* PATCH /api/v1/accounts/{account_id}/env/{key}?site_id=…
|
|
10
|
+
* DELETE /api/v1/accounts/{account_id}/env/{key}/value/{id}?site_id=…
|
|
11
|
+
*/
|
|
12
|
+
export function createNetlifyHandlers(fixture) {
|
|
13
|
+
/** Resolve site_id from either the query string or the fixture's account mapping. */
|
|
14
|
+
function findSiteForAccount(accountId, requestUrl) {
|
|
15
|
+
// Extract site_id from query string
|
|
16
|
+
const url = new URL(requestUrl);
|
|
17
|
+
const siteId = url.searchParams.get('site_id');
|
|
18
|
+
if (siteId)
|
|
19
|
+
return siteId;
|
|
20
|
+
// Reverse-lookup from fixture
|
|
21
|
+
return Object.entries(fixture.siteAccounts).find(([, acct]) => acct === accountId)?.[0];
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
// GET site info (for account_id resolution)
|
|
25
|
+
http.get('https://api.netlify.com/api/v1/sites/:siteId', ({ params, request }) => {
|
|
26
|
+
const siteId = params['siteId'];
|
|
27
|
+
// Only respond to bare site info requests — not /sites/:id/env
|
|
28
|
+
const url = new URL(request.url);
|
|
29
|
+
if (url.pathname !== `/api/v1/sites/${siteId}`) {
|
|
30
|
+
return undefined; // pass through to next handler
|
|
31
|
+
}
|
|
32
|
+
const accountId = fixture.siteAccounts[siteId];
|
|
33
|
+
if (!accountId) {
|
|
34
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
return HttpResponse.json({ id: siteId, account_id: accountId });
|
|
37
|
+
}),
|
|
38
|
+
// GET all env vars for a site
|
|
39
|
+
http.get('https://api.netlify.com/api/v1/sites/:siteId/env', ({ params }) => {
|
|
40
|
+
const siteId = params['siteId'];
|
|
41
|
+
const vars = fixture.sites[siteId];
|
|
42
|
+
if (!vars) {
|
|
43
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
return HttpResponse.json(vars);
|
|
46
|
+
}),
|
|
47
|
+
// PATCH (set/update) an env var value — account-level endpoint
|
|
48
|
+
http.patch('https://api.netlify.com/api/v1/accounts/:accountId/env/:key', async ({ params, request }) => {
|
|
49
|
+
const accountId = params['accountId'];
|
|
50
|
+
const key = decodeURIComponent(params['key']);
|
|
51
|
+
const siteId = findSiteForAccount(accountId, request.url);
|
|
52
|
+
if (!siteId) {
|
|
53
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
const vars = fixture.sites[siteId];
|
|
56
|
+
if (!vars) {
|
|
57
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
58
|
+
}
|
|
59
|
+
const body = (await request.json());
|
|
60
|
+
// Find or create the var
|
|
61
|
+
let envVar = vars.find((v) => v.key === key);
|
|
62
|
+
if (!envVar) {
|
|
63
|
+
envVar = { key, scopes: ['builds'], values: [] };
|
|
64
|
+
vars.push(envVar);
|
|
65
|
+
}
|
|
66
|
+
// Upsert the value
|
|
67
|
+
const existing = envVar.values.find((v) => v.context === body.context && v.context_parameter === body.context_parameter);
|
|
68
|
+
if (existing) {
|
|
69
|
+
existing.value = body.value;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
envVar.values.push({
|
|
73
|
+
id: `val-${Date.now()}`,
|
|
74
|
+
value: body.value,
|
|
75
|
+
context: body.context,
|
|
76
|
+
context_parameter: body.context_parameter,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return HttpResponse.json(envVar);
|
|
80
|
+
}),
|
|
81
|
+
// DELETE a specific env var value by value ID — account-level endpoint
|
|
82
|
+
http.delete('https://api.netlify.com/api/v1/accounts/:accountId/env/:key/value/:valueId', ({ params, request }) => {
|
|
83
|
+
const accountId = params['accountId'];
|
|
84
|
+
const key = decodeURIComponent(params['key']);
|
|
85
|
+
const valueId = params['valueId'];
|
|
86
|
+
const siteId = findSiteForAccount(accountId, request.url);
|
|
87
|
+
if (!siteId) {
|
|
88
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
89
|
+
}
|
|
90
|
+
const vars = fixture.sites[siteId];
|
|
91
|
+
if (!vars) {
|
|
92
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
93
|
+
}
|
|
94
|
+
const envVar = vars.find((v) => v.key === key);
|
|
95
|
+
if (!envVar) {
|
|
96
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
97
|
+
}
|
|
98
|
+
envVar.values = envVar.values.filter((v) => v.id !== valueId);
|
|
99
|
+
return new HttpResponse(null, { status: 204 });
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const server: import("msw/node").SetupServer;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sonarsource/marketing-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"marketing": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/migrations.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "tsx src/index.ts",
|
|
20
|
+
"build": "tsc -p tsconfig.build.json && cp src/migration.template dist/",
|
|
21
|
+
"prepack": "yarn build",
|
|
22
|
+
"ts:ci": "tsc --noEmit",
|
|
23
|
+
"format": "prettier -w -c .",
|
|
24
|
+
"format:ci": "prettier -c .",
|
|
25
|
+
"lint": "eslint . --max-warnings 0",
|
|
26
|
+
"lint:ci": "eslint . --max-warnings 0 -f json -o ./coverage/eslint.json",
|
|
27
|
+
"test": "vitest",
|
|
28
|
+
"test:ci": "vitest run --coverage"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@clack/prompts": "1.7.0",
|
|
32
|
+
"@kontent-ai/data-ops": "^2.8.0",
|
|
33
|
+
"@kontent-ai/management-sdk": "8.5.2",
|
|
34
|
+
"commander": "15.0.0",
|
|
35
|
+
"zod": "^4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@eslint/js": "10.0.1",
|
|
39
|
+
"@types/node": "26.1.1",
|
|
40
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
41
|
+
"@vitest/eslint-plugin": "1.6.21",
|
|
42
|
+
"eslint": "10.6.0",
|
|
43
|
+
"eslint-config-prettier": "10.1.8",
|
|
44
|
+
"globals": "17.7.0",
|
|
45
|
+
"jiti": "2.7.0",
|
|
46
|
+
"msw": "^2.15.0",
|
|
47
|
+
"prettier": "3.9.4",
|
|
48
|
+
"prettier-plugin-organize-imports": "4.3.0",
|
|
49
|
+
"tsx": "4.23.0",
|
|
50
|
+
"typescript": "6.0.3",
|
|
51
|
+
"typescript-eslint": "8.63.0",
|
|
52
|
+
"vitest": "4.1.10"
|
|
53
|
+
}
|
|
54
|
+
}
|