opensteer 0.6.13 → 0.7.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 +256 -184
- package/dist/chunk-PQYA6IX2.js +32571 -0
- package/dist/chunk-PQYA6IX2.js.map +1 -0
- package/dist/cli/bin.cjs +38201 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +5612 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/index.cjs +31309 -16009
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4440 -670
- package/dist/index.d.ts +4440 -670
- package/dist/index.js +438 -378
- package/dist/index.js.map +1 -0
- package/package.json +56 -62
- package/skills/README.md +21 -20
- package/skills/opensteer/SKILL.md +60 -194
- package/skills/opensteer/references/cli-reference.md +69 -113
- package/skills/opensteer/references/request-workflow.md +81 -0
- package/skills/opensteer/references/sdk-reference.md +101 -154
- package/CHANGELOG.md +0 -75
- package/bin/opensteer.mjs +0 -1423
- package/dist/browser-profile-client-CGXc0-P9.d.cts +0 -228
- package/dist/browser-profile-client-DHLzMf-K.d.ts +0 -228
- package/dist/chunk-2ES46WCO.js +0 -1437
- package/dist/chunk-3H5RRIMZ.js +0 -69
- package/dist/chunk-AVXUMEDG.js +0 -62
- package/dist/chunk-DN3GI5CH.js +0 -57
- package/dist/chunk-FAHE5DB2.js +0 -190
- package/dist/chunk-HBTSQ2V4.js +0 -15259
- package/dist/chunk-K5CL76MG.js +0 -81
- package/dist/chunk-U724TBY6.js +0 -1262
- package/dist/chunk-ZRCFF546.js +0 -77
- package/dist/cli/auth.cjs +0 -2022
- package/dist/cli/auth.d.cts +0 -114
- package/dist/cli/auth.d.ts +0 -114
- package/dist/cli/auth.js +0 -15
- package/dist/cli/local-profile.cjs +0 -197
- package/dist/cli/local-profile.d.cts +0 -18
- package/dist/cli/local-profile.d.ts +0 -18
- package/dist/cli/local-profile.js +0 -97
- package/dist/cli/profile.cjs +0 -18548
- package/dist/cli/profile.d.cts +0 -79
- package/dist/cli/profile.d.ts +0 -79
- package/dist/cli/profile.js +0 -1328
- package/dist/cli/server.cjs +0 -17232
- package/dist/cli/server.d.cts +0 -2
- package/dist/cli/server.d.ts +0 -2
- package/dist/cli/server.js +0 -977
- package/dist/cli/skills-installer.cjs +0 -230
- package/dist/cli/skills-installer.d.cts +0 -28
- package/dist/cli/skills-installer.d.ts +0 -28
- package/dist/cli/skills-installer.js +0 -201
- package/dist/extractor-4Q3TFZJB.js +0 -8
- package/dist/resolver-MGN64KCP.js +0 -7
- package/dist/types-Cr10igF3.d.cts +0 -345
- package/dist/types-Cr10igF3.d.ts +0 -345
- package/skills/electron/SKILL.md +0 -87
- package/skills/electron/references/opensteer-electron-recipes.md +0 -88
- package/skills/electron/references/opensteer-electron-workflow.md +0 -85
- package/skills/opensteer/references/examples.md +0 -118
package/dist/cli/profile.js
DELETED
|
@@ -1,1328 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BrowserProfileClient
|
|
3
|
-
} from "../chunk-ZRCFF546.js";
|
|
4
|
-
import {
|
|
5
|
-
createKeychainStore,
|
|
6
|
-
ensureCloudCredentialsForCommand
|
|
7
|
-
} from "../chunk-U724TBY6.js";
|
|
8
|
-
import {
|
|
9
|
-
Opensteer
|
|
10
|
-
} from "../chunk-HBTSQ2V4.js";
|
|
11
|
-
import {
|
|
12
|
-
resolveConfigWithEnv
|
|
13
|
-
} from "../chunk-2ES46WCO.js";
|
|
14
|
-
import {
|
|
15
|
-
expandHome
|
|
16
|
-
} from "../chunk-K5CL76MG.js";
|
|
17
|
-
import "../chunk-3H5RRIMZ.js";
|
|
18
|
-
|
|
19
|
-
// src/cli/profile.ts
|
|
20
|
-
import path from "path";
|
|
21
|
-
import { createInterface } from "readline/promises";
|
|
22
|
-
|
|
23
|
-
// src/browser/chromium-profile.ts
|
|
24
|
-
import { promisify } from "util";
|
|
25
|
-
import { execFile } from "child_process";
|
|
26
|
-
import { createDecipheriv, createHash, pbkdf2Sync } from "crypto";
|
|
27
|
-
import {
|
|
28
|
-
cp,
|
|
29
|
-
copyFile,
|
|
30
|
-
mkdtemp,
|
|
31
|
-
readdir,
|
|
32
|
-
readFile,
|
|
33
|
-
rm
|
|
34
|
-
} from "fs/promises";
|
|
35
|
-
import { existsSync, statSync } from "fs";
|
|
36
|
-
import { basename, dirname, join } from "path";
|
|
37
|
-
import { tmpdir } from "os";
|
|
38
|
-
import { chromium } from "playwright";
|
|
39
|
-
var execFileAsync = promisify(execFile);
|
|
40
|
-
var CHROMIUM_EPOCH_MICROS = 11644473600000000n;
|
|
41
|
-
var AES_BLOCK_BYTES = 16;
|
|
42
|
-
var MAC_KEY_ITERATIONS = 1003;
|
|
43
|
-
var LINUX_KEY_ITERATIONS = 1;
|
|
44
|
-
var KEY_LENGTH = 16;
|
|
45
|
-
var KEY_SALT = "saltysalt";
|
|
46
|
-
var DEFAULT_CHROMIUM_BRAND = {
|
|
47
|
-
macService: "Chrome Safe Storage",
|
|
48
|
-
macAccount: "Chrome",
|
|
49
|
-
linuxApplications: ["chrome", "google-chrome"]
|
|
50
|
-
};
|
|
51
|
-
var CHROMIUM_BRANDS = [
|
|
52
|
-
{
|
|
53
|
-
match: ["bravesoftware", "brave-browser"],
|
|
54
|
-
brand: {
|
|
55
|
-
macService: "Brave Safe Storage",
|
|
56
|
-
macAccount: "Brave",
|
|
57
|
-
linuxApplications: ["brave-browser", "brave"]
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
match: ["microsoft", "edge"],
|
|
62
|
-
brand: {
|
|
63
|
-
macService: "Microsoft Edge Safe Storage",
|
|
64
|
-
macAccount: "Microsoft Edge",
|
|
65
|
-
linuxApplications: ["microsoft-edge"],
|
|
66
|
-
playwrightChannel: "msedge"
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
match: ["google", "chrome beta"],
|
|
71
|
-
brand: {
|
|
72
|
-
macService: "Chrome Beta Safe Storage",
|
|
73
|
-
macAccount: "Chrome Beta",
|
|
74
|
-
linuxApplications: ["chrome-beta"],
|
|
75
|
-
playwrightChannel: "chrome-beta"
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
match: ["google", "chrome"],
|
|
80
|
-
brand: {
|
|
81
|
-
macService: "Chrome Safe Storage",
|
|
82
|
-
macAccount: "Chrome",
|
|
83
|
-
linuxApplications: ["chrome", "google-chrome"],
|
|
84
|
-
playwrightChannel: "chrome"
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
match: ["chromium"],
|
|
89
|
-
brand: {
|
|
90
|
-
macService: "Chromium Safe Storage",
|
|
91
|
-
macAccount: "Chromium",
|
|
92
|
-
linuxApplications: ["chromium"]
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
];
|
|
96
|
-
function directoryExists(filePath) {
|
|
97
|
-
try {
|
|
98
|
-
return statSync(filePath).isDirectory();
|
|
99
|
-
} catch {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function fileExists(filePath) {
|
|
104
|
-
try {
|
|
105
|
-
return statSync(filePath).isFile();
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function resolveCookieDbPath(profileDir) {
|
|
111
|
-
const candidates = [join(profileDir, "Network", "Cookies"), join(profileDir, "Cookies")];
|
|
112
|
-
for (const candidate of candidates) {
|
|
113
|
-
if (fileExists(candidate)) {
|
|
114
|
-
return candidate;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
async function selectProfileDirFromUserDataDir(userDataDir) {
|
|
120
|
-
const entries = await readdir(userDataDir, {
|
|
121
|
-
withFileTypes: true
|
|
122
|
-
}).catch(() => []);
|
|
123
|
-
const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => join(userDataDir, entry.name)).filter((entryPath) => resolveCookieDbPath(entryPath));
|
|
124
|
-
return candidates;
|
|
125
|
-
}
|
|
126
|
-
async function resolveChromiumProfileLocation(inputPath) {
|
|
127
|
-
const expandedPath = expandHome(inputPath.trim());
|
|
128
|
-
if (!expandedPath) {
|
|
129
|
-
throw new Error("Profile path cannot be empty.");
|
|
130
|
-
}
|
|
131
|
-
if (fileExists(expandedPath) && basename(expandedPath) === "Cookies") {
|
|
132
|
-
const directParent = dirname(expandedPath);
|
|
133
|
-
const profileDir = basename(directParent) === "Network" ? dirname(directParent) : directParent;
|
|
134
|
-
const userDataDir = dirname(profileDir);
|
|
135
|
-
return {
|
|
136
|
-
userDataDir,
|
|
137
|
-
profileDir,
|
|
138
|
-
profileDirectory: basename(profileDir),
|
|
139
|
-
cookieDbPath: expandedPath,
|
|
140
|
-
localStatePath: fileExists(join(userDataDir, "Local State")) ? join(userDataDir, "Local State") : null
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
if (fileExists(expandedPath)) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
if (!directoryExists(expandedPath)) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`Could not find a Chromium profile at "${inputPath}".`
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
const directCookieDb = resolveCookieDbPath(expandedPath);
|
|
154
|
-
if (directCookieDb) {
|
|
155
|
-
const userDataDir = dirname(expandedPath);
|
|
156
|
-
return {
|
|
157
|
-
userDataDir,
|
|
158
|
-
profileDir: expandedPath,
|
|
159
|
-
profileDirectory: basename(expandedPath),
|
|
160
|
-
cookieDbPath: directCookieDb,
|
|
161
|
-
localStatePath: fileExists(join(userDataDir, "Local State")) ? join(userDataDir, "Local State") : null
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
const localStatePath = join(expandedPath, "Local State");
|
|
165
|
-
if (!fileExists(localStatePath)) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
const profileDirs = await selectProfileDirFromUserDataDir(expandedPath);
|
|
171
|
-
if (profileDirs.length === 0) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`No Chromium profile with a Cookies database was found under "${inputPath}".`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
if (profileDirs.length > 1) {
|
|
177
|
-
const candidates = profileDirs.map((entry) => basename(entry)).join(", ");
|
|
178
|
-
throw new Error(
|
|
179
|
-
`"${inputPath}" contains multiple Chromium profiles (${candidates}). Pass a specific profile directory such as "${profileDirs[0]}".`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
const selectedProfileDir = profileDirs[0];
|
|
183
|
-
const cookieDbPath = resolveCookieDbPath(selectedProfileDir);
|
|
184
|
-
if (!cookieDbPath) {
|
|
185
|
-
throw new Error(
|
|
186
|
-
`No Chromium Cookies database was found for "${inputPath}".`
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
userDataDir: expandedPath,
|
|
191
|
-
profileDir: selectedProfileDir,
|
|
192
|
-
profileDirectory: basename(selectedProfileDir),
|
|
193
|
-
cookieDbPath,
|
|
194
|
-
localStatePath
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function detectChromiumBrand(location) {
|
|
198
|
-
const normalizedPath = location.userDataDir.toLowerCase();
|
|
199
|
-
for (const candidate of CHROMIUM_BRANDS) {
|
|
200
|
-
if (candidate.match.every((fragment) => normalizedPath.includes(fragment))) {
|
|
201
|
-
return candidate.brand;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return DEFAULT_CHROMIUM_BRAND;
|
|
205
|
-
}
|
|
206
|
-
async function createSqliteSnapshot(dbPath) {
|
|
207
|
-
const snapshotDir = await mkdtemp(join(tmpdir(), "opensteer-cookie-db-"));
|
|
208
|
-
const snapshotPath = join(snapshotDir, "Cookies");
|
|
209
|
-
await copyFile(dbPath, snapshotPath);
|
|
210
|
-
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
211
|
-
const source = `${dbPath}${suffix}`;
|
|
212
|
-
if (!existsSync(source)) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
await copyFile(source, `${snapshotPath}${suffix}`);
|
|
216
|
-
}
|
|
217
|
-
return {
|
|
218
|
-
snapshotPath,
|
|
219
|
-
cleanup: async () => {
|
|
220
|
-
await rm(snapshotDir, { recursive: true, force: true });
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
async function querySqliteJson(dbPath, query) {
|
|
225
|
-
const result = await execFileAsync("sqlite3", ["-json", dbPath, query], {
|
|
226
|
-
encoding: "utf8",
|
|
227
|
-
maxBuffer: 64 * 1024 * 1024
|
|
228
|
-
});
|
|
229
|
-
const stdout = result.stdout;
|
|
230
|
-
const trimmed = stdout.trim();
|
|
231
|
-
if (!trimmed) {
|
|
232
|
-
return [];
|
|
233
|
-
}
|
|
234
|
-
return JSON.parse(trimmed);
|
|
235
|
-
}
|
|
236
|
-
function convertChromiumTimestampToUnixSeconds(value) {
|
|
237
|
-
if (!value || value === "0") {
|
|
238
|
-
return -1;
|
|
239
|
-
}
|
|
240
|
-
const micros = BigInt(value);
|
|
241
|
-
if (micros <= CHROMIUM_EPOCH_MICROS) {
|
|
242
|
-
return -1;
|
|
243
|
-
}
|
|
244
|
-
return Number((micros - CHROMIUM_EPOCH_MICROS) / 1000000n);
|
|
245
|
-
}
|
|
246
|
-
function mapChromiumSameSite(value) {
|
|
247
|
-
if (value === 2) {
|
|
248
|
-
return "Strict";
|
|
249
|
-
}
|
|
250
|
-
if (value === 0) {
|
|
251
|
-
return "None";
|
|
252
|
-
}
|
|
253
|
-
return "Lax";
|
|
254
|
-
}
|
|
255
|
-
function stripChromiumPadding(buffer) {
|
|
256
|
-
const paddingLength = buffer[buffer.length - 1];
|
|
257
|
-
if (paddingLength <= 0 || paddingLength > AES_BLOCK_BYTES) {
|
|
258
|
-
return buffer;
|
|
259
|
-
}
|
|
260
|
-
return buffer.subarray(0, buffer.length - paddingLength);
|
|
261
|
-
}
|
|
262
|
-
function stripDomainHashPrefix(buffer, hostKey) {
|
|
263
|
-
if (buffer.length < 32) {
|
|
264
|
-
return buffer;
|
|
265
|
-
}
|
|
266
|
-
const domainHash = createHash("sha256").update(hostKey, "utf8").digest();
|
|
267
|
-
if (buffer.subarray(0, 32).equals(domainHash)) {
|
|
268
|
-
return buffer.subarray(32);
|
|
269
|
-
}
|
|
270
|
-
return buffer;
|
|
271
|
-
}
|
|
272
|
-
function decryptChromiumAes128CbcValue(encryptedValue, key, hostKey) {
|
|
273
|
-
const ciphertext = encryptedValue.length > 3 && encryptedValue[0] === 118 && encryptedValue[1] === 49 && (encryptedValue[2] === 48 || encryptedValue[2] === 49) ? encryptedValue.subarray(3) : encryptedValue;
|
|
274
|
-
const iv = Buffer.alloc(AES_BLOCK_BYTES, " ");
|
|
275
|
-
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
276
|
-
const plaintext = Buffer.concat([
|
|
277
|
-
decipher.update(ciphertext),
|
|
278
|
-
decipher.final()
|
|
279
|
-
]);
|
|
280
|
-
return stripDomainHashPrefix(stripChromiumPadding(plaintext), hostKey).toString(
|
|
281
|
-
"utf8"
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
function decryptChromiumAes256GcmValue(encryptedValue, key) {
|
|
285
|
-
const nonce = encryptedValue.subarray(3, 15);
|
|
286
|
-
const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16);
|
|
287
|
-
const authTag = encryptedValue.subarray(encryptedValue.length - 16);
|
|
288
|
-
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
289
|
-
decipher.setAuthTag(authTag);
|
|
290
|
-
return Buffer.concat([
|
|
291
|
-
decipher.update(ciphertext),
|
|
292
|
-
decipher.final()
|
|
293
|
-
]).toString("utf8");
|
|
294
|
-
}
|
|
295
|
-
async function dpapiUnprotect(buffer) {
|
|
296
|
-
const script = [
|
|
297
|
-
`$inputBytes = [Convert]::FromBase64String('${buffer.toString("base64")}')`,
|
|
298
|
-
"$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(",
|
|
299
|
-
" $inputBytes,",
|
|
300
|
-
" $null,",
|
|
301
|
-
" [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
302
|
-
")",
|
|
303
|
-
"[Convert]::ToBase64String($plainBytes)"
|
|
304
|
-
].join("\n");
|
|
305
|
-
const { stdout } = await execFileAsync(
|
|
306
|
-
"powershell.exe",
|
|
307
|
-
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
308
|
-
{
|
|
309
|
-
encoding: "utf8",
|
|
310
|
-
maxBuffer: 8 * 1024 * 1024
|
|
311
|
-
}
|
|
312
|
-
);
|
|
313
|
-
return Buffer.from(stdout.trim(), "base64");
|
|
314
|
-
}
|
|
315
|
-
async function buildChromiumDecryptor(location) {
|
|
316
|
-
if (process.platform === "darwin") {
|
|
317
|
-
const brand = detectChromiumBrand(location);
|
|
318
|
-
const keychainStore = createKeychainStore();
|
|
319
|
-
const password = keychainStore?.get(brand.macService, brand.macAccount) ?? null;
|
|
320
|
-
if (!password) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`Unable to read ${brand.macService} from macOS Keychain.`
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const key = pbkdf2Sync(password, KEY_SALT, MAC_KEY_ITERATIONS, KEY_LENGTH, "sha1");
|
|
326
|
-
return async (row) => decryptChromiumAes128CbcValue(
|
|
327
|
-
Buffer.from(row.encrypted_value || "", "hex"),
|
|
328
|
-
key,
|
|
329
|
-
row.host_key
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
if (process.platform === "linux") {
|
|
333
|
-
const brand = detectChromiumBrand(location);
|
|
334
|
-
const keychainStore = createKeychainStore();
|
|
335
|
-
const password = keychainStore?.get(brand.macService, brand.macAccount) ?? brand.linuxApplications.map((application) => keychainStore?.get(application, application) ?? null).find(Boolean) ?? null;
|
|
336
|
-
const key = pbkdf2Sync(
|
|
337
|
-
password || "peanuts",
|
|
338
|
-
KEY_SALT,
|
|
339
|
-
LINUX_KEY_ITERATIONS,
|
|
340
|
-
KEY_LENGTH,
|
|
341
|
-
"sha1"
|
|
342
|
-
);
|
|
343
|
-
return async (row) => decryptChromiumAes128CbcValue(
|
|
344
|
-
Buffer.from(row.encrypted_value || "", "hex"),
|
|
345
|
-
key,
|
|
346
|
-
row.host_key
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
if (process.platform === "win32") {
|
|
350
|
-
if (!location.localStatePath) {
|
|
351
|
-
throw new Error(
|
|
352
|
-
`Unable to locate Chromium Local State for profile: ${location.profileDir}`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
const localState = JSON.parse(
|
|
356
|
-
await readFile(location.localStatePath, "utf8")
|
|
357
|
-
);
|
|
358
|
-
const encryptedKeyBase64 = localState.os_crypt?.encrypted_key;
|
|
359
|
-
if (!encryptedKeyBase64) {
|
|
360
|
-
throw new Error(
|
|
361
|
-
`Local State did not include os_crypt.encrypted_key for ${location.userDataDir}`
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
const encryptedKey = Buffer.from(encryptedKeyBase64, "base64");
|
|
365
|
-
const masterKey = await dpapiUnprotect(encryptedKey.subarray(5));
|
|
366
|
-
return async (row) => {
|
|
367
|
-
const encryptedValue = Buffer.from(row.encrypted_value || "", "hex");
|
|
368
|
-
if (encryptedValue.length > 4 && encryptedValue[0] === 1 && encryptedValue[1] === 0 && encryptedValue[2] === 0 && encryptedValue[3] === 0) {
|
|
369
|
-
const decrypted = await dpapiUnprotect(encryptedValue);
|
|
370
|
-
return decrypted.toString("utf8");
|
|
371
|
-
}
|
|
372
|
-
return decryptChromiumAes256GcmValue(encryptedValue, masterKey);
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
throw new Error(
|
|
376
|
-
`Local Chromium cookie sync is not supported on ${process.platform}.`
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
function buildPlaywrightCookie(row, value) {
|
|
380
|
-
if (!row.name.trim()) {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
if (!row.host_key.trim()) {
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
const expires = row.has_expires === 1 ? convertChromiumTimestampToUnixSeconds(row.expires_utc) : -1;
|
|
387
|
-
if (expires !== -1 && expires <= Math.floor(Date.now() / 1e3)) {
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
return {
|
|
391
|
-
name: row.name,
|
|
392
|
-
value,
|
|
393
|
-
domain: row.host_key,
|
|
394
|
-
path: row.path || "/",
|
|
395
|
-
expires,
|
|
396
|
-
httpOnly: row.is_httponly === 1,
|
|
397
|
-
secure: row.is_secure === 1,
|
|
398
|
-
sameSite: mapChromiumSameSite(row.samesite)
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
async function loadCookiesFromLocalProfileDir(inputPath, options = {}) {
|
|
402
|
-
const location = await resolveChromiumProfileLocation(inputPath);
|
|
403
|
-
try {
|
|
404
|
-
return await loadCookiesFromSqlite(location);
|
|
405
|
-
} catch (error) {
|
|
406
|
-
if (!isMissingSqliteBinary(error)) {
|
|
407
|
-
throw error;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return await loadCookiesFromBrowserSnapshot(location, options);
|
|
411
|
-
}
|
|
412
|
-
async function loadCookiesFromSqlite(location) {
|
|
413
|
-
const snapshot = await createSqliteSnapshot(location.cookieDbPath);
|
|
414
|
-
try {
|
|
415
|
-
const rows = await querySqliteJson(
|
|
416
|
-
snapshot.snapshotPath,
|
|
417
|
-
[
|
|
418
|
-
"SELECT",
|
|
419
|
-
" host_key,",
|
|
420
|
-
" name,",
|
|
421
|
-
" value,",
|
|
422
|
-
" hex(encrypted_value) AS encrypted_value,",
|
|
423
|
-
" path,",
|
|
424
|
-
" CAST(expires_utc AS TEXT) AS expires_utc,",
|
|
425
|
-
" is_secure,",
|
|
426
|
-
" is_httponly,",
|
|
427
|
-
" has_expires,",
|
|
428
|
-
" samesite",
|
|
429
|
-
"FROM cookies"
|
|
430
|
-
].join(" ")
|
|
431
|
-
);
|
|
432
|
-
const decryptValue = await buildChromiumDecryptor(location);
|
|
433
|
-
const cookies = [];
|
|
434
|
-
for (const row of rows) {
|
|
435
|
-
let value = row.value || "";
|
|
436
|
-
if (!value && row.encrypted_value) {
|
|
437
|
-
value = await decryptValue(row);
|
|
438
|
-
}
|
|
439
|
-
const cookie = buildPlaywrightCookie(row, value);
|
|
440
|
-
if (cookie) {
|
|
441
|
-
cookies.push(cookie);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return cookies;
|
|
445
|
-
} finally {
|
|
446
|
-
await snapshot.cleanup();
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
async function loadCookiesFromBrowserSnapshot(location, options) {
|
|
450
|
-
const snapshotRootDir = await mkdtemp(join(tmpdir(), "opensteer-profile-"));
|
|
451
|
-
const snapshotProfileDir = join(
|
|
452
|
-
snapshotRootDir,
|
|
453
|
-
basename(location.profileDir)
|
|
454
|
-
);
|
|
455
|
-
let context = null;
|
|
456
|
-
try {
|
|
457
|
-
await cp(location.profileDir, snapshotProfileDir, {
|
|
458
|
-
recursive: true
|
|
459
|
-
});
|
|
460
|
-
if (location.localStatePath) {
|
|
461
|
-
await copyFile(location.localStatePath, join(snapshotRootDir, "Local State"));
|
|
462
|
-
}
|
|
463
|
-
const brand = detectChromiumBrand(location);
|
|
464
|
-
const args = [`--profile-directory=${basename(snapshotProfileDir)}`];
|
|
465
|
-
context = await chromium.launchPersistentContext(snapshotRootDir, {
|
|
466
|
-
channel: brand.playwrightChannel,
|
|
467
|
-
headless: options.headless ?? true,
|
|
468
|
-
timeout: options.timeout ?? 12e4,
|
|
469
|
-
args
|
|
470
|
-
});
|
|
471
|
-
return await context.cookies();
|
|
472
|
-
} finally {
|
|
473
|
-
await context?.close().catch(() => void 0);
|
|
474
|
-
await rm(snapshotRootDir, { recursive: true, force: true });
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function isMissingSqliteBinary(error) {
|
|
478
|
-
return Boolean(
|
|
479
|
-
error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// src/cli/profile-sync.ts
|
|
484
|
-
function normalizeCookieDomain(value) {
|
|
485
|
-
const trimmed = value.trim().toLowerCase();
|
|
486
|
-
return trimmed.replace(/^\.+/, "");
|
|
487
|
-
}
|
|
488
|
-
function extractCookieDomain(cookie) {
|
|
489
|
-
if (typeof cookie.domain === "string" && cookie.domain.trim()) {
|
|
490
|
-
return normalizeCookieDomain(cookie.domain);
|
|
491
|
-
}
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
function cookieMatchesDomainFilters(cookie, domainFilters) {
|
|
495
|
-
if (!domainFilters.length) return true;
|
|
496
|
-
const cookieDomain = extractCookieDomain(cookie);
|
|
497
|
-
if (!cookieDomain) return false;
|
|
498
|
-
return domainFilters.some((domain) => {
|
|
499
|
-
if (cookieDomain === domain) return true;
|
|
500
|
-
return cookieDomain.endsWith(`.${domain}`);
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
function toCookieParam(cookie) {
|
|
504
|
-
const name = typeof cookie.name === "string" ? cookie.name.trim() : "";
|
|
505
|
-
if (!name) return null;
|
|
506
|
-
if (typeof cookie.value !== "string") return null;
|
|
507
|
-
const output = {
|
|
508
|
-
name,
|
|
509
|
-
value: cookie.value
|
|
510
|
-
};
|
|
511
|
-
if (typeof cookie.domain === "string" && cookie.domain.trim()) {
|
|
512
|
-
output.domain = cookie.domain.trim();
|
|
513
|
-
}
|
|
514
|
-
if (typeof cookie.path === "string" && cookie.path.trim()) {
|
|
515
|
-
output.path = cookie.path;
|
|
516
|
-
}
|
|
517
|
-
if (!output.domain) {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
if (!output.path) {
|
|
521
|
-
output.path = "/";
|
|
522
|
-
}
|
|
523
|
-
if (typeof cookie.expires === "number" && Number.isFinite(cookie.expires)) {
|
|
524
|
-
if (cookie.expires > 0) {
|
|
525
|
-
output.expires = cookie.expires;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
if (typeof cookie.httpOnly === "boolean") {
|
|
529
|
-
output.httpOnly = cookie.httpOnly;
|
|
530
|
-
}
|
|
531
|
-
if (typeof cookie.secure === "boolean") {
|
|
532
|
-
output.secure = cookie.secure;
|
|
533
|
-
}
|
|
534
|
-
if (cookie.sameSite === "Strict" || cookie.sameSite === "Lax" || cookie.sameSite === "None") {
|
|
535
|
-
output.sameSite = cookie.sameSite;
|
|
536
|
-
}
|
|
537
|
-
return output;
|
|
538
|
-
}
|
|
539
|
-
function prepareCookiesForSync(cookies, options = {}) {
|
|
540
|
-
const filteredDomains = Array.from(
|
|
541
|
-
new Set(
|
|
542
|
-
(options.domains || []).map((domain) => normalizeCookieDomain(domain)).filter(Boolean)
|
|
543
|
-
)
|
|
544
|
-
);
|
|
545
|
-
const domainCounts = {};
|
|
546
|
-
let matchedCookies = 0;
|
|
547
|
-
let droppedInvalid = 0;
|
|
548
|
-
const dedupeMap = /* @__PURE__ */ new Map();
|
|
549
|
-
for (const cookie of cookies) {
|
|
550
|
-
if (!cookieMatchesDomainFilters(cookie, filteredDomains)) {
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
matchedCookies += 1;
|
|
554
|
-
const normalizedCookie = toCookieParam(cookie);
|
|
555
|
-
if (!normalizedCookie) {
|
|
556
|
-
droppedInvalid += 1;
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
const domainKey = extractCookieDomain(cookie) || "(unknown)";
|
|
560
|
-
domainCounts[domainKey] = (domainCounts[domainKey] || 0) + 1;
|
|
561
|
-
const identityDomain = normalizedCookie.domain ? normalizeCookieDomain(normalizedCookie.domain) : "";
|
|
562
|
-
const dedupeKey = [
|
|
563
|
-
normalizedCookie.name,
|
|
564
|
-
identityDomain,
|
|
565
|
-
normalizedCookie.path || "/"
|
|
566
|
-
].join("");
|
|
567
|
-
dedupeMap.set(dedupeKey, normalizedCookie);
|
|
568
|
-
}
|
|
569
|
-
const dedupedCookies = dedupeMap.size;
|
|
570
|
-
return {
|
|
571
|
-
cookies: Array.from(dedupeMap.values()),
|
|
572
|
-
totalCookies: cookies.length,
|
|
573
|
-
matchedCookies,
|
|
574
|
-
dedupedCookies,
|
|
575
|
-
droppedInvalid,
|
|
576
|
-
filteredDomains,
|
|
577
|
-
domainCounts
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/cli/profile.ts
|
|
582
|
-
var HELP_TEXT = `Usage: opensteer profile <command> [options]
|
|
583
|
-
|
|
584
|
-
Manage cloud browser profiles and sync local cookie state into cloud profiles.
|
|
585
|
-
|
|
586
|
-
Commands:
|
|
587
|
-
list List cloud browser profiles
|
|
588
|
-
create --name <name> Create a cloud browser profile
|
|
589
|
-
sync Sync local profile cookies to cloud
|
|
590
|
-
|
|
591
|
-
Cloud auth options (all commands):
|
|
592
|
-
--api-key <key> Cloud API key (defaults to OPENSTEER_API_KEY)
|
|
593
|
-
--access-token <token> Cloud bearer access token (defaults to OPENSTEER_ACCESS_TOKEN)
|
|
594
|
-
--base-url <url> Cloud API base URL (defaults to env or the last selected host)
|
|
595
|
-
--auth-scheme <scheme> api-key (default) or bearer
|
|
596
|
-
--json JSON output (progress logs go to stderr)
|
|
597
|
-
|
|
598
|
-
Sync options:
|
|
599
|
-
--from-profile-dir <dir> Local browser profile directory or Chromium user-data dir to read cookies from (required)
|
|
600
|
-
--to-profile-id <id> Destination cloud profile id
|
|
601
|
-
--name <name> Create destination cloud profile with this name if --to-profile-id is omitted
|
|
602
|
-
--domain <domain> Restrict sync to one domain (repeatable)
|
|
603
|
-
--all-domains Explicitly sync all domains
|
|
604
|
-
--dry-run Analyze cookies and scope without uploading to cloud
|
|
605
|
-
--yes Required for non-interactive execution
|
|
606
|
-
--headless <true|false> Browser headless mode for local/cloud sync sessions (default: true)
|
|
607
|
-
-h, --help Show this help
|
|
608
|
-
|
|
609
|
-
Examples:
|
|
610
|
-
opensteer profile list
|
|
611
|
-
opensteer profile create --name "My Session Profile"
|
|
612
|
-
opensteer profile sync --from-profile-dir ~/Library/Application\\ Support/Google/Chrome/Default --to-profile-id bp_123 --domain github.com
|
|
613
|
-
opensteer profile sync --from-profile-dir ./my-profile --name "Imported Profile" --all-domains --yes
|
|
614
|
-
`;
|
|
615
|
-
function parseBooleanValue(raw, flag) {
|
|
616
|
-
const normalized = raw.trim().toLowerCase();
|
|
617
|
-
if (normalized === "true" || normalized === "1") {
|
|
618
|
-
return { ok: true, value: true };
|
|
619
|
-
}
|
|
620
|
-
if (normalized === "false" || normalized === "0") {
|
|
621
|
-
return { ok: true, value: false };
|
|
622
|
-
}
|
|
623
|
-
return {
|
|
624
|
-
ok: false,
|
|
625
|
-
error: `${flag} must be "true" or "false".`
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
function parseAuthScheme(raw) {
|
|
629
|
-
const normalized = raw.trim().toLowerCase();
|
|
630
|
-
if (normalized === "api-key" || normalized === "bearer") {
|
|
631
|
-
return normalized;
|
|
632
|
-
}
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
function isBrowserProfileStatus(value) {
|
|
636
|
-
return value === "active" || value === "archived" || value === "error";
|
|
637
|
-
}
|
|
638
|
-
function readFlagValue(args, index, flag) {
|
|
639
|
-
const value = args[index + 1];
|
|
640
|
-
if (value === void 0 || value.startsWith("-")) {
|
|
641
|
-
return {
|
|
642
|
-
ok: false,
|
|
643
|
-
error: `${flag} requires a value.`
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
ok: true,
|
|
648
|
-
value,
|
|
649
|
-
nextIndex: index + 1
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
function parseListArgs(rawArgs) {
|
|
653
|
-
const args = {};
|
|
654
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
655
|
-
const arg = rawArgs[i];
|
|
656
|
-
if (arg === "--json") {
|
|
657
|
-
args.json = true;
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
if (arg === "--api-key") {
|
|
661
|
-
const value = readFlagValue(rawArgs, i, "--api-key");
|
|
662
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
663
|
-
args.apiKey = value.value;
|
|
664
|
-
i = value.nextIndex;
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
if (arg === "--access-token") {
|
|
668
|
-
const value = readFlagValue(rawArgs, i, "--access-token");
|
|
669
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
670
|
-
args.accessToken = value.value;
|
|
671
|
-
i = value.nextIndex;
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
if (arg === "--base-url") {
|
|
675
|
-
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
676
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
677
|
-
args.baseUrl = value.value;
|
|
678
|
-
i = value.nextIndex;
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
if (arg === "--auth-scheme") {
|
|
682
|
-
const value = readFlagValue(rawArgs, i, "--auth-scheme");
|
|
683
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
684
|
-
const parsed = parseAuthScheme(value.value);
|
|
685
|
-
if (!parsed) {
|
|
686
|
-
return {
|
|
687
|
-
mode: "error",
|
|
688
|
-
error: '--auth-scheme must be "api-key" or "bearer".'
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
args.authScheme = parsed;
|
|
692
|
-
i = value.nextIndex;
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
if (arg === "--cursor") {
|
|
696
|
-
const value = readFlagValue(rawArgs, i, "--cursor");
|
|
697
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
698
|
-
args.cursor = value.value;
|
|
699
|
-
i = value.nextIndex;
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (arg === "--limit") {
|
|
703
|
-
const value = readFlagValue(rawArgs, i, "--limit");
|
|
704
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
705
|
-
const parsed = Number.parseInt(value.value, 10);
|
|
706
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
707
|
-
return {
|
|
708
|
-
mode: "error",
|
|
709
|
-
error: "--limit must be a positive integer."
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
args.limit = parsed;
|
|
713
|
-
i = value.nextIndex;
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
if (arg === "--status") {
|
|
717
|
-
const value = readFlagValue(rawArgs, i, "--status");
|
|
718
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
719
|
-
const status = value.value.trim();
|
|
720
|
-
if (!isBrowserProfileStatus(status)) {
|
|
721
|
-
return {
|
|
722
|
-
mode: "error",
|
|
723
|
-
error: "--status must be one of: active, archived, error."
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
args.status = status;
|
|
727
|
-
i = value.nextIndex;
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
return {
|
|
731
|
-
mode: "error",
|
|
732
|
-
error: `Unsupported option "${arg}" for "opensteer profile list".`
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
return { mode: "list", args };
|
|
736
|
-
}
|
|
737
|
-
function parseCreateArgs(rawArgs) {
|
|
738
|
-
const args = {};
|
|
739
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
740
|
-
const arg = rawArgs[i];
|
|
741
|
-
if (arg === "--json") {
|
|
742
|
-
args.json = true;
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
if (arg === "--name") {
|
|
746
|
-
const value = readFlagValue(rawArgs, i, "--name");
|
|
747
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
748
|
-
args.name = value.value.trim();
|
|
749
|
-
i = value.nextIndex;
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
if (arg === "--api-key") {
|
|
753
|
-
const value = readFlagValue(rawArgs, i, "--api-key");
|
|
754
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
755
|
-
args.apiKey = value.value;
|
|
756
|
-
i = value.nextIndex;
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
if (arg === "--access-token") {
|
|
760
|
-
const value = readFlagValue(rawArgs, i, "--access-token");
|
|
761
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
762
|
-
args.accessToken = value.value;
|
|
763
|
-
i = value.nextIndex;
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
if (arg === "--base-url") {
|
|
767
|
-
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
768
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
769
|
-
args.baseUrl = value.value;
|
|
770
|
-
i = value.nextIndex;
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
if (arg === "--auth-scheme") {
|
|
774
|
-
const value = readFlagValue(rawArgs, i, "--auth-scheme");
|
|
775
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
776
|
-
const parsed = parseAuthScheme(value.value);
|
|
777
|
-
if (!parsed) {
|
|
778
|
-
return {
|
|
779
|
-
mode: "error",
|
|
780
|
-
error: '--auth-scheme must be "api-key" or "bearer".'
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
args.authScheme = parsed;
|
|
784
|
-
i = value.nextIndex;
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
return {
|
|
788
|
-
mode: "error",
|
|
789
|
-
error: `Unsupported option "${arg}" for "opensteer profile create".`
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
if (!args.name) {
|
|
793
|
-
return {
|
|
794
|
-
mode: "error",
|
|
795
|
-
error: '--name is required for "opensteer profile create".'
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
return {
|
|
799
|
-
mode: "create",
|
|
800
|
-
args: {
|
|
801
|
-
...args,
|
|
802
|
-
name: args.name
|
|
803
|
-
}
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
function parseSyncArgs(rawArgs) {
|
|
807
|
-
const args = {
|
|
808
|
-
fromProfileDir: "",
|
|
809
|
-
domains: [],
|
|
810
|
-
allDomains: false,
|
|
811
|
-
dryRun: false,
|
|
812
|
-
yes: false,
|
|
813
|
-
headless: true
|
|
814
|
-
};
|
|
815
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
816
|
-
const arg = rawArgs[i];
|
|
817
|
-
if (arg === "--json") {
|
|
818
|
-
args.json = true;
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
if (arg === "--all-domains") {
|
|
822
|
-
args.allDomains = true;
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
if (arg === "--dry-run") {
|
|
826
|
-
args.dryRun = true;
|
|
827
|
-
continue;
|
|
828
|
-
}
|
|
829
|
-
if (arg === "--yes") {
|
|
830
|
-
args.yes = true;
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
if (arg === "--from-profile-dir") {
|
|
834
|
-
const value = readFlagValue(rawArgs, i, "--from-profile-dir");
|
|
835
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
836
|
-
args.fromProfileDir = value.value;
|
|
837
|
-
i = value.nextIndex;
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
if (arg === "--to-profile-id") {
|
|
841
|
-
const value = readFlagValue(rawArgs, i, "--to-profile-id");
|
|
842
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
843
|
-
args.toProfileId = value.value;
|
|
844
|
-
i = value.nextIndex;
|
|
845
|
-
continue;
|
|
846
|
-
}
|
|
847
|
-
if (arg === "--name") {
|
|
848
|
-
const value = readFlagValue(rawArgs, i, "--name");
|
|
849
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
850
|
-
args.name = value.value;
|
|
851
|
-
i = value.nextIndex;
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
if (arg === "--domain") {
|
|
855
|
-
const value = readFlagValue(rawArgs, i, "--domain");
|
|
856
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
857
|
-
args.domains.push(value.value);
|
|
858
|
-
i = value.nextIndex;
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
if (arg === "--headless") {
|
|
862
|
-
const value = readFlagValue(rawArgs, i, "--headless");
|
|
863
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
864
|
-
const parsed = parseBooleanValue(value.value, "--headless");
|
|
865
|
-
if (!parsed.ok) return { mode: "error", error: parsed.error };
|
|
866
|
-
args.headless = parsed.value;
|
|
867
|
-
i = value.nextIndex;
|
|
868
|
-
continue;
|
|
869
|
-
}
|
|
870
|
-
if (arg === "--api-key") {
|
|
871
|
-
const value = readFlagValue(rawArgs, i, "--api-key");
|
|
872
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
873
|
-
args.apiKey = value.value;
|
|
874
|
-
i = value.nextIndex;
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
if (arg === "--access-token") {
|
|
878
|
-
const value = readFlagValue(rawArgs, i, "--access-token");
|
|
879
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
880
|
-
args.accessToken = value.value;
|
|
881
|
-
i = value.nextIndex;
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
if (arg === "--base-url") {
|
|
885
|
-
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
886
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
887
|
-
args.baseUrl = value.value;
|
|
888
|
-
i = value.nextIndex;
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
if (arg === "--auth-scheme") {
|
|
892
|
-
const value = readFlagValue(rawArgs, i, "--auth-scheme");
|
|
893
|
-
if (!value.ok) return { mode: "error", error: value.error };
|
|
894
|
-
const parsed = parseAuthScheme(value.value);
|
|
895
|
-
if (!parsed) {
|
|
896
|
-
return {
|
|
897
|
-
mode: "error",
|
|
898
|
-
error: '--auth-scheme must be "api-key" or "bearer".'
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
args.authScheme = parsed;
|
|
902
|
-
i = value.nextIndex;
|
|
903
|
-
continue;
|
|
904
|
-
}
|
|
905
|
-
return {
|
|
906
|
-
mode: "error",
|
|
907
|
-
error: `Unsupported option "${arg}" for "opensteer profile sync".`
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
if (!args.fromProfileDir.trim()) {
|
|
911
|
-
return {
|
|
912
|
-
mode: "error",
|
|
913
|
-
error: '--from-profile-dir is required for "opensteer profile sync".'
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
if (args.allDomains && args.domains.length > 0) {
|
|
917
|
-
return {
|
|
918
|
-
mode: "error",
|
|
919
|
-
error: "Use either --all-domains or --domain, not both."
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
return { mode: "sync", args };
|
|
923
|
-
}
|
|
924
|
-
function parseOpensteerProfileArgs(rawArgs) {
|
|
925
|
-
if (!rawArgs.length) {
|
|
926
|
-
return { mode: "help" };
|
|
927
|
-
}
|
|
928
|
-
const [subcommand, ...rest] = rawArgs;
|
|
929
|
-
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
930
|
-
return { mode: "help" };
|
|
931
|
-
}
|
|
932
|
-
if (subcommand === "list") {
|
|
933
|
-
return parseListArgs(rest);
|
|
934
|
-
}
|
|
935
|
-
if (subcommand === "create") {
|
|
936
|
-
return parseCreateArgs(rest);
|
|
937
|
-
}
|
|
938
|
-
if (subcommand === "sync") {
|
|
939
|
-
return parseSyncArgs(rest);
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
mode: "error",
|
|
943
|
-
error: `Unsupported profile subcommand "${subcommand}".`
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
function createDefaultDeps() {
|
|
947
|
-
return {
|
|
948
|
-
env: process.env,
|
|
949
|
-
createBrowserProfileClient: (context) => new BrowserProfileClient(
|
|
950
|
-
context.baseUrl,
|
|
951
|
-
context.token,
|
|
952
|
-
context.authScheme
|
|
953
|
-
),
|
|
954
|
-
createOpensteer: (config) => new Opensteer(config),
|
|
955
|
-
loadLocalProfileCookies: (profileDir, options) => loadCookiesFromLocalProfileDir(profileDir, options),
|
|
956
|
-
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
957
|
-
confirm: async (message) => {
|
|
958
|
-
const rl = createInterface({
|
|
959
|
-
input: process.stdin,
|
|
960
|
-
output: process.stderr
|
|
961
|
-
});
|
|
962
|
-
try {
|
|
963
|
-
const answer = await rl.question(`${message} [y/N] `);
|
|
964
|
-
const normalized = answer.trim().toLowerCase();
|
|
965
|
-
return normalized === "y" || normalized === "yes";
|
|
966
|
-
} finally {
|
|
967
|
-
rl.close();
|
|
968
|
-
}
|
|
969
|
-
},
|
|
970
|
-
writeStdout: (message) => process.stdout.write(message),
|
|
971
|
-
writeStderr: (message) => process.stderr.write(message)
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
async function buildCloudAuthContext(args, deps) {
|
|
975
|
-
const ensured = await ensureCloudCredentialsForCommand({
|
|
976
|
-
commandName: "opensteer profile",
|
|
977
|
-
env: deps.env,
|
|
978
|
-
apiKeyFlag: args.apiKey,
|
|
979
|
-
accessTokenFlag: args.accessToken,
|
|
980
|
-
baseUrl: args.baseUrl,
|
|
981
|
-
interactive: deps.isInteractive(),
|
|
982
|
-
autoLoginIfNeeded: true,
|
|
983
|
-
writeProgress: args.json ? deps.writeStderr : deps.writeStdout,
|
|
984
|
-
writeStderr: deps.writeStderr
|
|
985
|
-
});
|
|
986
|
-
if (args.authScheme) {
|
|
987
|
-
if (ensured.kind === "access-token" && args.authScheme !== "bearer") {
|
|
988
|
-
throw new Error(
|
|
989
|
-
"--auth-scheme=api-key is incompatible with --access-token or saved login credentials."
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
if (ensured.kind === "api-key" && args.authScheme === "bearer") {
|
|
993
|
-
return {
|
|
994
|
-
...ensured,
|
|
995
|
-
authScheme: "bearer"
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return {
|
|
1000
|
-
token: ensured.token,
|
|
1001
|
-
baseUrl: ensured.baseUrl,
|
|
1002
|
-
authScheme: ensured.authScheme,
|
|
1003
|
-
kind: ensured.kind,
|
|
1004
|
-
source: ensured.source
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
function printProfileHelp(deps) {
|
|
1008
|
-
deps.writeStdout(`${HELP_TEXT}
|
|
1009
|
-
`);
|
|
1010
|
-
}
|
|
1011
|
-
function writeJson(deps, payload) {
|
|
1012
|
-
deps.writeStdout(`${JSON.stringify(payload)}
|
|
1013
|
-
`);
|
|
1014
|
-
}
|
|
1015
|
-
function writeHumanLine(deps, message) {
|
|
1016
|
-
deps.writeStdout(`${message}
|
|
1017
|
-
`);
|
|
1018
|
-
}
|
|
1019
|
-
function writeProgressLine(deps, jsonOutput, message) {
|
|
1020
|
-
if (jsonOutput) {
|
|
1021
|
-
deps.writeStderr(`${message}
|
|
1022
|
-
`);
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
deps.writeStdout(`${message}
|
|
1026
|
-
`);
|
|
1027
|
-
}
|
|
1028
|
-
async function runList(args, deps) {
|
|
1029
|
-
const auth = await buildCloudAuthContext(args, deps);
|
|
1030
|
-
const client = deps.createBrowserProfileClient(auth);
|
|
1031
|
-
const response = await client.list({
|
|
1032
|
-
cursor: args.cursor,
|
|
1033
|
-
limit: args.limit,
|
|
1034
|
-
status: args.status
|
|
1035
|
-
});
|
|
1036
|
-
if (args.json) {
|
|
1037
|
-
writeJson(deps, response);
|
|
1038
|
-
return 0;
|
|
1039
|
-
}
|
|
1040
|
-
if (!response.profiles.length) {
|
|
1041
|
-
writeHumanLine(deps, "No cloud browser profiles found.");
|
|
1042
|
-
return 0;
|
|
1043
|
-
}
|
|
1044
|
-
writeHumanLine(deps, `Cloud browser profiles (${response.profiles.length}):`);
|
|
1045
|
-
for (const profile of response.profiles) {
|
|
1046
|
-
writeHumanLine(
|
|
1047
|
-
deps,
|
|
1048
|
-
` ${profile.profileId} ${profile.name} [${profile.status}]`
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
if (response.nextCursor) {
|
|
1052
|
-
writeHumanLine(
|
|
1053
|
-
deps,
|
|
1054
|
-
`Next cursor: ${response.nextCursor}`
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
return 0;
|
|
1058
|
-
}
|
|
1059
|
-
async function runCreate(args, deps) {
|
|
1060
|
-
const auth = await buildCloudAuthContext(args, deps);
|
|
1061
|
-
const client = deps.createBrowserProfileClient(auth);
|
|
1062
|
-
const profile = await client.create({
|
|
1063
|
-
name: args.name
|
|
1064
|
-
});
|
|
1065
|
-
if (args.json) {
|
|
1066
|
-
writeJson(deps, profile);
|
|
1067
|
-
return 0;
|
|
1068
|
-
}
|
|
1069
|
-
writeHumanLine(
|
|
1070
|
-
deps,
|
|
1071
|
-
`Created cloud browser profile "${profile.name}" (${profile.profileId}).`
|
|
1072
|
-
);
|
|
1073
|
-
return 0;
|
|
1074
|
-
}
|
|
1075
|
-
async function importCookiesInBatches(context, cookies, batchSize = 100) {
|
|
1076
|
-
let imported = 0;
|
|
1077
|
-
let skipped = 0;
|
|
1078
|
-
for (let offset = 0; offset < cookies.length; offset += batchSize) {
|
|
1079
|
-
const batch = cookies.slice(offset, offset + batchSize);
|
|
1080
|
-
try {
|
|
1081
|
-
await context.addCookies(batch);
|
|
1082
|
-
imported += batch.length;
|
|
1083
|
-
} catch {
|
|
1084
|
-
for (const cookie of batch) {
|
|
1085
|
-
try {
|
|
1086
|
-
await context.addCookies([cookie]);
|
|
1087
|
-
imported += 1;
|
|
1088
|
-
} catch {
|
|
1089
|
-
skipped += 1;
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return {
|
|
1095
|
-
imported,
|
|
1096
|
-
skipped
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
async function resolveTargetProfileId(args, deps, client) {
|
|
1100
|
-
const explicitProfileId = args.toProfileId?.trim();
|
|
1101
|
-
if (explicitProfileId) {
|
|
1102
|
-
return {
|
|
1103
|
-
profileId: explicitProfileId,
|
|
1104
|
-
created: false
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
const requestedName = args.name?.trim();
|
|
1108
|
-
if (requestedName) {
|
|
1109
|
-
const profile = await client.create({
|
|
1110
|
-
name: requestedName
|
|
1111
|
-
});
|
|
1112
|
-
return {
|
|
1113
|
-
profileId: profile.profileId,
|
|
1114
|
-
created: true
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
if (!deps.isInteractive()) {
|
|
1118
|
-
throw new Error(
|
|
1119
|
-
"Sync target is required in non-interactive mode. Use --to-profile-id <id> or --name <name>."
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
const defaultName = `Synced ${path.basename(args.fromProfileDir)}`;
|
|
1123
|
-
const shouldCreate = await deps.confirm(
|
|
1124
|
-
`No destination profile provided. Create a new cloud profile named "${defaultName}"?`
|
|
1125
|
-
);
|
|
1126
|
-
if (!shouldCreate) {
|
|
1127
|
-
throw new Error(
|
|
1128
|
-
"Profile sync cancelled. Provide --to-profile-id or --name to choose a destination profile."
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
const created = await client.create({
|
|
1132
|
-
name: defaultName
|
|
1133
|
-
});
|
|
1134
|
-
return {
|
|
1135
|
-
profileId: created.profileId,
|
|
1136
|
-
created: true
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
function resolveSyncBrowserProfilePreference(profileId, env) {
|
|
1140
|
-
const resolved = resolveConfigWithEnv({
|
|
1141
|
-
cloud: true
|
|
1142
|
-
}, {
|
|
1143
|
-
env
|
|
1144
|
-
}).config;
|
|
1145
|
-
const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
|
|
1146
|
-
const configured = cloudConfig?.browserProfile;
|
|
1147
|
-
if (configured && configured.profileId.trim() === profileId && configured.reuseIfActive !== void 0) {
|
|
1148
|
-
return {
|
|
1149
|
-
profileId,
|
|
1150
|
-
reuseIfActive: configured.reuseIfActive
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
return { profileId };
|
|
1154
|
-
}
|
|
1155
|
-
async function runSync(args, deps) {
|
|
1156
|
-
const sourceProfileDir = expandHome(args.fromProfileDir.trim());
|
|
1157
|
-
const nonInteractive = !deps.isInteractive();
|
|
1158
|
-
const hasExplicitScope = args.allDomains || args.domains.length > 0;
|
|
1159
|
-
if (nonInteractive && !args.yes) {
|
|
1160
|
-
throw new Error(
|
|
1161
|
-
"Non-interactive profile sync requires --yes."
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
if (nonInteractive && !hasExplicitScope) {
|
|
1165
|
-
throw new Error(
|
|
1166
|
-
"Non-interactive profile sync requires explicit scope: --domain <domain> (repeatable) or --all-domains."
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
if (!hasExplicitScope && !nonInteractive) {
|
|
1170
|
-
const confirmed = await deps.confirm(
|
|
1171
|
-
"No domain filter provided. Sync cookies for all domains?"
|
|
1172
|
-
);
|
|
1173
|
-
if (!confirmed) {
|
|
1174
|
-
throw new Error(
|
|
1175
|
-
"Profile sync cancelled. Use --domain <domain> or --all-domains."
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
writeProgressLine(
|
|
1180
|
-
deps,
|
|
1181
|
-
Boolean(args.json),
|
|
1182
|
-
`Reading cookies from local profile: ${sourceProfileDir}`
|
|
1183
|
-
);
|
|
1184
|
-
let sourceCookies = [];
|
|
1185
|
-
sourceCookies = await deps.loadLocalProfileCookies(sourceProfileDir, {
|
|
1186
|
-
headless: args.headless,
|
|
1187
|
-
timeout: 12e4
|
|
1188
|
-
});
|
|
1189
|
-
const prepared = prepareCookiesForSync(sourceCookies, {
|
|
1190
|
-
domains: args.allDomains ? [] : args.domains
|
|
1191
|
-
});
|
|
1192
|
-
if (!prepared.cookies.length) {
|
|
1193
|
-
throw new Error(
|
|
1194
|
-
"No syncable cookies found for the selected profile and scope."
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
if (args.dryRun) {
|
|
1198
|
-
const payload2 = {
|
|
1199
|
-
success: true,
|
|
1200
|
-
dryRun: true,
|
|
1201
|
-
profileId: args.toProfileId?.trim() || null,
|
|
1202
|
-
createdProfile: false,
|
|
1203
|
-
totalCookies: prepared.totalCookies,
|
|
1204
|
-
matchedCookies: prepared.matchedCookies,
|
|
1205
|
-
dedupedCookies: prepared.dedupedCookies,
|
|
1206
|
-
droppedInvalid: prepared.droppedInvalid,
|
|
1207
|
-
filteredDomains: prepared.filteredDomains,
|
|
1208
|
-
domainCounts: prepared.domainCounts
|
|
1209
|
-
};
|
|
1210
|
-
if (args.json) {
|
|
1211
|
-
writeJson(deps, payload2);
|
|
1212
|
-
} else {
|
|
1213
|
-
writeHumanLine(deps, "Dry run complete.");
|
|
1214
|
-
writeHumanLine(deps, ` Total cookies: ${prepared.totalCookies}`);
|
|
1215
|
-
writeHumanLine(deps, ` Scope-matched cookies: ${prepared.matchedCookies}`);
|
|
1216
|
-
writeHumanLine(deps, ` Deduped cookies: ${prepared.dedupedCookies}`);
|
|
1217
|
-
writeHumanLine(deps, ` Dropped invalid: ${prepared.droppedInvalid}`);
|
|
1218
|
-
if (prepared.filteredDomains.length) {
|
|
1219
|
-
writeHumanLine(
|
|
1220
|
-
deps,
|
|
1221
|
-
` Domain filters: ${prepared.filteredDomains.join(", ")}`
|
|
1222
|
-
);
|
|
1223
|
-
} else {
|
|
1224
|
-
writeHumanLine(deps, " Domain scope: all domains");
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
return 0;
|
|
1228
|
-
}
|
|
1229
|
-
const auth = await buildCloudAuthContext(args, deps);
|
|
1230
|
-
const client = deps.createBrowserProfileClient(auth);
|
|
1231
|
-
const target = await resolveTargetProfileId(args, deps, client);
|
|
1232
|
-
const targetBrowserProfile = resolveSyncBrowserProfilePreference(
|
|
1233
|
-
target.profileId,
|
|
1234
|
-
deps.env
|
|
1235
|
-
);
|
|
1236
|
-
writeProgressLine(
|
|
1237
|
-
deps,
|
|
1238
|
-
Boolean(args.json),
|
|
1239
|
-
`Importing ${prepared.cookies.length} cookies into cloud profile ${target.profileId}`
|
|
1240
|
-
);
|
|
1241
|
-
const cloud = deps.createOpensteer({
|
|
1242
|
-
cloud: {
|
|
1243
|
-
...auth.kind === "api-key" ? { apiKey: auth.token } : { accessToken: auth.token },
|
|
1244
|
-
baseUrl: auth.baseUrl,
|
|
1245
|
-
authScheme: auth.authScheme,
|
|
1246
|
-
browserProfile: targetBrowserProfile
|
|
1247
|
-
},
|
|
1248
|
-
cursor: { enabled: false }
|
|
1249
|
-
});
|
|
1250
|
-
let imported = 0;
|
|
1251
|
-
let skipped = 0;
|
|
1252
|
-
try {
|
|
1253
|
-
await cloud.launch({
|
|
1254
|
-
headless: args.headless,
|
|
1255
|
-
timeout: 12e4
|
|
1256
|
-
});
|
|
1257
|
-
const result = await importCookiesInBatches(cloud.context, prepared.cookies);
|
|
1258
|
-
imported = result.imported;
|
|
1259
|
-
skipped = result.skipped;
|
|
1260
|
-
} finally {
|
|
1261
|
-
await cloud.close().catch(() => void 0);
|
|
1262
|
-
}
|
|
1263
|
-
const payload = {
|
|
1264
|
-
success: true,
|
|
1265
|
-
profileId: target.profileId,
|
|
1266
|
-
createdProfile: target.created,
|
|
1267
|
-
totalCookies: prepared.totalCookies,
|
|
1268
|
-
matchedCookies: prepared.matchedCookies,
|
|
1269
|
-
dedupedCookies: prepared.dedupedCookies,
|
|
1270
|
-
droppedInvalid: prepared.droppedInvalid,
|
|
1271
|
-
importedCookies: imported,
|
|
1272
|
-
skippedCookies: skipped,
|
|
1273
|
-
filteredDomains: prepared.filteredDomains,
|
|
1274
|
-
domainCounts: prepared.domainCounts
|
|
1275
|
-
};
|
|
1276
|
-
if (args.json) {
|
|
1277
|
-
writeJson(deps, payload);
|
|
1278
|
-
return 0;
|
|
1279
|
-
}
|
|
1280
|
-
writeHumanLine(deps, "Profile cookie sync complete.");
|
|
1281
|
-
writeHumanLine(deps, ` Cloud profile: ${target.profileId}`);
|
|
1282
|
-
writeHumanLine(deps, ` Imported cookies: ${imported}`);
|
|
1283
|
-
writeHumanLine(deps, ` Skipped cookies: ${skipped}`);
|
|
1284
|
-
if (prepared.filteredDomains.length) {
|
|
1285
|
-
writeHumanLine(
|
|
1286
|
-
deps,
|
|
1287
|
-
` Domain filters: ${prepared.filteredDomains.join(", ")}`
|
|
1288
|
-
);
|
|
1289
|
-
} else {
|
|
1290
|
-
writeHumanLine(deps, " Domain scope: all domains");
|
|
1291
|
-
}
|
|
1292
|
-
return 0;
|
|
1293
|
-
}
|
|
1294
|
-
async function runOpensteerProfileCli(rawArgs, overrideDeps = {}) {
|
|
1295
|
-
const deps = {
|
|
1296
|
-
...createDefaultDeps(),
|
|
1297
|
-
...overrideDeps
|
|
1298
|
-
};
|
|
1299
|
-
const parsed = parseOpensteerProfileArgs(rawArgs);
|
|
1300
|
-
if (parsed.mode === "help") {
|
|
1301
|
-
printProfileHelp(deps);
|
|
1302
|
-
return 0;
|
|
1303
|
-
}
|
|
1304
|
-
if (parsed.mode === "error") {
|
|
1305
|
-
deps.writeStderr(`${parsed.error}
|
|
1306
|
-
`);
|
|
1307
|
-
deps.writeStderr('Run "opensteer profile --help" for usage.\n');
|
|
1308
|
-
return 1;
|
|
1309
|
-
}
|
|
1310
|
-
try {
|
|
1311
|
-
if (parsed.mode === "list") {
|
|
1312
|
-
return await runList(parsed.args, deps);
|
|
1313
|
-
}
|
|
1314
|
-
if (parsed.mode === "create") {
|
|
1315
|
-
return await runCreate(parsed.args, deps);
|
|
1316
|
-
}
|
|
1317
|
-
return await runSync(parsed.args, deps);
|
|
1318
|
-
} catch (error) {
|
|
1319
|
-
const message = error instanceof Error ? error.message : "Profile command failed.";
|
|
1320
|
-
deps.writeStderr(`${message}
|
|
1321
|
-
`);
|
|
1322
|
-
return 1;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
export {
|
|
1326
|
-
parseOpensteerProfileArgs,
|
|
1327
|
-
runOpensteerProfileCli
|
|
1328
|
-
};
|