opensteer 0.6.0 → 0.6.2
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/CHANGELOG.md +16 -1
- package/README.md +63 -0
- package/bin/opensteer.mjs +214 -3
- package/dist/browser-profile-client-CaL-mwqs.d.ts +168 -0
- package/dist/browser-profile-client-DK9qa_Dj.d.cts +168 -0
- package/dist/chunk-7RMY26CM.js +1130 -0
- package/dist/{chunk-SGZYTGY3.js → chunk-F2VDVOJO.js} +1890 -1520
- package/dist/chunk-WDRMHPWL.js +1320 -0
- package/dist/chunk-WJI7TGBQ.js +77 -0
- package/dist/cli/auth.cjs +1834 -0
- package/dist/cli/auth.d.cts +114 -0
- package/dist/cli/auth.d.ts +114 -0
- package/dist/cli/auth.js +15 -0
- package/dist/cli/profile.cjs +16222 -0
- package/dist/cli/profile.d.cts +79 -0
- package/dist/cli/profile.d.ts +79 -0
- package/dist/cli/profile.js +866 -0
- package/dist/cli/server.cjs +1816 -422
- package/dist/cli/server.js +310 -10
- package/dist/index.cjs +1772 -414
- package/dist/index.d.cts +141 -395
- package/dist/index.d.ts +141 -395
- package/dist/index.js +203 -8
- package/dist/types-BxiRblC7.d.cts +327 -0
- package/dist/types-BxiRblC7.d.ts +327 -0
- package/package.json +3 -2
- package/skills/opensteer/SKILL.md +34 -5
- package/skills/opensteer/references/cli-reference.md +10 -8
- package/skills/opensteer/references/sdk-reference.md +14 -3
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserProfileClient
|
|
3
|
+
} from "../chunk-WJI7TGBQ.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureCloudCredentialsForCommand
|
|
6
|
+
} from "../chunk-7RMY26CM.js";
|
|
7
|
+
import {
|
|
8
|
+
Opensteer,
|
|
9
|
+
expandHome,
|
|
10
|
+
loadCookiesFromLocalProfileDir
|
|
11
|
+
} from "../chunk-F2VDVOJO.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveConfigWithEnv
|
|
14
|
+
} from "../chunk-WDRMHPWL.js";
|
|
15
|
+
import "../chunk-3H5RRIMZ.js";
|
|
16
|
+
|
|
17
|
+
// src/cli/profile.ts
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { createInterface } from "readline/promises";
|
|
20
|
+
|
|
21
|
+
// src/cli/profile-sync.ts
|
|
22
|
+
function normalizeCookieDomain(value) {
|
|
23
|
+
const trimmed = value.trim().toLowerCase();
|
|
24
|
+
return trimmed.replace(/^\.+/, "");
|
|
25
|
+
}
|
|
26
|
+
function extractCookieDomain(cookie) {
|
|
27
|
+
if (typeof cookie.domain === "string" && cookie.domain.trim()) {
|
|
28
|
+
return normalizeCookieDomain(cookie.domain);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function cookieMatchesDomainFilters(cookie, domainFilters) {
|
|
33
|
+
if (!domainFilters.length) return true;
|
|
34
|
+
const cookieDomain = extractCookieDomain(cookie);
|
|
35
|
+
if (!cookieDomain) return false;
|
|
36
|
+
return domainFilters.some((domain) => {
|
|
37
|
+
if (cookieDomain === domain) return true;
|
|
38
|
+
return cookieDomain.endsWith(`.${domain}`);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function toCookieParam(cookie) {
|
|
42
|
+
const name = typeof cookie.name === "string" ? cookie.name.trim() : "";
|
|
43
|
+
if (!name) return null;
|
|
44
|
+
if (typeof cookie.value !== "string") return null;
|
|
45
|
+
const output = {
|
|
46
|
+
name,
|
|
47
|
+
value: cookie.value
|
|
48
|
+
};
|
|
49
|
+
if (typeof cookie.domain === "string" && cookie.domain.trim()) {
|
|
50
|
+
output.domain = cookie.domain.trim();
|
|
51
|
+
}
|
|
52
|
+
if (typeof cookie.path === "string" && cookie.path.trim()) {
|
|
53
|
+
output.path = cookie.path;
|
|
54
|
+
}
|
|
55
|
+
if (!output.domain) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!output.path) {
|
|
59
|
+
output.path = "/";
|
|
60
|
+
}
|
|
61
|
+
if (typeof cookie.expires === "number" && Number.isFinite(cookie.expires)) {
|
|
62
|
+
if (cookie.expires > 0) {
|
|
63
|
+
output.expires = cookie.expires;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (typeof cookie.httpOnly === "boolean") {
|
|
67
|
+
output.httpOnly = cookie.httpOnly;
|
|
68
|
+
}
|
|
69
|
+
if (typeof cookie.secure === "boolean") {
|
|
70
|
+
output.secure = cookie.secure;
|
|
71
|
+
}
|
|
72
|
+
if (cookie.sameSite === "Strict" || cookie.sameSite === "Lax" || cookie.sameSite === "None") {
|
|
73
|
+
output.sameSite = cookie.sameSite;
|
|
74
|
+
}
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
function prepareCookiesForSync(cookies, options = {}) {
|
|
78
|
+
const filteredDomains = Array.from(
|
|
79
|
+
new Set(
|
|
80
|
+
(options.domains || []).map((domain) => normalizeCookieDomain(domain)).filter(Boolean)
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
const domainCounts = {};
|
|
84
|
+
let matchedCookies = 0;
|
|
85
|
+
let droppedInvalid = 0;
|
|
86
|
+
const dedupeMap = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const cookie of cookies) {
|
|
88
|
+
if (!cookieMatchesDomainFilters(cookie, filteredDomains)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
matchedCookies += 1;
|
|
92
|
+
const normalizedCookie = toCookieParam(cookie);
|
|
93
|
+
if (!normalizedCookie) {
|
|
94
|
+
droppedInvalid += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const domainKey = extractCookieDomain(cookie) || "(unknown)";
|
|
98
|
+
domainCounts[domainKey] = (domainCounts[domainKey] || 0) + 1;
|
|
99
|
+
const identityDomain = normalizedCookie.domain ? normalizeCookieDomain(normalizedCookie.domain) : "";
|
|
100
|
+
const dedupeKey = [
|
|
101
|
+
normalizedCookie.name,
|
|
102
|
+
identityDomain,
|
|
103
|
+
normalizedCookie.path || "/"
|
|
104
|
+
].join("");
|
|
105
|
+
dedupeMap.set(dedupeKey, normalizedCookie);
|
|
106
|
+
}
|
|
107
|
+
const dedupedCookies = dedupeMap.size;
|
|
108
|
+
return {
|
|
109
|
+
cookies: Array.from(dedupeMap.values()),
|
|
110
|
+
totalCookies: cookies.length,
|
|
111
|
+
matchedCookies,
|
|
112
|
+
dedupedCookies,
|
|
113
|
+
droppedInvalid,
|
|
114
|
+
filteredDomains,
|
|
115
|
+
domainCounts
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/cli/profile.ts
|
|
120
|
+
var HELP_TEXT = `Usage: opensteer profile <command> [options]
|
|
121
|
+
|
|
122
|
+
Manage cloud browser profiles and sync local cookie state into cloud profiles.
|
|
123
|
+
|
|
124
|
+
Commands:
|
|
125
|
+
list List cloud browser profiles
|
|
126
|
+
create --name <name> Create a cloud browser profile
|
|
127
|
+
sync Sync local profile cookies to cloud
|
|
128
|
+
|
|
129
|
+
Cloud auth options (all commands):
|
|
130
|
+
--api-key <key> Cloud API key (defaults to OPENSTEER_API_KEY)
|
|
131
|
+
--access-token <token> Cloud bearer access token (defaults to OPENSTEER_ACCESS_TOKEN)
|
|
132
|
+
--base-url <url> Cloud API base URL (defaults to env or the last selected host)
|
|
133
|
+
--auth-scheme <scheme> api-key (default) or bearer
|
|
134
|
+
--json JSON output (progress logs go to stderr)
|
|
135
|
+
|
|
136
|
+
Sync options:
|
|
137
|
+
--from-profile-dir <dir> Local browser profile directory or Chromium user-data dir to read cookies from (required)
|
|
138
|
+
--to-profile-id <id> Destination cloud profile id
|
|
139
|
+
--name <name> Create destination cloud profile with this name if --to-profile-id is omitted
|
|
140
|
+
--domain <domain> Restrict sync to one domain (repeatable)
|
|
141
|
+
--all-domains Explicitly sync all domains
|
|
142
|
+
--dry-run Analyze cookies and scope without uploading to cloud
|
|
143
|
+
--yes Required for non-interactive execution
|
|
144
|
+
--headless <true|false> Browser headless mode for local/cloud sync sessions (default: true)
|
|
145
|
+
-h, --help Show this help
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
opensteer profile list
|
|
149
|
+
opensteer profile create --name "My Session Profile"
|
|
150
|
+
opensteer profile sync --from-profile-dir ~/Library/Application\\ Support/Google/Chrome/Default --to-profile-id bp_123 --domain github.com
|
|
151
|
+
opensteer profile sync --from-profile-dir ./my-profile --name "Imported Profile" --all-domains --yes
|
|
152
|
+
`;
|
|
153
|
+
function parseBooleanValue(raw, flag) {
|
|
154
|
+
const normalized = raw.trim().toLowerCase();
|
|
155
|
+
if (normalized === "true" || normalized === "1") {
|
|
156
|
+
return { ok: true, value: true };
|
|
157
|
+
}
|
|
158
|
+
if (normalized === "false" || normalized === "0") {
|
|
159
|
+
return { ok: true, value: false };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: `${flag} must be "true" or "false".`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function parseAuthScheme(raw) {
|
|
167
|
+
const normalized = raw.trim().toLowerCase();
|
|
168
|
+
if (normalized === "api-key" || normalized === "bearer") {
|
|
169
|
+
return normalized;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
function isBrowserProfileStatus(value) {
|
|
174
|
+
return value === "active" || value === "archived" || value === "error";
|
|
175
|
+
}
|
|
176
|
+
function readFlagValue(args, index, flag) {
|
|
177
|
+
const value = args[index + 1];
|
|
178
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: `${flag} requires a value.`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
value,
|
|
187
|
+
nextIndex: index + 1
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function parseListArgs(rawArgs) {
|
|
191
|
+
const args = {};
|
|
192
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
193
|
+
const arg = rawArgs[i];
|
|
194
|
+
if (arg === "--json") {
|
|
195
|
+
args.json = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg === "--api-key") {
|
|
199
|
+
const value = readFlagValue(rawArgs, i, "--api-key");
|
|
200
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
201
|
+
args.apiKey = value.value;
|
|
202
|
+
i = value.nextIndex;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (arg === "--access-token") {
|
|
206
|
+
const value = readFlagValue(rawArgs, i, "--access-token");
|
|
207
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
208
|
+
args.accessToken = value.value;
|
|
209
|
+
i = value.nextIndex;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (arg === "--base-url") {
|
|
213
|
+
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
214
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
215
|
+
args.baseUrl = value.value;
|
|
216
|
+
i = value.nextIndex;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (arg === "--auth-scheme") {
|
|
220
|
+
const value = readFlagValue(rawArgs, i, "--auth-scheme");
|
|
221
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
222
|
+
const parsed = parseAuthScheme(value.value);
|
|
223
|
+
if (!parsed) {
|
|
224
|
+
return {
|
|
225
|
+
mode: "error",
|
|
226
|
+
error: '--auth-scheme must be "api-key" or "bearer".'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
args.authScheme = parsed;
|
|
230
|
+
i = value.nextIndex;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (arg === "--cursor") {
|
|
234
|
+
const value = readFlagValue(rawArgs, i, "--cursor");
|
|
235
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
236
|
+
args.cursor = value.value;
|
|
237
|
+
i = value.nextIndex;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (arg === "--limit") {
|
|
241
|
+
const value = readFlagValue(rawArgs, i, "--limit");
|
|
242
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
243
|
+
const parsed = Number.parseInt(value.value, 10);
|
|
244
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
245
|
+
return {
|
|
246
|
+
mode: "error",
|
|
247
|
+
error: "--limit must be a positive integer."
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
args.limit = parsed;
|
|
251
|
+
i = value.nextIndex;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (arg === "--status") {
|
|
255
|
+
const value = readFlagValue(rawArgs, i, "--status");
|
|
256
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
257
|
+
const status = value.value.trim();
|
|
258
|
+
if (!isBrowserProfileStatus(status)) {
|
|
259
|
+
return {
|
|
260
|
+
mode: "error",
|
|
261
|
+
error: "--status must be one of: active, archived, error."
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
args.status = status;
|
|
265
|
+
i = value.nextIndex;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
mode: "error",
|
|
270
|
+
error: `Unsupported option "${arg}" for "opensteer profile list".`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return { mode: "list", args };
|
|
274
|
+
}
|
|
275
|
+
function parseCreateArgs(rawArgs) {
|
|
276
|
+
const args = {};
|
|
277
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
278
|
+
const arg = rawArgs[i];
|
|
279
|
+
if (arg === "--json") {
|
|
280
|
+
args.json = true;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (arg === "--name") {
|
|
284
|
+
const value = readFlagValue(rawArgs, i, "--name");
|
|
285
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
286
|
+
args.name = value.value.trim();
|
|
287
|
+
i = value.nextIndex;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (arg === "--api-key") {
|
|
291
|
+
const value = readFlagValue(rawArgs, i, "--api-key");
|
|
292
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
293
|
+
args.apiKey = value.value;
|
|
294
|
+
i = value.nextIndex;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (arg === "--access-token") {
|
|
298
|
+
const value = readFlagValue(rawArgs, i, "--access-token");
|
|
299
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
300
|
+
args.accessToken = value.value;
|
|
301
|
+
i = value.nextIndex;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (arg === "--base-url") {
|
|
305
|
+
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
306
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
307
|
+
args.baseUrl = value.value;
|
|
308
|
+
i = value.nextIndex;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (arg === "--auth-scheme") {
|
|
312
|
+
const value = readFlagValue(rawArgs, i, "--auth-scheme");
|
|
313
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
314
|
+
const parsed = parseAuthScheme(value.value);
|
|
315
|
+
if (!parsed) {
|
|
316
|
+
return {
|
|
317
|
+
mode: "error",
|
|
318
|
+
error: '--auth-scheme must be "api-key" or "bearer".'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
args.authScheme = parsed;
|
|
322
|
+
i = value.nextIndex;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
mode: "error",
|
|
327
|
+
error: `Unsupported option "${arg}" for "opensteer profile create".`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (!args.name) {
|
|
331
|
+
return {
|
|
332
|
+
mode: "error",
|
|
333
|
+
error: '--name is required for "opensteer profile create".'
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
mode: "create",
|
|
338
|
+
args: {
|
|
339
|
+
...args,
|
|
340
|
+
name: args.name
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function parseSyncArgs(rawArgs) {
|
|
345
|
+
const args = {
|
|
346
|
+
fromProfileDir: "",
|
|
347
|
+
domains: [],
|
|
348
|
+
allDomains: false,
|
|
349
|
+
dryRun: false,
|
|
350
|
+
yes: false,
|
|
351
|
+
headless: true
|
|
352
|
+
};
|
|
353
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
354
|
+
const arg = rawArgs[i];
|
|
355
|
+
if (arg === "--json") {
|
|
356
|
+
args.json = true;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (arg === "--all-domains") {
|
|
360
|
+
args.allDomains = true;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (arg === "--dry-run") {
|
|
364
|
+
args.dryRun = true;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (arg === "--yes") {
|
|
368
|
+
args.yes = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (arg === "--from-profile-dir") {
|
|
372
|
+
const value = readFlagValue(rawArgs, i, "--from-profile-dir");
|
|
373
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
374
|
+
args.fromProfileDir = value.value;
|
|
375
|
+
i = value.nextIndex;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (arg === "--to-profile-id") {
|
|
379
|
+
const value = readFlagValue(rawArgs, i, "--to-profile-id");
|
|
380
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
381
|
+
args.toProfileId = value.value;
|
|
382
|
+
i = value.nextIndex;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (arg === "--name") {
|
|
386
|
+
const value = readFlagValue(rawArgs, i, "--name");
|
|
387
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
388
|
+
args.name = value.value;
|
|
389
|
+
i = value.nextIndex;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (arg === "--domain") {
|
|
393
|
+
const value = readFlagValue(rawArgs, i, "--domain");
|
|
394
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
395
|
+
args.domains.push(value.value);
|
|
396
|
+
i = value.nextIndex;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (arg === "--headless") {
|
|
400
|
+
const value = readFlagValue(rawArgs, i, "--headless");
|
|
401
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
402
|
+
const parsed = parseBooleanValue(value.value, "--headless");
|
|
403
|
+
if (!parsed.ok) return { mode: "error", error: parsed.error };
|
|
404
|
+
args.headless = parsed.value;
|
|
405
|
+
i = value.nextIndex;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (arg === "--api-key") {
|
|
409
|
+
const value = readFlagValue(rawArgs, i, "--api-key");
|
|
410
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
411
|
+
args.apiKey = value.value;
|
|
412
|
+
i = value.nextIndex;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (arg === "--access-token") {
|
|
416
|
+
const value = readFlagValue(rawArgs, i, "--access-token");
|
|
417
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
418
|
+
args.accessToken = value.value;
|
|
419
|
+
i = value.nextIndex;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (arg === "--base-url") {
|
|
423
|
+
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
424
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
425
|
+
args.baseUrl = value.value;
|
|
426
|
+
i = value.nextIndex;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (arg === "--auth-scheme") {
|
|
430
|
+
const value = readFlagValue(rawArgs, i, "--auth-scheme");
|
|
431
|
+
if (!value.ok) return { mode: "error", error: value.error };
|
|
432
|
+
const parsed = parseAuthScheme(value.value);
|
|
433
|
+
if (!parsed) {
|
|
434
|
+
return {
|
|
435
|
+
mode: "error",
|
|
436
|
+
error: '--auth-scheme must be "api-key" or "bearer".'
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
args.authScheme = parsed;
|
|
440
|
+
i = value.nextIndex;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
mode: "error",
|
|
445
|
+
error: `Unsupported option "${arg}" for "opensteer profile sync".`
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (!args.fromProfileDir.trim()) {
|
|
449
|
+
return {
|
|
450
|
+
mode: "error",
|
|
451
|
+
error: '--from-profile-dir is required for "opensteer profile sync".'
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (args.allDomains && args.domains.length > 0) {
|
|
455
|
+
return {
|
|
456
|
+
mode: "error",
|
|
457
|
+
error: "Use either --all-domains or --domain, not both."
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return { mode: "sync", args };
|
|
461
|
+
}
|
|
462
|
+
function parseOpensteerProfileArgs(rawArgs) {
|
|
463
|
+
if (!rawArgs.length) {
|
|
464
|
+
return { mode: "help" };
|
|
465
|
+
}
|
|
466
|
+
const [subcommand, ...rest] = rawArgs;
|
|
467
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
468
|
+
return { mode: "help" };
|
|
469
|
+
}
|
|
470
|
+
if (subcommand === "list") {
|
|
471
|
+
return parseListArgs(rest);
|
|
472
|
+
}
|
|
473
|
+
if (subcommand === "create") {
|
|
474
|
+
return parseCreateArgs(rest);
|
|
475
|
+
}
|
|
476
|
+
if (subcommand === "sync") {
|
|
477
|
+
return parseSyncArgs(rest);
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
mode: "error",
|
|
481
|
+
error: `Unsupported profile subcommand "${subcommand}".`
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function createDefaultDeps() {
|
|
485
|
+
return {
|
|
486
|
+
env: process.env,
|
|
487
|
+
createBrowserProfileClient: (context) => new BrowserProfileClient(
|
|
488
|
+
context.baseUrl,
|
|
489
|
+
context.token,
|
|
490
|
+
context.authScheme
|
|
491
|
+
),
|
|
492
|
+
createOpensteer: (config) => new Opensteer(config),
|
|
493
|
+
loadLocalProfileCookies: (profileDir, options) => loadCookiesFromLocalProfileDir(profileDir, options),
|
|
494
|
+
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
495
|
+
confirm: async (message) => {
|
|
496
|
+
const rl = createInterface({
|
|
497
|
+
input: process.stdin,
|
|
498
|
+
output: process.stderr
|
|
499
|
+
});
|
|
500
|
+
try {
|
|
501
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
502
|
+
const normalized = answer.trim().toLowerCase();
|
|
503
|
+
return normalized === "y" || normalized === "yes";
|
|
504
|
+
} finally {
|
|
505
|
+
rl.close();
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
writeStdout: (message) => process.stdout.write(message),
|
|
509
|
+
writeStderr: (message) => process.stderr.write(message)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
async function buildCloudAuthContext(args, deps) {
|
|
513
|
+
const ensured = await ensureCloudCredentialsForCommand({
|
|
514
|
+
commandName: "opensteer profile",
|
|
515
|
+
env: deps.env,
|
|
516
|
+
apiKeyFlag: args.apiKey,
|
|
517
|
+
accessTokenFlag: args.accessToken,
|
|
518
|
+
baseUrl: args.baseUrl,
|
|
519
|
+
interactive: deps.isInteractive(),
|
|
520
|
+
autoLoginIfNeeded: true,
|
|
521
|
+
writeProgress: args.json ? deps.writeStderr : deps.writeStdout,
|
|
522
|
+
writeStderr: deps.writeStderr
|
|
523
|
+
});
|
|
524
|
+
if (args.authScheme) {
|
|
525
|
+
if (ensured.kind === "access-token" && args.authScheme !== "bearer") {
|
|
526
|
+
throw new Error(
|
|
527
|
+
"--auth-scheme=api-key is incompatible with --access-token or saved login credentials."
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
if (ensured.kind === "api-key" && args.authScheme === "bearer") {
|
|
531
|
+
return {
|
|
532
|
+
...ensured,
|
|
533
|
+
authScheme: "bearer"
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
token: ensured.token,
|
|
539
|
+
baseUrl: ensured.baseUrl,
|
|
540
|
+
authScheme: ensured.authScheme,
|
|
541
|
+
kind: ensured.kind,
|
|
542
|
+
source: ensured.source
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function printProfileHelp(deps) {
|
|
546
|
+
deps.writeStdout(`${HELP_TEXT}
|
|
547
|
+
`);
|
|
548
|
+
}
|
|
549
|
+
function writeJson(deps, payload) {
|
|
550
|
+
deps.writeStdout(`${JSON.stringify(payload)}
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
function writeHumanLine(deps, message) {
|
|
554
|
+
deps.writeStdout(`${message}
|
|
555
|
+
`);
|
|
556
|
+
}
|
|
557
|
+
function writeProgressLine(deps, jsonOutput, message) {
|
|
558
|
+
if (jsonOutput) {
|
|
559
|
+
deps.writeStderr(`${message}
|
|
560
|
+
`);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
deps.writeStdout(`${message}
|
|
564
|
+
`);
|
|
565
|
+
}
|
|
566
|
+
async function runList(args, deps) {
|
|
567
|
+
const auth = await buildCloudAuthContext(args, deps);
|
|
568
|
+
const client = deps.createBrowserProfileClient(auth);
|
|
569
|
+
const response = await client.list({
|
|
570
|
+
cursor: args.cursor,
|
|
571
|
+
limit: args.limit,
|
|
572
|
+
status: args.status
|
|
573
|
+
});
|
|
574
|
+
if (args.json) {
|
|
575
|
+
writeJson(deps, response);
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
if (!response.profiles.length) {
|
|
579
|
+
writeHumanLine(deps, "No cloud browser profiles found.");
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
writeHumanLine(deps, `Cloud browser profiles (${response.profiles.length}):`);
|
|
583
|
+
for (const profile of response.profiles) {
|
|
584
|
+
writeHumanLine(
|
|
585
|
+
deps,
|
|
586
|
+
` ${profile.profileId} ${profile.name} [${profile.status}]`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (response.nextCursor) {
|
|
590
|
+
writeHumanLine(
|
|
591
|
+
deps,
|
|
592
|
+
`Next cursor: ${response.nextCursor}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
async function runCreate(args, deps) {
|
|
598
|
+
const auth = await buildCloudAuthContext(args, deps);
|
|
599
|
+
const client = deps.createBrowserProfileClient(auth);
|
|
600
|
+
const profile = await client.create({
|
|
601
|
+
name: args.name
|
|
602
|
+
});
|
|
603
|
+
if (args.json) {
|
|
604
|
+
writeJson(deps, profile);
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
writeHumanLine(
|
|
608
|
+
deps,
|
|
609
|
+
`Created cloud browser profile "${profile.name}" (${profile.profileId}).`
|
|
610
|
+
);
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
async function importCookiesInBatches(context, cookies, batchSize = 100) {
|
|
614
|
+
let imported = 0;
|
|
615
|
+
let skipped = 0;
|
|
616
|
+
for (let offset = 0; offset < cookies.length; offset += batchSize) {
|
|
617
|
+
const batch = cookies.slice(offset, offset + batchSize);
|
|
618
|
+
try {
|
|
619
|
+
await context.addCookies(batch);
|
|
620
|
+
imported += batch.length;
|
|
621
|
+
} catch {
|
|
622
|
+
for (const cookie of batch) {
|
|
623
|
+
try {
|
|
624
|
+
await context.addCookies([cookie]);
|
|
625
|
+
imported += 1;
|
|
626
|
+
} catch {
|
|
627
|
+
skipped += 1;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
imported,
|
|
634
|
+
skipped
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
async function resolveTargetProfileId(args, deps, client) {
|
|
638
|
+
const explicitProfileId = args.toProfileId?.trim();
|
|
639
|
+
if (explicitProfileId) {
|
|
640
|
+
return {
|
|
641
|
+
profileId: explicitProfileId,
|
|
642
|
+
created: false
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const requestedName = args.name?.trim();
|
|
646
|
+
if (requestedName) {
|
|
647
|
+
const profile = await client.create({
|
|
648
|
+
name: requestedName
|
|
649
|
+
});
|
|
650
|
+
return {
|
|
651
|
+
profileId: profile.profileId,
|
|
652
|
+
created: true
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (!deps.isInteractive()) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
"Sync target is required in non-interactive mode. Use --to-profile-id <id> or --name <name>."
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const defaultName = `Synced ${path.basename(args.fromProfileDir)}`;
|
|
661
|
+
const shouldCreate = await deps.confirm(
|
|
662
|
+
`No destination profile provided. Create a new cloud profile named "${defaultName}"?`
|
|
663
|
+
);
|
|
664
|
+
if (!shouldCreate) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
"Profile sync cancelled. Provide --to-profile-id or --name to choose a destination profile."
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const created = await client.create({
|
|
670
|
+
name: defaultName
|
|
671
|
+
});
|
|
672
|
+
return {
|
|
673
|
+
profileId: created.profileId,
|
|
674
|
+
created: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function resolveSyncBrowserProfilePreference(profileId, env) {
|
|
678
|
+
const resolved = resolveConfigWithEnv({
|
|
679
|
+
cloud: true
|
|
680
|
+
}, {
|
|
681
|
+
env
|
|
682
|
+
}).config;
|
|
683
|
+
const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
|
|
684
|
+
const configured = cloudConfig?.browserProfile;
|
|
685
|
+
if (configured && configured.profileId.trim() === profileId && configured.reuseIfActive !== void 0) {
|
|
686
|
+
return {
|
|
687
|
+
profileId,
|
|
688
|
+
reuseIfActive: configured.reuseIfActive
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return { profileId };
|
|
692
|
+
}
|
|
693
|
+
async function runSync(args, deps) {
|
|
694
|
+
const sourceProfileDir = expandHome(args.fromProfileDir.trim());
|
|
695
|
+
const nonInteractive = !deps.isInteractive();
|
|
696
|
+
const hasExplicitScope = args.allDomains || args.domains.length > 0;
|
|
697
|
+
if (nonInteractive && !args.yes) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
"Non-interactive profile sync requires --yes."
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (nonInteractive && !hasExplicitScope) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
"Non-interactive profile sync requires explicit scope: --domain <domain> (repeatable) or --all-domains."
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (!hasExplicitScope && !nonInteractive) {
|
|
708
|
+
const confirmed = await deps.confirm(
|
|
709
|
+
"No domain filter provided. Sync cookies for all domains?"
|
|
710
|
+
);
|
|
711
|
+
if (!confirmed) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
"Profile sync cancelled. Use --domain <domain> or --all-domains."
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
writeProgressLine(
|
|
718
|
+
deps,
|
|
719
|
+
Boolean(args.json),
|
|
720
|
+
`Reading cookies from local profile: ${sourceProfileDir}`
|
|
721
|
+
);
|
|
722
|
+
let sourceCookies = [];
|
|
723
|
+
sourceCookies = await deps.loadLocalProfileCookies(sourceProfileDir, {
|
|
724
|
+
headless: args.headless,
|
|
725
|
+
timeout: 12e4
|
|
726
|
+
});
|
|
727
|
+
const prepared = prepareCookiesForSync(sourceCookies, {
|
|
728
|
+
domains: args.allDomains ? [] : args.domains
|
|
729
|
+
});
|
|
730
|
+
if (!prepared.cookies.length) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
"No syncable cookies found for the selected profile and scope."
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (args.dryRun) {
|
|
736
|
+
const payload2 = {
|
|
737
|
+
success: true,
|
|
738
|
+
dryRun: true,
|
|
739
|
+
profileId: args.toProfileId?.trim() || null,
|
|
740
|
+
createdProfile: false,
|
|
741
|
+
totalCookies: prepared.totalCookies,
|
|
742
|
+
matchedCookies: prepared.matchedCookies,
|
|
743
|
+
dedupedCookies: prepared.dedupedCookies,
|
|
744
|
+
droppedInvalid: prepared.droppedInvalid,
|
|
745
|
+
filteredDomains: prepared.filteredDomains,
|
|
746
|
+
domainCounts: prepared.domainCounts
|
|
747
|
+
};
|
|
748
|
+
if (args.json) {
|
|
749
|
+
writeJson(deps, payload2);
|
|
750
|
+
} else {
|
|
751
|
+
writeHumanLine(deps, "Dry run complete.");
|
|
752
|
+
writeHumanLine(deps, ` Total cookies: ${prepared.totalCookies}`);
|
|
753
|
+
writeHumanLine(deps, ` Scope-matched cookies: ${prepared.matchedCookies}`);
|
|
754
|
+
writeHumanLine(deps, ` Deduped cookies: ${prepared.dedupedCookies}`);
|
|
755
|
+
writeHumanLine(deps, ` Dropped invalid: ${prepared.droppedInvalid}`);
|
|
756
|
+
if (prepared.filteredDomains.length) {
|
|
757
|
+
writeHumanLine(
|
|
758
|
+
deps,
|
|
759
|
+
` Domain filters: ${prepared.filteredDomains.join(", ")}`
|
|
760
|
+
);
|
|
761
|
+
} else {
|
|
762
|
+
writeHumanLine(deps, " Domain scope: all domains");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
767
|
+
const auth = await buildCloudAuthContext(args, deps);
|
|
768
|
+
const client = deps.createBrowserProfileClient(auth);
|
|
769
|
+
const target = await resolveTargetProfileId(args, deps, client);
|
|
770
|
+
const targetBrowserProfile = resolveSyncBrowserProfilePreference(
|
|
771
|
+
target.profileId,
|
|
772
|
+
deps.env
|
|
773
|
+
);
|
|
774
|
+
writeProgressLine(
|
|
775
|
+
deps,
|
|
776
|
+
Boolean(args.json),
|
|
777
|
+
`Importing ${prepared.cookies.length} cookies into cloud profile ${target.profileId}`
|
|
778
|
+
);
|
|
779
|
+
const cloud = deps.createOpensteer({
|
|
780
|
+
cloud: {
|
|
781
|
+
...auth.kind === "api-key" ? { apiKey: auth.token } : { accessToken: auth.token },
|
|
782
|
+
baseUrl: auth.baseUrl,
|
|
783
|
+
authScheme: auth.authScheme,
|
|
784
|
+
browserProfile: targetBrowserProfile
|
|
785
|
+
},
|
|
786
|
+
cursor: { enabled: false }
|
|
787
|
+
});
|
|
788
|
+
let imported = 0;
|
|
789
|
+
let skipped = 0;
|
|
790
|
+
try {
|
|
791
|
+
await cloud.launch({
|
|
792
|
+
headless: args.headless,
|
|
793
|
+
timeout: 12e4
|
|
794
|
+
});
|
|
795
|
+
const result = await importCookiesInBatches(cloud.context, prepared.cookies);
|
|
796
|
+
imported = result.imported;
|
|
797
|
+
skipped = result.skipped;
|
|
798
|
+
} finally {
|
|
799
|
+
await cloud.close().catch(() => void 0);
|
|
800
|
+
}
|
|
801
|
+
const payload = {
|
|
802
|
+
success: true,
|
|
803
|
+
profileId: target.profileId,
|
|
804
|
+
createdProfile: target.created,
|
|
805
|
+
totalCookies: prepared.totalCookies,
|
|
806
|
+
matchedCookies: prepared.matchedCookies,
|
|
807
|
+
dedupedCookies: prepared.dedupedCookies,
|
|
808
|
+
droppedInvalid: prepared.droppedInvalid,
|
|
809
|
+
importedCookies: imported,
|
|
810
|
+
skippedCookies: skipped,
|
|
811
|
+
filteredDomains: prepared.filteredDomains,
|
|
812
|
+
domainCounts: prepared.domainCounts
|
|
813
|
+
};
|
|
814
|
+
if (args.json) {
|
|
815
|
+
writeJson(deps, payload);
|
|
816
|
+
return 0;
|
|
817
|
+
}
|
|
818
|
+
writeHumanLine(deps, "Profile cookie sync complete.");
|
|
819
|
+
writeHumanLine(deps, ` Cloud profile: ${target.profileId}`);
|
|
820
|
+
writeHumanLine(deps, ` Imported cookies: ${imported}`);
|
|
821
|
+
writeHumanLine(deps, ` Skipped cookies: ${skipped}`);
|
|
822
|
+
if (prepared.filteredDomains.length) {
|
|
823
|
+
writeHumanLine(
|
|
824
|
+
deps,
|
|
825
|
+
` Domain filters: ${prepared.filteredDomains.join(", ")}`
|
|
826
|
+
);
|
|
827
|
+
} else {
|
|
828
|
+
writeHumanLine(deps, " Domain scope: all domains");
|
|
829
|
+
}
|
|
830
|
+
return 0;
|
|
831
|
+
}
|
|
832
|
+
async function runOpensteerProfileCli(rawArgs, overrideDeps = {}) {
|
|
833
|
+
const deps = {
|
|
834
|
+
...createDefaultDeps(),
|
|
835
|
+
...overrideDeps
|
|
836
|
+
};
|
|
837
|
+
const parsed = parseOpensteerProfileArgs(rawArgs);
|
|
838
|
+
if (parsed.mode === "help") {
|
|
839
|
+
printProfileHelp(deps);
|
|
840
|
+
return 0;
|
|
841
|
+
}
|
|
842
|
+
if (parsed.mode === "error") {
|
|
843
|
+
deps.writeStderr(`${parsed.error}
|
|
844
|
+
`);
|
|
845
|
+
deps.writeStderr('Run "opensteer profile --help" for usage.\n');
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
if (parsed.mode === "list") {
|
|
850
|
+
return await runList(parsed.args, deps);
|
|
851
|
+
}
|
|
852
|
+
if (parsed.mode === "create") {
|
|
853
|
+
return await runCreate(parsed.args, deps);
|
|
854
|
+
}
|
|
855
|
+
return await runSync(parsed.args, deps);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
const message = error instanceof Error ? error.message : "Profile command failed.";
|
|
858
|
+
deps.writeStderr(`${message}
|
|
859
|
+
`);
|
|
860
|
+
return 1;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
export {
|
|
864
|
+
parseOpensteerProfileArgs,
|
|
865
|
+
runOpensteerProfileCli
|
|
866
|
+
};
|