@midscene/cli 1.14.1-beta-20261008033312.0 → 1.15.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/dist/es/index.mjs +427 -6
- package/dist/es/index.mjs.map +1 -1
- package/dist/lib/framework/index.js +6 -2
- package/dist/lib/framework/index.js.map +1 -1
- package/dist/lib/index.js +434 -6
- package/dist/lib/index.js.map +1 -1
- package/package.json +10 -9
package/dist/es/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { basename, dirname as external_node_path_dirname, extname, join, posix,
|
|
|
5
5
|
import { interpolateEnvVars, midsceneYamlTargetKeys, parseYamlScript as yaml_parseYamlScript, resolveWebTarget } from "@midscene/core/yaml";
|
|
6
6
|
import { getDebug } from "@midscene/shared/logger";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { lstat, readdir, readlink, realpath } from "node:fs/promises";
|
|
8
|
+
import { lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
import node_stream from "node:stream";
|
|
11
11
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath as external_url_fileURLToPath } from "url";
|
|
|
13
13
|
import node_process, { cwd as external_node_process_cwd } from "node:process";
|
|
14
14
|
import lodash_merge from "lodash.merge";
|
|
15
15
|
import { loadDotenvConfig, runModelCommand } from "@midscene/test/internal/model-command";
|
|
16
|
-
import { createHash } from "node:crypto";
|
|
16
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
17
17
|
import { getMidsceneRunSubDir } from "@midscene/shared/common";
|
|
18
18
|
import { buildChromeArgs, buildDownloadBehavior, defaultViewportHeight, defaultViewportWidth } from "@midscene/web/puppeteer-agent-launcher";
|
|
19
19
|
import puppeteer from "puppeteer";
|
|
@@ -21,6 +21,10 @@ import { createYamlPlayer as yaml_runtime_createYamlPlayer } from "@midscene/tes
|
|
|
21
21
|
import { stripVTControlCharacters } from "node:util";
|
|
22
22
|
import { createRequire } from "node:module";
|
|
23
23
|
import { serializeError } from "@midscene/shared/agent-tools/error-formatter";
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { createServer } from "node:http";
|
|
27
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
24
28
|
import { classifyWorkflowDocumentFormat } from "@midscene/core/internal/test-runner";
|
|
25
29
|
import * as __rspack_external_assert from "assert";
|
|
26
30
|
import * as __rspack_external_fs from "fs";
|
|
@@ -2796,7 +2800,9 @@ var __webpack_modules__ = {
|
|
|
2796
2800
|
module.exports = (string, columns, options)=>String(string).normalize().replace(/\r\n/g, '\n').split('\n').map((line)=>exec(line, columns, options)).join('\n');
|
|
2797
2801
|
},
|
|
2798
2802
|
"./src/index.ts" (__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
2799
|
-
var package_namespaceObject =
|
|
2803
|
+
var package_namespaceObject = {
|
|
2804
|
+
rE: "1.15.0"
|
|
2805
|
+
};
|
|
2800
2806
|
var brace_expansion = __webpack_require__("../../node_modules/.pnpm/brace-expansion@2.0.1/node_modules/brace-expansion/index.js");
|
|
2801
2807
|
const MAX_PATTERN_LENGTH = 65536;
|
|
2802
2808
|
const assertValidPattern = (pattern)=>{
|
|
@@ -8407,6 +8413,7 @@ Homepage: https://midscenejs.com
|
|
|
8407
8413
|
Github: https://github.com/web-infra-dev/midscene
|
|
8408
8414
|
|
|
8409
8415
|
Usage:
|
|
8416
|
+
$0 model siwc <login|refresh> [options]
|
|
8410
8417
|
$0 [options] <path-to-yaml-script-file-or-directory>
|
|
8411
8418
|
$0 [options] --files <yaml-file1> <yaml-file2 ...>
|
|
8412
8419
|
$0 [options] --config <path-to-config-yaml-file>
|
|
@@ -8468,7 +8475,7 @@ Usage:
|
|
|
8468
8475
|
type: 'boolean',
|
|
8469
8476
|
description: 'Whether to dismiss the HarmonyOS keyboard after text input'
|
|
8470
8477
|
}
|
|
8471
|
-
}).version('version', 'Show version number', "1.
|
|
8478
|
+
}).version('version', 'Show version number', "1.15.0").help().epilogue(`For complete list of configuration options, please visit:
|
|
8472
8479
|
• Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
|
|
8473
8480
|
• Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
|
|
8474
8481
|
• HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
|
|
@@ -12483,17 +12490,431 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12483
12490
|
const success = printExecutionSummary(results, summaryPath);
|
|
12484
12491
|
return success ? exitCode : 1;
|
|
12485
12492
|
}
|
|
12493
|
+
const defaultCredentialsPath = ()=>join(homedir(), '.midscene', 'siwc.json');
|
|
12494
|
+
async function readCredentialsFile(path) {
|
|
12495
|
+
const content = await readFile(path, 'utf8').catch((error)=>{
|
|
12496
|
+
if ('ENOENT' === error.code) return;
|
|
12497
|
+
throw new Error('Unable to read the local SIWC credentials file.');
|
|
12498
|
+
});
|
|
12499
|
+
if (void 0 === content) return;
|
|
12500
|
+
try {
|
|
12501
|
+
const value = JSON.parse(content);
|
|
12502
|
+
if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new Error('Invalid SIWC credentials record.');
|
|
12503
|
+
return value;
|
|
12504
|
+
} catch {
|
|
12505
|
+
throw new Error('Invalid local SIWC credentials file.');
|
|
12506
|
+
}
|
|
12507
|
+
}
|
|
12508
|
+
async function readRegistration(path) {
|
|
12509
|
+
return (await readCredentialsFile(path))?.credentials;
|
|
12510
|
+
}
|
|
12511
|
+
async function saveRegistration(path, value) {
|
|
12512
|
+
const saved = await readCredentialsFile(path);
|
|
12513
|
+
const now = new Date().toISOString();
|
|
12514
|
+
const record = {
|
|
12515
|
+
...saved,
|
|
12516
|
+
created_at: saved?.created_at ?? now,
|
|
12517
|
+
updated_at: now,
|
|
12518
|
+
credentials: value
|
|
12519
|
+
};
|
|
12520
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
12521
|
+
try {
|
|
12522
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
|
|
12523
|
+
mode: 384,
|
|
12524
|
+
flag: 'wx'
|
|
12525
|
+
});
|
|
12526
|
+
await rename(temporary, path);
|
|
12527
|
+
} finally{
|
|
12528
|
+
await rm(temporary, {
|
|
12529
|
+
force: true
|
|
12530
|
+
});
|
|
12531
|
+
}
|
|
12532
|
+
}
|
|
12533
|
+
async function lockCredentials(path) {
|
|
12534
|
+
await mkdir(external_node_path_dirname(path), {
|
|
12535
|
+
recursive: true,
|
|
12536
|
+
mode: 448
|
|
12537
|
+
});
|
|
12538
|
+
const lockPath = `${path}.lock`;
|
|
12539
|
+
try {
|
|
12540
|
+
await mkdir(lockPath, {
|
|
12541
|
+
mode: 448
|
|
12542
|
+
});
|
|
12543
|
+
} catch (error) {
|
|
12544
|
+
if ('EEXIST' === error.code) throw new Error(`Another SIWC command holds ${lockPath}. If it was interrupted, remove this lock only after confirming that no SIWC command is running.`);
|
|
12545
|
+
throw new Error('Unable to lock the local SIWC credentials file.');
|
|
12546
|
+
}
|
|
12547
|
+
return ()=>rm(lockPath, {
|
|
12548
|
+
recursive: true,
|
|
12549
|
+
force: true
|
|
12550
|
+
});
|
|
12551
|
+
}
|
|
12552
|
+
function requireCredentials(value) {
|
|
12553
|
+
if (!value || [
|
|
12554
|
+
'client_id',
|
|
12555
|
+
'subject',
|
|
12556
|
+
'issuer',
|
|
12557
|
+
'access_token',
|
|
12558
|
+
'refresh_token',
|
|
12559
|
+
'id_token'
|
|
12560
|
+
].some((key)=>'string' != typeof value[key] || !value[key]) || !Array.isArray(value.scopes) || !value.scopes.every((scope)=>'string' == typeof scope) || !Number.isFinite(value.expires_at)) throw new Error('No valid SIWC credentials. Run midscene model siwc login first.');
|
|
12561
|
+
return value;
|
|
12562
|
+
}
|
|
12563
|
+
const ISSUER = 'https://auth.openai.com';
|
|
12564
|
+
const RESOURCE = 'https://api.openai.com/v1';
|
|
12565
|
+
const SCOPES = 'openid profile email offline_access resource.invoke chatgpt.tokens.use.direct';
|
|
12566
|
+
async function loginWithOpenAI(options, dependencies) {
|
|
12567
|
+
const hostId = options.hostId ?? `urn:uuid:${randomUUID()}`;
|
|
12568
|
+
const existingClientId = options.clientId;
|
|
12569
|
+
const state = randomBytes(32).toString('base64url');
|
|
12570
|
+
const nonce = randomBytes(32).toString('base64url');
|
|
12571
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
12572
|
+
const signal = options.signal;
|
|
12573
|
+
signal.throwIfAborted();
|
|
12574
|
+
const server = createServer();
|
|
12575
|
+
const callback = new Promise((resolve, reject)=>{
|
|
12576
|
+
const consumed = {
|
|
12577
|
+
value: false
|
|
12578
|
+
};
|
|
12579
|
+
server.on('request', (request, response)=>{
|
|
12580
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
12581
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
12582
|
+
response.setHeader('Referrer-Policy', 'no-referrer');
|
|
12583
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
12584
|
+
if ('GET' !== request.method || '/auth/callback' !== url.pathname) return void response.writeHead(404).end('Not found');
|
|
12585
|
+
if (1 !== url.searchParams.getAll('state').length || url.searchParams.get('state') !== state) return void response.writeHead(400).end('Invalid login state. Use the original authorization link.');
|
|
12586
|
+
if (consumed.value) return void response.writeHead(409).end('Authorization already received.');
|
|
12587
|
+
if (url.searchParams.has('error')) {
|
|
12588
|
+
consumed.value = true;
|
|
12589
|
+
response.writeHead(400).end('Authorization was not granted. Return to the terminal.');
|
|
12590
|
+
reject(new Error('OpenAI authorization was denied or failed.'));
|
|
12591
|
+
return;
|
|
12592
|
+
}
|
|
12593
|
+
const code = url.searchParams.get('code');
|
|
12594
|
+
const clientId = url.searchParams.get('client_id') || existingClientId;
|
|
12595
|
+
if (!code || !clientId || 'dynamic_agent_client' === clientId || existingClientId && clientId !== existingClientId || 1 !== url.searchParams.getAll('code').length || url.searchParams.getAll('client_id').length > 1) return void response.writeHead(400).end('Invalid authorization callback.');
|
|
12596
|
+
consumed.value = true;
|
|
12597
|
+
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
12598
|
+
response.end(`<!doctype html>
|
|
12599
|
+
<html lang="en">
|
|
12600
|
+
<head>
|
|
12601
|
+
<meta charset="utf-8">
|
|
12602
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12603
|
+
<title>Authorization received · Midscene</title>
|
|
12604
|
+
<style>
|
|
12605
|
+
* { box-sizing: border-box; }
|
|
12606
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #f6f7f9; color: #202124; font-family: system-ui, sans-serif; }
|
|
12607
|
+
main { width: 100%; max-width: 480px; padding: 40px 28px; text-align: center; background: white; border: 1px solid #e5e7eb; border-radius: 20px; box-shadow: 0 8px 32px #00000008; }
|
|
12608
|
+
h1 { margin: 0 0 20px; font-size: 24px; line-height: 1.4; }
|
|
12609
|
+
h1 span { color: #16834a; }
|
|
12610
|
+
p { margin: 8px 0 0; color: #596273; font-size: 15px; line-height: 1.6; }
|
|
12611
|
+
</style>
|
|
12612
|
+
</head>
|
|
12613
|
+
<body>
|
|
12614
|
+
<main>
|
|
12615
|
+
<h1><span aria-hidden="true">✓</span> Authorization received</h1>
|
|
12616
|
+
<p>You can close this tab.</p>
|
|
12617
|
+
<p>Check your terminal for the sign-in result and access token.</p>
|
|
12618
|
+
</main>
|
|
12619
|
+
</body>
|
|
12620
|
+
</html>`);
|
|
12621
|
+
resolve({
|
|
12622
|
+
code,
|
|
12623
|
+
clientId
|
|
12624
|
+
});
|
|
12625
|
+
});
|
|
12626
|
+
server.on('error', reject);
|
|
12627
|
+
});
|
|
12628
|
+
callback.catch(()=>void 0);
|
|
12629
|
+
const abort = ()=>server.emit('error', new Error('OpenAI login cancelled or timed out.'));
|
|
12630
|
+
signal.addEventListener('abort', abort, {
|
|
12631
|
+
once: true
|
|
12632
|
+
});
|
|
12633
|
+
try {
|
|
12634
|
+
await new Promise((resolve, reject)=>{
|
|
12635
|
+
server.once('error', reject);
|
|
12636
|
+
server.listen(options.port ?? 0, '127.0.0.1', ()=>{
|
|
12637
|
+
server.removeListener('error', reject);
|
|
12638
|
+
resolve();
|
|
12639
|
+
});
|
|
12640
|
+
});
|
|
12641
|
+
signal.throwIfAborted();
|
|
12642
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/auth/callback`;
|
|
12643
|
+
const authorization = new URL(`${ISSUER}/api/accounts/authorize`);
|
|
12644
|
+
authorization.search = new URLSearchParams({
|
|
12645
|
+
client_id: existingClientId ?? 'dynamic_agent_client',
|
|
12646
|
+
...existingClientId ? {} : {
|
|
12647
|
+
agent_name_hint: 'Midscene'
|
|
12648
|
+
},
|
|
12649
|
+
ext_agent_host_id: hostId,
|
|
12650
|
+
response_type: 'code',
|
|
12651
|
+
redirect_uri: redirectUri,
|
|
12652
|
+
scope: SCOPES,
|
|
12653
|
+
resource: RESOURCE,
|
|
12654
|
+
state,
|
|
12655
|
+
nonce,
|
|
12656
|
+
code_challenge_method: 'S256',
|
|
12657
|
+
code_challenge: createHash('sha256').update(verifier).digest('base64url')
|
|
12658
|
+
}).toString();
|
|
12659
|
+
await options.onAuthorization(authorization.toString());
|
|
12660
|
+
const { code, clientId } = await callback;
|
|
12661
|
+
signal.throwIfAborted();
|
|
12662
|
+
const request = dependencies?.fetch ?? fetch;
|
|
12663
|
+
const result = await request(`${ISSUER}/api/accounts/oauth/token`, {
|
|
12664
|
+
method: 'POST',
|
|
12665
|
+
headers: {
|
|
12666
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
12667
|
+
},
|
|
12668
|
+
body: new URLSearchParams({
|
|
12669
|
+
grant_type: 'authorization_code',
|
|
12670
|
+
client_id: clientId,
|
|
12671
|
+
code,
|
|
12672
|
+
code_verifier: verifier,
|
|
12673
|
+
redirect_uri: redirectUri,
|
|
12674
|
+
resource: RESOURCE
|
|
12675
|
+
}),
|
|
12676
|
+
signal,
|
|
12677
|
+
redirect: 'error'
|
|
12678
|
+
});
|
|
12679
|
+
if (!result.ok) throw new Error(`OpenAI token exchange failed (HTTP ${result.status}). Run midscene model siwc login again.`);
|
|
12680
|
+
const tokens = await result.json();
|
|
12681
|
+
if (!tokens || 'string' != typeof tokens.access_token || !tokens.access_token || 'string' != typeof tokens.refresh_token || !tokens.refresh_token || 'string' != typeof tokens.id_token || 'string' != typeof tokens.scope || 'number' != typeof tokens.expires_in || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0 || 'string' != typeof tokens.token_type || 'bearer' !== tokens.token_type.toLowerCase()) throw new Error('OpenAI returned an invalid token response.');
|
|
12682
|
+
const scopes = tokens.scope.split(/\s+/);
|
|
12683
|
+
if (!scopes.includes('chatgpt.tokens.use.direct')) throw new Error('ChatGPT plan usage was not authorized. Run midscene model siwc login again and grant access.');
|
|
12684
|
+
const keys = dependencies?.keys ?? createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));
|
|
12685
|
+
const { payload } = await jwtVerify(tokens.id_token, keys, {
|
|
12686
|
+
issuer: ISSUER,
|
|
12687
|
+
audience: clientId,
|
|
12688
|
+
algorithms: [
|
|
12689
|
+
'RS256'
|
|
12690
|
+
],
|
|
12691
|
+
requiredClaims: [
|
|
12692
|
+
'sub',
|
|
12693
|
+
'exp',
|
|
12694
|
+
'iat',
|
|
12695
|
+
'nonce'
|
|
12696
|
+
]
|
|
12697
|
+
});
|
|
12698
|
+
if (payload.nonce !== nonce || !payload.sub) throw new Error('OpenAI login identity or nonce did not match.');
|
|
12699
|
+
signal.throwIfAborted();
|
|
12700
|
+
return {
|
|
12701
|
+
ext_agent_host_id: hostId,
|
|
12702
|
+
client_id: clientId,
|
|
12703
|
+
subject: payload.sub,
|
|
12704
|
+
issuer: ISSUER,
|
|
12705
|
+
...'string' == typeof payload.email ? {
|
|
12706
|
+
email: payload.email
|
|
12707
|
+
} : {},
|
|
12708
|
+
access_token: tokens.access_token,
|
|
12709
|
+
refresh_token: tokens.refresh_token,
|
|
12710
|
+
id_token: tokens.id_token,
|
|
12711
|
+
scopes,
|
|
12712
|
+
expires_at: Date.now() + 1000 * tokens.expires_in
|
|
12713
|
+
};
|
|
12714
|
+
} finally{
|
|
12715
|
+
signal.removeEventListener('abort', abort);
|
|
12716
|
+
server.closeAllConnections();
|
|
12717
|
+
await new Promise((resolve)=>server.close(()=>resolve()));
|
|
12718
|
+
}
|
|
12719
|
+
}
|
|
12720
|
+
async function refreshOpenAICredentials(saved, signal, dependencies) {
|
|
12721
|
+
const issuer = 'https://auth.openai.com';
|
|
12722
|
+
signal.throwIfAborted();
|
|
12723
|
+
const response = await (dependencies?.fetch ?? fetch)(`${issuer}/api/accounts/oauth/token`, {
|
|
12724
|
+
method: 'POST',
|
|
12725
|
+
headers: {
|
|
12726
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
12727
|
+
},
|
|
12728
|
+
body: new URLSearchParams({
|
|
12729
|
+
grant_type: 'refresh_token',
|
|
12730
|
+
client_id: saved.client_id,
|
|
12731
|
+
refresh_token: saved.refresh_token,
|
|
12732
|
+
resource: 'https://api.openai.com/v1'
|
|
12733
|
+
}),
|
|
12734
|
+
signal,
|
|
12735
|
+
redirect: 'error'
|
|
12736
|
+
});
|
|
12737
|
+
if (!response.ok) throw new Error('OpenAI refresh failed.');
|
|
12738
|
+
const tokens = await response.json();
|
|
12739
|
+
if (!tokens || 'string' != typeof tokens.access_token || !tokens.access_token || 'string' != typeof tokens.refresh_token || !tokens.refresh_token || 'number' != typeof tokens.expires_in || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0 || 'string' != typeof tokens.token_type || 'bearer' !== tokens.token_type.toLowerCase() || 'string' != typeof tokens.scope) throw new Error('OpenAI returned an invalid refresh response.');
|
|
12740
|
+
const scopes = tokens.scope.split(/\s+/);
|
|
12741
|
+
if (!scopes.includes('chatgpt.tokens.use.direct')) throw new Error('ChatGPT plan usage is no longer authorized.');
|
|
12742
|
+
if (void 0 !== tokens.id_token) {
|
|
12743
|
+
if ('string' != typeof tokens.id_token || !tokens.id_token) throw new Error('Invalid refreshed ID token.');
|
|
12744
|
+
const keys = dependencies?.keys ?? createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
|
|
12745
|
+
const { payload } = await jwtVerify(tokens.id_token, keys, {
|
|
12746
|
+
issuer,
|
|
12747
|
+
audience: saved.client_id,
|
|
12748
|
+
algorithms: [
|
|
12749
|
+
'RS256'
|
|
12750
|
+
],
|
|
12751
|
+
requiredClaims: [
|
|
12752
|
+
'sub',
|
|
12753
|
+
'exp',
|
|
12754
|
+
'iat'
|
|
12755
|
+
]
|
|
12756
|
+
});
|
|
12757
|
+
if (payload.sub !== saved.subject) throw new Error('Refreshed identity does not match the saved account.');
|
|
12758
|
+
}
|
|
12759
|
+
signal.throwIfAborted();
|
|
12760
|
+
return {
|
|
12761
|
+
...saved,
|
|
12762
|
+
access_token: tokens.access_token,
|
|
12763
|
+
refresh_token: tokens.refresh_token,
|
|
12764
|
+
id_token: tokens.id_token ?? saved.id_token,
|
|
12765
|
+
scopes,
|
|
12766
|
+
expires_at: Date.now() + 1000 * tokens.expires_in
|
|
12767
|
+
};
|
|
12768
|
+
}
|
|
12769
|
+
const USAGE = `Usage:
|
|
12770
|
+
midscene model siwc login [--no-open] [--port <port>]
|
|
12771
|
+
midscene model siwc refresh
|
|
12772
|
+
|
|
12773
|
+
Sign in with ChatGPT or refresh the saved credentials.
|
|
12774
|
+
Full credentials are saved to ~/.midscene/siwc.json. Only the access token is printed to stdout.
|
|
12775
|
+
Midscene does not collect your credentials.
|
|
12776
|
+
|
|
12777
|
+
Options (login):
|
|
12778
|
+
--no-open Print the authorization URL without opening a browser
|
|
12779
|
+
--port <port> Loopback callback port (default: an available port)
|
|
12780
|
+
--help, -h Show this help
|
|
12781
|
+
`;
|
|
12782
|
+
async function openBrowser(url) {
|
|
12783
|
+
const command = 'darwin' === process.platform ? 'open' : 'win32' === process.platform ? 'rundll32' : 'xdg-open';
|
|
12784
|
+
const args = 'win32' === process.platform ? [
|
|
12785
|
+
'url.dll,FileProtocolHandler',
|
|
12786
|
+
url
|
|
12787
|
+
] : [
|
|
12788
|
+
url
|
|
12789
|
+
];
|
|
12790
|
+
await new Promise((resolve, reject)=>{
|
|
12791
|
+
const child = spawn(command, args, {
|
|
12792
|
+
stdio: 'ignore',
|
|
12793
|
+
shell: false
|
|
12794
|
+
});
|
|
12795
|
+
child.once('error', reject);
|
|
12796
|
+
child.once('spawn', ()=>{
|
|
12797
|
+
child.unref();
|
|
12798
|
+
resolve();
|
|
12799
|
+
});
|
|
12800
|
+
});
|
|
12801
|
+
}
|
|
12802
|
+
async function runSiwcCommand(args, io = {
|
|
12803
|
+
stdout: console.log,
|
|
12804
|
+
stderr: console.error
|
|
12805
|
+
}, credentialsPath = defaultCredentialsPath()) {
|
|
12806
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
12807
|
+
io.stdout(USAGE);
|
|
12808
|
+
return 0;
|
|
12809
|
+
}
|
|
12810
|
+
const [action, ...options] = args;
|
|
12811
|
+
if ('login' !== action && 'refresh' !== action) {
|
|
12812
|
+
io.stderr(`Unknown SIWC command: ${action}\n${USAGE}`);
|
|
12813
|
+
return 1;
|
|
12814
|
+
}
|
|
12815
|
+
const portOptions = {
|
|
12816
|
+
port: 0,
|
|
12817
|
+
noOpen: false
|
|
12818
|
+
};
|
|
12819
|
+
const seen = new Set();
|
|
12820
|
+
for(let index = 0; index < options.length; index++){
|
|
12821
|
+
const option = options[index];
|
|
12822
|
+
if ('login' !== action || seen.has(option) || ![
|
|
12823
|
+
'--port',
|
|
12824
|
+
'--no-open'
|
|
12825
|
+
].includes(option)) {
|
|
12826
|
+
io.stderr(`Invalid midscene model siwc arguments.\n${USAGE}`);
|
|
12827
|
+
return 1;
|
|
12828
|
+
}
|
|
12829
|
+
seen.add(option);
|
|
12830
|
+
if ('--no-open' === option) portOptions.noOpen = true;
|
|
12831
|
+
else {
|
|
12832
|
+
const value = options[++index];
|
|
12833
|
+
if (!value || !/^\d+$/.test(value) || Number(value) > 65535) {
|
|
12834
|
+
io.stderr('--port must be an integer between 0 and 65535.');
|
|
12835
|
+
return 1;
|
|
12836
|
+
}
|
|
12837
|
+
portOptions.port = Number(value);
|
|
12838
|
+
}
|
|
12839
|
+
}
|
|
12840
|
+
const unlock = await lockCredentials(credentialsPath).catch((error)=>{
|
|
12841
|
+
io.stderr(error.message);
|
|
12842
|
+
});
|
|
12843
|
+
if (!unlock) return 1;
|
|
12844
|
+
const controller = new AbortController();
|
|
12845
|
+
const cancel = ()=>controller.abort();
|
|
12846
|
+
const timer = setTimeout(cancel, 300000);
|
|
12847
|
+
process.once('SIGINT', cancel);
|
|
12848
|
+
process.once('SIGTERM', cancel);
|
|
12849
|
+
try {
|
|
12850
|
+
io.stderr(source_default().cyan.bold(`🔄 Starting OpenAI SIWC ${action}...`));
|
|
12851
|
+
const saved = await readRegistration(credentialsPath);
|
|
12852
|
+
const credentials = await (async ()=>{
|
|
12853
|
+
if ('refresh' === action) return refreshOpenAICredentials(requireCredentials(saved), controller.signal);
|
|
12854
|
+
const registration = saved ?? {
|
|
12855
|
+
ext_agent_host_id: `urn:uuid:${randomUUID()}`
|
|
12856
|
+
};
|
|
12857
|
+
if (!saved) await saveRegistration(credentialsPath, registration);
|
|
12858
|
+
const result = await loginWithOpenAI({
|
|
12859
|
+
port: portOptions.port,
|
|
12860
|
+
hostId: registration.ext_agent_host_id,
|
|
12861
|
+
clientId: registration.client_id,
|
|
12862
|
+
signal: controller.signal,
|
|
12863
|
+
onAuthorization: async (url)=>{
|
|
12864
|
+
io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
|
|
12865
|
+
if (!portOptions.noOpen) try {
|
|
12866
|
+
await openBrowser(url);
|
|
12867
|
+
} catch {
|
|
12868
|
+
io.stderr('Open the URL above in your browser to continue.');
|
|
12869
|
+
}
|
|
12870
|
+
}
|
|
12871
|
+
});
|
|
12872
|
+
if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
|
|
12873
|
+
return result;
|
|
12874
|
+
})();
|
|
12875
|
+
await saveRegistration(credentialsPath, credentials);
|
|
12876
|
+
io.stderr('\n🔑 your Access token:');
|
|
12877
|
+
io.stdout(credentials.access_token);
|
|
12878
|
+
const expiresAt = new Intl.DateTimeFormat('en-GB', {
|
|
12879
|
+
dateStyle: 'medium',
|
|
12880
|
+
timeStyle: 'long'
|
|
12881
|
+
}).format(credentials.expires_at);
|
|
12882
|
+
const remainingMinutes = Math.ceil((credentials.expires_at - Date.now()) / 60000);
|
|
12883
|
+
const expiresIn = new Intl.RelativeTimeFormat('en', {
|
|
12884
|
+
numeric: 'always'
|
|
12885
|
+
}).format(60 === remainingMinutes ? 1 : remainingMinutes, 60 === remainingMinutes ? 'hour' : 'minute');
|
|
12886
|
+
io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
|
|
12887
|
+
io.stderr(`\n${source_default().green(`✅ SIWC ${action} succeeded.`)}\n\nYou can use this access token as your model API Key in Midscene by setting MIDSCENE_MODEL_API_KEY.\n\nFull credentials are saved locally to:\n${source_default().cyan(credentialsPath)}\n\n${source_default().gray('Midscene does not collect your credentials.')}`);
|
|
12888
|
+
return 0;
|
|
12889
|
+
} catch {
|
|
12890
|
+
io.stderr(source_default().red(controller.signal.aborted ? `OpenAI ${action} cancelled or timed out.` : `OpenAI ${action} failed. Check the local credentials file permissions or run midscene model siwc login to authorize again.`));
|
|
12891
|
+
return 1;
|
|
12892
|
+
} finally{
|
|
12893
|
+
clearTimeout(timer);
|
|
12894
|
+
process.removeListener('SIGINT', cancel);
|
|
12895
|
+
process.removeListener('SIGTERM', cancel);
|
|
12896
|
+
await unlock();
|
|
12897
|
+
}
|
|
12898
|
+
}
|
|
12486
12899
|
const model_command_runModelCommand = (rawArgs, deps, io = {
|
|
12487
12900
|
stdout: console.log,
|
|
12488
12901
|
stderr: console.error
|
|
12489
|
-
})=>
|
|
12902
|
+
})=>{
|
|
12903
|
+
const [, action, ...restArgs] = rawArgs;
|
|
12904
|
+
if ('siwc' === action) return runSiwcCommand(restArgs, io);
|
|
12905
|
+
const withSiwcUsage = (message)=>message.replace(' midscene model verify\n', ' midscene model verify\n midscene model siwc <login|refresh> [options]\n');
|
|
12906
|
+
return runModelCommand(rawArgs, {
|
|
12490
12907
|
loadDotenv: ()=>loadDotenvConfig({
|
|
12491
12908
|
dotenvDebug: true,
|
|
12492
12909
|
dotenvOverride: true,
|
|
12493
12910
|
log: io.stdout
|
|
12494
12911
|
}),
|
|
12495
12912
|
...deps
|
|
12496
|
-
},
|
|
12913
|
+
}, {
|
|
12914
|
+
stdout: (message)=>io.stdout(withSiwcUsage(message)),
|
|
12915
|
+
stderr: (message)=>io.stderr(withSiwcUsage(message))
|
|
12916
|
+
}, 'midscene');
|
|
12917
|
+
};
|
|
12497
12918
|
const probeWorkflowFormat = (file)=>{
|
|
12498
12919
|
try {
|
|
12499
12920
|
const content = (0, __rspack_external_node_fs_5ea92f0c.readFileSync)(file, 'utf8').replace(/\$\{[^}\r\n]*\}/g, 'MIDSCENE_ENV');
|