@dk/jolly 0.2.1 → 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.
package/README.md CHANGED
@@ -40,10 +40,12 @@ Read `AGENTS.md` for Jolly-specific constraints and `HANDOVER.md` for current st
40
40
 
41
41
  ## Development
42
42
 
43
+ Requires Node.js >= 23 + npm.
44
+
43
45
  ```bash
44
- bun install
45
- bun test
46
- bun run test:logic
47
- bun run test:bdd
48
- bun run typecheck
46
+ npm install
47
+ npm test
48
+ npm run test:logic
49
+ npm run test:bdd
50
+ npm run typecheck
49
51
  ```
package/bin/jolly CHANGED
@@ -4,7 +4,8 @@
4
4
  // Jolly CLI launcher (feature 006, decision 2026-06-12): the published Jolly
5
5
  // CLI is a Node.js program. This launcher runs the pre-built JavaScript bundle
6
6
  // (`dist/index.js`, compiled from `src/`) under Node.js >= 23 and never invokes
7
- // or requires Bun (Bun is the project's development/build environment only).
7
+ // or requires Bun. (Bun was dropped project-wide on 2026-06-13: dev, test, CI,
8
+ // and the build all run on native Node >= 23 + npm.)
8
9
  //
9
10
  // The published package ships compiled JS, not raw TypeScript: Node's native
10
11
  // type stripping is disabled for files under `node_modules`, so an installed
package/dist/index.js CHANGED
@@ -13,10 +13,9 @@ function cloudApiBase() {
13
13
  }
14
14
  return DEFAULT_CLOUD_API_BASE;
15
15
  }
16
- var POLL_INTERVAL_MS = 5000;
17
- var POLL_TIMEOUT_MS = 480000;
18
-
19
- class CloudApiError extends Error {
16
+ var POLL_INTERVAL_MS = 5e3;
17
+ var POLL_TIMEOUT_MS = 48e4;
18
+ var CloudApiError = class extends Error {
20
19
  code;
21
20
  httpStatus;
22
21
  constructor(message, code, httpStatus) {
@@ -25,7 +24,7 @@ class CloudApiError extends Error {
25
24
  this.code = code;
26
25
  this.httpStatus = httpStatus;
27
26
  }
28
- }
27
+ };
29
28
  async function cloudFetch(url, token, options = {}) {
30
29
  return await fetch(url, {
31
30
  ...options,
@@ -39,60 +38,103 @@ async function cloudFetch(url, token, options = {}) {
39
38
  async function listOrganizations(token) {
40
39
  const response = await cloudFetch(`${cloudApiBase()}/organizations/`, token);
41
40
  if (!response.ok) {
42
- throw new CloudApiError(`Failed to list organizations: HTTP ${response.status} ${await response.text()}`, "CLOUD_API_ERROR", response.status);
41
+ throw new CloudApiError(
42
+ `Failed to list organizations: HTTP ${response.status} ${await response.text()}`,
43
+ "CLOUD_API_ERROR",
44
+ response.status
45
+ );
43
46
  }
44
47
  return await response.json();
45
48
  }
46
49
  async function listProjects(token, organizationSlug) {
47
- const response = await cloudFetch(`${cloudApiBase()}/organizations/${organizationSlug}/projects/`, token);
50
+ const response = await cloudFetch(
51
+ `${cloudApiBase()}/organizations/${organizationSlug}/projects/`,
52
+ token
53
+ );
48
54
  if (!response.ok) {
49
- throw new CloudApiError(`Failed to list projects: HTTP ${response.status} ${await response.text()}`, "CLOUD_API_ERROR", response.status);
55
+ throw new CloudApiError(
56
+ `Failed to list projects: HTTP ${response.status} ${await response.text()}`,
57
+ "CLOUD_API_ERROR",
58
+ response.status
59
+ );
50
60
  }
51
61
  return await response.json();
52
62
  }
53
63
  async function createProject(token, organizationSlug, body) {
54
- const response = await cloudFetch(`${cloudApiBase()}/organizations/${organizationSlug}/projects/`, token, { method: "POST", body: JSON.stringify(body) });
64
+ const response = await cloudFetch(
65
+ `${cloudApiBase()}/organizations/${organizationSlug}/projects/`,
66
+ token,
67
+ { method: "POST", body: JSON.stringify(body) }
68
+ );
55
69
  if (!response.ok) {
56
- throw new CloudApiError(`Failed to create project: HTTP ${response.status} ${await response.text()}`, "PROJECT_CREATE_FAILED", response.status);
70
+ throw new CloudApiError(
71
+ `Failed to create project: HTTP ${response.status} ${await response.text()}`,
72
+ "PROJECT_CREATE_FAILED",
73
+ response.status
74
+ );
57
75
  }
58
76
  return await response.json();
59
77
  }
60
78
  async function listProjectServices(token, organizationSlug, projectSlug) {
61
- const response = await cloudFetch(`${cloudApiBase()}/organizations/${organizationSlug}/projects/${projectSlug}/services/`, token);
62
- if (!response.ok)
63
- return [];
79
+ const response = await cloudFetch(
80
+ `${cloudApiBase()}/organizations/${organizationSlug}/projects/${projectSlug}/services/`,
81
+ token
82
+ );
83
+ if (!response.ok) return [];
64
84
  return await response.json();
65
85
  }
66
86
  function pickService(services, region = "us-east-1") {
67
- const sandbox = services.filter((s) => String(s.service_type ?? "").toUpperCase() === "SANDBOX");
87
+ const sandbox = services.filter(
88
+ (s) => String(s.service_type ?? "").toUpperCase() === "SANDBOX"
89
+ );
68
90
  const inRegion = sandbox.find((s) => s.region === region);
69
91
  const chosen = inRegion ?? sandbox[0] ?? services[0];
70
92
  return chosen?.name ?? "saleor";
71
93
  }
72
94
  async function createEnvironment(token, organizationSlug, body) {
73
- const response = await cloudFetch(`${cloudApiBase()}/organizations/${organizationSlug}/environments/`, token, { method: "POST", body: JSON.stringify(body) });
95
+ const response = await cloudFetch(
96
+ `${cloudApiBase()}/organizations/${organizationSlug}/environments/`,
97
+ token,
98
+ { method: "POST", body: JSON.stringify(body) }
99
+ );
74
100
  if (!response.ok) {
75
101
  const text = await response.text();
76
102
  if (response.status >= 400 && response.status < 500 && /domain/i.test(text) && /taken|exists|already|unique|in use|duplicate/i.test(text)) {
77
- throw new CloudApiError(`The Cloud API rejected the environment creation: the domain label ` + `"${body.domain_label}" is already taken (HTTP ${response.status}).`, "DOMAIN_LABEL_TAKEN", response.status);
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
+ );
78
108
  }
79
109
  if (response.status >= 400 && response.status < 500 && /limit|quota|exceed/i.test(text)) {
80
- throw new CloudApiError("The organization's sandbox environment limit is reached. " + "Delete an unused environment or upgrade the plan, then re-run " + "`jolly create store --create-environment`.", "ENVIRONMENT_LIMIT_REACHED", response.status);
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
+ );
81
115
  }
82
- throw new CloudApiError(`Failed to create environment: HTTP ${response.status} ${text}`, "ENVIRONMENT_CREATE_FAILED", response.status);
116
+ throw new CloudApiError(
117
+ `Failed to create environment: HTTP ${response.status} ${text}`,
118
+ "ENVIRONMENT_CREATE_FAILED",
119
+ response.status
120
+ );
83
121
  }
84
122
  return await response.json();
85
123
  }
86
124
  async function listEnvironments(token, organizationSlug) {
87
- const response = await cloudFetch(`${cloudApiBase()}/organizations/${organizationSlug}/environments/`, token);
88
- if (!response.ok)
89
- return [];
125
+ const response = await cloudFetch(
126
+ `${cloudApiBase()}/organizations/${organizationSlug}/environments/`,
127
+ token
128
+ );
129
+ if (!response.ok) return [];
90
130
  return await response.json();
91
131
  }
92
132
  async function getEnvironment(token, organizationSlug, environmentKey) {
93
- const response = await cloudFetch(`${cloudApiBase()}/organizations/${organizationSlug}/environments/${environmentKey}/`, token);
94
- if (!response.ok)
95
- return;
133
+ const response = await cloudFetch(
134
+ `${cloudApiBase()}/organizations/${organizationSlug}/environments/${environmentKey}/`,
135
+ token
136
+ );
137
+ if (!response.ok) return void 0;
96
138
  return await response.json();
97
139
  }
98
140
  function taskStatusUrl(taskId) {
@@ -101,22 +143,31 @@ function taskStatusUrl(taskId) {
101
143
  async function pollTaskStatus(taskId, timeoutMs = POLL_TIMEOUT_MS) {
102
144
  const deadline = Date.now() + timeoutMs;
103
145
  const url = taskStatusUrl(taskId);
104
- for (;; ) {
146
+ for (; ; ) {
105
147
  const response = await fetch(url, {
106
148
  headers: { "Content-Type": "application/json" }
107
149
  });
108
150
  if (!response.ok) {
109
- throw new CloudApiError(`Task status check failed: HTTP ${response.status} ${await response.text()}`, "TASK_STATUS_FAILED", response.status);
151
+ throw new CloudApiError(
152
+ `Task status check failed: HTTP ${response.status} ${await response.text()}`,
153
+ "TASK_STATUS_FAILED",
154
+ response.status
155
+ );
110
156
  }
111
157
  const task = await response.json();
112
158
  const status = String(task.status ?? "").toUpperCase();
113
- if (status === "SUCCEEDED")
114
- return task;
159
+ if (status === "SUCCEEDED") return task;
115
160
  if (status === "FAILED" || status === "ERROR") {
116
- throw new CloudApiError(`Environment provisioning task ${taskId} failed: ${JSON.stringify(task)}`, "TASK_FAILED");
161
+ throw new CloudApiError(
162
+ `Environment provisioning task ${taskId} failed: ${JSON.stringify(task)}`,
163
+ "TASK_FAILED"
164
+ );
117
165
  }
118
166
  if (Date.now() + POLL_INTERVAL_MS > deadline) {
119
- throw new CloudApiError(`Environment provisioning task ${taskId} did not reach SUCCEEDED within ${Math.round(timeoutMs / 1000)}s (last status: ${status || "unknown"})`, "TASK_TIMEOUT");
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
+ );
120
171
  }
121
172
  await sleep(POLL_INTERVAL_MS);
122
173
  }
@@ -147,61 +198,98 @@ async function graphqlFetch(graphqlUrl, token, query, variables) {
147
198
  body: JSON.stringify(variables ? { query, variables } : { query })
148
199
  });
149
200
  if (!response.ok) {
150
- throw new CloudApiError(`GraphQL request to the Saleor instance failed: HTTP ${response.status}`, "GRAPHQL_HTTP_ERROR", response.status);
201
+ throw new CloudApiError(
202
+ `GraphQL request to the Saleor instance failed: HTTP ${response.status}`,
203
+ "GRAPHQL_HTTP_ERROR",
204
+ response.status
205
+ );
151
206
  }
152
207
  const body = await response.json();
153
208
  if (body.errors) {
154
- throw new CloudApiError(`GraphQL errors: ${JSON.stringify(body.errors)}`, "GRAPHQL_ERROR");
209
+ throw new CloudApiError(
210
+ `GraphQL errors: ${JSON.stringify(body.errors)}`,
211
+ "GRAPHQL_ERROR"
212
+ );
155
213
  }
156
214
  return body.data ?? {};
157
215
  }
158
216
  async function queryGetApps(graphqlUrl, token) {
159
- const data = await graphqlFetch(graphqlUrl, token, `query GetApps { apps(first: 100) { edges { node { id name } } } }`);
217
+ const data = await graphqlFetch(
218
+ graphqlUrl,
219
+ token,
220
+ `query GetApps { apps(first: 100) { edges { node { id name } } } }`
221
+ );
160
222
  const apps = data.apps;
161
223
  const edges = apps?.edges ?? [];
162
224
  return edges.map((edge) => edge.node);
163
225
  }
164
226
  async function queryPermissionEnum(graphqlUrl, token) {
165
- const data = await graphqlFetch(graphqlUrl, token, `query { __type(name: "PermissionEnum") { enumValues { name } } }`);
227
+ const data = await graphqlFetch(
228
+ graphqlUrl,
229
+ token,
230
+ `query { __type(name: "PermissionEnum") { enumValues { name } } }`
231
+ );
166
232
  const type = data.__type;
167
233
  const values = type?.enumValues ?? [];
168
234
  return values.map((value) => String(value.name));
169
235
  }
170
236
  async function createAppToken(graphqlUrl, token, appId) {
171
- const data = await graphqlFetch(graphqlUrl, token, `mutation AppTokenCreate($app: ID!) {
237
+ const data = await graphqlFetch(
238
+ graphqlUrl,
239
+ token,
240
+ `mutation AppTokenCreate($app: ID!) {
172
241
  appTokenCreate(input: { app: $app }) {
173
242
  authToken
174
243
  errors { field message }
175
244
  }
176
- }`, { app: appId });
245
+ }`,
246
+ { app: appId }
247
+ );
177
248
  const result = data.appTokenCreate;
178
249
  const errors = result?.errors ?? [];
179
250
  if (errors.length > 0) {
180
- throw new CloudApiError(`appTokenCreate failed: ${errors.map((e) => e.message).join("; ")}`, "APP_TOKEN_CREATE_FAILED");
251
+ throw new CloudApiError(
252
+ `appTokenCreate failed: ${errors.map((e) => e.message).join("; ")}`,
253
+ "APP_TOKEN_CREATE_FAILED"
254
+ );
181
255
  }
182
256
  const authToken = result?.authToken;
183
257
  if (typeof authToken !== "string" || authToken.length === 0) {
184
- throw new CloudApiError("appTokenCreate did not return an authToken", "APP_TOKEN_CREATE_FAILED");
258
+ throw new CloudApiError(
259
+ "appTokenCreate did not return an authToken",
260
+ "APP_TOKEN_CREATE_FAILED"
261
+ );
185
262
  }
186
263
  return { authToken };
187
264
  }
188
265
  async function createLocalApp(graphqlUrl, token, name, permissions) {
189
- const data = await graphqlFetch(graphqlUrl, token, `mutation AppCreate($input: AppInput!) {
266
+ const data = await graphqlFetch(
267
+ graphqlUrl,
268
+ token,
269
+ `mutation AppCreate($input: AppInput!) {
190
270
  appCreate(input: $input) {
191
271
  authToken
192
272
  app { id name }
193
273
  errors { field message }
194
274
  }
195
- }`, { input: { name, permissions } });
275
+ }`,
276
+ { input: { name, permissions } }
277
+ );
196
278
  const result = data.appCreate;
197
279
  const errors = result?.errors ?? [];
198
280
  if (errors.length > 0) {
199
- throw new CloudApiError(`appCreate failed: ${errors.map((e) => e.message).join("; ")}`, "APP_CREATE_FAILED");
281
+ throw new CloudApiError(
282
+ `appCreate failed: ${errors.map((e) => e.message).join("; ")}`,
283
+ "APP_CREATE_FAILED"
284
+ );
200
285
  }
201
286
  const app = result?.app;
202
287
  const authToken = result?.authToken;
203
288
  if (typeof authToken !== "string" || authToken.length === 0) {
204
- throw new CloudApiError("appCreate did not return an authToken", "APP_CREATE_FAILED");
289
+ throw new CloudApiError(
290
+ "appCreate did not return an authToken",
291
+ "APP_CREATE_FAILED"
292
+ );
205
293
  }
206
294
  return { appId: String(app?.id ?? ""), authToken };
207
295
  }
@@ -212,18 +300,22 @@ async function acquireAppToken(graphqlUrl, token, appName) {
212
300
  return authToken2;
213
301
  }
214
302
  const permissions = await queryPermissionEnum(graphqlUrl, token);
215
- const { authToken } = await createLocalApp(graphqlUrl, token, appName, permissions);
303
+ const { authToken } = await createLocalApp(
304
+ graphqlUrl,
305
+ token,
306
+ appName,
307
+ permissions
308
+ );
216
309
  return authToken;
217
310
  }
218
- async function withRetries(fn, attempts = 5, delayMs = 5000) {
311
+ async function withRetries(fn, attempts = 5, delayMs = 5e3) {
219
312
  let lastError;
220
- for (let attempt = 0;attempt < attempts; attempt++) {
313
+ for (let attempt = 0; attempt < attempts; attempt++) {
221
314
  try {
222
315
  return await fn();
223
316
  } catch (error) {
224
317
  lastError = error;
225
- if (attempt < attempts - 1)
226
- await sleep(delayMs);
318
+ if (attempt < attempts - 1) await sleep(delayMs);
227
319
  }
228
320
  }
229
321
  throw lastError;
@@ -236,39 +328,31 @@ function sleep(ms) {
236
328
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
237
329
  import { join } from "node:path";
238
330
  var ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
239
- function loadEnvValues(projectDir) {
240
- const path = join(projectDir, ".env");
241
- if (!existsSync(path))
242
- return {};
331
+ function loadEnvValues(projectDir2) {
332
+ const path = join(projectDir2, ".env");
333
+ if (!existsSync(path)) return {};
243
334
  const values = {};
244
- for (const line of readFileSync(path, "utf8").split(`
245
- `)) {
335
+ for (const line of readFileSync(path, "utf8").split("\n")) {
246
336
  const match = ENV_LINE.exec(line);
247
- if (match)
248
- values[match[1]] = match[2];
337
+ if (match) values[match[1]] = match[2];
249
338
  }
250
339
  return values;
251
340
  }
252
- function ensureEnvIgnored(projectDir) {
253
- const path = join(projectDir, ".gitignore");
341
+ function ensureEnvIgnored(projectDir2) {
342
+ const path = join(projectDir2, ".gitignore");
254
343
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
255
- const alreadyIgnored = existing.split(`
256
- `).some((line) => line.trim() === ".env");
257
- if (alreadyIgnored)
258
- return;
259
- const prefix = existing.length > 0 && !existing.endsWith(`
260
- `) ? `${existing}
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}
261
347
  ` : existing;
262
348
  writeFileSync(path, `${prefix}.env
263
349
  `);
264
350
  }
265
- function writeEnvValues(projectDir, values) {
266
- ensureEnvIgnored(projectDir);
267
- const path = join(projectDir, ".env");
268
- const lines = existsSync(path) ? readFileSync(path, "utf8").split(`
269
- `) : [];
270
- while (lines.length > 0 && lines[lines.length - 1] === "")
271
- lines.pop();
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();
272
356
  const pending = { ...values };
273
357
  const updated = lines.map((line) => {
274
358
  const match = ENV_LINE.exec(line);
@@ -282,14 +366,13 @@ function writeEnvValues(projectDir, values) {
282
366
  for (const [name, value] of Object.entries(pending)) {
283
367
  updated.push(`${name}=${value}`);
284
368
  }
285
- writeFileSync(path, `${updated.join(`
286
- `)}
369
+ writeFileSync(path, `${updated.join("\n")}
287
370
  `);
288
- return loadEnvValues(projectDir);
371
+ return loadEnvValues(projectDir2);
289
372
  }
290
373
 
291
374
  // src/lib/saleor-url.ts
292
- 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)?";
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)?";
293
376
  function normalizeSaleorUrl(input) {
294
377
  let url;
295
378
  try {
@@ -309,7 +392,7 @@ function normalizeSaleorUrl(input) {
309
392
  }
310
393
 
311
394
  // src/index.ts
312
- var VALUE_FLAGS = new Set([
395
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
313
396
  "token",
314
397
  "url",
315
398
  "name",
@@ -323,24 +406,19 @@ var VALUE_FLAGS = new Set([
323
406
  function parseArgs(argv) {
324
407
  const positionals = [];
325
408
  const options = {};
326
- const flags = new Set;
409
+ const flags = /* @__PURE__ */ new Set();
327
410
  let json = false;
328
411
  let quiet = false;
329
412
  let yes = false;
330
413
  let dryRun = false;
331
414
  let help = false;
332
- for (let i = 0;i < argv.length; i++) {
415
+ for (let i = 0; i < argv.length; i++) {
333
416
  const arg = argv[i];
334
- if (arg === "--json")
335
- json = true;
336
- else if (arg === "--quiet")
337
- quiet = true;
338
- else if (arg === "--yes" || arg === "-y")
339
- yes = true;
340
- else if (arg === "--dry-run")
341
- dryRun = true;
342
- else if (arg === "--help" || arg === "-h")
343
- help = true;
417
+ if (arg === "--json") json = true;
418
+ else if (arg === "--quiet") quiet = true;
419
+ else if (arg === "--yes" || arg === "-y") yes = true;
420
+ else if (arg === "--dry-run") dryRun = true;
421
+ else if (arg === "--help" || arg === "-h") help = true;
344
422
  else if (arg.startsWith("--")) {
345
423
  const body = arg.slice(2);
346
424
  const eq = body.indexOf("=");
@@ -378,10 +456,8 @@ function errorEnvelope(command, summary, errors, extra = {}) {
378
456
  });
379
457
  }
380
458
  function statusGlyph(status) {
381
- if (status === "success")
382
- return "ok";
383
- if (status === "warning")
384
- return "warn";
459
+ if (status === "success") return "ok";
460
+ if (status === "warning") return "warn";
385
461
  return "error";
386
462
  }
387
463
  function checkGlyph(status) {
@@ -400,27 +476,27 @@ function checkGlyph(status) {
400
476
  }
401
477
  function emit(env, args) {
402
478
  if (args.json) {
403
- process.stdout.write(JSON.stringify(env) + `
404
- `);
479
+ process.stdout.write(JSON.stringify(env) + "\n");
405
480
  } else {
406
481
  const lines = [];
407
482
  lines.push(`jolly ${env.command}: [${statusGlyph(env.status)}] ${env.summary}`);
408
483
  if (!args.quiet) {
409
484
  for (const check of env.checks) {
410
- lines.push(` - [${checkGlyph(check.status)}] ${check.id}${check.description ? `: ${check.description}` : ""}`);
485
+ lines.push(
486
+ ` - [${checkGlyph(check.status)}] ${check.id}${check.description ? `: ${check.description}` : ""}`
487
+ );
411
488
  }
412
489
  for (const step of env.nextSteps) {
413
490
  lines.push(` next: ${step.description}${step.command ? ` (\`${step.command}\`)` : ""}`);
414
491
  }
415
492
  for (const err of env.errors) {
416
- lines.push(` error[${err.code}]: ${err.message}${err.remediation ? ` — ${err.remediation}` : ""}`);
493
+ lines.push(
494
+ ` error[${err.code}]: ${err.message}${err.remediation ? ` \u2014 ${err.remediation}` : ""}`
495
+ );
417
496
  }
418
497
  }
419
- process.stdout.write(lines.join(`
420
- `) + `
421
- `);
422
- process.stdout.write(JSON.stringify(env) + `
423
- `);
498
+ process.stdout.write(lines.join("\n") + "\n");
499
+ process.stdout.write(JSON.stringify(env) + "\n");
424
500
  }
425
501
  return env.status === "error" ? 1 : 0;
426
502
  }
@@ -465,30 +541,40 @@ async function commandLogin(args) {
465
541
  if (args.dryRun) {
466
542
  return loginBrowserDryRun(command);
467
543
  }
468
- return errorEnvelope(command, "Browser-based login is not available in this environment.", [
469
- {
470
- code: "BROWSER_LOGIN_UNAVAILABLE",
471
- message: "No native browser or Playwright callback flow is available to complete browser OAuth.",
472
- remediation: `Create a token at ${TOKEN_PAGE} and run \`jolly login --token <value>\`.`
473
- }
474
- ], { data: { riskContext: loginRiskContext() } });
544
+ return errorEnvelope(
545
+ command,
546
+ "Browser-based login is not available in this environment.",
547
+ [
548
+ {
549
+ code: "BROWSER_LOGIN_UNAVAILABLE",
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
+ }
553
+ ],
554
+ { data: { riskContext: loginRiskContext() } }
555
+ );
475
556
  }
476
557
  if (!token) {
477
- return errorEnvelope(command, "No token provided and browser login is not available here.", [
478
- {
479
- code: "NO_LOGIN_METHOD",
480
- message: "jolly login needs `--token <value>` in this environment (no browser/Playwright callback flow).",
481
- remediation: `Create a token at ${TOKEN_PAGE} and run \`jolly login --token <value>\`.`
482
- }
483
- ], {
484
- nextSteps: [
558
+ return errorEnvelope(
559
+ command,
560
+ "No token provided and browser login is not available here.",
561
+ [
485
562
  {
486
- description: `Create a Saleor Cloud token at ${TOKEN_PAGE}, then run jolly login --token <value>.`,
487
- command: "jolly login --token <value>"
563
+ code: "NO_LOGIN_METHOD",
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>\`.`
488
566
  }
489
567
  ],
490
- data: { riskContext: loginRiskContext() }
491
- });
568
+ {
569
+ nextSteps: [
570
+ {
571
+ description: `Create a Saleor Cloud token at ${TOKEN_PAGE}, then run jolly login --token <value>.`,
572
+ command: "jolly login --token <value>"
573
+ }
574
+ ],
575
+ data: { riskContext: loginRiskContext() }
576
+ }
577
+ );
492
578
  }
493
579
  if (args.dryRun) {
494
580
  return envelope({
@@ -512,32 +598,37 @@ async function commandLogin(args) {
512
598
  verificationFailure = err;
513
599
  }
514
600
  if (verificationFailure instanceof CloudApiError && (verificationFailure.httpStatus === 401 || verificationFailure.httpStatus === 403)) {
515
- return errorEnvelope(command, "The token was rejected by the Cloud API. Nothing was written.", [
516
- {
517
- code: "INVALID_TOKEN",
518
- message: "Saleor Cloud rejected the token (HTTP 401/403). It was not stored.",
519
- remediation: `Create a new token at ${TOKEN_PAGE} and try again.`
520
- }
521
- ], {
522
- checks: [
601
+ return errorEnvelope(
602
+ command,
603
+ "The token was rejected by the Cloud API. Nothing was written.",
604
+ [
523
605
  {
524
- id: "cloud-token-verification",
525
- status: "fail",
526
- description: "Token rejected by the Cloud API."
606
+ code: "INVALID_TOKEN",
607
+ message: "Saleor Cloud rejected the token (HTTP 401/403). It was not stored.",
608
+ remediation: `Create a new token at ${TOKEN_PAGE} and try again.`
527
609
  }
528
610
  ],
529
- data: { riskContext: loginRiskContext() },
530
- nextSteps: [
531
- { description: `Create a new token at ${TOKEN_PAGE}.`, command: `open ${TOKEN_PAGE}` }
532
- ]
533
- });
611
+ {
612
+ checks: [
613
+ {
614
+ id: "cloud-token-verification",
615
+ status: "fail",
616
+ description: "Token rejected by the Cloud API."
617
+ }
618
+ ],
619
+ data: { riskContext: loginRiskContext() },
620
+ nextSteps: [
621
+ { description: `Create a new token at ${TOKEN_PAGE}.`, command: `open ${TOKEN_PAGE}` }
622
+ ]
623
+ }
624
+ );
534
625
  }
535
626
  if (verificationFailure) {
536
627
  writeEnvValues(projectDir(), { JOLLY_SALEOR_CLOUD_TOKEN: token });
537
628
  return envelope({
538
629
  command,
539
630
  status: "warning",
540
- summary: "Token stored, not verified the Cloud API was unreachable.",
631
+ summary: "Token stored, not verified \u2014 the Cloud API was unreachable.",
541
632
  data: {
542
633
  cloudTokenStored: true,
543
634
  verified: false,
@@ -548,7 +639,7 @@ async function commandLogin(args) {
548
639
  {
549
640
  id: "cloud-token-verification",
550
641
  status: "unknown",
551
- description: "stored, not verified the Cloud API was unreachable."
642
+ description: "stored, not verified \u2014 the Cloud API was unreachable."
552
643
  }
553
644
  ],
554
645
  nextSteps: [
@@ -561,8 +652,7 @@ async function commandLogin(args) {
561
652
  }
562
653
  const orgName = resolveOrgName(orgs ?? []);
563
654
  const values = { JOLLY_SALEOR_CLOUD_TOKEN: token };
564
- if (orgName)
565
- values["JOLLY_SALEOR_ORGANIZATION"] = orgName;
655
+ if (orgName) values["JOLLY_SALEOR_ORGANIZATION"] = orgName;
566
656
  writeEnvValues(projectDir(), values);
567
657
  return envelope({
568
658
  command,
@@ -591,10 +681,9 @@ async function commandLogin(args) {
591
681
  }
592
682
  function resolveOrgName(orgs) {
593
683
  const first = orgs[0];
594
- if (!first)
595
- return;
684
+ if (!first) return void 0;
596
685
  const name = first.name ?? first.slug;
597
- return typeof name === "string" && name.length > 0 ? name : undefined;
686
+ return typeof name === "string" && name.length > 0 ? name : void 0;
598
687
  }
599
688
  function base64url(buf) {
600
689
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -672,8 +761,7 @@ function commandLogout(_args) {
672
761
  const removed = [];
673
762
  if (existsSync2(path)) {
674
763
  const lineRe = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
675
- const kept = readFileSync2(path, "utf8").split(`
676
- `).filter((line) => {
764
+ const kept = readFileSync2(path, "utf8").split("\n").filter((line) => {
677
765
  const m = lineRe.exec(line);
678
766
  if (m && MANAGED_AUTH_VARS.includes(m[1])) {
679
767
  removed.push(m[1]);
@@ -681,10 +769,8 @@ function commandLogout(_args) {
681
769
  }
682
770
  return true;
683
771
  });
684
- let text = kept.join(`
685
- `).replace(/\n+$/, "");
686
- text = text.length > 0 ? text + `
687
- ` : "";
772
+ let text = kept.join("\n").replace(/\n+$/, "");
773
+ text = text.length > 0 ? text + "\n" : "";
688
774
  writeFileSync2(path, text);
689
775
  }
690
776
  return envelope({
@@ -767,13 +853,18 @@ async function commandCreateStore(args) {
767
853
  if (url && !args.flags.has("create-environment")) {
768
854
  const normalized = normalizeSaleorUrl(url);
769
855
  if (!normalized.endpoint) {
770
- return errorEnvelope(command, "The provided URL could not be normalized to a Saleor GraphQL endpoint.", [
771
- {
772
- code: "INVALID_SALEOR_URL",
773
- message: normalized.clarification ?? "Unrecognized Saleor URL.",
774
- remediation: "Paste a Saleor Dashboard, GraphQL, or root Saleor Cloud URL."
775
- }
776
- ], { data: { riskContext: createStoreRiskContext(url) } });
856
+ return errorEnvelope(
857
+ command,
858
+ "The provided URL could not be normalized to a Saleor GraphQL endpoint.",
859
+ [
860
+ {
861
+ code: "INVALID_SALEOR_URL",
862
+ message: normalized.clarification ?? "Unrecognized Saleor URL.",
863
+ remediation: "Paste a Saleor Dashboard, GraphQL, or root Saleor Cloud URL."
864
+ }
865
+ ],
866
+ { data: { riskContext: createStoreRiskContext(url) } }
867
+ );
777
868
  }
778
869
  if (args.dryRun) {
779
870
  return envelope({
@@ -798,7 +889,7 @@ async function commandCreateStore(args) {
798
889
  return envelope({
799
890
  command,
800
891
  status: "warning",
801
- 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.",
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.",
802
893
  data: {
803
894
  collision: true,
804
895
  existingEndpoint,
@@ -861,27 +952,32 @@ async function commandCreateStore(args) {
861
952
  const name = args.options["name"];
862
953
  const domainLabel = args.options["domain-label"];
863
954
  if (!token) {
864
- return errorEnvelope(command, "No Saleor Cloud token is configured; cannot provision a store.", [
865
- {
866
- code: "MISSING_CLOUD_TOKEN",
867
- message: "JOLLY_SALEOR_CLOUD_TOKEN is required to create a Saleor Cloud store.",
868
- remediation: "Run `jolly login --token <value>` first."
869
- }
870
- ], {
871
- data: {
872
- riskContext: createStoreRiskContext(`${cloudApiBase()} (organization unresolved)`)
873
- },
874
- nextSteps: [
955
+ return errorEnvelope(
956
+ command,
957
+ "No Saleor Cloud token is configured; cannot provision a store.",
958
+ [
875
959
  {
876
- description: "Run jolly login to acquire a Saleor Cloud token.",
877
- command: "jolly login --token <value>"
960
+ code: "MISSING_CLOUD_TOKEN",
961
+ message: "JOLLY_SALEOR_CLOUD_TOKEN is required to create a Saleor Cloud store.",
962
+ remediation: "Run `jolly login --token <value>` first."
878
963
  }
879
- ]
880
- });
964
+ ],
965
+ {
966
+ data: {
967
+ riskContext: createStoreRiskContext(`${cloudApiBase()} (organization unresolved)`)
968
+ },
969
+ nextSteps: [
970
+ {
971
+ description: "Run jolly login to acquire a Saleor Cloud token.",
972
+ command: "jolly login --token <value>"
973
+ }
974
+ ]
975
+ }
976
+ );
881
977
  }
882
978
  let orgs;
883
- const mock = args.flags.has("mock-organizations") ? "" : args.options["mock-organizations"] ?? undefined;
884
- if (mock !== undefined) {
979
+ const mock = args.flags.has("mock-organizations") ? "" : args.options["mock-organizations"] ?? void 0;
980
+ if (mock !== void 0) {
885
981
  orgs = (mock.length > 0 ? mock.split(",") : ["org-one", "org-two"]).map((slug) => ({
886
982
  slug: slug.trim()
887
983
  }));
@@ -897,13 +993,18 @@ async function commandCreateStore(args) {
897
993
  if (orgOverride) {
898
994
  selectedOrg = orgOverride;
899
995
  } else if (orgs.length === 0) {
900
- return errorEnvelope(command, "The Cloud token has access to no organizations.", [
901
- {
902
- code: "NO_ORGANIZATIONS",
903
- message: "No organizations are accessible with this Cloud token.",
904
- remediation: "Confirm the token's permissions at https://cloud.saleor.io/tokens."
905
- }
906
- ], { data: { riskContext: createStoreRiskContext(cloudApiBase()) } });
996
+ return errorEnvelope(
997
+ command,
998
+ "The Cloud token has access to no organizations.",
999
+ [
1000
+ {
1001
+ code: "NO_ORGANIZATIONS",
1002
+ message: "No organizations are accessible with this Cloud token.",
1003
+ remediation: "Confirm the token's permissions at https://cloud.saleor.io/tokens."
1004
+ }
1005
+ ],
1006
+ { data: { riskContext: createStoreRiskContext(cloudApiBase()) } }
1007
+ );
907
1008
  } else if (orgs.length === 1) {
908
1009
  selectedOrg = orgs[0].slug;
909
1010
  } else {
@@ -993,12 +1094,14 @@ async function commandCreateStore(args) {
993
1094
  }
994
1095
  const projectSlug = project.slug ?? project.name;
995
1096
  const existingEnvs = await listEnvironments(token, selectedOrg);
996
- const existingEnv = existingEnvs.find((e) => e.domain_label === effectiveDomainLabel || e.name === effectiveName);
1097
+ const existingEnv = existingEnvs.find(
1098
+ (e) => e.domain_label === effectiveDomainLabel || e.name === effectiveName
1099
+ );
997
1100
  let domainUrl;
998
1101
  let environmentCreated;
999
1102
  let environment;
1000
1103
  if (existingEnv) {
1001
- domainUrl = extractDomainUrl(undefined, existingEnv, effectiveDomainLabel);
1104
+ domainUrl = extractDomainUrl(void 0, existingEnv, effectiveDomainLabel);
1002
1105
  environmentCreated = false;
1003
1106
  environment = existingEnv;
1004
1107
  } else {
@@ -1013,15 +1116,14 @@ async function commandCreateStore(args) {
1013
1116
  region
1014
1117
  });
1015
1118
  const taskId = created.task_id;
1016
- let task = undefined;
1017
- if (taskId)
1018
- task = await pollTaskStatus(String(taskId));
1119
+ let task = void 0;
1120
+ if (taskId) task = await pollTaskStatus(String(taskId));
1019
1121
  const refreshed = created.key ? await getEnvironment(token, selectedOrg, String(created.key)) : created;
1020
1122
  domainUrl = extractDomainUrl(task, refreshed, effectiveDomainLabel);
1021
1123
  environmentCreated = true;
1022
1124
  environment = refreshed ?? created;
1023
1125
  }
1024
- const environmentKey = typeof environment.key === "string" ? environment.key : undefined;
1126
+ const environmentKey = typeof environment.key === "string" ? environment.key : void 0;
1025
1127
  const environmentName = typeof environment.name === "string" ? environment.name : effectiveName;
1026
1128
  const values = { NEXT_PUBLIC_SALEOR_API_URL: domainUrl };
1027
1129
  let appTokenStored = false;
@@ -1029,7 +1131,8 @@ async function commandCreateStore(args) {
1029
1131
  const appToken = await acquireAppToken(domainUrl, token, "Jolly Setup");
1030
1132
  values["JOLLY_SALEOR_APP_TOKEN"] = appToken;
1031
1133
  appTokenStored = true;
1032
- } catch {}
1134
+ } catch {
1135
+ }
1033
1136
  writeEnvValues(projectDir(), values);
1034
1137
  return envelope({
1035
1138
  command,
@@ -1073,13 +1176,18 @@ async function commandCreateStore(args) {
1073
1176
  function cloudErrorEnvelope(command, err, riskContext) {
1074
1177
  const code = err instanceof CloudApiError ? err.code : "CLOUD_API_ERROR";
1075
1178
  const message = err instanceof Error ? err.message : String(err);
1076
- return errorEnvelope(command, "The Cloud API request failed. Nothing was created.", [
1077
- {
1078
- code,
1079
- message,
1080
- 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."
1081
- }
1082
- ], { data: { riskContext } });
1179
+ return errorEnvelope(
1180
+ command,
1181
+ "The Cloud API request failed. Nothing was created.",
1182
+ [
1183
+ {
1184
+ code,
1185
+ message,
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
+ }
1188
+ ],
1189
+ { data: { riskContext } }
1190
+ );
1083
1191
  }
1084
1192
  function appTokenRiskContext(target) {
1085
1193
  return {
@@ -1119,22 +1227,32 @@ async function commandCreateAppToken(args) {
1119
1227
  });
1120
1228
  }
1121
1229
  if (!token) {
1122
- return errorEnvelope(command, "No Saleor Cloud token is configured; cannot acquire an app token.", [
1123
- {
1124
- code: "MISSING_CLOUD_TOKEN",
1125
- message: "JOLLY_SALEOR_CLOUD_TOKEN is required to acquire an app token.",
1126
- remediation: "Run `jolly login --token <value>` first."
1127
- }
1128
- ], { data: { riskContext: appTokenRiskContext(instanceUrl ?? "unresolved") } });
1230
+ return errorEnvelope(
1231
+ command,
1232
+ "No Saleor Cloud token is configured; cannot acquire an app token.",
1233
+ [
1234
+ {
1235
+ code: "MISSING_CLOUD_TOKEN",
1236
+ message: "JOLLY_SALEOR_CLOUD_TOKEN is required to acquire an app token.",
1237
+ remediation: "Run `jolly login --token <value>` first."
1238
+ }
1239
+ ],
1240
+ { data: { riskContext: appTokenRiskContext(instanceUrl ?? "unresolved") } }
1241
+ );
1129
1242
  }
1130
1243
  if (!instanceUrl) {
1131
- return errorEnvelope(command, "No Saleor GraphQL instance URL is available.", [
1132
- {
1133
- code: "MISSING_INSTANCE_URL",
1134
- message: "A Saleor GraphQL endpoint (NEXT_PUBLIC_SALEOR_API_URL) is required.",
1135
- remediation: "Run `jolly create store` first, or pass --url <graphql-endpoint>."
1136
- }
1137
- ], { data: { riskContext: appTokenRiskContext("unresolved") } });
1244
+ return errorEnvelope(
1245
+ command,
1246
+ "No Saleor GraphQL instance URL is available.",
1247
+ [
1248
+ {
1249
+ code: "MISSING_INSTANCE_URL",
1250
+ message: "A Saleor GraphQL endpoint (NEXT_PUBLIC_SALEOR_API_URL) is required.",
1251
+ remediation: "Run `jolly create store` first, or pass --url <graphql-endpoint>."
1252
+ }
1253
+ ],
1254
+ { data: { riskContext: appTokenRiskContext("unresolved") } }
1255
+ );
1138
1256
  }
1139
1257
  try {
1140
1258
  const appToken = await acquireAppToken(instanceUrl, token, "Jolly Setup");
@@ -1158,13 +1276,18 @@ async function commandCreateAppToken(args) {
1158
1276
  });
1159
1277
  } catch (err) {
1160
1278
  const code = err instanceof CloudApiError ? err.code : "APP_TOKEN_ACQUISITION_FAILED";
1161
- return errorEnvelope(command, "Could not acquire an app token. Nothing was stored.", [
1162
- {
1163
- code,
1164
- message: err instanceof Error ? err.message : String(err),
1165
- remediation: "Confirm the instance is reachable and the Cloud token has access; or create an app in the Saleor Dashboard."
1166
- }
1167
- ], { data: { riskContext: appTokenRiskContext(instanceUrl) } });
1279
+ return errorEnvelope(
1280
+ command,
1281
+ "Could not acquire an app token. Nothing was stored.",
1282
+ [
1283
+ {
1284
+ code,
1285
+ message: err instanceof Error ? err.message : String(err),
1286
+ remediation: "Confirm the instance is reachable and the Cloud token has access; or create an app in the Saleor Dashboard."
1287
+ }
1288
+ ],
1289
+ { data: { riskContext: appTokenRiskContext(instanceUrl) } }
1290
+ );
1168
1291
  }
1169
1292
  }
1170
1293
  function stripeRiskContext() {
@@ -1183,13 +1306,18 @@ function commandCreateStripe(args) {
1183
1306
  const publishable = args.options["publishable-key"];
1184
1307
  const secret = args.options["secret-key"];
1185
1308
  if (!publishable || !secret) {
1186
- return errorEnvelope(command, "Both --publishable-key and --secret-key are required.", [
1187
- {
1188
- code: "MISSING_STRIPE_KEYS",
1189
- message: "create stripe needs --publishable-key <pk_test_...> and --secret-key <sk_test_...>.",
1190
- remediation: "Copy both test-mode keys from the Stripe Dashboard and pass them as flags."
1191
- }
1192
- ], { data: { riskContext: stripeRiskContext() } });
1309
+ return errorEnvelope(
1310
+ command,
1311
+ "Both --publishable-key and --secret-key are required.",
1312
+ [
1313
+ {
1314
+ code: "MISSING_STRIPE_KEYS",
1315
+ message: "create stripe needs --publishable-key <pk_test_...> and --secret-key <sk_test_...>.",
1316
+ remediation: "Copy both test-mode keys from the Stripe Dashboard and pass them as flags."
1317
+ }
1318
+ ],
1319
+ { data: { riskContext: stripeRiskContext() } }
1320
+ );
1193
1321
  }
1194
1322
  if (args.dryRun) {
1195
1323
  return envelope({
@@ -1280,9 +1408,9 @@ function installSkill(skill) {
1280
1408
  const result = spawnSync("npx", ["--yes", "skills", "add", skill.ref], {
1281
1409
  cwd: projectDir(),
1282
1410
  encoding: "utf8",
1283
- timeout: 60000
1411
+ timeout: 6e4
1284
1412
  });
1285
- return { installed: result.status === 0, stderr: result.stderr ?? undefined };
1413
+ return { installed: result.status === 0, stderr: result.stderr ?? void 0 };
1286
1414
  }
1287
1415
  function mergeMcpJson() {
1288
1416
  const path = join2(projectDir(), ".mcp.json");
@@ -1303,8 +1431,7 @@ function mergeMcpJson() {
1303
1431
  const servers = config["mcpServers"] && typeof config["mcpServers"] === "object" ? config["mcpServers"] : {};
1304
1432
  servers["saleor-graphql"] = jollyEntry;
1305
1433
  config["mcpServers"] = servers;
1306
- writeFileSync2(path, JSON.stringify(config, null, 2) + `
1307
- `);
1434
+ writeFileSync2(path, JSON.stringify(config, null, 2) + "\n");
1308
1435
  return { merged: true };
1309
1436
  }
1310
1437
  function mergeAgentsMd() {
@@ -1344,8 +1471,7 @@ function commandInit(_args) {
1344
1471
  status: present ? "pass" : "fail",
1345
1472
  description: present ? `${skill.id} present on disk${already ? " (already installed)" : ""}.` : `${skill.id} could not be verified on disk after npx skills add.`
1346
1473
  });
1347
- if (!present)
1348
- installFailures.push(skill.id);
1474
+ if (!present) installFailures.push(skill.id);
1349
1475
  }
1350
1476
  const mcp = mergeMcpJson();
1351
1477
  checks.push({
@@ -1360,13 +1486,18 @@ function commandInit(_args) {
1360
1486
  description: "Merged the Jolly section into AGENTS.md."
1361
1487
  });
1362
1488
  if (installFailures.length > 0) {
1363
- return errorEnvelope(command, `Some skills could not be verified on disk: ${installFailures.join(", ")}.`, [
1364
- {
1365
- code: "SKILL_INSTALL_FAILED",
1366
- message: `Failed to install or verify: ${installFailures.join(", ")}.`,
1367
- remediation: "Ensure `npx skills` is available and the network is reachable, then re-run `jolly init`."
1368
- }
1369
- ], { checks });
1489
+ return errorEnvelope(
1490
+ command,
1491
+ `Some skills could not be verified on disk: ${installFailures.join(", ")}.`,
1492
+ [
1493
+ {
1494
+ code: "SKILL_INSTALL_FAILED",
1495
+ message: `Failed to install or verify: ${installFailures.join(", ")}.`,
1496
+ remediation: "Ensure `npx skills` is available and the network is reachable, then re-run `jolly init`."
1497
+ }
1498
+ ],
1499
+ { checks }
1500
+ );
1370
1501
  }
1371
1502
  return envelope({
1372
1503
  command,
@@ -1415,31 +1546,39 @@ function commandDoctor(args) {
1415
1546
  id: `skill-${skill.id}`,
1416
1547
  status: present ? "pass" : "fail",
1417
1548
  description: present ? `${skill.id} present.` : `${skill.id} not installed.`,
1418
- command: present ? undefined : "jolly init"
1549
+ command: present ? void 0 : "jolly init"
1419
1550
  });
1420
1551
  }
1421
1552
  }
1422
1553
  if (wants("saleor")) {
1423
- const hasCloud = Boolean(values["JOLLY_SALEOR_CLOUD_TOKEN"] ?? process.env["JOLLY_SALEOR_CLOUD_TOKEN"]);
1424
- const hasEndpoint = Boolean(values["NEXT_PUBLIC_SALEOR_API_URL"] ?? process.env["NEXT_PUBLIC_SALEOR_API_URL"]);
1425
- const hasApp = Boolean(values["JOLLY_SALEOR_APP_TOKEN"] ?? process.env["JOLLY_SALEOR_APP_TOKEN"]);
1554
+ const hasCloud = Boolean(
1555
+ values["JOLLY_SALEOR_CLOUD_TOKEN"] ?? process.env["JOLLY_SALEOR_CLOUD_TOKEN"]
1556
+ );
1557
+ const hasEndpoint = Boolean(
1558
+ values["NEXT_PUBLIC_SALEOR_API_URL"] ?? process.env["NEXT_PUBLIC_SALEOR_API_URL"]
1559
+ );
1560
+ const hasApp = Boolean(
1561
+ values["JOLLY_SALEOR_APP_TOKEN"] ?? process.env["JOLLY_SALEOR_APP_TOKEN"]
1562
+ );
1426
1563
  checks.push({
1427
1564
  id: "saleor-cloud-token",
1428
1565
  status: hasCloud ? "pass" : "fail",
1429
1566
  description: hasCloud ? "JOLLY_SALEOR_CLOUD_TOKEN present." : "No Saleor Cloud token configured.",
1430
- command: hasCloud ? undefined : "jolly login --token <value>"
1567
+ command: hasCloud ? void 0 : "jolly login --token <value>"
1431
1568
  });
1432
1569
  checks.push({
1433
1570
  id: "saleor-endpoint",
1571
+ // Presence is detectable; live connectivity is a @sandbox concern, so
1572
+ // report "unknown" (not a fabricated pass) when present without probing.
1434
1573
  status: hasEndpoint ? "unknown" : "fail",
1435
1574
  description: hasEndpoint ? "NEXT_PUBLIC_SALEOR_API_URL is set; live connectivity not verified in this run." : "No Saleor GraphQL endpoint configured.",
1436
- command: hasEndpoint ? undefined : "jolly create store --url <graphql-endpoint>"
1575
+ command: hasEndpoint ? void 0 : "jolly create store --url <graphql-endpoint>"
1437
1576
  });
1438
1577
  checks.push({
1439
1578
  id: "saleor-app-token",
1440
1579
  status: hasApp ? "pass" : "fail",
1441
1580
  description: hasApp ? "JOLLY_SALEOR_APP_TOKEN present." : "No Saleor app token configured.",
1442
- command: hasApp ? undefined : "jolly create app-token"
1581
+ command: hasApp ? void 0 : "jolly create app-token"
1443
1582
  });
1444
1583
  }
1445
1584
  if (wants("storefront")) {
@@ -1448,7 +1587,7 @@ function commandDoctor(args) {
1448
1587
  id: "storefront-present",
1449
1588
  status: storefrontPresent ? "unknown" : "fail",
1450
1589
  description: storefrontPresent ? "A project structure exists; Paper storefront readiness not verified in this run." : "No Paper storefront detected locally.",
1451
- command: storefrontPresent ? undefined : "Clone saleor/storefront (Paper) per the Jolly skill."
1590
+ command: storefrontPresent ? void 0 : "Clone saleor/storefront (Paper) per the Jolly skill."
1452
1591
  });
1453
1592
  }
1454
1593
  if (wants("deployment")) {
@@ -1460,13 +1599,17 @@ function commandDoctor(args) {
1460
1599
  });
1461
1600
  }
1462
1601
  if (wants("stripe")) {
1463
- const hasPub = Boolean(values["JOLLY_STRIPE_PUBLISHABLE_KEY"] ?? process.env["JOLLY_STRIPE_PUBLISHABLE_KEY"]);
1464
- const hasSecret = Boolean(values["JOLLY_STRIPE_SECRET_KEY"] ?? process.env["JOLLY_STRIPE_SECRET_KEY"]);
1602
+ const hasPub = Boolean(
1603
+ values["JOLLY_STRIPE_PUBLISHABLE_KEY"] ?? process.env["JOLLY_STRIPE_PUBLISHABLE_KEY"]
1604
+ );
1605
+ const hasSecret = Boolean(
1606
+ values["JOLLY_STRIPE_SECRET_KEY"] ?? process.env["JOLLY_STRIPE_SECRET_KEY"]
1607
+ );
1465
1608
  checks.push({
1466
1609
  id: "stripe-keys",
1467
1610
  status: hasPub && hasSecret ? "pass" : "fail",
1468
1611
  description: hasPub && hasSecret ? "Stripe test-mode keys present in .env." : "Stripe keys not configured.",
1469
- command: hasPub && hasSecret ? undefined : "jolly create stripe --publishable-key <pk> --secret-key <sk>"
1612
+ command: hasPub && hasSecret ? void 0 : "jolly create stripe --publishable-key <pk> --secret-key <sk>"
1470
1613
  });
1471
1614
  }
1472
1615
  const hasFail = checks.some((c) => c.status === "fail");
@@ -1495,8 +1638,7 @@ function commandSkills(args) {
1495
1638
  if (sub === "install" || sub === "update") {
1496
1639
  const checks2 = DEFAULT_SKILLS.map((skill) => {
1497
1640
  const already = skillInstalledOnDisk(skill);
1498
- if (!already && sub === "install")
1499
- installSkill(skill);
1641
+ if (!already && sub === "install") installSkill(skill);
1500
1642
  const present = skillInstalledOnDisk(skill);
1501
1643
  return {
1502
1644
  id: `skill-${skill.id}`,
@@ -1612,7 +1754,9 @@ function startPlan() {
1612
1754
  networkHostsContacted: ["cloud.saleor.io"],
1613
1755
  repositoriesCloned: []
1614
1756
  },
1615
- riskContext: createStoreRiskContext(`${cloudApiBase()}/organizations/{organization}/environments/`)
1757
+ riskContext: createStoreRiskContext(
1758
+ `${cloudApiBase()}/organizations/{organization}/environments/`
1759
+ )
1616
1760
  },
1617
1761
  {
1618
1762
  stage: "storefront",
@@ -1624,7 +1768,7 @@ function startPlan() {
1624
1768
  },
1625
1769
  riskContext: {
1626
1770
  action: "clone storefront",
1627
- target: "saleor/storefront (Paper) storefront/",
1771
+ target: "saleor/storefront (Paper) \u2192 storefront/",
1628
1772
  riskLevel: "low",
1629
1773
  categories: [],
1630
1774
  reversible: true,
@@ -1700,8 +1844,7 @@ function commandStartDryRun() {
1700
1844
  });
1701
1845
  }
1702
1846
  function commandStart(args) {
1703
- if (args.dryRun)
1704
- return commandStartDryRun();
1847
+ if (args.dryRun) return commandStartDryRun();
1705
1848
  const command = "start";
1706
1849
  const initEnv = commandInit(args);
1707
1850
  const doctorEnv = commandDoctor({
@@ -1739,7 +1882,7 @@ function commandHelp() {
1739
1882
  return envelope({
1740
1883
  command: "help",
1741
1884
  status: "success",
1742
- summary: "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)",
1743
1886
  data: {
1744
1887
  commands: [
1745
1888
  "login",
@@ -1767,7 +1910,7 @@ function commandHelp() {
1767
1910
  async function dispatch(args) {
1768
1911
  const cmd = args.positionals[0];
1769
1912
  switch (cmd) {
1770
- case undefined:
1913
+ case void 0:
1771
1914
  case "help":
1772
1915
  return commandHelp();
1773
1916
  case "login":
@@ -1775,8 +1918,7 @@ async function dispatch(args) {
1775
1918
  case "logout":
1776
1919
  return commandLogout(args);
1777
1920
  case "auth":
1778
- if (args.positionals[1] === "status")
1779
- return commandAuthStatus(args);
1921
+ if (args.positionals[1] === "status") return commandAuthStatus(args);
1780
1922
  return errorEnvelope("auth", `Unknown auth subcommand "${args.positionals[1] ?? ""}".`, [
1781
1923
  {
1782
1924
  code: "UNKNOWN_AUTH_SUBCOMMAND",
@@ -1823,4 +1965,4 @@ async function main() {
1823
1965
  const exitCode = emit(env, args);
1824
1966
  process.exit(exitCode);
1825
1967
  }
1826
- main();
1968
+ void main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dk/jolly",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Ahoy, agent. Go build a Saleor storefront.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,22 +19,23 @@
19
19
  "node": ">=23.0.0"
20
20
  },
21
21
  "scripts": {
22
- "build": "bun build src/index.ts --target node --outfile dist/index.js",
23
- "prepack": "bun build src/index.ts --target node --outfile dist/index.js",
24
- "prepublishOnly": "bun build src/index.ts --target node --outfile dist/index.js",
25
- "dev": "bun run --watch src/index.ts",
26
- "start": "bun run src/index.ts",
27
- "test": "bun test tests/",
28
- "test:bdd": "bun x --bun cucumber-js",
29
- "test:logic": "bun x --bun cucumber-js -p logic",
30
- "test:sandbox": "bun x --bun cucumber-js -p sandbox",
31
- "typecheck": "bun x tsc --noEmit"
22
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
23
+ "prepack": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
24
+ "prepublishOnly": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
25
+ "dev": "node --watch src/index.ts",
26
+ "start": "node src/index.ts",
27
+ "test": "node --test \"tests/**/*.test.ts\"",
28
+ "test:bdd": "cucumber-js",
29
+ "test:logic": "cucumber-js -p logic",
30
+ "test:sandbox": "cucumber-js -p sandbox",
31
+ "test:eval": "cucumber-js -p eval",
32
+ "typecheck": "tsc --noEmit"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@cucumber/cucumber": "^11.3.0",
35
36
  "@earendil-works/pi-coding-agent": "^0.79.1",
36
- "@types/bun": "^1.3.14",
37
37
  "@types/node": "^22.10.0",
38
+ "esbuild": "^0.24.0",
38
39
  "happy-dom": "^15.11.0",
39
40
  "typescript": "^5.7.0"
40
41
  }