@ascendkit/cli 0.2.6 → 0.3.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.
@@ -9,10 +9,10 @@ export async function listTemplates(client, params) {
9
9
  searchParams.set("isSystem", String(params.isSystem));
10
10
  const qs = searchParams.toString();
11
11
  const path = qs ? `/api/content/templates?${qs}` : "/api/content/templates";
12
- return client.get(path);
12
+ return client.managedGet(path);
13
13
  }
14
14
  export async function getTemplate(client, templateId) {
15
- return client.get(`/api/content/templates/${templateId}`);
15
+ return client.managedGet(`/api/content/templates/${templateId}`);
16
16
  }
17
17
  export async function updateTemplate(client, templateId, params) {
18
18
  return client.managedPut(`/api/content/templates/${templateId}`, params);
@@ -21,8 +21,8 @@ export async function deleteTemplate(client, templateId) {
21
21
  return client.managedDelete(`/api/content/templates/${templateId}`);
22
22
  }
23
23
  export async function listVersions(client, templateId) {
24
- return client.get(`/api/content/templates/${templateId}/versions`);
24
+ return client.managedGet(`/api/content/templates/${templateId}/versions`);
25
25
  }
26
26
  export async function getVersion(client, templateId, versionNumber) {
27
- return client.get(`/api/content/templates/${templateId}/versions/${versionNumber}`);
27
+ return client.managedGet(`/api/content/templates/${templateId}/versions/${versionNumber}`);
28
28
  }
@@ -23,15 +23,71 @@ export interface EmailSettings {
23
23
  value: string;
24
24
  }>;
25
25
  dnsProvider?: DnsProviderInfo;
26
+ identities?: EmailIdentity[];
27
+ }
28
+ export interface EmailIdentity {
29
+ email: string;
30
+ displayName: string;
31
+ verificationStatus: string;
32
+ isDefault: boolean;
33
+ }
34
+ export interface EmailIdentitiesResponse {
35
+ identities: EmailIdentity[];
36
+ active?: {
37
+ fromEmail?: string;
38
+ fromName?: string;
39
+ };
40
+ }
41
+ export interface DnsCheckRecord {
42
+ name: string;
43
+ type: string;
44
+ expectedValue: string;
45
+ found: boolean;
46
+ observedValue: string | null;
47
+ mismatch: boolean;
48
+ error: string | null;
49
+ checkedAt: string;
50
+ }
51
+ export interface DnsCheckResponse {
52
+ summary: {
53
+ total: number;
54
+ found: number;
55
+ notFound: number;
56
+ mismatch: number;
57
+ errored: number;
58
+ };
59
+ records: DnsCheckRecord[];
26
60
  }
27
61
  export declare function getSettings(client: AscendKitClient): Promise<EmailSettings>;
28
62
  export declare function updateSettings(client: AscendKitClient, params: UpdateEmailSettingsParams): Promise<EmailSettings>;
29
63
  export declare function setupDomain(client: AscendKitClient, domain: string): Promise<EmailSettings>;
30
- export declare function checkDomainStatus(client: AscendKitClient): Promise<unknown>;
64
+ export declare function checkDomainStatus(client: AscendKitClient): Promise<{
65
+ domain?: string;
66
+ status?: string;
67
+ dnsRecords?: Array<{
68
+ type: string;
69
+ name: string;
70
+ value: string;
71
+ }>;
72
+ dnsProvider?: DnsProviderInfo;
73
+ }>;
74
+ export declare function checkDnsRecords(client: AscendKitClient): Promise<DnsCheckResponse>;
31
75
  export declare function getDnsProvider(client: AscendKitClient, domain?: string): Promise<DnsProviderInfo>;
32
76
  export declare function removeDomain(client: AscendKitClient): Promise<EmailSettings>;
33
- export declare function useDefaultIdentity(client: AscendKitClient): Promise<EmailSettings>;
34
- export declare function useCustomIdentity(client: AscendKitClient, domain: string, options?: {
35
- fromEmail?: string;
36
- fromName?: string;
37
- }): Promise<EmailSettings>;
77
+ export declare function listIdentities(client: AscendKitClient): Promise<EmailIdentitiesResponse>;
78
+ export declare function createIdentity(client: AscendKitClient, identity: {
79
+ email: string;
80
+ displayName?: string;
81
+ }): Promise<EmailIdentitiesResponse>;
82
+ export declare function resendIdentityVerification(client: AscendKitClient, email: string): Promise<EmailIdentitiesResponse>;
83
+ export declare function setDefaultIdentity(client: AscendKitClient, identity: {
84
+ email: string;
85
+ displayName?: string;
86
+ }): Promise<EmailIdentitiesResponse>;
87
+ export declare function removeIdentity(client: AscendKitClient, email: string): Promise<EmailIdentitiesResponse>;
88
+ export declare function sendTestEmail(client: AscendKitClient, request: {
89
+ to: string;
90
+ fromIdentityEmail?: string;
91
+ }): Promise<{
92
+ message: string;
93
+ }>;
@@ -1,5 +1,5 @@
1
1
  export async function getSettings(client) {
2
- return client.get("/api/email/settings");
2
+ return client.managedGet("/api/email/settings");
3
3
  }
4
4
  export async function updateSettings(client, params) {
5
5
  const body = {};
@@ -13,27 +13,36 @@ export async function setupDomain(client, domain) {
13
13
  return client.managedPost("/api/email/settings/setup-domain", { domain });
14
14
  }
15
15
  export async function checkDomainStatus(client) {
16
- return client.get("/api/email/settings/domain-status");
16
+ return client.managedGet("/api/email/settings/domain-status");
17
+ }
18
+ export async function checkDnsRecords(client) {
19
+ return client.managedGet("/api/email/settings/check-dns-records");
17
20
  }
18
21
  export async function getDnsProvider(client, domain) {
19
22
  const suffix = domain ? `?domain=${encodeURIComponent(domain)}` : "";
20
- return client.get(`/api/email/settings/dns-provider${suffix}`);
23
+ return client.managedGet(`/api/email/settings/dns-provider${suffix}`);
21
24
  }
22
25
  export async function removeDomain(client) {
23
26
  return client.managedDelete("/api/email/settings/domain");
24
27
  }
25
- export async function useDefaultIdentity(client) {
26
- await removeDomain(client);
27
- return updateSettings(client, { fromEmail: "", fromName: "" });
28
- }
29
- export async function useCustomIdentity(client, domain, options) {
30
- const configured = await setupDomain(client, domain);
31
- if (options?.fromEmail !== undefined || options?.fromName !== undefined) {
32
- await updateSettings(client, {
33
- fromEmail: options.fromEmail,
34
- fromName: options.fromName,
35
- });
36
- return getSettings(client);
37
- }
38
- return configured;
28
+ export async function listIdentities(client) {
29
+ return client.managedGet("/api/email/settings/identities");
30
+ }
31
+ export async function createIdentity(client, identity) {
32
+ return client.managedPost("/api/email/settings/identities", {
33
+ email: identity.email,
34
+ displayName: identity.displayName ?? "",
35
+ });
36
+ }
37
+ export async function resendIdentityVerification(client, email) {
38
+ return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(email)}/resend`, {});
39
+ }
40
+ export async function setDefaultIdentity(client, identity) {
41
+ return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(identity.email)}/default`, { displayName: identity.displayName ?? "" });
42
+ }
43
+ export async function removeIdentity(client, email) {
44
+ return client.managedDelete(`/api/email/settings/identities/${encodeURIComponent(email)}`);
45
+ }
46
+ export async function sendTestEmail(client, request) {
47
+ return client.managedPost("/api/email/settings/send-test", request);
39
48
  }
@@ -30,6 +30,7 @@ export declare function updateJourney(client: AscendKitClient, journeyId: string
30
30
  export declare function deleteJourney(client: AscendKitClient, journeyId: string): Promise<unknown>;
31
31
  export declare function activateJourney(client: AscendKitClient, journeyId: string): Promise<unknown>;
32
32
  export declare function pauseJourney(client: AscendKitClient, journeyId: string): Promise<unknown>;
33
+ export declare function resumeJourney(client: AscendKitClient, journeyId: string): Promise<unknown>;
33
34
  export declare function archiveJourney(client: AscendKitClient, journeyId: string): Promise<unknown>;
34
35
  export declare function getJourneyAnalytics(client: AscendKitClient, journeyId: string): Promise<unknown>;
35
36
  export declare function getJourneyVisualization(client: AscendKitClient, journeyId: string): Promise<unknown>;
@@ -11,10 +11,10 @@ export async function listJourneys(client, opts) {
11
11
  if (opts?.offset != null)
12
12
  qs.set("offset", String(opts.offset));
13
13
  const query = qs.toString();
14
- return client.get(`${BASE}${query ? `?${query}` : ""}`);
14
+ return client.managedGet(`${BASE}${query ? `?${query}` : ""}`);
15
15
  }
16
16
  export async function getJourney(client, journeyId) {
17
- return client.get(`${BASE}/${journeyId}`);
17
+ return client.managedGet(`${BASE}/${journeyId}`);
18
18
  }
19
19
  export async function updateJourney(client, journeyId, params) {
20
20
  return client.managedRequest("PATCH", `${BASE}/${journeyId}`, params);
@@ -28,17 +28,20 @@ export async function activateJourney(client, journeyId) {
28
28
  export async function pauseJourney(client, journeyId) {
29
29
  return client.managedPost(`${BASE}/${journeyId}/pause`);
30
30
  }
31
+ export async function resumeJourney(client, journeyId) {
32
+ return client.managedPost(`${BASE}/${journeyId}/resume`);
33
+ }
31
34
  export async function archiveJourney(client, journeyId) {
32
35
  return client.managedPost(`${BASE}/${journeyId}/archive`);
33
36
  }
34
37
  export async function getJourneyAnalytics(client, journeyId) {
35
- return client.get(`${BASE}/${journeyId}/analytics`);
38
+ return client.managedGet(`${BASE}/${journeyId}/analytics`);
36
39
  }
37
40
  export async function getJourneyVisualization(client, journeyId) {
38
- return client.get(`${BASE}/${journeyId}/visualization`);
41
+ return client.managedGet(`${BASE}/${journeyId}/visualization`);
39
42
  }
40
43
  export async function listNodes(client, journeyId) {
41
- return client.get(`${BASE}/${journeyId}/nodes`);
44
+ return client.managedGet(`${BASE}/${journeyId}/nodes`);
42
45
  }
43
46
  export async function addNode(client, journeyId, params) {
44
47
  return client.managedPost(`${BASE}/${journeyId}/nodes`, params);
@@ -56,7 +59,7 @@ export async function listTransitions(client, journeyId, opts) {
56
59
  if (opts?.to_node)
57
60
  qs.set("to_node", opts.to_node);
58
61
  const query = qs.toString();
59
- return client.get(`${BASE}/${journeyId}/transitions${query ? `?${query}` : ""}`);
62
+ return client.managedGet(`${BASE}/${journeyId}/transitions${query ? `?${query}` : ""}`);
60
63
  }
61
64
  export async function addTransition(client, journeyId, params) {
62
65
  return client.managedPost(`${BASE}/${journeyId}/transitions`, params);
@@ -1,6 +1,7 @@
1
1
  export declare function init(apiUrl?: string, portalUrl?: string): Promise<void>;
2
2
  export declare function logout(): void;
3
3
  export declare function listProjects(): Promise<unknown>;
4
+ export declare function showProject(projectId: string): Promise<Record<string, unknown>>;
4
5
  export declare function createProject(name: string, description?: string, enabledServices?: string[]): Promise<unknown>;
5
6
  export declare function listEnvironments(projectId: string): Promise<unknown>;
6
7
  export declare function useEnvironment(tier: string, projectId: string): Promise<void>;
@@ -30,6 +31,21 @@ export declare function mcpLogin(client: AscendKitClient, params: McpLoginParams
30
31
  };
31
32
  accountId: string;
32
33
  }>;
34
+ export declare function mcpSetEnvironmentByPublicKey(client: AscendKitClient, publicKey: string): Promise<{
35
+ updatedFiles: string[];
36
+ environment: {
37
+ id: string;
38
+ name: string;
39
+ tier: string;
40
+ publicKey: string;
41
+ secretKey?: string | null;
42
+ };
43
+ project: {
44
+ id: string;
45
+ name: string;
46
+ };
47
+ role: string;
48
+ }>;
33
49
  export declare function mcpListProjects(client: AscendKitClient): Promise<unknown>;
34
50
  export declare function mcpCreateProject(client: AscendKitClient, params: McpCreateProjectParams): Promise<unknown>;
35
51
  export declare function mcpListEnvironments(client: AscendKitClient, projectId: string): Promise<unknown>;
@@ -48,10 +64,14 @@ export declare function mcpPromoteEnvironment(client: AscendKitClient, params: {
48
64
  targetTier: string;
49
65
  }): Promise<unknown>;
50
66
  export interface McpUpdateEnvironmentVariablesParams {
51
- projectId: string;
52
- envId: string;
67
+ projectId?: string;
68
+ envId?: string;
53
69
  variables: Record<string, string>;
54
70
  }
71
+ export declare function mcpGetEnvironment(client: AscendKitClient, params: {
72
+ projectId?: string;
73
+ envId?: string;
74
+ }): Promise<Record<string, unknown>>;
55
75
  export declare function mcpUpdateEnvironmentVariables(client: AscendKitClient, params: McpUpdateEnvironmentVariablesParams): Promise<unknown>;
56
76
  export declare function getEnvironment(projectId: string, envId: string): Promise<Record<string, unknown>>;
57
77
  export declare function updateEnvironmentVariables(projectId: string, envId: string, variables: Record<string, string>): Promise<unknown>;
@@ -1,7 +1,7 @@
1
1
  import { hostname, platform as osPlatform, release } from "node:os";
2
2
  import { readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { loadAuth, saveAuth, deleteAuth, saveEnvContext, ensureGitignore } from "../utils/credentials.js";
4
+ import { loadAuth, loadEnvContext, saveAuth, deleteAuth, saveEnvContext, ensureGitignore, } from "../utils/credentials.js";
5
5
  import { DEFAULT_API_URL, DEFAULT_PORTAL_URL } from "../constants.js";
6
6
  const POLL_INTERVAL_MS = 2000;
7
7
  const DEVICE_CODE_EXPIRY_MS = 300_000; // 5 minutes
@@ -16,6 +16,14 @@ const ASCENDKIT_ENV_KEYS = [
16
16
  ];
17
17
  const ASCENDKIT_BLOCK_START = "# --- AscendKit Managed Environment Variables ---";
18
18
  const ASCENDKIT_BLOCK_END = "# --- End AscendKit Managed Environment Variables ---";
19
+ function normalizePromotionTier(targetTier) {
20
+ const normalized = targetTier.trim().toLowerCase();
21
+ if (normalized === "production")
22
+ return "prod";
23
+ if (normalized === "staging")
24
+ return "beta";
25
+ return normalized;
26
+ }
19
27
  async function promptYesNo(question, defaultYes) {
20
28
  const suffix = defaultYes ? "[Y/n]" : "[y/N]";
21
29
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -184,13 +192,14 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
184
192
  const existingPublicKey = readEnvValue(original, "ASCENDKIT_ENV_KEY");
185
193
  const existingSecretKey = readEnvValue(original, "ASCENDKIT_SECRET_KEY");
186
194
  const existingWebhookSecret = readEnvValue(original, "ASCENDKIT_WEBHOOK_SECRET") ?? "";
195
+ const incomingSecretKey = (secretKey ?? "").trim();
187
196
  const resolvedApiUrl = apiUrl;
188
197
  const resolvedPublicKey = preserveExistingKeys
189
198
  ? (existingPublicKey && existingPublicKey.trim() ? existingPublicKey : publicKey)
190
199
  : publicKey;
191
200
  const resolvedSecretKey = preserveExistingKeys
192
- ? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey : secretKey)
193
- : secretKey;
201
+ ? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey : incomingSecretKey)
202
+ : (incomingSecretKey || existingSecretKey || "");
194
203
  const resolvedWebhookSecret = options?.webhookSecret ?? existingWebhookSecret;
195
204
  return rewriteAscendKitEnvBlock(filePath, {
196
205
  NEXT_PUBLIC_ASCENDKIT_API_URL: resolvedApiUrl,
@@ -201,6 +210,124 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
201
210
  ASCENDKIT_WEBHOOK_SECRET: resolvedWebhookSecret,
202
211
  });
203
212
  }
213
+ async function syncLocalEnvFiles(apiUrl, publicKey, secretKey) {
214
+ const cwd = process.cwd();
215
+ const folders = await findEnvFolders(cwd);
216
+ const updatedFiles = [];
217
+ for (const folder of folders) {
218
+ for (const filePath of folder.examples) {
219
+ const changed = await updateEnvExampleFile(filePath);
220
+ if (changed)
221
+ updatedFiles.push(path.relative(cwd, filePath));
222
+ }
223
+ for (const filePath of folder.runtime) {
224
+ const changed = await updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey);
225
+ if (changed)
226
+ updatedFiles.push(path.relative(cwd, filePath));
227
+ }
228
+ }
229
+ return updatedFiles;
230
+ }
231
+ function saveActiveEnvironmentContext(result) {
232
+ saveEnvContext({
233
+ publicKey: result.environment.publicKey,
234
+ projectId: result.project.id,
235
+ projectName: result.project.name,
236
+ environmentId: result.environment.id,
237
+ environmentName: result.environment.name,
238
+ tier: result.environment.tier,
239
+ });
240
+ }
241
+ async function activateEnvironment(auth, result, options) {
242
+ const promptForFileUpdates = options?.promptForFileUpdates ?? true;
243
+ const shouldSetupWebhook = options?.setupWebhook ?? true;
244
+ const logOutput = options?.logOutput ?? true;
245
+ saveActiveEnvironmentContext(result);
246
+ if (logOutput) {
247
+ console.log(` → Project: ${result.project.name}`);
248
+ console.log(` → Environment: ${result.environment.name}`);
249
+ console.log(` → Role: ${result.role}`);
250
+ }
251
+ const cwd = process.cwd();
252
+ const folders = await findEnvFolders(cwd);
253
+ const updatedFiles = [];
254
+ const consentedRuntimeFolders = [];
255
+ if (folders.length > 0) {
256
+ if (logOutput) {
257
+ console.log(`\nFound env files in ${folders.length} folder(s).`);
258
+ }
259
+ for (const folder of folders) {
260
+ let update = true;
261
+ if (promptForFileUpdates) {
262
+ const fileNames = [...folder.examples, ...folder.runtime]
263
+ .map(f => path.basename(f)).join(", ");
264
+ update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
265
+ }
266
+ if (!update)
267
+ continue;
268
+ for (const filePath of folder.examples) {
269
+ const changed = await updateEnvExampleFile(filePath);
270
+ if (changed)
271
+ updatedFiles.push(path.relative(cwd, filePath));
272
+ }
273
+ if (folder.runtime.length > 0) {
274
+ consentedRuntimeFolders.push(folder);
275
+ for (const filePath of folder.runtime) {
276
+ const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey);
277
+ if (changed)
278
+ updatedFiles.push(path.relative(cwd, filePath));
279
+ }
280
+ }
281
+ }
282
+ }
283
+ if (logOutput) {
284
+ console.log("\nUpdated files:");
285
+ if (updatedFiles.length === 0) {
286
+ console.log(" (none)");
287
+ }
288
+ else {
289
+ for (const filePath of updatedFiles)
290
+ console.log(` - ${filePath}`);
291
+ }
292
+ if (!result.environment.secretKey) {
293
+ console.log("\nNote: your role does not have access to the environment secret key.");
294
+ }
295
+ }
296
+ if (!shouldSetupWebhook) {
297
+ return { updatedFiles, webhookSecret: null };
298
+ }
299
+ const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
300
+ if (webhookSecret) {
301
+ if (consentedRuntimeFolders.length > 0) {
302
+ for (const folder of consentedRuntimeFolders) {
303
+ for (const filePath of folder.runtime) {
304
+ await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey, { preserveExistingKeys: true, webhookSecret });
305
+ }
306
+ }
307
+ if (logOutput) {
308
+ console.log("\nWebhook secret written to .env file(s).");
309
+ }
310
+ }
311
+ else if (logOutput) {
312
+ console.log(`\nAdd this to your environment:`);
313
+ console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
314
+ }
315
+ }
316
+ if (logOutput) {
317
+ const remainingSteps = [];
318
+ if (!webhookSecret) {
319
+ remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
320
+ }
321
+ remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
322
+ if (remainingSteps.length > 0) {
323
+ console.log("\nWhat you still need to do:");
324
+ for (let i = 0; i < remainingSteps.length; i++) {
325
+ console.log(` ${i + 1}. ${remainingSteps[i]}`);
326
+ }
327
+ }
328
+ }
329
+ return { updatedFiles, webhookSecret };
330
+ }
204
331
  function generateDeviceCode() {
205
332
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I, O, 0, 1 for readability
206
333
  let code = "";
@@ -223,6 +350,28 @@ async function apiRequest(apiUrl, method, path, body, token, publicKey) {
223
350
  const response = await fetch(`${apiUrl}${path}`, init);
224
351
  if (!response.ok) {
225
352
  const text = await response.text();
353
+ if (!text) {
354
+ throw new Error(`API error ${response.status}: ${response.statusText}`);
355
+ }
356
+ try {
357
+ const parsed = JSON.parse(text);
358
+ if (typeof parsed.error === "string") {
359
+ throw new Error(parsed.error);
360
+ }
361
+ if (typeof parsed.detail === "string") {
362
+ throw new Error(parsed.detail);
363
+ }
364
+ if (parsed.detail
365
+ && typeof parsed.detail === "object"
366
+ && typeof parsed.detail.message === "string") {
367
+ throw new Error(parsed.detail.message);
368
+ }
369
+ }
370
+ catch (error) {
371
+ if (error instanceof Error && error.message !== text) {
372
+ throw error;
373
+ }
374
+ }
226
375
  throw new Error(`API error ${response.status}: ${text}`);
227
376
  }
228
377
  const json = await response.json();
@@ -330,6 +479,14 @@ export async function listProjects() {
330
479
  const auth = requireAuth();
331
480
  return apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
332
481
  }
482
+ export async function showProject(projectId) {
483
+ const projects = await listProjects();
484
+ const project = projects.find((item) => item.id === projectId);
485
+ if (!project) {
486
+ throw new Error(`Project ${projectId} not found.`);
487
+ }
488
+ return project;
489
+ }
333
490
  export async function createProject(name, description, enabledServices) {
334
491
  const auth = requireAuth();
335
492
  return apiRequest(auth.apiUrl, "POST", "/api/platform/projects", {
@@ -349,26 +506,7 @@ export async function useEnvironment(tier, projectId) {
349
506
  if (!env) {
350
507
  throw new Error(`No ${tier} environment found for project ${projectId}. Available: ${envs.map(e => e.tier).join(", ")}`);
351
508
  }
352
- // Fetch project name for echo-back
353
- let projectName = projectId;
354
- try {
355
- const projects = await apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
356
- const project = projects.find(p => p.id === projectId);
357
- if (project)
358
- projectName = project.name;
359
- }
360
- catch { /* non-critical */ }
361
- saveEnvContext({
362
- publicKey: env.publicKey,
363
- projectId,
364
- projectName,
365
- environmentId: env.id,
366
- environmentName: env.name,
367
- tier: env.tier,
368
- });
369
- console.log(`Active environment: ${env.name} (${env.tier})`);
370
- console.log(`Project: ${projectName}`);
371
- console.log(`Public key: ${env.publicKey}`);
509
+ await setEnv(env.publicKey);
372
510
  }
373
511
  async function setupWebhook(auth, publicKey) {
374
512
  // Never run webhook setup in non-interactive mode — selecting a webhook
@@ -434,82 +572,11 @@ async function setupWebhook(auth, publicKey) {
434
572
  export async function setEnv(publicKey) {
435
573
  const auth = requireAuth();
436
574
  const result = await apiRequest(auth.apiUrl, "GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`, undefined, auth.token);
437
- saveEnvContext({
438
- publicKey: result.environment.publicKey,
439
- projectId: result.project.id,
440
- projectName: result.project.name,
441
- environmentId: result.environment.id,
442
- environmentName: result.environment.name,
443
- tier: result.environment.tier,
575
+ await activateEnvironment(auth, result, {
576
+ promptForFileUpdates: true,
577
+ setupWebhook: true,
578
+ logOutput: true,
444
579
  });
445
- console.log(` → Project: ${result.project.name}`);
446
- console.log(` → Environment: ${result.environment.name}`);
447
- console.log(` → Role: ${result.role}`);
448
- const cwd = process.cwd();
449
- const folders = await findEnvFolders(cwd);
450
- const updatedFiles = [];
451
- const consentedRuntimeFolders = [];
452
- if (folders.length > 0) {
453
- console.log(`\nFound env files in ${folders.length} folder(s).`);
454
- for (const folder of folders) {
455
- const fileNames = [...folder.examples, ...folder.runtime]
456
- .map(f => path.basename(f)).join(", ");
457
- const update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
458
- if (!update)
459
- continue;
460
- for (const filePath of folder.examples) {
461
- const changed = await updateEnvExampleFile(filePath);
462
- if (changed)
463
- updatedFiles.push(path.relative(cwd, filePath));
464
- }
465
- if (folder.runtime.length > 0) {
466
- consentedRuntimeFolders.push(folder);
467
- for (const filePath of folder.runtime) {
468
- const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "");
469
- if (changed)
470
- updatedFiles.push(path.relative(cwd, filePath));
471
- }
472
- }
473
- }
474
- }
475
- console.log("\nUpdated files:");
476
- if (updatedFiles.length === 0) {
477
- console.log(" (none)");
478
- }
479
- else {
480
- for (const filePath of updatedFiles)
481
- console.log(` - ${filePath}`);
482
- }
483
- if (!result.environment.secretKey) {
484
- console.log("\nNote: your role does not have access to the environment secret key.");
485
- }
486
- // --- Webhook setup ---
487
- const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
488
- if (webhookSecret) {
489
- if (consentedRuntimeFolders.length > 0) {
490
- for (const folder of consentedRuntimeFolders) {
491
- for (const filePath of folder.runtime) {
492
- await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "", { preserveExistingKeys: true, webhookSecret });
493
- }
494
- }
495
- console.log("\nWebhook secret written to .env file(s).");
496
- }
497
- else {
498
- console.log(`\nAdd this to your environment:`);
499
- console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
500
- }
501
- }
502
- const remainingSteps = [];
503
- if (!webhookSecret) {
504
- remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
505
- }
506
- remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
507
- if (remainingSteps.length > 0) {
508
- console.log("\nWhat you still need to do:");
509
- for (let i = 0; i < remainingSteps.length; i++) {
510
- console.log(` ${i + 1}. ${remainingSteps[i]}`);
511
- }
512
- }
513
580
  }
514
581
  export async function mcpLogin(client, params) {
515
582
  const data = await client.publicRequest("POST", "/api/platform/auth/token", {
@@ -517,8 +584,20 @@ export async function mcpLogin(client, params) {
517
584
  password: params.password,
518
585
  });
519
586
  client.configurePlatform(data.token);
587
+ saveAuth({ token: data.token, apiUrl: client.currentApiUrl });
588
+ ensureGitignore();
520
589
  return data;
521
590
  }
591
+ export async function mcpSetEnvironmentByPublicKey(client, publicKey) {
592
+ const result = await client.platformRequest("GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`);
593
+ client.configure(result.environment.publicKey);
594
+ const { updatedFiles } = await activateEnvironment({ token: "__mcp__", apiUrl: client.currentApiUrl }, result, {
595
+ promptForFileUpdates: false,
596
+ setupWebhook: false,
597
+ logOutput: false,
598
+ });
599
+ return { ...result, updatedFiles };
600
+ }
522
601
  export async function mcpListProjects(client) {
523
602
  return client.platformRequest("GET", "/api/platform/projects");
524
603
  }
@@ -546,7 +625,7 @@ export async function updateEnvironment(projectId, envId, name, description) {
546
625
  }
547
626
  export async function promoteEnvironment(environmentId, targetTier) {
548
627
  const auth = requireAuth();
549
- return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier, dryRun: false }, auth.token);
628
+ return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier: normalizePromotionTier(targetTier), dryRun: false }, auth.token);
550
629
  }
551
630
  export async function mcpUpdateEnvironment(client, params) {
552
631
  const body = {};
@@ -557,10 +636,33 @@ export async function mcpUpdateEnvironment(client, params) {
557
636
  return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.environmentId}`, body);
558
637
  }
559
638
  export async function mcpPromoteEnvironment(client, params) {
560
- return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
639
+ return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: normalizePromotionTier(params.targetTier), dryRun: false });
640
+ }
641
+ function resolveActiveEnvironmentIds(params) {
642
+ if (params?.projectId && params?.envId) {
643
+ return { projectId: params.projectId, envId: params.envId };
644
+ }
645
+ const ctx = loadEnvContext();
646
+ if (!ctx?.projectId || !ctx.environmentId) {
647
+ throw new Error("No active environment found. Use ascendkit_set_env or ascendkit set-env first.");
648
+ }
649
+ return {
650
+ projectId: params?.projectId ?? ctx.projectId,
651
+ envId: params?.envId ?? ctx.environmentId,
652
+ };
653
+ }
654
+ export async function mcpGetEnvironment(client, params) {
655
+ const target = resolveActiveEnvironmentIds(params);
656
+ const envs = await client.platformRequest("GET", `/api/platform/projects/${target.projectId}/environments`);
657
+ const env = envs.find((item) => item.id === target.envId);
658
+ if (!env) {
659
+ throw new Error(`Environment ${target.envId} not found in project ${target.projectId}.`);
660
+ }
661
+ return env;
561
662
  }
562
663
  export async function mcpUpdateEnvironmentVariables(client, params) {
563
- return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.envId}`, { variables: params.variables });
664
+ const target = resolveActiveEnvironmentIds(params);
665
+ return client.platformRequest("PATCH", `/api/platform/projects/${target.projectId}/environments/${target.envId}`, { variables: params.variables });
564
666
  }
565
667
  export async function getEnvironment(projectId, envId) {
566
668
  const auth = requireAuth();