@dk/jolly 0.2.0 → 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.
@@ -1,110 +1,398 @@
1
- // Jolly — the thin, skill-driven CLI (decision 2026-06-13).
2
- //
3
- // Jolly does not replace the customer's agent. It does deterministic plumbing
4
- // (login/logout/auth status, create store/app-token/stripe, init, start,
5
- // doctor, upgrade, skills) and installs the Jolly skill plus the Saleor
6
- // agent-skills; the customer's agent runs the official CLIs (`npx vercel`,
7
- // `@saleor/configurator`, `git`, `pnpm`). Jolly never shells out to the Vercel
8
- // CLI or Configurator and holds no Vercel token.
9
- //
10
- // Every command emits exactly one output envelope (feature 020):
11
- // { command, status, summary, data, checks, nextSteps, errors }
12
- // Field names are camelCase; checks[].status uses the doctor vocabulary;
13
- // errors[].code is a stable uppercase machine identifier; secrets are
14
- // referenced by name, never printed. Side-effecting actions carry a feature
15
- // 021 riskContext inside the envelope, identical for --dry-run and real runs.
16
- //
17
- // Runtime: ES module TypeScript, run directly by Bun in dev/test and by
18
- // Node >= 23 (native type stripping) in production via bin/jolly. Only Node
19
- // built-ins and the project's own src/lib/ helpers are used.
20
-
21
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
- import { join } from "node:path";
1
+ // src/index.ts
2
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
3
+ import { join as join2 } from "node:path";
23
4
  import { createHash, randomBytes } from "node:crypto";
24
5
  import { spawnSync } from "node:child_process";
25
6
 
26
- import {
27
- cloudApiBase,
28
- listOrganizations,
29
- listProjects,
30
- createProject,
31
- listProjectServices,
32
- pickService,
33
- listEnvironments,
34
- createEnvironment,
35
- pollTaskStatus,
36
- getEnvironment,
37
- extractDomainUrl,
38
- acquireAppToken,
39
- CloudApiError,
40
- type CloudOrganization,
41
- } from "./lib/cloud-api.ts";
42
- import { loadEnvValues, writeEnvValues } from "./lib/env-file.ts";
43
- import { normalizeSaleorUrl } from "./lib/saleor-url.ts";
44
-
45
- // ─── Envelope types (mirror features/support/envelope.ts) ─────────────────
46
-
47
- type EnvelopeStatus = "success" | "warning" | "error";
48
- type CheckStatus = "pass" | "warning" | "fail" | "skipped" | "unknown";
49
- type RiskLevel = "low" | "medium" | "high";
50
-
51
- interface Check {
52
- id: string;
53
- status: CheckStatus;
54
- description?: string;
55
- command?: string;
56
- remediation?: string;
57
- [key: string]: unknown;
7
+ // src/lib/cloud-api.ts
8
+ var DEFAULT_CLOUD_API_BASE = "https://cloud.saleor.io/platform/api";
9
+ function cloudApiBase() {
10
+ const override = process.env["JOLLY_SALEOR_CLOUD_API_URL"];
11
+ if (override && override.trim().length > 0) {
12
+ return override.trim().replace(/\/+$/, "");
13
+ }
14
+ return DEFAULT_CLOUD_API_BASE;
58
15
  }
59
-
60
- interface NextStep {
61
- description: string;
62
- command?: string;
63
- [key: string]: unknown;
16
+ var POLL_INTERVAL_MS = 5e3;
17
+ var POLL_TIMEOUT_MS = 48e4;
18
+ var CloudApiError = class extends Error {
19
+ code;
20
+ httpStatus;
21
+ constructor(message, code, httpStatus) {
22
+ super(message);
23
+ this.name = "CloudApiError";
24
+ this.code = code;
25
+ this.httpStatus = httpStatus;
26
+ }
27
+ };
28
+ async function cloudFetch(url, token, options = {}) {
29
+ return await fetch(url, {
30
+ ...options,
31
+ headers: {
32
+ Authorization: `Token ${token}`,
33
+ "Content-Type": "application/json",
34
+ ...options.headers ?? {}
35
+ }
36
+ });
64
37
  }
65
-
66
- interface ErrorEntry {
67
- code: string;
68
- message: string;
69
- remediation?: string;
70
- [key: string]: unknown;
38
+ async function listOrganizations(token) {
39
+ const response = await cloudFetch(`${cloudApiBase()}/organizations/`, token);
40
+ if (!response.ok) {
41
+ throw new CloudApiError(
42
+ `Failed to list organizations: HTTP ${response.status} ${await response.text()}`,
43
+ "CLOUD_API_ERROR",
44
+ response.status
45
+ );
46
+ }
47
+ return await response.json();
71
48
  }
72
-
73
- interface RiskContext {
74
- action: string;
75
- target: unknown;
76
- riskLevel: RiskLevel;
77
- categories: string[];
78
- reversible: boolean;
79
- sideEffects: unknown[];
80
- dryRunAvailable: boolean;
49
+ async function listProjects(token, organizationSlug) {
50
+ const response = await cloudFetch(
51
+ `${cloudApiBase()}/organizations/${organizationSlug}/projects/`,
52
+ token
53
+ );
54
+ if (!response.ok) {
55
+ throw new CloudApiError(
56
+ `Failed to list projects: HTTP ${response.status} ${await response.text()}`,
57
+ "CLOUD_API_ERROR",
58
+ response.status
59
+ );
60
+ }
61
+ return await response.json();
81
62
  }
82
-
83
- interface Envelope {
84
- command: string;
85
- status: EnvelopeStatus;
86
- summary: string;
87
- data: Record<string, unknown>;
88
- checks: Check[];
89
- nextSteps: NextStep[];
90
- errors: ErrorEntry[];
63
+ async function createProject(token, organizationSlug, body) {
64
+ const response = await cloudFetch(
65
+ `${cloudApiBase()}/organizations/${organizationSlug}/projects/`,
66
+ token,
67
+ { method: "POST", body: JSON.stringify(body) }
68
+ );
69
+ if (!response.ok) {
70
+ throw new CloudApiError(
71
+ `Failed to create project: HTTP ${response.status} ${await response.text()}`,
72
+ "PROJECT_CREATE_FAILED",
73
+ response.status
74
+ );
75
+ }
76
+ return await response.json();
77
+ }
78
+ async function listProjectServices(token, organizationSlug, projectSlug) {
79
+ const response = await cloudFetch(
80
+ `${cloudApiBase()}/organizations/${organizationSlug}/projects/${projectSlug}/services/`,
81
+ token
82
+ );
83
+ if (!response.ok) return [];
84
+ return await response.json();
85
+ }
86
+ function pickService(services, region = "us-east-1") {
87
+ const sandbox = services.filter(
88
+ (s) => String(s.service_type ?? "").toUpperCase() === "SANDBOX"
89
+ );
90
+ const inRegion = sandbox.find((s) => s.region === region);
91
+ const chosen = inRegion ?? sandbox[0] ?? services[0];
92
+ return chosen?.name ?? "saleor";
93
+ }
94
+ async function createEnvironment(token, organizationSlug, body) {
95
+ const response = await cloudFetch(
96
+ `${cloudApiBase()}/organizations/${organizationSlug}/environments/`,
97
+ token,
98
+ { method: "POST", body: JSON.stringify(body) }
99
+ );
100
+ if (!response.ok) {
101
+ const text = await response.text();
102
+ if (response.status >= 400 && response.status < 500 && /domain/i.test(text) && /taken|exists|already|unique|in use|duplicate/i.test(text)) {
103
+ throw new CloudApiError(
104
+ `The Cloud API rejected the environment creation: the domain label "${body.domain_label}" is already taken (HTTP ${response.status}).`,
105
+ "DOMAIN_LABEL_TAKEN",
106
+ response.status
107
+ );
108
+ }
109
+ if (response.status >= 400 && response.status < 500 && /limit|quota|exceed/i.test(text)) {
110
+ throw new CloudApiError(
111
+ "The organization's sandbox environment limit is reached. Delete an unused environment or upgrade the plan, then re-run `jolly create store --create-environment`.",
112
+ "ENVIRONMENT_LIMIT_REACHED",
113
+ response.status
114
+ );
115
+ }
116
+ throw new CloudApiError(
117
+ `Failed to create environment: HTTP ${response.status} ${text}`,
118
+ "ENVIRONMENT_CREATE_FAILED",
119
+ response.status
120
+ );
121
+ }
122
+ return await response.json();
123
+ }
124
+ async function listEnvironments(token, organizationSlug) {
125
+ const response = await cloudFetch(
126
+ `${cloudApiBase()}/organizations/${organizationSlug}/environments/`,
127
+ token
128
+ );
129
+ if (!response.ok) return [];
130
+ return await response.json();
131
+ }
132
+ async function getEnvironment(token, organizationSlug, environmentKey) {
133
+ const response = await cloudFetch(
134
+ `${cloudApiBase()}/organizations/${organizationSlug}/environments/${environmentKey}/`,
135
+ token
136
+ );
137
+ if (!response.ok) return void 0;
138
+ return await response.json();
139
+ }
140
+ function taskStatusUrl(taskId) {
141
+ return `${cloudApiBase()}/service/task-status/${taskId}/`;
142
+ }
143
+ async function pollTaskStatus(taskId, timeoutMs = POLL_TIMEOUT_MS) {
144
+ const deadline = Date.now() + timeoutMs;
145
+ const url = taskStatusUrl(taskId);
146
+ for (; ; ) {
147
+ const response = await fetch(url, {
148
+ headers: { "Content-Type": "application/json" }
149
+ });
150
+ if (!response.ok) {
151
+ throw new CloudApiError(
152
+ `Task status check failed: HTTP ${response.status} ${await response.text()}`,
153
+ "TASK_STATUS_FAILED",
154
+ response.status
155
+ );
156
+ }
157
+ const task = await response.json();
158
+ const status = String(task.status ?? "").toUpperCase();
159
+ if (status === "SUCCEEDED") return task;
160
+ if (status === "FAILED" || status === "ERROR") {
161
+ throw new CloudApiError(
162
+ `Environment provisioning task ${taskId} failed: ${JSON.stringify(task)}`,
163
+ "TASK_FAILED"
164
+ );
165
+ }
166
+ if (Date.now() + POLL_INTERVAL_MS > deadline) {
167
+ throw new CloudApiError(
168
+ `Environment provisioning task ${taskId} did not reach SUCCEEDED within ${Math.round(timeoutMs / 1e3)}s (last status: ${status || "unknown"})`,
169
+ "TASK_TIMEOUT"
170
+ );
171
+ }
172
+ await sleep(POLL_INTERVAL_MS);
173
+ }
174
+ }
175
+ function extractDomainUrl(task, environment, domainLabel) {
176
+ const candidates = [];
177
+ if (task) {
178
+ const result = task.result;
179
+ candidates.push(result?.domain, task.domain, environment?.domain);
180
+ } else {
181
+ candidates.push(environment?.domain);
182
+ }
183
+ for (const candidate of candidates) {
184
+ if (typeof candidate === "string" && candidate.length > 0) {
185
+ const domain = candidate.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
186
+ return `https://${domain}/graphql/`;
187
+ }
188
+ }
189
+ return `https://${domainLabel}.saleor.cloud/graphql/`;
190
+ }
191
+ async function graphqlFetch(graphqlUrl, token, query, variables) {
192
+ const response = await fetch(graphqlUrl, {
193
+ method: "POST",
194
+ headers: {
195
+ Authorization: `Bearer ${token}`,
196
+ "Content-Type": "application/json"
197
+ },
198
+ body: JSON.stringify(variables ? { query, variables } : { query })
199
+ });
200
+ if (!response.ok) {
201
+ throw new CloudApiError(
202
+ `GraphQL request to the Saleor instance failed: HTTP ${response.status}`,
203
+ "GRAPHQL_HTTP_ERROR",
204
+ response.status
205
+ );
206
+ }
207
+ const body = await response.json();
208
+ if (body.errors) {
209
+ throw new CloudApiError(
210
+ `GraphQL errors: ${JSON.stringify(body.errors)}`,
211
+ "GRAPHQL_ERROR"
212
+ );
213
+ }
214
+ return body.data ?? {};
215
+ }
216
+ async function queryGetApps(graphqlUrl, token) {
217
+ const data = await graphqlFetch(
218
+ graphqlUrl,
219
+ token,
220
+ `query GetApps { apps(first: 100) { edges { node { id name } } } }`
221
+ );
222
+ const apps = data.apps;
223
+ const edges = apps?.edges ?? [];
224
+ return edges.map((edge) => edge.node);
225
+ }
226
+ async function queryPermissionEnum(graphqlUrl, token) {
227
+ const data = await graphqlFetch(
228
+ graphqlUrl,
229
+ token,
230
+ `query { __type(name: "PermissionEnum") { enumValues { name } } }`
231
+ );
232
+ const type = data.__type;
233
+ const values = type?.enumValues ?? [];
234
+ return values.map((value) => String(value.name));
235
+ }
236
+ async function createAppToken(graphqlUrl, token, appId) {
237
+ const data = await graphqlFetch(
238
+ graphqlUrl,
239
+ token,
240
+ `mutation AppTokenCreate($app: ID!) {
241
+ appTokenCreate(input: { app: $app }) {
242
+ authToken
243
+ errors { field message }
244
+ }
245
+ }`,
246
+ { app: appId }
247
+ );
248
+ const result = data.appTokenCreate;
249
+ const errors = result?.errors ?? [];
250
+ if (errors.length > 0) {
251
+ throw new CloudApiError(
252
+ `appTokenCreate failed: ${errors.map((e) => e.message).join("; ")}`,
253
+ "APP_TOKEN_CREATE_FAILED"
254
+ );
255
+ }
256
+ const authToken = result?.authToken;
257
+ if (typeof authToken !== "string" || authToken.length === 0) {
258
+ throw new CloudApiError(
259
+ "appTokenCreate did not return an authToken",
260
+ "APP_TOKEN_CREATE_FAILED"
261
+ );
262
+ }
263
+ return { authToken };
264
+ }
265
+ async function createLocalApp(graphqlUrl, token, name, permissions) {
266
+ const data = await graphqlFetch(
267
+ graphqlUrl,
268
+ token,
269
+ `mutation AppCreate($input: AppInput!) {
270
+ appCreate(input: $input) {
271
+ authToken
272
+ app { id name }
273
+ errors { field message }
274
+ }
275
+ }`,
276
+ { input: { name, permissions } }
277
+ );
278
+ const result = data.appCreate;
279
+ const errors = result?.errors ?? [];
280
+ if (errors.length > 0) {
281
+ throw new CloudApiError(
282
+ `appCreate failed: ${errors.map((e) => e.message).join("; ")}`,
283
+ "APP_CREATE_FAILED"
284
+ );
285
+ }
286
+ const app = result?.app;
287
+ const authToken = result?.authToken;
288
+ if (typeof authToken !== "string" || authToken.length === 0) {
289
+ throw new CloudApiError(
290
+ "appCreate did not return an authToken",
291
+ "APP_CREATE_FAILED"
292
+ );
293
+ }
294
+ return { appId: String(app?.id ?? ""), authToken };
295
+ }
296
+ async function acquireAppToken(graphqlUrl, token, appName) {
297
+ const apps = await withRetries(() => queryGetApps(graphqlUrl, token));
298
+ if (apps.length > 0) {
299
+ const { authToken: authToken2 } = await createAppToken(graphqlUrl, token, apps[0].id);
300
+ return authToken2;
301
+ }
302
+ const permissions = await queryPermissionEnum(graphqlUrl, token);
303
+ const { authToken } = await createLocalApp(
304
+ graphqlUrl,
305
+ token,
306
+ appName,
307
+ permissions
308
+ );
309
+ return authToken;
310
+ }
311
+ async function withRetries(fn, attempts = 5, delayMs = 5e3) {
312
+ let lastError;
313
+ for (let attempt = 0; attempt < attempts; attempt++) {
314
+ try {
315
+ return await fn();
316
+ } catch (error) {
317
+ lastError = error;
318
+ if (attempt < attempts - 1) await sleep(delayMs);
319
+ }
320
+ }
321
+ throw lastError;
322
+ }
323
+ function sleep(ms) {
324
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
325
  }
92
326
 
93
- // ─── Argv parsing ─────────────────────────────────────────────────────────
327
+ // src/lib/env-file.ts
328
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
329
+ import { join } from "node:path";
330
+ var ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
331
+ function loadEnvValues(projectDir2) {
332
+ const path = join(projectDir2, ".env");
333
+ if (!existsSync(path)) return {};
334
+ const values = {};
335
+ for (const line of readFileSync(path, "utf8").split("\n")) {
336
+ const match = ENV_LINE.exec(line);
337
+ if (match) values[match[1]] = match[2];
338
+ }
339
+ return values;
340
+ }
341
+ function ensureEnvIgnored(projectDir2) {
342
+ const path = join(projectDir2, ".gitignore");
343
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
344
+ const alreadyIgnored = existing.split("\n").some((line) => line.trim() === ".env");
345
+ if (alreadyIgnored) return;
346
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? `${existing}
347
+ ` : existing;
348
+ writeFileSync(path, `${prefix}.env
349
+ `);
350
+ }
351
+ function writeEnvValues(projectDir2, values) {
352
+ ensureEnvIgnored(projectDir2);
353
+ const path = join(projectDir2, ".env");
354
+ const lines = existsSync(path) ? readFileSync(path, "utf8").split("\n") : [];
355
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
356
+ const pending = { ...values };
357
+ const updated = lines.map((line) => {
358
+ const match = ENV_LINE.exec(line);
359
+ if (match && match[1] in pending) {
360
+ const replacement = `${match[1]}=${pending[match[1]]}`;
361
+ delete pending[match[1]];
362
+ return replacement;
363
+ }
364
+ return line;
365
+ });
366
+ for (const [name, value] of Object.entries(pending)) {
367
+ updated.push(`${name}=${value}`);
368
+ }
369
+ writeFileSync(path, `${updated.join("\n")}
370
+ `);
371
+ return loadEnvValues(projectDir2);
372
+ }
94
373
 
95
- interface ParsedArgs {
96
- positionals: string[];
97
- json: boolean;
98
- quiet: boolean;
99
- yes: boolean;
100
- dryRun: boolean;
101
- help: boolean;
102
- options: Record<string, string>;
103
- flags: Set<string>;
374
+ // src/lib/saleor-url.ts
375
+ var CLARIFICATION = "That doesn't look like a Saleor URL I can use. Could you paste your Saleor Dashboard URL, GraphQL API URL, or root Saleor Cloud URL (for example https://your-store.eu.saleor.cloud)?";
376
+ function normalizeSaleorUrl(input) {
377
+ let url;
378
+ try {
379
+ url = new URL(input.trim());
380
+ } catch {
381
+ return { endpoint: null, clarification: CLARIFICATION };
382
+ }
383
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
384
+ return { endpoint: null, clarification: CLARIFICATION };
385
+ }
386
+ const path = url.pathname.replace(/\/+$/, "");
387
+ const recognized = path === "" || path === "/graphql" || path === "/dashboard" || /^\/dashboard\//.test(url.pathname);
388
+ if (!recognized) {
389
+ return { endpoint: null, clarification: CLARIFICATION };
390
+ }
391
+ return { endpoint: `${url.protocol}//${url.host}/graphql/` };
104
392
  }
105
393
 
106
- // Flags that take a value (so `--name foo` consumes `foo`).
107
- const VALUE_FLAGS = new Set([
394
+ // src/index.ts
395
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
108
396
  "token",
109
397
  "url",
110
398
  "name",
@@ -113,19 +401,17 @@ const VALUE_FLAGS = new Set([
113
401
  "organization",
114
402
  "mock-organizations",
115
403
  "publishable-key",
116
- "secret-key",
404
+ "secret-key"
117
405
  ]);
118
-
119
- function parseArgs(argv: string[]): ParsedArgs {
120
- const positionals: string[] = [];
121
- const options: Record<string, string> = {};
122
- const flags = new Set<string>();
406
+ function parseArgs(argv) {
407
+ const positionals = [];
408
+ const options = {};
409
+ const flags = /* @__PURE__ */ new Set();
123
410
  let json = false;
124
411
  let quiet = false;
125
412
  let yes = false;
126
413
  let dryRun = false;
127
414
  let help = false;
128
-
129
415
  for (let i = 0; i < argv.length; i++) {
130
416
  const arg = argv[i];
131
417
  if (arg === "--json") json = true;
@@ -147,15 +433,9 @@ function parseArgs(argv: string[]): ParsedArgs {
147
433
  positionals.push(arg);
148
434
  }
149
435
  }
150
-
151
436
  return { positionals, json, quiet, yes, dryRun, help, options, flags };
152
437
  }
153
-
154
- // ─── Envelope construction helpers ────────────────────────────────────────
155
-
156
- function envelope(
157
- partial: Partial<Envelope> & { command: string; status: EnvelopeStatus; summary: string },
158
- ): Envelope {
438
+ function envelope(partial) {
159
439
  return {
160
440
  command: partial.command,
161
441
  status: partial.status,
@@ -163,34 +443,24 @@ function envelope(
163
443
  data: partial.data ?? {},
164
444
  checks: partial.checks ?? [],
165
445
  nextSteps: partial.nextSteps ?? [],
166
- errors: partial.errors ?? [],
446
+ errors: partial.errors ?? []
167
447
  };
168
448
  }
169
-
170
- function errorEnvelope(
171
- command: string,
172
- summary: string,
173
- errors: ErrorEntry[],
174
- extra: Partial<Envelope> = {},
175
- ): Envelope {
449
+ function errorEnvelope(command, summary, errors, extra = {}) {
176
450
  return envelope({
177
451
  command,
178
452
  status: "error",
179
453
  summary,
180
454
  errors,
181
- ...extra,
455
+ ...extra
182
456
  });
183
457
  }
184
-
185
- // ─── Output rendering ─────────────────────────────────────────────────────
186
-
187
- function statusGlyph(status: EnvelopeStatus): string {
458
+ function statusGlyph(status) {
188
459
  if (status === "success") return "ok";
189
460
  if (status === "warning") return "warn";
190
461
  return "error";
191
462
  }
192
-
193
- function checkGlyph(status: CheckStatus): string {
463
+ function checkGlyph(status) {
194
464
  switch (status) {
195
465
  case "pass":
196
466
  return "pass";
@@ -204,21 +474,16 @@ function checkGlyph(status: CheckStatus): string {
204
474
  return "?";
205
475
  }
206
476
  }
207
-
208
- /**
209
- * Render and emit one envelope, honoring --json / --quiet / default mode.
210
- * Returns the process exit code (non-zero only for error status).
211
- */
212
- function emit(env: Envelope, args: ParsedArgs): number {
477
+ function emit(env, args) {
213
478
  if (args.json) {
214
479
  process.stdout.write(JSON.stringify(env) + "\n");
215
480
  } else {
216
- const lines: string[] = [];
481
+ const lines = [];
217
482
  lines.push(`jolly ${env.command}: [${statusGlyph(env.status)}] ${env.summary}`);
218
483
  if (!args.quiet) {
219
484
  for (const check of env.checks) {
220
485
  lines.push(
221
- ` - [${checkGlyph(check.status)}] ${check.id}${check.description ? `: ${check.description}` : ""}`,
486
+ ` - [${checkGlyph(check.status)}] ${check.id}${check.description ? `: ${check.description}` : ""}`
222
487
  );
223
488
  }
224
489
  for (const step of env.nextSteps) {
@@ -226,60 +491,38 @@ function emit(env: Envelope, args: ParsedArgs): number {
226
491
  }
227
492
  for (const err of env.errors) {
228
493
  lines.push(
229
- ` error[${err.code}]: ${err.message}${err.remediation ? ` ${err.remediation}` : ""}`,
494
+ ` error[${err.code}]: ${err.message}${err.remediation ? ` \u2014 ${err.remediation}` : ""}`
230
495
  );
231
496
  }
232
497
  }
233
- // Human text first, then the machine-readable envelope on its own line.
234
498
  process.stdout.write(lines.join("\n") + "\n");
235
499
  process.stdout.write(JSON.stringify(env) + "\n");
236
500
  }
237
501
  return env.status === "error" ? 1 : 0;
238
502
  }
239
-
240
- // ─── Project directory ────────────────────────────────────────────────────
241
-
242
- function projectDir(): string {
503
+ function projectDir() {
243
504
  return process.cwd();
244
505
  }
245
-
246
- function envFilePath(): string {
247
- return join(projectDir(), ".env");
248
- }
249
-
250
- // ─── Shared skill set (features 007/001) ──────────────────────────────────
251
-
252
- interface SkillSpec {
253
- id: string;
254
- ref: string;
255
- description: string;
506
+ function envFilePath() {
507
+ return join2(projectDir(), ".env");
256
508
  }
257
-
258
- const DEFAULT_SKILLS: SkillSpec[] = [
509
+ var DEFAULT_SKILLS = [
259
510
  { id: "jolly", ref: "dmytri/jolly", description: "The Jolly end-to-end playbook" },
260
511
  { id: "saleor-storefront", ref: "saleor/saleor-storefront", description: "Saleor storefront guidance" },
261
512
  { id: "saleor-configurator", ref: "saleor/saleor-configurator", description: "Configuration-as-code guidance" },
262
513
  { id: "storefront-builder", ref: "saleor/storefront-builder", description: "Storefront build guidance" },
263
514
  { id: "saleor-core", ref: "saleor/saleor-core", description: "Saleor core concepts" },
264
- { id: "saleor-app", ref: "saleor/saleor-app", description: "Saleor app development guidance" },
515
+ { id: "saleor-app", ref: "saleor/saleor-app", description: "Saleor app development guidance" }
265
516
  ];
266
-
267
- // Standard project-local skill location used by `npx skills add`.
268
- function skillsBaseDir(): string {
269
- return join(projectDir(), ".claude", "skills");
517
+ function skillsBaseDir() {
518
+ return join2(projectDir(), ".claude", "skills");
270
519
  }
271
-
272
- function skillInstalledOnDisk(skill: SkillSpec): boolean {
273
- // A skill is present when its directory exists on disk.
274
- const dir = join(skillsBaseDir(), skill.id);
275
- return existsSync(join(dir, "SKILL.md")) || existsSync(dir);
520
+ function skillInstalledOnDisk(skill) {
521
+ const dir = join2(skillsBaseDir(), skill.id);
522
+ return existsSync2(join2(dir, "SKILL.md")) || existsSync2(dir);
276
523
  }
277
-
278
- // ─── login / token verification (feature 018) ─────────────────────────────
279
-
280
- const TOKEN_PAGE = "https://cloud.saleor.io/tokens";
281
-
282
- function loginRiskContext(dryRunAvailable = true): RiskContext {
524
+ var TOKEN_PAGE = "https://cloud.saleor.io/tokens";
525
+ function loginRiskContext(dryRunAvailable = true) {
283
526
  return {
284
527
  action: "login",
285
528
  target: cloudApiBase(),
@@ -287,36 +530,30 @@ function loginRiskContext(dryRunAvailable = true): RiskContext {
287
530
  categories: ["credential handling"],
288
531
  reversible: true,
289
532
  sideEffects: ["Writes JOLLY_SALEOR_CLOUD_TOKEN to .env when verification permits"],
290
- dryRunAvailable,
533
+ dryRunAvailable
291
534
  };
292
535
  }
293
-
294
- async function commandLogin(args: ParsedArgs): Promise<Envelope> {
536
+ async function commandLogin(args) {
295
537
  const command = "login";
296
538
  const token = args.options["token"];
297
539
  const browser = args.flags.has("browser");
298
-
299
- // --browser flows (PKCE preview, or honest unavailability) -------------
300
540
  if (browser) {
301
541
  if (args.dryRun) {
302
542
  return loginBrowserDryRun(command);
303
543
  }
304
- // Real browser/Playwright callback flow is not implemented on this VM.
305
544
  return errorEnvelope(
306
545
  command,
307
546
  "Browser-based login is not available in this environment.",
308
547
  [
309
548
  {
310
549
  code: "BROWSER_LOGIN_UNAVAILABLE",
311
- message:
312
- "No native browser or Playwright callback flow is available to complete browser OAuth.",
313
- remediation: `Create a token at ${TOKEN_PAGE} and run \`jolly login --token <value>\`.`,
314
- },
550
+ message: "No native browser or Playwright callback flow is available to complete browser OAuth.",
551
+ remediation: `Create a token at ${TOKEN_PAGE} and run \`jolly login --token <value>\`.`
552
+ }
315
553
  ],
316
- { data: { riskContext: loginRiskContext() } },
554
+ { data: { riskContext: loginRiskContext() } }
317
555
  );
318
556
  }
319
-
320
557
  if (!token) {
321
558
  return errorEnvelope(
322
559
  command,
@@ -324,24 +561,21 @@ async function commandLogin(args: ParsedArgs): Promise<Envelope> {
324
561
  [
325
562
  {
326
563
  code: "NO_LOGIN_METHOD",
327
- message:
328
- "jolly login needs `--token <value>` in this environment (no browser/Playwright callback flow).",
329
- remediation: `Create a token at ${TOKEN_PAGE} and run \`jolly login --token <value>\`.`,
330
- },
564
+ message: "jolly login needs `--token <value>` in this environment (no browser/Playwright callback flow).",
565
+ remediation: `Create a token at ${TOKEN_PAGE} and run \`jolly login --token <value>\`.`
566
+ }
331
567
  ],
332
568
  {
333
569
  nextSteps: [
334
570
  {
335
571
  description: `Create a Saleor Cloud token at ${TOKEN_PAGE}, then run jolly login --token <value>.`,
336
- command: "jolly login --token <value>",
337
- },
572
+ command: "jolly login --token <value>"
573
+ }
338
574
  ],
339
- data: { riskContext: loginRiskContext() },
340
- },
575
+ data: { riskContext: loginRiskContext() }
576
+ }
341
577
  );
342
578
  }
343
-
344
- // --token --dry-run: write nothing, show riskContext + nextSteps -------
345
579
  if (args.dryRun) {
346
580
  return envelope({
347
581
  command,
@@ -351,26 +585,19 @@ async function commandLogin(args: ParsedArgs): Promise<Envelope> {
351
585
  nextSteps: [
352
586
  {
353
587
  description: "Run jolly login --token <value> to verify and store the token.",
354
- command: "jolly login --token <value>",
355
- },
356
- ],
588
+ command: "jolly login --token <value>"
589
+ }
590
+ ]
357
591
  });
358
592
  }
359
-
360
- // Real --token login: verify via authenticated GET of organizations/ ----
361
- let orgs: CloudOrganization[] | undefined;
362
- let verificationFailure: unknown;
593
+ let orgs;
594
+ let verificationFailure;
363
595
  try {
364
596
  orgs = await listOrganizations(token);
365
597
  } catch (err) {
366
598
  verificationFailure = err;
367
599
  }
368
-
369
- if (
370
- verificationFailure instanceof CloudApiError &&
371
- (verificationFailure.httpStatus === 401 || verificationFailure.httpStatus === 403)
372
- ) {
373
- // Invalid token: write nothing, error honestly.
600
+ if (verificationFailure instanceof CloudApiError && (verificationFailure.httpStatus === 401 || verificationFailure.httpStatus === 403)) {
374
601
  return errorEnvelope(
375
602
  command,
376
603
  "The token was rejected by the Cloud API. Nothing was written.",
@@ -378,100 +605,90 @@ async function commandLogin(args: ParsedArgs): Promise<Envelope> {
378
605
  {
379
606
  code: "INVALID_TOKEN",
380
607
  message: "Saleor Cloud rejected the token (HTTP 401/403). It was not stored.",
381
- remediation: `Create a new token at ${TOKEN_PAGE} and try again.`,
382
- },
608
+ remediation: `Create a new token at ${TOKEN_PAGE} and try again.`
609
+ }
383
610
  ],
384
611
  {
385
612
  checks: [
386
613
  {
387
614
  id: "cloud-token-verification",
388
615
  status: "fail",
389
- description: "Token rejected by the Cloud API.",
390
- },
616
+ description: "Token rejected by the Cloud API."
617
+ }
391
618
  ],
392
619
  data: { riskContext: loginRiskContext() },
393
620
  nextSteps: [
394
- { description: `Create a new token at ${TOKEN_PAGE}.`, command: `open ${TOKEN_PAGE}` },
395
- ],
396
- },
621
+ { description: `Create a new token at ${TOKEN_PAGE}.`, command: `open ${TOKEN_PAGE}` }
622
+ ]
623
+ }
397
624
  );
398
625
  }
399
-
400
626
  if (verificationFailure) {
401
- // Unreachable / 5xx / timeout: store token, warn "stored, not verified".
402
627
  writeEnvValues(projectDir(), { JOLLY_SALEOR_CLOUD_TOKEN: token });
403
628
  return envelope({
404
629
  command,
405
630
  status: "warning",
406
- summary: "Token stored, not verified the Cloud API was unreachable.",
631
+ summary: "Token stored, not verified \u2014 the Cloud API was unreachable.",
407
632
  data: {
408
633
  cloudTokenStored: true,
409
634
  verified: false,
410
635
  verification: "stored, not verified",
411
- riskContext: loginRiskContext(),
636
+ riskContext: loginRiskContext()
412
637
  },
413
638
  checks: [
414
639
  {
415
640
  id: "cloud-token-verification",
416
641
  status: "unknown",
417
- description: "stored, not verified the Cloud API was unreachable.",
418
- },
642
+ description: "stored, not verified \u2014 the Cloud API was unreachable."
643
+ }
419
644
  ],
420
645
  nextSteps: [
421
646
  {
422
647
  description: "Re-run jolly login when the Cloud API is reachable to verify the token.",
423
- command: "jolly login --token <value>",
424
- },
425
- ],
648
+ command: "jolly login --token <value>"
649
+ }
650
+ ]
426
651
  });
427
652
  }
428
-
429
- // Verified: store token + the real organization name.
430
653
  const orgName = resolveOrgName(orgs ?? []);
431
- const values: Record<string, string> = { JOLLY_SALEOR_CLOUD_TOKEN: token };
654
+ const values = { JOLLY_SALEOR_CLOUD_TOKEN: token };
432
655
  if (orgName) values["JOLLY_SALEOR_ORGANIZATION"] = orgName;
433
656
  writeEnvValues(projectDir(), values);
434
-
435
657
  return envelope({
436
658
  command,
437
659
  status: "success",
438
- summary: orgName
439
- ? `Token verified and stored. Authenticated as "${orgName}".`
440
- : "Token verified and stored.",
660
+ summary: orgName ? `Token verified and stored. Authenticated as "${orgName}".` : "Token verified and stored.",
441
661
  data: {
442
662
  cloudTokenStored: true,
443
663
  verified: true,
444
664
  accountContext: orgName ?? "unknown",
445
- riskContext: loginRiskContext(),
665
+ riskContext: loginRiskContext()
446
666
  },
447
667
  checks: [
448
668
  {
449
669
  id: "cloud-token-verification",
450
670
  status: "pass",
451
- description: "Token verified against the Cloud API organizations endpoint.",
452
- },
671
+ description: "Token verified against the Cloud API organizations endpoint."
672
+ }
453
673
  ],
454
674
  nextSteps: [
455
675
  {
456
676
  description: "Run jolly create store to provision a Saleor Cloud environment.",
457
- command: "jolly create store --create-environment",
458
- },
459
- ],
677
+ command: "jolly create store --create-environment"
678
+ }
679
+ ]
460
680
  });
461
681
  }
462
-
463
- function resolveOrgName(orgs: CloudOrganization[]): string | undefined {
682
+ function resolveOrgName(orgs) {
464
683
  const first = orgs[0];
465
- if (!first) return undefined;
684
+ if (!first) return void 0;
466
685
  const name = first.name ?? first.slug;
467
- return typeof name === "string" && name.length > 0 ? name : undefined;
686
+ return typeof name === "string" && name.length > 0 ? name : void 0;
468
687
  }
469
-
470
- function base64url(buf: Buffer): string {
688
+ function base64url(buf) {
471
689
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
472
690
  }
473
-
474
- function loginBrowserDryRun(command: string): Envelope {
691
+ function loginBrowserDryRun(command) {
475
692
  const verifier = base64url(randomBytes(32));
476
693
  const challenge = base64url(createHash("sha256").update(verifier).digest());
477
694
  const state = base64url(randomBytes(16));
@@ -484,17 +701,10 @@ function loginBrowserDryRun(command: string): Envelope {
484
701
  code_challenge_method: "S256",
485
702
  state,
486
703
  redirect_uri: redirectUri,
487
- scope: "email openid profile",
704
+ scope: "email openid profile"
488
705
  });
489
706
  const authorizationUrl = `${authBase}?${params.toString()}`;
490
-
491
- // The code-exchange preview: the two real POSTs the localhost callback would
492
- // make, described without sending them or claiming any of them succeeded
493
- // (feature 018, "previews the OAuth code exchange requests"). The token
494
- // endpoint is Keycloak (auth.saleor.io); the resulting OIDC id_token is then
495
- // exchanged for a Cloud API token at /platform/api/tokens.
496
- const tokenEndpoint =
497
- "https://auth.saleor.io/realms/saleor-cloud/protocol/openid-connect/token";
707
+ const tokenEndpoint = "https://auth.saleor.io/realms/saleor-cloud/protocol/openid-connect/token";
498
708
  const tokensEndpoint = `${cloudApiBase()}/tokens`;
499
709
  const exchangePreview = {
500
710
  tokenExchange: {
@@ -505,22 +715,20 @@ function loginBrowserDryRun(command: string): Envelope {
505
715
  code: "<authorization code from the localhost callback>",
506
716
  code_verifier: "<the PKCE code_verifier>",
507
717
  client_id: "saleor-cli",
508
- redirect_uri: redirectUri,
509
- },
718
+ redirect_uri: redirectUri
719
+ }
510
720
  },
511
721
  cloudTokenExchange: {
512
722
  method: "POST",
513
723
  url: tokensEndpoint,
514
724
  requestPath: "/platform/api/tokens",
515
- body: { id_token: "<the OIDC id_token returned by Keycloak>" },
516
- },
725
+ body: { id_token: "<the OIDC id_token returned by Keycloak>" }
726
+ }
517
727
  };
518
-
519
728
  return envelope({
520
729
  command,
521
730
  status: "success",
522
- summary:
523
- "Prepared the browser OAuth authorization URL and code-exchange preview (PKCE). Nothing was written.",
731
+ summary: "Prepared the browser OAuth authorization URL and code-exchange preview (PKCE). Nothing was written.",
524
732
  data: {
525
733
  dryRun: true,
526
734
  authorizationUrl,
@@ -531,131 +739,101 @@ function loginBrowserDryRun(command: string): Envelope {
531
739
  clientId: "saleor-cli",
532
740
  responseType: "code",
533
741
  exchangePreview,
534
- riskContext: loginRiskContext(),
742
+ riskContext: loginRiskContext()
535
743
  },
536
744
  nextSteps: [
537
745
  {
538
- description:
539
- "Open the authorization URL in a browser to complete OAuth, or use jolly login --token <value>.",
540
- command: "jolly login --browser",
541
- },
542
- ],
746
+ description: "Open the authorization URL in a browser to complete OAuth, or use jolly login --token <value>.",
747
+ command: "jolly login --browser"
748
+ }
749
+ ]
543
750
  });
544
751
  }
545
-
546
- // ─── logout (feature 018) ─────────────────────────────────────────────────
547
-
548
- const MANAGED_AUTH_VARS = [
752
+ var MANAGED_AUTH_VARS = [
549
753
  "JOLLY_SALEOR_CLOUD_TOKEN",
550
754
  "JOLLY_SALEOR_APP_TOKEN",
551
- "JOLLY_SALEOR_ORGANIZATION",
755
+ "JOLLY_SALEOR_ORGANIZATION"
552
756
  ];
553
-
554
- function commandLogout(_args: ParsedArgs): Envelope {
757
+ function commandLogout(_args) {
555
758
  const command = "logout";
556
759
  const before = loadEnvValues(projectDir());
557
760
  const path = envFilePath();
558
- const removed: string[] = [];
559
-
560
- if (existsSync(path)) {
761
+ const removed = [];
762
+ if (existsSync2(path)) {
561
763
  const lineRe = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
562
- const kept = readFileSync(path, "utf8")
563
- .split("\n")
564
- .filter((line) => {
565
- const m = lineRe.exec(line);
566
- if (m && MANAGED_AUTH_VARS.includes(m[1])) {
567
- removed.push(m[1]);
568
- return false;
569
- }
570
- return true;
571
- });
572
- // Rewrite .env without the managed auth vars, preserving everything else
573
- // (comments, blank lines, third-party credentials) verbatim.
764
+ const kept = readFileSync2(path, "utf8").split("\n").filter((line) => {
765
+ const m = lineRe.exec(line);
766
+ if (m && MANAGED_AUTH_VARS.includes(m[1])) {
767
+ removed.push(m[1]);
768
+ return false;
769
+ }
770
+ return true;
771
+ });
574
772
  let text = kept.join("\n").replace(/\n+$/, "");
575
773
  text = text.length > 0 ? text + "\n" : "";
576
- writeFileSync(path, text);
774
+ writeFileSync2(path, text);
577
775
  }
578
-
579
776
  return envelope({
580
777
  command,
581
778
  status: "success",
582
- summary:
583
- removed.length > 0
584
- ? `Removed Jolly-managed Saleor auth values from .env (${[...new Set(removed)].join(", ")}).`
585
- : "No Jolly-managed Saleor auth values were present in .env.",
779
+ summary: removed.length > 0 ? `Removed Jolly-managed Saleor auth values from .env (${[...new Set(removed)].join(", ")}).` : "No Jolly-managed Saleor auth values were present in .env.",
586
780
  data: {
587
781
  removed: [...new Set(removed)],
588
- preservedOthers: true,
782
+ preservedOthers: true
589
783
  },
590
784
  checks: [
591
785
  {
592
786
  id: "auth-cleared",
593
787
  status: "pass",
594
- description: "Jolly-managed Saleor auth values are no longer in .env.",
595
- },
788
+ description: "Jolly-managed Saleor auth values are no longer in .env."
789
+ }
596
790
  ],
597
791
  nextSteps: [
598
792
  {
599
793
  description: "Run jolly login to authenticate again when needed.",
600
- command: "jolly login --token <value>",
601
- },
602
- ],
794
+ command: "jolly login --token <value>"
795
+ }
796
+ ]
603
797
  });
604
798
  }
605
-
606
- // ─── auth status (feature 018) ────────────────────────────────────────────
607
-
608
- function commandAuthStatus(_args: ParsedArgs): Envelope {
799
+ function commandAuthStatus(_args) {
609
800
  const command = "auth status";
610
801
  const values = loadEnvValues(projectDir());
611
802
  const hasCloudToken = Boolean(values["JOLLY_SALEOR_CLOUD_TOKEN"]);
612
803
  const hasAppToken = Boolean(values["JOLLY_SALEOR_APP_TOKEN"]);
613
804
  const org = values["JOLLY_SALEOR_ORGANIZATION"];
614
805
  const accountContext = org && org.length > 0 ? org : "unknown";
615
-
616
- const checks: Check[] = [
806
+ const checks = [
617
807
  {
618
808
  id: "cloud-token-configured",
619
809
  status: hasCloudToken ? "pass" : "warning",
620
- description: hasCloudToken
621
- ? "JOLLY_SALEOR_CLOUD_TOKEN is configured in .env."
622
- : "JOLLY_SALEOR_CLOUD_TOKEN is not configured.",
810
+ description: hasCloudToken ? "JOLLY_SALEOR_CLOUD_TOKEN is configured in .env." : "JOLLY_SALEOR_CLOUD_TOKEN is not configured."
623
811
  },
624
812
  {
625
813
  id: "app-token-configured",
626
814
  status: hasAppToken ? "pass" : "skipped",
627
- description: hasAppToken
628
- ? "JOLLY_SALEOR_APP_TOKEN is configured in .env."
629
- : "JOLLY_SALEOR_APP_TOKEN is not configured.",
630
- },
815
+ description: hasAppToken ? "JOLLY_SALEOR_APP_TOKEN is configured in .env." : "JOLLY_SALEOR_APP_TOKEN is not configured."
816
+ }
631
817
  ];
632
-
633
818
  return envelope({
634
819
  command,
635
820
  status: "success",
636
- summary: hasCloudToken
637
- ? `Saleor Cloud authentication is configured (account context: ${accountContext}).`
638
- : "Saleor Cloud authentication is not configured.",
821
+ summary: hasCloudToken ? `Saleor Cloud authentication is configured (account context: ${accountContext}).` : "Saleor Cloud authentication is not configured.",
639
822
  data: {
640
823
  hasCloudToken,
641
824
  hasAppToken,
642
- accountContext,
825
+ accountContext
643
826
  },
644
827
  checks,
645
- nextSteps: hasCloudToken
646
- ? []
647
- : [
648
- {
649
- description: "Run jolly login to configure Saleor Cloud authentication.",
650
- command: "jolly login --token <value>",
651
- },
652
- ],
828
+ nextSteps: hasCloudToken ? [] : [
829
+ {
830
+ description: "Run jolly login to configure Saleor Cloud authentication.",
831
+ command: "jolly login --token <value>"
832
+ }
833
+ ]
653
834
  });
654
835
  }
655
-
656
- // ─── create store (features 012/024) ──────────────────────────────────────
657
-
658
- function createStoreRiskContext(target: unknown, dryRunAvailable = true): RiskContext {
836
+ function createStoreRiskContext(target, dryRunAvailable = true) {
659
837
  return {
660
838
  action: "create store",
661
839
  target,
@@ -664,17 +842,14 @@ function createStoreRiskContext(target: unknown, dryRunAvailable = true): RiskCo
664
842
  reversible: false,
665
843
  sideEffects: [
666
844
  "Creates a Saleor Cloud project and/or environment",
667
- "Writes NEXT_PUBLIC_SALEOR_API_URL and JOLLY_SALEOR_APP_TOKEN to .env",
845
+ "Writes NEXT_PUBLIC_SALEOR_API_URL and JOLLY_SALEOR_APP_TOKEN to .env"
668
846
  ],
669
- dryRunAvailable,
847
+ dryRunAvailable
670
848
  };
671
849
  }
672
-
673
- async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
850
+ async function commandCreateStore(args) {
674
851
  const command = "create store";
675
852
  const url = args.options["url"];
676
-
677
- // Mode 1: write a pasted Saleor URL to .env (feature 012). -------------
678
853
  if (url && !args.flags.has("create-environment")) {
679
854
  const normalized = normalizeSaleorUrl(url);
680
855
  if (!normalized.endpoint) {
@@ -685,13 +860,12 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
685
860
  {
686
861
  code: "INVALID_SALEOR_URL",
687
862
  message: normalized.clarification ?? "Unrecognized Saleor URL.",
688
- remediation: "Paste a Saleor Dashboard, GraphQL, or root Saleor Cloud URL.",
689
- },
863
+ remediation: "Paste a Saleor Dashboard, GraphQL, or root Saleor Cloud URL."
864
+ }
690
865
  ],
691
- { data: { riskContext: createStoreRiskContext(url) } },
866
+ { data: { riskContext: createStoreRiskContext(url) } }
692
867
  );
693
868
  }
694
-
695
869
  if (args.dryRun) {
696
870
  return envelope({
697
871
  command,
@@ -700,33 +874,22 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
700
874
  data: {
701
875
  dryRun: true,
702
876
  normalizedUrl: normalized.endpoint,
703
- riskContext: createStoreRiskContext(normalized.endpoint),
877
+ riskContext: createStoreRiskContext(normalized.endpoint)
704
878
  },
705
879
  nextSteps: [
706
880
  {
707
881
  description: "Run the command without --dry-run to write the endpoint to .env.",
708
- command: `jolly create store --url ${normalized.endpoint}`,
709
- },
710
- ],
882
+ command: `jolly create store --url ${normalized.endpoint}`
883
+ }
884
+ ]
711
885
  });
712
886
  }
713
-
714
- // Collision guard (feature 022): if .env already carries a DIFFERENT
715
- // endpoint Jolly is being asked to overwrite, pause and ask rather than
716
- // silently replacing state Jolly did not create. The agent decides via
717
- // the feature 021 riskContext; --yes is its explicit go-ahead.
718
887
  const existingEndpoint = loadEnvValues(projectDir())["NEXT_PUBLIC_SALEOR_API_URL"];
719
- if (
720
- existingEndpoint &&
721
- existingEndpoint !== normalized.endpoint &&
722
- !args.flags.has("yes")
723
- ) {
888
+ if (existingEndpoint && existingEndpoint !== normalized.endpoint && !args.flags.has("yes")) {
724
889
  return envelope({
725
890
  command,
726
891
  status: "warning",
727
- summary:
728
- "A different NEXT_PUBLIC_SALEOR_API_URL already exists in .env; " +
729
- "Jolly paused instead of overwriting it. Re-run with --yes to replace it.",
892
+ summary: "A different NEXT_PUBLIC_SALEOR_API_URL already exists in .env; Jolly paused instead of overwriting it. Re-run with --yes to replace it.",
730
893
  data: {
731
894
  collision: true,
732
895
  existingEndpoint,
@@ -738,29 +901,26 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
738
901
  categories: ["destructive operations", "production configuration changes"],
739
902
  reversible: false,
740
903
  sideEffects: [
741
- `Replaces the existing endpoint "${existingEndpoint}" with "${normalized.endpoint}"`,
904
+ `Replaces the existing endpoint "${existingEndpoint}" with "${normalized.endpoint}"`
742
905
  ],
743
- dryRunAvailable: true,
744
- },
906
+ dryRunAvailable: true
907
+ }
745
908
  },
746
909
  checks: [
747
910
  {
748
911
  id: "saleor-endpoint-collision",
749
912
  status: "warning",
750
- description:
751
- "An existing NEXT_PUBLIC_SALEOR_API_URL would be overwritten; not replaced without --yes.",
752
- },
913
+ description: "An existing NEXT_PUBLIC_SALEOR_API_URL would be overwritten; not replaced without --yes."
914
+ }
753
915
  ],
754
916
  nextSteps: [
755
917
  {
756
- description:
757
- "Re-run with --yes to overwrite the existing endpoint (the agent decides).",
758
- command: `jolly create store --url ${normalized.endpoint} --yes`,
759
- },
760
- ],
918
+ description: "Re-run with --yes to overwrite the existing endpoint (the agent decides).",
919
+ command: `jolly create store --url ${normalized.endpoint} --yes`
920
+ }
921
+ ]
761
922
  });
762
923
  }
763
-
764
924
  writeEnvValues(projectDir(), { NEXT_PUBLIC_SALEOR_API_URL: normalized.endpoint });
765
925
  return envelope({
766
926
  command,
@@ -769,31 +929,28 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
769
929
  data: {
770
930
  stored: true,
771
931
  envVar: "NEXT_PUBLIC_SALEOR_API_URL",
772
- riskContext: createStoreRiskContext(normalized.endpoint),
932
+ riskContext: createStoreRiskContext(normalized.endpoint)
773
933
  },
774
934
  checks: [
775
935
  {
776
936
  id: "saleor-endpoint-stored",
777
937
  status: "pass",
778
- description: "NEXT_PUBLIC_SALEOR_API_URL written to .env.",
779
- },
938
+ description: "NEXT_PUBLIC_SALEOR_API_URL written to .env."
939
+ }
780
940
  ],
781
941
  nextSteps: [
782
942
  {
783
943
  description: "Run jolly create app-token to acquire a Saleor app token.",
784
- command: "jolly create app-token",
785
- },
786
- ],
944
+ command: "jolly create app-token"
945
+ }
946
+ ]
787
947
  });
788
948
  }
789
-
790
- // Mode 2: provision a Saleor Cloud environment via the Cloud API. ------
791
949
  const token = process.env["JOLLY_SALEOR_CLOUD_TOKEN"];
792
950
  const region = args.options["region"] ?? "us-east-1";
793
951
  const orgOverride = args.options["organization"];
794
952
  const name = args.options["name"];
795
953
  const domainLabel = args.options["domain-label"];
796
-
797
954
  if (!token) {
798
955
  return errorEnvelope(
799
956
  command,
@@ -802,32 +959,27 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
802
959
  {
803
960
  code: "MISSING_CLOUD_TOKEN",
804
961
  message: "JOLLY_SALEOR_CLOUD_TOKEN is required to create a Saleor Cloud store.",
805
- remediation: "Run `jolly login --token <value>` first.",
806
- },
962
+ remediation: "Run `jolly login --token <value>` first."
963
+ }
807
964
  ],
808
965
  {
809
966
  data: {
810
- riskContext: createStoreRiskContext(`${cloudApiBase()} (organization unresolved)`),
967
+ riskContext: createStoreRiskContext(`${cloudApiBase()} (organization unresolved)`)
811
968
  },
812
969
  nextSteps: [
813
970
  {
814
971
  description: "Run jolly login to acquire a Saleor Cloud token.",
815
- command: "jolly login --token <value>",
816
- },
817
- ],
818
- },
972
+ command: "jolly login --token <value>"
973
+ }
974
+ ]
975
+ }
819
976
  );
820
977
  }
821
-
822
- // Resolve the organization. --mock-organizations injects a deterministic
823
- // org list for the @logic multi-org warning scenario (no network).
824
- let orgs: CloudOrganization[];
825
- const mock = args.flags.has("mock-organizations")
826
- ? ""
827
- : (args.options["mock-organizations"] ?? undefined);
828
- if (mock !== undefined) {
978
+ let orgs;
979
+ const mock = args.flags.has("mock-organizations") ? "" : args.options["mock-organizations"] ?? void 0;
980
+ if (mock !== void 0) {
829
981
  orgs = (mock.length > 0 ? mock.split(",") : ["org-one", "org-two"]).map((slug) => ({
830
- slug: slug.trim(),
982
+ slug: slug.trim()
831
983
  }));
832
984
  } else {
833
985
  try {
@@ -836,8 +988,7 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
836
988
  return cloudErrorEnvelope(command, err, createStoreRiskContext(cloudApiBase()));
837
989
  }
838
990
  }
839
-
840
- let selectedOrg: string;
991
+ let selectedOrg;
841
992
  let multiOrgWarning = false;
842
993
  if (orgOverride) {
843
994
  selectedOrg = orgOverride;
@@ -849,10 +1000,10 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
849
1000
  {
850
1001
  code: "NO_ORGANIZATIONS",
851
1002
  message: "No organizations are accessible with this Cloud token.",
852
- remediation: "Confirm the token's permissions at https://cloud.saleor.io/tokens.",
853
- },
1003
+ remediation: "Confirm the token's permissions at https://cloud.saleor.io/tokens."
1004
+ }
854
1005
  ],
855
- { data: { riskContext: createStoreRiskContext(cloudApiBase()) } },
1006
+ { data: { riskContext: createStoreRiskContext(cloudApiBase()) } }
856
1007
  );
857
1008
  } else if (orgs.length === 1) {
858
1009
  selectedOrg = orgs[0].slug;
@@ -860,12 +1011,9 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
860
1011
  selectedOrg = orgs[0].slug;
861
1012
  multiOrgWarning = true;
862
1013
  }
863
-
864
1014
  const resolvedTarget = `${cloudApiBase()}/organizations/${selectedOrg}/environments/`;
865
1015
  const effectiveName = name ?? "jolly-store";
866
1016
  const effectiveDomainLabel = domainLabel ?? effectiveName;
867
-
868
- // --dry-run: show the real resolved request, write nothing. -----------
869
1017
  if (args.dryRun) {
870
1018
  const requestBody = {
871
1019
  name: effectiveName,
@@ -873,14 +1021,12 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
873
1021
  domain_label: effectiveDomainLabel,
874
1022
  database_population: "sample",
875
1023
  service: "saleor",
876
- region,
1024
+ region
877
1025
  };
878
1026
  const env = envelope({
879
1027
  command,
880
1028
  status: multiOrgWarning ? "warning" : "success",
881
- summary: multiOrgWarning
882
- ? `Previewed environment creation in "${selectedOrg}" (token has multiple organizations).`
883
- : `Previewed environment creation in organization "${selectedOrg}".`,
1029
+ summary: multiOrgWarning ? `Previewed environment creation in "${selectedOrg}" (token has multiple organizations).` : `Previewed environment creation in organization "${selectedOrg}".`,
884
1030
  data: {
885
1031
  dryRun: true,
886
1032
  method: "POST",
@@ -890,14 +1036,14 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
890
1036
  region,
891
1037
  databaseTemplate: "sample",
892
1038
  requestBody,
893
- riskContext: createStoreRiskContext(resolvedTarget),
1039
+ riskContext: createStoreRiskContext(resolvedTarget)
894
1040
  },
895
1041
  nextSteps: [
896
1042
  {
897
1043
  description: "Run the command without --dry-run to create the environment.",
898
- command: "jolly create store --create-environment",
899
- },
900
- ],
1044
+ command: "jolly create store --create-environment"
1045
+ }
1046
+ ]
901
1047
  });
902
1048
  if (multiOrgWarning) {
903
1049
  env.data["availableOrganizations"] = orgs.map((o) => o.slug);
@@ -905,9 +1051,6 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
905
1051
  }
906
1052
  return env;
907
1053
  }
908
-
909
- // Multi-org without --organization (non-dry-run): warn before proceeding
910
- // so the agent can re-run with the right org (feature 012).
911
1054
  if (multiOrgWarning) {
912
1055
  return envelope({
913
1056
  command,
@@ -916,32 +1059,28 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
916
1059
  data: {
917
1060
  availableOrganizations: orgs.map((o) => o.slug),
918
1061
  selectedOrganization: selectedOrg,
919
- riskContext: createStoreRiskContext(resolvedTarget),
1062
+ riskContext: createStoreRiskContext(resolvedTarget)
920
1063
  },
921
1064
  checks: [
922
1065
  {
923
1066
  id: "organization-selection",
924
1067
  status: "warning",
925
- description: `Selected "${selectedOrg}". Re-run with --organization <slug> if this is wrong.`,
926
- },
1068
+ description: `Selected "${selectedOrg}". Re-run with --organization <slug> if this is wrong.`
1069
+ }
927
1070
  ],
928
1071
  nextSteps: [
929
1072
  {
930
- description: `Re-run with --organization <slug> to choose explicitly. Available: ${orgs
931
- .map((o) => o.slug)
932
- .join(", ")}.`,
933
- command: `jolly create store --create-environment --organization ${selectedOrg}`,
934
- },
935
- ],
1073
+ description: `Re-run with --organization <slug> to choose explicitly. Available: ${orgs.map((o) => o.slug).join(", ")}.`,
1074
+ command: `jolly create store --create-environment --organization ${selectedOrg}`
1075
+ }
1076
+ ]
936
1077
  });
937
1078
  }
938
-
939
- // Real provisioning: create-or-reuse project, create env, poll, write .env
940
1079
  try {
941
1080
  const projects = await listProjects(token, selectedOrg);
942
1081
  const existingProject = projects.find((p) => p.name === effectiveName) ?? projects[0];
943
- let project: { name: string; slug?: string };
944
- let projectCreated: boolean;
1082
+ let project;
1083
+ let projectCreated;
945
1084
  if (existingProject) {
946
1085
  project = existingProject;
947
1086
  projectCreated = false;
@@ -949,24 +1088,20 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
949
1088
  project = await createProject(token, selectedOrg, {
950
1089
  name: effectiveName,
951
1090
  plan: "dev",
952
- region,
1091
+ region
953
1092
  });
954
1093
  projectCreated = true;
955
1094
  }
956
1095
  const projectSlug = project.slug ?? project.name;
957
-
958
- // Reuse an environment with our domain label if it already exists
959
- // (idempotency, feature 022).
960
1096
  const existingEnvs = await listEnvironments(token, selectedOrg);
961
1097
  const existingEnv = existingEnvs.find(
962
- (e) => e.domain_label === effectiveDomainLabel || e.name === effectiveName,
1098
+ (e) => e.domain_label === effectiveDomainLabel || e.name === effectiveName
963
1099
  );
964
-
965
- let domainUrl: string;
966
- let environmentCreated: boolean;
967
- let environment: { key?: unknown; name?: unknown };
1100
+ let domainUrl;
1101
+ let environmentCreated;
1102
+ let environment;
968
1103
  if (existingEnv) {
969
- domainUrl = extractDomainUrl(undefined, existingEnv, effectiveDomainLabel);
1104
+ domainUrl = extractDomainUrl(void 0, existingEnv, effectiveDomainLabel);
970
1105
  environmentCreated = false;
971
1106
  environment = existingEnv;
972
1107
  } else {
@@ -978,37 +1113,27 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
978
1113
  domain_label: effectiveDomainLabel,
979
1114
  database_population: "sample",
980
1115
  service,
981
- region,
1116
+ region
982
1117
  });
983
1118
  const taskId = created.task_id;
984
- let task = undefined;
1119
+ let task = void 0;
985
1120
  if (taskId) task = await pollTaskStatus(String(taskId));
986
- const refreshed = created.key
987
- ? await getEnvironment(token, selectedOrg, String(created.key))
988
- : created;
1121
+ const refreshed = created.key ? await getEnvironment(token, selectedOrg, String(created.key)) : created;
989
1122
  domainUrl = extractDomainUrl(task, refreshed, effectiveDomainLabel);
990
1123
  environmentCreated = true;
991
1124
  environment = refreshed ?? created;
992
1125
  }
993
- const environmentKey =
994
- typeof environment.key === "string" ? environment.key : undefined;
995
- const environmentName =
996
- typeof environment.name === "string" ? environment.name : effectiveName;
997
-
998
- const values: Record<string, string> = { NEXT_PUBLIC_SALEOR_API_URL: domainUrl };
999
-
1000
- // Acquire an app token against the new instance GraphQL endpoint.
1126
+ const environmentKey = typeof environment.key === "string" ? environment.key : void 0;
1127
+ const environmentName = typeof environment.name === "string" ? environment.name : effectiveName;
1128
+ const values = { NEXT_PUBLIC_SALEOR_API_URL: domainUrl };
1001
1129
  let appTokenStored = false;
1002
1130
  try {
1003
1131
  const appToken = await acquireAppToken(domainUrl, token, "Jolly Setup");
1004
1132
  values["JOLLY_SALEOR_APP_TOKEN"] = appToken;
1005
1133
  appTokenStored = true;
1006
1134
  } catch {
1007
- // Non-fatal: the env exists; the agent can run create app-token later.
1008
1135
  }
1009
-
1010
1136
  writeEnvValues(projectDir(), values);
1011
-
1012
1137
  return envelope({
1013
1138
  command,
1014
1139
  status: "success",
@@ -1017,45 +1142,38 @@ async function commandCreateStore(args: ParsedArgs): Promise<Envelope> {
1017
1142
  organization: selectedOrg,
1018
1143
  organizationSlug: selectedOrg,
1019
1144
  environmentName,
1020
- ...(environmentKey ? { environmentKey } : {}),
1145
+ ...environmentKey ? { environmentKey } : {},
1021
1146
  projectCreated,
1022
1147
  projectReused: !projectCreated,
1023
1148
  environmentCreated,
1024
1149
  graphqlEndpointStored: true,
1025
1150
  appTokenStored,
1026
- riskContext: createStoreRiskContext(resolvedTarget),
1151
+ riskContext: createStoreRiskContext(resolvedTarget)
1027
1152
  },
1028
1153
  checks: [
1029
1154
  {
1030
1155
  id: "environment-provisioned",
1031
1156
  status: "pass",
1032
- description: environmentCreated
1033
- ? "Environment created and verified via task status."
1034
- : "Existing environment reused.",
1157
+ description: environmentCreated ? "Environment created and verified via task status." : "Existing environment reused."
1035
1158
  },
1036
1159
  {
1037
1160
  id: "app-token-acquired",
1038
1161
  status: appTokenStored ? "pass" : "unknown",
1039
- description: appTokenStored
1040
- ? "App token acquired and stored."
1041
- : "App token not acquired; run jolly create app-token.",
1042
- },
1162
+ description: appTokenStored ? "App token acquired and stored." : "App token not acquired; run jolly create app-token."
1163
+ }
1043
1164
  ],
1044
- nextSteps: appTokenStored
1045
- ? []
1046
- : [
1047
- {
1048
- description: "Run jolly create app-token to acquire an app token.",
1049
- command: "jolly create app-token",
1050
- },
1051
- ],
1165
+ nextSteps: appTokenStored ? [] : [
1166
+ {
1167
+ description: "Run jolly create app-token to acquire an app token.",
1168
+ command: "jolly create app-token"
1169
+ }
1170
+ ]
1052
1171
  });
1053
1172
  } catch (err) {
1054
1173
  return cloudErrorEnvelope(command, err, createStoreRiskContext(resolvedTarget));
1055
1174
  }
1056
1175
  }
1057
-
1058
- function cloudErrorEnvelope(command: string, err: unknown, riskContext: RiskContext): Envelope {
1176
+ function cloudErrorEnvelope(command, err, riskContext) {
1059
1177
  const code = err instanceof CloudApiError ? err.code : "CLOUD_API_ERROR";
1060
1178
  const message = err instanceof Error ? err.message : String(err);
1061
1179
  return errorEnvelope(
@@ -1065,21 +1183,13 @@ function cloudErrorEnvelope(command: string, err: unknown, riskContext: RiskCont
1065
1183
  {
1066
1184
  code,
1067
1185
  message,
1068
- remediation:
1069
- code === "ENVIRONMENT_LIMIT_REACHED"
1070
- ? "Delete an unused environment or upgrade the plan, then re-run."
1071
- : code === "DOMAIN_LABEL_TAKEN"
1072
- ? "Choose a different domain label with --domain-label <label>."
1073
- : "Confirm the Cloud token and that the Cloud API is reachable.",
1074
- },
1186
+ remediation: code === "ENVIRONMENT_LIMIT_REACHED" ? "Delete an unused environment or upgrade the plan, then re-run." : code === "DOMAIN_LABEL_TAKEN" ? "Choose a different domain label with --domain-label <label>." : "Confirm the Cloud token and that the Cloud API is reachable."
1187
+ }
1075
1188
  ],
1076
- { data: { riskContext } },
1189
+ { data: { riskContext } }
1077
1190
  );
1078
1191
  }
1079
-
1080
- // ─── create app-token (feature 024) ───────────────────────────────────────
1081
-
1082
- function appTokenRiskContext(target: unknown): RiskContext {
1192
+ function appTokenRiskContext(target) {
1083
1193
  return {
1084
1194
  action: "create app-token",
1085
1195
  target,
@@ -1088,21 +1198,16 @@ function appTokenRiskContext(target: unknown): RiskContext {
1088
1198
  reversible: true,
1089
1199
  sideEffects: [
1090
1200
  "Creates a Saleor app token via GraphQL",
1091
- "Writes JOLLY_SALEOR_APP_TOKEN to .env",
1201
+ "Writes JOLLY_SALEOR_APP_TOKEN to .env"
1092
1202
  ],
1093
- dryRunAvailable: true,
1203
+ dryRunAvailable: true
1094
1204
  };
1095
1205
  }
1096
-
1097
- async function commandCreateAppToken(args: ParsedArgs): Promise<Envelope> {
1206
+ async function commandCreateAppToken(args) {
1098
1207
  const command = "create app-token";
1099
1208
  const token = process.env["JOLLY_SALEOR_CLOUD_TOKEN"];
1100
1209
  const values = loadEnvValues(projectDir());
1101
- const instanceUrl =
1102
- args.options["url"] ??
1103
- values["NEXT_PUBLIC_SALEOR_API_URL"] ??
1104
- process.env["NEXT_PUBLIC_SALEOR_API_URL"];
1105
-
1210
+ const instanceUrl = args.options["url"] ?? values["NEXT_PUBLIC_SALEOR_API_URL"] ?? process.env["NEXT_PUBLIC_SALEOR_API_URL"];
1106
1211
  if (args.dryRun) {
1107
1212
  return envelope({
1108
1213
  command,
@@ -1111,17 +1216,16 @@ async function commandCreateAppToken(args: ParsedArgs): Promise<Envelope> {
1111
1216
  data: {
1112
1217
  dryRun: true,
1113
1218
  instanceUrl: instanceUrl ?? null,
1114
- riskContext: appTokenRiskContext(instanceUrl ?? "unresolved Saleor GraphQL endpoint"),
1219
+ riskContext: appTokenRiskContext(instanceUrl ?? "unresolved Saleor GraphQL endpoint")
1115
1220
  },
1116
1221
  nextSteps: [
1117
1222
  {
1118
1223
  description: "Run the command without --dry-run to create and store the app token.",
1119
- command: "jolly create app-token",
1120
- },
1121
- ],
1224
+ command: "jolly create app-token"
1225
+ }
1226
+ ]
1122
1227
  });
1123
1228
  }
1124
-
1125
1229
  if (!token) {
1126
1230
  return errorEnvelope(
1127
1231
  command,
@@ -1130,13 +1234,12 @@ async function commandCreateAppToken(args: ParsedArgs): Promise<Envelope> {
1130
1234
  {
1131
1235
  code: "MISSING_CLOUD_TOKEN",
1132
1236
  message: "JOLLY_SALEOR_CLOUD_TOKEN is required to acquire an app token.",
1133
- remediation: "Run `jolly login --token <value>` first.",
1134
- },
1237
+ remediation: "Run `jolly login --token <value>` first."
1238
+ }
1135
1239
  ],
1136
- { data: { riskContext: appTokenRiskContext(instanceUrl ?? "unresolved") } },
1240
+ { data: { riskContext: appTokenRiskContext(instanceUrl ?? "unresolved") } }
1137
1241
  );
1138
1242
  }
1139
-
1140
1243
  if (!instanceUrl) {
1141
1244
  return errorEnvelope(
1142
1245
  command,
@@ -1145,13 +1248,12 @@ async function commandCreateAppToken(args: ParsedArgs): Promise<Envelope> {
1145
1248
  {
1146
1249
  code: "MISSING_INSTANCE_URL",
1147
1250
  message: "A Saleor GraphQL endpoint (NEXT_PUBLIC_SALEOR_API_URL) is required.",
1148
- remediation: "Run `jolly create store` first, or pass --url <graphql-endpoint>.",
1149
- },
1251
+ remediation: "Run `jolly create store` first, or pass --url <graphql-endpoint>."
1252
+ }
1150
1253
  ],
1151
- { data: { riskContext: appTokenRiskContext("unresolved") } },
1254
+ { data: { riskContext: appTokenRiskContext("unresolved") } }
1152
1255
  );
1153
1256
  }
1154
-
1155
1257
  try {
1156
1258
  const appToken = await acquireAppToken(instanceUrl, token, "Jolly Setup");
1157
1259
  writeEnvValues(projectDir(), { JOLLY_SALEOR_APP_TOKEN: appToken });
@@ -1162,15 +1264,15 @@ async function commandCreateAppToken(args: ParsedArgs): Promise<Envelope> {
1162
1264
  data: {
1163
1265
  appTokenStored: true,
1164
1266
  instanceUrl,
1165
- riskContext: appTokenRiskContext(instanceUrl),
1267
+ riskContext: appTokenRiskContext(instanceUrl)
1166
1268
  },
1167
1269
  checks: [
1168
1270
  {
1169
1271
  id: "app-token-acquired",
1170
1272
  status: "pass",
1171
- description: "App token created via GraphQL and stored.",
1172
- },
1173
- ],
1273
+ description: "App token created via GraphQL and stored."
1274
+ }
1275
+ ]
1174
1276
  });
1175
1277
  } catch (err) {
1176
1278
  const code = err instanceof CloudApiError ? err.code : "APP_TOKEN_ACQUISITION_FAILED";
@@ -1181,18 +1283,14 @@ async function commandCreateAppToken(args: ParsedArgs): Promise<Envelope> {
1181
1283
  {
1182
1284
  code,
1183
1285
  message: err instanceof Error ? err.message : String(err),
1184
- remediation:
1185
- "Confirm the instance is reachable and the Cloud token has access; or create an app in the Saleor Dashboard.",
1186
- },
1286
+ remediation: "Confirm the instance is reachable and the Cloud token has access; or create an app in the Saleor Dashboard."
1287
+ }
1187
1288
  ],
1188
- { data: { riskContext: appTokenRiskContext(instanceUrl) } },
1289
+ { data: { riskContext: appTokenRiskContext(instanceUrl) } }
1189
1290
  );
1190
1291
  }
1191
1292
  }
1192
-
1193
- // ─── create stripe (feature 005) ──────────────────────────────────────────
1194
-
1195
- function stripeRiskContext(): RiskContext {
1293
+ function stripeRiskContext() {
1196
1294
  return {
1197
1295
  action: "create stripe",
1198
1296
  target: ".env (JOLLY_STRIPE_PUBLISHABLE_KEY, JOLLY_STRIPE_SECRET_KEY)",
@@ -1200,15 +1298,13 @@ function stripeRiskContext(): RiskContext {
1200
1298
  categories: ["payment setup", "credential handling"],
1201
1299
  reversible: true,
1202
1300
  sideEffects: ["Writes Stripe test-mode keys to .env"],
1203
- dryRunAvailable: true,
1301
+ dryRunAvailable: true
1204
1302
  };
1205
1303
  }
1206
-
1207
- function commandCreateStripe(args: ParsedArgs): Envelope {
1304
+ function commandCreateStripe(args) {
1208
1305
  const command = "create stripe";
1209
1306
  const publishable = args.options["publishable-key"];
1210
1307
  const secret = args.options["secret-key"];
1211
-
1212
1308
  if (!publishable || !secret) {
1213
1309
  return errorEnvelope(
1214
1310
  command,
@@ -1217,13 +1313,12 @@ function commandCreateStripe(args: ParsedArgs): Envelope {
1217
1313
  {
1218
1314
  code: "MISSING_STRIPE_KEYS",
1219
1315
  message: "create stripe needs --publishable-key <pk_test_...> and --secret-key <sk_test_...>.",
1220
- remediation: "Copy both test-mode keys from the Stripe Dashboard and pass them as flags.",
1221
- },
1316
+ remediation: "Copy both test-mode keys from the Stripe Dashboard and pass them as flags."
1317
+ }
1222
1318
  ],
1223
- { data: { riskContext: stripeRiskContext() } },
1319
+ { data: { riskContext: stripeRiskContext() } }
1224
1320
  );
1225
1321
  }
1226
-
1227
1322
  if (args.dryRun) {
1228
1323
  return envelope({
1229
1324
  command,
@@ -1233,41 +1328,33 @@ function commandCreateStripe(args: ParsedArgs): Envelope {
1233
1328
  nextSteps: [
1234
1329
  {
1235
1330
  description: "Run the command without --dry-run to write the Stripe keys to .env.",
1236
- command: "jolly create stripe --publishable-key <pk> --secret-key <sk>",
1237
- },
1238
- ],
1331
+ command: "jolly create stripe --publishable-key <pk> --secret-key <sk>"
1332
+ }
1333
+ ]
1239
1334
  });
1240
1335
  }
1241
-
1242
1336
  writeEnvValues(projectDir(), {
1243
1337
  JOLLY_STRIPE_PUBLISHABLE_KEY: publishable,
1244
- JOLLY_STRIPE_SECRET_KEY: secret,
1338
+ JOLLY_STRIPE_SECRET_KEY: secret
1245
1339
  });
1246
-
1247
1340
  return envelope({
1248
1341
  command,
1249
1342
  status: "success",
1250
- summary:
1251
- "Stored Stripe test-mode keys as JOLLY_STRIPE_PUBLISHABLE_KEY and JOLLY_STRIPE_SECRET_KEY.",
1343
+ summary: "Stored Stripe test-mode keys as JOLLY_STRIPE_PUBLISHABLE_KEY and JOLLY_STRIPE_SECRET_KEY.",
1252
1344
  data: { stored: true, riskContext: stripeRiskContext() },
1253
1345
  checks: [
1254
- { id: "stripe-keys-stored", status: "pass", description: "Stripe test-mode keys written to .env." },
1346
+ { id: "stripe-keys-stored", status: "pass", description: "Stripe test-mode keys written to .env." }
1255
1347
  ],
1256
1348
  nextSteps: [
1257
1349
  {
1258
- description:
1259
- "Configure Saleor's Stripe integration via @saleor/configurator, guided by the Jolly skill.",
1260
- command: "jolly doctor stripe",
1261
- },
1262
- ],
1350
+ description: "Configure Saleor's Stripe integration via @saleor/configurator, guided by the Jolly skill.",
1351
+ command: "jolly doctor stripe"
1352
+ }
1353
+ ]
1263
1354
  });
1264
1355
  }
1265
-
1266
- // ─── create dispatcher + help ─────────────────────────────────────────────
1267
-
1268
- const CREATE_SUBCOMMANDS = ["store", "app-token", "stripe"] as const;
1269
-
1270
- function commandCreateHelp(): Envelope {
1356
+ var CREATE_SUBCOMMANDS = ["store", "app-token", "stripe"];
1357
+ function commandCreateHelp() {
1271
1358
  const command = "create --help";
1272
1359
  return envelope({
1273
1360
  command,
@@ -1277,26 +1364,25 @@ function commandCreateHelp(): Envelope {
1277
1364
  subcommands: [
1278
1365
  {
1279
1366
  name: "store",
1280
- description: "Provision a Saleor Cloud store/environment, or store a pasted Saleor URL.",
1367
+ description: "Provision a Saleor Cloud store/environment, or store a pasted Saleor URL."
1281
1368
  },
1282
1369
  {
1283
1370
  name: "app-token",
1284
- description: "Acquire a Saleor app token via GraphQL and write it to .env.",
1371
+ description: "Acquire a Saleor app token via GraphQL and write it to .env."
1285
1372
  },
1286
- { name: "stripe", description: "Write Stripe test-mode keys to .env." },
1373
+ { name: "stripe", description: "Write Stripe test-mode keys to .env." }
1287
1374
  ],
1288
- note: "Other setup work is run by your agent via the official CLIs, guided by the Jolly skill.",
1375
+ note: "Other setup work is run by your agent via the official CLIs, guided by the Jolly skill."
1289
1376
  },
1290
1377
  nextSteps: [
1291
1378
  {
1292
1379
  description: "Run jolly create store --create-environment to provision a Saleor Cloud environment.",
1293
- command: "jolly create store --create-environment",
1294
- },
1295
- ],
1380
+ command: "jolly create store --create-environment"
1381
+ }
1382
+ ]
1296
1383
  });
1297
1384
  }
1298
-
1299
- async function commandCreate(args: ParsedArgs): Promise<Envelope> {
1385
+ async function commandCreate(args) {
1300
1386
  const sub = args.positionals[1];
1301
1387
  if (!sub || args.help || sub === "help") {
1302
1388
  return commandCreateHelp();
@@ -1313,59 +1399,43 @@ async function commandCreate(args: ParsedArgs): Promise<Envelope> {
1313
1399
  {
1314
1400
  code: "UNKNOWN_CREATE_SUBCOMMAND",
1315
1401
  message: `"${sub}" is not a create subcommand. Valid: ${CREATE_SUBCOMMANDS.join(", ")}.`,
1316
- remediation: "Run `jolly create --help` to list available subcommands.",
1317
- },
1402
+ remediation: "Run `jolly create --help` to list available subcommands."
1403
+ }
1318
1404
  ]);
1319
1405
  }
1320
1406
  }
1321
-
1322
- // ─── init (feature 007) ───────────────────────────────────────────────────
1323
-
1324
- function installSkill(skill: SkillSpec): { installed: boolean; stderr?: string } {
1325
- // npx skills add <ref> — best effort; verification is on-disk below.
1407
+ function installSkill(skill) {
1326
1408
  const result = spawnSync("npx", ["--yes", "skills", "add", skill.ref], {
1327
1409
  cwd: projectDir(),
1328
1410
  encoding: "utf8",
1329
- timeout: 60_000,
1411
+ timeout: 6e4
1330
1412
  });
1331
- return { installed: result.status === 0, stderr: result.stderr ?? undefined };
1413
+ return { installed: result.status === 0, stderr: result.stderr ?? void 0 };
1332
1414
  }
1333
-
1334
- function mergeMcpJson(): { merged: boolean; warning?: string } {
1335
- const path = join(projectDir(), ".mcp.json");
1336
- const endpoint =
1337
- loadEnvValues(projectDir())["NEXT_PUBLIC_SALEOR_API_URL"] ??
1338
- process.env["NEXT_PUBLIC_SALEOR_API_URL"] ??
1339
- "https://your-store.saleor.cloud/graphql/";
1415
+ function mergeMcpJson() {
1416
+ const path = join2(projectDir(), ".mcp.json");
1417
+ const endpoint = loadEnvValues(projectDir())["NEXT_PUBLIC_SALEOR_API_URL"] ?? process.env["NEXT_PUBLIC_SALEOR_API_URL"] ?? "https://your-store.saleor.cloud/graphql/";
1340
1418
  const jollyEntry = {
1341
1419
  command: "npx",
1342
1420
  args: ["-y", "mcp-graphql"],
1343
- env: { ENDPOINT: endpoint },
1421
+ env: { ENDPOINT: endpoint }
1344
1422
  };
1345
-
1346
- let config: Record<string, unknown> = { mcpServers: {} };
1347
- if (existsSync(path)) {
1423
+ let config = { mcpServers: {} };
1424
+ if (existsSync2(path)) {
1348
1425
  try {
1349
- config = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
1426
+ config = JSON.parse(readFileSync2(path, "utf8"));
1350
1427
  } catch {
1351
- // Leave an unparseable file untouched and warn.
1352
1428
  return { merged: false, warning: "Existing .mcp.json is not valid JSON; left untouched." };
1353
1429
  }
1354
1430
  }
1355
- const servers = (
1356
- config["mcpServers"] && typeof config["mcpServers"] === "object"
1357
- ? (config["mcpServers"] as Record<string, unknown>)
1358
- : {}
1359
- ) as Record<string, unknown>;
1360
- // Merge: add our entry without removing user-authored servers.
1431
+ const servers = config["mcpServers"] && typeof config["mcpServers"] === "object" ? config["mcpServers"] : {};
1361
1432
  servers["saleor-graphql"] = jollyEntry;
1362
1433
  config["mcpServers"] = servers;
1363
- writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
1434
+ writeFileSync2(path, JSON.stringify(config, null, 2) + "\n");
1364
1435
  return { merged: true };
1365
1436
  }
1366
-
1367
- function mergeAgentsMd(): void {
1368
- const path = join(projectDir(), "AGENTS.md");
1437
+ function mergeAgentsMd() {
1438
+ const path = join2(projectDir(), "AGENTS.md");
1369
1439
  const begin = "<!-- jolly:begin -->";
1370
1440
  const end = "<!-- jolly:end -->";
1371
1441
  const section = `${begin}
@@ -1374,59 +1444,47 @@ function mergeAgentsMd(): void {
1374
1444
  This project uses Jolly to set up a Saleor storefront. Run \`jolly start\` to
1375
1445
  bootstrap, then follow the Jolly skill to drive the official CLIs.
1376
1446
  ${end}`;
1377
-
1378
- let existing = existsSync(path) ? readFileSync(path, "utf8") : "";
1447
+ let existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
1379
1448
  if (existing.includes(begin) && existing.includes(end)) {
1380
1449
  existing = existing.replace(new RegExp(`${begin}[\\s\\S]*?${end}`), section);
1381
1450
  } else {
1382
- existing =
1383
- existing.length > 0
1384
- ? `${existing.replace(/\n+$/, "")}\n\n${section}\n`
1385
- : `${section}\n`;
1451
+ existing = existing.length > 0 ? `${existing.replace(/\n+$/, "")}
1452
+
1453
+ ${section}
1454
+ ` : `${section}
1455
+ `;
1386
1456
  }
1387
- writeFileSync(path, existing);
1457
+ writeFileSync2(path, existing);
1388
1458
  }
1389
-
1390
- function commandInit(_args: ParsedArgs): Envelope {
1459
+ function commandInit(_args) {
1391
1460
  const command = "init";
1392
- const checks: Check[] = [];
1393
- const installFailures: string[] = [];
1394
-
1461
+ const checks = [];
1462
+ const installFailures = [];
1395
1463
  for (const skill of DEFAULT_SKILLS) {
1396
1464
  const already = skillInstalledOnDisk(skill);
1397
1465
  if (!already) {
1398
1466
  installSkill(skill);
1399
1467
  }
1400
- // Verify on disk — never unconditionally claim success.
1401
1468
  const present = skillInstalledOnDisk(skill);
1402
1469
  checks.push({
1403
1470
  id: `skill-${skill.id}`,
1404
1471
  status: present ? "pass" : "fail",
1405
- description: present
1406
- ? `${skill.id} present on disk${already ? " (already installed)" : ""}.`
1407
- : `${skill.id} could not be verified on disk after npx skills add.`,
1472
+ description: present ? `${skill.id} present on disk${already ? " (already installed)" : ""}.` : `${skill.id} could not be verified on disk after npx skills add.`
1408
1473
  });
1409
1474
  if (!present) installFailures.push(skill.id);
1410
1475
  }
1411
-
1412
- // Merge .mcp.json (local mcp-graphql against the customer endpoint).
1413
1476
  const mcp = mergeMcpJson();
1414
1477
  checks.push({
1415
1478
  id: "mcp-config",
1416
1479
  status: mcp.merged ? "pass" : "warning",
1417
- description: mcp.merged
1418
- ? "Merged saleor-graphql entry into .mcp.json."
1419
- : mcp.warning ?? "Could not merge .mcp.json.",
1480
+ description: mcp.merged ? "Merged saleor-graphql entry into .mcp.json." : mcp.warning ?? "Could not merge .mcp.json."
1420
1481
  });
1421
-
1422
- // Merge AGENTS.md guidance.
1423
1482
  mergeAgentsMd();
1424
1483
  checks.push({
1425
1484
  id: "agents-md",
1426
1485
  status: "pass",
1427
- description: "Merged the Jolly section into AGENTS.md.",
1486
+ description: "Merged the Jolly section into AGENTS.md."
1428
1487
  });
1429
-
1430
1488
  if (installFailures.length > 0) {
1431
1489
  return errorEnvelope(
1432
1490
  command,
@@ -1435,14 +1493,12 @@ function commandInit(_args: ParsedArgs): Envelope {
1435
1493
  {
1436
1494
  code: "SKILL_INSTALL_FAILED",
1437
1495
  message: `Failed to install or verify: ${installFailures.join(", ")}.`,
1438
- remediation:
1439
- "Ensure `npx skills` is available and the network is reachable, then re-run `jolly init`.",
1440
- },
1496
+ remediation: "Ensure `npx skills` is available and the network is reachable, then re-run `jolly init`."
1497
+ }
1441
1498
  ],
1442
- { checks },
1499
+ { checks }
1443
1500
  );
1444
1501
  }
1445
-
1446
1502
  return envelope({
1447
1503
  command,
1448
1504
  status: "success",
@@ -1450,51 +1506,39 @@ function commandInit(_args: ParsedArgs): Envelope {
1450
1506
  data: {
1451
1507
  skills: DEFAULT_SKILLS.map((s) => s.id),
1452
1508
  mcpMerged: mcp.merged,
1453
- agentsMdMerged: true,
1509
+ agentsMdMerged: true
1454
1510
  },
1455
1511
  checks,
1456
1512
  nextSteps: [
1457
1513
  {
1458
1514
  description: "Run jolly start to bootstrap setup and get the ordered playbook.",
1459
- command: "jolly start",
1460
- },
1461
- ],
1515
+ command: "jolly start"
1516
+ }
1517
+ ]
1462
1518
  });
1463
1519
  }
1464
-
1465
- // ─── doctor (feature 014) ─────────────────────────────────────────────────
1466
-
1467
- const DOCTOR_GROUPS = ["skills", "saleor", "storefront", "deployment", "stripe"] as const;
1468
-
1469
- function commandDoctor(args: ParsedArgs): Envelope {
1520
+ var DOCTOR_GROUPS = ["skills", "saleor", "storefront", "deployment", "stripe"];
1521
+ function commandDoctor(args) {
1470
1522
  const group = args.positionals[1];
1471
1523
  const values = loadEnvValues(projectDir());
1472
- const checks: Check[] = [];
1473
-
1474
- if (
1475
- group &&
1476
- !DOCTOR_GROUPS.includes(group as (typeof DOCTOR_GROUPS)[number])
1477
- ) {
1524
+ const checks = [];
1525
+ if (group && !DOCTOR_GROUPS.includes(group)) {
1478
1526
  return errorEnvelope("doctor", `Unknown doctor group "${group}".`, [
1479
1527
  {
1480
1528
  code: "UNKNOWN_DOCTOR_GROUP",
1481
1529
  message: `"${group}" is not a doctor group. Valid: ${DOCTOR_GROUPS.join(", ")}.`,
1482
- remediation: "Run `jolly doctor` for all checks or name a valid group.",
1483
- },
1530
+ remediation: "Run `jolly doctor` for all checks or name a valid group."
1531
+ }
1484
1532
  ]);
1485
1533
  }
1486
-
1487
- const wants = (g: string) => !group || group === g;
1488
-
1489
- // CLI availability (always reportable, read-only).
1534
+ const wants = (g) => !group || group === g;
1490
1535
  if (!group) {
1491
1536
  checks.push({
1492
1537
  id: "cli-available",
1493
1538
  status: "pass",
1494
- description: `Jolly CLI is available (Node ${process.versions.node}).`,
1539
+ description: `Jolly CLI is available (Node ${process.versions.node}).`
1495
1540
  });
1496
1541
  }
1497
-
1498
1542
  if (wants("skills")) {
1499
1543
  for (const skill of DEFAULT_SKILLS) {
1500
1544
  const present = skillInstalledOnDisk(skill);
@@ -1502,202 +1546,155 @@ function commandDoctor(args: ParsedArgs): Envelope {
1502
1546
  id: `skill-${skill.id}`,
1503
1547
  status: present ? "pass" : "fail",
1504
1548
  description: present ? `${skill.id} present.` : `${skill.id} not installed.`,
1505
- command: present ? undefined : "jolly init",
1549
+ command: present ? void 0 : "jolly init"
1506
1550
  });
1507
1551
  }
1508
1552
  }
1509
-
1510
1553
  if (wants("saleor")) {
1511
1554
  const hasCloud = Boolean(
1512
- values["JOLLY_SALEOR_CLOUD_TOKEN"] ?? process.env["JOLLY_SALEOR_CLOUD_TOKEN"],
1555
+ values["JOLLY_SALEOR_CLOUD_TOKEN"] ?? process.env["JOLLY_SALEOR_CLOUD_TOKEN"]
1513
1556
  );
1514
1557
  const hasEndpoint = Boolean(
1515
- values["NEXT_PUBLIC_SALEOR_API_URL"] ?? process.env["NEXT_PUBLIC_SALEOR_API_URL"],
1558
+ values["NEXT_PUBLIC_SALEOR_API_URL"] ?? process.env["NEXT_PUBLIC_SALEOR_API_URL"]
1516
1559
  );
1517
1560
  const hasApp = Boolean(
1518
- values["JOLLY_SALEOR_APP_TOKEN"] ?? process.env["JOLLY_SALEOR_APP_TOKEN"],
1561
+ values["JOLLY_SALEOR_APP_TOKEN"] ?? process.env["JOLLY_SALEOR_APP_TOKEN"]
1519
1562
  );
1520
1563
  checks.push({
1521
1564
  id: "saleor-cloud-token",
1522
1565
  status: hasCloud ? "pass" : "fail",
1523
1566
  description: hasCloud ? "JOLLY_SALEOR_CLOUD_TOKEN present." : "No Saleor Cloud token configured.",
1524
- command: hasCloud ? undefined : "jolly login --token <value>",
1567
+ command: hasCloud ? void 0 : "jolly login --token <value>"
1525
1568
  });
1526
1569
  checks.push({
1527
1570
  id: "saleor-endpoint",
1528
1571
  // Presence is detectable; live connectivity is a @sandbox concern, so
1529
1572
  // report "unknown" (not a fabricated pass) when present without probing.
1530
1573
  status: hasEndpoint ? "unknown" : "fail",
1531
- description: hasEndpoint
1532
- ? "NEXT_PUBLIC_SALEOR_API_URL is set; live connectivity not verified in this run."
1533
- : "No Saleor GraphQL endpoint configured.",
1534
- command: hasEndpoint ? undefined : "jolly create store --url <graphql-endpoint>",
1574
+ description: hasEndpoint ? "NEXT_PUBLIC_SALEOR_API_URL is set; live connectivity not verified in this run." : "No Saleor GraphQL endpoint configured.",
1575
+ command: hasEndpoint ? void 0 : "jolly create store --url <graphql-endpoint>"
1535
1576
  });
1536
1577
  checks.push({
1537
1578
  id: "saleor-app-token",
1538
1579
  status: hasApp ? "pass" : "fail",
1539
1580
  description: hasApp ? "JOLLY_SALEOR_APP_TOKEN present." : "No Saleor app token configured.",
1540
- command: hasApp ? undefined : "jolly create app-token",
1581
+ command: hasApp ? void 0 : "jolly create app-token"
1541
1582
  });
1542
1583
  }
1543
-
1544
1584
  if (wants("storefront")) {
1545
- const storefrontPresent =
1546
- existsSync(join(projectDir(), "package.json")) &&
1547
- existsSync(join(projectDir(), "src", "app"));
1548
- // Without a verified Paper storefront, report fail/unknown — never pass.
1585
+ const storefrontPresent = existsSync2(join2(projectDir(), "package.json")) && existsSync2(join2(projectDir(), "src", "app"));
1549
1586
  checks.push({
1550
1587
  id: "storefront-present",
1551
1588
  status: storefrontPresent ? "unknown" : "fail",
1552
- description: storefrontPresent
1553
- ? "A project structure exists; Paper storefront readiness not verified in this run."
1554
- : "No Paper storefront detected locally.",
1555
- command: storefrontPresent ? undefined : "Clone saleor/storefront (Paper) per the Jolly skill.",
1589
+ description: storefrontPresent ? "A project structure exists; Paper storefront readiness not verified in this run." : "No Paper storefront detected locally.",
1590
+ command: storefrontPresent ? void 0 : "Clone saleor/storefront (Paper) per the Jolly skill."
1556
1591
  });
1557
1592
  }
1558
-
1559
1593
  if (wants("deployment")) {
1560
- // Deployment is agent-run via the Vercel CLI; Jolly cannot verify it from
1561
- // its own first-party-host code, so report skipped (honest, not fail).
1562
1594
  checks.push({
1563
1595
  id: "deployment-status",
1564
1596
  status: "skipped",
1565
1597
  description: "Deployment is run by your agent via the Vercel CLI; Jolly does not contact Vercel.",
1566
- command: "npx vercel",
1598
+ command: "npx vercel"
1567
1599
  });
1568
1600
  }
1569
-
1570
1601
  if (wants("stripe")) {
1571
1602
  const hasPub = Boolean(
1572
- values["JOLLY_STRIPE_PUBLISHABLE_KEY"] ?? process.env["JOLLY_STRIPE_PUBLISHABLE_KEY"],
1603
+ values["JOLLY_STRIPE_PUBLISHABLE_KEY"] ?? process.env["JOLLY_STRIPE_PUBLISHABLE_KEY"]
1573
1604
  );
1574
1605
  const hasSecret = Boolean(
1575
- values["JOLLY_STRIPE_SECRET_KEY"] ?? process.env["JOLLY_STRIPE_SECRET_KEY"],
1606
+ values["JOLLY_STRIPE_SECRET_KEY"] ?? process.env["JOLLY_STRIPE_SECRET_KEY"]
1576
1607
  );
1577
1608
  checks.push({
1578
1609
  id: "stripe-keys",
1579
1610
  status: hasPub && hasSecret ? "pass" : "fail",
1580
- description:
1581
- hasPub && hasSecret ? "Stripe test-mode keys present in .env." : "Stripe keys not configured.",
1582
- command: hasPub && hasSecret ? undefined : "jolly create stripe --publishable-key <pk> --secret-key <sk>",
1611
+ description: hasPub && hasSecret ? "Stripe test-mode keys present in .env." : "Stripe keys not configured.",
1612
+ command: hasPub && hasSecret ? void 0 : "jolly create stripe --publishable-key <pk> --secret-key <sk>"
1583
1613
  });
1584
1614
  }
1585
-
1586
1615
  const hasFail = checks.some((c) => c.status === "fail");
1587
1616
  const hasWarn = checks.some((c) => c.status === "warning");
1588
- const status: EnvelopeStatus = hasFail ? "error" : hasWarn ? "warning" : "success";
1589
-
1590
- // Gather next steps from actionable checks.
1591
- const nextSteps: NextStep[] = checks
1592
- .filter((c) => (c.status === "fail" || c.status === "warning") && c.command)
1593
- .map((c) => ({ description: c.description ?? `Address ${c.id}.`, command: c.command }));
1594
-
1617
+ const status = hasFail ? "error" : hasWarn ? "warning" : "success";
1618
+ const nextSteps = checks.filter((c) => (c.status === "fail" || c.status === "warning") && c.command).map((c) => ({ description: c.description ?? `Address ${c.id}.`, command: c.command }));
1595
1619
  return envelope({
1596
1620
  command: group ? `doctor ${group}` : "doctor",
1597
1621
  status,
1598
- summary:
1599
- status === "success"
1600
- ? "All performed checks passed."
1601
- : status === "warning"
1602
- ? "Some checks need attention."
1603
- : "Some checks failed; see next steps.",
1622
+ summary: status === "success" ? "All performed checks passed." : status === "warning" ? "Some checks need attention." : "Some checks failed; see next steps.",
1604
1623
  data: { group: group ?? "all" },
1605
1624
  checks,
1606
1625
  nextSteps,
1607
- errors: hasFail
1608
- ? [
1609
- {
1610
- code: "DOCTOR_CHECKS_FAILED",
1611
- message: "One or more diagnostics failed.",
1612
- remediation: "Address the failing checks listed in nextSteps.",
1613
- },
1614
- ]
1615
- : [],
1626
+ errors: hasFail ? [
1627
+ {
1628
+ code: "DOCTOR_CHECKS_FAILED",
1629
+ message: "One or more diagnostics failed.",
1630
+ remediation: "Address the failing checks listed in nextSteps."
1631
+ }
1632
+ ] : []
1616
1633
  });
1617
1634
  }
1618
-
1619
- // ─── skills (feature 006/001) ─────────────────────────────────────────────
1620
-
1621
- function commandSkills(args: ParsedArgs): Envelope {
1635
+ function commandSkills(args) {
1622
1636
  const command = "skills";
1623
1637
  const sub = args.positionals[1];
1624
-
1625
1638
  if (sub === "install" || sub === "update") {
1626
- const checks: Check[] = DEFAULT_SKILLS.map((skill) => {
1639
+ const checks2 = DEFAULT_SKILLS.map((skill) => {
1627
1640
  const already = skillInstalledOnDisk(skill);
1628
1641
  if (!already && sub === "install") installSkill(skill);
1629
1642
  const present = skillInstalledOnDisk(skill);
1630
1643
  return {
1631
1644
  id: `skill-${skill.id}`,
1632
1645
  status: present ? "pass" : "fail",
1633
- description: present ? `${skill.id} present.` : `${skill.id} not verified on disk.`,
1646
+ description: present ? `${skill.id} present.` : `${skill.id} not verified on disk.`
1634
1647
  };
1635
1648
  });
1636
- const failed = checks.filter((c) => c.status === "fail").map((c) => c.id);
1649
+ const failed = checks2.filter((c) => c.status === "fail").map((c) => c.id);
1637
1650
  return envelope({
1638
1651
  command: `skills ${sub}`,
1639
1652
  status: failed.length > 0 ? "warning" : "success",
1640
- summary:
1641
- failed.length > 0
1642
- ? `Some skills not verified: ${failed.join(", ")}.`
1643
- : `Skills ${sub === "install" ? "installed" : "checked"}.`,
1653
+ summary: failed.length > 0 ? `Some skills not verified: ${failed.join(", ")}.` : `Skills ${sub === "install" ? "installed" : "checked"}.`,
1644
1654
  data: { skills: DEFAULT_SKILLS.map((s) => s.id) },
1645
- checks,
1655
+ checks: checks2
1646
1656
  });
1647
1657
  }
1648
-
1649
- // Default: list/inspect the skill set.
1650
- const checks: Check[] = DEFAULT_SKILLS.map((skill) => {
1658
+ const checks = DEFAULT_SKILLS.map((skill) => {
1651
1659
  const present = skillInstalledOnDisk(skill);
1652
1660
  return {
1653
1661
  id: `skill-${skill.id}`,
1654
1662
  status: present ? "pass" : "unknown",
1655
- description: `${skill.description}${present ? " (installed)" : " (not installed)"}.`,
1663
+ description: `${skill.description}${present ? " (installed)" : " (not installed)"}.`
1656
1664
  };
1657
1665
  });
1658
-
1659
1666
  return envelope({
1660
1667
  command,
1661
1668
  status: "success",
1662
1669
  summary: `Jolly manages ${DEFAULT_SKILLS.length} skills (install via npx skills add).`,
1663
1670
  data: {
1664
- skills: DEFAULT_SKILLS.map((s) => ({ id: s.id, ref: s.ref, description: s.description })),
1671
+ skills: DEFAULT_SKILLS.map((s) => ({ id: s.id, ref: s.ref, description: s.description }))
1665
1672
  },
1666
1673
  checks,
1667
1674
  nextSteps: [
1668
1675
  {
1669
1676
  description: "Run jolly init (or jolly start) to install the skill set.",
1670
- command: "jolly init",
1671
- },
1672
- ],
1677
+ command: "jolly init"
1678
+ }
1679
+ ]
1673
1680
  });
1674
1681
  }
1675
-
1676
- // ─── upgrade (feature 017) ────────────────────────────────────────────────
1677
-
1678
- function commandUpgrade(_args: ParsedArgs): Envelope {
1682
+ function commandUpgrade(_args) {
1679
1683
  const command = "upgrade";
1680
- const checks: Check[] = DEFAULT_SKILLS.map((skill) => {
1684
+ const checks = DEFAULT_SKILLS.map((skill) => {
1681
1685
  const present = skillInstalledOnDisk(skill);
1682
1686
  return {
1683
1687
  id: `skill-${skill.id}`,
1684
1688
  status: present ? "pass" : "skipped",
1685
- description: present
1686
- ? `${skill.id} is managed; checked for updates.`
1687
- : `${skill.id} not installed; skipped.`,
1689
+ description: present ? `${skill.id} is managed; checked for updates.` : `${skill.id} not installed; skipped.`
1688
1690
  };
1689
1691
  });
1690
-
1691
- // Detect a cloned Paper storefront for plan-only baseline guidance.
1692
- const paperPresent = existsSync(join(projectDir(), "paper-version.json"));
1692
+ const paperPresent = existsSync2(join2(projectDir(), "paper-version.json"));
1693
1693
  checks.push({
1694
1694
  id: "paper-baseline",
1695
1695
  status: paperPresent ? "unknown" : "skipped",
1696
- description: paperPresent
1697
- ? "Paper storefront detected; Jolly plans Paper migrations but does not auto-apply them in v1."
1698
- : "No Paper storefront detected; nothing to plan.",
1696
+ description: paperPresent ? "Paper storefront detected; Jolly plans Paper migrations but does not auto-apply them in v1." : "No Paper storefront detected; nothing to plan."
1699
1697
  });
1700
-
1701
1698
  return envelope({
1702
1699
  command,
1703
1700
  status: "success",
@@ -1705,29 +1702,13 @@ function commandUpgrade(_args: ParsedArgs): Envelope {
1705
1702
  data: {
1706
1703
  skillsChecked: DEFAULT_SKILLS.map((s) => s.id),
1707
1704
  paperBaselineDetected: paperPresent,
1708
- paperAutoApply: false,
1705
+ paperAutoApply: false
1709
1706
  },
1710
1707
  checks,
1711
- nextSteps: paperPresent
1712
- ? [{ description: "Review the Paper upgrade plan before applying any migration manually." }]
1713
- : [],
1708
+ nextSteps: paperPresent ? [{ description: "Review the Paper upgrade plan before applying any migration manually." }] : []
1714
1709
  });
1715
1710
  }
1716
-
1717
- // ─── start (features 001/006) ─────────────────────────────────────────────
1718
-
1719
- interface PlanStage {
1720
- stage: string;
1721
- effects: {
1722
- directoriesCreated: string[];
1723
- filesWritten: string[];
1724
- networkHostsContacted: string[];
1725
- repositoriesCloned: string[];
1726
- };
1727
- riskContext?: RiskContext;
1728
- }
1729
-
1730
- function startPlan(): PlanStage[] {
1711
+ function startPlan() {
1731
1712
  return [
1732
1713
  {
1733
1714
  stage: "init",
@@ -1735,7 +1716,7 @@ function startPlan(): PlanStage[] {
1735
1716
  directoriesCreated: [".claude/skills"],
1736
1717
  filesWritten: [".mcp.json", "AGENTS.md"],
1737
1718
  networkHostsContacted: ["github.com"],
1738
- repositoriesCloned: [],
1719
+ repositoriesCloned: []
1739
1720
  },
1740
1721
  riskContext: {
1741
1722
  action: "init",
@@ -1744,8 +1725,8 @@ function startPlan(): PlanStage[] {
1744
1725
  categories: [],
1745
1726
  reversible: true,
1746
1727
  sideEffects: ["Installs skills, writes .mcp.json and AGENTS.md"],
1747
- dryRunAvailable: true,
1748
- },
1728
+ dryRunAvailable: true
1729
+ }
1749
1730
  },
1750
1731
  {
1751
1732
  stage: "auth",
@@ -1753,7 +1734,7 @@ function startPlan(): PlanStage[] {
1753
1734
  directoriesCreated: [],
1754
1735
  filesWritten: [".env"],
1755
1736
  networkHostsContacted: ["cloud.saleor.io", "auth.saleor.io"],
1756
- repositoriesCloned: [],
1737
+ repositoriesCloned: []
1757
1738
  },
1758
1739
  riskContext: {
1759
1740
  action: "login",
@@ -1762,8 +1743,8 @@ function startPlan(): PlanStage[] {
1762
1743
  categories: ["credential handling"],
1763
1744
  reversible: true,
1764
1745
  sideEffects: ["Acquires and stores a Saleor Cloud token in .env"],
1765
- dryRunAvailable: true,
1766
- },
1746
+ dryRunAvailable: true
1747
+ }
1767
1748
  },
1768
1749
  {
1769
1750
  stage: "store",
@@ -1771,11 +1752,11 @@ function startPlan(): PlanStage[] {
1771
1752
  directoriesCreated: [],
1772
1753
  filesWritten: [".env"],
1773
1754
  networkHostsContacted: ["cloud.saleor.io"],
1774
- repositoriesCloned: [],
1755
+ repositoriesCloned: []
1775
1756
  },
1776
1757
  riskContext: createStoreRiskContext(
1777
- `${cloudApiBase()}/organizations/{organization}/environments/`,
1778
- ),
1758
+ `${cloudApiBase()}/organizations/{organization}/environments/`
1759
+ )
1779
1760
  },
1780
1761
  {
1781
1762
  stage: "storefront",
@@ -1783,17 +1764,17 @@ function startPlan(): PlanStage[] {
1783
1764
  directoriesCreated: ["storefront"],
1784
1765
  filesWritten: [],
1785
1766
  networkHostsContacted: ["github.com"],
1786
- repositoriesCloned: ["saleor/storefront"],
1767
+ repositoriesCloned: ["saleor/storefront"]
1787
1768
  },
1788
1769
  riskContext: {
1789
1770
  action: "clone storefront",
1790
- target: "saleor/storefront (Paper) storefront/",
1771
+ target: "saleor/storefront (Paper) \u2192 storefront/",
1791
1772
  riskLevel: "low",
1792
1773
  categories: [],
1793
1774
  reversible: true,
1794
1775
  sideEffects: ["Clones the Saleor Paper storefront repository into storefront/"],
1795
- dryRunAvailable: true,
1796
- },
1776
+ dryRunAvailable: true
1777
+ }
1797
1778
  },
1798
1779
  {
1799
1780
  stage: "deploy",
@@ -1801,44 +1782,42 @@ function startPlan(): PlanStage[] {
1801
1782
  directoriesCreated: [],
1802
1783
  filesWritten: [],
1803
1784
  networkHostsContacted: [],
1804
- repositoriesCloned: [],
1805
- },
1806
- },
1785
+ repositoriesCloned: []
1786
+ }
1787
+ }
1807
1788
  ];
1808
1789
  }
1809
-
1810
- function startPlaybook(): NextStep[] {
1790
+ function startPlaybook() {
1811
1791
  return [
1812
1792
  {
1813
1793
  description: "1. Bootstrap: jolly init installed skills, wrote .mcp.json, and ran doctor.",
1814
- command: "jolly init",
1794
+ command: "jolly init"
1815
1795
  },
1816
1796
  { description: "2. Authenticate Saleor Cloud.", command: "jolly login --token <value>" },
1817
1797
  {
1818
1798
  description: "3. Provision a Saleor Cloud store/environment.",
1819
- command: "jolly create store --create-environment",
1799
+ command: "jolly create store --create-environment"
1820
1800
  },
1821
1801
  { description: "4. Acquire a Saleor app token.", command: "jolly create app-token" },
1822
1802
  {
1823
1803
  description: "5. Clone the Paper storefront with git and install with pnpm, guided by the Jolly skill.",
1824
- command: "git clone https://github.com/saleor/storefront",
1804
+ command: "git clone https://github.com/saleor/storefront"
1825
1805
  },
1826
1806
  {
1827
- description: "6. Apply the Jolly starter recipe with @saleor/configurator, guided by the Jolly skill.",
1807
+ description: "6. Apply the Jolly starter recipe with @saleor/configurator, guided by the Jolly skill."
1828
1808
  },
1829
1809
  {
1830
1810
  description: "7. Deploy with the Vercel CLI under your own vercel login session.",
1831
- command: "npx vercel",
1811
+ command: "npx vercel"
1832
1812
  },
1833
1813
  {
1834
1814
  description: "8. Provide Stripe test-mode keys.",
1835
- command: "jolly create stripe --publishable-key <pk> --secret-key <sk>",
1815
+ command: "jolly create stripe --publishable-key <pk> --secret-key <sk>"
1836
1816
  },
1837
- { description: "9. Verify operational readiness.", command: "jolly doctor" },
1817
+ { description: "9. Verify operational readiness.", command: "jolly doctor" }
1838
1818
  ];
1839
1819
  }
1840
-
1841
- function commandStartDryRun(): Envelope {
1820
+ function commandStartDryRun() {
1842
1821
  const command = "start";
1843
1822
  const plan = startPlan();
1844
1823
  return envelope({
@@ -1847,79 +1826,63 @@ function commandStartDryRun(): Envelope {
1847
1826
  summary: "Previewed the jolly start plan. No files were written and no network requests were made.",
1848
1827
  data: {
1849
1828
  dryRun: true,
1850
- plan,
1829
+ plan
1851
1830
  },
1852
1831
  checks: [
1853
1832
  {
1854
1833
  id: "start-dry-run",
1855
1834
  status: "skipped",
1856
- description: "This is a dry-run preview; no stage was executed.",
1857
- },
1835
+ description: "This is a dry-run preview; no stage was executed."
1836
+ }
1858
1837
  ],
1859
1838
  nextSteps: [
1860
1839
  {
1861
1840
  description: "Run jolly start to execute the plan and get the ordered playbook.",
1862
- command: "jolly start",
1863
- },
1864
- ],
1841
+ command: "jolly start"
1842
+ }
1843
+ ]
1865
1844
  });
1866
1845
  }
1867
-
1868
- function commandStart(args: ParsedArgs): Envelope {
1846
+ function commandStart(args) {
1869
1847
  if (args.dryRun) return commandStartDryRun();
1870
-
1871
1848
  const command = "start";
1872
-
1873
- // Bootstrap: run init (real, on-disk) + run doctor (read-only). Never
1874
- // fabricate stages the agent must perform.
1875
1849
  const initEnv = commandInit(args);
1876
1850
  const doctorEnv = commandDoctor({
1877
1851
  ...args,
1878
1852
  positionals: ["doctor"],
1879
1853
  json: true,
1880
- dryRun: false,
1854
+ dryRun: false
1881
1855
  });
1882
-
1883
- const checks: Check[] = [
1856
+ const checks = [
1884
1857
  ...initEnv.checks.map((c) => ({ ...c, id: `init-${c.id}` })),
1885
- ...doctorEnv.checks.map((c) => ({ ...c, id: `doctor-${c.id}` })),
1858
+ ...doctorEnv.checks.map((c) => ({ ...c, id: `doctor-${c.id}` }))
1886
1859
  ];
1887
-
1888
- // start never reports overall "success" for an end-to-end flow it did not
1889
- // complete: bootstrap may succeed, but downstream agent stages are pending.
1890
1860
  const bootstrapFailed = initEnv.status === "error";
1891
- const status: EnvelopeStatus = bootstrapFailed ? "error" : "warning";
1892
-
1861
+ const status = bootstrapFailed ? "error" : "warning";
1893
1862
  return envelope({
1894
1863
  command,
1895
1864
  status,
1896
- summary: bootstrapFailed
1897
- ? "Bootstrap failed; see errors. No downstream stage was performed."
1898
- : "Bootstrap complete (skills, scaffold, doctor). Follow the playbook to finish setup.",
1865
+ summary: bootstrapFailed ? "Bootstrap failed; see errors. No downstream stage was performed." : "Bootstrap complete (skills, scaffold, doctor). Follow the playbook to finish setup.",
1899
1866
  data: {
1900
1867
  bootstrap: {
1901
1868
  skillsInstalled: !bootstrapFailed,
1902
1869
  mcpMerged: initEnv.data["mcpMerged"] ?? false,
1903
1870
  agentsMdMerged: initEnv.data["agentsMdMerged"] ?? false,
1904
- doctorRan: true,
1871
+ doctorRan: true
1905
1872
  },
1906
1873
  playbook: startPlaybook().map((s) => s.description),
1907
- pendingStages: ["storefront", "recipe", "deploy"],
1874
+ pendingStages: ["storefront", "recipe", "deploy"]
1908
1875
  },
1909
1876
  checks,
1910
1877
  nextSteps: startPlaybook(),
1911
- errors: bootstrapFailed ? initEnv.errors : [],
1878
+ errors: bootstrapFailed ? initEnv.errors : []
1912
1879
  });
1913
1880
  }
1914
-
1915
- // ─── top-level help ───────────────────────────────────────────────────────
1916
-
1917
- function commandHelp(): Envelope {
1881
+ function commandHelp() {
1918
1882
  return envelope({
1919
1883
  command: "help",
1920
1884
  status: "success",
1921
- summary:
1922
- "Jolly — Ahoy, agent. Go build a store. (a tool by Dmytri Kleiner; not an official Saleor/Vercel/Stripe product)",
1885
+ summary: "Jolly \u2014 Ahoy, agent. Go build a store. (a tool by Dmytri Kleiner; not an official Saleor/Vercel/Stripe product)",
1923
1886
  data: {
1924
1887
  commands: [
1925
1888
  "login",
@@ -1932,26 +1895,22 @@ function commandHelp(): Envelope {
1932
1895
  "skills",
1933
1896
  "create store",
1934
1897
  "create app-token",
1935
- "create stripe",
1898
+ "create stripe"
1936
1899
  ],
1937
- globalFlags: ["--json", "--quiet", "--yes/-y", "--dry-run"],
1900
+ globalFlags: ["--json", "--quiet", "--yes/-y", "--dry-run"]
1938
1901
  },
1939
1902
  nextSteps: [
1940
1903
  {
1941
1904
  description: "Run jolly start to bootstrap setup and get the ordered playbook.",
1942
- command: "jolly start",
1943
- },
1944
- ],
1905
+ command: "jolly start"
1906
+ }
1907
+ ]
1945
1908
  });
1946
1909
  }
1947
-
1948
- // ─── dispatch ─────────────────────────────────────────────────────────────
1949
-
1950
- async function dispatch(args: ParsedArgs): Promise<Envelope> {
1910
+ async function dispatch(args) {
1951
1911
  const cmd = args.positionals[0];
1952
-
1953
1912
  switch (cmd) {
1954
- case undefined:
1913
+ case void 0:
1955
1914
  case "help":
1956
1915
  return commandHelp();
1957
1916
  case "login":
@@ -1964,8 +1923,8 @@ async function dispatch(args: ParsedArgs): Promise<Envelope> {
1964
1923
  {
1965
1924
  code: "UNKNOWN_AUTH_SUBCOMMAND",
1966
1925
  message: 'The only auth subcommand is "status".',
1967
- remediation: "Run `jolly auth status`.",
1968
- },
1926
+ remediation: "Run `jolly auth status`."
1927
+ }
1969
1928
  ]);
1970
1929
  case "create":
1971
1930
  return commandCreate(args);
@@ -1984,15 +1943,14 @@ async function dispatch(args: ParsedArgs): Promise<Envelope> {
1984
1943
  {
1985
1944
  code: "UNKNOWN_COMMAND",
1986
1945
  message: `"${cmd}" is not a Jolly command.`,
1987
- remediation: "Run `jolly help` to list available commands.",
1988
- },
1946
+ remediation: "Run `jolly help` to list available commands."
1947
+ }
1989
1948
  ]);
1990
1949
  }
1991
1950
  }
1992
-
1993
- async function main(): Promise<void> {
1951
+ async function main() {
1994
1952
  const args = parseArgs(process.argv.slice(2));
1995
- let env: Envelope;
1953
+ let env;
1996
1954
  try {
1997
1955
  env = await dispatch(args);
1998
1956
  } catch (err) {
@@ -2000,12 +1958,11 @@ async function main(): Promise<void> {
2000
1958
  {
2001
1959
  code: "UNEXPECTED_ERROR",
2002
1960
  message: err instanceof Error ? err.message : String(err),
2003
- remediation: "Re-run with --json and report the error code.",
2004
- },
1961
+ remediation: "Re-run with --json and report the error code."
1962
+ }
2005
1963
  ]);
2006
1964
  }
2007
1965
  const exitCode = emit(env, args);
2008
1966
  process.exit(exitCode);
2009
1967
  }
2010
-
2011
1968
  void main();