@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/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
- const external_node_crypto_namespaceObject = require("node:crypto");
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, external_node_crypto_namespaceObject.createHash)('sha1').update((0, external_node_path_.resolve)(file)).digest('hex').slice(0, 8);
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 = JSON.parse('{"rE":"1.14.1-beta-20261008033312.0"}');
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.14.1-beta-20261008033312.0").help().epilogue(`For complete list of configuration options, please visit:
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
- })=>(0, model_command_namespaceObject.runModelCommand)(rawArgs, {
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
- }, io, 'midscene');
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");