@duffcloudservices/cli 0.3.1 → 0.3.3
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 +26 -2
- package/dist/index.js +487 -150
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import chalk8 from "chalk";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.3.
|
|
8
|
+
var version = "0.3.3";
|
|
9
9
|
|
|
10
10
|
// src/commands/auth.ts
|
|
11
11
|
import chalk2 from "chalk";
|
|
@@ -17,64 +17,375 @@ import chalk from "chalk";
|
|
|
17
17
|
|
|
18
18
|
// src/auth/credentials.ts
|
|
19
19
|
import Conf from "conf";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
|
|
21
|
+
// src/auth/keychain/run.ts
|
|
22
|
+
import { spawnSync } from "child_process";
|
|
23
|
+
function run(cmd, args, opts = {}) {
|
|
24
|
+
const options = {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
27
|
+
windowsHide: true
|
|
28
|
+
};
|
|
29
|
+
if (opts.input !== void 0) options.input = opts.input;
|
|
30
|
+
const res = spawnSync(cmd, args, options);
|
|
31
|
+
if (res.error) {
|
|
32
|
+
return { ok: false, code: null, stdout: "", stderr: String(res.error), spawnError: true };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
ok: res.status === 0,
|
|
36
|
+
code: res.status,
|
|
37
|
+
stdout: typeof res.stdout === "string" ? res.stdout : "",
|
|
38
|
+
stderr: typeof res.stderr === "string" ? res.stderr : "",
|
|
39
|
+
spawnError: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function commandExists(cmd, probeArgs) {
|
|
43
|
+
const res = run(cmd, probeArgs);
|
|
44
|
+
return !res.spawnError;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/auth/keychain/types.ts
|
|
48
|
+
var SERVICE = "dcs-cli";
|
|
49
|
+
|
|
50
|
+
// src/auth/keychain/macos.ts
|
|
51
|
+
var macosKeychain = {
|
|
52
|
+
name: "macos-keychain",
|
|
53
|
+
isAvailable() {
|
|
54
|
+
return process.platform === "darwin" && commandExists("security", ["help"]);
|
|
55
|
+
},
|
|
56
|
+
get(account) {
|
|
57
|
+
const res = run("security", [
|
|
58
|
+
"find-generic-password",
|
|
59
|
+
"-s",
|
|
60
|
+
SERVICE,
|
|
61
|
+
"-a",
|
|
62
|
+
account,
|
|
63
|
+
"-w"
|
|
64
|
+
]);
|
|
65
|
+
if (!res.ok) return null;
|
|
66
|
+
return res.stdout.replace(/\n$/, "");
|
|
67
|
+
},
|
|
68
|
+
set(account, secret) {
|
|
69
|
+
const res = run("security", [
|
|
70
|
+
"add-generic-password",
|
|
71
|
+
"-s",
|
|
72
|
+
SERVICE,
|
|
73
|
+
"-a",
|
|
74
|
+
account,
|
|
75
|
+
"-w",
|
|
76
|
+
secret,
|
|
77
|
+
"-U"
|
|
78
|
+
]);
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`macOS Keychain write failed: ${res.stderr.trim() || res.code}`);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
delete(account) {
|
|
84
|
+
run("security", ["delete-generic-password", "-s", SERVICE, "-a", account]);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/auth/keychain/libsecret.ts
|
|
89
|
+
var libsecretKeychain = {
|
|
90
|
+
name: "libsecret",
|
|
91
|
+
isAvailable() {
|
|
92
|
+
return process.platform === "linux" && commandExists("secret-tool", ["--version"]);
|
|
93
|
+
},
|
|
94
|
+
get(account) {
|
|
95
|
+
const res = run("secret-tool", ["lookup", "service", SERVICE, "account", account]);
|
|
96
|
+
if (!res.ok) return null;
|
|
97
|
+
return res.stdout.replace(/\n$/, "");
|
|
98
|
+
},
|
|
99
|
+
set(account, secret) {
|
|
100
|
+
const res = run(
|
|
101
|
+
"secret-tool",
|
|
102
|
+
["store", "--label", `${SERVICE}:${account}`, "service", SERVICE, "account", account],
|
|
103
|
+
{ input: secret }
|
|
104
|
+
);
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(`libsecret write failed: ${res.stderr.trim() || res.code}`);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
delete(account) {
|
|
110
|
+
run("secret-tool", ["clear", "service", SERVICE, "account", account]);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/auth/keychain/windows.ts
|
|
115
|
+
import fs3 from "fs";
|
|
116
|
+
|
|
117
|
+
// src/auth/keychain/paths.ts
|
|
118
|
+
import os from "os";
|
|
119
|
+
import path from "path";
|
|
120
|
+
import fs from "fs";
|
|
121
|
+
var APP_DIR = "dcs-cli-nodejs";
|
|
122
|
+
function configDir() {
|
|
123
|
+
const override2 = process.env.DCS_CLI_CONFIG_DIR;
|
|
124
|
+
if (override2) return override2;
|
|
125
|
+
if (process.platform === "win32") {
|
|
126
|
+
const base2 = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
127
|
+
return path.join(base2, APP_DIR);
|
|
128
|
+
}
|
|
129
|
+
if (process.platform === "darwin") {
|
|
130
|
+
return path.join(os.homedir(), "Library", "Application Support", APP_DIR);
|
|
131
|
+
}
|
|
132
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
133
|
+
return path.join(base, APP_DIR);
|
|
134
|
+
}
|
|
135
|
+
function secretFile(account, ext) {
|
|
136
|
+
const safe = account.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
137
|
+
return path.join(configDir(), `${safe}.${ext}`);
|
|
138
|
+
}
|
|
139
|
+
function ensureConfigDir() {
|
|
140
|
+
const dir = configDir();
|
|
141
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
142
|
+
return dir;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/auth/keychain/encrypted-file.ts
|
|
146
|
+
import fs2 from "fs";
|
|
147
|
+
import os2 from "os";
|
|
148
|
+
import crypto from "crypto";
|
|
149
|
+
var EXT = "enc";
|
|
150
|
+
var MAGIC = "DCSK1";
|
|
151
|
+
var STATIC_SALT = Buffer.from("dcs-cli-keychain-v1-salt", "utf8");
|
|
152
|
+
function deriveKey() {
|
|
153
|
+
let username = "unknown";
|
|
154
|
+
try {
|
|
155
|
+
username = os2.userInfo().username;
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
const material = `${username}\0${os2.hostname()}\0dcs-cli`;
|
|
159
|
+
return crypto.scryptSync(material, STATIC_SALT, 32);
|
|
160
|
+
}
|
|
161
|
+
var encryptedFile = {
|
|
162
|
+
name: "encrypted-file",
|
|
163
|
+
isAvailable() {
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
get(account) {
|
|
167
|
+
let raw;
|
|
168
|
+
try {
|
|
169
|
+
raw = fs2.readFileSync(secretFile(account, EXT), "utf8").trim();
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const parts = raw.split(":");
|
|
174
|
+
if (parts.length !== 4 || parts[0] !== MAGIC) return null;
|
|
175
|
+
try {
|
|
176
|
+
const iv = Buffer.from(parts[1], "base64");
|
|
177
|
+
const tag = Buffer.from(parts[2], "base64");
|
|
178
|
+
const ciphertext = Buffer.from(parts[3], "base64");
|
|
179
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", deriveKey(), iv);
|
|
180
|
+
decipher.setAuthTag(tag);
|
|
181
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
182
|
+
return plaintext.toString("utf8");
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
set(account, secret) {
|
|
188
|
+
ensureConfigDir();
|
|
189
|
+
const iv = crypto.randomBytes(12);
|
|
190
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", deriveKey(), iv);
|
|
191
|
+
const ciphertext = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
|
|
192
|
+
const tag = cipher.getAuthTag();
|
|
193
|
+
const envelope = [
|
|
194
|
+
MAGIC,
|
|
195
|
+
iv.toString("base64"),
|
|
196
|
+
tag.toString("base64"),
|
|
197
|
+
ciphertext.toString("base64")
|
|
198
|
+
].join(":");
|
|
199
|
+
fs2.writeFileSync(secretFile(account, EXT), envelope, { encoding: "utf8", mode: 384 });
|
|
200
|
+
},
|
|
201
|
+
delete(account) {
|
|
202
|
+
try {
|
|
203
|
+
fs2.unlinkSync(secretFile(account, EXT));
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// src/auth/keychain/windows.ts
|
|
210
|
+
var EXT2 = "dpapi";
|
|
211
|
+
var PROTECT = "$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $b=[Text.Encoding]::UTF8.GetBytes($env:DCS_KEYCHAIN_SECRET); $p=[Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser'); [Convert]::ToBase64String($p)";
|
|
212
|
+
var UNPROTECT = "$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $p=[Convert]::FromBase64String($env:DCS_KEYCHAIN_ENC); $b=[Security.Cryptography.ProtectedData]::Unprotect($p,$null,'CurrentUser'); [Text.Encoding]::UTF8.GetString($b)";
|
|
213
|
+
function powershell(script, env) {
|
|
214
|
+
return run("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { env });
|
|
215
|
+
}
|
|
216
|
+
function deleteDpapiSidecar(account) {
|
|
217
|
+
try {
|
|
218
|
+
fs3.unlinkSync(secretFile(account, EXT2));
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
var dpapiUnavailable = false;
|
|
223
|
+
var windowsDpapi = {
|
|
224
|
+
name: "windows-dpapi",
|
|
225
|
+
isAvailable() {
|
|
226
|
+
return process.platform === "win32" && !dpapiUnavailable;
|
|
227
|
+
},
|
|
228
|
+
get(account) {
|
|
229
|
+
if (dpapiUnavailable) return encryptedFile.get(account);
|
|
230
|
+
let enc;
|
|
231
|
+
try {
|
|
232
|
+
enc = fs3.readFileSync(secretFile(account, EXT2), "utf8").trim();
|
|
233
|
+
} catch {
|
|
234
|
+
return encryptedFile.get(account);
|
|
235
|
+
}
|
|
236
|
+
if (!enc) return null;
|
|
237
|
+
const res = powershell(UNPROTECT, { DCS_KEYCHAIN_ENC: enc });
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
if (res.spawnError) {
|
|
240
|
+
dpapiUnavailable = true;
|
|
241
|
+
return encryptedFile.get(account);
|
|
31
242
|
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return res.stdout.replace(/\r?\n$/, "");
|
|
246
|
+
},
|
|
247
|
+
set(account, secret) {
|
|
248
|
+
if (dpapiUnavailable) {
|
|
249
|
+
encryptedFile.set(account, secret);
|
|
250
|
+
deleteDpapiSidecar(account);
|
|
251
|
+
return;
|
|
32
252
|
}
|
|
253
|
+
ensureConfigDir();
|
|
254
|
+
const res = powershell(PROTECT, { DCS_KEYCHAIN_SECRET: secret });
|
|
255
|
+
if (!res.ok || !res.stdout.trim()) {
|
|
256
|
+
dpapiUnavailable = true;
|
|
257
|
+
encryptedFile.set(account, secret);
|
|
258
|
+
deleteDpapiSidecar(account);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
fs3.writeFileSync(secretFile(account, EXT2), res.stdout.trim(), { encoding: "utf8", mode: 384 });
|
|
262
|
+
encryptedFile.delete(account);
|
|
263
|
+
},
|
|
264
|
+
delete(account) {
|
|
265
|
+
deleteDpapiSidecar(account);
|
|
266
|
+
encryptedFile.delete(account);
|
|
33
267
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/auth/keychain/index.ts
|
|
271
|
+
var CANDIDATES = [macosKeychain, libsecretKeychain, windowsDpapi];
|
|
272
|
+
var resolved;
|
|
273
|
+
var override = null;
|
|
274
|
+
function resolve() {
|
|
275
|
+
for (const backend of CANDIDATES) {
|
|
276
|
+
try {
|
|
277
|
+
if (backend.isAvailable()) return backend;
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return encryptedFile;
|
|
282
|
+
}
|
|
283
|
+
function getKeychain() {
|
|
284
|
+
if (override) return override;
|
|
285
|
+
if (!resolved) resolved = resolve();
|
|
286
|
+
return resolved;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/auth/credentials.ts
|
|
290
|
+
var ACCOUNT = "auth";
|
|
291
|
+
function defaultLegacyStore() {
|
|
292
|
+
const config = new Conf({ projectName: "dcs-cli" });
|
|
293
|
+
return {
|
|
294
|
+
read: () => config.get("auth"),
|
|
295
|
+
clear: () => {
|
|
296
|
+
if (config.has("auth")) config.delete("auth");
|
|
297
|
+
}
|
|
298
|
+
};
|
|
37
299
|
}
|
|
38
|
-
|
|
39
|
-
|
|
300
|
+
var legacyStore;
|
|
301
|
+
function getLegacyStore() {
|
|
302
|
+
if (!legacyStore) legacyStore = defaultLegacyStore();
|
|
303
|
+
return legacyStore;
|
|
304
|
+
}
|
|
305
|
+
var cache;
|
|
306
|
+
function load() {
|
|
307
|
+
if (cache !== void 0) return cache;
|
|
308
|
+
const raw = getKeychain().get(ACCOUNT);
|
|
309
|
+
if (raw) {
|
|
310
|
+
try {
|
|
311
|
+
cache = JSON.parse(raw);
|
|
312
|
+
return cache;
|
|
313
|
+
} catch {
|
|
314
|
+
getKeychain().delete(ACCOUNT);
|
|
315
|
+
cache = null;
|
|
316
|
+
return cache;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const legacy = getLegacyStore().read();
|
|
320
|
+
if (legacy) {
|
|
321
|
+
getKeychain().set(ACCOUNT, JSON.stringify(legacy));
|
|
322
|
+
getLegacyStore().clear();
|
|
323
|
+
cache = legacy;
|
|
324
|
+
return cache;
|
|
325
|
+
}
|
|
326
|
+
cache = null;
|
|
327
|
+
return cache;
|
|
328
|
+
}
|
|
329
|
+
function persist(creds) {
|
|
330
|
+
getKeychain().set(ACCOUNT, JSON.stringify(creds));
|
|
331
|
+
cache = creds;
|
|
332
|
+
getLegacyStore().clear();
|
|
333
|
+
}
|
|
334
|
+
function storeCredentials(credentials) {
|
|
335
|
+
persist(credentials);
|
|
40
336
|
}
|
|
41
337
|
function clearCredentials() {
|
|
42
|
-
|
|
338
|
+
getKeychain().delete(ACCOUNT);
|
|
339
|
+
getLegacyStore().clear();
|
|
340
|
+
cache = null;
|
|
43
341
|
}
|
|
44
342
|
function isAuthenticated() {
|
|
45
|
-
const creds =
|
|
343
|
+
const creds = load();
|
|
46
344
|
if (!creds) return false;
|
|
47
345
|
const bufferMs = 5 * 60 * 1e3;
|
|
48
346
|
return creds.expiresAt > Date.now() + bufferMs;
|
|
49
347
|
}
|
|
348
|
+
function hasStoredSession() {
|
|
349
|
+
return load() !== null;
|
|
350
|
+
}
|
|
50
351
|
function getCurrentUserEmail() {
|
|
51
|
-
return
|
|
352
|
+
return load()?.email;
|
|
52
353
|
}
|
|
53
354
|
function getAccessToken() {
|
|
54
|
-
|
|
55
|
-
if (!creds) return void 0;
|
|
56
|
-
return creds.accessToken;
|
|
355
|
+
return load()?.accessToken;
|
|
57
356
|
}
|
|
58
357
|
function getRefreshToken() {
|
|
59
|
-
return
|
|
358
|
+
return load()?.refreshToken;
|
|
60
359
|
}
|
|
61
360
|
function updateAccessToken(accessToken, expiresIn) {
|
|
62
|
-
const creds =
|
|
361
|
+
const creds = load();
|
|
63
362
|
if (!creds) return;
|
|
64
|
-
|
|
363
|
+
persist({
|
|
65
364
|
...creds,
|
|
66
365
|
accessToken,
|
|
67
366
|
expiresAt: Date.now() + expiresIn * 1e3
|
|
68
367
|
});
|
|
69
368
|
}
|
|
70
|
-
function
|
|
71
|
-
return
|
|
369
|
+
function getStorageBackendName() {
|
|
370
|
+
return getKeychain().name;
|
|
72
371
|
}
|
|
73
372
|
|
|
74
373
|
// src/auth/device-flow.ts
|
|
75
374
|
var DEFAULT_API_URL = "https://portal.duffcloudservices.com";
|
|
375
|
+
var RefreshTokenRevokedError = class extends Error {
|
|
376
|
+
constructor(message = "Refresh token was rejected (expired or revoked).") {
|
|
377
|
+
super(message);
|
|
378
|
+
this.name = "RefreshTokenRevokedError";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
var ReauthRequiredError = class extends Error {
|
|
382
|
+
constructor(message = "Your CLI session expired or was revoked. Run `dcs login` to sign in again.") {
|
|
383
|
+
super(message);
|
|
384
|
+
this.name = "ReauthRequiredError";
|
|
385
|
+
}
|
|
386
|
+
};
|
|
76
387
|
function sleep(ms) {
|
|
77
|
-
return new Promise((
|
|
388
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
78
389
|
}
|
|
79
390
|
async function startDeviceAuth(options = {}) {
|
|
80
391
|
const apiUrl = options.apiUrl || DEFAULT_API_URL;
|
|
@@ -201,8 +512,11 @@ async function refreshAccessToken(refreshToken, options = {}) {
|
|
|
201
512
|
},
|
|
202
513
|
body: JSON.stringify({ refresh_token: refreshToken })
|
|
203
514
|
});
|
|
515
|
+
if (response.status === 401) {
|
|
516
|
+
throw new RefreshTokenRevokedError();
|
|
517
|
+
}
|
|
204
518
|
if (!response.ok) {
|
|
205
|
-
throw new Error(
|
|
519
|
+
throw new Error(`Failed to refresh access token (HTTP ${response.status}).`);
|
|
206
520
|
}
|
|
207
521
|
const data = await response.json();
|
|
208
522
|
return {
|
|
@@ -234,7 +548,7 @@ async function whoamiCommand() {
|
|
|
234
548
|
return;
|
|
235
549
|
}
|
|
236
550
|
console.log("Logged in as:", chalk2.green(email));
|
|
237
|
-
console.log(chalk2.dim("
|
|
551
|
+
console.log(chalk2.dim("Token storage:"), chalk2.dim(getStorageBackendName()));
|
|
238
552
|
}
|
|
239
553
|
|
|
240
554
|
// src/commands/sites.ts
|
|
@@ -248,36 +562,59 @@ var PortalClient = class {
|
|
|
248
562
|
constructor(options = {}) {
|
|
249
563
|
this.apiUrl = options.apiUrl || process.env.DCS_API_URL || DEFAULT_API_URL2;
|
|
250
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Refresh the session using the stored refresh token. Exactly one attempt —
|
|
567
|
+
* never a retry loop. On a revoked/expired refresh token the dead local
|
|
568
|
+
* session is dropped and a clean re-auth is demanded; transient failures
|
|
569
|
+
* (network/5xx/429) keep the session and surface a retryable error.
|
|
570
|
+
*/
|
|
571
|
+
async refreshSession() {
|
|
572
|
+
const refreshToken = getRefreshToken();
|
|
573
|
+
try {
|
|
574
|
+
const { accessToken: newToken, expiresIn } = await refreshAccessToken(refreshToken, {
|
|
575
|
+
apiUrl: this.apiUrl
|
|
576
|
+
});
|
|
577
|
+
updateAccessToken(newToken, expiresIn);
|
|
578
|
+
return newToken;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (error instanceof RefreshTokenRevokedError) {
|
|
581
|
+
clearCredentials();
|
|
582
|
+
throw new ReauthRequiredError();
|
|
583
|
+
}
|
|
584
|
+
throw new Error(
|
|
585
|
+
`Could not reach the DCS Portal to refresh your session: ${error instanceof Error ? error.message : String(error)}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
251
589
|
/**
|
|
252
590
|
* Make an authenticated API request.
|
|
253
591
|
*/
|
|
254
|
-
async request(
|
|
592
|
+
async request(path6, options = {}) {
|
|
255
593
|
let accessToken = getAccessToken();
|
|
594
|
+
let refreshedThisRequest = false;
|
|
256
595
|
if (!isAuthenticated() && getRefreshToken()) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const { accessToken: newToken, expiresIn } = await refreshAccessToken(refreshToken, {
|
|
260
|
-
apiUrl: this.apiUrl
|
|
261
|
-
});
|
|
262
|
-
updateAccessToken(newToken, expiresIn);
|
|
263
|
-
accessToken = newToken;
|
|
264
|
-
} catch {
|
|
265
|
-
throw new Error("Session expired. Please login again with `dcs login`.");
|
|
266
|
-
}
|
|
596
|
+
accessToken = await this.refreshSession();
|
|
597
|
+
refreshedThisRequest = true;
|
|
267
598
|
}
|
|
268
599
|
if (!accessToken) {
|
|
269
|
-
throw new
|
|
600
|
+
throw new ReauthRequiredError("Not authenticated. Run `dcs login` to sign in.");
|
|
270
601
|
}
|
|
271
|
-
const
|
|
602
|
+
const send = (token) => fetch(`${this.apiUrl}${path6}`, {
|
|
272
603
|
...options,
|
|
273
604
|
headers: {
|
|
274
605
|
...options.headers,
|
|
275
|
-
Authorization: `Bearer ${
|
|
606
|
+
Authorization: `Bearer ${token}`,
|
|
276
607
|
"Content-Type": "application/json"
|
|
277
608
|
}
|
|
278
609
|
});
|
|
610
|
+
let response = await send(accessToken);
|
|
611
|
+
if (response.status === 401 && !refreshedThisRequest && getRefreshToken()) {
|
|
612
|
+
const newToken = await this.refreshSession();
|
|
613
|
+
response = await send(newToken);
|
|
614
|
+
}
|
|
279
615
|
if (response.status === 401) {
|
|
280
|
-
|
|
616
|
+
clearCredentials();
|
|
617
|
+
throw new ReauthRequiredError();
|
|
281
618
|
}
|
|
282
619
|
if (!response.ok) {
|
|
283
620
|
const error = await response.text();
|
|
@@ -340,7 +677,7 @@ function getPortalClient(options) {
|
|
|
340
677
|
|
|
341
678
|
// src/commands/sites.ts
|
|
342
679
|
async function listSitesCommand() {
|
|
343
|
-
if (!
|
|
680
|
+
if (!hasStoredSession()) {
|
|
344
681
|
console.log(chalk3.yellow("Not logged in."));
|
|
345
682
|
console.log("Run", chalk3.cyan("dcs login"), "to authenticate.");
|
|
346
683
|
process.exit(1);
|
|
@@ -387,7 +724,7 @@ async function listSitesCommand() {
|
|
|
387
724
|
}
|
|
388
725
|
}
|
|
389
726
|
async function showSiteCommand(slug) {
|
|
390
|
-
if (!
|
|
727
|
+
if (!hasStoredSession()) {
|
|
391
728
|
console.log(chalk3.yellow("Not logged in."));
|
|
392
729
|
console.log("Run", chalk3.cyan("dcs login"), "to authenticate.");
|
|
393
730
|
process.exit(1);
|
|
@@ -435,8 +772,8 @@ function displaySiteDetails(site) {
|
|
|
435
772
|
}
|
|
436
773
|
|
|
437
774
|
// src/commands/init.ts
|
|
438
|
-
import
|
|
439
|
-
import
|
|
775
|
+
import fs4 from "fs/promises";
|
|
776
|
+
import path2 from "path";
|
|
440
777
|
import chalk4 from "chalk";
|
|
441
778
|
import ora2 from "ora";
|
|
442
779
|
|
|
@@ -638,7 +975,7 @@ jobs:
|
|
|
638
975
|
// src/commands/init.ts
|
|
639
976
|
async function initCommand(options) {
|
|
640
977
|
const { siteSlug, siteName, framework = "vue", target = ".", dryRun = false, force = false } = options;
|
|
641
|
-
if (!
|
|
978
|
+
if (!hasStoredSession()) {
|
|
642
979
|
console.log(chalk4.yellow("Not logged in."));
|
|
643
980
|
console.log("Run", chalk4.cyan("dcs login"), "to authenticate.");
|
|
644
981
|
process.exit(1);
|
|
@@ -669,7 +1006,7 @@ async function initCommand(options) {
|
|
|
669
1006
|
}
|
|
670
1007
|
process.exit(1);
|
|
671
1008
|
}
|
|
672
|
-
const targetDir =
|
|
1009
|
+
const targetDir = path2.resolve(target);
|
|
673
1010
|
console.log(chalk4.dim(`Target directory: ${targetDir}`));
|
|
674
1011
|
const generatedFiles = [];
|
|
675
1012
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -721,21 +1058,21 @@ async function initCommand(options) {
|
|
|
721
1058
|
console.log();
|
|
722
1059
|
}
|
|
723
1060
|
async function writeFile(targetDir, relativePath, content, options) {
|
|
724
|
-
const fullPath =
|
|
1061
|
+
const fullPath = path2.join(targetDir, relativePath);
|
|
725
1062
|
if (options.dryRun) {
|
|
726
1063
|
console.log(chalk4.dim(`Would create: ${relativePath}`));
|
|
727
1064
|
return relativePath;
|
|
728
1065
|
}
|
|
729
1066
|
try {
|
|
730
|
-
await
|
|
1067
|
+
await fs4.access(fullPath);
|
|
731
1068
|
if (!options.force) {
|
|
732
1069
|
console.log(chalk4.yellow(`Skipped (exists): ${relativePath}`));
|
|
733
1070
|
return relativePath;
|
|
734
1071
|
}
|
|
735
1072
|
} catch {
|
|
736
1073
|
}
|
|
737
|
-
await
|
|
738
|
-
await
|
|
1074
|
+
await fs4.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
1075
|
+
await fs4.writeFile(fullPath, content, "utf8");
|
|
739
1076
|
return relativePath;
|
|
740
1077
|
}
|
|
741
1078
|
function generateSiteYaml(data) {
|
|
@@ -1346,19 +1683,19 @@ After completing this plan:
|
|
|
1346
1683
|
}
|
|
1347
1684
|
|
|
1348
1685
|
// src/commands/validate.ts
|
|
1349
|
-
import
|
|
1350
|
-
import
|
|
1686
|
+
import fs5 from "fs/promises";
|
|
1687
|
+
import path3 from "path";
|
|
1351
1688
|
import chalk5 from "chalk";
|
|
1352
1689
|
import yaml from "js-yaml";
|
|
1353
1690
|
async function validateCommand(options) {
|
|
1354
1691
|
const { target = ".", fix = false, verbose = false } = options;
|
|
1355
|
-
const targetDir =
|
|
1356
|
-
const dcsDir =
|
|
1692
|
+
const targetDir = path3.resolve(target);
|
|
1693
|
+
const dcsDir = path3.join(targetDir, ".dcs");
|
|
1357
1694
|
console.log(chalk5.bold("Validating DCS configuration..."));
|
|
1358
1695
|
console.log(chalk5.dim(`Directory: ${targetDir}`));
|
|
1359
1696
|
console.log();
|
|
1360
1697
|
try {
|
|
1361
|
-
await
|
|
1698
|
+
await fs5.access(dcsDir);
|
|
1362
1699
|
} catch {
|
|
1363
1700
|
console.log(chalk5.red("No .dcs directory found."));
|
|
1364
1701
|
console.log("Run", chalk5.cyan("dcs init"), "to create DCS configuration.");
|
|
@@ -1404,7 +1741,7 @@ async function validateCommand(options) {
|
|
|
1404
1741
|
}
|
|
1405
1742
|
}
|
|
1406
1743
|
async function validateSiteYaml(dcsDir, verbose) {
|
|
1407
|
-
const filePath =
|
|
1744
|
+
const filePath = path3.join(dcsDir, "site.yaml");
|
|
1408
1745
|
const result = {
|
|
1409
1746
|
file: ".dcs/site.yaml",
|
|
1410
1747
|
valid: true,
|
|
@@ -1412,28 +1749,28 @@ async function validateSiteYaml(dcsDir, verbose) {
|
|
|
1412
1749
|
warnings: []
|
|
1413
1750
|
};
|
|
1414
1751
|
try {
|
|
1415
|
-
const content = await
|
|
1416
|
-
const
|
|
1417
|
-
if (!
|
|
1752
|
+
const content = await fs5.readFile(filePath, "utf8");
|
|
1753
|
+
const config = yaml.load(content);
|
|
1754
|
+
if (!config.site_name) {
|
|
1418
1755
|
result.errors.push("Missing required field: site_name");
|
|
1419
1756
|
result.valid = false;
|
|
1420
1757
|
}
|
|
1421
|
-
if (!
|
|
1758
|
+
if (!config.site_slug) {
|
|
1422
1759
|
result.errors.push("Missing required field: site_slug");
|
|
1423
1760
|
result.valid = false;
|
|
1424
|
-
} else if (!/^[a-z0-9-]+$/.test(
|
|
1761
|
+
} else if (!/^[a-z0-9-]+$/.test(config.site_slug)) {
|
|
1425
1762
|
result.errors.push("site_slug must be lowercase alphanumeric with hyphens only");
|
|
1426
1763
|
result.valid = false;
|
|
1427
1764
|
}
|
|
1428
|
-
if (!
|
|
1765
|
+
if (!config.swa_resource_id) {
|
|
1429
1766
|
result.warnings.push("swa_resource_id is not set (required for deployment)");
|
|
1430
1767
|
}
|
|
1431
|
-
if (!
|
|
1768
|
+
if (!config.production_url) {
|
|
1432
1769
|
result.warnings.push("production_url is not set");
|
|
1433
1770
|
}
|
|
1434
1771
|
if (verbose) {
|
|
1435
|
-
console.log(chalk5.dim(` site_name: ${
|
|
1436
|
-
console.log(chalk5.dim(` site_slug: ${
|
|
1772
|
+
console.log(chalk5.dim(` site_name: ${config.site_name}`));
|
|
1773
|
+
console.log(chalk5.dim(` site_slug: ${config.site_slug}`));
|
|
1437
1774
|
}
|
|
1438
1775
|
} catch (error) {
|
|
1439
1776
|
if (error.code === "ENOENT") {
|
|
@@ -1446,7 +1783,7 @@ async function validateSiteYaml(dcsDir, verbose) {
|
|
|
1446
1783
|
return result;
|
|
1447
1784
|
}
|
|
1448
1785
|
async function validatePagesYaml(dcsDir, verbose) {
|
|
1449
|
-
const filePath =
|
|
1786
|
+
const filePath = path3.join(dcsDir, "pages.yaml");
|
|
1450
1787
|
const result = {
|
|
1451
1788
|
file: ".dcs/pages.yaml",
|
|
1452
1789
|
valid: true,
|
|
@@ -1454,17 +1791,17 @@ async function validatePagesYaml(dcsDir, verbose) {
|
|
|
1454
1791
|
warnings: []
|
|
1455
1792
|
};
|
|
1456
1793
|
try {
|
|
1457
|
-
const content = await
|
|
1458
|
-
const
|
|
1459
|
-
if (
|
|
1794
|
+
const content = await fs5.readFile(filePath, "utf8");
|
|
1795
|
+
const config = yaml.load(content);
|
|
1796
|
+
if (config.version === void 0) {
|
|
1460
1797
|
result.errors.push("Missing required field: version");
|
|
1461
1798
|
result.valid = false;
|
|
1462
1799
|
}
|
|
1463
|
-
if (!
|
|
1800
|
+
if (!config.siteSlug) {
|
|
1464
1801
|
result.errors.push("Missing required field: siteSlug");
|
|
1465
1802
|
result.valid = false;
|
|
1466
1803
|
}
|
|
1467
|
-
const pages =
|
|
1804
|
+
const pages = config.pages;
|
|
1468
1805
|
if (!pages || !Array.isArray(pages)) {
|
|
1469
1806
|
result.errors.push("Missing or invalid pages array");
|
|
1470
1807
|
result.valid = false;
|
|
@@ -1502,7 +1839,7 @@ async function validatePagesYaml(dcsDir, verbose) {
|
|
|
1502
1839
|
return result;
|
|
1503
1840
|
}
|
|
1504
1841
|
async function validateContentYaml(dcsDir, verbose) {
|
|
1505
|
-
const filePath =
|
|
1842
|
+
const filePath = path3.join(dcsDir, "content.yaml");
|
|
1506
1843
|
const result = {
|
|
1507
1844
|
file: ".dcs/content.yaml",
|
|
1508
1845
|
valid: true,
|
|
@@ -1510,23 +1847,23 @@ async function validateContentYaml(dcsDir, verbose) {
|
|
|
1510
1847
|
warnings: []
|
|
1511
1848
|
};
|
|
1512
1849
|
try {
|
|
1513
|
-
const content = await
|
|
1514
|
-
const
|
|
1515
|
-
if (
|
|
1850
|
+
const content = await fs5.readFile(filePath, "utf8");
|
|
1851
|
+
const config = yaml.load(content);
|
|
1852
|
+
if (config.version === void 0) {
|
|
1516
1853
|
result.errors.push("Missing required field: version");
|
|
1517
1854
|
result.valid = false;
|
|
1518
1855
|
}
|
|
1519
|
-
if (
|
|
1856
|
+
if (config.global !== void 0 && typeof config.global !== "object") {
|
|
1520
1857
|
result.errors.push("global must be an object");
|
|
1521
1858
|
result.valid = false;
|
|
1522
1859
|
}
|
|
1523
|
-
if (
|
|
1860
|
+
if (config.pages !== void 0 && typeof config.pages !== "object") {
|
|
1524
1861
|
result.errors.push("pages must be an object");
|
|
1525
1862
|
result.valid = false;
|
|
1526
1863
|
}
|
|
1527
1864
|
if (verbose) {
|
|
1528
|
-
const globalKeys =
|
|
1529
|
-
const pageCount =
|
|
1865
|
+
const globalKeys = config.global ? Object.keys(config.global).length : 0;
|
|
1866
|
+
const pageCount = config.pages ? Object.keys(config.pages).length : 0;
|
|
1530
1867
|
console.log(chalk5.dim(` ${globalKeys} global keys, ${pageCount} page sections`));
|
|
1531
1868
|
}
|
|
1532
1869
|
} catch (error) {
|
|
@@ -1540,7 +1877,7 @@ async function validateContentYaml(dcsDir, verbose) {
|
|
|
1540
1877
|
return result;
|
|
1541
1878
|
}
|
|
1542
1879
|
async function validateSeoYaml(dcsDir, verbose) {
|
|
1543
|
-
const filePath =
|
|
1880
|
+
const filePath = path3.join(dcsDir, "seo.yaml");
|
|
1544
1881
|
const result = {
|
|
1545
1882
|
file: ".dcs/seo.yaml",
|
|
1546
1883
|
valid: true,
|
|
@@ -1548,13 +1885,13 @@ async function validateSeoYaml(dcsDir, verbose) {
|
|
|
1548
1885
|
warnings: []
|
|
1549
1886
|
};
|
|
1550
1887
|
try {
|
|
1551
|
-
const content = await
|
|
1552
|
-
const
|
|
1553
|
-
if (
|
|
1888
|
+
const content = await fs5.readFile(filePath, "utf8");
|
|
1889
|
+
const config = yaml.load(content);
|
|
1890
|
+
if (config.version === void 0) {
|
|
1554
1891
|
result.errors.push("Missing required field: version");
|
|
1555
1892
|
result.valid = false;
|
|
1556
1893
|
}
|
|
1557
|
-
const global =
|
|
1894
|
+
const global = config.global;
|
|
1558
1895
|
if (!global) {
|
|
1559
1896
|
result.errors.push("Missing required section: global");
|
|
1560
1897
|
result.valid = false;
|
|
@@ -1569,7 +1906,7 @@ async function validateSeoYaml(dcsDir, verbose) {
|
|
|
1569
1906
|
result.warnings.push("global.defaultDescription is not set");
|
|
1570
1907
|
}
|
|
1571
1908
|
}
|
|
1572
|
-
const pages =
|
|
1909
|
+
const pages = config.pages;
|
|
1573
1910
|
if (pages && typeof pages === "object") {
|
|
1574
1911
|
const pageKeys = Object.keys(pages);
|
|
1575
1912
|
if (!pageKeys.includes("home")) {
|
|
@@ -1591,20 +1928,20 @@ async function validateSeoYaml(dcsDir, verbose) {
|
|
|
1591
1928
|
}
|
|
1592
1929
|
|
|
1593
1930
|
// src/commands/plans.ts
|
|
1594
|
-
import
|
|
1595
|
-
import
|
|
1931
|
+
import fs6 from "fs/promises";
|
|
1932
|
+
import path4 from "path";
|
|
1596
1933
|
import chalk6 from "chalk";
|
|
1597
1934
|
import yaml2 from "js-yaml";
|
|
1598
1935
|
async function plansCommand(options) {
|
|
1599
1936
|
const { target = ".", force = false } = options;
|
|
1600
|
-
const targetDir =
|
|
1601
|
-
const dcsDir =
|
|
1602
|
-
const plansDir =
|
|
1937
|
+
const targetDir = path4.resolve(target);
|
|
1938
|
+
const dcsDir = path4.join(targetDir, ".dcs");
|
|
1939
|
+
const plansDir = path4.join(targetDir, ".plans");
|
|
1603
1940
|
console.log(chalk6.bold("Generating DCS integration plans..."));
|
|
1604
1941
|
console.log(chalk6.dim(`Directory: ${targetDir}`));
|
|
1605
1942
|
console.log();
|
|
1606
1943
|
try {
|
|
1607
|
-
await
|
|
1944
|
+
await fs6.access(dcsDir);
|
|
1608
1945
|
} catch {
|
|
1609
1946
|
console.log(chalk6.red("No .dcs directory found."));
|
|
1610
1947
|
console.log("Run", chalk6.cyan("dcs init"), "first to create DCS configuration.");
|
|
@@ -1613,7 +1950,7 @@ async function plansCommand(options) {
|
|
|
1613
1950
|
let siteName = "Customer Site";
|
|
1614
1951
|
let framework = "vue";
|
|
1615
1952
|
try {
|
|
1616
|
-
const siteYaml = await
|
|
1953
|
+
const siteYaml = await fs6.readFile(path4.join(dcsDir, "site.yaml"), "utf8");
|
|
1617
1954
|
const siteConfig = yaml2.load(siteYaml);
|
|
1618
1955
|
siteName = siteConfig.site_name || siteName;
|
|
1619
1956
|
const metadata = siteConfig.metadata;
|
|
@@ -1629,19 +1966,19 @@ async function plansCommand(options) {
|
|
|
1629
1966
|
{ name: "04-capture-snapshots.md", content: generateSnapshotsPlan(siteName) },
|
|
1630
1967
|
{ name: "05-verify-deployment.md", content: generateVerifyPlan(siteName) }
|
|
1631
1968
|
];
|
|
1632
|
-
await
|
|
1969
|
+
await fs6.mkdir(plansDir, { recursive: true });
|
|
1633
1970
|
const generatedFiles = [];
|
|
1634
1971
|
for (const plan of plans) {
|
|
1635
|
-
const filePath =
|
|
1972
|
+
const filePath = path4.join(plansDir, plan.name);
|
|
1636
1973
|
try {
|
|
1637
|
-
await
|
|
1974
|
+
await fs6.access(filePath);
|
|
1638
1975
|
if (!force) {
|
|
1639
1976
|
console.log(chalk6.yellow(`Skipped (exists): .plans/${plan.name}`));
|
|
1640
1977
|
continue;
|
|
1641
1978
|
}
|
|
1642
1979
|
} catch {
|
|
1643
1980
|
}
|
|
1644
|
-
await
|
|
1981
|
+
await fs6.writeFile(filePath, plan.content, "utf8");
|
|
1645
1982
|
generatedFiles.push(plan.name);
|
|
1646
1983
|
console.log(`${chalk6.green("\u2713")} .plans/${plan.name}`);
|
|
1647
1984
|
}
|
|
@@ -2100,8 +2437,8 @@ Once verified:
|
|
|
2100
2437
|
}
|
|
2101
2438
|
|
|
2102
2439
|
// src/commands/capture-snapshots.ts
|
|
2103
|
-
import
|
|
2104
|
-
import
|
|
2440
|
+
import fs7 from "fs";
|
|
2441
|
+
import path5 from "path";
|
|
2105
2442
|
import yaml3 from "js-yaml";
|
|
2106
2443
|
import chalk7 from "chalk";
|
|
2107
2444
|
import ora3 from "ora";
|
|
@@ -2133,15 +2470,15 @@ async function waitForAboveFoldAssets(page) {
|
|
|
2133
2470
|
if (img.complete && img.naturalWidth > 0) {
|
|
2134
2471
|
return Promise.resolve();
|
|
2135
2472
|
}
|
|
2136
|
-
return new Promise((
|
|
2137
|
-
const done = () =>
|
|
2473
|
+
return new Promise((resolve2) => {
|
|
2474
|
+
const done = () => resolve2();
|
|
2138
2475
|
img.addEventListener("load", done, { once: true });
|
|
2139
2476
|
img.addEventListener("error", done, { once: true });
|
|
2140
2477
|
});
|
|
2141
2478
|
}));
|
|
2142
2479
|
await Promise.race([
|
|
2143
2480
|
Promise.all([waitForFonts, waitForImages]),
|
|
2144
|
-
new Promise((
|
|
2481
|
+
new Promise((resolve2) => window.setTimeout(resolve2, timeoutMs))
|
|
2145
2482
|
]);
|
|
2146
2483
|
}, ASSET_WAIT_TIMEOUT_MS).catch((error) => {
|
|
2147
2484
|
console.warn(` Warning: could not verify above-fold assets: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2164,7 +2501,7 @@ async function resetScrollForFullPageCapture(page) {
|
|
|
2164
2501
|
window.dispatchEvent(new Event("scroll"));
|
|
2165
2502
|
});
|
|
2166
2503
|
await page.waitForFunction(() => Math.abs(window.scrollY) < 1, void 0, { timeout: 1e3 }).catch(() => void 0);
|
|
2167
|
-
await page.evaluate(() => new Promise((
|
|
2504
|
+
await page.evaluate(() => new Promise((resolve2) => requestAnimationFrame(() => requestAnimationFrame(resolve2)))).catch(() => void 0);
|
|
2168
2505
|
await page.waitForTimeout(350);
|
|
2169
2506
|
await waitForAboveFoldAssets(page);
|
|
2170
2507
|
}
|
|
@@ -2207,7 +2544,7 @@ async function withFullPageCaptureLayout(page, capture) {
|
|
|
2207
2544
|
window.scrollTo(0, 0);
|
|
2208
2545
|
window.dispatchEvent(new Event("scroll"));
|
|
2209
2546
|
});
|
|
2210
|
-
await page.evaluate(() => new Promise((
|
|
2547
|
+
await page.evaluate(() => new Promise((resolve2) => requestAnimationFrame(() => requestAnimationFrame(resolve2)))).catch(() => void 0);
|
|
2211
2548
|
try {
|
|
2212
2549
|
return await capture();
|
|
2213
2550
|
} finally {
|
|
@@ -2246,27 +2583,27 @@ var SECTION_SELECTORS = {
|
|
|
2246
2583
|
cta: '[class*="cta"], [data-section-type="cta"], .call-to-action'
|
|
2247
2584
|
};
|
|
2248
2585
|
function loadPagesConfig(targetDir) {
|
|
2249
|
-
const configPath =
|
|
2250
|
-
if (!
|
|
2586
|
+
const configPath = path5.join(targetDir, ".dcs", "pages.yaml");
|
|
2587
|
+
if (!fs7.existsSync(configPath)) {
|
|
2251
2588
|
throw new Error(`Configuration file not found: ${configPath}`);
|
|
2252
2589
|
}
|
|
2253
|
-
const content =
|
|
2590
|
+
const content = fs7.readFileSync(configPath, "utf-8");
|
|
2254
2591
|
return yaml3.load(content);
|
|
2255
2592
|
}
|
|
2256
2593
|
function loadContentConfig(targetDir) {
|
|
2257
|
-
const contentPath =
|
|
2258
|
-
if (!
|
|
2594
|
+
const contentPath = path5.join(targetDir, ".dcs", "content.yaml");
|
|
2595
|
+
if (!fs7.existsSync(contentPath)) {
|
|
2259
2596
|
return null;
|
|
2260
2597
|
}
|
|
2261
|
-
const content =
|
|
2598
|
+
const content = fs7.readFileSync(contentPath, "utf-8");
|
|
2262
2599
|
return yaml3.load(content);
|
|
2263
2600
|
}
|
|
2264
2601
|
function loadTargetedSnapshotsConfig(targetDir) {
|
|
2265
|
-
const targetedPath =
|
|
2266
|
-
if (!
|
|
2602
|
+
const targetedPath = path5.join(targetDir, ".dcs", "targeted-snapshots.yaml");
|
|
2603
|
+
if (!fs7.existsSync(targetedPath)) {
|
|
2267
2604
|
return null;
|
|
2268
2605
|
}
|
|
2269
|
-
const content =
|
|
2606
|
+
const content = fs7.readFileSync(targetedPath, "utf-8");
|
|
2270
2607
|
return yaml3.load(content);
|
|
2271
2608
|
}
|
|
2272
2609
|
function getValidTextKeysForPage(contentConfig, pageSlug) {
|
|
@@ -2286,27 +2623,27 @@ function getValidTextKeysForPage(contentConfig, pageSlug) {
|
|
|
2286
2623
|
}
|
|
2287
2624
|
return keys;
|
|
2288
2625
|
}
|
|
2289
|
-
async function resolveAllPages(
|
|
2290
|
-
const
|
|
2291
|
-
for (const page of
|
|
2626
|
+
async function resolveAllPages(config) {
|
|
2627
|
+
const resolved2 = [];
|
|
2628
|
+
for (const page of config.pages) {
|
|
2292
2629
|
if (page.type === "dynamic" && page.instances) {
|
|
2293
2630
|
for (const instance of page.instances) {
|
|
2294
2631
|
const instancePath = page.pathTemplate ? page.pathTemplate.replace(/:[\w]+/, instance) : `${page.path}/${instance}`;
|
|
2295
|
-
|
|
2632
|
+
resolved2.push({
|
|
2296
2633
|
slug: `${page.slug}-${instance}`,
|
|
2297
2634
|
path: instancePath,
|
|
2298
2635
|
config: { ...page, slug: `${page.slug}-${instance}` }
|
|
2299
2636
|
});
|
|
2300
2637
|
}
|
|
2301
2638
|
} else {
|
|
2302
|
-
|
|
2639
|
+
resolved2.push({
|
|
2303
2640
|
slug: page.slug,
|
|
2304
2641
|
path: page.path,
|
|
2305
2642
|
config: page
|
|
2306
2643
|
});
|
|
2307
2644
|
}
|
|
2308
2645
|
}
|
|
2309
|
-
return
|
|
2646
|
+
return resolved2;
|
|
2310
2647
|
}
|
|
2311
2648
|
async function getDomPath(element) {
|
|
2312
2649
|
return element.evaluate((el) => {
|
|
@@ -2434,12 +2771,12 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2434
2771
|
}
|
|
2435
2772
|
await page.waitForTimeout(snapshotConfig.waitAfterLoad);
|
|
2436
2773
|
await waitForAboveFoldAssets(page);
|
|
2437
|
-
const pageOutputDir =
|
|
2438
|
-
|
|
2774
|
+
const pageOutputDir = path5.join(outputDir, siteSlug, pageInfo.slug);
|
|
2775
|
+
fs7.mkdirSync(path5.join(pageOutputDir, "sections"), { recursive: true });
|
|
2439
2776
|
const title = await page.title();
|
|
2440
2777
|
let pageHeight = await lazyLoadPage(page, snapshotConfig.viewport.height);
|
|
2441
2778
|
await resetScrollForFullPageCapture(page);
|
|
2442
|
-
const fullPagePath =
|
|
2779
|
+
const fullPagePath = path5.join(pageOutputDir, "full-page.png");
|
|
2443
2780
|
await withFullPageCaptureLayout(
|
|
2444
2781
|
page,
|
|
2445
2782
|
() => page.screenshot({
|
|
@@ -2447,7 +2784,7 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2447
2784
|
fullPage: snapshotConfig.captureFullPage
|
|
2448
2785
|
})
|
|
2449
2786
|
);
|
|
2450
|
-
const thumbnailPath =
|
|
2787
|
+
const thumbnailPath = path5.join(pageOutputDir, "thumbnail.png");
|
|
2451
2788
|
await captureHeaderThumbnail(page, thumbnailPath, snapshotConfig.viewport);
|
|
2452
2789
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
2453
2790
|
await page.waitForTimeout(100);
|
|
@@ -2501,7 +2838,7 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2501
2838
|
const bounds = await getAccurateBounds(element, resolvedType);
|
|
2502
2839
|
if (!bounds || bounds.height < 10) continue;
|
|
2503
2840
|
const sectionId = basicMetadata.sectionId || `section-${i}`;
|
|
2504
|
-
const sectionPath =
|
|
2841
|
+
const sectionPath = path5.join(pageOutputDir, "sections", `${sectionId}.png`);
|
|
2505
2842
|
try {
|
|
2506
2843
|
await element.screenshot({ path: sectionPath });
|
|
2507
2844
|
} catch {
|
|
@@ -2572,7 +2909,7 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2572
2909
|
const bounds = await getAccurateBounds(element, sectionType);
|
|
2573
2910
|
if (!bounds || bounds.height < 10) continue;
|
|
2574
2911
|
const sectionId = `${sectionType}-${i}`;
|
|
2575
|
-
const sectionPath =
|
|
2912
|
+
const sectionPath = path5.join(pageOutputDir, "sections", `${sectionId}.png`);
|
|
2576
2913
|
try {
|
|
2577
2914
|
await element.screenshot({ path: sectionPath });
|
|
2578
2915
|
} catch {
|
|
@@ -2625,7 +2962,7 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2625
2962
|
});
|
|
2626
2963
|
if (!bounds || bounds.height < 10) continue;
|
|
2627
2964
|
const sectionId = `content-fallback-${i}`;
|
|
2628
|
-
const sectionPath =
|
|
2965
|
+
const sectionPath = path5.join(pageOutputDir, "sections", `${sectionId}.png`);
|
|
2629
2966
|
try {
|
|
2630
2967
|
await element.screenshot({ path: sectionPath });
|
|
2631
2968
|
} catch {
|
|
@@ -2672,22 +3009,22 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2672
3009
|
}
|
|
2673
3010
|
async function captureSnapshotsCommand(options) {
|
|
2674
3011
|
const { target, baseUrl, dryRun, verbose, pages: targetedPages } = options;
|
|
2675
|
-
const targetDir =
|
|
3012
|
+
const targetDir = path5.resolve(target);
|
|
2676
3013
|
console.log(chalk7.blue("\u{1F4F8} Page Snapshot Capture"));
|
|
2677
3014
|
console.log(chalk7.gray("========================"));
|
|
2678
3015
|
console.log(chalk7.gray(`Target: ${targetDir}`));
|
|
2679
3016
|
console.log(chalk7.gray(`Base URL: ${baseUrl}`));
|
|
2680
3017
|
process.env.SITE_BASE_URL = baseUrl;
|
|
2681
|
-
let
|
|
3018
|
+
let config;
|
|
2682
3019
|
try {
|
|
2683
|
-
|
|
3020
|
+
config = loadPagesConfig(targetDir);
|
|
2684
3021
|
} catch (error) {
|
|
2685
3022
|
console.error(chalk7.red("Error:"), error.message);
|
|
2686
3023
|
console.log(chalk7.yellow("\nMake sure .dcs/pages.yaml exists in the target directory."));
|
|
2687
3024
|
process.exit(1);
|
|
2688
3025
|
}
|
|
2689
|
-
console.log(chalk7.gray(`Site: ${
|
|
2690
|
-
const outputDir =
|
|
3026
|
+
console.log(chalk7.gray(`Site: ${config.siteSlug}`));
|
|
3027
|
+
const outputDir = path5.join(targetDir, config.snapshot.outputDir || ".dcs/snapshots");
|
|
2691
3028
|
const contentConfig = loadContentConfig(targetDir);
|
|
2692
3029
|
if (contentConfig) {
|
|
2693
3030
|
const pageCount = Object.keys(contentConfig.pages || {}).length;
|
|
@@ -2704,19 +3041,19 @@ async function captureSnapshotsCommand(options) {
|
|
|
2704
3041
|
console.log(chalk7.gray(` Reason: ${targetedConfig.reason}`));
|
|
2705
3042
|
console.log(chalk7.gray(" No snapshots will be captured."));
|
|
2706
3043
|
if (!dryRun) {
|
|
2707
|
-
|
|
3044
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
2708
3045
|
const skipMarker = {
|
|
2709
3046
|
skipped: true,
|
|
2710
3047
|
reason: targetedConfig.reason,
|
|
2711
3048
|
triggeredBy: targetedConfig.triggeredBy,
|
|
2712
3049
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2713
3050
|
};
|
|
2714
|
-
|
|
3051
|
+
fs7.writeFileSync(path5.join(outputDir, "skipped.json"), JSON.stringify(skipMarker, null, 2));
|
|
2715
3052
|
}
|
|
2716
3053
|
console.log(chalk7.green("\n\u2705 Snapshot capture skipped (as requested)"));
|
|
2717
3054
|
return;
|
|
2718
3055
|
}
|
|
2719
|
-
let pagesToCapture = await resolveAllPages(
|
|
3056
|
+
let pagesToCapture = await resolveAllPages(config);
|
|
2720
3057
|
console.log(chalk7.gray(`Found ${pagesToCapture.length} total pages in configuration`));
|
|
2721
3058
|
if (targetedPages && targetedPages.length > 0) {
|
|
2722
3059
|
const targetedSlugs = new Set(targetedPages);
|
|
@@ -2746,10 +3083,10 @@ async function captureSnapshotsCommand(options) {
|
|
|
2746
3083
|
return;
|
|
2747
3084
|
}
|
|
2748
3085
|
const { chromium } = await import("playwright");
|
|
2749
|
-
if (
|
|
2750
|
-
|
|
3086
|
+
if (fs7.existsSync(outputDir)) {
|
|
3087
|
+
fs7.rmSync(outputDir, { recursive: true });
|
|
2751
3088
|
}
|
|
2752
|
-
|
|
3089
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
2753
3090
|
const spinner = ora3("Launching browser...").start();
|
|
2754
3091
|
const browser = await chromium.launch();
|
|
2755
3092
|
spinner.succeed("Browser launched");
|
|
@@ -2762,15 +3099,15 @@ async function captureSnapshotsCommand(options) {
|
|
|
2762
3099
|
const snapshot = await capturePageSnapshot(
|
|
2763
3100
|
browser,
|
|
2764
3101
|
pageInfo,
|
|
2765
|
-
|
|
2766
|
-
|
|
3102
|
+
config.snapshot,
|
|
3103
|
+
config.siteSlug,
|
|
2767
3104
|
validTextKeys,
|
|
2768
3105
|
outputDir,
|
|
2769
3106
|
verbose || false
|
|
2770
3107
|
);
|
|
2771
3108
|
snapshots.push(snapshot);
|
|
2772
|
-
const snapshotPath =
|
|
2773
|
-
|
|
3109
|
+
const snapshotPath = path5.join(outputDir, config.siteSlug, pageInfo.slug, "snapshot.json");
|
|
3110
|
+
fs7.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
2774
3111
|
pageSpinner.succeed(`${pageInfo.slug}: ${snapshot.sections.length} sections captured`);
|
|
2775
3112
|
} catch (error) {
|
|
2776
3113
|
const message = `${pageInfo.slug}: ${error.message}`;
|
|
@@ -2780,11 +3117,11 @@ async function captureSnapshotsCommand(options) {
|
|
|
2780
3117
|
}
|
|
2781
3118
|
await browser.close();
|
|
2782
3119
|
const manifest = {
|
|
2783
|
-
siteSlug:
|
|
3120
|
+
siteSlug: config.siteSlug,
|
|
2784
3121
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2785
3122
|
capturedVersion: process.env.GITHUB_SHA,
|
|
2786
3123
|
deploymentRef: process.env.GITHUB_REF || "local",
|
|
2787
|
-
pagesConfigVersion:
|
|
3124
|
+
pagesConfigVersion: config.version,
|
|
2788
3125
|
pages: snapshots.map((s) => ({
|
|
2789
3126
|
pageSlug: s.pageSlug,
|
|
2790
3127
|
pagePath: s.path,
|
|
@@ -2796,11 +3133,11 @@ async function captureSnapshotsCommand(options) {
|
|
|
2796
3133
|
hasSnapshot: true
|
|
2797
3134
|
}))
|
|
2798
3135
|
};
|
|
2799
|
-
const manifestPath =
|
|
3136
|
+
const manifestPath = path5.join(outputDir, config.siteSlug, "manifest.json");
|
|
2800
3137
|
if (isTargetedCapture) {
|
|
2801
3138
|
console.log(chalk7.gray(" Targeted capture: leaving the existing remote manifest intact"));
|
|
2802
3139
|
} else {
|
|
2803
|
-
|
|
3140
|
+
fs7.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2804
3141
|
}
|
|
2805
3142
|
console.log("");
|
|
2806
3143
|
console.log(chalk7.green("\u2705 Snapshot capture complete!"));
|