dskcode 0.1.20 → 0.1.21

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
@@ -1182,38 +1182,58 @@ function estimateMessageTokens(msg) {
1182
1182
  }
1183
1183
  return estimateTokens(text) + 10;
1184
1184
  }
1185
+ function groupIntoTurns(messages) {
1186
+ const turns = [];
1187
+ let current = null;
1188
+ for (const msg of messages) {
1189
+ if (msg.role === "user") {
1190
+ if (current) turns.push(current);
1191
+ current = [msg];
1192
+ } else {
1193
+ if (!current) current = [];
1194
+ current.push(msg);
1195
+ }
1196
+ }
1197
+ if (current && current.length > 0) turns.push(current);
1198
+ return turns;
1199
+ }
1200
+ function estimateTurnTokens(turn) {
1201
+ let sum = 0;
1202
+ for (const msg of turn) sum += estimateMessageTokens(msg);
1203
+ return sum;
1204
+ }
1185
1205
  function trimMessages(messages, opts) {
1186
1206
  const meta = getModelMeta(opts.model);
1187
1207
  const maxInputTokens = meta.contextWindow - opts.reservedForOutput;
1188
1208
  const systemTokens = estimateTokens(opts.systemPrompt);
1189
1209
  let remaining = maxInputTokens - systemTokens;
1190
- const preserved = [];
1210
+ const turns = groupIntoTurns(messages);
1211
+ const preservedTurns = [];
1191
1212
  let roundsPreserved = 0;
1192
- for (let i = messages.length - 1; i >= 0 && roundsPreserved < opts.preserveRecentRounds; i--) {
1193
- preserved.unshift(messages[i]);
1194
- if (messages[i].role === "user") {
1195
- roundsPreserved++;
1196
- }
1213
+ for (let i = turns.length - 1; i >= 0 && roundsPreserved < opts.preserveRecentRounds; i--) {
1214
+ preservedTurns.unshift(turns[i]);
1215
+ roundsPreserved++;
1197
1216
  }
1217
+ const preserved = preservedTurns.flat();
1198
1218
  for (const msg of preserved) {
1199
1219
  remaining -= estimateMessageTokens(msg);
1200
1220
  }
1201
1221
  if (remaining < 0) {
1202
- while (preserved.length > 1 && remaining < 0) {
1203
- const removed = preserved.shift();
1204
- remaining += estimateMessageTokens(removed);
1222
+ while (preservedTurns.length > 1 && remaining < 0) {
1223
+ const dropped = preservedTurns.shift();
1224
+ remaining += estimateTurnTokens(dropped);
1205
1225
  }
1206
- return [preserved, true];
1226
+ return [preservedTurns.flat(), true];
1207
1227
  }
1208
- const olderMessages = messages.slice(0, messages.length - preserved.length);
1209
- const kept = [];
1210
- for (let i = olderMessages.length - 1; i >= 0; i--) {
1211
- const cost = estimateMessageTokens(olderMessages[i]);
1228
+ const olderTurns = turns.slice(0, turns.length - preservedTurns.length);
1229
+ const keptTurns = [];
1230
+ for (let i = olderTurns.length - 1; i >= 0; i--) {
1231
+ const cost = estimateTurnTokens(olderTurns[i]);
1212
1232
  if (remaining - cost < 0) break;
1213
1233
  remaining -= cost;
1214
- kept.unshift(olderMessages[i]);
1234
+ keptTurns.unshift(olderTurns[i]);
1215
1235
  }
1216
- const result = [...kept, ...preserved];
1236
+ const result = [...keptTurns, ...preservedTurns].flat();
1217
1237
  const trimmed = result.length < messages.length;
1218
1238
  return [result, trimmed];
1219
1239
  }
@@ -1815,7 +1835,8 @@ ${item.result.diff.patch}`;
1815
1835
  async #executeBatch(calls) {
1816
1836
  const toolCtx = {
1817
1837
  cwd: this.#options.cwd,
1818
- signal: this.#abortController.signal
1838
+ signal: this.#abortController.signal,
1839
+ writeRoots: this.#options.writeRoots
1819
1840
  };
1820
1841
  const allReadOnly = calls.every((tc) => {
1821
1842
  const tool = this.#toolRegistry.get(tc.name);
@@ -1977,6 +1998,39 @@ function resolvePath(inputPath, cwd) {
1977
1998
  const resolved = isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath);
1978
1999
  return resolve(resolved);
1979
2000
  }
2001
+ async function realPath(target) {
2002
+ try {
2003
+ return await realpath2(target);
2004
+ } catch {
2005
+ const parent = resolve(target, "..");
2006
+ try {
2007
+ const realParent = await realpath2(parent);
2008
+ return resolve(realParent, relative(parent, target));
2009
+ } catch {
2010
+ return resolve(target);
2011
+ }
2012
+ }
2013
+ }
2014
+ async function confine(allowedRoots, target) {
2015
+ if (allowedRoots.length === 0) {
2016
+ return { ok: true };
2017
+ }
2018
+ const realTarget = await realPath(target);
2019
+ for (const root of allowedRoots) {
2020
+ const realRoot = await realPath(root);
2021
+ const rel = relative(realRoot, realTarget);
2022
+ if (!rel.startsWith("..") && rel !== "" && !rel.startsWith("/") && !rel.startsWith("\\")) {
2023
+ return { ok: true };
2024
+ }
2025
+ if (realTarget === realRoot) {
2026
+ return { ok: true };
2027
+ }
2028
+ }
2029
+ return {
2030
+ ok: false,
2031
+ error: `\u8DEF\u5F84 "${target}" \u4E0D\u5728\u5141\u8BB8\u7684\u5199\u5165\u8303\u56F4\u5185 ${allowedRoots.join(", ")}`
2032
+ };
2033
+ }
1980
2034
  function truncateOutput(content, maxLength = DEFAULT_MAX_OUTPUT_LENGTH) {
1981
2035
  if (content.length <= maxLength) return content;
1982
2036
  const truncated = content.slice(0, maxLength);
@@ -2320,6 +2374,41 @@ function computeFileDiff(oldContent, newContent, filePath) {
2320
2374
  };
2321
2375
  }
2322
2376
 
2377
+ // src/tool/eol.ts
2378
+ import { writeFile as writeFile3 } from "fs/promises";
2379
+ function detectEol(text) {
2380
+ if (text.length === 0) return "\n";
2381
+ const crlfIdx = text.indexOf("\r\n");
2382
+ const lfIdx = text.indexOf("\n");
2383
+ if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
2384
+ return "\r\n";
2385
+ }
2386
+ return "\n";
2387
+ }
2388
+ function hasTrailingNewline(text) {
2389
+ return text.endsWith("\n") || text.endsWith("\r\n");
2390
+ }
2391
+ function normalizeEol(originalContent, newContent) {
2392
+ const targetEol = detectEol(originalContent);
2393
+ const lines = newContent.replace(/\r\n/g, "\n").split("\n");
2394
+ const originalHasTrailing = hasTrailingNewline(originalContent);
2395
+ const newHasTrailing = lines.length > 0 && lines[lines.length - 1] === "";
2396
+ let result = lines.join(targetEol);
2397
+ if (originalHasTrailing && !newHasTrailing) {
2398
+ result += targetEol;
2399
+ }
2400
+ if (!originalHasTrailing && newHasTrailing) {
2401
+ if (result.endsWith(targetEol)) {
2402
+ result = result.slice(0, -targetEol.length);
2403
+ }
2404
+ }
2405
+ return result;
2406
+ }
2407
+ async function writeFileWithEol(filePath, originalContent, newContent) {
2408
+ const content = originalContent.length > 0 ? normalizeEol(originalContent, newContent) : newContent;
2409
+ await writeFile3(filePath, content, "utf-8");
2410
+ }
2411
+
2323
2412
  // src/tool/builtins/read-file.ts
2324
2413
  import { readFile as readFile4, stat } from "fs/promises";
2325
2414
  import { open } from "fs/promises";
@@ -2427,7 +2516,7 @@ var readFileTool = {
2427
2516
  };
2428
2517
 
2429
2518
  // src/tool/builtins/write-file.ts
2430
- import { writeFile as writeFile3, mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
2519
+ import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
2431
2520
  import { dirname, relative as relative3, basename } from "path";
2432
2521
  var writeFileSchema = {
2433
2522
  type: "object",
@@ -2459,6 +2548,12 @@ var writeFileTool = {
2459
2548
  }
2460
2549
  const filePath = resolvePath(params.path, ctx.cwd);
2461
2550
  try {
2551
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2552
+ const conf = await confine(ctx.writeRoots, filePath);
2553
+ if (!conf.ok) {
2554
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2555
+ }
2556
+ }
2462
2557
  let oldContent = "";
2463
2558
  let existedBefore = false;
2464
2559
  try {
@@ -2468,7 +2563,7 @@ var writeFileTool = {
2468
2563
  }
2469
2564
  await mkdir4(dirname(filePath), { recursive: true });
2470
2565
  const content = String(params.content);
2471
- await writeFile3(filePath, content, "utf-8");
2566
+ await writeFileWithEol(filePath, oldContent, content);
2472
2567
  const diff = computeFileDiff(oldContent, content, filePath);
2473
2568
  diff.existedBefore = existedBefore;
2474
2569
  const lineCount = content.split("\n").length;
@@ -2495,7 +2590,7 @@ var writeFileTool = {
2495
2590
  };
2496
2591
 
2497
2592
  // src/tool/builtins/edit-file.ts
2498
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
2593
+ import { readFile as readFile6 } from "fs/promises";
2499
2594
  import { basename as basename2 } from "path";
2500
2595
  var editFileSchema = {
2501
2596
  type: "object",
@@ -2533,6 +2628,12 @@ var editFileTool = {
2533
2628
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 new_text", error: "INVALID_ARGS" };
2534
2629
  }
2535
2630
  const filePath = resolvePath(params.path, ctx.cwd);
2631
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2632
+ const conf = await confine(ctx.writeRoots, filePath);
2633
+ if (!conf.ok) {
2634
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2635
+ }
2636
+ }
2536
2637
  try {
2537
2638
  const content = await readFile6(filePath, "utf-8");
2538
2639
  const firstIndex = content.indexOf(params.old_text);
@@ -2552,7 +2653,7 @@ var editFileTool = {
2552
2653
  };
2553
2654
  }
2554
2655
  const newContent = content.replace(params.old_text, params.new_text);
2555
- await writeFile4(filePath, newContent, "utf-8");
2656
+ await writeFileWithEol(filePath, content, newContent);
2556
2657
  const diff = computeFileDiff(content, newContent, filePath);
2557
2658
  diff.existedBefore = true;
2558
2659
  const beforeText = content.slice(0, firstIndex);
@@ -2582,7 +2683,7 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
2582
2683
  };
2583
2684
 
2584
2685
  // src/tool/builtins/multi-edit.ts
2585
- import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
2686
+ import { readFile as readFile7 } from "fs/promises";
2586
2687
  import { basename as basename3 } from "path";
2587
2688
  var multiEditSchema = {
2588
2689
  type: "object",
@@ -2623,6 +2724,12 @@ var multiEditTool = {
2623
2724
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 edits\uFF08\u975E\u7A7A\u6570\u7EC4\uFF09", error: "INVALID_ARGS" };
2624
2725
  }
2625
2726
  const filePath = resolvePath(params.path, ctx.cwd);
2727
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2728
+ const conf = await confine(ctx.writeRoots, filePath);
2729
+ if (!conf.ok) {
2730
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2731
+ }
2732
+ }
2626
2733
  try {
2627
2734
  const originalContent = await readFile7(filePath, "utf-8");
2628
2735
  let currentContent = originalContent;
@@ -2669,7 +2776,7 @@ var multiEditTool = {
2669
2776
  currentContent = currentContent.replace(step.oldText, step.newText);
2670
2777
  }
2671
2778
  }
2672
- await writeFile5(filePath, currentContent, "utf-8");
2779
+ await writeFileWithEol(filePath, originalContent, currentContent);
2673
2780
  const diff = computeFileDiff(originalContent, currentContent, filePath);
2674
2781
  diff.existedBefore = true;
2675
2782
  return {
@@ -2692,7 +2799,7 @@ var multiEditTool = {
2692
2799
  };
2693
2800
 
2694
2801
  // src/tool/builtins/delete-range.ts
2695
- import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
2802
+ import { readFile as readFile8 } from "fs/promises";
2696
2803
  import { basename as basename4 } from "path";
2697
2804
  var deleteRangeSchema = {
2698
2805
  type: "object",
@@ -2750,6 +2857,12 @@ var deleteRangeTool = {
2750
2857
  }
2751
2858
  const filePath = resolvePath(params.path, ctx.cwd);
2752
2859
  const inclusive = params.inclusive ?? false;
2860
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2861
+ const conf = await confine(ctx.writeRoots, filePath);
2862
+ if (!conf.ok) {
2863
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2864
+ }
2865
+ }
2753
2866
  try {
2754
2867
  const content = await readFile8(filePath, "utf-8");
2755
2868
  const lines = content.split("\n");
@@ -2781,14 +2894,8 @@ var deleteRangeTool = {
2781
2894
  }
2782
2895
  const newLines = [...lines.slice(0, rangeStart), ...lines.slice(rangeEnd + 1)];
2783
2896
  const newContent = newLines.join("\n");
2784
- let writeContent = newContent;
2785
- if (content.endsWith("\n") && !newContent.endsWith("\n")) {
2786
- writeContent = newContent + "\n";
2787
- } else if (content.endsWith("\r\n") && !newContent.endsWith("\r\n") && !newContent.endsWith("\n")) {
2788
- writeContent = newContent + "\r\n";
2789
- }
2790
- await writeFile6(filePath, writeContent, "utf-8");
2791
- const diff = computeFileDiff(content, writeContent, filePath);
2897
+ await writeFileWithEol(filePath, content, newContent);
2898
+ const diff = computeFileDiff(content, newContent, filePath);
2792
2899
  diff.existedBefore = true;
2793
2900
  const deletedLines = rangeEnd - rangeStart + 1;
2794
2901
  const summary = `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
@@ -4224,7 +4331,7 @@ function BrickBreakerGame({ onExit: _onExit }) {
4224
4331
  const [initialLevel, setInitialLevel] = useState5(1);
4225
4332
  const [selectingLevel, setSelectingLevel] = useState5(true);
4226
4333
  const stateRef = useRef3(createInitialState(initialLevel));
4227
- const [tick, setTick] = useState5(0);
4334
+ const [tick2, setTick] = useState5(0);
4228
4335
  const onExitRef = useRef3(_onExit);
4229
4336
  onExitRef.current = _onExit;
4230
4337
  useEffect4(() => {
@@ -4279,7 +4386,7 @@ function BrickBreakerGame({ onExit: _onExit }) {
4279
4386
  const aliveCount = s.bricks.filter((b) => b.alive).length;
4280
4387
  const board = buildBoard(s);
4281
4388
  const def = getLevel(s.level);
4282
- void tick;
4389
+ void tick2;
4283
4390
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
4284
4391
  /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", children: [
4285
4392
  /* @__PURE__ */ jsx10(Box10, { width: 20, children: /* @__PURE__ */ jsxs10(Text11, { children: [
@@ -4754,7 +4861,7 @@ function buildGameView(s, scoreLines, scoreColor, message) {
4754
4861
  }
4755
4862
  function CoderCheck({ onExit: _onExit }) {
4756
4863
  const stateRef = useRef4(createInitialState2());
4757
- const [tick, setTick] = useState6(0);
4864
+ const [tick2, setTick] = useState6(0);
4758
4865
  const [colorOffset, setColorOffset] = useState6(0);
4759
4866
  const onExitRef = useRef4(_onExit);
4760
4867
  onExitRef.current = _onExit;
@@ -4832,7 +4939,7 @@ function CoderCheck({ onExit: _onExit }) {
4832
4939
  const scoreStr = String(s.score).padStart(5, "0");
4833
4940
  const scoreLines = buildScoreLines(scoreStr);
4834
4941
  const view = buildGameView(s, scoreLines, scoreColor, s.message);
4835
- void tick;
4942
+ void tick2;
4836
4943
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
4837
4944
  /* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs11(Text12, { children: [
4838
4945
  "\u751F\u547D ",
@@ -4889,16 +4996,375 @@ var coder_check_default = {
4889
4996
  }
4890
4997
  };
4891
4998
 
4999
+ // src/game/snake/engine.ts
5000
+ function isOpposite(a, b) {
5001
+ return a === "UP" && b === "DOWN" || a === "DOWN" && b === "UP" || a === "LEFT" && b === "RIGHT" || a === "RIGHT" && b === "LEFT";
5002
+ }
5003
+ function samePoint(a, b) {
5004
+ return a.x === b.x && a.y === b.y;
5005
+ }
5006
+ function movePoint(p, dir) {
5007
+ switch (dir) {
5008
+ case "UP":
5009
+ return { x: p.x, y: p.y - 1 };
5010
+ case "DOWN":
5011
+ return { x: p.x, y: p.y + 1 };
5012
+ case "LEFT":
5013
+ return { x: p.x - 1, y: p.y };
5014
+ case "RIGHT":
5015
+ return { x: p.x + 1, y: p.y };
5016
+ }
5017
+ }
5018
+ function wrapPoint(p, config) {
5019
+ return {
5020
+ x: (p.x % config.width + config.width) % config.width,
5021
+ y: (p.y % config.height + config.height) % config.height
5022
+ };
5023
+ }
5024
+ function generateFood(snake, config, type, excludePositions = []) {
5025
+ const occupied = new Set(snake.map((p) => `${p.x},${p.y}`));
5026
+ for (const p of excludePositions) {
5027
+ occupied.add(`${p.x},${p.y}`);
5028
+ }
5029
+ const totalCells = config.width * config.height;
5030
+ if (occupied.size >= totalCells) return null;
5031
+ let pos;
5032
+ do {
5033
+ pos = {
5034
+ x: Math.floor(Math.random() * config.width),
5035
+ y: Math.floor(Math.random() * config.height)
5036
+ };
5037
+ } while (occupied.has(`${pos.x},${pos.y}`));
5038
+ return {
5039
+ position: pos,
5040
+ type,
5041
+ ...type === "special" ? { spawnedAt: Date.now() } : {}
5042
+ };
5043
+ }
5044
+ function createInitialState3(config) {
5045
+ const startX = Math.floor(config.width / 2);
5046
+ const startY = Math.floor(config.height / 2);
5047
+ const snake = [
5048
+ { x: startX, y: startY },
5049
+ { x: startX - 1, y: startY },
5050
+ { x: startX - 2, y: startY }
5051
+ ];
5052
+ const food = generateFood(snake, config, "normal");
5053
+ return {
5054
+ snake,
5055
+ direction: "RIGHT",
5056
+ nextDirection: "RIGHT",
5057
+ food,
5058
+ specialFood: null,
5059
+ score: 0,
5060
+ speedLevel: 1,
5061
+ isGameOver: false,
5062
+ isPaused: false,
5063
+ wallWrap: Math.random() < 0.5,
5064
+ tickInterval: config.initialSpeed
5065
+ };
5066
+ }
5067
+ function tick(state, config) {
5068
+ state.direction = state.nextDirection;
5069
+ const head = state.snake[0];
5070
+ let newHead = movePoint(head, state.direction);
5071
+ const outOfBounds = newHead.x < 0 || newHead.x >= config.width || newHead.y < 0 || newHead.y >= config.height;
5072
+ if (outOfBounds) {
5073
+ if (state.wallWrap) {
5074
+ newHead = wrapPoint(newHead, config);
5075
+ } else {
5076
+ state.isGameOver = true;
5077
+ return;
5078
+ }
5079
+ }
5080
+ const ateNormal = samePoint(newHead, state.food.position);
5081
+ const ateSpecial = state.specialFood !== null && samePoint(newHead, state.specialFood.position);
5082
+ const willEat = ateNormal || ateSpecial;
5083
+ const bodyToCheck = willEat ? state.snake : state.snake.slice(0, -1);
5084
+ for (const segment of bodyToCheck) {
5085
+ if (samePoint(newHead, segment)) {
5086
+ state.isGameOver = true;
5087
+ return;
5088
+ }
5089
+ }
5090
+ state.snake.unshift(newHead);
5091
+ if (ateNormal) {
5092
+ state.score += 1;
5093
+ updateSpeed(state, config);
5094
+ const exclude = state.specialFood ? [state.specialFood.position] : [];
5095
+ const newFood = generateFood(state.snake, config, "normal", exclude);
5096
+ if (newFood) {
5097
+ state.food = newFood;
5098
+ } else {
5099
+ state.isGameOver = true;
5100
+ return;
5101
+ }
5102
+ if (state.specialFood === null && Math.random() < 0.25) {
5103
+ state.specialFood = generateFood(state.snake, config, "special", [state.food.position]);
5104
+ }
5105
+ } else if (ateSpecial) {
5106
+ state.score += 3;
5107
+ updateSpeed(state, config);
5108
+ state.specialFood = null;
5109
+ } else {
5110
+ state.snake.pop();
5111
+ }
5112
+ if (state.specialFood?.spawnedAt !== void 0) {
5113
+ if (Date.now() - state.specialFood.spawnedAt > 5e3) {
5114
+ state.specialFood = null;
5115
+ }
5116
+ }
5117
+ }
5118
+ function updateSpeed(state, config) {
5119
+ const newLevel = Math.floor(state.score / config.scorePerLevel) + 1;
5120
+ state.speedLevel = newLevel;
5121
+ state.tickInterval = Math.max(
5122
+ config.minSpeed,
5123
+ config.initialSpeed - (newLevel - 1) * config.speedDecrement
5124
+ );
5125
+ }
5126
+ function changeDirection(state, newDir) {
5127
+ if (!isOpposite(newDir, state.direction)) {
5128
+ state.nextDirection = newDir;
5129
+ }
5130
+ }
5131
+
5132
+ // src/game/snake/renderer.ts
5133
+ import chalk4 from "chalk";
5134
+ var CELL_W = 2;
5135
+ var EMPTY = " ";
5136
+ var BODY_CHAR = "\u2593";
5137
+ var HEAD_CHAR = "\u2593";
5138
+ var NORMAL_FOOD_CHAR = "\u25C6";
5139
+ var SPECIAL_FOOD_CHAR = "\u2605";
5140
+ var CSI = "\x1B[";
5141
+ var CURSOR_HIDE = `${CSI}?25l`;
5142
+ var CURSOR_SHOW = `${CSI}?25h`;
5143
+ var CURSOR_HOME = `${CSI}H`;
5144
+ function hideCursor() {
5145
+ process.stdout.write(CURSOR_HIDE);
5146
+ }
5147
+ function showCursor() {
5148
+ process.stdout.write(CURSOR_SHOW);
5149
+ }
5150
+ function clearScreen() {
5151
+ process.stdout.write(`${CSI}2J${CURSOR_HOME}`);
5152
+ }
5153
+ function cellColor(char, type) {
5154
+ switch (type) {
5155
+ case "snake-head":
5156
+ return chalk4.greenBright(char);
5157
+ case "snake-body":
5158
+ return chalk4.green(char);
5159
+ case "normal-food":
5160
+ return chalk4.red(char);
5161
+ case "special-food":
5162
+ return chalk4.yellow(char);
5163
+ case "empty":
5164
+ return char;
5165
+ }
5166
+ }
5167
+ function renderToTerminal(state, config) {
5168
+ const lines = buildRenderLines(state, config);
5169
+ process.stdout.write(`${CSI}2J${CURSOR_HOME}` + lines.join("\n"));
5170
+ }
5171
+ function buildRenderLines(state, config) {
5172
+ const { width: W, height: H } = config;
5173
+ const lines = [];
5174
+ const grid = Array.from({ length: H }, () => Array(W).fill("empty"));
5175
+ for (let i = 1; i < state.snake.length; i++) {
5176
+ const seg = state.snake[i];
5177
+ if (seg.y >= 0 && seg.y < H && seg.x >= 0 && seg.x < W) {
5178
+ grid[seg.y][seg.x] = "snake-body";
5179
+ }
5180
+ }
5181
+ const head = state.snake[0];
5182
+ if (head.y >= 0 && head.y < H && head.x >= 0 && head.x < W) {
5183
+ grid[head.y][head.x] = "snake-head";
5184
+ }
5185
+ const fp = state.food.position;
5186
+ if (grid[fp.y][fp.x] === "empty") {
5187
+ grid[fp.y][fp.x] = "normal-food";
5188
+ }
5189
+ if (state.specialFood) {
5190
+ const sp = state.specialFood.position;
5191
+ if (grid[sp.y][sp.x] === "empty") {
5192
+ grid[sp.y][sp.x] = "special-food";
5193
+ }
5194
+ }
5195
+ lines.push(" " + chalk4.gray("\u2554" + "\u2550\u2550".repeat(W) + "\u2557"));
5196
+ for (let y = 0; y < H; y++) {
5197
+ let row = " " + chalk4.gray("\u2551");
5198
+ for (let x = 0; x < W; x++) {
5199
+ const cellType = grid[y][x];
5200
+ let char;
5201
+ switch (cellType) {
5202
+ case "snake-head":
5203
+ char = HEAD_CHAR.repeat(CELL_W);
5204
+ break;
5205
+ case "snake-body":
5206
+ char = BODY_CHAR.repeat(CELL_W);
5207
+ break;
5208
+ case "normal-food":
5209
+ char = NORMAL_FOOD_CHAR + " ";
5210
+ break;
5211
+ case "special-food":
5212
+ char = SPECIAL_FOOD_CHAR + " ";
5213
+ break;
5214
+ default:
5215
+ char = EMPTY;
5216
+ }
5217
+ row += cellColor(char, cellType);
5218
+ }
5219
+ row += chalk4.gray("\u2551");
5220
+ lines.push(row);
5221
+ }
5222
+ lines.push(" " + chalk4.gray("\u255A" + "\u2550\u2550".repeat(W) + "\u255D"));
5223
+ lines.push("");
5224
+ lines.push(
5225
+ ` ${chalk4.cyan("\u5206\u6570")}: ${state.score} ${chalk4.cyan("\u957F\u5EA6")}: ${state.snake.length} ${chalk4.cyan("\u901F\u5EA6")}: Lv.${state.speedLevel} ${chalk4.cyan("\u8FB9\u754C")}: ${state.wallWrap ? "\u7A7F\u5899" : "\u649E\u5899"}`
5226
+ );
5227
+ if (state.specialFood?.spawnedAt !== void 0) {
5228
+ const remaining = Math.ceil((5e3 - (Date.now() - state.specialFood.spawnedAt)) / 1e3);
5229
+ if (remaining > 0) {
5230
+ lines.push(` ${chalk4.yellow(`\u2B50 \u7279\u6B8A\u98DF\u7269 ${remaining} \u79D2\u540E\u6D88\u5931`)}`);
5231
+ }
5232
+ }
5233
+ lines.push(` ${chalk4.gray("W/A/S/D \u6216 \u65B9\u5411\u952E\u79FB\u52A8 | P \u6682\u505C | M \u5207\u6362\u6A21\u5F0F | Q \u9000\u51FA")}`);
5234
+ if (state.isPaused) {
5235
+ lines.push("");
5236
+ lines.push(` ${chalk4.yellow("\u23F8 \u5DF2\u6682\u505C \u2014 \u6309 P \u7EE7\u7EED")}`);
5237
+ }
5238
+ if (state.isGameOver) {
5239
+ lines.push("");
5240
+ lines.push(` ${chalk4.red.bold("\u{1F480} \u6E38\u620F\u7ED3\u675F!")} ${chalk4.cyan("\u6700\u7EC8\u5206\u6570: " + state.score)}`);
5241
+ lines.push(` ${chalk4.gray("\u6309 R \u91CD\u65B0\u5F00\u59CB | \u6309 Q \u9000\u51FA")}`);
5242
+ }
5243
+ return lines;
5244
+ }
5245
+
5246
+ // src/game/snake/input.ts
5247
+ import * as readline from "readline";
5248
+ var KEY_TO_DIR = {
5249
+ up: "UP",
5250
+ w: "UP",
5251
+ W: "UP",
5252
+ down: "DOWN",
5253
+ s: "DOWN",
5254
+ S: "DOWN",
5255
+ left: "LEFT",
5256
+ a: "LEFT",
5257
+ A: "LEFT",
5258
+ right: "RIGHT",
5259
+ d: "RIGHT",
5260
+ D: "RIGHT"
5261
+ };
5262
+ function setupInput(ctx, onQuit, onRestart) {
5263
+ readline.emitKeypressEvents(process.stdin);
5264
+ if (process.stdin.isTTY) {
5265
+ process.stdin.setRawMode(true);
5266
+ }
5267
+ const handler = (_, key) => {
5268
+ if (!key || !key.name) return;
5269
+ const { name, ctrl } = key;
5270
+ if (ctrl && (name === "c" || name === "d")) {
5271
+ onQuit();
5272
+ return;
5273
+ }
5274
+ if (name === "q") {
5275
+ onQuit();
5276
+ return;
5277
+ }
5278
+ const state = ctx.state;
5279
+ if (state.isGameOver) {
5280
+ if (name === "r") {
5281
+ ctx.state = onRestart();
5282
+ }
5283
+ return;
5284
+ }
5285
+ if (name === "p") {
5286
+ state.isPaused = !state.isPaused;
5287
+ return;
5288
+ }
5289
+ if (name === "m") {
5290
+ state.wallWrap = !state.wallWrap;
5291
+ return;
5292
+ }
5293
+ const dir = KEY_TO_DIR[name];
5294
+ if (dir) {
5295
+ changeDirection(state, dir);
5296
+ }
5297
+ };
5298
+ process.stdin.on("keypress", handler);
5299
+ return () => {
5300
+ process.stdin.removeListener("keypress", handler);
5301
+ if (process.stdin.isTTY) {
5302
+ process.stdin.setRawMode(false);
5303
+ }
5304
+ };
5305
+ }
5306
+
5307
+ // src/game/snake/index.ts
5308
+ var CONFIG = {
5309
+ width: 20,
5310
+ height: 16,
5311
+ initialSpeed: 180,
5312
+ minSpeed: 60,
5313
+ speedDecrement: 10,
5314
+ scorePerLevel: 5
5315
+ };
5316
+ var IDLE_INTERVAL = 200;
5317
+ var snakeGame = {
5318
+ id: "snake",
5319
+ name: "Big Snake",
5320
+ description: "\u7ECF\u5178\u8D2A\u5403\u86C7\uFF0C\u53CC\u98DF\u7269\u7CFB\u7EDF\u3001\u96BE\u5EA6\u9012\u589E\u3001\u7A7F\u5899/\u649E\u5899\u6A21\u5F0F",
5321
+ play: async () => {
5322
+ const ctx = { state: createInitialState3(CONFIG) };
5323
+ return new Promise((resolve2) => {
5324
+ const cleanupInput = setupInput(
5325
+ ctx,
5326
+ () => {
5327
+ stop = true;
5328
+ clearTimeout(timerId);
5329
+ showCursor();
5330
+ cleanupInput();
5331
+ resolve2();
5332
+ },
5333
+ () => {
5334
+ return createInitialState3(CONFIG);
5335
+ }
5336
+ );
5337
+ clearScreen();
5338
+ hideCursor();
5339
+ let stop = false;
5340
+ let timerId;
5341
+ function loop() {
5342
+ if (stop) return;
5343
+ const s = ctx.state;
5344
+ if (!s.isGameOver && !s.isPaused) {
5345
+ tick(s, CONFIG);
5346
+ }
5347
+ renderToTerminal(s, CONFIG);
5348
+ const interval = s.isGameOver || s.isPaused ? IDLE_INTERVAL : s.tickInterval;
5349
+ timerId = setTimeout(loop, interval);
5350
+ }
5351
+ timerId = setTimeout(loop, 0);
5352
+ });
5353
+ }
5354
+ };
5355
+ var snake_default = snakeGame;
5356
+
4892
5357
  // src/game/registry.ts
4893
5358
  function initGames() {
4894
5359
  registerGame(brick_breaker_default);
4895
5360
  registerGame(coder_check_default);
5361
+ registerGame(snake_default);
4896
5362
  return listGames();
4897
5363
  }
4898
5364
 
4899
5365
  // src/cli/index.tsx
4900
5366
  import { render as render4 } from "ink";
4901
- import chalk4 from "chalk";
5367
+ import chalk5 from "chalk";
4902
5368
 
4903
5369
  // src/stock/StockList.tsx
4904
5370
  import { Box as Box12, Text as Text13, useInput as useInput5 } from "ink";
@@ -5331,7 +5797,7 @@ function createCli() {
5331
5797
  const key = await promptForApiKey();
5332
5798
  if (!key) process.exit(1);
5333
5799
  const savedPath = await saveApiKey(key);
5334
- console.log(` ${chalk4.green("\u2714")} API Key \u5DF2\u4FDD\u5B58\u5230 ${chalk4.dim(savedPath)}
5800
+ console.log(` ${chalk5.green("\u2714")} API Key \u5DF2\u4FDD\u5B58\u5230 ${chalk5.dim(savedPath)}
5335
5801
  `);
5336
5802
  const result = await loadAndValidate();
5337
5803
  ctx = { ...ctx, config: result.config };
@@ -5478,8 +5944,8 @@ compdef _dskcode_completion dskcode`);
5478
5944
  { code: "sh601899" }
5479
5945
  ];
5480
5946
  const savedPath = await saveStockConfig(defaultSymbols);
5481
- console.log(`${chalk4.green("\u2714")} \u5DF2\u751F\u6210\u81EA\u9009\u80A1\u914D\u7F6E: ${chalk4.dim(savedPath)}`);
5482
- console.log(`${chalk4.dim(" \u63D0\u793A: \u53EF\u7F16\u8F91\u4E0A\u8FF0\u6587\u4EF6\u81EA\u5B9A\u4E49\u81EA\u9009\u80A1\u5217\u8868")}
5947
+ console.log(`${chalk5.green("\u2714")} \u5DF2\u751F\u6210\u81EA\u9009\u80A1\u914D\u7F6E: ${chalk5.dim(savedPath)}`);
5948
+ console.log(`${chalk5.dim(" \u63D0\u793A: \u53EF\u7F16\u8F91\u4E0A\u8FF0\u6587\u4EF6\u81EA\u5B9A\u4E49\u81EA\u9009\u80A1\u5217\u8868")}
5483
5949
  `);
5484
5950
  }
5485
5951
  const freshResult = await loadAndValidate();
@@ -5533,7 +5999,7 @@ compdef _dskcode_completion dskcode`);
5533
5999
  });
5534
6000
  if (selectedGame) {
5535
6001
  console.log(`
5536
- \u542F\u52A8\u6E38\u620F: ${chalk4.green(selectedGame.name)}
6002
+ \u542F\u52A8\u6E38\u620F: ${chalk5.green(selectedGame.name)}
5537
6003
  `);
5538
6004
  await selectedGame.play();
5539
6005
  }