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,510 @@
1
+ /**
2
+ * Ensures the .git directory structure exists in the VirtualFS.
3
+ * If .git doesn't exist, initializes minimal structure.
4
+ */
5
+ function ensureGitInit(fs) {
6
+ if (!fs.exists('.git')) {
7
+ return false;
8
+ }
9
+ return true;
10
+ }
11
+ function getCurrentBranch(fs) {
12
+ if (fs.exists('.git/HEAD')) {
13
+ const head = fs.readFile('.git/HEAD').trim();
14
+ // Format: "ref: refs/heads/main"
15
+ if (head.startsWith('ref: refs/heads/')) {
16
+ return head.replace('ref: refs/heads/', '');
17
+ }
18
+ return head;
19
+ }
20
+ return 'main';
21
+ }
22
+ function generateHash() {
23
+ const chars = '0123456789abcdef';
24
+ let hash = '';
25
+ for (let i = 0; i < 7; i++) {
26
+ hash += chars[Math.floor(Math.random() * chars.length)];
27
+ }
28
+ return hash;
29
+ }
30
+ function gitStatus(fs, _args) {
31
+ const branch = getCurrentBranch(fs);
32
+ // If a pre-built status file exists, return its content
33
+ if (fs.exists('.git/status')) {
34
+ return { output: fs.readFile('.git/status') };
35
+ }
36
+ // Build status from staged and tracked state
37
+ const lines = [];
38
+ lines.push(`On branch ${branch}`);
39
+ // Check staged files
40
+ const staged = [];
41
+ if (fs.exists('.git/staged')) {
42
+ const content = fs.readFile('.git/staged').trim();
43
+ if (content) {
44
+ staged.push(...content.split('\n').filter(Boolean));
45
+ }
46
+ }
47
+ if (staged.length > 0) {
48
+ lines.push('Changes to be committed:');
49
+ lines.push(' (use "git restore --staged <file>..." to unstage)');
50
+ for (const file of staged) {
51
+ lines.push(`\tnew file: ${file}`);
52
+ }
53
+ lines.push('');
54
+ }
55
+ // Check tracked files for modifications
56
+ const modified = [];
57
+ if (fs.exists('.git/tracked')) {
58
+ const tracked = fs.readFile('.git/tracked').trim().split('\n').filter(Boolean);
59
+ for (const filePath of tracked) {
60
+ if (fs.exists(filePath)) {
61
+ // File exists and is tracked - check if content differs
62
+ if (fs.exists(`.git/snapshots/${filePath}`)) {
63
+ const current = fs.readFile(filePath);
64
+ const snapshot = fs.readFile(`.git/snapshots/${filePath}`);
65
+ if (current !== snapshot) {
66
+ modified.push(filePath);
67
+ }
68
+ }
69
+ else {
70
+ // No snapshot -- treat as modified
71
+ modified.push(filePath);
72
+ }
73
+ }
74
+ else {
75
+ modified.push(filePath);
76
+ }
77
+ }
78
+ }
79
+ if (modified.length > 0) {
80
+ lines.push('Changes not staged for commit:');
81
+ lines.push(' (use "git add <file>..." to update what will be committed)');
82
+ for (const file of modified) {
83
+ if (fs.exists(file)) {
84
+ lines.push(`\tmodified: ${file}`);
85
+ }
86
+ else {
87
+ lines.push(`\tdeleted: ${file}`);
88
+ }
89
+ }
90
+ lines.push('');
91
+ }
92
+ if (staged.length === 0 && modified.length === 0) {
93
+ lines.push('nothing to commit, working tree clean');
94
+ }
95
+ return { output: lines.join('\n') };
96
+ }
97
+ function gitLog(fs, _args) {
98
+ if (!fs.exists('.git/log')) {
99
+ return { output: '' };
100
+ }
101
+ return { output: fs.readFile('.git/log') };
102
+ }
103
+ function gitDiff(fs, args) {
104
+ // If a specific filename is given, look for .git/diff/<filename>
105
+ if (args.length > 0) {
106
+ const filename = args[0];
107
+ if (fs.exists(`.git/diff/${filename}`)) {
108
+ return { output: fs.readFile(`.git/diff/${filename}`) };
109
+ }
110
+ // Fall through to general diff
111
+ }
112
+ if (fs.exists('.git/diff')) {
113
+ // .git/diff could be a file (general diff) or a directory
114
+ if (fs.isFile('.git/diff')) {
115
+ return { output: fs.readFile('.git/diff') };
116
+ }
117
+ // If it's a directory, concatenate all diff files
118
+ if (fs.isDirectory('.git/diff')) {
119
+ const files = fs.listDir('.git/diff');
120
+ if (files.length === 0) {
121
+ return { output: '' };
122
+ }
123
+ const diffs = files.map(f => fs.readFile(`.git/diff/${f}`));
124
+ return { output: diffs.join('\n') };
125
+ }
126
+ }
127
+ return { output: '' };
128
+ }
129
+ function gitStash(fs, args) {
130
+ const subcmd = args[0];
131
+ if (!subcmd || subcmd === 'push') {
132
+ // Save current changes to stash
133
+ if (!fs.exists('.git/stash-stack')) {
134
+ fs.mkdir('.git/stash-stack', true);
135
+ }
136
+ // Count existing stashes
137
+ let stashCount = 0;
138
+ if (fs.exists('.git/stash-count')) {
139
+ stashCount = parseInt(fs.readFile('.git/stash-count'), 10) || 0;
140
+ }
141
+ // Save current status to stash
142
+ const branch = getCurrentBranch(fs);
143
+ let statusContent = '';
144
+ if (fs.exists('.git/status')) {
145
+ statusContent = fs.readFile('.git/status');
146
+ }
147
+ // Save staged content
148
+ let stagedContent = '';
149
+ if (fs.exists('.git/staged')) {
150
+ stagedContent = fs.readFile('.git/staged');
151
+ }
152
+ if (!statusContent && !stagedContent) {
153
+ return { output: 'No local changes to save' };
154
+ }
155
+ // Store stash entry
156
+ fs.writeFile(`.git/stash-stack/${stashCount}`, JSON.stringify({
157
+ branch,
158
+ status: statusContent,
159
+ staged: stagedContent,
160
+ }));
161
+ fs.writeFile('.git/stash-count', String(stashCount + 1));
162
+ // Clear current state
163
+ if (fs.exists('.git/status')) {
164
+ fs.writeFile('.git/status', `On branch ${branch}\nnothing to commit, working tree clean`);
165
+ }
166
+ if (fs.exists('.git/staged')) {
167
+ fs.writeFile('.git/staged', '');
168
+ }
169
+ return { output: `Saved working directory and index state WIP on ${branch}: stash@{${stashCount}}` };
170
+ }
171
+ if (subcmd === 'pop') {
172
+ if (!fs.exists('.git/stash-count')) {
173
+ return { output: '', error: 'No stash entries found.' };
174
+ }
175
+ const stashCount = parseInt(fs.readFile('.git/stash-count'), 10) || 0;
176
+ if (stashCount === 0) {
177
+ return { output: '', error: 'No stash entries found.' };
178
+ }
179
+ const lastIdx = stashCount - 1;
180
+ const stashPath = `.git/stash-stack/${lastIdx}`;
181
+ if (!fs.exists(stashPath)) {
182
+ return { output: '', error: 'No stash entries found.' };
183
+ }
184
+ const entry = JSON.parse(fs.readFile(stashPath));
185
+ // Restore state
186
+ if (entry.status) {
187
+ fs.writeFile('.git/status', entry.status);
188
+ }
189
+ if (entry.staged) {
190
+ fs.writeFile('.git/staged', entry.staged);
191
+ }
192
+ // Remove stash entry
193
+ fs.remove(stashPath);
194
+ fs.writeFile('.git/stash-count', String(lastIdx));
195
+ const branch = getCurrentBranch(fs);
196
+ return { output: `On ${branch}, dropped stash@{0}` };
197
+ }
198
+ if (subcmd === 'list') {
199
+ if (!fs.exists('.git/stash-count')) {
200
+ return { output: '' };
201
+ }
202
+ const stashCount = parseInt(fs.readFile('.git/stash-count'), 10) || 0;
203
+ if (stashCount === 0) {
204
+ return { output: '' };
205
+ }
206
+ const lines = [];
207
+ for (let i = stashCount - 1; i >= 0; i--) {
208
+ const stashPath = `.git/stash-stack/${i}`;
209
+ if (fs.exists(stashPath)) {
210
+ const entry = JSON.parse(fs.readFile(stashPath));
211
+ lines.push(`stash@{${stashCount - 1 - i}}: WIP on ${entry.branch}`);
212
+ }
213
+ }
214
+ return { output: lines.join('\n') };
215
+ }
216
+ return { output: '', error: `usage: git stash [push | pop | list]` };
217
+ }
218
+ function gitBranch(fs, args) {
219
+ const currentBranch = getCurrentBranch(fs);
220
+ // Parse flags
221
+ let deleteFlag = false;
222
+ const names = [];
223
+ for (const arg of args) {
224
+ if (arg === '-d' || arg === '-D' || arg === '--delete') {
225
+ deleteFlag = true;
226
+ }
227
+ else if (!arg.startsWith('-')) {
228
+ names.push(arg);
229
+ }
230
+ }
231
+ // Get or initialize branches list
232
+ let branches = [];
233
+ if (fs.exists('.git/branches')) {
234
+ branches = fs.readFile('.git/branches').trim().split('\n').filter(Boolean);
235
+ }
236
+ if (branches.length === 0) {
237
+ branches = [currentBranch];
238
+ }
239
+ // Delete branch
240
+ if (deleteFlag) {
241
+ if (names.length === 0) {
242
+ return { output: '', error: 'fatal: branch name required' };
243
+ }
244
+ const branchName = names[0];
245
+ if (branchName === currentBranch) {
246
+ return { output: '', error: `error: Cannot delete branch '${branchName}' checked out at '${fs.getCwd()}'` };
247
+ }
248
+ if (!branches.includes(branchName)) {
249
+ return { output: '', error: `error: branch '${branchName}' not found.` };
250
+ }
251
+ branches = branches.filter(b => b !== branchName);
252
+ fs.writeFile('.git/branches', branches.join('\n'));
253
+ return { output: `Deleted branch ${branchName}.` };
254
+ }
255
+ // Create new branch
256
+ if (names.length > 0) {
257
+ const branchName = names[0];
258
+ if (branches.includes(branchName)) {
259
+ return { output: '', error: `fatal: A branch named '${branchName}' already exists.` };
260
+ }
261
+ branches.push(branchName);
262
+ fs.writeFile('.git/branches', branches.join('\n'));
263
+ return { output: '' };
264
+ }
265
+ // List branches
266
+ const lines = branches.map(b => {
267
+ if (b === currentBranch) {
268
+ return `* ${b}`;
269
+ }
270
+ return ` ${b}`;
271
+ });
272
+ return { output: lines.join('\n') };
273
+ }
274
+ function gitCheckout(fs, args) {
275
+ let createBranch = false;
276
+ const names = [];
277
+ for (const arg of args) {
278
+ if (arg === '-b') {
279
+ createBranch = true;
280
+ }
281
+ else if (!arg.startsWith('-')) {
282
+ names.push(arg);
283
+ }
284
+ }
285
+ if (names.length === 0) {
286
+ return { output: '', error: 'error: you must specify a branch to checkout' };
287
+ }
288
+ const branchName = names[0];
289
+ // Get or initialize branches list
290
+ let branches = [];
291
+ if (fs.exists('.git/branches')) {
292
+ branches = fs.readFile('.git/branches').trim().split('\n').filter(Boolean);
293
+ }
294
+ const currentBranch = getCurrentBranch(fs);
295
+ if (branches.length === 0) {
296
+ branches = [currentBranch];
297
+ }
298
+ if (createBranch) {
299
+ // Create and switch
300
+ if (branches.includes(branchName)) {
301
+ return { output: '', error: `fatal: A branch named '${branchName}' already exists.` };
302
+ }
303
+ branches.push(branchName);
304
+ fs.writeFile('.git/branches', branches.join('\n'));
305
+ fs.writeFile('.git/HEAD', `ref: refs/heads/${branchName}`);
306
+ return { output: `Switched to a new branch '${branchName}'` };
307
+ }
308
+ // Switch to existing branch
309
+ if (!branches.includes(branchName)) {
310
+ return { output: '', error: `error: pathspec '${branchName}' did not match any file(s) known to git` };
311
+ }
312
+ fs.writeFile('.git/HEAD', `ref: refs/heads/${branchName}`);
313
+ return { output: `Switched to branch '${branchName}'` };
314
+ }
315
+ function gitMerge(fs, args) {
316
+ if (args.length === 0) {
317
+ return { output: '', error: 'fatal: No remote for the current branch.' };
318
+ }
319
+ const branchName = args[0];
320
+ // Check if the branch exists
321
+ let branches = [];
322
+ if (fs.exists('.git/branches')) {
323
+ branches = fs.readFile('.git/branches').trim().split('\n').filter(Boolean);
324
+ }
325
+ const currentBranch = getCurrentBranch(fs);
326
+ if (branches.length === 0) {
327
+ branches = [currentBranch];
328
+ }
329
+ if (!branches.includes(branchName)) {
330
+ return { output: '', error: `merge: ${branchName} - not something we can merge` };
331
+ }
332
+ if (branchName === currentBranch) {
333
+ return { output: `Already up to date.` };
334
+ }
335
+ // Check for pre-built merge result
336
+ if (fs.exists('.git/merge-result')) {
337
+ return { output: fs.readFile('.git/merge-result') };
338
+ }
339
+ // Simulate successful merge
340
+ return { output: `Merge made by the 'recursive' strategy.\n Already up to date with branch '${branchName}'.` };
341
+ }
342
+ function gitAdd(fs, args) {
343
+ if (args.length === 0) {
344
+ return { output: '', error: 'Nothing specified, nothing added.' };
345
+ }
346
+ // Ensure .git directory exists
347
+ if (!fs.exists('.git')) {
348
+ return { output: '', error: 'fatal: not a git repository (or any of the parent directories): .git' };
349
+ }
350
+ const filesToAdd = [];
351
+ for (const arg of args) {
352
+ if (arg === '.') {
353
+ // Add all files in cwd (simple simulation: find all files)
354
+ const cwd = fs.getCwd();
355
+ const allFiles = fs.find(cwd, (path, node) => {
356
+ return node.type === 'file' && !path.includes('/.git/');
357
+ });
358
+ // Convert absolute paths to relative
359
+ for (const f of allFiles) {
360
+ const relative = cwd === '/' ? f.slice(1) : f.slice(cwd.length + 1);
361
+ if (relative)
362
+ filesToAdd.push(relative);
363
+ }
364
+ }
365
+ else {
366
+ if (!fs.exists(arg)) {
367
+ return { output: '', error: `fatal: pathspec '${arg}' did not match any files` };
368
+ }
369
+ filesToAdd.push(arg);
370
+ }
371
+ }
372
+ // Read existing staged files
373
+ let staged = [];
374
+ if (fs.exists('.git/staged')) {
375
+ const content = fs.readFile('.git/staged').trim();
376
+ if (content) {
377
+ staged = content.split('\n').filter(Boolean);
378
+ }
379
+ }
380
+ // Add new files (deduplicate)
381
+ for (const file of filesToAdd) {
382
+ if (!staged.includes(file)) {
383
+ staged.push(file);
384
+ }
385
+ }
386
+ fs.writeFile('.git/staged', staged.join('\n'));
387
+ return { output: '' };
388
+ }
389
+ function gitCommit(fs, args) {
390
+ // Parse -m "message"
391
+ let message = '';
392
+ for (let i = 0; i < args.length; i++) {
393
+ if (args[i] === '-m' && i + 1 < args.length) {
394
+ message = args[i + 1];
395
+ break;
396
+ }
397
+ // Handle -m"message" (no space)
398
+ if (args[i].startsWith('-m') && args[i].length > 2) {
399
+ message = args[i].slice(2);
400
+ break;
401
+ }
402
+ }
403
+ if (!message) {
404
+ return { output: '', error: 'error: switch `m\' requires a value' };
405
+ }
406
+ // Check if there are staged files
407
+ let staged = [];
408
+ if (fs.exists('.git/staged')) {
409
+ const content = fs.readFile('.git/staged').trim();
410
+ if (content) {
411
+ staged = content.split('\n').filter(Boolean);
412
+ }
413
+ }
414
+ if (staged.length === 0) {
415
+ return { output: '', error: 'nothing to commit, working tree clean' };
416
+ }
417
+ const branch = getCurrentBranch(fs);
418
+ const hash = generateHash();
419
+ const date = new Date().toISOString().split('T')[0];
420
+ // Build commit entry
421
+ const commitEntry = [
422
+ `commit ${hash}`,
423
+ `Author: User`,
424
+ `Date: ${date}`,
425
+ '',
426
+ ` ${message}`,
427
+ ].join('\n');
428
+ // Prepend to log
429
+ let existingLog = '';
430
+ if (fs.exists('.git/log')) {
431
+ existingLog = fs.readFile('.git/log');
432
+ }
433
+ const newLog = existingLog ? commitEntry + '\n\n' + existingLog : commitEntry;
434
+ fs.writeFile('.git/log', newLog);
435
+ // Update tracked files
436
+ let tracked = [];
437
+ if (fs.exists('.git/tracked')) {
438
+ const content = fs.readFile('.git/tracked').trim();
439
+ if (content) {
440
+ tracked = content.split('\n').filter(Boolean);
441
+ }
442
+ }
443
+ for (const file of staged) {
444
+ if (!tracked.includes(file)) {
445
+ tracked.push(file);
446
+ }
447
+ }
448
+ fs.writeFile('.git/tracked', tracked.join('\n'));
449
+ // Take snapshots of committed files
450
+ if (!fs.exists('.git/snapshots')) {
451
+ fs.mkdir('.git/snapshots', true);
452
+ }
453
+ for (const file of staged) {
454
+ if (fs.exists(file) && fs.isFile(file)) {
455
+ const content = fs.readFile(file);
456
+ // Create parent directories in snapshots if needed
457
+ const parts = file.split('/');
458
+ if (parts.length > 1) {
459
+ const dir = '.git/snapshots/' + parts.slice(0, -1).join('/');
460
+ fs.mkdir(dir, true);
461
+ }
462
+ fs.writeFile(`.git/snapshots/${file}`, content);
463
+ }
464
+ }
465
+ // Clear staged
466
+ fs.writeFile('.git/staged', '');
467
+ // Clear status if it was pre-built
468
+ if (fs.exists('.git/status')) {
469
+ fs.remove('.git/status');
470
+ }
471
+ const fileCount = staged.length;
472
+ const output = `[${branch} ${hash}] ${message}\n ${fileCount} file${fileCount !== 1 ? 's' : ''} changed`;
473
+ return { output };
474
+ }
475
+ export function git(fs, args) {
476
+ if (args.length === 0) {
477
+ return {
478
+ output: 'usage: git <command> [<args>]\n\nAvailable commands:\n status Show the working tree status\n log Show commit logs\n diff Show changes\n stash Stash the changes in a dirty working directory\n branch List, create, or delete branches\n checkout Switch branches\n merge Join two development histories\n add Add file contents to the index\n commit Record changes to the repository',
479
+ };
480
+ }
481
+ const subcommand = args[0];
482
+ const subArgs = args.slice(1);
483
+ // All subcommands (except help-like usage) require .git to exist
484
+ if (!ensureGitInit(fs) && subcommand !== 'init') {
485
+ return { output: '', error: 'fatal: not a git repository (or any of the parent directories): .git' };
486
+ }
487
+ switch (subcommand) {
488
+ case 'status':
489
+ return gitStatus(fs, subArgs);
490
+ case 'log':
491
+ return gitLog(fs, subArgs);
492
+ case 'diff':
493
+ return gitDiff(fs, subArgs);
494
+ case 'stash':
495
+ return gitStash(fs, subArgs);
496
+ case 'branch':
497
+ return gitBranch(fs, subArgs);
498
+ case 'checkout':
499
+ return gitCheckout(fs, subArgs);
500
+ case 'merge':
501
+ return gitMerge(fs, subArgs);
502
+ case 'add':
503
+ return gitAdd(fs, subArgs);
504
+ case 'commit':
505
+ return gitCommit(fs, subArgs);
506
+ default:
507
+ return { output: '', error: `git: '${subcommand}' is not a git command. See 'git --help'.` };
508
+ }
509
+ }
510
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1,4 @@
1
+ import { VirtualFS } from '../VirtualFS.js';
2
+ import type { CommandResult } from './index.js';
3
+ export declare function grep(fs: VirtualFS, args: string[]): CommandResult;
4
+ //# sourceMappingURL=grep.d.ts.map
@@ -0,0 +1,127 @@
1
+ export function grep(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 ignoreCase = false;
10
+ let showLineNumbers = false;
11
+ let recursive = false;
12
+ const nonFlagArgs = [];
13
+ for (const arg of args) {
14
+ if (arg.startsWith('-') && arg.length > 1 && !arg.startsWith('--')) {
15
+ for (const ch of arg.slice(1)) {
16
+ if (ch === 'i')
17
+ ignoreCase = true;
18
+ else if (ch === 'n')
19
+ showLineNumbers = true;
20
+ else if (ch === 'r')
21
+ recursive = true;
22
+ else
23
+ return { output: '', error: `grep: invalid option -- '${ch}'` };
24
+ }
25
+ }
26
+ else {
27
+ nonFlagArgs.push(arg);
28
+ }
29
+ }
30
+ if (nonFlagArgs.length < 1) {
31
+ return { output: '', error: 'grep: missing pattern' };
32
+ }
33
+ const pattern = nonFlagArgs[0];
34
+ const targets = nonFlagArgs.slice(1);
35
+ if (targets.length === 0 && stdin === undefined) {
36
+ return { output: '', error: 'grep: missing file operand' };
37
+ }
38
+ // If no file targets but stdin is available, search stdin
39
+ if (targets.length === 0 && stdin !== undefined) {
40
+ const flags = ignoreCase ? 'i' : '';
41
+ let regex;
42
+ try {
43
+ regex = new RegExp(pattern, flags);
44
+ }
45
+ catch {
46
+ return { output: '', error: `grep: invalid regular expression '${pattern}'` };
47
+ }
48
+ const lines = stdin.split('\n');
49
+ const results = [];
50
+ for (let i = 0; i < lines.length; i++) {
51
+ if (regex.test(lines[i])) {
52
+ let line = '';
53
+ if (showLineNumbers) {
54
+ line += `${i + 1}:`;
55
+ }
56
+ line += lines[i];
57
+ results.push(line);
58
+ }
59
+ }
60
+ return { output: results.join('\n') };
61
+ }
62
+ const flags = ignoreCase ? 'i' : '';
63
+ let regex;
64
+ try {
65
+ regex = new RegExp(pattern, flags);
66
+ }
67
+ catch {
68
+ return { output: '', error: `grep: invalid regular expression '${pattern}'` };
69
+ }
70
+ const results = [];
71
+ const multipleFiles = targets.length > 1 || recursive;
72
+ function searchFile(filePath) {
73
+ try {
74
+ const content = fs.readFile(filePath);
75
+ const lines = content.split('\n');
76
+ for (let i = 0; i < lines.length; i++) {
77
+ if (regex.test(lines[i])) {
78
+ let line = '';
79
+ if (multipleFiles) {
80
+ line += `${filePath}:`;
81
+ }
82
+ if (showLineNumbers) {
83
+ line += `${i + 1}:`;
84
+ }
85
+ line += lines[i];
86
+ results.push(line);
87
+ }
88
+ }
89
+ }
90
+ catch {
91
+ // Skip files that can't be read
92
+ }
93
+ }
94
+ function searchRecursive(dirPath) {
95
+ try {
96
+ const entries = fs.listDirDetailed(dirPath);
97
+ for (const entry of entries) {
98
+ const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`;
99
+ if (entry.type === 'file') {
100
+ searchFile(fullPath);
101
+ }
102
+ else if (entry.type === 'directory') {
103
+ searchRecursive(fullPath);
104
+ }
105
+ }
106
+ }
107
+ catch {
108
+ // Skip directories that can't be read
109
+ }
110
+ }
111
+ for (const target of targets) {
112
+ if (recursive && fs.isDirectory(target)) {
113
+ searchRecursive(fs.resolvePath(target));
114
+ }
115
+ else if (fs.isFile(target)) {
116
+ searchFile(fs.resolvePath(target));
117
+ }
118
+ else if (!fs.exists(target)) {
119
+ return { output: '', error: `grep: ${target}: No such file or directory` };
120
+ }
121
+ else if (fs.isDirectory(target) && !recursive) {
122
+ return { output: '', error: `grep: ${target}: Is a directory` };
123
+ }
124
+ }
125
+ return { output: results.join('\n') };
126
+ }
127
+ //# sourceMappingURL=grep.js.map
@@ -0,0 +1,4 @@
1
+ import { VirtualFS } from '../VirtualFS.js';
2
+ import type { CommandResult } from './index.js';
3
+ export declare function head(fs: VirtualFS, args: string[]): CommandResult;
4
+ //# sourceMappingURL=head.d.ts.map