@mojir/dvala 0.0.15 → 0.0.17

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.
Files changed (2) hide show
  1. package/dist/cli/cli.js +436 -14
  2. package/package.json +3 -1
package/dist/cli/cli.js CHANGED
@@ -118,7 +118,8 @@ const NodeTypes = {
118
118
  SpecialBuiltinSymbol: 7,
119
119
  ReservedSymbol: 8,
120
120
  Binding: 9,
121
- Spread: 10
121
+ Spread: 10,
122
+ TemplateString: 11
122
123
  };
123
124
  const NodeTypesSet = new Set(Object.values(NodeTypes));
124
125
  function getNodeTypeName(type) {
@@ -477,7 +478,7 @@ function findAllOccurrences(input, pattern) {
477
478
  }
478
479
  //#endregion
479
480
  //#region package.json
480
- var version = "0.0.15";
481
+ var version = "0.0.17";
481
482
  //#endregion
482
483
  //#region src/typeGuards/string.ts
483
484
  function isString(value, options = {}) {
@@ -24481,6 +24482,128 @@ const standardEffects = {
24481
24482
  "-effect-dvala.io.read-stdin",
24482
24483
  "-effect-dvala.io.print",
24483
24484
  "-effect-dvala.io.println",
24485
+ "-effect-dvala.io.pick",
24486
+ "-effect-dvala.io.confirm",
24487
+ "perform",
24488
+ "effect"
24489
+ ]
24490
+ }
24491
+ },
24492
+ "dvala.io.pick": {
24493
+ handler: (args, k, sourceCodeInfo) => {
24494
+ const items = args[0];
24495
+ const options = args[1];
24496
+ if (!Array.isArray(items)) throw new DvalaError(`dvala.io.pick: first argument must be an array, got ${typeof items}`, sourceCodeInfo);
24497
+ if (items.length === 0) throw new DvalaError("dvala.io.pick: items array must not be empty", sourceCodeInfo);
24498
+ for (let i = 0; i < items.length; i++) if (typeof items[i] !== "string") throw new DvalaError(`dvala.io.pick: items[${i}] must be a string, got ${typeof items[i]}`, sourceCodeInfo);
24499
+ let promptMessage;
24500
+ let defaultIndex;
24501
+ if (options !== void 0) {
24502
+ if (typeof options !== "object" || options === null || Array.isArray(options)) throw new DvalaError(`dvala.io.pick: second argument must be an object, got ${typeof options}`, sourceCodeInfo);
24503
+ const opts = options;
24504
+ if (opts["prompt"] !== void 0) {
24505
+ if (typeof opts["prompt"] !== "string") throw new DvalaError("dvala.io.pick: options.prompt must be a string", sourceCodeInfo);
24506
+ promptMessage = opts["prompt"];
24507
+ }
24508
+ if (opts["default"] !== void 0) {
24509
+ if (typeof opts["default"] !== "number" || !Number.isInteger(opts["default"])) throw new DvalaError("dvala.io.pick: options.default must be an integer", sourceCodeInfo);
24510
+ defaultIndex = opts["default"];
24511
+ if (defaultIndex < 0 || defaultIndex >= items.length) throw new DvalaError(`dvala.io.pick: options.default (${defaultIndex}) is out of bounds for array of length ${items.length}`, sourceCodeInfo);
24512
+ }
24513
+ }
24514
+ if (typeof globalThis.prompt === "function") {
24515
+ const listLines = items.map((item, i) => `${i}: ${item}`).join("\n");
24516
+ const message = `${promptMessage ?? "Choose an item:"}${defaultIndex !== void 0 ? ` [default: ${defaultIndex}]` : ""}\n${listLines}`;
24517
+ const result = globalThis.prompt(message);
24518
+ if (result === null) return {
24519
+ type: "Value",
24520
+ value: null,
24521
+ k
24522
+ };
24523
+ const trimmed = result.trim();
24524
+ if (trimmed === "") return {
24525
+ type: "Value",
24526
+ value: defaultIndex !== void 0 ? defaultIndex : null,
24527
+ k
24528
+ };
24529
+ const parsed = Number(trimmed);
24530
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed >= items.length) throw new DvalaError(`dvala.io.pick: invalid selection "${trimmed}"`, sourceCodeInfo);
24531
+ return {
24532
+ type: "Value",
24533
+ value: parsed,
24534
+ k
24535
+ };
24536
+ }
24537
+ throw new DvalaError("dvala.io.pick is not supported in this environment. In Node.js, register a \"dvala.io.pick\" host handler.", sourceCodeInfo);
24538
+ },
24539
+ arity: {
24540
+ min: 1,
24541
+ max: 2
24542
+ },
24543
+ docs: {
24544
+ category: "effect",
24545
+ description: "Presents a numbered list of items and asks the user to choose one. In browsers uses `window.prompt()`. In Node.js, register a host handler. Resumes with the index of the chosen item, or `null` if the user cancels.",
24546
+ returns: { type: ["integer", "null"] },
24547
+ args: {
24548
+ items: {
24549
+ type: "array",
24550
+ description: "Non-empty array of strings to display."
24551
+ },
24552
+ options: {
24553
+ type: "object",
24554
+ description: "Optional settings: `prompt` (string label) and `default` (integer index to use when the user submits an empty input)."
24555
+ }
24556
+ },
24557
+ variants: [{ argumentNames: ["items"] }, { argumentNames: ["items", "options"] }],
24558
+ examples: ["effect(dvala.io.pick)"],
24559
+ seeAlso: [
24560
+ "-effect-dvala.io.read-line",
24561
+ "-effect-dvala.io.confirm",
24562
+ "perform",
24563
+ "effect"
24564
+ ]
24565
+ }
24566
+ },
24567
+ "dvala.io.confirm": {
24568
+ handler: (args, k, sourceCodeInfo) => {
24569
+ const question = args[0];
24570
+ const options = args[1];
24571
+ if (typeof question !== "string") throw new DvalaError(`dvala.io.confirm: first argument must be a string, got ${typeof question}`, sourceCodeInfo);
24572
+ if (options !== void 0) {
24573
+ if (typeof options !== "object" || options === null || Array.isArray(options)) throw new DvalaError(`dvala.io.confirm: second argument must be an object, got ${typeof options}`, sourceCodeInfo);
24574
+ const opts = options;
24575
+ if (opts["default"] !== void 0 && typeof opts["default"] !== "boolean") throw new DvalaError("dvala.io.confirm: options.default must be a boolean", sourceCodeInfo);
24576
+ }
24577
+ if (typeof globalThis.confirm === "function") return {
24578
+ type: "Value",
24579
+ value: globalThis.confirm(question),
24580
+ k
24581
+ };
24582
+ throw new DvalaError("dvala.io.confirm is not supported in this environment. In Node.js, register a \"dvala.io.confirm\" host handler.", sourceCodeInfo);
24583
+ },
24584
+ arity: {
24585
+ min: 1,
24586
+ max: 2
24587
+ },
24588
+ docs: {
24589
+ category: "effect",
24590
+ description: "Asks the user a yes/no question. In browsers uses `window.confirm()` and returns `true` (OK) or `false` (Cancel). In Node.js, register a host handler. The optional `default` hints the preferred answer to host handlers (e.g. for rendering `[Y/n]` in a CLI), but has no effect on the browser implementation.",
24591
+ returns: { type: "boolean" },
24592
+ args: {
24593
+ question: {
24594
+ type: "string",
24595
+ description: "The yes/no question to present."
24596
+ },
24597
+ options: {
24598
+ type: "object",
24599
+ description: "Optional settings: `default` (boolean, hints the preferred answer for host handlers)."
24600
+ }
24601
+ },
24602
+ variants: [{ argumentNames: ["question"] }, { argumentNames: ["question", "options"] }],
24603
+ examples: ["effect(dvala.io.confirm)"],
24604
+ seeAlso: [
24605
+ "-effect-dvala.io.read-line",
24606
+ "-effect-dvala.io.pick",
24484
24607
  "perform",
24485
24608
  "effect"
24486
24609
  ]
@@ -25688,6 +25811,94 @@ const tokenizeShebang = (input, position) => {
25688
25811
  }
25689
25812
  return NO_MATCH;
25690
25813
  };
25814
+ const tokenizeTemplateString = (input, position) => {
25815
+ if (input[position] !== "`") return NO_MATCH;
25816
+ let value = "`";
25817
+ let length = 1;
25818
+ while (position + length < input.length) {
25819
+ const char = input[position + length];
25820
+ if (char === "`") {
25821
+ value += "`";
25822
+ length += 1;
25823
+ return [length, ["TemplateString", value]];
25824
+ }
25825
+ if (char === "$" && input[position + length + 1] === "{") {
25826
+ value += "${";
25827
+ length += 2;
25828
+ let braceDepth = 1;
25829
+ while (position + length < input.length && braceDepth > 0) {
25830
+ const c = input[position + length];
25831
+ if (c === "{") {
25832
+ braceDepth += 1;
25833
+ value += c;
25834
+ length += 1;
25835
+ } else if (c === "}") {
25836
+ braceDepth -= 1;
25837
+ value += c;
25838
+ length += 1;
25839
+ } else if (c === "\"") {
25840
+ value += c;
25841
+ length += 1;
25842
+ let escaping = false;
25843
+ while (position + length < input.length) {
25844
+ const sc = input[position + length];
25845
+ value += sc;
25846
+ length += 1;
25847
+ if (escaping) escaping = false;
25848
+ else if (sc === "\\") escaping = true;
25849
+ else if (sc === "\"") break;
25850
+ }
25851
+ } else if (c === "'") {
25852
+ value += c;
25853
+ length += 1;
25854
+ let escaping = false;
25855
+ while (position + length < input.length) {
25856
+ const sc = input[position + length];
25857
+ value += sc;
25858
+ length += 1;
25859
+ if (escaping) escaping = false;
25860
+ else if (sc === "\\") escaping = true;
25861
+ else if (sc === "'") break;
25862
+ }
25863
+ } else if (c === "`") {
25864
+ const [nestedLength, nestedToken] = tokenizeTemplateString(input, position + length);
25865
+ if (nestedLength === 0 || !nestedToken) return [length, [
25866
+ "Error",
25867
+ value,
25868
+ void 0,
25869
+ `Unclosed nested template string at position ${position + length}`
25870
+ ]];
25871
+ if (nestedToken[0] === "Error") return [length + nestedLength, [
25872
+ "Error",
25873
+ value + nestedToken[1],
25874
+ void 0,
25875
+ nestedToken[3]
25876
+ ]];
25877
+ value += nestedToken[1];
25878
+ length += nestedLength;
25879
+ } else {
25880
+ value += c;
25881
+ length += 1;
25882
+ }
25883
+ }
25884
+ if (braceDepth > 0) return [length, [
25885
+ "Error",
25886
+ value,
25887
+ void 0,
25888
+ `Unclosed interpolation in template string at position ${position}`
25889
+ ]];
25890
+ } else {
25891
+ value += char;
25892
+ length += 1;
25893
+ }
25894
+ }
25895
+ return [length, [
25896
+ "Error",
25897
+ value,
25898
+ void 0,
25899
+ `Unclosed template string at position ${position}`
25900
+ ]];
25901
+ };
25691
25902
  const tokenizeSingleLineComment = (input, position) => {
25692
25903
  if (input[position] === "/" && input[position + 1] === "/") {
25693
25904
  let length = 2;
@@ -25712,6 +25923,7 @@ const tokenizers = [
25712
25923
  tokenizeLBrace,
25713
25924
  tokenizeRBrace,
25714
25925
  tokenizeString,
25926
+ tokenizeTemplateString,
25715
25927
  tokenizeRegexpShorthand,
25716
25928
  tokenizeBasePrefixedNumber,
25717
25929
  tokenizeNumber,
@@ -26506,6 +26718,11 @@ function findUnresolvedSymbolsInNode(node, contextStack, builtin) {
26506
26718
  });
26507
26719
  }
26508
26720
  case NodeTypes.Spread: return findUnresolvedSymbolsInNode(node[1], contextStack, builtin);
26721
+ case NodeTypes.TemplateString: {
26722
+ const unresolvedSymbols = /* @__PURE__ */ new Set();
26723
+ for (const segment of node[1]) findUnresolvedSymbolsInNode(segment, contextStack, builtin)?.forEach((symbol) => unresolvedSymbols.add(symbol));
26724
+ return unresolvedSymbols;
26725
+ }
26509
26726
  default: throw new DvalaError(`Unhandled node type: ${nodeType}`, node[2]);
26510
26727
  }
26511
26728
  }
@@ -27440,6 +27657,176 @@ function parseObject(ctx) {
27440
27657
  return withSourceCodeInfo([NodeTypes.SpecialExpression, [specialExpressionTypes.object, params]], firstToken[2]);
27441
27658
  }
27442
27659
  //#endregion
27660
+ //#region src/tokenizer/minifyTokenStream.ts
27661
+ function minifyTokenStream(tokenStream, { removeWhiteSpace }) {
27662
+ const tokens = tokenStream.tokens.filter((token) => {
27663
+ if (isSingleLineCommentToken(token) || isMultiLineCommentToken(token) || isShebangToken(token) || removeWhiteSpace && isWhitespaceToken(token)) return false;
27664
+ return true;
27665
+ });
27666
+ return {
27667
+ ...tokenStream,
27668
+ tokens
27669
+ };
27670
+ }
27671
+ //#endregion
27672
+ //#region src/parser/subParsers/parseTemplateString.ts
27673
+ /**
27674
+ * Scan from `start` inside a `${...}` interpolation until the matching `}`.
27675
+ * Returns the expression source text (without the outer `${` and `}`) and
27676
+ * the number of characters consumed (including the closing `}`).
27677
+ */
27678
+ function scanExpression(raw, start) {
27679
+ let i = start;
27680
+ let expr = "";
27681
+ let depth = 1;
27682
+ while (i < raw.length && depth > 0) {
27683
+ const c = raw[i];
27684
+ if (c === "{") {
27685
+ depth++;
27686
+ expr += c;
27687
+ i++;
27688
+ } else if (c === "}") {
27689
+ depth--;
27690
+ if (depth > 0) expr += c;
27691
+ i++;
27692
+ } else if (c === "\"") {
27693
+ const { str, consumed } = scanString(raw, i);
27694
+ expr += str;
27695
+ i += consumed;
27696
+ } else if (c === "'") {
27697
+ const { str, consumed } = scanQuotedSymbol(raw, i);
27698
+ expr += str;
27699
+ i += consumed;
27700
+ } else if (c === "`") {
27701
+ const { str, consumed } = scanNestedTemplate(raw, i);
27702
+ expr += str;
27703
+ i += consumed;
27704
+ } else {
27705
+ expr += c;
27706
+ i++;
27707
+ }
27708
+ }
27709
+ return {
27710
+ expr,
27711
+ consumed: i - start
27712
+ };
27713
+ }
27714
+ function scanString(raw, start) {
27715
+ let i = start + 1;
27716
+ let str = "\"";
27717
+ let escaping = false;
27718
+ while (i < raw.length) {
27719
+ const c = raw[i];
27720
+ str += c;
27721
+ i++;
27722
+ if (escaping) escaping = false;
27723
+ else if (c === "\\") escaping = true;
27724
+ else if (c === "\"") break;
27725
+ }
27726
+ return {
27727
+ str,
27728
+ consumed: i - start
27729
+ };
27730
+ }
27731
+ function scanQuotedSymbol(raw, start) {
27732
+ let i = start + 1;
27733
+ let str = "'";
27734
+ let escaping = false;
27735
+ while (i < raw.length) {
27736
+ const c = raw[i];
27737
+ str += c;
27738
+ i++;
27739
+ if (escaping) escaping = false;
27740
+ else if (c === "\\") escaping = true;
27741
+ else if (c === "'") break;
27742
+ }
27743
+ return {
27744
+ str,
27745
+ consumed: i - start
27746
+ };
27747
+ }
27748
+ /**
27749
+ * Scan a full nested template string starting at `start` (pointing at the opening backtick).
27750
+ * Handles ${...} spans inside the template recursively.
27751
+ */
27752
+ function scanNestedTemplate(raw, start) {
27753
+ let i = start + 1;
27754
+ let str = "`";
27755
+ while (i < raw.length) {
27756
+ const c = raw[i];
27757
+ if (c === "`") {
27758
+ str += c;
27759
+ i++;
27760
+ break;
27761
+ } else if (c === "$" && raw[i + 1] === "{") {
27762
+ str += "${";
27763
+ i += 2;
27764
+ const { expr, consumed } = scanExpression(raw, i);
27765
+ str += `${expr}}`;
27766
+ i += consumed;
27767
+ } else {
27768
+ str += c;
27769
+ i++;
27770
+ }
27771
+ }
27772
+ return {
27773
+ str,
27774
+ consumed: i - start
27775
+ };
27776
+ }
27777
+ /**
27778
+ * Split the raw content of a template string (between the surrounding backticks)
27779
+ * into alternating literal and expression segments.
27780
+ */
27781
+ function splitSegments(raw) {
27782
+ const segments = [];
27783
+ let i = 0;
27784
+ let literal = "";
27785
+ while (i < raw.length) if (raw[i] === "$" && raw[i + 1] === "{") {
27786
+ if (literal.length > 0) {
27787
+ segments.push({
27788
+ type: "literal",
27789
+ value: literal
27790
+ });
27791
+ literal = "";
27792
+ }
27793
+ i += 2;
27794
+ const { expr, consumed } = scanExpression(raw, i);
27795
+ i += consumed;
27796
+ segments.push({
27797
+ type: "expression",
27798
+ value: expr
27799
+ });
27800
+ } else {
27801
+ literal += raw[i];
27802
+ i++;
27803
+ }
27804
+ if (literal.length > 0) segments.push({
27805
+ type: "literal",
27806
+ value: literal
27807
+ });
27808
+ return segments;
27809
+ }
27810
+ function parseTemplateString(ctx, token) {
27811
+ ctx.advance();
27812
+ const sourceCodeInfo = token[2];
27813
+ const segments = splitSegments(token[1].slice(1, -1));
27814
+ if (segments.length === 0) return withSourceCodeInfo([NodeTypes.String, ""], sourceCodeInfo);
27815
+ if (segments.length === 1 && segments[0].type === "literal") return withSourceCodeInfo([NodeTypes.String, segments[0].value], sourceCodeInfo);
27816
+ const segmentNodes = [];
27817
+ for (const segment of segments) if (segment.type === "literal") {
27818
+ if (segment.value.length === 0) continue;
27819
+ segmentNodes.push(withSourceCodeInfo([NodeTypes.String, segment.value], sourceCodeInfo));
27820
+ } else {
27821
+ if (segment.value.trim().length === 0) throw new DvalaError("Empty interpolation in template string", sourceCodeInfo);
27822
+ const minified = minifyTokenStream(tokenize(segment.value, false, sourceCodeInfo?.filePath), { removeWhiteSpace: true });
27823
+ for (const t of minified.tokens) if (t[0] === "Error") throw new DvalaError(`Template string interpolation error: ${t[3]}`, sourceCodeInfo);
27824
+ const expr = parseExpression(createParserContext(minified), 0);
27825
+ segmentNodes.push(expr);
27826
+ }
27827
+ return withSourceCodeInfo([NodeTypes.TemplateString, segmentNodes], sourceCodeInfo);
27828
+ }
27829
+ //#endregion
27443
27830
  //#region src/parser/subParsers/parseOperand.ts
27444
27831
  function parseOperand(ctx) {
27445
27832
  let operand = parseOperandPart(ctx);
@@ -27494,6 +27881,7 @@ function parseOperandPart(ctx) {
27494
27881
  case "Number":
27495
27882
  case "BasePrefixedNumber": return parseNumber(ctx);
27496
27883
  case "string": return parseString(ctx, token);
27884
+ case "TemplateString": return parseTemplateString(ctx, token);
27497
27885
  case "Symbol": {
27498
27886
  ctx.storePosition();
27499
27887
  const lamdaFunction = parseLambdaFunction(ctx);
@@ -27625,18 +28013,6 @@ function parse(tokenStream) {
27625
28013
  return nodes;
27626
28014
  }
27627
28015
  //#endregion
27628
- //#region src/tokenizer/minifyTokenStream.ts
27629
- function minifyTokenStream(tokenStream, { removeWhiteSpace }) {
27630
- const tokens = tokenStream.tokens.filter((token) => {
27631
- if (isSingleLineCommentToken(token) || isMultiLineCommentToken(token) || isShebangToken(token) || removeWhiteSpace && isWhitespaceToken(token)) return false;
27632
- return true;
27633
- });
27634
- return {
27635
- ...tokenStream,
27636
- tokens
27637
- };
27638
- }
27639
- //#endregion
27640
28016
  //#region src/evaluator/effectTypes.ts
27641
28017
  const SUSPENDED_MESSAGE = "Program suspended";
27642
28018
  /**
@@ -28317,9 +28693,33 @@ function stepNode(node, env, k) {
28317
28693
  };
28318
28694
  case NodeTypes.NormalExpression: return stepNormalExpression(node, env, k);
28319
28695
  case NodeTypes.SpecialExpression: return stepSpecialExpression(node, env, k);
28696
+ case NodeTypes.TemplateString: return stepTemplateString(node, env, k);
28320
28697
  default: throw new DvalaError(`${getNodeTypeName(node[0])}-node cannot be evaluated`, node[2]);
28321
28698
  }
28322
28699
  }
28700
+ function stepTemplateString(node, env, k) {
28701
+ const segments = node[1];
28702
+ const sourceCodeInfo = node[2];
28703
+ if (segments.length === 0) return {
28704
+ type: "Value",
28705
+ value: "",
28706
+ k
28707
+ };
28708
+ const frame = {
28709
+ type: "TemplateStringBuild",
28710
+ segments,
28711
+ index: 0,
28712
+ result: "",
28713
+ env,
28714
+ sourceCodeInfo
28715
+ };
28716
+ return {
28717
+ type: "Eval",
28718
+ node: segments[0],
28719
+ env,
28720
+ k: [frame, ...k]
28721
+ };
28722
+ }
28323
28723
  /**
28324
28724
  * Normal expressions: evaluate arguments left-to-right, then dispatch.
28325
28725
  * Push EvalArgsFrame + NanCheckFrame, then start evaluating the first arg.
@@ -29249,6 +29649,7 @@ function applyFrame(frame, value, k) {
29249
29649
  case "And": return applyAnd(frame, value, k);
29250
29650
  case "Or": return applyOr(frame, value, k);
29251
29651
  case "Qq": return applyQq(frame, value, k);
29652
+ case "TemplateStringBuild": return applyTemplateStringBuild(frame, value, k);
29252
29653
  case "ArrayBuild": return applyArrayBuild(frame, value, k);
29253
29654
  case "ObjectBuild": return applyObjectBuild(frame, value, k);
29254
29655
  case "LetBind": return applyLetBind(frame, value, k);
@@ -29535,6 +29936,27 @@ function advanceQq(frame, k) {
29535
29936
  function skipUndefinedQq(frame, k) {
29536
29937
  return advanceQq(frame, k);
29537
29938
  }
29939
+ function applyTemplateStringBuild(frame, value, k) {
29940
+ const { segments, env } = frame;
29941
+ const result = frame.result + String(value);
29942
+ const nextIndex = frame.index + 1;
29943
+ if (nextIndex >= segments.length) return {
29944
+ type: "Value",
29945
+ value: result,
29946
+ k
29947
+ };
29948
+ const newFrame = {
29949
+ ...frame,
29950
+ index: nextIndex,
29951
+ result
29952
+ };
29953
+ return {
29954
+ type: "Eval",
29955
+ node: segments[nextIndex],
29956
+ env,
29957
+ k: [newFrame, ...k]
29958
+ };
29959
+ }
29538
29960
  function applyArrayBuild(frame, value, k) {
29539
29961
  const { nodes, result, env, sourceCodeInfo } = frame;
29540
29962
  if (frame.isSpread) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojir/dvala",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "dvala",
5
5
  "author": "Albert Mojir",
6
6
  "license": "MIT",
@@ -138,7 +138,9 @@
138
138
  "dvala": "node ./dist/cli/cli.js",
139
139
  "dev": "npx serve docs -p 9901",
140
140
  "test:e2e": "npx playwright test",
141
+ "test:e2e:prod": "E2E_BASE_URL=https://mojir.github.io/dvala npx playwright test",
141
142
  "test:e2e:headed": "npx playwright test --headed",
143
+ "test:e2e:prod:headed": "E2E_BASE_URL=https://mojir.github.io/dvala npx playwright test --headed",
142
144
  "lcov": "open-cli ./coverage/index.html",
143
145
  "mcp:inspect": "npx @modelcontextprotocol/inspector node ./dist/mcp-server/server.js"
144
146
  },