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,97 @@
1
+ export type ObjectiveCheck = {
2
+ type: 'command_executed';
3
+ command: string;
4
+ args?: string[];
5
+ } | {
6
+ type: 'output_contains';
7
+ pattern: string;
8
+ } | {
9
+ type: 'file_exists';
10
+ path: string;
11
+ } | {
12
+ type: 'file_contains';
13
+ path: string;
14
+ pattern: string;
15
+ } | {
16
+ type: 'file_not_exists';
17
+ path: string;
18
+ } | {
19
+ type: 'cwd_equals';
20
+ path: string;
21
+ };
22
+ export interface Hint {
23
+ level: 1 | 2 | 3;
24
+ text: string;
25
+ }
26
+ export interface Objective {
27
+ id: string;
28
+ description: string;
29
+ checks: ObjectiveCheck[];
30
+ hints: Hint[];
31
+ }
32
+ export interface FSNode {
33
+ type: 'file' | 'directory';
34
+ content?: string;
35
+ children?: Record<string, FSNode>;
36
+ permissions?: string;
37
+ }
38
+ export interface Mission {
39
+ id: string;
40
+ title: string;
41
+ description: string;
42
+ narrative: string;
43
+ objectives: Objective[];
44
+ initialFS: FSNode;
45
+ initialCwd: string;
46
+ newCommands?: string[];
47
+ }
48
+ export interface Story {
49
+ id: string;
50
+ title: string;
51
+ description: string;
52
+ emoji: string;
53
+ missions: Mission[];
54
+ unlockRequires: string[];
55
+ course?: 'kids' | 'beginner' | 'engineer';
56
+ }
57
+ export interface StoryProgress {
58
+ storyId: string;
59
+ completedMissions: string[];
60
+ currentMissionIndex: number;
61
+ hintsUsed: Record<string, number>;
62
+ }
63
+ export interface Achievement {
64
+ id: string;
65
+ title: string;
66
+ description: string;
67
+ emoji: string;
68
+ }
69
+ export interface GameProgress {
70
+ completedStories: string[];
71
+ storyProgress: Record<string, StoryProgress>;
72
+ totalCommandsExecuted: number;
73
+ totalHintsUsed: number;
74
+ achievements: string[];
75
+ }
76
+ export type Screen = {
77
+ type: 'title';
78
+ } | {
79
+ type: 'storySelect';
80
+ } | {
81
+ type: 'missionBrief';
82
+ storyId: string;
83
+ missionIndex: number;
84
+ } | {
85
+ type: 'terminal';
86
+ storyId: string;
87
+ missionIndex: number;
88
+ } | {
89
+ type: 'missionComplete';
90
+ storyId: string;
91
+ missionIndex: number;
92
+ } | {
93
+ type: 'progress';
94
+ } | {
95
+ type: 'settings';
96
+ };
97
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,5 @@
1
+ import type { Achievement, GameProgress } from '../data/types.js';
2
+ export declare const achievements: Achievement[];
3
+ export declare function checkAchievements(progress: GameProgress, engineerStoryIds: string[]): string[];
4
+ export declare function getAchievement(id: string): Achievement | undefined;
5
+ //# sourceMappingURL=Achievements.d.ts.map
@@ -0,0 +1,93 @@
1
+ export const achievements = [
2
+ {
3
+ id: 'first-command',
4
+ title: '第一歩',
5
+ description: '最初のコマンドを実行した',
6
+ emoji: '👣',
7
+ },
8
+ {
9
+ id: 'no-hint-clear',
10
+ title: 'ヒントなしクリア',
11
+ description: 'ヒントを使わずにミッションをクリアした',
12
+ emoji: '🧠',
13
+ },
14
+ {
15
+ id: 'story-complete',
16
+ title: 'ストーリーマスター',
17
+ description: 'ストーリーを1つクリアした',
18
+ emoji: '📖',
19
+ },
20
+ {
21
+ id: 'all-engineer',
22
+ title: 'エンジニアコース制覇',
23
+ description: 'エンジニアコースの全ストーリーをクリアした',
24
+ emoji: '🎓',
25
+ },
26
+ {
27
+ id: 'commands-100',
28
+ title: 'コマンド100回',
29
+ description: 'コマンドを100回実行した',
30
+ emoji: '💯',
31
+ },
32
+ {
33
+ id: 'commands-500',
34
+ title: 'ベテラン',
35
+ description: 'コマンドを500回実行した',
36
+ emoji: '⭐',
37
+ },
38
+ {
39
+ id: 'pipe-master',
40
+ title: 'パイプの達人',
41
+ description: 'Story 6「パイプの達人」をクリアした',
42
+ emoji: '🔗',
43
+ },
44
+ ];
45
+ export function checkAchievements(progress, engineerStoryIds) {
46
+ const newAchievements = [];
47
+ const existing = progress.achievements ?? [];
48
+ // first-command: executed at least 1 command
49
+ if (!existing.includes('first-command') && progress.totalCommandsExecuted >= 1) {
50
+ newAchievements.push('first-command');
51
+ }
52
+ // no-hint-clear: any mission completed with 0 hints
53
+ if (!existing.includes('no-hint-clear')) {
54
+ for (const sp of Object.values(progress.storyProgress)) {
55
+ for (const missionId of sp.completedMissions) {
56
+ if ((sp.hintsUsed[missionId] ?? 0) === 0) {
57
+ newAchievements.push('no-hint-clear');
58
+ break;
59
+ }
60
+ }
61
+ if (newAchievements.includes('no-hint-clear'))
62
+ break;
63
+ }
64
+ }
65
+ // story-complete: at least 1 story completed
66
+ if (!existing.includes('story-complete') && progress.completedStories.length >= 1) {
67
+ newAchievements.push('story-complete');
68
+ }
69
+ // all-engineer: all engineer course stories completed
70
+ if (!existing.includes('all-engineer') && engineerStoryIds.length > 0) {
71
+ const allDone = engineerStoryIds.every(id => progress.completedStories.includes(id));
72
+ if (allDone) {
73
+ newAchievements.push('all-engineer');
74
+ }
75
+ }
76
+ // commands-100
77
+ if (!existing.includes('commands-100') && progress.totalCommandsExecuted >= 100) {
78
+ newAchievements.push('commands-100');
79
+ }
80
+ // commands-500
81
+ if (!existing.includes('commands-500') && progress.totalCommandsExecuted >= 500) {
82
+ newAchievements.push('commands-500');
83
+ }
84
+ // pipe-master: story-06 completed
85
+ if (!existing.includes('pipe-master') && progress.completedStories.includes('story-06')) {
86
+ newAchievements.push('pipe-master');
87
+ }
88
+ return newAchievements;
89
+ }
90
+ export function getAchievement(id) {
91
+ return achievements.find(a => a.id === id);
92
+ }
93
+ //# sourceMappingURL=Achievements.js.map
@@ -0,0 +1,17 @@
1
+ import { VirtualFS } from './VirtualFS.js';
2
+ import { type CommandResult } from './commands/index.js';
3
+ export declare class CommandHandler {
4
+ private fs;
5
+ constructor(fs: VirtualFS);
6
+ execute(input: string): CommandResult;
7
+ private executePipeline;
8
+ private executeSingle;
9
+ private extractRedirect;
10
+ private splitOnPipe;
11
+ /**
12
+ * Tokenize input string, handling single and double quotes.
13
+ * Quoted strings preserve internal spaces. Quotes are removed from the result.
14
+ */
15
+ private tokenize;
16
+ }
17
+ //# sourceMappingURL=CommandHandler.d.ts.map
@@ -0,0 +1,177 @@
1
+ import { commandRegistry } from './commands/index.js';
2
+ export class CommandHandler {
3
+ fs;
4
+ constructor(fs) {
5
+ this.fs = fs;
6
+ }
7
+ execute(input) {
8
+ const trimmed = input.trim();
9
+ if (trimmed === '') {
10
+ return { output: '' };
11
+ }
12
+ // Check for pipe: split on unquoted '|'
13
+ const pipeSegments = this.splitOnPipe(trimmed);
14
+ if (pipeSegments.length > 1) {
15
+ return this.executePipeline(pipeSegments);
16
+ }
17
+ return this.executeSingle(trimmed);
18
+ }
19
+ executePipeline(segments) {
20
+ let previousOutput = '';
21
+ for (let i = 0; i < segments.length; i++) {
22
+ const segment = segments[i].trim();
23
+ if (segment === '') {
24
+ return { output: '', error: 'syntax error near unexpected token `|`' };
25
+ }
26
+ // For piped commands, pass previous output as stdin via args if applicable
27
+ const result = this.executeSingle(segment, previousOutput);
28
+ if (result.error) {
29
+ return result;
30
+ }
31
+ previousOutput = result.output;
32
+ }
33
+ return { output: previousOutput };
34
+ }
35
+ executeSingle(input, stdin) {
36
+ // Parse redirect operators from the input before tokenizing fully
37
+ const { command: commandPart, redirect } = this.extractRedirect(input);
38
+ const tokens = this.tokenize(commandPart);
39
+ if (tokens.length === 0) {
40
+ return { output: '' };
41
+ }
42
+ const commandName = tokens[0];
43
+ const args = tokens.slice(1);
44
+ const commandFn = commandRegistry[commandName];
45
+ if (!commandFn) {
46
+ return { output: '', error: `${commandName}: command not found` };
47
+ }
48
+ // Pass stdin via special arg marker for pipe support
49
+ const finalArgs = stdin != null && stdin !== ''
50
+ ? [...args, `__stdin__:${stdin}`]
51
+ : args;
52
+ const result = commandFn(this.fs, finalArgs);
53
+ // Handle redirect
54
+ if (redirect && !result.error) {
55
+ try {
56
+ if (redirect.mode === 'overwrite') {
57
+ this.fs.writeFile(redirect.target, result.output);
58
+ }
59
+ else {
60
+ this.fs.appendFile(redirect.target, result.output);
61
+ }
62
+ return { output: '' };
63
+ }
64
+ catch (e) {
65
+ const msg = e instanceof Error ? e.message : String(e);
66
+ return { output: '', error: msg };
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ extractRedirect(input) {
72
+ // Look for >> or > outside of quotes
73
+ let inSingle = false;
74
+ let inDouble = false;
75
+ let redirectIndex = -1;
76
+ let isAppend = false;
77
+ for (let i = 0; i < input.length; i++) {
78
+ const ch = input[i];
79
+ if (ch === "'" && !inDouble) {
80
+ inSingle = !inSingle;
81
+ }
82
+ else if (ch === '"' && !inSingle) {
83
+ inDouble = !inDouble;
84
+ }
85
+ else if (!inSingle && !inDouble) {
86
+ if (ch === '>' && i + 1 < input.length && input[i + 1] === '>') {
87
+ redirectIndex = i;
88
+ isAppend = true;
89
+ break;
90
+ }
91
+ else if (ch === '>') {
92
+ redirectIndex = i;
93
+ isAppend = false;
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ if (redirectIndex === -1) {
99
+ return { command: input, redirect: null };
100
+ }
101
+ const commandPart = input.slice(0, redirectIndex).trim();
102
+ const targetPart = input.slice(redirectIndex + (isAppend ? 2 : 1)).trim();
103
+ // Tokenize the target to handle potential quotes
104
+ const targetTokens = this.tokenize(targetPart);
105
+ if (targetTokens.length === 0) {
106
+ return { command: input, redirect: null };
107
+ }
108
+ return {
109
+ command: commandPart,
110
+ redirect: {
111
+ mode: isAppend ? 'append' : 'overwrite',
112
+ target: targetTokens[0],
113
+ },
114
+ };
115
+ }
116
+ splitOnPipe(input) {
117
+ const segments = [];
118
+ let current = '';
119
+ let inSingle = false;
120
+ let inDouble = false;
121
+ for (let i = 0; i < input.length; i++) {
122
+ const ch = input[i];
123
+ if (ch === "'" && !inDouble) {
124
+ inSingle = !inSingle;
125
+ current += ch;
126
+ }
127
+ else if (ch === '"' && !inSingle) {
128
+ inDouble = !inDouble;
129
+ current += ch;
130
+ }
131
+ else if (ch === '|' && !inSingle && !inDouble) {
132
+ segments.push(current);
133
+ current = '';
134
+ }
135
+ else {
136
+ current += ch;
137
+ }
138
+ }
139
+ segments.push(current);
140
+ return segments;
141
+ }
142
+ /**
143
+ * Tokenize input string, handling single and double quotes.
144
+ * Quoted strings preserve internal spaces. Quotes are removed from the result.
145
+ */
146
+ tokenize(input) {
147
+ const tokens = [];
148
+ let current = '';
149
+ let inSingle = false;
150
+ let inDouble = false;
151
+ for (let i = 0; i < input.length; i++) {
152
+ const ch = input[i];
153
+ if (ch === "'" && !inDouble) {
154
+ inSingle = !inSingle;
155
+ // Don't add the quote character to the token
156
+ }
157
+ else if (ch === '"' && !inSingle) {
158
+ inDouble = !inDouble;
159
+ // Don't add the quote character to the token
160
+ }
161
+ else if (ch === ' ' && !inSingle && !inDouble) {
162
+ if (current.length > 0) {
163
+ tokens.push(current);
164
+ current = '';
165
+ }
166
+ }
167
+ else {
168
+ current += ch;
169
+ }
170
+ }
171
+ if (current.length > 0) {
172
+ tokens.push(current);
173
+ }
174
+ return tokens;
175
+ }
176
+ }
177
+ //# sourceMappingURL=CommandHandler.js.map
@@ -0,0 +1,10 @@
1
+ import type { Hint } from '../data/types.js';
2
+ export declare class HintEngine {
3
+ private currentHintLevel;
4
+ private totalHintsUsed;
5
+ constructor();
6
+ getNextHint(objectiveId: string, hints: Hint[]): Hint | null;
7
+ getCurrentLevel(objectiveId: string): number;
8
+ getTotalHintsUsed(): number;
9
+ }
10
+ //# sourceMappingURL=HintEngine.d.ts.map
@@ -0,0 +1,26 @@
1
+ export class HintEngine {
2
+ currentHintLevel;
3
+ totalHintsUsed;
4
+ constructor() {
5
+ this.currentHintLevel = {};
6
+ this.totalHintsUsed = 0;
7
+ }
8
+ getNextHint(objectiveId, hints) {
9
+ const currentLevel = this.currentHintLevel[objectiveId] ?? 0;
10
+ const nextLevel = currentLevel + 1;
11
+ const hint = hints.find((h) => h.level === nextLevel);
12
+ if (!hint) {
13
+ return null;
14
+ }
15
+ this.currentHintLevel[objectiveId] = nextLevel;
16
+ this.totalHintsUsed++;
17
+ return hint;
18
+ }
19
+ getCurrentLevel(objectiveId) {
20
+ return this.currentHintLevel[objectiveId] ?? 0;
21
+ }
22
+ getTotalHintsUsed() {
23
+ return this.totalHintsUsed;
24
+ }
25
+ }
26
+ //# sourceMappingURL=HintEngine.js.map
@@ -0,0 +1,17 @@
1
+ import type { Mission } from '../data/types.js';
2
+ import { VirtualFS } from './VirtualFS.js';
3
+ export declare class MissionEngine {
4
+ private fs;
5
+ private mission;
6
+ private completedObjectives;
7
+ private commandHistory;
8
+ constructor(mission: Mission);
9
+ getFS(): VirtualFS;
10
+ getMission(): Mission;
11
+ getCompletedObjectives(): string[];
12
+ getCurrentObjectiveIndex(): number;
13
+ isAllComplete(): boolean;
14
+ checkObjectives(command: string, args: string[], output: string): string[];
15
+ private evaluateCheck;
16
+ }
17
+ //# sourceMappingURL=MissionEngine.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { VirtualFS } from './VirtualFS.js';
2
+ export class MissionEngine {
3
+ fs;
4
+ mission;
5
+ completedObjectives;
6
+ commandHistory;
7
+ constructor(mission) {
8
+ this.mission = mission;
9
+ this.fs = new VirtualFS(mission.initialFS, mission.initialCwd);
10
+ this.completedObjectives = new Set();
11
+ this.commandHistory = [];
12
+ }
13
+ getFS() {
14
+ return this.fs;
15
+ }
16
+ getMission() {
17
+ return this.mission;
18
+ }
19
+ getCompletedObjectives() {
20
+ return [...this.completedObjectives];
21
+ }
22
+ getCurrentObjectiveIndex() {
23
+ for (let i = 0; i < this.mission.objectives.length; i++) {
24
+ if (!this.completedObjectives.has(this.mission.objectives[i].id)) {
25
+ return i;
26
+ }
27
+ }
28
+ return this.mission.objectives.length;
29
+ }
30
+ isAllComplete() {
31
+ return this.mission.objectives.every((obj) => this.completedObjectives.has(obj.id));
32
+ }
33
+ checkObjectives(command, args, output) {
34
+ this.commandHistory.push({ command, args, output });
35
+ const newlyCompleted = [];
36
+ for (const objective of this.mission.objectives) {
37
+ if (this.completedObjectives.has(objective.id)) {
38
+ continue;
39
+ }
40
+ const allChecksPassed = objective.checks.every((check) => this.evaluateCheck(check, command, args, output));
41
+ if (allChecksPassed) {
42
+ this.completedObjectives.add(objective.id);
43
+ newlyCompleted.push(objective.id);
44
+ }
45
+ }
46
+ return newlyCompleted;
47
+ }
48
+ evaluateCheck(check, command, args, output) {
49
+ switch (check.type) {
50
+ case 'command_executed': {
51
+ if (check.command !== command)
52
+ return false;
53
+ if (check.args) {
54
+ return check.args.every((expected, i) => args[i] === expected);
55
+ }
56
+ return true;
57
+ }
58
+ case 'output_contains': {
59
+ return output.includes(check.pattern);
60
+ }
61
+ case 'file_exists': {
62
+ return this.fs.exists(check.path);
63
+ }
64
+ case 'file_contains': {
65
+ try {
66
+ const content = this.fs.readFile(check.path);
67
+ return content.includes(check.pattern);
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ case 'file_not_exists': {
74
+ return !this.fs.exists(check.path);
75
+ }
76
+ case 'cwd_equals': {
77
+ return this.fs.getCwd() === check.path;
78
+ }
79
+ default:
80
+ return false;
81
+ }
82
+ }
83
+ }
84
+ //# sourceMappingURL=MissionEngine.js.map
@@ -0,0 +1,14 @@
1
+ import { VirtualFS } from './VirtualFS.js';
2
+ export interface CompletionResult {
3
+ completed: string;
4
+ candidates: string[];
5
+ }
6
+ export declare class TabCompletion {
7
+ private fs;
8
+ constructor(fs: VirtualFS);
9
+ complete(input: string): CompletionResult;
10
+ private completeCommand;
11
+ private completePath;
12
+ private longestCommonPrefix;
13
+ }
14
+ //# sourceMappingURL=TabCompletion.d.ts.map
@@ -0,0 +1,93 @@
1
+ import { commandRegistry } from './commands/index.js';
2
+ export class TabCompletion {
3
+ fs;
4
+ constructor(fs) {
5
+ this.fs = fs;
6
+ }
7
+ complete(input) {
8
+ if (input === '') {
9
+ return { completed: '', candidates: [] };
10
+ }
11
+ const parts = input.split(/\s+/);
12
+ const endsWithSpace = input.endsWith(' ');
13
+ if (parts.length === 1 && !endsWithSpace) {
14
+ return this.completeCommand(parts[0]);
15
+ }
16
+ const partial = endsWithSpace ? '' : parts[parts.length - 1];
17
+ const prefix = endsWithSpace ? input : parts.slice(0, -1).join(' ') + ' ';
18
+ const result = this.completePath(partial);
19
+ return {
20
+ completed: prefix + result.completed,
21
+ candidates: result.candidates,
22
+ };
23
+ }
24
+ completeCommand(partial) {
25
+ const commands = Object.keys(commandRegistry);
26
+ const matches = commands.filter(cmd => cmd.startsWith(partial));
27
+ if (matches.length === 0) {
28
+ return { completed: partial, candidates: [] };
29
+ }
30
+ if (matches.length === 1) {
31
+ return { completed: matches[0] + ' ', candidates: [] };
32
+ }
33
+ const common = this.longestCommonPrefix(matches);
34
+ return { completed: common, candidates: matches };
35
+ }
36
+ completePath(partial) {
37
+ let dirPath;
38
+ let filePrefix;
39
+ const lastSlash = partial.lastIndexOf('/');
40
+ if (lastSlash === -1) {
41
+ dirPath = '.';
42
+ filePrefix = partial;
43
+ }
44
+ else {
45
+ dirPath = partial.slice(0, lastSlash) || '/';
46
+ filePrefix = partial.slice(lastSlash + 1);
47
+ }
48
+ try {
49
+ if (!this.fs.exists(dirPath) || !this.fs.isDirectory(dirPath)) {
50
+ return { completed: partial, candidates: [] };
51
+ }
52
+ const entries = this.fs.listDirDetailed(dirPath);
53
+ const matches = entries.filter(e => e.name.startsWith(filePrefix));
54
+ if (matches.length === 0) {
55
+ return { completed: partial, candidates: [] };
56
+ }
57
+ const dirPrefix = lastSlash === -1 ? '' : partial.slice(0, lastSlash + 1);
58
+ if (matches.length === 1) {
59
+ const match = matches[0];
60
+ const suffix = match.type === 'directory' ? '/' : ' ';
61
+ return { completed: dirPrefix + match.name + suffix, candidates: [] };
62
+ }
63
+ const matchNames = matches.map(m => m.name);
64
+ const common = this.longestCommonPrefix(matchNames);
65
+ return {
66
+ completed: dirPrefix + common,
67
+ candidates: matchNames.map(n => {
68
+ const entry = matches.find(m => m.name === n);
69
+ return entry.type === 'directory' ? n + '/' : n;
70
+ }),
71
+ };
72
+ }
73
+ catch {
74
+ return { completed: partial, candidates: [] };
75
+ }
76
+ }
77
+ longestCommonPrefix(strings) {
78
+ if (strings.length === 0)
79
+ return '';
80
+ if (strings.length === 1)
81
+ return strings[0];
82
+ let prefix = strings[0];
83
+ for (let i = 1; i < strings.length; i++) {
84
+ while (!strings[i].startsWith(prefix)) {
85
+ prefix = prefix.slice(0, -1);
86
+ if (prefix === '')
87
+ return '';
88
+ }
89
+ }
90
+ return prefix;
91
+ }
92
+ }
93
+ //# sourceMappingURL=TabCompletion.js.map
@@ -0,0 +1,33 @@
1
+ import type { FSNode } from '../data/types.js';
2
+ export declare class VirtualFS {
3
+ private root;
4
+ private cwd;
5
+ constructor(initialFS: FSNode, initialCwd?: string);
6
+ private deepClone;
7
+ getCwd(): string;
8
+ resolvePath(path: string): string;
9
+ private normalizePath;
10
+ private getNode;
11
+ private getParentAndName;
12
+ exists(path: string): boolean;
13
+ isDirectory(path: string): boolean;
14
+ isFile(path: string): boolean;
15
+ readFile(path: string): string;
16
+ writeFile(path: string, content: string): void;
17
+ appendFile(path: string, content: string): void;
18
+ listDir(path: string): string[];
19
+ listDirDetailed(path: string): Array<{
20
+ name: string;
21
+ type: 'file' | 'directory';
22
+ }>;
23
+ mkdir(path: string, recursive?: boolean): void;
24
+ private mkdirSingle;
25
+ remove(path: string, recursive?: boolean): void;
26
+ copy(src: string, dest: string, recursive?: boolean): void;
27
+ move(src: string, dest: string): void;
28
+ changeCwd(path: string): void;
29
+ getPermissions(path: string): string;
30
+ setPermissions(path: string, permissions: string): void;
31
+ find(startPath: string, predicate: (path: string, node: FSNode) => boolean): string[];
32
+ }
33
+ //# sourceMappingURL=VirtualFS.d.ts.map