terminal-quest 1.0.2 โ†’ 1.1.2

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.
@@ -9,6 +9,7 @@ export function grep(fs, args) {
9
9
  let ignoreCase = false;
10
10
  let showLineNumbers = false;
11
11
  let recursive = false;
12
+ let countOnly = false;
12
13
  const nonFlagArgs = [];
13
14
  for (const arg of args) {
14
15
  if (arg.startsWith('-') && arg.length > 1 && !arg.startsWith('--')) {
@@ -19,6 +20,8 @@ export function grep(fs, args) {
19
20
  showLineNumbers = true;
20
21
  else if (ch === 'r')
21
22
  recursive = true;
23
+ else if (ch === 'c')
24
+ countOnly = true;
22
25
  else
23
26
  return { output: '', error: `grep: invalid option -- '${ch}'` };
24
27
  }
@@ -47,16 +50,23 @@ export function grep(fs, args) {
47
50
  }
48
51
  const lines = stdin.split('\n');
49
52
  const results = [];
53
+ let matchCount = 0;
50
54
  for (let i = 0; i < lines.length; i++) {
51
55
  if (regex.test(lines[i])) {
52
- let line = '';
53
- if (showLineNumbers) {
54
- line += `${i + 1}:`;
56
+ matchCount++;
57
+ if (!countOnly) {
58
+ let line = '';
59
+ if (showLineNumbers) {
60
+ line += `${i + 1}:`;
61
+ }
62
+ line += lines[i];
63
+ results.push(line);
55
64
  }
56
- line += lines[i];
57
- results.push(line);
58
65
  }
59
66
  }
67
+ if (countOnly) {
68
+ return { output: String(matchCount) };
69
+ }
60
70
  return { output: results.join('\n') };
61
71
  }
62
72
  const flags = ignoreCase ? 'i' : '';
@@ -68,27 +78,35 @@ export function grep(fs, args) {
68
78
  return { output: '', error: `grep: invalid regular expression '${pattern}'` };
69
79
  }
70
80
  const results = [];
81
+ const fileCounts = [];
71
82
  const multipleFiles = targets.length > 1 || recursive;
72
83
  function searchFile(filePath) {
73
84
  try {
74
85
  const content = fs.readFile(filePath);
75
86
  const lines = content.split('\n');
87
+ let fileMatchCount = 0;
76
88
  for (let i = 0; i < lines.length; i++) {
77
89
  if (regex.test(lines[i])) {
78
- let line = '';
79
- if (multipleFiles) {
80
- line += `${filePath}:`;
81
- }
82
- if (showLineNumbers) {
83
- line += `${i + 1}:`;
90
+ fileMatchCount++;
91
+ if (!countOnly) {
92
+ let line = '';
93
+ if (multipleFiles) {
94
+ line += `${filePath}:`;
95
+ }
96
+ if (showLineNumbers) {
97
+ line += `${i + 1}:`;
98
+ }
99
+ line += lines[i];
100
+ results.push(line);
84
101
  }
85
- line += lines[i];
86
- results.push(line);
87
102
  }
88
103
  }
104
+ if (countOnly) {
105
+ fileCounts.push({ path: filePath, count: fileMatchCount });
106
+ }
89
107
  }
90
- catch {
91
- // Skip files that can't be read
108
+ catch (e) {
109
+ results.push(`grep: ${filePath}: ${e instanceof Error ? e.message : 'cannot read file'}`);
92
110
  }
93
111
  }
94
112
  function searchRecursive(dirPath) {
@@ -104,8 +122,8 @@ export function grep(fs, args) {
104
122
  }
105
123
  }
106
124
  }
107
- catch {
108
- // Skip directories that can't be read
125
+ catch (e) {
126
+ results.push(`grep: ${dirPath}: ${e instanceof Error ? e.message : 'cannot read directory'}`);
109
127
  }
110
128
  }
111
129
  for (const target of targets) {
@@ -122,6 +140,13 @@ export function grep(fs, args) {
122
140
  return { output: '', error: `grep: ${target}: Is a directory` };
123
141
  }
124
142
  }
143
+ if (countOnly) {
144
+ if (multipleFiles) {
145
+ return { output: fileCounts.map(fc => `${fc.path}:${fc.count}`).join('\n') };
146
+ }
147
+ const total = fileCounts.reduce((sum, fc) => sum + fc.count, 0);
148
+ return { output: String(total) };
149
+ }
125
150
  return { output: results.join('\n') };
126
151
  }
127
152
  //# sourceMappingURL=grep.js.map
@@ -33,35 +33,64 @@ export function wc(fs, args) {
33
33
  showWords = true;
34
34
  showBytes = true;
35
35
  }
36
- let content;
37
- let filename;
38
36
  if (files.length > 0) {
39
- filename = files[0];
40
- try {
41
- content = fs.readFile(filename);
37
+ const outputLines = [];
38
+ let totalLines = 0;
39
+ let totalWords = 0;
40
+ let totalBytes = 0;
41
+ for (const filename of files) {
42
+ let content;
43
+ try {
44
+ content = fs.readFile(filename);
45
+ }
46
+ catch (e) {
47
+ return { output: '', error: `wc: ${e.message}` };
48
+ }
49
+ const lines = content === '' ? 0 : (content.match(/\n/g) || []).length;
50
+ const words = content === '' ? 0 : content.split(/\s+/).filter(Boolean).length;
51
+ const bytes = new TextEncoder().encode(content).length;
52
+ totalLines += lines;
53
+ totalWords += words;
54
+ totalBytes += bytes;
55
+ const parts = [];
56
+ if (showLines)
57
+ parts.push(String(lines));
58
+ if (showWords)
59
+ parts.push(String(words));
60
+ if (showBytes)
61
+ parts.push(String(bytes));
62
+ parts.push(filename);
63
+ outputLines.push(parts.join(' '));
42
64
  }
43
- catch (e) {
44
- return { output: '', error: `wc: ${e.message}` };
65
+ if (files.length > 1) {
66
+ const totalParts = [];
67
+ if (showLines)
68
+ totalParts.push(String(totalLines));
69
+ if (showWords)
70
+ totalParts.push(String(totalWords));
71
+ if (showBytes)
72
+ totalParts.push(String(totalBytes));
73
+ totalParts.push('total');
74
+ outputLines.push(totalParts.join(' '));
45
75
  }
76
+ return { output: outputLines.join('\n') };
46
77
  }
47
78
  else if (stdin !== undefined) {
48
- content = stdin;
79
+ const content = stdin;
80
+ const lines = content === '' ? 0 : (content.match(/\n/g) || []).length;
81
+ const words = content === '' ? 0 : content.split(/\s+/).filter(Boolean).length;
82
+ const bytes = new TextEncoder().encode(content).length;
83
+ const parts = [];
84
+ if (showLines)
85
+ parts.push(String(lines));
86
+ if (showWords)
87
+ parts.push(String(words));
88
+ if (showBytes)
89
+ parts.push(String(bytes));
90
+ return { output: parts.join(' ') };
49
91
  }
50
92
  else {
51
93
  return { output: '', error: 'wc: missing file operand' };
52
94
  }
53
- const lines = content === '' ? 0 : (content.match(/\n/g) || []).length;
54
- const words = content === '' ? 0 : content.split(/\s+/).filter(Boolean).length;
55
- const bytes = new TextEncoder().encode(content).length;
56
- const parts = [];
57
- if (showLines)
58
- parts.push(String(lines));
59
- if (showWords)
60
- parts.push(String(words));
61
- if (showBytes)
62
- parts.push(String(bytes));
63
- if (filename)
64
- parts.push(filename);
65
- return { output: parts.join(' ') };
66
95
  }
67
96
  //# sourceMappingURL=wc.js.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { colors } from '../utils/colors.js';
4
4
  import { stories } from '../data/stories/index.js';
@@ -15,13 +15,13 @@ export function MissionBriefScreen({ storyId, missionIndex, onNavigate }) {
15
15
  }
16
16
  });
17
17
  if (!story || !mission) {
18
- return _jsx(Text, { color: colors.error, children: "\u30DF\u30C3\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
18
+ return _jsxs(Text, { color: colors.error, children: ["\u30DF\u30C3\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (story=", storyId, ", mission=", missionIndex, ")"] });
19
19
  }
20
20
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.secondary, children: [story.emoji, " ", story.title, " - \u30DF\u30C3\u30B7\u30E7\u30F3 ", missionIndex + 1, "/", story.missions.length] }) }), _jsxs(Box, { borderStyle: "double", borderColor: colors.primary, paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.primary, children: mission.title }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.narrative, children: mission.narrative }) })] }), mission.newCommands && mission.newCommands.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.primary, children: "\uD83D\uDCD6 \u65B0\u3057\u3044\u30B3\u30DE\u30F3\u30C9:" }), mission.newCommands.map(cmdName => {
21
21
  const meta = getCommandMeta(cmdName);
22
22
  if (!meta)
23
23
  return null;
24
24
  return (_jsxs(Box, { marginLeft: 2, marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: meta.name }), _jsxs(Text, { color: colors.file, children: [" ", meta.description] }), meta.examples.map((ex, i) => (_jsxs(Text, { color: colors.muted, children: [' ', "$ ", ex.cmd.padEnd(28), " ", ex.desc] }, i)))] }, cmdName));
25
- })] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u76EE\u6A19:" }), mission.objectives.map((obj, i) => (_jsx(Text, { color: colors.file, children: ` ${i + 1}. ${obj.description}` }, obj.id)))] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Enter\u3067\u30B9\u30BF\u30FC\u30C8\u3001Esc\u3067\u623B\u308B" }) })] }));
25
+ })] })), mission.goal && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: ["\uD83C\uDFAF \u5230\u9054\u76EE\u6A19: ", mission.goal] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u76EE\u6A19:" }), mission.objectives.map((obj, i) => (_jsx(Text, { color: colors.file, children: ` ${i + 1}. ${obj.description}` }, obj.id)))] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Enter\u3067\u30B9\u30BF\u30FC\u30C8\u3001Esc\u3067\u623B\u308B" }) })] }));
26
26
  }
27
27
  //# sourceMappingURL=MissionBriefScreen.js.map
@@ -2,8 +2,9 @@ import type { Screen } from '../data/types.js';
2
2
  interface MissionCompleteScreenProps {
3
3
  storyId: string;
4
4
  missionIndex: number;
5
+ commandCount?: number;
5
6
  onNavigate: (screen: Screen) => void;
6
7
  }
7
- export declare function MissionCompleteScreen({ storyId, missionIndex, onNavigate }: MissionCompleteScreenProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function MissionCompleteScreen({ storyId, missionIndex, commandCount, onNavigate }: MissionCompleteScreenProps): import("react/jsx-runtime").JSX.Element;
8
9
  export {};
9
10
  //# sourceMappingURL=MissionCompleteScreen.d.ts.map
@@ -1,30 +1,55 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import { Box, Text, useInput } from 'ink';
3
4
  import { colors } from '../utils/colors.js';
4
5
  import { missionCompleteArt, storyCompleteArt } from '../utils/ascii-art.js';
5
6
  import { stories } from '../data/stories/index.js';
6
- export function MissionCompleteScreen({ storyId, missionIndex, onNavigate }) {
7
+ export function MissionCompleteScreen({ storyId, missionIndex, commandCount, onNavigate }) {
7
8
  const story = stories.find(s => s.id === storyId);
8
9
  const mission = story?.missions[missionIndex];
9
10
  const isLastMission = story ? missionIndex >= story.missions.length - 1 : false;
11
+ const review = mission?.review;
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ const [reviewState, setReviewState] = useState(review ? 'answering' : 'done');
14
+ const [wasCorrect, setWasCorrect] = useState(false);
10
15
  useInput((_input, key) => {
11
- if (key.return) {
12
- if (isLastMission) {
13
- onNavigate({ type: 'storySelect' });
16
+ if (key.escape) {
17
+ onNavigate({ type: 'storySelect' });
18
+ return;
19
+ }
20
+ if (reviewState === 'answering' && review) {
21
+ if (key.upArrow) {
22
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : review.choices.length - 1));
14
23
  }
15
- else {
16
- onNavigate({ type: 'missionBrief', storyId, missionIndex: missionIndex + 1 });
24
+ if (key.downArrow) {
25
+ setSelectedIndex(prev => (prev < review.choices.length - 1 ? prev + 1 : 0));
26
+ }
27
+ if (key.return) {
28
+ setWasCorrect(selectedIndex === review.correctIndex);
29
+ setReviewState('showResult');
17
30
  }
18
31
  }
19
- if (key.escape) {
20
- onNavigate({ type: 'storySelect' });
32
+ else if (reviewState === 'showResult') {
33
+ if (key.return) {
34
+ setReviewState('done');
35
+ }
36
+ }
37
+ else if (reviewState === 'done') {
38
+ if (key.return) {
39
+ if (isLastMission) {
40
+ onNavigate({ type: 'storySelect' });
41
+ }
42
+ else {
43
+ onNavigate({ type: 'missionBrief', storyId, missionIndex: missionIndex + 1 });
44
+ }
45
+ }
21
46
  }
22
47
  });
23
48
  if (!story || !mission) {
24
- return _jsx(Text, { color: colors.error, children: "\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
49
+ return _jsxs(Text, { color: colors.error, children: ["\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (story=", storyId, ", mission=", missionIndex, ")"] });
25
50
  }
26
- return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingX: 2, children: [_jsx(Text, { color: colors.success, bold: true, children: isLastMission ? storyCompleteArt : missionCompleteArt }), _jsx(Text, { bold: true, color: colors.primary, children: mission.title }), isLastMission && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.secondary, bold: true, children: ["\u300C", story.title, "\u300D\u3092\u30AF\u30EA\u30A2\u3057\u307E\u3057\u305F\uFF01"] }) })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: isLastMission
51
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingX: 2, children: [_jsx(Text, { color: colors.success, bold: true, children: isLastMission ? storyCompleteArt : missionCompleteArt }), _jsx(Text, { bold: true, color: colors.primary, children: mission.title }), commandCount != null && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["\u30B3\u30DE\u30F3\u30C9\u5B9F\u884C\u56DE\u6570: ", commandCount, "\u56DE"] }) })), isLastMission && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.secondary, bold: true, children: ["\u300C", story.title, "\u300D\u3092\u30AF\u30EA\u30A2\u3057\u307E\u3057\u305F\uFF01"] }) })), reviewState === 'answering' && review && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.secondary, bold: true, children: "\uD83D\uDCDD \u3075\u308A\u304B\u3048\u308A\u554F\u984C" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: review.question }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: review.choices.map((choice, i) => (_jsxs(Text, { color: i === selectedIndex ? colors.primary : colors.muted, children: [i === selectedIndex ? 'โ–ธ ' : ' ', choice] }, i))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193\u3067\u9078\u629E\u3001Enter\u3067\u56DE\u7B54" }) })] })), reviewState === 'showResult' && review && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: wasCorrect ? colors.success : colors.error, bold: true, children: wasCorrect ? 'โญ• ๆญฃ่งฃ๏ผ' : 'โŒ ไธๆญฃ่งฃ' }), !wasCorrect && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: ["\u6B63\u89E3: ", review.choices[review.correctIndex]] }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["\uD83D\uDCA1 ", review.explanation] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Enter\u3067\u7D9A\u3051\u308B" }) })] })), reviewState === 'done' && (_jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: isLastMission
27
52
  ? 'Enterใงใ‚นใƒˆใƒผใƒชใƒผ้ธๆŠžใซๆˆปใ‚‹'
28
- : 'EnterใงๆฌกใฎใƒŸใƒƒใ‚ทใƒงใƒณใธ' }) })] }));
53
+ : 'EnterใงๆฌกใฎใƒŸใƒƒใ‚ทใƒงใƒณใธ' }) }))] }));
29
54
  }
30
55
  //# sourceMappingURL=MissionCompleteScreen.js.map
@@ -3,7 +3,7 @@ interface TerminalScreenProps {
3
3
  storyId: string;
4
4
  missionIndex: number;
5
5
  onNavigate: (screen: Screen) => void;
6
- onMissionComplete: (storyId: string, missionId: string, hintsUsed: number) => void;
6
+ onMissionComplete: (storyId: string, missionId: string, hintsUsed: number, commandCount: number) => void;
7
7
  onStoryComplete: (storyId: string) => void;
8
8
  onCommandExecuted: () => void;
9
9
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState, useCallback } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { colors } from '../utils/colors.js';
@@ -11,9 +11,11 @@ import { TerminalPrompt } from '../components/TerminalPrompt.js';
11
11
  import { TerminalOutput } from '../components/TerminalOutput.js';
12
12
  import { ObjectivePanel } from '../components/ObjectivePanel.js';
13
13
  import { HintBar } from '../components/HintBar.js';
14
+ import { suggestCommand, checkMissionFeedback } from '../engine/CommandFeedback.js';
14
15
  export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionComplete, onStoryComplete, onCommandExecuted, }) {
15
16
  const story = stories.find(s => s.id === storyId);
16
17
  const mission = story?.missions[missionIndex];
18
+ const course = story?.course;
17
19
  const [missionEngine] = useState(() => {
18
20
  if (!mission)
19
21
  return null;
@@ -30,11 +32,20 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
30
32
  return null;
31
33
  return new TabCompletion(missionEngine.getFS());
32
34
  });
33
- const [outputLines, setOutputLines] = useState([]);
35
+ const [outputLines, setOutputLines] = useState(() => {
36
+ if (course === 'kids') {
37
+ return [{ text: '๐Ÿ’ก ใ‚ณใƒžใƒณใƒ‰ใ‚’ใซใ‚…ใ†ใ‚Šใ‚‡ใใ—ใฆ Enter ใ‚ญใƒผใ‚’ใŠใ—ใฆใญใ€‚Tab ใ‚ญใƒผใงใ˜ใฉใ†ใปใ‹ใ‚“ใงใใ‚‹ใ‚ˆใ€‚', type: 'system' }];
38
+ }
39
+ if (course === 'beginner') {
40
+ return [{ text: '๐Ÿ’ก ใ‚ณใƒžใƒณใƒ‰ใ‚’ๅ…ฅๅŠ›ใ—ใฆ Enter ใ‚’ๆŠผใ—ใฆใใ ใ•ใ„ใ€‚Tab ใ‚ญใƒผใง่ฃœๅฎŒใ€โ†‘โ†“ใ‚ญใƒผใงๅฑฅๆญดใ‚’ๅ‘ผใณๅ‡บใ›ใพใ™ใ€‚', type: 'system' }];
41
+ }
42
+ return [];
43
+ });
34
44
  const [commandHistory, setCommandHistory] = useState([]);
35
45
  const [completedObjectives, setCompletedObjectives] = useState([]);
36
46
  const [currentHint, setCurrentHint] = useState(null);
37
47
  const [hintLevel, setHintLevel] = useState(0);
48
+ const [commandCount, setCommandCount] = useState(0);
38
49
  useInput((input, key) => {
39
50
  if (key.escape) {
40
51
  onNavigate({ type: 'storySelect' });
@@ -61,6 +72,7 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
61
72
  if (!trimmed || !commandHandler || !missionEngine || !mission)
62
73
  return;
63
74
  setCommandHistory(prev => [...prev, trimmed]);
75
+ setCommandCount(prev => prev + 1);
64
76
  onCommandExecuted();
65
77
  const cwd = missionEngine.getFS().getCwd();
66
78
  setOutputLines(prev => [
@@ -90,7 +102,19 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
90
102
  return;
91
103
  }
92
104
  if (result.error) {
93
- setOutputLines(prev => [...prev, { text: result.error, type: 'error' }]);
105
+ let errorText = result.error;
106
+ if (course === 'kids' && errorText.endsWith(': command not found')) {
107
+ const cmdName = errorText.replace(': command not found', '');
108
+ errorText = `ใ€Œ${cmdName}ใ€ใจใ„ใ†ใ‚ณใƒžใƒณใƒ‰ใฏใชใ„ใ‚ˆใ€‚ใ‚‚ใ†ใ„ใกใฉใŸใ—ใ‹ใ‚ใฆใฟใฆใญใ€‚`;
109
+ }
110
+ setOutputLines(prev => [...prev, { text: errorText, type: 'error' }]);
111
+ // Command suggestion for typos
112
+ if (result.error.endsWith(': command not found')) {
113
+ const suggestion = suggestCommand(trimmed);
114
+ if (suggestion) {
115
+ setOutputLines(prev => [...prev, { text: `๐Ÿ’ก ใ‚‚ใ—ใ‹ใ—ใฆ: ${suggestion}`, type: 'system' }]);
116
+ }
117
+ }
94
118
  }
95
119
  else if (result.output) {
96
120
  const lines = result.output.split('\n');
@@ -99,6 +123,17 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
99
123
  ...lines.map(line => ({ text: line, type: 'output' })),
100
124
  ]);
101
125
  }
126
+ // Mission-specific feedback (check for all commands, not just errors)
127
+ const currentObjIndex = missionEngine.getCurrentObjectiveIndex();
128
+ if (currentObjIndex < mission.objectives.length) {
129
+ const obj = mission.objectives[currentObjIndex];
130
+ if (obj.feedbacks) {
131
+ const feedback = checkMissionFeedback(trimmed, obj.feedbacks);
132
+ if (feedback) {
133
+ setOutputLines(prev => [...prev, { text: `๐Ÿ’ก ${feedback}`, type: 'system' }]);
134
+ }
135
+ }
136
+ }
102
137
  const parts = trimmed.split(/\s+/);
103
138
  const cmd = parts[0];
104
139
  const args = parts.slice(1);
@@ -117,12 +152,12 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
117
152
  setCurrentHint(null);
118
153
  if (missionEngine.isAllComplete()) {
119
154
  setTimeout(() => {
120
- onMissionComplete(storyId, mission.id, hintEngine.getTotalHintsUsed());
155
+ onMissionComplete(storyId, mission.id, hintEngine.getTotalHintsUsed(), commandCount + 1);
121
156
  const isLast = story ? missionIndex >= story.missions.length - 1 : false;
122
157
  if (isLast) {
123
158
  onStoryComplete(storyId);
124
159
  }
125
- onNavigate({ type: 'missionComplete', storyId, missionIndex });
160
+ onNavigate({ type: 'missionComplete', storyId, missionIndex, commandCount: commandCount + 1 });
126
161
  }, 500);
127
162
  }
128
163
  }
@@ -134,6 +169,7 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
134
169
  missionIndex,
135
170
  story,
136
171
  completedObjectives,
172
+ commandCount,
137
173
  hintEngine,
138
174
  onCommandExecuted,
139
175
  onMissionComplete,
@@ -142,9 +178,13 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
142
178
  handleHintRequest,
143
179
  ]);
144
180
  if (!story || !mission || !missionEngine) {
145
- return _jsx(Text, { color: colors.error, children: "\u30DF\u30C3\u30B7\u30E7\u30F3\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
181
+ return _jsxs(Text, { color: colors.error, children: ["\u30DF\u30C3\u30B7\u30E7\u30F3\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (story=", storyId, ", mission=", missionIndex, ")"] });
146
182
  }
147
183
  const currentObj = mission.objectives[missionEngine.getCurrentObjectiveIndex()];
148
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: colors.secondary, children: [story.emoji, " ", mission.title] }), _jsx(Text, { color: colors.muted, children: "Esc: \u623B\u308B | Tab: \u88DC\u5B8C | Ctrl+H: \u30D2\u30F3\u30C8 | hint: \u30D2\u30F3\u30C8 | obj: \u76EE\u6A19" })] }), _jsx(ObjectivePanel, { objectives: mission.objectives, completedIds: completedObjectives }), currentHint && currentObj && (_jsx(HintBar, { hint: currentHint, currentLevel: hintLevel, maxLevel: currentObj.hints.length })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TerminalOutput, { lines: outputLines, maxLines: 30 }), _jsx(TerminalPrompt, { cwd: missionEngine.getFS().getCwd(), onSubmit: handleCommand, history: commandHistory, tabCompletion: tabCompletion ?? undefined })] })] }));
184
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: colors.secondary, children: [story.emoji, " ", mission.title] }), _jsx(Text, { color: colors.muted, children: course === 'kids'
185
+ ? 'Esc: ใ‚‚ใฉใ‚‹ | Tab: ใปใ‹ใ‚“ | hint: ใƒ’ใƒณใƒˆ | obj: ใ‚‚ใใฒใ‚‡ใ†'
186
+ : course === 'beginner'
187
+ ? 'Esc: ๆˆปใ‚‹ | Tab: ่ฃœๅฎŒ | Ctrl+H: ใƒ’ใƒณใƒˆ | hint: ใƒ’ใƒณใƒˆ | obj: ็›ฎๆจ™ไธ€่ฆง'
188
+ : 'Esc: ๆˆปใ‚‹ | Tab: ่ฃœๅฎŒ | Ctrl+H: ใƒ’ใƒณใƒˆ | hint: ใƒ’ใƒณใƒˆ | obj: ็›ฎๆจ™' })] }), _jsx(ObjectivePanel, { objectives: mission.objectives, completedIds: completedObjectives }), currentHint && currentObj && (_jsx(HintBar, { hint: currentHint, currentLevel: hintLevel, maxLevel: currentObj.hints.length })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TerminalOutput, { lines: outputLines, maxLines: 30 }), _jsx(TerminalPrompt, { cwd: missionEngine.getFS().getCwd(), onSubmit: handleCommand, history: commandHistory, tabCompletion: tabCompletion ?? undefined })] })] }));
149
189
  }
150
190
  //# sourceMappingURL=TerminalScreen.js.map
@@ -3,7 +3,7 @@ export declare function useGameState(): {
3
3
  screen: Screen;
4
4
  progress: GameProgress;
5
5
  navigateTo: (newScreen: Screen) => void;
6
- completeMission: (storyId: string, missionId: string, hintsUsed: number) => void;
6
+ completeMission: (storyId: string, missionId: string, hintsUsed: number, commandCount?: number) => void;
7
7
  completeStory: (storyId: string) => void;
8
8
  incrementCommands: () => void;
9
9
  resetAll: () => void;
@@ -12,7 +12,7 @@ export function useGameState() {
12
12
  const navigateTo = useCallback((newScreen) => {
13
13
  setScreen(newScreen);
14
14
  }, []);
15
- const completeMission = useCallback((storyId, missionId, hintsUsed) => {
15
+ const completeMission = useCallback((storyId, missionId, hintsUsed, commandCount) => {
16
16
  setProgress(prev => {
17
17
  const storyProg = prev.storyProgress[storyId] ?? {
18
18
  storyId,
@@ -31,6 +31,10 @@ export function useGameState() {
31
31
  ...storyProg.hintsUsed,
32
32
  [missionId]: hintsUsed,
33
33
  },
34
+ commandsPerMission: {
35
+ ...storyProg.commandsPerMission,
36
+ ...(commandCount != null ? { [missionId]: commandCount } : {}),
37
+ },
34
38
  };
35
39
  return {
36
40
  ...prev,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-quest",
3
- "version": "1.0.2",
3
+ "version": "1.1.2",
4
4
  "description": "ใ‚นใƒˆใƒผใƒชใƒผ้ง†ๅ‹•ๅž‹ใ‚ฟใƒผใƒŸใƒŠใƒซใ‚ณใƒžใƒณใƒ‰ๅญฆ็ฟ’CLI - Learn terminal commands through interactive stories",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",