my-singbox 2.0.0-next.0 → 3.0.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/index.js CHANGED
@@ -48,10 +48,10 @@ var require_identity = __commonJS({
48
48
  var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
49
49
  var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
50
50
  var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
51
- var isMap3 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
51
+ var isMap2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
52
52
  var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
53
- var isScalar3 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
54
- var isSeq3 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
53
+ var isScalar2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
54
+ var isSeq2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
55
55
  function isCollection(node) {
56
56
  if (node && typeof node === "object")
57
57
  switch (node[NODE_TYPE]) {
@@ -72,7 +72,7 @@ var require_identity = __commonJS({
72
72
  }
73
73
  return false;
74
74
  }
75
- var hasAnchor = (node) => (isScalar3(node) || isCollection(node)) && !!node.anchor;
75
+ var hasAnchor = (node) => (isScalar2(node) || isCollection(node)) && !!node.anchor;
76
76
  exports.ALIAS = ALIAS;
77
77
  exports.DOC = DOC;
78
78
  exports.MAP = MAP;
@@ -84,11 +84,11 @@ var require_identity = __commonJS({
84
84
  exports.isAlias = isAlias;
85
85
  exports.isCollection = isCollection;
86
86
  exports.isDocument = isDocument;
87
- exports.isMap = isMap3;
87
+ exports.isMap = isMap2;
88
88
  exports.isNode = isNode;
89
89
  exports.isPair = isPair;
90
- exports.isScalar = isScalar3;
91
- exports.isSeq = isSeq3;
90
+ exports.isScalar = isScalar2;
91
+ exports.isSeq = isSeq2;
92
92
  }
93
93
  });
94
94
 
@@ -100,7 +100,7 @@ var require_visit = __commonJS({
100
100
  var BREAK = /* @__PURE__ */ Symbol("break visit");
101
101
  var SKIP = /* @__PURE__ */ Symbol("skip children");
102
102
  var REMOVE = /* @__PURE__ */ Symbol("remove node");
103
- function visit3(node, visitor) {
103
+ function visit2(node, visitor) {
104
104
  const visitor_ = initVisitor(visitor);
105
105
  if (identity.isDocument(node)) {
106
106
  const cd = visit_(null, node.contents, visitor_, Object.freeze([node]));
@@ -109,9 +109,9 @@ var require_visit = __commonJS({
109
109
  } else
110
110
  visit_(null, node, visitor_, Object.freeze([]));
111
111
  }
112
- visit3.BREAK = BREAK;
113
- visit3.SKIP = SKIP;
114
- visit3.REMOVE = REMOVE;
112
+ visit2.BREAK = BREAK;
113
+ visit2.SKIP = SKIP;
114
+ visit2.REMOVE = REMOVE;
115
115
  function visit_(key, node, visitor, path) {
116
116
  const ctrl = callVisitor(key, node, visitor, path);
117
117
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
@@ -245,7 +245,7 @@ var require_visit = __commonJS({
245
245
  throw new Error(`Cannot replace node with ${pt} parent`);
246
246
  }
247
247
  }
248
- exports.visit = visit3;
248
+ exports.visit = visit2;
249
249
  exports.visitAsync = visitAsync;
250
250
  }
251
251
  });
@@ -255,7 +255,7 @@ var require_directives = __commonJS({
255
255
  "node_modules/yaml/dist/doc/directives.js"(exports) {
256
256
  "use strict";
257
257
  var identity = require_identity();
258
- var visit3 = require_visit();
258
+ var visit2 = require_visit();
259
259
  var escapeChars = {
260
260
  "!": "%21",
261
261
  ",": "%2C",
@@ -399,7 +399,7 @@ var require_directives = __commonJS({
399
399
  let tagNames;
400
400
  if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
401
401
  const tags = {};
402
- visit3.visit(doc.contents, (_key, node) => {
402
+ visit2.visit(doc.contents, (_key, node) => {
403
403
  if (identity.isNode(node) && node.tag)
404
404
  tags[node.tag] = true;
405
405
  });
@@ -426,7 +426,7 @@ var require_anchors = __commonJS({
426
426
  "node_modules/yaml/dist/doc/anchors.js"(exports) {
427
427
  "use strict";
428
428
  var identity = require_identity();
429
- var visit3 = require_visit();
429
+ var visit2 = require_visit();
430
430
  function anchorIsValid(anchor) {
431
431
  if (/[\x00-\x19\s,[\]{}]/.test(anchor)) {
432
432
  const sa = JSON.stringify(anchor);
@@ -437,7 +437,7 @@ var require_anchors = __commonJS({
437
437
  }
438
438
  function anchorNames(root) {
439
439
  const anchors = /* @__PURE__ */ new Set();
440
- visit3.visit(root, {
440
+ visit2.visit(root, {
441
441
  Value(_key, node) {
442
442
  if (node.anchor)
443
443
  anchors.add(node.anchor);
@@ -617,7 +617,7 @@ var require_Alias = __commonJS({
617
617
  "node_modules/yaml/dist/nodes/Alias.js"(exports) {
618
618
  "use strict";
619
619
  var anchors = require_anchors();
620
- var visit3 = require_visit();
620
+ var visit2 = require_visit();
621
621
  var identity = require_identity();
622
622
  var Node = require_Node();
623
623
  var toJS = require_toJS();
@@ -643,7 +643,7 @@ var require_Alias = __commonJS({
643
643
  nodes = ctx.aliasResolveCache;
644
644
  } else {
645
645
  nodes = [];
646
- visit3.visit(doc, {
646
+ visit2.visit(doc, {
647
647
  Node: (_key, node) => {
648
648
  if (identity.isAlias(node) || identity.hasAnchor(node))
649
649
  nodes.push(node);
@@ -4196,9 +4196,9 @@ var require_resolve_flow_collection = __commonJS({
4196
4196
  var blockMsg = "Block collections are not allowed within flow collections";
4197
4197
  var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
4198
4198
  function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
4199
- const isMap3 = fc.start.source === "{";
4200
- const fcName = isMap3 ? "flow map" : "flow sequence";
4201
- const NodeClass = tag?.nodeClass ?? (isMap3 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
4199
+ const isMap2 = fc.start.source === "{";
4200
+ const fcName = isMap2 ? "flow map" : "flow sequence";
4201
+ const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
4202
4202
  const coll = new NodeClass(ctx.schema);
4203
4203
  coll.flow = true;
4204
4204
  const atRoot = ctx.atRoot;
@@ -4234,7 +4234,7 @@ var require_resolve_flow_collection = __commonJS({
4234
4234
  offset = props.end;
4235
4235
  continue;
4236
4236
  }
4237
- if (!isMap3 && ctx.options.strict && utilContainsNewline.containsNewline(key))
4237
+ if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
4238
4238
  onError(
4239
4239
  key,
4240
4240
  // checked by containsNewline()
@@ -4274,7 +4274,7 @@ var require_resolve_flow_collection = __commonJS({
4274
4274
  }
4275
4275
  }
4276
4276
  }
4277
- if (!isMap3 && !sep && !props.found) {
4277
+ if (!isMap2 && !sep && !props.found) {
4278
4278
  const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
4279
4279
  coll.items.push(valueNode);
4280
4280
  offset = valueNode.range[2];
@@ -4297,7 +4297,7 @@ var require_resolve_flow_collection = __commonJS({
4297
4297
  startOnNewline: false
4298
4298
  });
4299
4299
  if (valueProps.found) {
4300
- if (!isMap3 && !props.found && ctx.options.strict) {
4300
+ if (!isMap2 && !props.found && ctx.options.strict) {
4301
4301
  if (sep)
4302
4302
  for (const st of sep) {
4303
4303
  if (st === valueProps.found)
@@ -4329,7 +4329,7 @@ var require_resolve_flow_collection = __commonJS({
4329
4329
  const pair = new Pair.Pair(keyNode, valueNode);
4330
4330
  if (ctx.options.keepSourceTokens)
4331
4331
  pair.srcToken = collItem;
4332
- if (isMap3) {
4332
+ if (isMap2) {
4333
4333
  const map = coll;
4334
4334
  if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
4335
4335
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
@@ -4345,7 +4345,7 @@ var require_resolve_flow_collection = __commonJS({
4345
4345
  offset = valueNode ? valueNode.range[2] : valueProps.end;
4346
4346
  }
4347
4347
  }
4348
- const expectedEnd = isMap3 ? "}" : "]";
4348
+ const expectedEnd = isMap2 ? "}" : "]";
4349
4349
  const [ce, ...ee] = fc.end;
4350
4350
  let cePos = offset;
4351
4351
  if (ce?.source === expectedEnd)
@@ -5565,15 +5565,15 @@ var require_cst_visit = __commonJS({
5565
5565
  var BREAK = /* @__PURE__ */ Symbol("break visit");
5566
5566
  var SKIP = /* @__PURE__ */ Symbol("skip children");
5567
5567
  var REMOVE = /* @__PURE__ */ Symbol("remove item");
5568
- function visit3(cst, visitor) {
5568
+ function visit2(cst, visitor) {
5569
5569
  if ("type" in cst && cst.type === "document")
5570
5570
  cst = { start: cst.start, value: cst.value };
5571
5571
  _visit(Object.freeze([]), cst, visitor);
5572
5572
  }
5573
- visit3.BREAK = BREAK;
5574
- visit3.SKIP = SKIP;
5575
- visit3.REMOVE = REMOVE;
5576
- visit3.itemAtPath = (cst, path) => {
5573
+ visit2.BREAK = BREAK;
5574
+ visit2.SKIP = SKIP;
5575
+ visit2.REMOVE = REMOVE;
5576
+ visit2.itemAtPath = (cst, path) => {
5577
5577
  let item = cst;
5578
5578
  for (const [field, index] of path) {
5579
5579
  const tok = item?.[field];
@@ -5584,8 +5584,8 @@ var require_cst_visit = __commonJS({
5584
5584
  }
5585
5585
  return item;
5586
5586
  };
5587
- visit3.parentCollection = (cst, path) => {
5588
- const parent = visit3.itemAtPath(cst, path.slice(0, -1));
5587
+ visit2.parentCollection = (cst, path) => {
5588
+ const parent = visit2.itemAtPath(cst, path.slice(0, -1));
5589
5589
  const field = path[path.length - 1][0];
5590
5590
  const coll = parent?.[field];
5591
5591
  if (coll && "items" in coll)
@@ -5616,7 +5616,7 @@ var require_cst_visit = __commonJS({
5616
5616
  }
5617
5617
  return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
5618
5618
  }
5619
- exports.visit = visit3;
5619
+ exports.visit = visit2;
5620
5620
  }
5621
5621
  });
5622
5622
 
@@ -5632,7 +5632,7 @@ var require_cst = __commonJS({
5632
5632
  var FLOW_END = "";
5633
5633
  var SCALAR = "";
5634
5634
  var isCollection = (token) => !!token && "items" in token;
5635
- var isScalar3 = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar");
5635
+ var isScalar2 = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar");
5636
5636
  function prettyToken(token) {
5637
5637
  switch (token) {
5638
5638
  case BOM:
@@ -5716,7 +5716,7 @@ var require_cst = __commonJS({
5716
5716
  exports.FLOW_END = FLOW_END;
5717
5717
  exports.SCALAR = SCALAR;
5718
5718
  exports.isCollection = isCollection;
5719
- exports.isScalar = isScalar3;
5719
+ exports.isScalar = isScalar2;
5720
5720
  exports.prettyToken = prettyToken;
5721
5721
  exports.tokenType = tokenType;
5722
5722
  }
@@ -7246,7 +7246,7 @@ var require_public_api = __commonJS({
7246
7246
  return docs;
7247
7247
  return Object.assign([], { empty: true }, composer$1.streamInfo());
7248
7248
  }
7249
- function parseDocument3(source, options = {}) {
7249
+ function parseDocument2(source, options = {}) {
7250
7250
  const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
7251
7251
  const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
7252
7252
  const composer$1 = new composer.Composer(options);
@@ -7272,7 +7272,7 @@ var require_public_api = __commonJS({
7272
7272
  } else if (options === void 0 && reviver && typeof reviver === "object") {
7273
7273
  options = reviver;
7274
7274
  }
7275
- const doc = parseDocument3(src, options);
7275
+ const doc = parseDocument2(src, options);
7276
7276
  if (!doc)
7277
7277
  return null;
7278
7278
  doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
@@ -7308,7 +7308,7 @@ var require_public_api = __commonJS({
7308
7308
  }
7309
7309
  exports.parse = parse;
7310
7310
  exports.parseAllDocuments = parseAllDocuments;
7311
- exports.parseDocument = parseDocument3;
7311
+ exports.parseDocument = parseDocument2;
7312
7312
  exports.stringify = stringify;
7313
7313
  }
7314
7314
  });
@@ -7332,7 +7332,7 @@ var require_dist = __commonJS({
7332
7332
  var lineCounter = require_line_counter();
7333
7333
  var parser = require_parser();
7334
7334
  var publicApi = require_public_api();
7335
- var visit3 = require_visit();
7335
+ var visit2 = require_visit();
7336
7336
  exports.Composer = composer.Composer;
7337
7337
  exports.Document = Document.Document;
7338
7338
  exports.Schema = Schema.Schema;
@@ -7360,8 +7360,8 @@ var require_dist = __commonJS({
7360
7360
  exports.parseAllDocuments = publicApi.parseAllDocuments;
7361
7361
  exports.parseDocument = publicApi.parseDocument;
7362
7362
  exports.stringify = publicApi.stringify;
7363
- exports.visit = visit3.visit;
7364
- exports.visitAsync = visit3.visitAsync;
7363
+ exports.visit = visit2.visit;
7364
+ exports.visitAsync = visit2.visitAsync;
7365
7365
  }
7366
7366
  });
7367
7367
 
@@ -7433,7 +7433,7 @@ async function dockerAccessGuidance(s) {
7433
7433
  const already = groups.trim().split(/\s+/).includes("docker");
7434
7434
  return [
7435
7435
  "This tool never uses sudo and never escalates privileges itself.",
7436
- "Open firewall / cloud security group ports: 8388/tcp, 8388/udp, 8443/tcp; only for optional ISP outbound: 13839/tcp, 13839/udp, 18444/tcp.",
7436
+ "Open firewall / cloud security group ports: 8388/tcp, 8388/udp, 8443/tcp. ISP routing uses the same listening ports.",
7437
7437
  ...already ? [
7438
7438
  "The Docker daemon may not be running, or your session predates the group change. Log out and back in (or run `newgrp docker`)."
7439
7439
  ] : [
@@ -7838,20 +7838,20 @@ function buildModel(env, opts = {}) {
7838
7838
  publicKey,
7839
7839
  shortId: v.REALITY_SHORT_ID
7840
7840
  };
7841
- const node = (name, protocol, tag, port, egress) => ({
7841
+ const node = (name, protocol, tag, port) => ({
7842
7842
  name,
7843
7843
  protocol,
7844
7844
  inbound: { tag, port },
7845
- egress,
7846
7845
  creds: protocol === "shadowsocks" ? ss : reality
7847
7846
  });
7848
7847
  const model = {
7849
7848
  server,
7850
7849
  nodes: [
7851
- node(NODE_SS, "shadowsocks", SS_TAG, SS_PORT, "direct"),
7852
- node(NODE_VLESS, "vless-reality", VLESS_TAG, VLESS_PORT, "direct")
7850
+ node(NODE_SS, "shadowsocks", SS_TAG, SS_PORT),
7851
+ node(NODE_VLESS, "vless-reality", VLESS_TAG, VLESS_PORT)
7853
7852
  ]
7854
7853
  };
7854
+ if (target === "client") return model;
7855
7855
  const {
7856
7856
  ISP_SOCKS_ADDR: rawAddr = "",
7857
7857
  ISP_SOCKS_USER: rawUser = "",
@@ -7874,16 +7874,6 @@ function buildModel(env, opts = {}) {
7874
7874
  pass,
7875
7875
  ...ip ? { egressIp: ip } : {}
7876
7876
  };
7877
- model.nodes.push(
7878
- node(NODE_SS_ISP, "shadowsocks", SS_ISP_TAG, SS_ISP_PORT, "isp-socks"),
7879
- node(
7880
- NODE_VLESS_ISP,
7881
- "vless-reality",
7882
- VLESS_ISP_TAG,
7883
- VLESS_ISP_PORT,
7884
- "isp-socks"
7885
- )
7886
- );
7887
7877
  return model;
7888
7878
  }
7889
7879
 
@@ -7916,6 +7906,64 @@ function readTemplate(name, projectDir) {
7916
7906
  return readFileSync2(findTemplate(name, projectDir), "utf8");
7917
7907
  }
7918
7908
 
7909
+ // src/core/routing.ts
7910
+ function isRecord(value) {
7911
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7912
+ }
7913
+ function isIspRule(value) {
7914
+ return isRecord(value) && value.outbound === OUT_TAG;
7915
+ }
7916
+ function isLegacyIspRule(value) {
7917
+ if (!isIspRule(value)) return false;
7918
+ const inbound = typeof value.inbound === "string" ? [value.inbound] : value.inbound;
7919
+ return Array.isArray(inbound) && inbound.length > 0 && inbound.every((tag) => tag === SS_ISP_TAG || tag === VLESS_ISP_TAG) && (value.action === void 0 || value.action === "route") && Object.keys(value).every((key) => ["inbound", "outbound", "action"].includes(key));
7920
+ }
7921
+ function checkNoIspReferences(config) {
7922
+ const referenceKeys = /* @__PURE__ */ new Set(["outbound", "outbounds", "detour", "final", "default"]);
7923
+ const walk = (value, path, key = "") => {
7924
+ if (value === OUT_TAG && referenceKeys.has(key))
7925
+ throw new Error(`Unsupported ISP reference at ${path}. Remove this reference before disabling ISP.`);
7926
+ if (Array.isArray(value))
7927
+ value.forEach((item, i) => walk(item, `${path}[${i}]`, key));
7928
+ else if (isRecord(value))
7929
+ for (const [name, item] of Object.entries(value)) walk(item, `${path}.${name}`, name);
7930
+ };
7931
+ walk(config, "config");
7932
+ }
7933
+ function upsertIspOutbound(config, isp) {
7934
+ const outbounds = config.outbounds ??= [];
7935
+ const matches = outbounds.filter((o) => o.tag === OUT_TAG);
7936
+ if (matches.length > 1) throw new Error(`Duplicate outbound tag: ${OUT_TAG}.`);
7937
+ const fields = {
7938
+ type: "socks",
7939
+ tag: OUT_TAG,
7940
+ server: isp.host,
7941
+ server_port: isp.port,
7942
+ version: "5",
7943
+ username: isp.user,
7944
+ password: isp.pass
7945
+ };
7946
+ if (matches[0]) {
7947
+ if (matches[0].type !== "socks")
7948
+ throw new Error(`Outbound ${OUT_TAG} must have type socks.`);
7949
+ Object.assign(matches[0], fields);
7950
+ } else outbounds.push(fields);
7951
+ }
7952
+ function omitIsp(config) {
7953
+ if (config.outbounds) config.outbounds = config.outbounds.filter((o) => o.tag !== OUT_TAG);
7954
+ if (config.route?.rules) config.route.rules = config.route.rules.filter((r) => !isIspRule(r));
7955
+ checkNoIspReferences(config);
7956
+ }
7957
+ function ensureRoute(config, defaults) {
7958
+ const route = config.route ??= {};
7959
+ const rules = route.rules ??= [];
7960
+ if (!rules.some((r) => isRecord(r) && r.action === "sniff"))
7961
+ rules.unshift({ action: "sniff" });
7962
+ if (!rules.some(isIspRule) && defaults?.rules)
7963
+ rules.push(...structuredClone(defaults.rules.filter(isIspRule)));
7964
+ route.final ??= "direct";
7965
+ }
7966
+
7919
7967
  // src/core/render-config.ts
7920
7968
  function renderSingbox(model, template) {
7921
7969
  const ss = model.nodes.find((n) => n.protocol === "shadowsocks").creds;
@@ -7933,7 +7981,7 @@ function renderSingbox(model, template) {
7933
7981
  const prototypes = /* @__PURE__ */ new Map();
7934
7982
  for (const protocol of ["shadowsocks", "vless-reality"]) {
7935
7983
  const base = model.nodes.find(
7936
- (n) => n.protocol === protocol && n.egress === "direct"
7984
+ (n) => n.protocol === protocol
7937
7985
  );
7938
7986
  const proto = config.inbounds.find(
7939
7987
  (i) => i.type === (protocol === "shadowsocks" ? "shadowsocks" : "vless")
@@ -7947,27 +7995,9 @@ function renderSingbox(model, template) {
7947
7995
  tag: n.inbound.tag,
7948
7996
  listen_port: n.inbound.port
7949
7997
  }));
7950
- const ispNodes = model.nodes.filter((n) => n.egress === "isp-socks");
7951
- if (ispNodes.length) {
7952
- const isp = model.isp;
7953
- config.outbounds = [
7954
- ...config.outbounds ?? [],
7955
- {
7956
- type: "socks",
7957
- tag: OUT_TAG,
7958
- server: isp.host,
7959
- server_port: isp.port,
7960
- version: "5",
7961
- username: isp.user,
7962
- password: isp.pass
7963
- }
7964
- ];
7965
- config.route ??= {};
7966
- config.route.rules = [
7967
- ...config.route.rules ?? [],
7968
- { inbound: ispNodes.map((n) => n.inbound.tag), outbound: OUT_TAG }
7969
- ];
7970
- }
7998
+ ensureRoute(config);
7999
+ if (model.isp) upsertIspOutbound(config, model.isp);
8000
+ else omitIsp(config);
7971
8001
  if (JSON.stringify(config).includes("${"))
7972
8002
  throw new Error("Unresolved placeholder left in config.json.");
7973
8003
  return config;
@@ -8015,11 +8045,9 @@ function renderClash(model, templateText) {
8015
8045
  }
8016
8046
  proxies.items = model.nodes.map((n) => {
8017
8047
  const proto = prototypes.get(n.protocol);
8018
- if (n.egress === "direct") return proto;
8019
- const clone = proto.clone();
8020
- clone.set("name", n.name);
8021
- clone.set("port", n.inbound.port);
8022
- return clone;
8048
+ proto.set("name", n.name);
8049
+ proto.set("port", n.inbound.port);
8050
+ return proto;
8023
8051
  });
8024
8052
  const group = groups.items.find((g) => (0, import_yaml.isMap)(g) && g.get("name") === "Proxy");
8025
8053
  const list = (0, import_yaml.isMap)(group) ? group.get("proxies", true) : null;
@@ -8038,19 +8066,154 @@ function renderClash(model, templateText) {
8038
8066
  // src/core/isp-service.ts
8039
8067
  import { spawn } from "node:child_process";
8040
8068
  import { createConnection } from "node:net";
8069
+ import { readFileSync as readFileSync5 } from "node:fs";
8070
+ import { join as join5 } from "node:path";
8071
+
8072
+ // src/core/start.ts
8073
+ import { execFile as execFile2 } from "node:child_process";
8074
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
8075
+ import { connect } from "node:net";
8076
+ import { join as join3 } from "node:path";
8077
+ function parseListenPorts(configText) {
8078
+ const ports = /* @__PURE__ */ new Set();
8079
+ for (const m of configText.matchAll(/"listen_port"\s*:\s*([0-9]+)/g)) {
8080
+ const port = Number(m[1]);
8081
+ if (port < 1 || port > 65535)
8082
+ throw new Error(`Invalid listen_port in config.json: ${m[1]}`);
8083
+ ports.add(port);
8084
+ }
8085
+ if (!ports.size) throw new Error("config.json has no listen_port.");
8086
+ return [...ports];
8087
+ }
8088
+ function secretValues(envText) {
8089
+ return [...envText.matchAll(/^(?:REALITY_PRIVATE_KEY|SS_PASSWORD|VLESS_UUID|ISP_SOCKS_USER|ISP_SOCKS_PASS)=([^\r\n]*)\r?$/gm)].map((m) => envUnquote(m[1])).filter((value) => value !== null && value !== "");
8090
+ }
8091
+ function redact(text, secrets) {
8092
+ for (const secret of [...secrets].sort((a, b) => b.length - a.length))
8093
+ text = text.replaceAll(secret, "***");
8094
+ return text;
8095
+ }
8096
+ function composeImageTag(text) {
8097
+ const m = /^\s*image:\s*ghcr\.io\/sagernet\/sing-box:([^\s#]+)/m.exec(text);
8098
+ if (!m) throw new Error("sing-box image tag not found in compose.yaml.");
8099
+ return m[1];
8100
+ }
8101
+ function firewallPorts(configText) {
8102
+ const config = JSON.parse(configText);
8103
+ return config.inbounds.flatMap(
8104
+ (i) => i.type === "shadowsocks" ? [`${i.listen_port}/tcp`, `${i.listen_port}/udp`] : [`${i.listen_port}/tcp`]
8105
+ );
8106
+ }
8107
+ function realStartDeps() {
8108
+ return {
8109
+ run: (file, args) => new Promise((resolve) => {
8110
+ execFile2(file, args, (error, stdout, stderr) => {
8111
+ const code = error ? typeof error.code === "number" ? error.code : error.code === "ENOENT" ? 127 : 1 : 0;
8112
+ resolve({ code, stdout, stderr });
8113
+ });
8114
+ }),
8115
+ tcpOpen: (port) => new Promise((resolve) => {
8116
+ const socket = connect(port, "127.0.0.1");
8117
+ const finish = (open) => {
8118
+ socket.destroy();
8119
+ resolve(open);
8120
+ };
8121
+ socket.setTimeout(500, () => finish(false));
8122
+ socket.once("connect", () => finish(true));
8123
+ socket.once("error", () => finish(false));
8124
+ }),
8125
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
8126
+ };
8127
+ }
8128
+ async function startNode(opts) {
8129
+ const { project, deps } = opts;
8130
+ if (!existsSync2(join3(project, "compose.yaml")))
8131
+ throw new Error("compose.yaml does not exist.");
8132
+ if (!existsSync2(join3(project, "config.json")))
8133
+ throw new Error("config.json does not exist. Run my-singbox gen-config first.");
8134
+ const ports = parseListenPorts(readFileSync3(join3(project, "config.json"), "utf8"));
8135
+ if (!await opts.dockerAvailable()) throw new Error("docker is not installed.");
8136
+ if ((await deps.run("docker", ["compose", "version"])).code)
8137
+ throw new Error("docker compose is not available.");
8138
+ if (!await opts.dockerDaemonUp()) throw new Error("Docker daemon is not running.");
8139
+ const envPath = join3(project, ".env");
8140
+ const secrets = secretValues(existsSync2(envPath) ? readFileSync3(envPath, "utf8") : "");
8141
+ const compose = (...args) => deps.run("docker", ["compose", "--project-directory", project, ...args]);
8142
+ const forward = (output) => {
8143
+ opts.stdout(redact(output.stdout, secrets));
8144
+ opts.stderr(redact(output.stderr, secrets));
8145
+ };
8146
+ const fail = async (message) => {
8147
+ const logs = await compose("logs", "--tail", "50", "sing-box");
8148
+ opts.stderr(redact(logs.stdout, secrets));
8149
+ opts.stderr(redact(logs.stderr, secrets));
8150
+ throw new Error(redact(message, secrets));
8151
+ };
8152
+ const status = async () => {
8153
+ const output = await deps.run("docker", ["inspect", "-f", "{{.State.Status}}", "sing-box"]);
8154
+ return output.code ? "missing" : output.stdout.trim();
8155
+ };
8156
+ opts.stdout("Starting sing-box...\n");
8157
+ const up = await compose("up", "-d");
8158
+ forward(up);
8159
+ if (up.code) throw new Error("docker compose up failed.");
8160
+ let state = "missing";
8161
+ for (let i = 0; i < 15; i++) {
8162
+ state = await status();
8163
+ if (state === "running") break;
8164
+ await deps.sleep(1e3);
8165
+ }
8166
+ if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
8167
+ const check = await compose("exec", "-T", "sing-box", "sing-box", "check", "-c", "/etc/sing-box/config.json");
8168
+ forward(check);
8169
+ if (check.code) await fail("sing-box config check failed.");
8170
+ for (const port of ports) {
8171
+ let open = false;
8172
+ for (let i = 0; i < 10; i++) {
8173
+ open = await deps.tcpOpen(port);
8174
+ if (open) break;
8175
+ await deps.sleep(1e3);
8176
+ }
8177
+ if (!open) await fail(`Port ${port} is not accepting connections.`);
8178
+ }
8179
+ if (ports.includes(8388)) {
8180
+ let listening = false;
8181
+ for (let i = 0; i < 10; i++) {
8182
+ const udp = await deps.run("ss", ["-uln"]);
8183
+ if (udp.code === 127) {
8184
+ opts.stderr("Warning: ss is not installed. Skipping UDP listener check for port 8388.\n");
8185
+ listening = true;
8186
+ break;
8187
+ }
8188
+ if (udp.code === 0 && /:8388\s/.test(udp.stdout)) {
8189
+ const owners = await deps.run("ss", ["-ulnp"]);
8190
+ const lines = owners.stdout.split("\n").filter((l) => /:8388\s/.test(l)).join("\n");
8191
+ if (lines.includes("users:") && !lines.includes("sing-box"))
8192
+ await fail("UDP port 8388 is not owned by sing-box.");
8193
+ listening = true;
8194
+ break;
8195
+ }
8196
+ await deps.sleep(1e3);
8197
+ }
8198
+ if (!listening) await fail("UDP port 8388 is not listening.");
8199
+ }
8200
+ state = await status();
8201
+ if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
8202
+ opts.stdout(`Self-check passed. Ports: ${ports.join(" ")}
8203
+ `);
8204
+ }
8041
8205
 
8042
8206
  // src/core/isp.ts
8043
- var import_yaml2 = __toESM(require_dist(), 1);
8044
8207
  import {
8045
8208
  chmodSync,
8046
8209
  chownSync,
8047
8210
  closeSync as closeSync2,
8048
8211
  copyFileSync,
8049
- existsSync as existsSync2,
8212
+ existsSync as existsSync3,
8050
8213
  fchmodSync as fchmodSync2,
8051
8214
  mkdirSync,
8052
8215
  openSync as openSync2,
8053
- readFileSync as readFileSync3,
8216
+ readFileSync as readFileSync4,
8054
8217
  readdirSync,
8055
8218
  renameSync as renameSync2,
8056
8219
  rmSync as rmSync2,
@@ -8058,14 +8221,13 @@ import {
8058
8221
  utimesSync,
8059
8222
  writeFileSync as writeFileSync2
8060
8223
  } from "node:fs";
8061
- import { join as join3 } from "node:path";
8224
+ import { join as join4 } from "node:path";
8062
8225
  import { randomBytes as randomBytes3 } from "node:crypto";
8063
- import { isDeepStrictEqual } from "node:util";
8064
8226
  var ISP_BACKUP_DIR = ".isp-backup";
8065
8227
  var ISP_LOCK_FILE = ".isp.lock";
8066
8228
  var ISP_TMP_PREFIX = ".isp-tmp.";
8067
8229
  var GROUP_MAIN = "Proxy";
8068
- var MANAGED_FILES = ["config.json", "clash.yaml", ".env"];
8230
+ var MANAGED_FILES = ["config.json", ".env"];
8069
8231
  var IspError = class extends Error {
8070
8232
  name = "IspError";
8071
8233
  };
@@ -8078,116 +8240,63 @@ function envHasIsp(text) {
8078
8240
  return /^ISP_SOCKS_[^=]*=/.test(line) || line === ENV_HEADER || line === ENV_HEADER_NOEOL;
8079
8241
  });
8080
8242
  }
8081
- function isRecord(value) {
8082
- return value !== null && typeof value === "object" && !Array.isArray(value);
8083
- }
8084
8243
  function checkIspConfig(config) {
8085
8244
  if (!isRecord(config)) throw new IspError("config.json is not valid JSON.");
8086
8245
  const inbounds = config.inbounds;
8087
- if (!Array.isArray(inbounds) || ![SS_TAG, VLESS_TAG].every((tag) => inbounds.some((i) => isRecord(i) && i.tag === tag)))
8246
+ if (!Array.isArray(inbounds) || !inbounds.every(isRecord) || ![SS_TAG, VLESS_TAG].every((tag) => inbounds.some((i) => i.tag === tag)))
8088
8247
  throw new IspError("config.json has no inbounds tagged ss-in and vless-reality-in.");
8089
- if (inbounds.some((i) => isRecord(i) && i.tag !== SS_ISP_TAG && i.tag !== VLESS_ISP_TAG && (i.listen_port === SS_ISP_PORT || i.listen_port === VLESS_ISP_PORT)))
8090
- throw new IspError(`Another inbound in config.json already uses port ${SS_ISP_PORT} or ${VLESS_ISP_PORT}.`);
8248
+ if (config.outbounds !== void 0 && (!Array.isArray(config.outbounds) || !config.outbounds.every(isRecord))) throw new IspError("config.json has invalid outbounds.");
8249
+ if (config.route !== void 0 && (!isRecord(config.route) || config.route.rules !== void 0 && !Array.isArray(config.route.rules)))
8250
+ throw new IspError("config.json has invalid route rules.");
8091
8251
  }
8092
8252
  function configHasIsp(config) {
8093
8253
  return [...config.inbounds ?? [], ...config.outbounds ?? []].some(
8094
8254
  (i) => isRecord(i) && [SS_ISP_TAG, VLESS_ISP_TAG, OUT_TAG].includes(i.tag)
8095
- );
8255
+ ) || (config.route?.rules ?? []).some((r) => isRecord(r) && r.outbound === OUT_TAG);
8096
8256
  }
8097
- function addIspToConfig(config, isp) {
8098
- checkIspConfig(config);
8257
+ function withoutLegacyListeners(config) {
8099
8258
  const next = structuredClone(config);
8100
- for (const [base, tag, port] of [
8101
- [SS_TAG, SS_ISP_TAG, SS_ISP_PORT],
8102
- [VLESS_TAG, VLESS_ISP_TAG, VLESS_ISP_PORT]
8103
- ]) {
8104
- const existing = next.inbounds.find((i) => i.tag === tag);
8105
- if (existing) existing.listen_port = port;
8106
- else next.inbounds.push({
8107
- ...structuredClone(next.inbounds.find((i) => i.tag === base)),
8108
- tag,
8109
- listen_port: port
8110
- });
8111
- }
8112
- const outbounds = next.outbounds ??= [];
8113
- const outbound = outbounds.find((o) => o.tag === OUT_TAG);
8114
- const fields = {
8115
- type: "socks",
8116
- tag: OUT_TAG,
8117
- server: isp.host,
8118
- server_port: isp.port,
8119
- version: "5",
8120
- username: isp.user,
8121
- password: isp.pass
8259
+ const tags = /* @__PURE__ */ new Set([SS_ISP_TAG, VLESS_ISP_TAG]);
8260
+ next.inbounds = next.inbounds.filter((inbound) => {
8261
+ if (!tags.has(inbound.tag)) return true;
8262
+ const ss = inbound.tag === SS_ISP_TAG;
8263
+ if (inbound.type !== (ss ? "shadowsocks" : "vless") || inbound.listen_port !== (ss ? SS_ISP_PORT : VLESS_ISP_PORT))
8264
+ throw new IspError("Legacy ISP inbound was customized. Migrate its tag and references manually first.");
8265
+ return false;
8266
+ });
8267
+ if (next.route?.rules) next.route.rules = next.route.rules.flatMap((rule) => {
8268
+ if (!isRecord(rule)) return [rule];
8269
+ const inbound = typeof rule.inbound === "string" ? [rule.inbound] : rule.inbound;
8270
+ if (!Array.isArray(inbound) || !inbound.some((tag) => tags.has(tag))) return [rule];
8271
+ const remaining = inbound.filter((tag) => !tags.has(tag));
8272
+ if (remaining.length) return [{ ...rule, inbound: remaining }];
8273
+ if (isLegacyIspRule(rule)) return [];
8274
+ throw new IspError("A custom route depends on legacy ISP inbounds. Migrate it manually first.");
8275
+ });
8276
+ const check = (value, key = "") => {
8277
+ if (typeof value === "string" && tags.has(value) && ["inbound", "detour"].includes(key))
8278
+ throw new IspError("A custom reference depends on legacy ISP inbounds. Migrate it manually first.");
8279
+ if (Array.isArray(value)) value.forEach((item) => check(item, key));
8280
+ else if (isRecord(value)) Object.entries(value).forEach(([name, item]) => check(item, name));
8122
8281
  };
8123
- if (outbound) Object.assign(outbound, fields);
8124
- else outbounds.push(fields);
8125
- const rules = (next.route ??= {}).rules ??= [];
8126
- const rule = rules.find((r) => isRecord(r) && r.outbound === OUT_TAG);
8127
- const inbound = [SS_ISP_TAG, VLESS_ISP_TAG];
8128
- if (isRecord(rule)) {
8129
- if (!isDeepStrictEqual(rule.inbound, inbound)) rule.inbound = inbound;
8130
- } else rules.push({ inbound, outbound: OUT_TAG });
8282
+ check(next);
8131
8283
  return next;
8132
8284
  }
8133
- function clashHasIsp(text) {
8134
- if (/^\s*#.*\bmy-singbox-isp\b/m.test(text)) return true;
8135
- const doc = (0, import_yaml2.parseDocument)(text);
8136
- const proxies = doc.get("proxies", true);
8137
- return (0, import_yaml2.isSeq)(proxies) && proxies.items.some((p) => (0, import_yaml2.isMap)(p) && [NODE_SS_ISP, NODE_VLESS_ISP].includes(p.get("name")));
8285
+ function addIspToConfig(config, isp, routeDefaults) {
8286
+ checkIspConfig(config);
8287
+ const next = withoutLegacyListeners(config);
8288
+ upsertIspOutbound(next, isp);
8289
+ ensureRoute(next, routeDefaults);
8290
+ return next;
8138
8291
  }
8139
- function addIspToClash(text, realityPrivateKey) {
8140
- const doc = (0, import_yaml2.parseDocument)(text);
8141
- if (doc.errors.length) throw new IspError("clash.yaml is not valid YAML.");
8142
- const proxies = doc.get("proxies", true);
8143
- const missing = `clash.yaml has no usable node named "${NODE_SS}" and "${NODE_VLESS}". It was probably generated by an older version (My-sing-box / My-VLESS-Reality). Delete clash.yaml, run scripts/conf-clash.sh to regenerate it, re-import it in your clients, then re-run this script.`;
8144
- if (!(0, import_yaml2.isSeq)(proxies)) throw new IspError(missing);
8145
- let changed = false;
8146
- for (const [base, name, basePort, port] of [
8147
- [NODE_SS, NODE_SS_ISP, SS_PORT, SS_ISP_PORT],
8148
- [NODE_VLESS, NODE_VLESS_ISP, VLESS_PORT, VLESS_ISP_PORT]
8149
- ]) {
8150
- const proto = proxies.items.find((p) => (0, import_yaml2.isMap)(p) && p.get("name") === base);
8151
- if (!(0, import_yaml2.isMap)(proto) || proto.get("port") !== basePort) throw new IspError(missing);
8152
- const existing = proxies.items.find((p) => (0, import_yaml2.isMap)(p) && p.get("name") === name);
8153
- if ((0, import_yaml2.isMap)(existing)) {
8154
- if (existing.get("port") !== port) {
8155
- existing.set("port", port);
8156
- changed = true;
8157
- }
8158
- } else {
8159
- const clone = proto.clone();
8160
- (0, import_yaml2.visit)(clone, (_key, node) => {
8161
- if ((0, import_yaml2.isMap)(node) || (0, import_yaml2.isSeq)(node) || (0, import_yaml2.isScalar)(node)) {
8162
- delete node.comment;
8163
- delete node.commentBefore;
8164
- delete node.spaceBefore;
8165
- }
8166
- });
8167
- clone.set("name", name);
8168
- clone.set("port", port);
8169
- proxies.add(clone);
8170
- changed = true;
8171
- }
8172
- }
8173
- const groups = doc.get("proxy-groups", true);
8174
- const group = (0, import_yaml2.isSeq)(groups) ? groups.items.find((g) => (0, import_yaml2.isMap)(g) && g.get("name") === GROUP_MAIN) : void 0;
8175
- const list = (0, import_yaml2.isMap)(group) ? group.get("proxies", true) : void 0;
8176
- if (!(0, import_yaml2.isSeq)(list))
8177
- throw new IspError(`clash.yaml has no '${GROUP_MAIN}' proxy group with a 'proxies:' list in the expected format (generated by scripts/conf-clash.sh). Regenerate clash.yaml, re-import it in your clients, then re-run this script.`);
8178
- for (const name of [NODE_SS_ISP, NODE_VLESS_ISP]) {
8179
- if (!list.items.some((item) => (0, import_yaml2.isScalar)(item) && item.value === name)) {
8180
- list.add(name);
8181
- changed = true;
8182
- }
8183
- }
8184
- const result2 = changed ? doc.toString({ lineWidth: 0 }) : text;
8185
- if (result2.includes("REALITY_PRIVATE_KEY") || realityPrivateKey && result2.includes(realityPrivateKey))
8186
- throw new IspError("Refusing to write REALITY_PRIVATE_KEY into clash.yaml.");
8187
- return result2;
8292
+ function removeIspFromConfig(config) {
8293
+ checkIspConfig(config);
8294
+ const next = withoutLegacyListeners(config);
8295
+ omitIsp(next);
8296
+ return next;
8188
8297
  }
8189
8298
  function makeTmpPath(projectDir) {
8190
- return join3(projectDir, ISP_TMP_PREFIX + randomBytes3(16).toString("hex"));
8299
+ return join4(projectDir, ISP_TMP_PREFIX + randomBytes3(16).toString("hex"));
8191
8300
  }
8192
8301
  function installFile(dest, data, projectDir) {
8193
8302
  const tmp = makeTmpPath(projectDir);
@@ -8199,7 +8308,7 @@ function installFile(dest, data, projectDir) {
8199
8308
  } finally {
8200
8309
  closeSync2(fd);
8201
8310
  }
8202
- if (process.getuid?.() === 0 && existsSync2(dest)) {
8311
+ if (process.getuid?.() === 0 && existsSync3(dest)) {
8203
8312
  const { uid, gid } = statSync(dest);
8204
8313
  chownSync(tmp, uid, gid);
8205
8314
  }
@@ -8216,59 +8325,43 @@ function copyPreserved(src, dest) {
8216
8325
  utimesSync(dest, info.atimeMs / 1e3, info.mtimeMs / 1e3);
8217
8326
  }
8218
8327
  function backupDirs(projectDir) {
8219
- const root = join3(projectDir, ISP_BACKUP_DIR);
8220
- return existsSync2(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join3(root, entry.name)).sort() : [];
8328
+ const root = join4(projectDir, ISP_BACKUP_DIR);
8329
+ return existsSync3(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join4(root, entry.name)).sort() : [];
8221
8330
  }
8222
8331
  var lastStamp = 0n;
8223
- function makeBackup(projectDir) {
8332
+ function makeBackup(projectDir, files = MANAGED_FILES) {
8224
8333
  const now = BigInt(Date.now()) * 1000000n;
8225
8334
  lastStamp = now > lastStamp ? now : lastStamp + 1n;
8226
8335
  const d = new Date(Number(lastStamp / 1000000n));
8227
8336
  const p2 = (n) => String(n).padStart(2, "0");
8228
8337
  const stamp = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}-${(lastStamp % 1000000000n).toString().padStart(9, "0")}-${process.pid}`;
8229
- const root = join3(projectDir, ISP_BACKUP_DIR), dir = join3(root, stamp);
8338
+ const root = join4(projectDir, ISP_BACKUP_DIR), dir = join4(root, stamp);
8230
8339
  mkdirSync(root, { recursive: true, mode: 448 });
8231
- mkdirSync(dir, { mode: 448 });
8232
8340
  chmodSync(root, 448);
8233
- chmodSync(dir, 448);
8234
- for (const file of MANAGED_FILES) {
8235
- const src = join3(projectDir, file);
8236
- if (existsSync2(src) && statSync(src).isFile()) copyPreserved(src, join3(dir, file));
8341
+ mkdirSync(dir, { mode: 448 });
8342
+ for (const file of files) {
8343
+ const src = join4(projectDir, file);
8344
+ if (existsSync3(src) && statSync(src).isFile()) copyPreserved(src, join4(dir, file));
8237
8345
  }
8238
8346
  const all = backupDirs(projectDir);
8239
- for (const old of all.slice(1, Math.max(1, all.length - 4)))
8240
- rmSync2(old, { recursive: true });
8347
+ for (const old of all.slice(1, Math.max(1, all.length - 4))) rmSync2(old, { recursive: true });
8241
8348
  return dir;
8242
8349
  }
8243
- function findCleanBackup(projectDir) {
8244
- return backupDirs(projectDir).reverse().find((dir) => MANAGED_FILES.every((file) => {
8245
- const path = join3(dir, file);
8246
- if (!existsSync2(path)) return true;
8247
- const text = readFileSync3(path, "utf8");
8248
- if (file === ".env") return !envHasIsp(text);
8249
- if (file === "clash.yaml") return !clashHasIsp(text);
8250
- try {
8251
- return !configHasIsp(JSON.parse(text));
8252
- } catch {
8253
- return false;
8254
- }
8255
- }));
8256
- }
8257
- function restoreBackup(projectDir, backupDir) {
8258
- for (const file of MANAGED_FILES) {
8259
- const src = join3(backupDir, file);
8260
- if (existsSync2(src) && statSync(src).isFile()) copyPreserved(src, join3(projectDir, file));
8350
+ function restoreBackup(projectDir, backupDir, files = MANAGED_FILES) {
8351
+ for (const file of files) {
8352
+ const src = join4(backupDir, file), dest = join4(projectDir, file);
8353
+ if (existsSync3(src) && statSync(src).isFile()) copyPreserved(src, dest);
8261
8354
  }
8262
8355
  }
8263
8356
  function sweepStaleTmp(projectDir) {
8264
8357
  for (const entry of readdirSync(projectDir, { withFileTypes: true }))
8265
8358
  if (entry.name.startsWith(ISP_TMP_PREFIX) && entry.isFile())
8266
- rmSync2(join3(projectDir, entry.name));
8359
+ rmSync2(join4(projectDir, entry.name));
8267
8360
  }
8268
8361
  function lockState(path) {
8269
8362
  let pid;
8270
8363
  try {
8271
- pid = Number(readFileSync3(path, "utf8").trim());
8364
+ pid = Number(readFileSync4(path, "utf8").trim());
8272
8365
  } catch (error) {
8273
8366
  if (error.code === "ENOENT") return "absent";
8274
8367
  throw error;
@@ -8302,7 +8395,7 @@ function claimLock(path) {
8302
8395
  }
8303
8396
  }
8304
8397
  async function withLock(projectDir, fn) {
8305
- const path = join3(projectDir, ISP_LOCK_FILE);
8398
+ const path = join4(projectDir, ISP_LOCK_FILE);
8306
8399
  claimLock(path);
8307
8400
  try {
8308
8401
  sweepStaleTmp(projectDir);
@@ -8456,22 +8549,7 @@ var IspService = class {
8456
8549
  }
8457
8550
  return false;
8458
8551
  }
8459
- async portsFree() {
8460
- const probe = await this.command("ss", ["-H", "-ltn"]);
8461
- if (probe.missing) {
8462
- this.warn(`ss is not installed. Skipping the check that ports ${SS_ISP_PORT} and ${VLESS_ISP_PORT} are free.`);
8463
- return;
8464
- }
8465
- for (const [proto, port] of [
8466
- ["tcp", SS_ISP_PORT],
8467
- ["udp", SS_ISP_PORT],
8468
- ["tcp", VLESS_ISP_PORT]
8469
- ]) {
8470
- if (await this.portListening(proto, port))
8471
- throw new IspError(`Port ${port}/${proto} is already in use by another process.`);
8472
- }
8473
- }
8474
- async verifyService(expectIsp) {
8552
+ async verifyService() {
8475
8553
  let status = "missing";
8476
8554
  for (let i = 0; i < 15; i++) {
8477
8555
  status = await this.containerStatus();
@@ -8486,16 +8564,15 @@ var IspService = class {
8486
8564
  this.warn("sing-box config check failed inside the container.");
8487
8565
  return false;
8488
8566
  }
8489
- for (const port of [SS_PORT, VLESS_PORT, ...expectIsp ? [SS_ISP_PORT, VLESS_ISP_PORT] : []]) {
8490
- if (!await this.waitListening("tcp", port)) {
8491
- this.warn(`Port ${port}/tcp is not accepting connections.`);
8567
+ const listeners = firewallPorts(readFileSync5(join5(this.io.projectDir, "config.json"), "utf8"));
8568
+ for (const listener of new Set(listeners)) {
8569
+ const [number, proto] = listener.split("/");
8570
+ const port = Number(number);
8571
+ if (!await this.waitListening(proto, port)) {
8572
+ this.warn(`Port ${port}/${proto} is not listening.`);
8492
8573
  return false;
8493
8574
  }
8494
8575
  }
8495
- if (expectIsp && !await this.waitListening("udp", SS_ISP_PORT)) {
8496
- this.warn(`Port ${SS_ISP_PORT}/udp is not listening.`);
8497
- return false;
8498
- }
8499
8576
  return true;
8500
8577
  }
8501
8578
  async socksEgressIp(isp) {
@@ -8525,140 +8602,6 @@ max-time = 15
8525
8602
  return void 0;
8526
8603
  }
8527
8604
  };
8528
-
8529
- // src/core/start.ts
8530
- import { execFile as execFile2 } from "node:child_process";
8531
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
8532
- import { connect } from "node:net";
8533
- import { join as join4 } from "node:path";
8534
- function parseListenPorts(configText) {
8535
- const ports = /* @__PURE__ */ new Set();
8536
- for (const m of configText.matchAll(/"listen_port"\s*:\s*([0-9]+)/g)) {
8537
- const port = Number(m[1]);
8538
- if (port < 1 || port > 65535)
8539
- throw new Error(`Invalid listen_port in config.json: ${m[1]}`);
8540
- ports.add(port);
8541
- }
8542
- if (!ports.size) throw new Error("config.json has no listen_port.");
8543
- return [...ports];
8544
- }
8545
- function secretValues(envText) {
8546
- return [...envText.matchAll(/^(?:REALITY_PRIVATE_KEY|SS_PASSWORD|VLESS_UUID|ISP_SOCKS_USER|ISP_SOCKS_PASS)=([^\r\n]*)\r?$/gm)].map((m) => envUnquote(m[1])).filter((value) => value !== null && value !== "");
8547
- }
8548
- function redact(text, secrets) {
8549
- for (const secret of [...secrets].sort((a, b) => b.length - a.length))
8550
- text = text.replaceAll(secret, "***");
8551
- return text;
8552
- }
8553
- function composeImageTag(text) {
8554
- const m = /^\s*image:\s*ghcr\.io\/sagernet\/sing-box:([^\s#]+)/m.exec(text);
8555
- if (!m) throw new Error("sing-box image tag not found in compose.yaml.");
8556
- return m[1];
8557
- }
8558
- function firewallPorts(configText) {
8559
- const config = JSON.parse(configText);
8560
- return config.inbounds.flatMap(
8561
- (i) => i.type === "shadowsocks" ? [`${i.listen_port}/tcp`, `${i.listen_port}/udp`] : [`${i.listen_port}/tcp`]
8562
- );
8563
- }
8564
- function realStartDeps() {
8565
- return {
8566
- run: (file, args) => new Promise((resolve) => {
8567
- execFile2(file, args, (error, stdout, stderr) => {
8568
- const code = error ? typeof error.code === "number" ? error.code : error.code === "ENOENT" ? 127 : 1 : 0;
8569
- resolve({ code, stdout, stderr });
8570
- });
8571
- }),
8572
- tcpOpen: (port) => new Promise((resolve) => {
8573
- const socket = connect(port, "127.0.0.1");
8574
- const finish = (open) => {
8575
- socket.destroy();
8576
- resolve(open);
8577
- };
8578
- socket.setTimeout(500, () => finish(false));
8579
- socket.once("connect", () => finish(true));
8580
- socket.once("error", () => finish(false));
8581
- }),
8582
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
8583
- };
8584
- }
8585
- async function startNode(opts) {
8586
- const { project, deps } = opts;
8587
- if (!existsSync3(join4(project, "compose.yaml")))
8588
- throw new Error("compose.yaml does not exist.");
8589
- if (!existsSync3(join4(project, "config.json")))
8590
- throw new Error("config.json does not exist. Run my-singbox gen-config first.");
8591
- const ports = parseListenPorts(readFileSync4(join4(project, "config.json"), "utf8"));
8592
- if (!await opts.dockerAvailable()) throw new Error("docker is not installed.");
8593
- if ((await deps.run("docker", ["compose", "version"])).code)
8594
- throw new Error("docker compose is not available.");
8595
- if (!await opts.dockerDaemonUp()) throw new Error("Docker daemon is not running.");
8596
- const envPath = join4(project, ".env");
8597
- const secrets = secretValues(existsSync3(envPath) ? readFileSync4(envPath, "utf8") : "");
8598
- const compose = (...args) => deps.run("docker", ["compose", "--project-directory", project, ...args]);
8599
- const forward = (output) => {
8600
- opts.stdout(redact(output.stdout, secrets));
8601
- opts.stderr(redact(output.stderr, secrets));
8602
- };
8603
- const fail = async (message) => {
8604
- const logs = await compose("logs", "--tail", "50", "sing-box");
8605
- opts.stderr(redact(logs.stdout, secrets));
8606
- opts.stderr(redact(logs.stderr, secrets));
8607
- throw new Error(redact(message, secrets));
8608
- };
8609
- const status = async () => {
8610
- const output = await deps.run("docker", ["inspect", "-f", "{{.State.Status}}", "sing-box"]);
8611
- return output.code ? "missing" : output.stdout.trim();
8612
- };
8613
- opts.stdout("Starting sing-box...\n");
8614
- const up = await compose("up", "-d");
8615
- forward(up);
8616
- if (up.code) throw new Error("docker compose up failed.");
8617
- let state = "missing";
8618
- for (let i = 0; i < 15; i++) {
8619
- state = await status();
8620
- if (state === "running") break;
8621
- await deps.sleep(1e3);
8622
- }
8623
- if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
8624
- const check = await compose("exec", "-T", "sing-box", "sing-box", "check", "-c", "/etc/sing-box/config.json");
8625
- forward(check);
8626
- if (check.code) await fail("sing-box config check failed.");
8627
- for (const port of ports) {
8628
- let open = false;
8629
- for (let i = 0; i < 10; i++) {
8630
- open = await deps.tcpOpen(port);
8631
- if (open) break;
8632
- await deps.sleep(1e3);
8633
- }
8634
- if (!open) await fail(`Port ${port} is not accepting connections.`);
8635
- }
8636
- if (ports.includes(8388)) {
8637
- let listening = false;
8638
- for (let i = 0; i < 10; i++) {
8639
- const udp = await deps.run("ss", ["-uln"]);
8640
- if (udp.code === 127) {
8641
- opts.stderr("Warning: ss is not installed. Skipping UDP listener check for port 8388.\n");
8642
- listening = true;
8643
- break;
8644
- }
8645
- if (udp.code === 0 && /:8388\s/.test(udp.stdout)) {
8646
- const owners = await deps.run("ss", ["-ulnp"]);
8647
- const lines = owners.stdout.split("\n").filter((l) => /:8388\s/.test(l)).join("\n");
8648
- if (lines.includes("users:") && !lines.includes("sing-box"))
8649
- await fail("UDP port 8388 is not owned by sing-box.");
8650
- listening = true;
8651
- break;
8652
- }
8653
- await deps.sleep(1e3);
8654
- }
8655
- if (!listening) await fail("UDP port 8388 is not listening.");
8656
- }
8657
- state = await status();
8658
- if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
8659
- opts.stdout(`Self-check passed. Ports: ${ports.join(" ")}
8660
- `);
8661
- }
8662
8605
  export {
8663
8606
  ENV_HEADER,
8664
8607
  ENV_HEADER_NOEOL,
@@ -8691,7 +8634,6 @@ export {
8691
8634
  VLESS_ISP_TAG,
8692
8635
  VLESS_PORT,
8693
8636
  VLESS_TAG,
8694
- addIspToClash,
8695
8637
  addIspToConfig,
8696
8638
  buildModel,
8697
8639
  checkArch,
@@ -8700,7 +8642,6 @@ export {
8700
8642
  checkNode,
8701
8643
  checkPlatform,
8702
8644
  checkSocket,
8703
- clashHasIsp,
8704
8645
  composeImageTag,
8705
8646
  configHasIsp,
8706
8647
  defaultKeyDeps,
@@ -8714,7 +8655,6 @@ export {
8714
8655
  envUnquote,
8715
8656
  envWithIsp,
8716
8657
  envWithoutIsp,
8717
- findCleanBackup,
8718
8658
  findPackageFile,
8719
8659
  findTemplate,
8720
8660
  firewallPorts,
@@ -8736,6 +8676,7 @@ export {
8736
8676
  readTemplate,
8737
8677
  realStartDeps,
8738
8678
  redact,
8679
+ removeIspFromConfig,
8739
8680
  renderClash,
8740
8681
  renderGeneratedEnv,
8741
8682
  renderSingbox,