mmt-testlight 1.42.2-pre → 1.42.3-pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,8 @@ npm run build
24
24
 
25
25
  ## Bundle standalone binaries (pkg)
26
26
 
27
+ Uses `@yao-pkg/pkg` in SEA mode (Node 22). Requires Node ≥ 22 on the machine that builds binaries.
28
+
27
29
  ```
28
30
  npm run pkg
29
31
  ```
package/dist/cli.js CHANGED
@@ -36328,118 +36328,138 @@ var require_browser = __commonJS({
36328
36328
  }
36329
36329
  });
36330
36330
 
36331
- // node_modules/has-flag/index.js
36332
- var require_has_flag = __commonJS({
36333
- "node_modules/has-flag/index.js"(exports2, module2) {
36334
- "use strict";
36335
- module2.exports = (flag, argv = process.argv) => {
36336
- const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
36337
- const position = argv.indexOf(prefix + flag);
36338
- const terminatorPosition = argv.indexOf("--");
36339
- return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
36340
- };
36341
- }
36331
+ // ../node_modules/supports-color/index.js
36332
+ var supports_color_exports = {};
36333
+ __export(supports_color_exports, {
36334
+ createSupportsColor: () => createSupportsColor,
36335
+ default: () => supports_color_default
36342
36336
  });
36343
-
36344
- // node_modules/supports-color/index.js
36345
- var require_supports_color = __commonJS({
36346
- "node_modules/supports-color/index.js"(exports2, module2) {
36347
- "use strict";
36348
- var os3 = require("os");
36349
- var tty2 = require("tty");
36350
- var hasFlag2 = require_has_flag();
36351
- var { env: env2 } = process;
36352
- var forceColor;
36353
- if (hasFlag2("no-color") || hasFlag2("no-colors") || hasFlag2("color=false") || hasFlag2("color=never")) {
36354
- forceColor = 0;
36355
- } else if (hasFlag2("color") || hasFlag2("colors") || hasFlag2("color=true") || hasFlag2("color=always")) {
36356
- forceColor = 1;
36357
- }
36358
- if ("FORCE_COLOR" in env2) {
36359
- if (env2.FORCE_COLOR === "true") {
36360
- forceColor = 1;
36361
- } else if (env2.FORCE_COLOR === "false") {
36362
- forceColor = 0;
36363
- } else {
36364
- forceColor = env2.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env2.FORCE_COLOR, 10), 3);
36365
- }
36337
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : import_node_process.default.argv) {
36338
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
36339
+ const position = argv.indexOf(prefix + flag);
36340
+ const terminatorPosition = argv.indexOf("--");
36341
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
36342
+ }
36343
+ function envForceColor() {
36344
+ if ("FORCE_COLOR" in env) {
36345
+ if (env.FORCE_COLOR === "true") {
36346
+ return 1;
36366
36347
  }
36367
- function translateLevel2(level) {
36368
- if (level === 0) {
36369
- return false;
36370
- }
36371
- return {
36372
- level,
36373
- hasBasic: true,
36374
- has256: level >= 2,
36375
- has16m: level >= 3
36376
- };
36348
+ if (env.FORCE_COLOR === "false") {
36349
+ return 0;
36377
36350
  }
36378
- function supportsColor2(haveStream, streamIsTTY) {
36379
- if (forceColor === 0) {
36380
- return 0;
36381
- }
36382
- if (hasFlag2("color=16m") || hasFlag2("color=full") || hasFlag2("color=truecolor")) {
36383
- return 3;
36384
- }
36385
- if (hasFlag2("color=256")) {
36386
- return 2;
36387
- }
36388
- if (haveStream && !streamIsTTY && forceColor === void 0) {
36389
- return 0;
36390
- }
36391
- const min = forceColor || 0;
36392
- if (env2.TERM === "dumb") {
36393
- return min;
36394
- }
36395
- if (process.platform === "win32") {
36396
- const osRelease = os3.release().split(".");
36397
- if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
36398
- return Number(osRelease[2]) >= 14931 ? 3 : 2;
36399
- }
36400
- return 1;
36401
- }
36402
- if ("CI" in env2) {
36403
- if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE"].some((sign) => sign in env2) || env2.CI_NAME === "codeship") {
36404
- return 1;
36405
- }
36406
- return min;
36407
- }
36408
- if ("TEAMCITY_VERSION" in env2) {
36409
- return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env2.TEAMCITY_VERSION) ? 1 : 0;
36410
- }
36411
- if (env2.COLORTERM === "truecolor") {
36412
- return 3;
36413
- }
36414
- if ("TERM_PROGRAM" in env2) {
36415
- const version = parseInt((env2.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
36416
- switch (env2.TERM_PROGRAM) {
36417
- case "iTerm.app":
36418
- return version >= 3 ? 3 : 2;
36419
- case "Apple_Terminal":
36420
- return 2;
36421
- }
36351
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
36352
+ }
36353
+ }
36354
+ function translateLevel(level) {
36355
+ if (level === 0) {
36356
+ return false;
36357
+ }
36358
+ return {
36359
+ level,
36360
+ hasBasic: true,
36361
+ has256: level >= 2,
36362
+ has16m: level >= 3
36363
+ };
36364
+ }
36365
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
36366
+ const noFlagForceColor = envForceColor();
36367
+ if (noFlagForceColor !== void 0) {
36368
+ flagForceColor = noFlagForceColor;
36369
+ }
36370
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
36371
+ if (forceColor === 0) {
36372
+ return 0;
36373
+ }
36374
+ if (sniffFlags) {
36375
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
36376
+ return 3;
36377
+ }
36378
+ if (hasFlag("color=256")) {
36379
+ return 2;
36380
+ }
36381
+ }
36382
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) {
36383
+ return 1;
36384
+ }
36385
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
36386
+ return 0;
36387
+ }
36388
+ const min = forceColor || 0;
36389
+ if (env.TERM === "dumb") {
36390
+ return min;
36391
+ }
36392
+ if (import_node_process.default.platform === "win32") {
36393
+ const osRelease = import_node_os.default.release().split(".");
36394
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
36395
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
36396
+ }
36397
+ return 1;
36398
+ }
36399
+ if ("CI" in env) {
36400
+ if ("GITHUB_ACTIONS" in env || "GITEA_ACTIONS" in env) {
36401
+ return 3;
36402
+ }
36403
+ if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
36404
+ return 1;
36405
+ }
36406
+ return min;
36407
+ }
36408
+ if ("TEAMCITY_VERSION" in env) {
36409
+ return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
36410
+ }
36411
+ if (env.COLORTERM === "truecolor") {
36412
+ return 3;
36413
+ }
36414
+ if (env.TERM === "xterm-kitty") {
36415
+ return 3;
36416
+ }
36417
+ if ("TERM_PROGRAM" in env) {
36418
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
36419
+ switch (env.TERM_PROGRAM) {
36420
+ case "iTerm.app": {
36421
+ return version >= 3 ? 3 : 2;
36422
36422
  }
36423
- if (/-256(color)?$/i.test(env2.TERM)) {
36423
+ case "Apple_Terminal": {
36424
36424
  return 2;
36425
36425
  }
36426
- if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env2.TERM)) {
36427
- return 1;
36428
- }
36429
- if ("COLORTERM" in env2) {
36430
- return 1;
36431
- }
36432
- return min;
36433
36426
  }
36434
- function getSupportLevel(stream4) {
36435
- const level = supportsColor2(stream4, stream4 && stream4.isTTY);
36436
- return translateLevel2(level);
36427
+ }
36428
+ if (/-256(color)?$/i.test(env.TERM)) {
36429
+ return 2;
36430
+ }
36431
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
36432
+ return 1;
36433
+ }
36434
+ if ("COLORTERM" in env) {
36435
+ return 1;
36436
+ }
36437
+ return min;
36438
+ }
36439
+ function createSupportsColor(stream4, options = {}) {
36440
+ const level = _supportsColor(stream4, {
36441
+ streamIsTTY: stream4 && stream4.isTTY,
36442
+ ...options
36443
+ });
36444
+ return translateLevel(level);
36445
+ }
36446
+ var import_node_process, import_node_os, import_node_tty, env, flagForceColor, supportsColor, supports_color_default;
36447
+ var init_supports_color = __esm({
36448
+ "../node_modules/supports-color/index.js"() {
36449
+ import_node_process = __toESM(require("node:process"), 1);
36450
+ import_node_os = __toESM(require("node:os"), 1);
36451
+ import_node_tty = __toESM(require("node:tty"), 1);
36452
+ ({ env } = import_node_process.default);
36453
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
36454
+ flagForceColor = 0;
36455
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
36456
+ flagForceColor = 1;
36437
36457
  }
36438
- module2.exports = {
36439
- supportsColor: getSupportLevel,
36440
- stdout: translateLevel2(supportsColor2(true, tty2.isatty(1))),
36441
- stderr: translateLevel2(supportsColor2(true, tty2.isatty(2)))
36458
+ supportsColor = {
36459
+ stdout: createSupportsColor({ isTTY: import_node_tty.default.isatty(1) }),
36460
+ stderr: createSupportsColor({ isTTY: import_node_tty.default.isatty(2) })
36442
36461
  };
36462
+ supports_color_default = supportsColor;
36443
36463
  }
36444
36464
  });
36445
36465
 
@@ -36461,7 +36481,7 @@ var require_node = __commonJS({
36461
36481
  );
36462
36482
  exports2.colors = [6, 2, 3, 4, 5, 1];
36463
36483
  try {
36464
- const supportsColor2 = require_supports_color();
36484
+ const supportsColor2 = (init_supports_color(), __toCommonJS(supports_color_exports));
36465
36485
  if (supportsColor2 && (supportsColor2.stderr || supportsColor2).level >= 2) {
36466
36486
  exports2.colors = [
36467
36487
  20,
@@ -51751,141 +51771,6 @@ var require_browser2 = __commonJS({
51751
51771
  }
51752
51772
  });
51753
51773
 
51754
- // ../node_modules/supports-color/index.js
51755
- var supports_color_exports = {};
51756
- __export(supports_color_exports, {
51757
- createSupportsColor: () => createSupportsColor,
51758
- default: () => supports_color_default
51759
- });
51760
- function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : import_node_process.default.argv) {
51761
- const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
51762
- const position = argv.indexOf(prefix + flag);
51763
- const terminatorPosition = argv.indexOf("--");
51764
- return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
51765
- }
51766
- function envForceColor() {
51767
- if ("FORCE_COLOR" in env) {
51768
- if (env.FORCE_COLOR === "true") {
51769
- return 1;
51770
- }
51771
- if (env.FORCE_COLOR === "false") {
51772
- return 0;
51773
- }
51774
- return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
51775
- }
51776
- }
51777
- function translateLevel(level) {
51778
- if (level === 0) {
51779
- return false;
51780
- }
51781
- return {
51782
- level,
51783
- hasBasic: true,
51784
- has256: level >= 2,
51785
- has16m: level >= 3
51786
- };
51787
- }
51788
- function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
51789
- const noFlagForceColor = envForceColor();
51790
- if (noFlagForceColor !== void 0) {
51791
- flagForceColor = noFlagForceColor;
51792
- }
51793
- const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
51794
- if (forceColor === 0) {
51795
- return 0;
51796
- }
51797
- if (sniffFlags) {
51798
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
51799
- return 3;
51800
- }
51801
- if (hasFlag("color=256")) {
51802
- return 2;
51803
- }
51804
- }
51805
- if ("TF_BUILD" in env && "AGENT_NAME" in env) {
51806
- return 1;
51807
- }
51808
- if (haveStream && !streamIsTTY && forceColor === void 0) {
51809
- return 0;
51810
- }
51811
- const min = forceColor || 0;
51812
- if (env.TERM === "dumb") {
51813
- return min;
51814
- }
51815
- if (import_node_process.default.platform === "win32") {
51816
- const osRelease = import_node_os.default.release().split(".");
51817
- if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
51818
- return Number(osRelease[2]) >= 14931 ? 3 : 2;
51819
- }
51820
- return 1;
51821
- }
51822
- if ("CI" in env) {
51823
- if ("GITHUB_ACTIONS" in env || "GITEA_ACTIONS" in env) {
51824
- return 3;
51825
- }
51826
- if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
51827
- return 1;
51828
- }
51829
- return min;
51830
- }
51831
- if ("TEAMCITY_VERSION" in env) {
51832
- return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
51833
- }
51834
- if (env.COLORTERM === "truecolor") {
51835
- return 3;
51836
- }
51837
- if (env.TERM === "xterm-kitty") {
51838
- return 3;
51839
- }
51840
- if ("TERM_PROGRAM" in env) {
51841
- const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
51842
- switch (env.TERM_PROGRAM) {
51843
- case "iTerm.app": {
51844
- return version >= 3 ? 3 : 2;
51845
- }
51846
- case "Apple_Terminal": {
51847
- return 2;
51848
- }
51849
- }
51850
- }
51851
- if (/-256(color)?$/i.test(env.TERM)) {
51852
- return 2;
51853
- }
51854
- if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
51855
- return 1;
51856
- }
51857
- if ("COLORTERM" in env) {
51858
- return 1;
51859
- }
51860
- return min;
51861
- }
51862
- function createSupportsColor(stream4, options = {}) {
51863
- const level = _supportsColor(stream4, {
51864
- streamIsTTY: stream4 && stream4.isTTY,
51865
- ...options
51866
- });
51867
- return translateLevel(level);
51868
- }
51869
- var import_node_process, import_node_os, import_node_tty, env, flagForceColor, supportsColor, supports_color_default;
51870
- var init_supports_color = __esm({
51871
- "../node_modules/supports-color/index.js"() {
51872
- import_node_process = __toESM(require("node:process"), 1);
51873
- import_node_os = __toESM(require("node:os"), 1);
51874
- import_node_tty = __toESM(require("node:tty"), 1);
51875
- ({ env } = import_node_process.default);
51876
- if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
51877
- flagForceColor = 0;
51878
- } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
51879
- flagForceColor = 1;
51880
- }
51881
- supportsColor = {
51882
- stdout: createSupportsColor({ isTTY: import_node_tty.default.isatty(1) }),
51883
- stderr: createSupportsColor({ isTTY: import_node_tty.default.isatty(2) })
51884
- };
51885
- supports_color_default = supportsColor;
51886
- }
51887
- });
51888
-
51889
51774
  // ../node_modules/debug/src/node.js
51890
51775
  var require_node2 = __commonJS({
51891
51776
  "../node_modules/debug/src/node.js"(exports2, module2) {
@@ -84330,10 +84215,18 @@ function detectPlatform(unameOs = process.platform, unameArch = process.arch) {
84330
84215
  }
84331
84216
  return `${osName}-${arch}`;
84332
84217
  }
84218
+ function isSeaBinary() {
84219
+ try {
84220
+ const sea = require("node:sea");
84221
+ return typeof sea.isSea === "function" && sea.isSea();
84222
+ } catch {
84223
+ return false;
84224
+ }
84225
+ }
84333
84226
  function detectInstallChannel(execPath, scriptPath) {
84334
84227
  const exec = execPath.replace(/\\/g, "/");
84335
84228
  const script = (scriptPath || "").replace(/\\/g, "/");
84336
- if (process.pkg) {
84229
+ if (process.pkg || isSeaBinary()) {
84337
84230
  return "standalone";
84338
84231
  }
84339
84232
  if (/\/Cellar\/mmt-testlight\//i.test(exec) || /\/homebrew\/.*(mmt-testlight|testlight)/i.test(exec)) {
@@ -0,0 +1,39 @@
1
+ import * as esbuild from 'esbuild';
2
+ import path from 'path';
3
+ import {fileURLToPath} from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const coreDir = path.resolve(__dirname, '..', 'core', 'src');
7
+
8
+ // Same resolver as esbuild.mjs so the pkg bundle inlines core from source.
9
+ const mmtCorePlugin = {
10
+ name: 'mmt-core-resolver',
11
+ setup(build) {
12
+ build.onResolve({filter: /^mmt-core$/}, () => ({
13
+ path: path.join(coreDir, 'index.ts'),
14
+ }));
15
+ build.onResolve({filter: /^mmt-core\//}, (args) => {
16
+ const sub = args.path.replace(/^mmt-core\//, '');
17
+ return {path: path.join(coreDir, sub + '.ts')};
18
+ });
19
+ },
20
+ };
21
+
22
+ await esbuild.build({
23
+ entryPoints: ['src/pkg-entry.cjs'],
24
+ bundle: true,
25
+ platform: 'node',
26
+ target: 'node22',
27
+ format: 'cjs',
28
+ outfile: 'dist-cjs/pkg-bundle.cjs',
29
+ plugins: [mmtCorePlugin],
30
+ resolveExtensions: ['.ts', '.js', '.cjs', '.mjs', '.json'],
31
+ // Native addons + reflection protos stay on disk for pkg to embed.
32
+ external: [
33
+ '@grpc/grpc-js',
34
+ '@grpc/proto-loader',
35
+ ],
36
+ logOverride: {
37
+ 'unsupported-dynamic-import': 'silent',
38
+ },
39
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmt-testlight",
3
- "version": "1.42.2-pre",
3
+ "version": "1.42.3-pre",
4
4
  "description": "AI-powered REST Client and Git-native API testing CLI for YAML .mmt files. Same tests in VS Code and CI. Alternative to Postman.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -33,25 +33,20 @@
33
33
  "build": "node esbuild.mjs",
34
34
  "build:tsc": "tsc -p tsconfig.json",
35
35
  "build:cjs": "tsc -p tsconfig.cjs.json",
36
+ "build:pkg-bundle": "node esbuild.pkg.mjs",
36
37
  "dev": "tsx src/cli.ts --help",
37
38
  "clean": "rimraf dist",
38
- "pkg": "rimraf dist-cjs && npm run build:cjs && bash ../scripts/build-binaries.sh",
39
+ "pkg": "rimraf dist-cjs && npm run build:pkg-bundle && bash ../scripts/build-binaries.sh",
39
40
  "test": "echo \"(no tests yet)\""
40
41
  },
41
42
  "pkg": {
42
- "scripts": [
43
- "dist-cjs/**/*.js",
44
- "node_modules/mmt-core/dist/**/*.js",
45
- "../core/dist/**/*.js"
46
- ],
47
43
  "assets": [
48
- "node_modules/axios/dist/**",
49
- "node_modules/axios/lib/**",
50
- "node_modules/axios/dist/node/axios.cjs",
51
- "node_modules/ws/**/*.js",
52
- "node_modules/mmt-core/dist/**",
53
- "node_modules/mmt-core/package.json",
54
- "../core/dist/**"
44
+ "node_modules/@grpc/grpc-js/**",
45
+ "node_modules/@grpc/proto-loader/**",
46
+ "../node_modules/@grpc/grpc-js/**",
47
+ "../node_modules/@grpc/proto-loader/**",
48
+ "../core/node_modules/@grpc/grpc-js/**",
49
+ "../core/node_modules/@grpc/proto-loader/**"
55
50
  ]
56
51
  },
57
52
  "dependencies": {
@@ -64,9 +59,14 @@
64
59
  "node-forge": "^1.3.2"
65
60
  },
66
61
  "devDependencies": {
62
+ "@grpc/grpc-js": "^1.12.6",
63
+ "@grpc/proto-loader": "^0.7.13",
67
64
  "@types/js-yaml": "^4.0.9",
68
65
  "@types/node": "^20.11.30",
69
- "pkg": "^5.8.1",
66
+ "@yao-pkg/pkg": "^6.22.0",
67
+ "pe-library": "^2.0.1",
68
+ "png-to-ico": "^2.1.8",
69
+ "resedit": "^3.1.0",
70
70
  "rimraf": "^5.0.5",
71
71
  "tsx": "^4.7.0",
72
72
  "typescript": "^5.4.2"
package/src/pkg-entry.cjs CHANGED
@@ -547,6 +547,7 @@ function printHelp() {
547
547
  const commands = formatHelpRows([
548
548
  ['run [options] <file>', 'Run a .mmt file'],
549
549
  ['print-js [options] <file>', 'Print generated JS for a test'],
550
+ ['update [options]', 'Update this standalone binary from GitHub Releases'],
550
551
  ['debug-resolve', 'Print pkg module resolution info'],
551
552
  ]);
552
553
  const options = formatHelpRows([
@@ -569,6 +570,8 @@ function printHelp() {
569
570
  ['testlight run path/to/test.mmt'],
570
571
  ['testlight run path/to/test.mmt -e api_url=https://test.mmt.dev -q'],
571
572
  ['testlight print-js path/to/test.mmt'],
573
+ ['testlight update'],
574
+ ['testlight update --check'],
572
575
  ], ' ', 0);
573
576
  const out = [
574
577
  'Usage: testlight <command> [options]',
@@ -643,6 +646,35 @@ function printPrintJsHelp() {
643
646
  process.stdout.write(out + '\n');
644
647
  }
645
648
 
649
+ function printUpdateHelp() {
650
+ const options = formatHelpRows([
651
+ ['-h, --help', 'Show help'],
652
+ ['--check', 'Only check whether an update is available'],
653
+ ['--to <version>', 'Install a specific version (e.g. 1.38.1)'],
654
+ ['--channel <name>', 'Latest matching channel (beta, rc, prerelease)'],
655
+ ['--force', 'Reinstall even when already newest'],
656
+ ['--repo <owner/name>', 'GitHub repo (default: mshobeyri/multimeter)'],
657
+ ['--base-url <url>', 'Portal/mirror base URL'],
658
+ ]);
659
+ const examples = formatHelpRows([
660
+ ['testlight update'],
661
+ ['testlight update --check'],
662
+ ['testlight update --to 1.38.1'],
663
+ ], ' ', 0);
664
+ const out = [
665
+ 'Usage: testlight update [options]',
666
+ '',
667
+ 'Update this standalone binary from GitHub Releases (or a portal mirror)',
668
+ '',
669
+ 'Options:',
670
+ ...options,
671
+ '',
672
+ 'Examples:',
673
+ ...examples,
674
+ ].join('\n');
675
+ process.stdout.write(out + '\n');
676
+ }
677
+
646
678
  function coerceCliValue(v) {
647
679
  const t = String(v ?? '').trim();
648
680
  if (/^(true|false)$/i.test(t)) {
@@ -919,6 +951,53 @@ function parsePrintJsArgv(argv) {
919
951
  return { filePath, opts };
920
952
  }
921
953
 
954
+ function parseUpdateArgv(argv) {
955
+ const opts = {
956
+ check: false,
957
+ to: undefined,
958
+ channel: undefined,
959
+ force: false,
960
+ repo: undefined,
961
+ baseUrl: undefined,
962
+ help: false,
963
+ };
964
+ for (let i = 1; i < argv.length; i++) {
965
+ const a = argv[i];
966
+ if (!a) {
967
+ continue;
968
+ }
969
+ if (a === '-h' || a === '--help') {
970
+ opts.help = true;
971
+ continue;
972
+ }
973
+ if (a === '--check') {
974
+ opts.check = true;
975
+ continue;
976
+ }
977
+ if (a === '--force') {
978
+ opts.force = true;
979
+ continue;
980
+ }
981
+ if (a === '--to') {
982
+ opts.to = argv[++i];
983
+ continue;
984
+ }
985
+ if (a === '--channel') {
986
+ opts.channel = argv[++i];
987
+ continue;
988
+ }
989
+ if (a === '--repo') {
990
+ opts.repo = argv[++i];
991
+ continue;
992
+ }
993
+ if (a === '--base-url') {
994
+ opts.baseUrl = argv[++i];
995
+ continue;
996
+ }
997
+ }
998
+ return opts;
999
+ }
1000
+
922
1001
  function parseLogLevel(argv) {
923
1002
  for (let i = 1; i < argv.length; i++) {
924
1003
  const a = argv[i];
@@ -1023,6 +1102,39 @@ async function main() {
1023
1102
  process.stdout.write(msgs.join('\n') + '\n');
1024
1103
  return;
1025
1104
  }
1105
+ if (command === 'update') {
1106
+ const parsed = parseUpdateArgv(argv);
1107
+ if (parsed.help) {
1108
+ printUpdateHelp();
1109
+ return;
1110
+ }
1111
+ let currentVersion = '';
1112
+ try {
1113
+ currentVersion = String(require('../package.json').version || '');
1114
+ } catch {
1115
+ currentVersion = '';
1116
+ }
1117
+ try {
1118
+ const {runUpdate} = require('./selfUpdate');
1119
+ const result = await runUpdate({
1120
+ currentVersion,
1121
+ checkOnly: !!parsed.check,
1122
+ version: parsed.to,
1123
+ channel: parsed.channel,
1124
+ force: !!parsed.force,
1125
+ repo: parsed.repo,
1126
+ releaseBaseUrl: parsed.baseUrl,
1127
+ });
1128
+ process.stdout.write(String(result.message || '') + '\n');
1129
+ if (!result.ok) {
1130
+ process.exitCode = 2;
1131
+ }
1132
+ } catch (e) {
1133
+ process.stderr.write(`Error updating testlight: ${e && e.message ? e.message : e}\n`);
1134
+ process.exitCode = 2;
1135
+ }
1136
+ return;
1137
+ }
1026
1138
  if (command !== 'run' && command !== 'print-js') {
1027
1139
  process.stderr.write(`Unknown command: ${command}\n`);
1028
1140
  printHelp();
@@ -1,3 +1,7 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import {execFileSync} from 'child_process';
1
5
  import {
2
6
  buildDownloadUrl,
3
7
  compareVersions,
@@ -5,6 +9,7 @@ import {
5
9
  detectPlatform,
6
10
  normalizeVersion,
7
11
  planUpdate,
12
+ runUpdate,
8
13
  } from './selfUpdate';
9
14
 
10
15
  describe('selfUpdate helpers', () => {
@@ -31,6 +36,7 @@ describe('selfUpdate helpers', () => {
31
36
  '/usr/local/bin/node',
32
37
  '/usr/local/lib/node_modules/mmt-testlight/dist/cli.js'))
33
38
  .toBe('npm');
39
+ expect(detectInstallChannel('/Applications/testlight')).toBe('standalone');
34
40
  });
35
41
 
36
42
  it('builds github and portal download urls', () => {
@@ -76,4 +82,41 @@ describe('selfUpdate helpers', () => {
76
82
  expect(plan.action).toBe('download');
77
83
  expect(plan.downloadUrl).toContain('/v1.2.3/testlight-');
78
84
  });
85
+
86
+ it('replaces a standalone binary from a downloaded archive', async () => {
87
+ if (process.platform === 'win32') {
88
+ return;
89
+ }
90
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'testlight-update-test-'));
91
+ try {
92
+ const installDir = path.join(tmp, 'install');
93
+ const payloadDir = path.join(tmp, 'payload');
94
+ fs.mkdirSync(installDir);
95
+ fs.mkdirSync(payloadDir);
96
+ const current = path.join(installDir, 'testlight');
97
+ fs.writeFileSync(current, 'OLD');
98
+ fs.chmodSync(current, 0o755);
99
+ fs.writeFileSync(path.join(payloadDir, 'testlight'), 'NEW');
100
+ const archive = path.join(tmp, 'pkg.tar.gz');
101
+ execFileSync('tar', ['-czf', archive, '-C', payloadDir, 'testlight']);
102
+
103
+ const result = await runUpdate({
104
+ currentVersion: '1.0.0',
105
+ version: '1.2.3',
106
+ execPath: current,
107
+ fetchJson: async () => ({tag_name: 'v1.2.3'}),
108
+ downloadToFile: async (_url, dest) => {
109
+ fs.copyFileSync(archive, dest);
110
+ },
111
+ log: () => {},
112
+ });
113
+
114
+ expect(result.ok).toBe(true);
115
+ expect(result.updated).toBe(true);
116
+ expect(fs.readFileSync(current, 'utf8')).toBe('NEW');
117
+ expect(fs.lstatSync(path.join(installDir, 'mmt')).isSymbolicLink()).toBe(true);
118
+ } finally {
119
+ fs.rmSync(tmp, {recursive: true, force: true});
120
+ }
121
+ });
79
122
  });
package/src/selfUpdate.ts CHANGED
@@ -107,11 +107,21 @@ export function detectPlatform(
107
107
  return `${osName}-${arch}` as UpdatePlatform;
108
108
  }
109
109
 
110
+ function isSeaBinary(): boolean {
111
+ try {
112
+ // Node 20+ official SEA (used by @yao-pkg/pkg --sea). Not set for vercel/pkg.
113
+ const sea = require('node:sea') as {isSea?: () => boolean};
114
+ return typeof sea.isSea === 'function' && sea.isSea();
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
110
120
  export function detectInstallChannel(execPath: string, scriptPath?: string):
111
121
  InstallChannel {
112
122
  const exec = execPath.replace(/\\/g, '/');
113
123
  const script = (scriptPath || '').replace(/\\/g, '/');
114
- if ((process as any).pkg) {
124
+ if ((process as any).pkg || isSeaBinary()) {
115
125
  return 'standalone';
116
126
  }
117
127
  if (/\/Cellar\/mmt-testlight\//i.test(exec) ||