terminal-quest 1.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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/bin/terminal-quest.js +2 -0
  3. package/dist/App.d.ts +2 -0
  4. package/dist/App.js +33 -0
  5. package/dist/components/HintBar.d.ts +9 -0
  6. package/dist/components/HintBar.js +11 -0
  7. package/dist/components/MenuItem.d.ts +9 -0
  8. package/dist/components/MenuItem.js +9 -0
  9. package/dist/components/ObjectivePanel.d.ts +8 -0
  10. package/dist/components/ObjectivePanel.js +10 -0
  11. package/dist/components/ProgressBar.d.ts +8 -0
  12. package/dist/components/ProgressBar.js +10 -0
  13. package/dist/components/TerminalOutput.d.ts +11 -0
  14. package/dist/components/TerminalOutput.js +25 -0
  15. package/dist/components/TerminalPrompt.d.ts +10 -0
  16. package/dist/components/TerminalPrompt.js +46 -0
  17. package/dist/data/commands-meta.d.ts +17 -0
  18. package/dist/data/commands-meta.js +256 -0
  19. package/dist/data/stories/00-beginner-pc.d.ts +3 -0
  20. package/dist/data/stories/00-beginner-pc.js +841 -0
  21. package/dist/data/stories/01-first-server.d.ts +3 -0
  22. package/dist/data/stories/01-first-server.js +364 -0
  23. package/dist/data/stories/02-messy-project.d.ts +3 -0
  24. package/dist/data/stories/02-messy-project.js +433 -0
  25. package/dist/data/stories/03-log-detective.d.ts +3 -0
  26. package/dist/data/stories/03-log-detective.js +291 -0
  27. package/dist/data/stories/04-deploy-day.d.ts +3 -0
  28. package/dist/data/stories/04-deploy-day.js +337 -0
  29. package/dist/data/stories/05-git-incident.d.ts +3 -0
  30. package/dist/data/stories/05-git-incident.js +534 -0
  31. package/dist/data/stories/06-pipe-master.d.ts +3 -0
  32. package/dist/data/stories/06-pipe-master.js +377 -0
  33. package/dist/data/stories/07-dangerous-commands.d.ts +3 -0
  34. package/dist/data/stories/07-dangerous-commands.js +411 -0
  35. package/dist/data/stories/index.d.ts +4 -0
  36. package/dist/data/stories/index.js +14 -0
  37. package/dist/data/stories/k1-treasure-hunt.d.ts +3 -0
  38. package/dist/data/stories/k1-treasure-hunt.js +815 -0
  39. package/dist/data/types.d.ts +97 -0
  40. package/dist/data/types.js +2 -0
  41. package/dist/engine/Achievements.d.ts +5 -0
  42. package/dist/engine/Achievements.js +93 -0
  43. package/dist/engine/CommandHandler.d.ts +17 -0
  44. package/dist/engine/CommandHandler.js +177 -0
  45. package/dist/engine/HintEngine.d.ts +10 -0
  46. package/dist/engine/HintEngine.js +26 -0
  47. package/dist/engine/MissionEngine.d.ts +17 -0
  48. package/dist/engine/MissionEngine.js +84 -0
  49. package/dist/engine/TabCompletion.d.ts +14 -0
  50. package/dist/engine/TabCompletion.js +93 -0
  51. package/dist/engine/VirtualFS.d.ts +33 -0
  52. package/dist/engine/VirtualFS.js +276 -0
  53. package/dist/engine/commands/cat.d.ts +4 -0
  54. package/dist/engine/commands/cat.js +18 -0
  55. package/dist/engine/commands/cd.d.ts +4 -0
  56. package/dist/engine/commands/cd.js +12 -0
  57. package/dist/engine/commands/chmod.d.ts +4 -0
  58. package/dist/engine/commands/chmod.js +98 -0
  59. package/dist/engine/commands/clear.d.ts +4 -0
  60. package/dist/engine/commands/clear.js +4 -0
  61. package/dist/engine/commands/cp.d.ts +4 -0
  62. package/dist/engine/commands/cp.js +26 -0
  63. package/dist/engine/commands/cut.d.ts +4 -0
  64. package/dist/engine/commands/cut.js +76 -0
  65. package/dist/engine/commands/echo.d.ts +4 -0
  66. package/dist/engine/commands/echo.js +4 -0
  67. package/dist/engine/commands/find.d.ts +4 -0
  68. package/dist/engine/commands/find.js +60 -0
  69. package/dist/engine/commands/git.d.ts +4 -0
  70. package/dist/engine/commands/git.js +510 -0
  71. package/dist/engine/commands/grep.d.ts +4 -0
  72. package/dist/engine/commands/grep.js +127 -0
  73. package/dist/engine/commands/head.d.ts +4 -0
  74. package/dist/engine/commands/head.js +59 -0
  75. package/dist/engine/commands/help.d.ts +4 -0
  76. package/dist/engine/commands/help.js +32 -0
  77. package/dist/engine/commands/hint.d.ts +4 -0
  78. package/dist/engine/commands/hint.js +4 -0
  79. package/dist/engine/commands/index.d.ts +8 -0
  80. package/dist/engine/commands/index.js +51 -0
  81. package/dist/engine/commands/ls.d.ts +4 -0
  82. package/dist/engine/commands/ls.js +50 -0
  83. package/dist/engine/commands/man.d.ts +4 -0
  84. package/dist/engine/commands/man.js +51 -0
  85. package/dist/engine/commands/mkdir.d.ts +4 -0
  86. package/dist/engine/commands/mkdir.js +31 -0
  87. package/dist/engine/commands/mv.d.ts +4 -0
  88. package/dist/engine/commands/mv.js +15 -0
  89. package/dist/engine/commands/pwd.d.ts +4 -0
  90. package/dist/engine/commands/pwd.js +4 -0
  91. package/dist/engine/commands/rm.d.ts +4 -0
  92. package/dist/engine/commands/rm.js +49 -0
  93. package/dist/engine/commands/sort.d.ts +4 -0
  94. package/dist/engine/commands/sort.js +100 -0
  95. package/dist/engine/commands/tail.d.ts +4 -0
  96. package/dist/engine/commands/tail.js +59 -0
  97. package/dist/engine/commands/touch.d.ts +4 -0
  98. package/dist/engine/commands/touch.js +18 -0
  99. package/dist/engine/commands/uniq.d.ts +4 -0
  100. package/dist/engine/commands/uniq.js +61 -0
  101. package/dist/engine/commands/wc.d.ts +4 -0
  102. package/dist/engine/commands/wc.js +67 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.js +6 -0
  105. package/dist/screens/MissionBriefScreen.d.ts +9 -0
  106. package/dist/screens/MissionBriefScreen.js +27 -0
  107. package/dist/screens/MissionCompleteScreen.d.ts +9 -0
  108. package/dist/screens/MissionCompleteScreen.js +30 -0
  109. package/dist/screens/ProgressScreen.d.ts +8 -0
  110. package/dist/screens/ProgressScreen.js +24 -0
  111. package/dist/screens/SettingsScreen.d.ts +8 -0
  112. package/dist/screens/SettingsScreen.js +45 -0
  113. package/dist/screens/StorySelectScreen.d.ts +8 -0
  114. package/dist/screens/StorySelectScreen.js +81 -0
  115. package/dist/screens/TerminalScreen.d.ts +12 -0
  116. package/dist/screens/TerminalScreen.js +150 -0
  117. package/dist/screens/TitleScreen.d.ts +7 -0
  118. package/dist/screens/TitleScreen.js +27 -0
  119. package/dist/state/GameState.d.ts +8 -0
  120. package/dist/state/GameState.js +12 -0
  121. package/dist/state/ProgressStore.d.ts +9 -0
  122. package/dist/state/ProgressStore.js +45 -0
  123. package/dist/state/useGameState.d.ts +11 -0
  124. package/dist/state/useGameState.js +92 -0
  125. package/dist/utils/ascii-art.d.ts +4 -0
  126. package/dist/utils/ascii-art.js +22 -0
  127. package/dist/utils/colors.d.ts +17 -0
  128. package/dist/utils/colors.js +17 -0
  129. package/dist/utils/text.d.ts +4 -0
  130. package/dist/utils/text.js +28 -0
  131. package/package.json +58 -0
@@ -0,0 +1,61 @@
1
+ export function uniq(fs, args) {
2
+ // Extract stdin
3
+ let stdin;
4
+ const stdinIdx = args.findIndex(a => a.startsWith('__stdin__:'));
5
+ if (stdinIdx !== -1) {
6
+ stdin = args[stdinIdx].slice('__stdin__:'.length);
7
+ args = [...args.slice(0, stdinIdx), ...args.slice(stdinIdx + 1)];
8
+ }
9
+ let showCount = false;
10
+ const files = [];
11
+ for (const arg of args) {
12
+ if (arg.startsWith('-') && arg.length > 1) {
13
+ for (const ch of arg.slice(1)) {
14
+ if (ch === 'c')
15
+ showCount = true;
16
+ else
17
+ return { output: '', error: `uniq: invalid option -- '${ch}'` };
18
+ }
19
+ }
20
+ else {
21
+ files.push(arg);
22
+ }
23
+ }
24
+ let content;
25
+ if (files.length > 0) {
26
+ try {
27
+ content = fs.readFile(files[0]);
28
+ }
29
+ catch (e) {
30
+ return { output: '', error: `uniq: ${e.message}` };
31
+ }
32
+ }
33
+ else if (stdin !== undefined) {
34
+ content = stdin;
35
+ }
36
+ else {
37
+ return { output: '', error: 'uniq: missing file operand' };
38
+ }
39
+ const lines = content.split('\n');
40
+ const result = [];
41
+ if (showCount) {
42
+ let i = 0;
43
+ while (i < lines.length) {
44
+ let count = 1;
45
+ while (i + count < lines.length && lines[i + count] === lines[i]) {
46
+ count++;
47
+ }
48
+ result.push(`${count} ${lines[i]}`);
49
+ i += count;
50
+ }
51
+ }
52
+ else {
53
+ for (let i = 0; i < lines.length; i++) {
54
+ if (i === 0 || lines[i] !== lines[i - 1]) {
55
+ result.push(lines[i]);
56
+ }
57
+ }
58
+ }
59
+ return { output: result.join('\n') };
60
+ }
61
+ //# sourceMappingURL=uniq.js.map
@@ -0,0 +1,4 @@
1
+ import { VirtualFS } from '../VirtualFS.js';
2
+ import type { CommandResult } from './index.js';
3
+ export declare function wc(fs: VirtualFS, args: string[]): CommandResult;
4
+ //# sourceMappingURL=wc.d.ts.map
@@ -0,0 +1,67 @@
1
+ export function wc(fs, args) {
2
+ // Extract stdin
3
+ let stdin;
4
+ const stdinIdx = args.findIndex(a => a.startsWith('__stdin__:'));
5
+ if (stdinIdx !== -1) {
6
+ stdin = args[stdinIdx].slice('__stdin__:'.length);
7
+ args = [...args.slice(0, stdinIdx), ...args.slice(stdinIdx + 1)];
8
+ }
9
+ let showLines = false;
10
+ let showWords = false;
11
+ let showBytes = false;
12
+ const files = [];
13
+ for (const arg of args) {
14
+ if (arg.startsWith('-') && arg.length > 1) {
15
+ for (const ch of arg.slice(1)) {
16
+ if (ch === 'l')
17
+ showLines = true;
18
+ else if (ch === 'w')
19
+ showWords = true;
20
+ else if (ch === 'c')
21
+ showBytes = true;
22
+ else
23
+ return { output: '', error: `wc: invalid option -- '${ch}'` };
24
+ }
25
+ }
26
+ else {
27
+ files.push(arg);
28
+ }
29
+ }
30
+ // If no flags specified, show all
31
+ if (!showLines && !showWords && !showBytes) {
32
+ showLines = true;
33
+ showWords = true;
34
+ showBytes = true;
35
+ }
36
+ let content;
37
+ let filename;
38
+ if (files.length > 0) {
39
+ filename = files[0];
40
+ try {
41
+ content = fs.readFile(filename);
42
+ }
43
+ catch (e) {
44
+ return { output: '', error: `wc: ${e.message}` };
45
+ }
46
+ }
47
+ else if (stdin !== undefined) {
48
+ content = stdin;
49
+ }
50
+ else {
51
+ return { output: '', error: 'wc: missing file operand' };
52
+ }
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
+ }
67
+ //# sourceMappingURL=wc.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import { App } from './App.js';
5
+ render(React.createElement(App));
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import type { Screen } from '../data/types.js';
2
+ interface MissionBriefScreenProps {
3
+ storyId: string;
4
+ missionIndex: number;
5
+ onNavigate: (screen: Screen) => void;
6
+ }
7
+ export declare function MissionBriefScreen({ storyId, missionIndex, onNavigate }: MissionBriefScreenProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=MissionBriefScreen.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { colors } from '../utils/colors.js';
4
+ import { stories } from '../data/stories/index.js';
5
+ import { getCommandMeta } from '../data/commands-meta.js';
6
+ export function MissionBriefScreen({ storyId, missionIndex, onNavigate }) {
7
+ const story = stories.find(s => s.id === storyId);
8
+ const mission = story?.missions[missionIndex];
9
+ useInput((_input, key) => {
10
+ if (key.return) {
11
+ onNavigate({ type: 'terminal', storyId, missionIndex });
12
+ }
13
+ if (key.escape) {
14
+ onNavigate({ type: 'storySelect' });
15
+ }
16
+ });
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" });
19
+ }
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
+ const meta = getCommandMeta(cmdName);
22
+ if (!meta)
23
+ return null;
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" }) })] }));
26
+ }
27
+ //# sourceMappingURL=MissionBriefScreen.js.map
@@ -0,0 +1,9 @@
1
+ import type { Screen } from '../data/types.js';
2
+ interface MissionCompleteScreenProps {
3
+ storyId: string;
4
+ missionIndex: number;
5
+ onNavigate: (screen: Screen) => void;
6
+ }
7
+ export declare function MissionCompleteScreen({ storyId, missionIndex, onNavigate }: MissionCompleteScreenProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=MissionCompleteScreen.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { colors } from '../utils/colors.js';
4
+ import { missionCompleteArt, storyCompleteArt } from '../utils/ascii-art.js';
5
+ import { stories } from '../data/stories/index.js';
6
+ export function MissionCompleteScreen({ storyId, missionIndex, onNavigate }) {
7
+ const story = stories.find(s => s.id === storyId);
8
+ const mission = story?.missions[missionIndex];
9
+ const isLastMission = story ? missionIndex >= story.missions.length - 1 : false;
10
+ useInput((_input, key) => {
11
+ if (key.return) {
12
+ if (isLastMission) {
13
+ onNavigate({ type: 'storySelect' });
14
+ }
15
+ else {
16
+ onNavigate({ type: 'missionBrief', storyId, missionIndex: missionIndex + 1 });
17
+ }
18
+ }
19
+ if (key.escape) {
20
+ onNavigate({ type: 'storySelect' });
21
+ }
22
+ });
23
+ if (!story || !mission) {
24
+ return _jsx(Text, { color: colors.error, children: "\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
25
+ }
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
27
+ ? 'Enterでストーリー選択に戻る'
28
+ : 'Enterで次のミッションへ' }) })] }));
29
+ }
30
+ //# sourceMappingURL=MissionCompleteScreen.js.map
@@ -0,0 +1,8 @@
1
+ import type { GameProgress, Screen } from '../data/types.js';
2
+ interface ProgressScreenProps {
3
+ progress: GameProgress;
4
+ onNavigate: (screen: Screen) => void;
5
+ }
6
+ export declare function ProgressScreen({ progress, onNavigate }: ProgressScreenProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=ProgressScreen.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { colors } from '../utils/colors.js';
4
+ import { ProgressBar } from '../components/ProgressBar.js';
5
+ import { stories } from '../data/stories/index.js';
6
+ import { achievements } from '../engine/Achievements.js';
7
+ export function ProgressScreen({ progress, onNavigate }) {
8
+ useInput((_input, key) => {
9
+ if (key.escape || key.return) {
10
+ onNavigate({ type: 'title' });
11
+ }
12
+ });
13
+ const totalMissions = stories.reduce((sum, s) => sum + s.missions.length, 0);
14
+ const completedMissions = Object.values(progress.storyProgress).reduce((sum, sp) => sum + sp.completedMissions.length, 0);
15
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u25C6 \u9032\u6357\u72B6\u6CC1 \u25C6" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.file, children: "\u7DCF\u5408\u9032\u6357:" }), _jsx(ProgressBar, { current: completedMissions, total: totalMissions, width: 30 })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.file, children: ["\u5B9F\u884C\u30B3\u30DE\u30F3\u30C9\u6570: ", _jsx(Text, { color: colors.primary, children: progress.totalCommandsExecuted })] }), _jsxs(Text, { color: colors.file, children: ["\u4F7F\u7528\u30D2\u30F3\u30C8\u6570: ", _jsx(Text, { color: colors.primary, children: progress.totalHintsUsed })] }), _jsxs(Text, { color: colors.file, children: ["\u30AF\u30EA\u30A2\u30B9\u30C8\u30FC\u30EA\u30FC: ", _jsxs(Text, { color: colors.primary, children: [progress.completedStories.length, "/", stories.length] })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u30B9\u30C8\u30FC\u30EA\u30FC\u5225:" }), stories.map(story => {
16
+ const sp = progress.storyProgress[story.id];
17
+ const completed = sp?.completedMissions.length ?? 0;
18
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.file, children: [story.emoji, " ", story.title] }), _jsx(Box, { marginLeft: 2, children: _jsx(ProgressBar, { current: completed, total: story.missions.length, width: 15 }) })] }, story.id));
19
+ })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u30D0\u30C3\u30B8:" }), achievements.map(badge => {
20
+ const earned = (progress.achievements ?? []).includes(badge.id);
21
+ return (_jsxs(Text, { color: earned ? colors.success : colors.muted, children: [earned ? badge.emoji : '🔒', " ", badge.title, " - ", badge.description] }, badge.id));
22
+ })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Enter/Esc\u3067\u623B\u308B" }) })] }));
23
+ }
24
+ //# sourceMappingURL=ProgressScreen.js.map
@@ -0,0 +1,8 @@
1
+ import type { Screen } from '../data/types.js';
2
+ interface SettingsScreenProps {
3
+ onNavigate: (screen: Screen) => void;
4
+ onReset: () => void;
5
+ }
6
+ export declare function SettingsScreen({ onNavigate, onReset }: SettingsScreenProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=SettingsScreen.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { colors } from '../utils/colors.js';
5
+ import { MenuItem } from '../components/MenuItem.js';
6
+ export function SettingsScreen({ onNavigate, onReset }) {
7
+ const [selectedIndex, setSelectedIndex] = useState(0);
8
+ const [confirmReset, setConfirmReset] = useState(false);
9
+ const menuItems = [
10
+ { label: '進捗をリセット', action: 'reset' },
11
+ { label: '← タイトルに戻る', action: 'back' },
12
+ ];
13
+ useInput((_input, key) => {
14
+ if (confirmReset) {
15
+ if (_input === 'y' || _input === 'Y') {
16
+ onReset();
17
+ setConfirmReset(false);
18
+ }
19
+ else {
20
+ setConfirmReset(false);
21
+ }
22
+ return;
23
+ }
24
+ if (key.upArrow) {
25
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : menuItems.length - 1));
26
+ }
27
+ if (key.downArrow) {
28
+ setSelectedIndex(prev => (prev < menuItems.length - 1 ? prev + 1 : 0));
29
+ }
30
+ if (key.return) {
31
+ const item = menuItems[selectedIndex];
32
+ if (item.action === 'reset') {
33
+ setConfirmReset(true);
34
+ }
35
+ else if (item.action === 'back') {
36
+ onNavigate({ type: 'title' });
37
+ }
38
+ }
39
+ if (key.escape) {
40
+ onNavigate({ type: 'title' });
41
+ }
42
+ });
43
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u25C6 \u8A2D\u5B9A \u25C6" }), confirmReset ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u672C\u5F53\u306B\u5168\u3066\u306E\u9032\u6357\u3092\u30EA\u30BB\u30C3\u30C8\u3057\u307E\u3059\u304B\uFF1F" }), _jsx(Text, { color: colors.muted, children: "y: \u306F\u3044 / \u305D\u306E\u4ED6\u306E\u30AD\u30FC: \u3044\u3044\u3048" })] })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: menuItems.map((item, i) => (_jsx(MenuItem, { label: item.label, isSelected: i === selectedIndex }, item.label))) })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Esc\u3067\u623B\u308B" }) })] }));
44
+ }
45
+ //# sourceMappingURL=SettingsScreen.js.map
@@ -0,0 +1,8 @@
1
+ import type { GameProgress, Screen } from '../data/types.js';
2
+ interface StorySelectScreenProps {
3
+ progress: GameProgress;
4
+ onNavigate: (screen: Screen) => void;
5
+ }
6
+ export declare function StorySelectScreen({ progress, onNavigate }: StorySelectScreenProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=StorySelectScreen.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { colors } from '../utils/colors.js';
5
+ import { MenuItem } from '../components/MenuItem.js';
6
+ import { ProgressBar } from '../components/ProgressBar.js';
7
+ import { stories } from '../data/stories/index.js';
8
+ import { isStoryUnlocked } from '../state/ProgressStore.js';
9
+ const courseConfig = [
10
+ { key: 'kids', label: '✨ 小学生向けコース', emoji: '✨' },
11
+ { key: 'beginner', label: '💻 はじめてコース', emoji: '💻' },
12
+ { key: 'engineer', label: '🖥️ エンジニアコース', emoji: '🖥️' },
13
+ ];
14
+ export function StorySelectScreen({ progress, onNavigate }) {
15
+ const [selectedIndex, setSelectedIndex] = useState(0);
16
+ const groupedStories = useMemo(() => {
17
+ const groups = [];
18
+ let flatIndex = 0;
19
+ for (const course of courseConfig) {
20
+ const courseStories = stories.filter(s => s.course === course.key);
21
+ if (courseStories.length === 0)
22
+ continue;
23
+ groups.push({ type: 'header', label: course.label });
24
+ for (const story of courseStories) {
25
+ groups.push({ type: 'story', story, flatIndex });
26
+ flatIndex++;
27
+ }
28
+ }
29
+ // courseが未設定のストーリーがあれば最後に追加
30
+ const uncategorized = stories.filter(s => !s.course);
31
+ if (uncategorized.length > 0) {
32
+ groups.push({ type: 'header', label: '📚 その他' });
33
+ for (const story of uncategorized) {
34
+ groups.push({ type: 'story', story, flatIndex });
35
+ flatIndex++;
36
+ }
37
+ }
38
+ return { groups, totalStories: flatIndex };
39
+ }, []);
40
+ const { groups, totalStories } = groupedStories;
41
+ useInput((_input, key) => {
42
+ if (key.upArrow) {
43
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : totalStories));
44
+ }
45
+ if (key.downArrow) {
46
+ setSelectedIndex(prev => (prev < totalStories ? prev + 1 : 0));
47
+ }
48
+ if (key.return) {
49
+ if (selectedIndex === totalStories) {
50
+ onNavigate({ type: 'title' });
51
+ return;
52
+ }
53
+ const storyItem = groups.find(g => g.type === 'story' && g.flatIndex === selectedIndex);
54
+ if (!storyItem || storyItem.type !== 'story')
55
+ return;
56
+ const story = storyItem.story;
57
+ const unlocked = isStoryUnlocked(progress, story.id, stories);
58
+ if (!unlocked)
59
+ return;
60
+ const storyProg = progress.storyProgress[story.id];
61
+ const missionIndex = storyProg ? storyProg.currentMissionIndex : 0;
62
+ const clampedIndex = Math.min(missionIndex, story.missions.length - 1);
63
+ onNavigate({ type: 'missionBrief', storyId: story.id, missionIndex: clampedIndex });
64
+ }
65
+ if (key.escape) {
66
+ onNavigate({ type: 'title' });
67
+ }
68
+ });
69
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.secondary, children: "\u25C6 \u30B3\u30FC\u30B9\u3092\u9078\u629E \u25C6" }) }), groups.map((item, i) => {
70
+ if (item.type === 'header') {
71
+ return (_jsx(Box, { marginTop: i > 0 ? 1 : 0, marginBottom: 0, children: _jsxs(Text, { bold: true, color: colors.muted, children: ["\u2500\u2500 ", item.label, " \u2500\u2500"] }) }, `header-${i}`));
72
+ }
73
+ const { story, flatIndex } = item;
74
+ const unlocked = isStoryUnlocked(progress, story.id, stories);
75
+ const storyProg = progress.storyProgress[story.id];
76
+ const completed = storyProg?.completedMissions.length ?? 0;
77
+ const total = story.missions.length;
78
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsx(MenuItem, { label: `${story.emoji} ${story.title}`, isSelected: flatIndex === selectedIndex, isLocked: !unlocked, description: unlocked ? story.description : undefined }), unlocked && (_jsx(Box, { marginLeft: 4, children: _jsx(ProgressBar, { current: completed, total: total, width: 15 }) }))] }, story.id));
79
+ }), _jsx(Box, { marginTop: 1, children: _jsx(MenuItem, { label: "\u2190 \u30BF\u30A4\u30C8\u30EB\u306B\u623B\u308B", isSelected: selectedIndex === totalStories }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193\u3067\u9078\u629E\u3001Enter\u3067\u6C7A\u5B9A\u3001Esc\u3067\u623B\u308B" }) })] }));
80
+ }
81
+ //# sourceMappingURL=StorySelectScreen.js.map
@@ -0,0 +1,12 @@
1
+ import type { Screen } from '../data/types.js';
2
+ interface TerminalScreenProps {
3
+ storyId: string;
4
+ missionIndex: number;
5
+ onNavigate: (screen: Screen) => void;
6
+ onMissionComplete: (storyId: string, missionId: string, hintsUsed: number) => void;
7
+ onStoryComplete: (storyId: string) => void;
8
+ onCommandExecuted: () => void;
9
+ }
10
+ export declare function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionComplete, onStoryComplete, onCommandExecuted, }: TerminalScreenProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=TerminalScreen.d.ts.map
@@ -0,0 +1,150 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { colors } from '../utils/colors.js';
5
+ import { stories } from '../data/stories/index.js';
6
+ import { CommandHandler } from '../engine/CommandHandler.js';
7
+ import { MissionEngine } from '../engine/MissionEngine.js';
8
+ import { HintEngine } from '../engine/HintEngine.js';
9
+ import { TabCompletion } from '../engine/TabCompletion.js';
10
+ import { TerminalPrompt } from '../components/TerminalPrompt.js';
11
+ import { TerminalOutput } from '../components/TerminalOutput.js';
12
+ import { ObjectivePanel } from '../components/ObjectivePanel.js';
13
+ import { HintBar } from '../components/HintBar.js';
14
+ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionComplete, onStoryComplete, onCommandExecuted, }) {
15
+ const story = stories.find(s => s.id === storyId);
16
+ const mission = story?.missions[missionIndex];
17
+ const [missionEngine] = useState(() => {
18
+ if (!mission)
19
+ return null;
20
+ return new MissionEngine(mission);
21
+ });
22
+ const [commandHandler] = useState(() => {
23
+ if (!missionEngine)
24
+ return null;
25
+ return new CommandHandler(missionEngine.getFS());
26
+ });
27
+ const [hintEngine] = useState(() => new HintEngine());
28
+ const [tabCompletion] = useState(() => {
29
+ if (!missionEngine)
30
+ return null;
31
+ return new TabCompletion(missionEngine.getFS());
32
+ });
33
+ const [outputLines, setOutputLines] = useState([]);
34
+ const [commandHistory, setCommandHistory] = useState([]);
35
+ const [completedObjectives, setCompletedObjectives] = useState([]);
36
+ const [currentHint, setCurrentHint] = useState(null);
37
+ const [hintLevel, setHintLevel] = useState(0);
38
+ useInput((input, key) => {
39
+ if (key.escape) {
40
+ onNavigate({ type: 'storySelect' });
41
+ }
42
+ if (input === 'h' && key.ctrl && mission && missionEngine) {
43
+ handleHintRequest();
44
+ }
45
+ });
46
+ const handleHintRequest = useCallback(() => {
47
+ if (!mission || !missionEngine)
48
+ return;
49
+ const currentObjIndex = missionEngine.getCurrentObjectiveIndex();
50
+ if (currentObjIndex >= mission.objectives.length)
51
+ return;
52
+ const obj = mission.objectives[currentObjIndex];
53
+ const hint = hintEngine.getNextHint(obj.id, obj.hints);
54
+ if (hint) {
55
+ setCurrentHint(hint);
56
+ setHintLevel(hintEngine.getCurrentLevel(obj.id));
57
+ }
58
+ }, [mission, missionEngine, hintEngine]);
59
+ const handleCommand = useCallback((input) => {
60
+ const trimmed = input.trim();
61
+ if (!trimmed || !commandHandler || !missionEngine || !mission)
62
+ return;
63
+ setCommandHistory(prev => [...prev, trimmed]);
64
+ onCommandExecuted();
65
+ const cwd = missionEngine.getFS().getCwd();
66
+ setOutputLines(prev => [
67
+ ...prev,
68
+ { text: `${cwd} $ ${trimmed}`, type: 'system' },
69
+ ]);
70
+ if (trimmed === 'hint') {
71
+ handleHintRequest();
72
+ return;
73
+ }
74
+ if (trimmed === 'objectives' || trimmed === 'obj') {
75
+ mission.objectives.forEach((obj, i) => {
76
+ const done = completedObjectives.includes(obj.id);
77
+ setOutputLines(prev => [
78
+ ...prev,
79
+ {
80
+ text: `${done ? '✓' : '○'} ${i + 1}. ${obj.description}`,
81
+ type: done ? 'success' : 'output',
82
+ },
83
+ ]);
84
+ });
85
+ return;
86
+ }
87
+ const result = commandHandler.execute(trimmed);
88
+ if (result.output === 'CLEAR_SCREEN') {
89
+ setOutputLines([]);
90
+ return;
91
+ }
92
+ if (result.error) {
93
+ setOutputLines(prev => [...prev, { text: result.error, type: 'error' }]);
94
+ }
95
+ else if (result.output) {
96
+ const lines = result.output.split('\n');
97
+ setOutputLines(prev => [
98
+ ...prev,
99
+ ...lines.map(line => ({ text: line, type: 'output' })),
100
+ ]);
101
+ }
102
+ const parts = trimmed.split(/\s+/);
103
+ const cmd = parts[0];
104
+ const args = parts.slice(1);
105
+ const newlyCompleted = missionEngine.checkObjectives(cmd, args, result.output);
106
+ if (newlyCompleted.length > 0) {
107
+ setCompletedObjectives(prev => [...prev, ...newlyCompleted]);
108
+ for (const objId of newlyCompleted) {
109
+ const obj = mission.objectives.find(o => o.id === objId);
110
+ if (obj) {
111
+ setOutputLines(prev => [
112
+ ...prev,
113
+ { text: `✓ 目標達成: ${obj.description}`, type: 'success' },
114
+ ]);
115
+ }
116
+ }
117
+ setCurrentHint(null);
118
+ if (missionEngine.isAllComplete()) {
119
+ setTimeout(() => {
120
+ onMissionComplete(storyId, mission.id, hintEngine.getTotalHintsUsed());
121
+ const isLast = story ? missionIndex >= story.missions.length - 1 : false;
122
+ if (isLast) {
123
+ onStoryComplete(storyId);
124
+ }
125
+ onNavigate({ type: 'missionComplete', storyId, missionIndex });
126
+ }, 500);
127
+ }
128
+ }
129
+ }, [
130
+ commandHandler,
131
+ missionEngine,
132
+ mission,
133
+ storyId,
134
+ missionIndex,
135
+ story,
136
+ completedObjectives,
137
+ hintEngine,
138
+ onCommandExecuted,
139
+ onMissionComplete,
140
+ onStoryComplete,
141
+ onNavigate,
142
+ handleHintRequest,
143
+ ]);
144
+ 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" });
146
+ }
147
+ 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 })] })] }));
149
+ }
150
+ //# sourceMappingURL=TerminalScreen.js.map
@@ -0,0 +1,7 @@
1
+ import type { Screen } from '../data/types.js';
2
+ interface TitleScreenProps {
3
+ onNavigate: (screen: Screen) => void;
4
+ }
5
+ export declare function TitleScreen({ onNavigate }: TitleScreenProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=TitleScreen.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { colors } from '../utils/colors.js';
5
+ import { titleArt } from '../utils/ascii-art.js';
6
+ import { MenuItem } from '../components/MenuItem.js';
7
+ const menuItems = [
8
+ { label: 'ゲームスタート', screen: { type: 'storySelect' } },
9
+ { label: '進捗を見る', screen: { type: 'progress' } },
10
+ { label: '設定', screen: { type: 'settings' } },
11
+ ];
12
+ export function TitleScreen({ onNavigate }) {
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ useInput((_input, key) => {
15
+ if (key.upArrow) {
16
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : menuItems.length - 1));
17
+ }
18
+ if (key.downArrow) {
19
+ setSelectedIndex(prev => (prev < menuItems.length - 1 ? prev + 1 : 0));
20
+ }
21
+ if (key.return) {
22
+ onNavigate(menuItems[selectedIndex].screen);
23
+ }
24
+ });
25
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: colors.primary, children: titleArt }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: menuItems.map((item, i) => (_jsx(MenuItem, { label: item.label, isSelected: i === selectedIndex }, item.label))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193\u3067\u9078\u629E\u3001Enter\u3067\u6C7A\u5B9A\u3001Ctrl+C\u3067\u7D42\u4E86" }) })] }));
26
+ }
27
+ //# sourceMappingURL=TitleScreen.js.map
@@ -0,0 +1,8 @@
1
+ import type { GameProgress, Screen } from '../data/types.js';
2
+ export interface GameState {
3
+ screen: Screen;
4
+ progress: GameProgress;
5
+ }
6
+ export declare const initialProgress: GameProgress;
7
+ export declare const initialGameState: GameState;
8
+ //# sourceMappingURL=GameState.d.ts.map
@@ -0,0 +1,12 @@
1
+ export const initialProgress = {
2
+ completedStories: [],
3
+ storyProgress: {},
4
+ totalCommandsExecuted: 0,
5
+ totalHintsUsed: 0,
6
+ achievements: [],
7
+ };
8
+ export const initialGameState = {
9
+ screen: { type: 'title' },
10
+ progress: initialProgress,
11
+ };
12
+ //# sourceMappingURL=GameState.js.map
@@ -0,0 +1,9 @@
1
+ import type { GameProgress } from '../data/types.js';
2
+ export declare function loadProgress(): GameProgress;
3
+ export declare function saveProgress(progress: GameProgress): void;
4
+ export declare function resetProgress(): void;
5
+ export declare function isStoryUnlocked(progress: GameProgress, storyId: string, allStories: Array<{
6
+ id: string;
7
+ unlockRequires: string[];
8
+ }>): boolean;
9
+ //# sourceMappingURL=ProgressStore.d.ts.map