Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/app/data/commands/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export function cmdExport(args: string[], state: TerminalState): { lines: Output
lines.push({ text: '', type: 'output' });
} else {
const name = arg.slice(0, eqIdx);
const value = arg.slice(eqIdx + 1).replace(/^["']|["']$/g, '');
// Bash expands the value when it is assigned: `export PATH=$PATH:/opt/bin`.
const value = arg
.slice(eqIdx + 1)
.replace(/^["']|["']$/g, '')
.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (_, ref: string) => newEnv[ref] ?? '');
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
lines.push({ text: `export: '${name}': not a valid identifier`, type: 'error' });
} else {
Expand Down
23 changes: 21 additions & 2 deletions src/app/data/commands/network.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import type { TerminalState, CommandOutput } from './types';
import type { TerminalState, TerminalEnv, CommandOutput } from './types';

export function handleNetwork(cmd: string, args: string[], newState: TerminalState): CommandOutput {
/** A stable address per host, so the output reads like the lessons. */
function fakeIp(host: string): string {
return host === 'google.com' ? '142.250.74.46' : '93.184.216.34';
}

export function handleNetwork(cmd: string, args: string[], newState: TerminalState, env: TerminalEnv = 'linux'): CommandOutput {
switch (cmd) {
case 'ping': {
const host = args.find((a) => !a.startsWith('-') && isNaN(Number(a))) ?? '';
if (!host) return { lines: [{ text: 'Usage: ping <hostname>', type: 'error' }], newState };
if (env === 'windows') {
// Windows ping: 4 echo requests, its own wording.
const ip = fakeIp(host);
return {
lines: [
{ text: `Pinging ${host} [${ip}] with 32 bytes of data:`, type: 'output' },
...[12, 11, 12, 13].map((ms) => ({ text: `Reply from ${ip}: bytes=32 time=${ms}ms TTL=117`, type: 'output' as const })),
{ text: '', type: 'output' },
{ text: `Ping statistics for ${ip}:`, type: 'output' },
{ text: ' Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),', type: 'success' },
],
newState,
};
}
return {
lines: [
{ text: `PING ${host}: 56 data bytes`, type: 'output' },
Expand Down
17 changes: 17 additions & 0 deletions src/app/data/commands/shellVars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { TerminalEnv } from './types';

/** PATH of the simulated Linux / macOS session. */
export const UNIX_DEFAULT_PATH = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';

/** What `$env:PATH` shows on a fresh Windows session (the lessons print this value). */
export const WINDOWS_DEFAULT_PATH = 'C:\\Windows\\System32;C:\\Windows;C:\\Program Files\\Git\\bin';

/**
* The variables as the given shell shows them. The session keeps one set of
* variables; an untouched PATH reads as a Windows PATH under PowerShell.
* Never written back to the state: switching environment keeps each view.
*/
export function varsForEnv(vars: Record<string, string>, env: TerminalEnv): Record<string, string> {
if (env === 'windows' && vars.PATH === UNIX_DEFAULT_PATH) return { ...vars, PATH: WINDOWS_DEFAULT_PATH };
return vars;
}
4 changes: 4 additions & 0 deletions src/app/data/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ export interface GitState {
export interface TerminalState {
root: DirectoryNode;
cwd: string[];
/** Directory before the last `cd` ($OLDPWD), for `cd -`. */
previousCwd?: string[];
commandHistory: string[];
/** sudo already asked for the password in this session (its credential cache). */
sudoAuthenticated?: boolean;
user: string;
hostname: string;
envVars: Record<string, string>;
Expand Down
35 changes: 31 additions & 4 deletions src/app/data/commands/windows.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { TerminalState, TerminalEnv, CommandOutput, OutputLine, DirectoryNode } from './types';
import { varsForEnv } from './shellVars';

export interface WindowsCmdDeps {
cmdPwd: (state: TerminalState, env: TerminalEnv) => OutputLine[];
cmdCd: (state: TerminalState, args: string[]) => { lines: OutputLine[]; newCwd?: string[] };
cmdCd: (state: TerminalState, args: string[], env?: TerminalEnv) => { lines: OutputLine[]; newCwd?: string[] };
cmdLs: (state: TerminalState, args: string[]) => OutputLine[];
cmdCat: (state: TerminalState, args: string[]) => OutputLine[];
cmdMkdir: (state: TerminalState, args: string[]) => { lines: OutputLine[]; newRoot?: DirectoryNode };
Expand All @@ -17,6 +18,12 @@ export interface WindowsCmdDeps {

const EXECUTION_POLICIES = ['Restricted', 'AllSigned', 'RemoteSigned', 'Unrestricted', 'Bypass', 'Undefined', 'Default'];

/** Value that follows a PowerShell parameter (`-Verb RunAs`), case-insensitive. */
function psParam(args: string[], name: string): string | undefined {
const i = args.findIndex((a) => a.toLowerCase() === name);
return i >= 0 ? args[i + 1] : undefined;
}

/** `-Path x`, `-LiteralPath x`, or the first positional argument. */
function psPath(args: string[]): string | undefined {
const i = args.findIndex((a) => ['-path', '-literalpath'].includes(a.toLowerCase()));
Expand All @@ -43,8 +50,8 @@ export function handleWindows(
// ── cd equivalents ────────────────────────────────────────────────────────
case 'set-location':
case 'sl': {
const { lines, newCwd } = deps.cmdCd(newState, args);
if (newCwd) newState = { ...newState, cwd: newCwd };
const { lines, newCwd } = deps.cmdCd(newState, args, env);
if (newCwd) newState = { ...newState, cwd: newCwd, previousCwd: newState.cwd };
return { lines, newState };
}

Expand All @@ -71,6 +78,25 @@ export function handleWindows(
return { lines: [{ text: target, type: 'output' }], newState };
}

// ── Start-Process: opens a program in a new window (nothing to show here) ──
case 'start-process':
case 'saps':
case 'start': {
if (env !== 'windows') return null;
const program = psParam(args, '-filepath') ?? args.find((a) => !a.startsWith('-'));
if (!program) return { lines: [{ text: 'Start-Process: indiquez un programme, par exemple Start-Process notepad', type: 'error' }], newState };
const asAdmin = args.some((a) => a.toLowerCase() === '-verb') && /runas/i.test(psParam(args, '-verb') ?? '');
return {
lines: [{
text: asAdmin
? `(Windows demanderait une confirmation (UAC), puis ouvrirait « ${program} » en administrateur dans une nouvelle fenêtre.)`
: `(« ${program} » s'ouvrirait dans une nouvelle fenêtre.)`,
type: 'info',
}],
newState,
};
}

// ── cat equivalents ───────────────────────────────────────────────────────
case 'get-content':
case 'gc':
Expand Down Expand Up @@ -132,7 +158,7 @@ export function handleWindows(
// ── echo equivalents ──────────────────────────────────────────────────────
case 'write-host':
case 'write-output':
return { lines: deps.cmdEcho(args.filter((a) => !a.startsWith('-')), newState.envVars), newState };
return { lines: deps.cmdEcho(args.filter((a) => !a.startsWith('-')), varsForEnv(newState.envVars, env)), newState };

// ── ps equivalents ────────────────────────────────────────────────────────
case 'get-process':
Expand Down Expand Up @@ -319,6 +345,7 @@ export const WINDOWS_COMMANDS = new Set([
'get-childitem', 'gci', 'dir',
'get-content', 'gc',
'get-item', 'gi',
'start-process', 'saps', 'start',
'new-item', 'ni',
'copy-item', 'cpi', 'copy',
'move-item', 'mi', 'move',
Expand Down
Loading
Loading