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