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.
- package/LICENSE +21 -0
- package/bin/terminal-quest.js +2 -0
- package/dist/App.d.ts +2 -0
- package/dist/App.js +33 -0
- package/dist/components/HintBar.d.ts +9 -0
- package/dist/components/HintBar.js +11 -0
- package/dist/components/MenuItem.d.ts +9 -0
- package/dist/components/MenuItem.js +9 -0
- package/dist/components/ObjectivePanel.d.ts +8 -0
- package/dist/components/ObjectivePanel.js +10 -0
- package/dist/components/ProgressBar.d.ts +8 -0
- package/dist/components/ProgressBar.js +10 -0
- package/dist/components/TerminalOutput.d.ts +11 -0
- package/dist/components/TerminalOutput.js +25 -0
- package/dist/components/TerminalPrompt.d.ts +10 -0
- package/dist/components/TerminalPrompt.js +46 -0
- package/dist/data/commands-meta.d.ts +17 -0
- package/dist/data/commands-meta.js +256 -0
- package/dist/data/stories/00-beginner-pc.d.ts +3 -0
- package/dist/data/stories/00-beginner-pc.js +841 -0
- package/dist/data/stories/01-first-server.d.ts +3 -0
- package/dist/data/stories/01-first-server.js +364 -0
- package/dist/data/stories/02-messy-project.d.ts +3 -0
- package/dist/data/stories/02-messy-project.js +433 -0
- package/dist/data/stories/03-log-detective.d.ts +3 -0
- package/dist/data/stories/03-log-detective.js +291 -0
- package/dist/data/stories/04-deploy-day.d.ts +3 -0
- package/dist/data/stories/04-deploy-day.js +337 -0
- package/dist/data/stories/05-git-incident.d.ts +3 -0
- package/dist/data/stories/05-git-incident.js +534 -0
- package/dist/data/stories/06-pipe-master.d.ts +3 -0
- package/dist/data/stories/06-pipe-master.js +377 -0
- package/dist/data/stories/07-dangerous-commands.d.ts +3 -0
- package/dist/data/stories/07-dangerous-commands.js +411 -0
- package/dist/data/stories/index.d.ts +4 -0
- package/dist/data/stories/index.js +14 -0
- package/dist/data/stories/k1-treasure-hunt.d.ts +3 -0
- package/dist/data/stories/k1-treasure-hunt.js +815 -0
- package/dist/data/types.d.ts +97 -0
- package/dist/data/types.js +2 -0
- package/dist/engine/Achievements.d.ts +5 -0
- package/dist/engine/Achievements.js +93 -0
- package/dist/engine/CommandHandler.d.ts +17 -0
- package/dist/engine/CommandHandler.js +177 -0
- package/dist/engine/HintEngine.d.ts +10 -0
- package/dist/engine/HintEngine.js +26 -0
- package/dist/engine/MissionEngine.d.ts +17 -0
- package/dist/engine/MissionEngine.js +84 -0
- package/dist/engine/TabCompletion.d.ts +14 -0
- package/dist/engine/TabCompletion.js +93 -0
- package/dist/engine/VirtualFS.d.ts +33 -0
- package/dist/engine/VirtualFS.js +276 -0
- package/dist/engine/commands/cat.d.ts +4 -0
- package/dist/engine/commands/cat.js +18 -0
- package/dist/engine/commands/cd.d.ts +4 -0
- package/dist/engine/commands/cd.js +12 -0
- package/dist/engine/commands/chmod.d.ts +4 -0
- package/dist/engine/commands/chmod.js +98 -0
- package/dist/engine/commands/clear.d.ts +4 -0
- package/dist/engine/commands/clear.js +4 -0
- package/dist/engine/commands/cp.d.ts +4 -0
- package/dist/engine/commands/cp.js +26 -0
- package/dist/engine/commands/cut.d.ts +4 -0
- package/dist/engine/commands/cut.js +76 -0
- package/dist/engine/commands/echo.d.ts +4 -0
- package/dist/engine/commands/echo.js +4 -0
- package/dist/engine/commands/find.d.ts +4 -0
- package/dist/engine/commands/find.js +60 -0
- package/dist/engine/commands/git.d.ts +4 -0
- package/dist/engine/commands/git.js +510 -0
- package/dist/engine/commands/grep.d.ts +4 -0
- package/dist/engine/commands/grep.js +127 -0
- package/dist/engine/commands/head.d.ts +4 -0
- package/dist/engine/commands/head.js +59 -0
- package/dist/engine/commands/help.d.ts +4 -0
- package/dist/engine/commands/help.js +32 -0
- package/dist/engine/commands/hint.d.ts +4 -0
- package/dist/engine/commands/hint.js +4 -0
- package/dist/engine/commands/index.d.ts +8 -0
- package/dist/engine/commands/index.js +51 -0
- package/dist/engine/commands/ls.d.ts +4 -0
- package/dist/engine/commands/ls.js +50 -0
- package/dist/engine/commands/man.d.ts +4 -0
- package/dist/engine/commands/man.js +51 -0
- package/dist/engine/commands/mkdir.d.ts +4 -0
- package/dist/engine/commands/mkdir.js +31 -0
- package/dist/engine/commands/mv.d.ts +4 -0
- package/dist/engine/commands/mv.js +15 -0
- package/dist/engine/commands/pwd.d.ts +4 -0
- package/dist/engine/commands/pwd.js +4 -0
- package/dist/engine/commands/rm.d.ts +4 -0
- package/dist/engine/commands/rm.js +49 -0
- package/dist/engine/commands/sort.d.ts +4 -0
- package/dist/engine/commands/sort.js +100 -0
- package/dist/engine/commands/tail.d.ts +4 -0
- package/dist/engine/commands/tail.js +59 -0
- package/dist/engine/commands/touch.d.ts +4 -0
- package/dist/engine/commands/touch.js +18 -0
- package/dist/engine/commands/uniq.d.ts +4 -0
- package/dist/engine/commands/uniq.js +61 -0
- package/dist/engine/commands/wc.d.ts +4 -0
- package/dist/engine/commands/wc.js +67 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/screens/MissionBriefScreen.d.ts +9 -0
- package/dist/screens/MissionBriefScreen.js +27 -0
- package/dist/screens/MissionCompleteScreen.d.ts +9 -0
- package/dist/screens/MissionCompleteScreen.js +30 -0
- package/dist/screens/ProgressScreen.d.ts +8 -0
- package/dist/screens/ProgressScreen.js +24 -0
- package/dist/screens/SettingsScreen.d.ts +8 -0
- package/dist/screens/SettingsScreen.js +45 -0
- package/dist/screens/StorySelectScreen.d.ts +8 -0
- package/dist/screens/StorySelectScreen.js +81 -0
- package/dist/screens/TerminalScreen.d.ts +12 -0
- package/dist/screens/TerminalScreen.js +150 -0
- package/dist/screens/TitleScreen.d.ts +7 -0
- package/dist/screens/TitleScreen.js +27 -0
- package/dist/state/GameState.d.ts +8 -0
- package/dist/state/GameState.js +12 -0
- package/dist/state/ProgressStore.d.ts +9 -0
- package/dist/state/ProgressStore.js +45 -0
- package/dist/state/useGameState.d.ts +11 -0
- package/dist/state/useGameState.js +92 -0
- package/dist/utils/ascii-art.d.ts +4 -0
- package/dist/utils/ascii-art.js +22 -0
- package/dist/utils/colors.d.ts +17 -0
- package/dist/utils/colors.js +17 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +28 -0
- 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,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
|