@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/lib/index.js
CHANGED
|
@@ -2788,7 +2788,7 @@ var __webpack_modules__ = {
|
|
|
2788
2788
|
});
|
|
2789
2789
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
2790
2790
|
var external_node_path_ = __webpack_require__("node:path");
|
|
2791
|
-
|
|
2791
|
+
var external_node_crypto_ = __webpack_require__("node:crypto");
|
|
2792
2792
|
var core_ = __webpack_require__("@midscene/core");
|
|
2793
2793
|
const common_namespaceObject = require("@midscene/shared/common");
|
|
2794
2794
|
var logger_ = __webpack_require__("@midscene/shared/logger");
|
|
@@ -2945,7 +2945,7 @@ var __webpack_modules__ = {
|
|
|
2945
2945
|
const safeReportNamePart = (value)=>value.replace(/[:*?"<>|# ]/g, '-');
|
|
2946
2946
|
const createRetryReportName = (file)=>{
|
|
2947
2947
|
const fileName = (0, external_node_path_.basename)(file, (0, external_node_path_.extname)(file)) || 'yaml';
|
|
2948
|
-
const fileHash = (0,
|
|
2948
|
+
const fileHash = (0, external_node_crypto_.createHash)('sha1').update((0, external_node_path_.resolve)(file)).digest('hex').slice(0, 8);
|
|
2949
2949
|
return `${safeReportNamePart(fileName)}-${fileHash}-retry-attempts`;
|
|
2950
2950
|
};
|
|
2951
2951
|
const createRetryAttemptReport = (result)=>{
|
|
@@ -4587,7 +4587,9 @@ defineYamlBatchTest(test, testOptions);
|
|
|
4587
4587
|
"use strict";
|
|
4588
4588
|
var core_ = __webpack_require__("@midscene/core");
|
|
4589
4589
|
const cli_namespaceObject = require("@midscene/shared/cli");
|
|
4590
|
-
var package_namespaceObject =
|
|
4590
|
+
var package_namespaceObject = {
|
|
4591
|
+
rE: "1.15.0"
|
|
4592
|
+
};
|
|
4591
4593
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
4592
4594
|
var external_node_fs_namespaceObject = /*#__PURE__*/ __webpack_require__.t(external_node_fs_, 2);
|
|
4593
4595
|
var external_node_path_ = __webpack_require__("node:path");
|
|
@@ -10209,6 +10211,7 @@ Homepage: https://midscenejs.com
|
|
|
10209
10211
|
Github: https://github.com/web-infra-dev/midscene
|
|
10210
10212
|
|
|
10211
10213
|
Usage:
|
|
10214
|
+
$0 model siwc <login|refresh> [options]
|
|
10212
10215
|
$0 [options] <path-to-yaml-script-file-or-directory>
|
|
10213
10216
|
$0 [options] --files <yaml-file1> <yaml-file2 ...>
|
|
10214
10217
|
$0 [options] --config <path-to-config-yaml-file>
|
|
@@ -10270,7 +10273,7 @@ Usage:
|
|
|
10270
10273
|
type: 'boolean',
|
|
10271
10274
|
description: 'Whether to dismiss the HarmonyOS keyboard after text input'
|
|
10272
10275
|
}
|
|
10273
|
-
}).version('version', 'Show version number', "1.
|
|
10276
|
+
}).version('version', 'Show version number', "1.15.0").help().epilogue(`For complete list of configuration options, please visit:
|
|
10274
10277
|
• Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
|
|
10275
10278
|
• Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
|
|
10276
10279
|
• HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
|
|
@@ -12522,17 +12525,438 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
|
|
|
12522
12525
|
}
|
|
12523
12526
|
const model_command_namespaceObject = require("@midscene/test/internal/model-command");
|
|
12524
12527
|
var framework = __webpack_require__("./src/framework/index.ts");
|
|
12528
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
12529
|
+
var external_node_crypto_ = __webpack_require__("node:crypto");
|
|
12530
|
+
var chalk_source = __webpack_require__("../../node_modules/.pnpm/chalk@4.1.2/node_modules/chalk/source/index.js");
|
|
12531
|
+
var source_default = /*#__PURE__*/ __webpack_require__.n(chalk_source);
|
|
12532
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
12533
|
+
const defaultCredentialsPath = ()=>(0, external_node_path_.join)((0, external_node_os_namespaceObject.homedir)(), '.midscene', 'siwc.json');
|
|
12534
|
+
async function readCredentialsFile(path) {
|
|
12535
|
+
const content = await (0, promises_namespaceObject.readFile)(path, 'utf8').catch((error)=>{
|
|
12536
|
+
if ('ENOENT' === error.code) return;
|
|
12537
|
+
throw new Error('Unable to read the local SIWC credentials file.');
|
|
12538
|
+
});
|
|
12539
|
+
if (void 0 === content) return;
|
|
12540
|
+
try {
|
|
12541
|
+
const value = JSON.parse(content);
|
|
12542
|
+
if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new Error('Invalid SIWC credentials record.');
|
|
12543
|
+
return value;
|
|
12544
|
+
} catch {
|
|
12545
|
+
throw new Error('Invalid local SIWC credentials file.');
|
|
12546
|
+
}
|
|
12547
|
+
}
|
|
12548
|
+
async function readRegistration(path) {
|
|
12549
|
+
return (await readCredentialsFile(path))?.credentials;
|
|
12550
|
+
}
|
|
12551
|
+
async function saveRegistration(path, value) {
|
|
12552
|
+
const saved = await readCredentialsFile(path);
|
|
12553
|
+
const now = new Date().toISOString();
|
|
12554
|
+
const record = {
|
|
12555
|
+
...saved,
|
|
12556
|
+
created_at: saved?.created_at ?? now,
|
|
12557
|
+
updated_at: now,
|
|
12558
|
+
credentials: value
|
|
12559
|
+
};
|
|
12560
|
+
const temporary = `${path}.${(0, external_node_crypto_.randomUUID)()}.tmp`;
|
|
12561
|
+
try {
|
|
12562
|
+
await (0, promises_namespaceObject.writeFile)(temporary, `${JSON.stringify(record, null, 2)}\n`, {
|
|
12563
|
+
mode: 384,
|
|
12564
|
+
flag: 'wx'
|
|
12565
|
+
});
|
|
12566
|
+
await (0, promises_namespaceObject.rename)(temporary, path);
|
|
12567
|
+
} finally{
|
|
12568
|
+
await (0, promises_namespaceObject.rm)(temporary, {
|
|
12569
|
+
force: true
|
|
12570
|
+
});
|
|
12571
|
+
}
|
|
12572
|
+
}
|
|
12573
|
+
async function lockCredentials(path) {
|
|
12574
|
+
await (0, promises_namespaceObject.mkdir)((0, external_node_path_.dirname)(path), {
|
|
12575
|
+
recursive: true,
|
|
12576
|
+
mode: 448
|
|
12577
|
+
});
|
|
12578
|
+
const lockPath = `${path}.lock`;
|
|
12579
|
+
try {
|
|
12580
|
+
await (0, promises_namespaceObject.mkdir)(lockPath, {
|
|
12581
|
+
mode: 448
|
|
12582
|
+
});
|
|
12583
|
+
} catch (error) {
|
|
12584
|
+
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.`);
|
|
12585
|
+
throw new Error('Unable to lock the local SIWC credentials file.');
|
|
12586
|
+
}
|
|
12587
|
+
return ()=>(0, promises_namespaceObject.rm)(lockPath, {
|
|
12588
|
+
recursive: true,
|
|
12589
|
+
force: true
|
|
12590
|
+
});
|
|
12591
|
+
}
|
|
12592
|
+
function requireCredentials(value) {
|
|
12593
|
+
if (!value || [
|
|
12594
|
+
'client_id',
|
|
12595
|
+
'subject',
|
|
12596
|
+
'issuer',
|
|
12597
|
+
'access_token',
|
|
12598
|
+
'refresh_token',
|
|
12599
|
+
'id_token'
|
|
12600
|
+
].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.');
|
|
12601
|
+
return value;
|
|
12602
|
+
}
|
|
12603
|
+
const external_node_http_namespaceObject = require("node:http");
|
|
12604
|
+
const external_jose_namespaceObject = require("jose");
|
|
12605
|
+
const ISSUER = 'https://auth.openai.com';
|
|
12606
|
+
const RESOURCE = 'https://api.openai.com/v1';
|
|
12607
|
+
const SCOPES = 'openid profile email offline_access resource.invoke chatgpt.tokens.use.direct';
|
|
12608
|
+
async function loginWithOpenAI(options, dependencies) {
|
|
12609
|
+
const hostId = options.hostId ?? `urn:uuid:${(0, external_node_crypto_.randomUUID)()}`;
|
|
12610
|
+
const existingClientId = options.clientId;
|
|
12611
|
+
const state = (0, external_node_crypto_.randomBytes)(32).toString('base64url');
|
|
12612
|
+
const nonce = (0, external_node_crypto_.randomBytes)(32).toString('base64url');
|
|
12613
|
+
const verifier = (0, external_node_crypto_.randomBytes)(32).toString('base64url');
|
|
12614
|
+
const signal = options.signal;
|
|
12615
|
+
signal.throwIfAborted();
|
|
12616
|
+
const server = (0, external_node_http_namespaceObject.createServer)();
|
|
12617
|
+
const callback = new Promise((resolve, reject)=>{
|
|
12618
|
+
const consumed = {
|
|
12619
|
+
value: false
|
|
12620
|
+
};
|
|
12621
|
+
server.on('request', (request, response)=>{
|
|
12622
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
12623
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
12624
|
+
response.setHeader('Referrer-Policy', 'no-referrer');
|
|
12625
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
12626
|
+
if ('GET' !== request.method || '/auth/callback' !== url.pathname) return void response.writeHead(404).end('Not found');
|
|
12627
|
+
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.');
|
|
12628
|
+
if (consumed.value) return void response.writeHead(409).end('Authorization already received.');
|
|
12629
|
+
if (url.searchParams.has('error')) {
|
|
12630
|
+
consumed.value = true;
|
|
12631
|
+
response.writeHead(400).end('Authorization was not granted. Return to the terminal.');
|
|
12632
|
+
reject(new Error('OpenAI authorization was denied or failed.'));
|
|
12633
|
+
return;
|
|
12634
|
+
}
|
|
12635
|
+
const code = url.searchParams.get('code');
|
|
12636
|
+
const clientId = url.searchParams.get('client_id') || existingClientId;
|
|
12637
|
+
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.');
|
|
12638
|
+
consumed.value = true;
|
|
12639
|
+
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
12640
|
+
response.end(`<!doctype html>
|
|
12641
|
+
<html lang="en">
|
|
12642
|
+
<head>
|
|
12643
|
+
<meta charset="utf-8">
|
|
12644
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12645
|
+
<title>Authorization received · Midscene</title>
|
|
12646
|
+
<style>
|
|
12647
|
+
* { box-sizing: border-box; }
|
|
12648
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #f6f7f9; color: #202124; font-family: system-ui, sans-serif; }
|
|
12649
|
+
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; }
|
|
12650
|
+
h1 { margin: 0 0 20px; font-size: 24px; line-height: 1.4; }
|
|
12651
|
+
h1 span { color: #16834a; }
|
|
12652
|
+
p { margin: 8px 0 0; color: #596273; font-size: 15px; line-height: 1.6; }
|
|
12653
|
+
</style>
|
|
12654
|
+
</head>
|
|
12655
|
+
<body>
|
|
12656
|
+
<main>
|
|
12657
|
+
<h1><span aria-hidden="true">✓</span> Authorization received</h1>
|
|
12658
|
+
<p>You can close this tab.</p>
|
|
12659
|
+
<p>Check your terminal for the sign-in result and access token.</p>
|
|
12660
|
+
</main>
|
|
12661
|
+
</body>
|
|
12662
|
+
</html>`);
|
|
12663
|
+
resolve({
|
|
12664
|
+
code,
|
|
12665
|
+
clientId
|
|
12666
|
+
});
|
|
12667
|
+
});
|
|
12668
|
+
server.on('error', reject);
|
|
12669
|
+
});
|
|
12670
|
+
callback.catch(()=>void 0);
|
|
12671
|
+
const abort = ()=>server.emit('error', new Error('OpenAI login cancelled or timed out.'));
|
|
12672
|
+
signal.addEventListener('abort', abort, {
|
|
12673
|
+
once: true
|
|
12674
|
+
});
|
|
12675
|
+
try {
|
|
12676
|
+
await new Promise((resolve, reject)=>{
|
|
12677
|
+
server.once('error', reject);
|
|
12678
|
+
server.listen(options.port ?? 0, '127.0.0.1', ()=>{
|
|
12679
|
+
server.removeListener('error', reject);
|
|
12680
|
+
resolve();
|
|
12681
|
+
});
|
|
12682
|
+
});
|
|
12683
|
+
signal.throwIfAborted();
|
|
12684
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/auth/callback`;
|
|
12685
|
+
const authorization = new URL(`${ISSUER}/api/accounts/authorize`);
|
|
12686
|
+
authorization.search = new URLSearchParams({
|
|
12687
|
+
client_id: existingClientId ?? 'dynamic_agent_client',
|
|
12688
|
+
...existingClientId ? {} : {
|
|
12689
|
+
agent_name_hint: 'Midscene'
|
|
12690
|
+
},
|
|
12691
|
+
ext_agent_host_id: hostId,
|
|
12692
|
+
response_type: 'code',
|
|
12693
|
+
redirect_uri: redirectUri,
|
|
12694
|
+
scope: SCOPES,
|
|
12695
|
+
resource: RESOURCE,
|
|
12696
|
+
state,
|
|
12697
|
+
nonce,
|
|
12698
|
+
code_challenge_method: 'S256',
|
|
12699
|
+
code_challenge: (0, external_node_crypto_.createHash)('sha256').update(verifier).digest('base64url')
|
|
12700
|
+
}).toString();
|
|
12701
|
+
await options.onAuthorization(authorization.toString());
|
|
12702
|
+
const { code, clientId } = await callback;
|
|
12703
|
+
signal.throwIfAborted();
|
|
12704
|
+
const request = dependencies?.fetch ?? fetch;
|
|
12705
|
+
const result = await request(`${ISSUER}/api/accounts/oauth/token`, {
|
|
12706
|
+
method: 'POST',
|
|
12707
|
+
headers: {
|
|
12708
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
12709
|
+
},
|
|
12710
|
+
body: new URLSearchParams({
|
|
12711
|
+
grant_type: 'authorization_code',
|
|
12712
|
+
client_id: clientId,
|
|
12713
|
+
code,
|
|
12714
|
+
code_verifier: verifier,
|
|
12715
|
+
redirect_uri: redirectUri,
|
|
12716
|
+
resource: RESOURCE
|
|
12717
|
+
}),
|
|
12718
|
+
signal,
|
|
12719
|
+
redirect: 'error'
|
|
12720
|
+
});
|
|
12721
|
+
if (!result.ok) throw new Error(`OpenAI token exchange failed (HTTP ${result.status}). Run midscene model siwc login again.`);
|
|
12722
|
+
const tokens = await result.json();
|
|
12723
|
+
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.');
|
|
12724
|
+
const scopes = tokens.scope.split(/\s+/);
|
|
12725
|
+
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.');
|
|
12726
|
+
const keys = dependencies?.keys ?? (0, external_jose_namespaceObject.createRemoteJWKSet)(new URL(`${ISSUER}/.well-known/jwks.json`));
|
|
12727
|
+
const { payload } = await (0, external_jose_namespaceObject.jwtVerify)(tokens.id_token, keys, {
|
|
12728
|
+
issuer: ISSUER,
|
|
12729
|
+
audience: clientId,
|
|
12730
|
+
algorithms: [
|
|
12731
|
+
'RS256'
|
|
12732
|
+
],
|
|
12733
|
+
requiredClaims: [
|
|
12734
|
+
'sub',
|
|
12735
|
+
'exp',
|
|
12736
|
+
'iat',
|
|
12737
|
+
'nonce'
|
|
12738
|
+
]
|
|
12739
|
+
});
|
|
12740
|
+
if (payload.nonce !== nonce || !payload.sub) throw new Error('OpenAI login identity or nonce did not match.');
|
|
12741
|
+
signal.throwIfAborted();
|
|
12742
|
+
return {
|
|
12743
|
+
ext_agent_host_id: hostId,
|
|
12744
|
+
client_id: clientId,
|
|
12745
|
+
subject: payload.sub,
|
|
12746
|
+
issuer: ISSUER,
|
|
12747
|
+
...'string' == typeof payload.email ? {
|
|
12748
|
+
email: payload.email
|
|
12749
|
+
} : {},
|
|
12750
|
+
access_token: tokens.access_token,
|
|
12751
|
+
refresh_token: tokens.refresh_token,
|
|
12752
|
+
id_token: tokens.id_token,
|
|
12753
|
+
scopes,
|
|
12754
|
+
expires_at: Date.now() + 1000 * tokens.expires_in
|
|
12755
|
+
};
|
|
12756
|
+
} finally{
|
|
12757
|
+
signal.removeEventListener('abort', abort);
|
|
12758
|
+
server.closeAllConnections();
|
|
12759
|
+
await new Promise((resolve)=>server.close(()=>resolve()));
|
|
12760
|
+
}
|
|
12761
|
+
}
|
|
12762
|
+
async function refreshOpenAICredentials(saved, signal, dependencies) {
|
|
12763
|
+
const issuer = 'https://auth.openai.com';
|
|
12764
|
+
signal.throwIfAborted();
|
|
12765
|
+
const response = await (dependencies?.fetch ?? fetch)(`${issuer}/api/accounts/oauth/token`, {
|
|
12766
|
+
method: 'POST',
|
|
12767
|
+
headers: {
|
|
12768
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
12769
|
+
},
|
|
12770
|
+
body: new URLSearchParams({
|
|
12771
|
+
grant_type: 'refresh_token',
|
|
12772
|
+
client_id: saved.client_id,
|
|
12773
|
+
refresh_token: saved.refresh_token,
|
|
12774
|
+
resource: 'https://api.openai.com/v1'
|
|
12775
|
+
}),
|
|
12776
|
+
signal,
|
|
12777
|
+
redirect: 'error'
|
|
12778
|
+
});
|
|
12779
|
+
if (!response.ok) throw new Error('OpenAI refresh failed.');
|
|
12780
|
+
const tokens = await response.json();
|
|
12781
|
+
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.');
|
|
12782
|
+
const scopes = tokens.scope.split(/\s+/);
|
|
12783
|
+
if (!scopes.includes('chatgpt.tokens.use.direct')) throw new Error('ChatGPT plan usage is no longer authorized.');
|
|
12784
|
+
if (void 0 !== tokens.id_token) {
|
|
12785
|
+
if ('string' != typeof tokens.id_token || !tokens.id_token) throw new Error('Invalid refreshed ID token.');
|
|
12786
|
+
const keys = dependencies?.keys ?? (0, external_jose_namespaceObject.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`));
|
|
12787
|
+
const { payload } = await (0, external_jose_namespaceObject.jwtVerify)(tokens.id_token, keys, {
|
|
12788
|
+
issuer,
|
|
12789
|
+
audience: saved.client_id,
|
|
12790
|
+
algorithms: [
|
|
12791
|
+
'RS256'
|
|
12792
|
+
],
|
|
12793
|
+
requiredClaims: [
|
|
12794
|
+
'sub',
|
|
12795
|
+
'exp',
|
|
12796
|
+
'iat'
|
|
12797
|
+
]
|
|
12798
|
+
});
|
|
12799
|
+
if (payload.sub !== saved.subject) throw new Error('Refreshed identity does not match the saved account.');
|
|
12800
|
+
}
|
|
12801
|
+
signal.throwIfAborted();
|
|
12802
|
+
return {
|
|
12803
|
+
...saved,
|
|
12804
|
+
access_token: tokens.access_token,
|
|
12805
|
+
refresh_token: tokens.refresh_token,
|
|
12806
|
+
id_token: tokens.id_token ?? saved.id_token,
|
|
12807
|
+
scopes,
|
|
12808
|
+
expires_at: Date.now() + 1000 * tokens.expires_in
|
|
12809
|
+
};
|
|
12810
|
+
}
|
|
12811
|
+
const USAGE = `Usage:
|
|
12812
|
+
midscene model siwc login [--no-open] [--port <port>]
|
|
12813
|
+
midscene model siwc refresh
|
|
12814
|
+
|
|
12815
|
+
Sign in with ChatGPT or refresh the saved credentials.
|
|
12816
|
+
Full credentials are saved to ~/.midscene/siwc.json. Only the access token is printed to stdout.
|
|
12817
|
+
Midscene does not collect your credentials.
|
|
12818
|
+
|
|
12819
|
+
Options (login):
|
|
12820
|
+
--no-open Print the authorization URL without opening a browser
|
|
12821
|
+
--port <port> Loopback callback port (default: an available port)
|
|
12822
|
+
--help, -h Show this help
|
|
12823
|
+
`;
|
|
12824
|
+
async function openBrowser(url) {
|
|
12825
|
+
const command = 'darwin' === process.platform ? 'open' : 'win32' === process.platform ? 'rundll32' : 'xdg-open';
|
|
12826
|
+
const args = 'win32' === process.platform ? [
|
|
12827
|
+
'url.dll,FileProtocolHandler',
|
|
12828
|
+
url
|
|
12829
|
+
] : [
|
|
12830
|
+
url
|
|
12831
|
+
];
|
|
12832
|
+
await new Promise((resolve, reject)=>{
|
|
12833
|
+
const child = (0, external_node_child_process_namespaceObject.spawn)(command, args, {
|
|
12834
|
+
stdio: 'ignore',
|
|
12835
|
+
shell: false
|
|
12836
|
+
});
|
|
12837
|
+
child.once('error', reject);
|
|
12838
|
+
child.once('spawn', ()=>{
|
|
12839
|
+
child.unref();
|
|
12840
|
+
resolve();
|
|
12841
|
+
});
|
|
12842
|
+
});
|
|
12843
|
+
}
|
|
12844
|
+
async function runSiwcCommand(args, io = {
|
|
12845
|
+
stdout: console.log,
|
|
12846
|
+
stderr: console.error
|
|
12847
|
+
}, credentialsPath = defaultCredentialsPath()) {
|
|
12848
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
12849
|
+
io.stdout(USAGE);
|
|
12850
|
+
return 0;
|
|
12851
|
+
}
|
|
12852
|
+
const [action, ...options] = args;
|
|
12853
|
+
if ('login' !== action && 'refresh' !== action) {
|
|
12854
|
+
io.stderr(`Unknown SIWC command: ${action}\n${USAGE}`);
|
|
12855
|
+
return 1;
|
|
12856
|
+
}
|
|
12857
|
+
const portOptions = {
|
|
12858
|
+
port: 0,
|
|
12859
|
+
noOpen: false
|
|
12860
|
+
};
|
|
12861
|
+
const seen = new Set();
|
|
12862
|
+
for(let index = 0; index < options.length; index++){
|
|
12863
|
+
const option = options[index];
|
|
12864
|
+
if ('login' !== action || seen.has(option) || ![
|
|
12865
|
+
'--port',
|
|
12866
|
+
'--no-open'
|
|
12867
|
+
].includes(option)) {
|
|
12868
|
+
io.stderr(`Invalid midscene model siwc arguments.\n${USAGE}`);
|
|
12869
|
+
return 1;
|
|
12870
|
+
}
|
|
12871
|
+
seen.add(option);
|
|
12872
|
+
if ('--no-open' === option) portOptions.noOpen = true;
|
|
12873
|
+
else {
|
|
12874
|
+
const value = options[++index];
|
|
12875
|
+
if (!value || !/^\d+$/.test(value) || Number(value) > 65535) {
|
|
12876
|
+
io.stderr('--port must be an integer between 0 and 65535.');
|
|
12877
|
+
return 1;
|
|
12878
|
+
}
|
|
12879
|
+
portOptions.port = Number(value);
|
|
12880
|
+
}
|
|
12881
|
+
}
|
|
12882
|
+
const unlock = await lockCredentials(credentialsPath).catch((error)=>{
|
|
12883
|
+
io.stderr(error.message);
|
|
12884
|
+
});
|
|
12885
|
+
if (!unlock) return 1;
|
|
12886
|
+
const controller = new AbortController();
|
|
12887
|
+
const cancel = ()=>controller.abort();
|
|
12888
|
+
const timer = setTimeout(cancel, 300000);
|
|
12889
|
+
process.once('SIGINT', cancel);
|
|
12890
|
+
process.once('SIGTERM', cancel);
|
|
12891
|
+
try {
|
|
12892
|
+
io.stderr(source_default().cyan.bold(`🔄 Starting OpenAI SIWC ${action}...`));
|
|
12893
|
+
const saved = await readRegistration(credentialsPath);
|
|
12894
|
+
const credentials = await (async ()=>{
|
|
12895
|
+
if ('refresh' === action) return refreshOpenAICredentials(requireCredentials(saved), controller.signal);
|
|
12896
|
+
const registration = saved ?? {
|
|
12897
|
+
ext_agent_host_id: `urn:uuid:${(0, external_node_crypto_.randomUUID)()}`
|
|
12898
|
+
};
|
|
12899
|
+
if (!saved) await saveRegistration(credentialsPath, registration);
|
|
12900
|
+
const result = await loginWithOpenAI({
|
|
12901
|
+
port: portOptions.port,
|
|
12902
|
+
hostId: registration.ext_agent_host_id,
|
|
12903
|
+
clientId: registration.client_id,
|
|
12904
|
+
signal: controller.signal,
|
|
12905
|
+
onAuthorization: async (url)=>{
|
|
12906
|
+
io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
|
|
12907
|
+
if (!portOptions.noOpen) try {
|
|
12908
|
+
await openBrowser(url);
|
|
12909
|
+
} catch {
|
|
12910
|
+
io.stderr('Open the URL above in your browser to continue.');
|
|
12911
|
+
}
|
|
12912
|
+
}
|
|
12913
|
+
});
|
|
12914
|
+
if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
|
|
12915
|
+
return result;
|
|
12916
|
+
})();
|
|
12917
|
+
await saveRegistration(credentialsPath, credentials);
|
|
12918
|
+
io.stderr('\n🔑 your Access token:');
|
|
12919
|
+
io.stdout(credentials.access_token);
|
|
12920
|
+
const expiresAt = new Intl.DateTimeFormat('en-GB', {
|
|
12921
|
+
dateStyle: 'medium',
|
|
12922
|
+
timeStyle: 'long'
|
|
12923
|
+
}).format(credentials.expires_at);
|
|
12924
|
+
const remainingMinutes = Math.ceil((credentials.expires_at - Date.now()) / 60000);
|
|
12925
|
+
const expiresIn = new Intl.RelativeTimeFormat('en', {
|
|
12926
|
+
numeric: 'always'
|
|
12927
|
+
}).format(60 === remainingMinutes ? 1 : remainingMinutes, 60 === remainingMinutes ? 'hour' : 'minute');
|
|
12928
|
+
io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
|
|
12929
|
+
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.')}`);
|
|
12930
|
+
return 0;
|
|
12931
|
+
} catch {
|
|
12932
|
+
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.`));
|
|
12933
|
+
return 1;
|
|
12934
|
+
} finally{
|
|
12935
|
+
clearTimeout(timer);
|
|
12936
|
+
process.removeListener('SIGINT', cancel);
|
|
12937
|
+
process.removeListener('SIGTERM', cancel);
|
|
12938
|
+
await unlock();
|
|
12939
|
+
}
|
|
12940
|
+
}
|
|
12525
12941
|
const runModelCommand = (rawArgs, deps, io = {
|
|
12526
12942
|
stdout: console.log,
|
|
12527
12943
|
stderr: console.error
|
|
12528
|
-
})=>
|
|
12944
|
+
})=>{
|
|
12945
|
+
const [, action, ...restArgs] = rawArgs;
|
|
12946
|
+
if ('siwc' === action) return runSiwcCommand(restArgs, io);
|
|
12947
|
+
const withSiwcUsage = (message)=>message.replace(' midscene model verify\n', ' midscene model verify\n midscene model siwc <login|refresh> [options]\n');
|
|
12948
|
+
return (0, model_command_namespaceObject.runModelCommand)(rawArgs, {
|
|
12529
12949
|
loadDotenv: ()=>(0, model_command_namespaceObject.loadDotenvConfig)({
|
|
12530
12950
|
dotenvDebug: true,
|
|
12531
12951
|
dotenvOverride: true,
|
|
12532
12952
|
log: io.stdout
|
|
12533
12953
|
}),
|
|
12534
12954
|
...deps
|
|
12535
|
-
},
|
|
12955
|
+
}, {
|
|
12956
|
+
stdout: (message)=>io.stdout(withSiwcUsage(message)),
|
|
12957
|
+
stderr: (message)=>io.stderr(withSiwcUsage(message))
|
|
12958
|
+
}, 'midscene');
|
|
12959
|
+
};
|
|
12536
12960
|
const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
|
|
12537
12961
|
const probeWorkflowFormat = (file)=>{
|
|
12538
12962
|
try {
|
|
@@ -12679,6 +13103,10 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
|
|
|
12679
13103
|
"use strict";
|
|
12680
13104
|
module.exports = require("lodash.merge");
|
|
12681
13105
|
},
|
|
13106
|
+
"node:crypto" (module) {
|
|
13107
|
+
"use strict";
|
|
13108
|
+
module.exports = require("node:crypto");
|
|
13109
|
+
},
|
|
12682
13110
|
"node:fs" (module) {
|
|
12683
13111
|
"use strict";
|
|
12684
13112
|
module.exports = require("node:fs");
|