@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 +7 -5
- package/bin/jolly +2 -1
- package/dist/index.js +399 -257
- package/package.json +13 -12
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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 =
|
|
17
|
-
var POLL_TIMEOUT_MS =
|
|
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(
|
|
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(
|
|
50
|
+
const response = await cloudFetch(
|
|
51
|
+
`${cloudApiBase()}/organizations/${organizationSlug}/projects/`,
|
|
52
|
+
token
|
|
53
|
+
);
|
|
48
54
|
if (!response.ok) {
|
|
49
|
-
throw new CloudApiError(
|
|
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(
|
|
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(
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
}`,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
}`,
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
240
|
-
const path = join(
|
|
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(
|
|
253
|
-
const path = join(
|
|
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
|
-
|
|
257
|
-
|
|
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(
|
|
266
|
-
ensureEnvIgnored(
|
|
267
|
-
const path = join(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
336
|
-
else if (arg === "--
|
|
337
|
-
|
|
338
|
-
else if (arg === "--
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
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
|
|
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 :
|
|
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
|
-
|
|
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(
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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;
|
|
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(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
877
|
-
|
|
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"] ??
|
|
884
|
-
if (mock !==
|
|
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(
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 :
|
|
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(
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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(
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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(
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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(
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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:
|
|
1411
|
+
timeout: 6e4
|
|
1284
1412
|
});
|
|
1285
|
-
return { installed: result.status === 0, stderr: result.stderr ??
|
|
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(
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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 ?
|
|
1549
|
+
command: present ? void 0 : "jolly init"
|
|
1419
1550
|
});
|
|
1420
1551
|
}
|
|
1421
1552
|
}
|
|
1422
1553
|
if (wants("saleor")) {
|
|
1423
|
-
const hasCloud = Boolean(
|
|
1424
|
-
|
|
1425
|
-
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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(
|
|
1464
|
-
|
|
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 ?
|
|
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(
|
|
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)
|
|
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
|
|
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
|
|
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.
|
|
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": "
|
|
23
|
-
"prepack": "
|
|
24
|
-
"prepublishOnly": "
|
|
25
|
-
"dev": "
|
|
26
|
-
"start": "
|
|
27
|
-
"test": "
|
|
28
|
-
"test:bdd": "
|
|
29
|
-
"test:logic": "
|
|
30
|
-
"test:sandbox": "
|
|
31
|
-
"
|
|
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
|
}
|