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,45 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { initialProgress } from './GameState.js';
5
+ const SAVE_DIR = join(homedir(), '.terminal-quest');
6
+ const SAVE_FILE = join(SAVE_DIR, 'progress.json');
7
+ export function loadProgress() {
8
+ try {
9
+ if (!existsSync(SAVE_FILE))
10
+ return { ...initialProgress };
11
+ const data = readFileSync(SAVE_FILE, 'utf-8');
12
+ const parsed = JSON.parse(data);
13
+ // Migration: add achievements if missing
14
+ if (!parsed.achievements) {
15
+ parsed.achievements = [];
16
+ }
17
+ return parsed;
18
+ }
19
+ catch {
20
+ return { ...initialProgress };
21
+ }
22
+ }
23
+ export function saveProgress(progress) {
24
+ try {
25
+ if (!existsSync(SAVE_DIR)) {
26
+ mkdirSync(SAVE_DIR, { recursive: true });
27
+ }
28
+ writeFileSync(SAVE_FILE, JSON.stringify(progress, null, 2), 'utf-8');
29
+ }
30
+ catch {
31
+ // 保存失敗は無視(ゲーム継続可能)
32
+ }
33
+ }
34
+ export function resetProgress() {
35
+ saveProgress(initialProgress);
36
+ }
37
+ export function isStoryUnlocked(progress, storyId, allStories) {
38
+ const story = allStories.find(s => s.id === storyId);
39
+ if (!story)
40
+ return false;
41
+ if (story.unlockRequires.length === 0)
42
+ return true;
43
+ return story.unlockRequires.every(reqId => progress.completedStories.includes(reqId));
44
+ }
45
+ //# sourceMappingURL=ProgressStore.js.map
@@ -0,0 +1,11 @@
1
+ import type { GameProgress, Screen } from '../data/types.js';
2
+ export declare function useGameState(): {
3
+ screen: Screen;
4
+ progress: GameProgress;
5
+ navigateTo: (newScreen: Screen) => void;
6
+ completeMission: (storyId: string, missionId: string, hintsUsed: number) => void;
7
+ completeStory: (storyId: string) => void;
8
+ incrementCommands: () => void;
9
+ resetAll: () => void;
10
+ };
11
+ //# sourceMappingURL=useGameState.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { initialGameState } from './GameState.js';
3
+ import { loadProgress, saveProgress } from './ProgressStore.js';
4
+ import { checkAchievements } from '../engine/Achievements.js';
5
+ import { stories } from '../data/stories/index.js';
6
+ export function useGameState() {
7
+ const [screen, setScreen] = useState(initialGameState.screen);
8
+ const [progress, setProgress] = useState(() => loadProgress());
9
+ useEffect(() => {
10
+ saveProgress(progress);
11
+ }, [progress]);
12
+ const navigateTo = useCallback((newScreen) => {
13
+ setScreen(newScreen);
14
+ }, []);
15
+ const completeMission = useCallback((storyId, missionId, hintsUsed) => {
16
+ setProgress(prev => {
17
+ const storyProg = prev.storyProgress[storyId] ?? {
18
+ storyId,
19
+ completedMissions: [],
20
+ currentMissionIndex: 0,
21
+ hintsUsed: {},
22
+ };
23
+ if (storyProg.completedMissions.includes(missionId))
24
+ return prev;
25
+ const newCompletedMissions = [...storyProg.completedMissions, missionId];
26
+ const newStoryProg = {
27
+ ...storyProg,
28
+ completedMissions: newCompletedMissions,
29
+ currentMissionIndex: storyProg.currentMissionIndex + 1,
30
+ hintsUsed: {
31
+ ...storyProg.hintsUsed,
32
+ [missionId]: hintsUsed,
33
+ },
34
+ };
35
+ return {
36
+ ...prev,
37
+ storyProgress: {
38
+ ...prev.storyProgress,
39
+ [storyId]: newStoryProg,
40
+ },
41
+ totalHintsUsed: prev.totalHintsUsed + hintsUsed,
42
+ };
43
+ });
44
+ }, []);
45
+ const completeStory = useCallback((storyId) => {
46
+ setProgress(prev => {
47
+ if (prev.completedStories.includes(storyId))
48
+ return prev;
49
+ return {
50
+ ...prev,
51
+ completedStories: [...prev.completedStories, storyId],
52
+ };
53
+ });
54
+ }, []);
55
+ const incrementCommands = useCallback(() => {
56
+ setProgress(prev => ({
57
+ ...prev,
58
+ totalCommandsExecuted: prev.totalCommandsExecuted + 1,
59
+ }));
60
+ }, []);
61
+ useEffect(() => {
62
+ const engineerStoryIds = stories
63
+ .filter(s => s.course === 'engineer')
64
+ .map(s => s.id);
65
+ const newAchievements = checkAchievements(progress, engineerStoryIds);
66
+ if (newAchievements.length > 0) {
67
+ setProgress(prev => {
68
+ const currentAchievements = prev.achievements ?? [];
69
+ const toAdd = newAchievements.filter(a => !currentAchievements.includes(a));
70
+ if (toAdd.length === 0)
71
+ return prev;
72
+ return {
73
+ ...prev,
74
+ achievements: [...currentAchievements, ...toAdd],
75
+ };
76
+ });
77
+ }
78
+ }, [progress]);
79
+ const resetAll = useCallback(() => {
80
+ setProgress(initialGameState.progress);
81
+ }, []);
82
+ return {
83
+ screen,
84
+ progress,
85
+ navigateTo,
86
+ completeMission,
87
+ completeStory,
88
+ incrementCommands,
89
+ resetAll,
90
+ };
91
+ }
92
+ //# sourceMappingURL=useGameState.js.map
@@ -0,0 +1,4 @@
1
+ export declare const titleArt = "\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 \u2551\n\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2551\n\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551 \u2551\n\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551 \u2551\n\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2551\n\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551 \u2551\n\u2551 \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u2551\n\u2551 \u2551\n\u2551 \u25C6 TERMINAL QUEST \u25C6 \u2551\n\u2551 \uFF5E\u30BF\u30FC\u30DF\u30CA\u30EB\u306E\u5192\u967A\uFF5E \u2551\n\u2551 \u2551\n\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n";
2
+ export declare const missionCompleteArt = "\n \u2605 \u30DF\u30C3\u30B7\u30E7\u30F3\u5B8C\u4E86\uFF01 \u2605\n";
3
+ export declare const storyCompleteArt = "\n \u25C6\u25C6\u25C6 \u30B9\u30C8\u30FC\u30EA\u30FC\u30AF\u30EA\u30A2\uFF01 \u25C6\u25C6\u25C6\n";
4
+ //# sourceMappingURL=ascii-art.d.ts.map
@@ -0,0 +1,22 @@
1
+ export const titleArt = `
2
+ ╔══════════════════════════════════════════╗
3
+ ║ ║
4
+ ║ ████████╗███████╗██████╗ ███╗ ███╗ ║
5
+ ║ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║ ║
6
+ ║ ██║ █████╗ ██████╔╝██╔████╔██║ ║
7
+ ║ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║ ║
8
+ ║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║ ║
9
+ ║ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ║
10
+ ║ ║
11
+ ║ ◆ TERMINAL QUEST ◆ ║
12
+ ║ ~ターミナルの冒険~ ║
13
+ ║ ║
14
+ ╚══════════════════════════════════════════╝
15
+ `;
16
+ export const missionCompleteArt = `
17
+ ★ ミッション完了! ★
18
+ `;
19
+ export const storyCompleteArt = `
20
+ ◆◆◆ ストーリークリア! ◆◆◆
21
+ `;
22
+ //# sourceMappingURL=ascii-art.js.map
@@ -0,0 +1,17 @@
1
+ export declare const colors: {
2
+ primary: string;
3
+ secondary: string;
4
+ error: string;
5
+ warning: string;
6
+ success: string;
7
+ muted: string;
8
+ info: string;
9
+ directory: string;
10
+ file: string;
11
+ prompt: string;
12
+ narrative: string;
13
+ hint1: string;
14
+ hint2: string;
15
+ hint3: string;
16
+ };
17
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1,17 @@
1
+ export const colors = {
2
+ primary: '#00D4AA',
3
+ secondary: '#FFD700',
4
+ error: '#FF6B6B',
5
+ warning: '#FFA500',
6
+ success: '#00FF88',
7
+ muted: '#888888',
8
+ info: '#87CEEB',
9
+ directory: '#5FAFFF',
10
+ file: '#FFFFFF',
11
+ prompt: '#00D4AA',
12
+ narrative: '#DDA0DD',
13
+ hint1: '#87CEEB',
14
+ hint2: '#FFD700',
15
+ hint3: '#FF6B6B',
16
+ };
17
+ //# sourceMappingURL=colors.js.map
@@ -0,0 +1,4 @@
1
+ export declare function truncate(text: string, maxLength: number): string;
2
+ export declare function padRight(text: string, width: number): string;
3
+ export declare function wrapText(text: string, maxWidth: number): string[];
4
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1,28 @@
1
+ export function truncate(text, maxLength) {
2
+ if (text.length <= maxLength)
3
+ return text;
4
+ return text.slice(0, maxLength - 3) + '...';
5
+ }
6
+ export function padRight(text, width) {
7
+ if (text.length >= width)
8
+ return text;
9
+ return text + ' '.repeat(width - text.length);
10
+ }
11
+ export function wrapText(text, maxWidth) {
12
+ const words = text.split(' ');
13
+ const lines = [];
14
+ let currentLine = '';
15
+ for (const word of words) {
16
+ if (currentLine.length + word.length + 1 > maxWidth) {
17
+ lines.push(currentLine);
18
+ currentLine = word;
19
+ }
20
+ else {
21
+ currentLine = currentLine ? currentLine + ' ' + word : word;
22
+ }
23
+ }
24
+ if (currentLine)
25
+ lines.push(currentLine);
26
+ return lines;
27
+ }
28
+ //# sourceMappingURL=text.js.map
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "terminal-quest",
3
+ "version": "1.0.0",
4
+ "description": "ストーリー駆動型ターミナルコマンド学習CLI - Learn terminal commands through interactive stories",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "terminal-quest": "bin/terminal-quest.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "dist/",
13
+ "!dist/**/*.test.*",
14
+ "!dist/**/*.map",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "dev": "tsx src/index.tsx",
19
+ "build": "tsc",
20
+ "start": "node dist/index.js",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "prepublishOnly": "npm run build && npm test"
24
+ },
25
+ "keywords": [
26
+ "terminal",
27
+ "cli",
28
+ "learning",
29
+ "tutorial",
30
+ "command-line",
31
+ "interactive",
32
+ "japanese",
33
+ "education"
34
+ ],
35
+ "author": "nasuda",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/nasuda/terminal-quest.git"
40
+ },
41
+ "homepage": "https://github.com/nasuda/terminal-quest",
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "ink": "^5.1.0",
47
+ "ink-text-input": "^6.0.0",
48
+ "react": "^18.3.1"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^25.2.1",
52
+ "@types/react": "^18.3.12",
53
+ "ink-testing-library": "^4.0.0",
54
+ "tsx": "^4.19.2",
55
+ "typescript": "^5.7.2",
56
+ "vitest": "^2.1.8"
57
+ }
58
+ }