typescript-virtual-container 1.2.8 → 1.3.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 (307) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +462 -44
  3. package/biome.json +7 -0
  4. package/dist/SSHMimic/exec.d.ts.map +1 -1
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +35 -21
  7. package/dist/SSHMimic/index.d.ts.map +1 -1
  8. package/dist/SSHMimic/index.js +20 -6
  9. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/binaryPack.js +29 -6
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +36 -13
  13. package/dist/VirtualPackageManager/index.d.ts +202 -0
  14. package/dist/VirtualPackageManager/index.d.ts.map +1 -0
  15. package/dist/VirtualPackageManager/index.js +825 -0
  16. package/dist/VirtualShell/index.d.ts +93 -12
  17. package/dist/VirtualShell/index.d.ts.map +1 -1
  18. package/dist/VirtualShell/index.js +95 -13
  19. package/dist/VirtualShell/shell.d.ts.map +1 -1
  20. package/dist/VirtualShell/shell.js +3 -1
  21. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  22. package/dist/VirtualUserManager/index.d.ts +52 -20
  23. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  24. package/dist/VirtualUserManager/index.js +54 -20
  25. package/dist/commands/adduser.d.ts +6 -0
  26. package/dist/commands/adduser.d.ts.map +1 -1
  27. package/dist/commands/adduser.js +6 -0
  28. package/dist/commands/alias.d.ts +9 -0
  29. package/dist/commands/alias.d.ts.map +1 -0
  30. package/dist/commands/alias.js +63 -0
  31. package/dist/commands/apt.d.ts +9 -0
  32. package/dist/commands/apt.d.ts.map +1 -0
  33. package/dist/commands/apt.js +205 -0
  34. package/dist/commands/awk.d.ts +11 -0
  35. package/dist/commands/awk.d.ts.map +1 -1
  36. package/dist/commands/awk.js +15 -2
  37. package/dist/commands/base64.d.ts +5 -0
  38. package/dist/commands/base64.d.ts.map +1 -1
  39. package/dist/commands/base64.js +9 -1
  40. package/dist/commands/cat.d.ts +5 -0
  41. package/dist/commands/cat.d.ts.map +1 -1
  42. package/dist/commands/cat.js +35 -8
  43. package/dist/commands/cd.d.ts +5 -0
  44. package/dist/commands/cd.d.ts.map +1 -1
  45. package/dist/commands/cd.js +5 -0
  46. package/dist/commands/chmod.d.ts +5 -0
  47. package/dist/commands/chmod.d.ts.map +1 -1
  48. package/dist/commands/chmod.js +57 -3
  49. package/dist/commands/command-helpers.d.ts +78 -4
  50. package/dist/commands/command-helpers.d.ts.map +1 -1
  51. package/dist/commands/command-helpers.js +78 -4
  52. package/dist/commands/cp.d.ts +5 -0
  53. package/dist/commands/cp.d.ts.map +1 -1
  54. package/dist/commands/cp.js +5 -0
  55. package/dist/commands/curl.d.ts +5 -0
  56. package/dist/commands/curl.d.ts.map +1 -1
  57. package/dist/commands/curl.js +106 -26
  58. package/dist/commands/cut.d.ts +5 -0
  59. package/dist/commands/cut.d.ts.map +1 -1
  60. package/dist/commands/cut.js +8 -1
  61. package/dist/commands/date.d.ts +5 -0
  62. package/dist/commands/date.d.ts.map +1 -1
  63. package/dist/commands/date.js +7 -1
  64. package/dist/commands/declare.d.ts +3 -0
  65. package/dist/commands/declare.d.ts.map +1 -0
  66. package/dist/commands/declare.js +39 -0
  67. package/dist/commands/diff.d.ts +5 -0
  68. package/dist/commands/diff.d.ts.map +1 -1
  69. package/dist/commands/diff.js +5 -0
  70. package/dist/commands/dpkg.d.ts +9 -0
  71. package/dist/commands/dpkg.d.ts.map +1 -0
  72. package/dist/commands/dpkg.js +161 -0
  73. package/dist/commands/du.d.ts.map +1 -1
  74. package/dist/commands/du.js +8 -2
  75. package/dist/commands/echo.d.ts +5 -0
  76. package/dist/commands/echo.d.ts.map +1 -1
  77. package/dist/commands/echo.js +33 -12
  78. package/dist/commands/env.d.ts +5 -0
  79. package/dist/commands/env.d.ts.map +1 -1
  80. package/dist/commands/env.js +11 -1
  81. package/dist/commands/exit.d.ts +5 -0
  82. package/dist/commands/exit.d.ts.map +1 -1
  83. package/dist/commands/exit.js +12 -2
  84. package/dist/commands/export.d.ts.map +1 -1
  85. package/dist/commands/export.js +3 -1
  86. package/dist/commands/find.d.ts +5 -0
  87. package/dist/commands/find.d.ts.map +1 -1
  88. package/dist/commands/find.js +5 -0
  89. package/dist/commands/free.d.ts +8 -0
  90. package/dist/commands/free.d.ts.map +1 -0
  91. package/dist/commands/free.js +43 -0
  92. package/dist/commands/grep.d.ts +5 -0
  93. package/dist/commands/grep.d.ts.map +1 -1
  94. package/dist/commands/grep.js +12 -2
  95. package/dist/commands/gzip.d.ts +5 -0
  96. package/dist/commands/gzip.d.ts.map +1 -1
  97. package/dist/commands/gzip.js +18 -2
  98. package/dist/commands/head.d.ts +5 -0
  99. package/dist/commands/head.d.ts.map +1 -1
  100. package/dist/commands/head.js +5 -0
  101. package/dist/commands/help.d.ts.map +1 -1
  102. package/dist/commands/help.js +98 -45
  103. package/dist/commands/helpers.d.ts +3 -0
  104. package/dist/commands/helpers.d.ts.map +1 -1
  105. package/dist/commands/helpers.js +3 -0
  106. package/dist/commands/history.d.ts +8 -0
  107. package/dist/commands/history.d.ts.map +1 -0
  108. package/dist/commands/history.js +26 -0
  109. package/dist/commands/hostname.d.ts +5 -0
  110. package/dist/commands/hostname.d.ts.map +1 -1
  111. package/dist/commands/hostname.js +5 -0
  112. package/dist/commands/id.d.ts.map +1 -1
  113. package/dist/commands/id.js +4 -1
  114. package/dist/commands/index.d.ts +2 -10
  115. package/dist/commands/index.d.ts.map +1 -1
  116. package/dist/commands/index.js +2 -231
  117. package/dist/commands/ls.d.ts.map +1 -1
  118. package/dist/commands/ls.js +6 -3
  119. package/dist/commands/lsb-release.d.ts +3 -0
  120. package/dist/commands/lsb-release.d.ts.map +1 -0
  121. package/dist/commands/lsb-release.js +56 -0
  122. package/dist/commands/man.d.ts +3 -0
  123. package/dist/commands/man.d.ts.map +1 -0
  124. package/dist/commands/man.js +155 -0
  125. package/dist/commands/nano.js +1 -1
  126. package/dist/commands/neofetch.d.ts.map +1 -1
  127. package/dist/commands/neofetch.js +6 -1
  128. package/dist/commands/node.d.ts +9 -0
  129. package/dist/commands/node.d.ts.map +1 -0
  130. package/dist/commands/node.js +316 -0
  131. package/dist/commands/npm.d.ts +19 -0
  132. package/dist/commands/npm.d.ts.map +1 -0
  133. package/dist/commands/npm.js +109 -0
  134. package/dist/commands/ping.d.ts.map +1 -1
  135. package/dist/commands/ping.js +7 -2
  136. package/dist/commands/printf.d.ts +3 -0
  137. package/dist/commands/printf.d.ts.map +1 -0
  138. package/dist/commands/printf.js +113 -0
  139. package/dist/commands/ps.d.ts.map +1 -1
  140. package/dist/commands/ps.js +30 -6
  141. package/dist/commands/python.d.ts +30 -0
  142. package/dist/commands/python.d.ts.map +1 -0
  143. package/dist/commands/python.js +2058 -0
  144. package/dist/commands/read.d.ts +3 -0
  145. package/dist/commands/read.d.ts.map +1 -0
  146. package/dist/commands/read.js +34 -0
  147. package/dist/commands/registry.d.ts +8 -0
  148. package/dist/commands/registry.d.ts.map +1 -0
  149. package/dist/commands/registry.js +229 -0
  150. package/dist/commands/runtime.d.ts +6 -0
  151. package/dist/commands/runtime.d.ts.map +1 -0
  152. package/dist/commands/runtime.js +280 -0
  153. package/dist/commands/sed.d.ts.map +1 -1
  154. package/dist/commands/sed.js +11 -3
  155. package/dist/commands/set.d.ts.map +1 -1
  156. package/dist/commands/set.js +9 -3
  157. package/dist/commands/sh.d.ts.map +1 -1
  158. package/dist/commands/sh.js +69 -30
  159. package/dist/commands/shift.d.ts +5 -0
  160. package/dist/commands/shift.d.ts.map +1 -0
  161. package/dist/commands/shift.js +52 -0
  162. package/dist/commands/sleep.d.ts.map +1 -1
  163. package/dist/commands/sort.d.ts.map +1 -1
  164. package/dist/commands/sort.js +4 -2
  165. package/dist/commands/source.d.ts +3 -0
  166. package/dist/commands/source.d.ts.map +1 -0
  167. package/dist/commands/source.js +34 -0
  168. package/dist/commands/sudo.js +1 -1
  169. package/dist/commands/tar.d.ts.map +1 -1
  170. package/dist/commands/tar.js +11 -3
  171. package/dist/commands/tee.d.ts.map +1 -1
  172. package/dist/commands/tee.js +8 -6
  173. package/dist/commands/test.d.ts +3 -0
  174. package/dist/commands/test.d.ts.map +1 -0
  175. package/dist/commands/test.js +114 -0
  176. package/dist/commands/tr.d.ts.map +1 -1
  177. package/dist/commands/tr.js +3 -1
  178. package/dist/commands/true.d.ts +4 -0
  179. package/dist/commands/true.d.ts.map +1 -0
  180. package/dist/commands/true.js +14 -0
  181. package/dist/commands/type.d.ts +3 -0
  182. package/dist/commands/type.d.ts.map +1 -0
  183. package/dist/commands/type.js +34 -0
  184. package/dist/commands/uname.d.ts.map +1 -1
  185. package/dist/commands/uname.js +4 -1
  186. package/dist/commands/uniq.d.ts.map +1 -1
  187. package/dist/commands/uptime.d.ts +3 -0
  188. package/dist/commands/uptime.d.ts.map +1 -0
  189. package/dist/commands/uptime.js +43 -0
  190. package/dist/commands/wget.d.ts.map +1 -1
  191. package/dist/commands/wget.js +92 -96
  192. package/dist/commands/which.d.ts +3 -0
  193. package/dist/commands/which.d.ts.map +1 -0
  194. package/dist/commands/which.js +32 -0
  195. package/dist/commands/xargs.d.ts.map +1 -1
  196. package/dist/commands/xargs.js +1 -1
  197. package/dist/index.d.ts +15 -11
  198. package/dist/index.d.ts.map +1 -1
  199. package/dist/index.js +9 -8
  200. package/dist/modules/linuxRootfs.d.ts +41 -0
  201. package/dist/modules/linuxRootfs.d.ts.map +1 -0
  202. package/dist/modules/linuxRootfs.js +440 -0
  203. package/dist/modules/neofetch.d.ts.map +1 -1
  204. package/dist/modules/neofetch.js +1 -0
  205. package/dist/standalone-wo-sftp.d.ts +2 -0
  206. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  207. package/dist/standalone-wo-sftp.js +30 -0
  208. package/dist/utils/expand.d.ts +50 -0
  209. package/dist/utils/expand.d.ts.map +1 -0
  210. package/dist/utils/expand.js +183 -0
  211. package/dist/utils/vfsDiff.d.ts +90 -0
  212. package/dist/utils/vfsDiff.d.ts.map +1 -0
  213. package/dist/utils/vfsDiff.js +177 -0
  214. package/package.json +3 -1
  215. package/src/SSHMimic/exec.ts +10 -1
  216. package/src/SSHMimic/executor.ts +105 -21
  217. package/src/SSHMimic/index.ts +49 -15
  218. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  219. package/src/VirtualFileSystem/index.ts +78 -28
  220. package/src/VirtualPackageManager/index.ts +979 -0
  221. package/src/VirtualShell/index.ts +133 -14
  222. package/src/VirtualShell/shell.ts +23 -3
  223. package/src/VirtualShell/shellParser.ts +134 -36
  224. package/src/VirtualUserManager/index.ts +62 -22
  225. package/src/commands/adduser.ts +6 -0
  226. package/src/commands/alias.ts +64 -0
  227. package/src/commands/apt.ts +228 -0
  228. package/src/commands/awk.ts +20 -6
  229. package/src/commands/base64.ts +13 -2
  230. package/src/commands/cat.ts +40 -8
  231. package/src/commands/cd.ts +5 -0
  232. package/src/commands/chmod.ts +53 -3
  233. package/src/commands/command-helpers.ts +78 -4
  234. package/src/commands/cp.ts +5 -0
  235. package/src/commands/curl.ts +118 -33
  236. package/src/commands/cut.ts +8 -1
  237. package/src/commands/date.ts +7 -1
  238. package/src/commands/declare.ts +44 -0
  239. package/src/commands/diff.ts +17 -3
  240. package/src/commands/dpkg.ts +180 -0
  241. package/src/commands/du.ts +17 -5
  242. package/src/commands/echo.ts +41 -12
  243. package/src/commands/env.ts +11 -1
  244. package/src/commands/exit.ts +12 -2
  245. package/src/commands/export.ts +3 -1
  246. package/src/commands/find.ts +5 -0
  247. package/src/commands/free.ts +47 -0
  248. package/src/commands/grep.ts +12 -2
  249. package/src/commands/gzip.ts +28 -4
  250. package/src/commands/head.ts +5 -0
  251. package/src/commands/help.ts +121 -47
  252. package/src/commands/helpers.ts +8 -0
  253. package/src/commands/history.ts +34 -0
  254. package/src/commands/hostname.ts +5 -0
  255. package/src/commands/id.ts +4 -1
  256. package/src/commands/index.ts +9 -255
  257. package/src/commands/ls.ts +6 -3
  258. package/src/commands/lsb-release.ts +58 -0
  259. package/src/commands/man.ts +166 -0
  260. package/src/commands/nano.ts +1 -1
  261. package/src/commands/neofetch.ts +6 -1
  262. package/src/commands/node.ts +341 -0
  263. package/src/commands/npm.ts +132 -0
  264. package/src/commands/ping.ts +10 -3
  265. package/src/commands/printf.ts +112 -0
  266. package/src/commands/ps.ts +40 -6
  267. package/src/commands/python.ts +2229 -0
  268. package/src/commands/read.ts +41 -0
  269. package/src/commands/registry.ts +244 -0
  270. package/src/commands/runtime.ts +353 -0
  271. package/src/commands/sed.ts +27 -9
  272. package/src/commands/set.ts +9 -3
  273. package/src/commands/sh.ts +170 -44
  274. package/src/commands/shift.ts +53 -0
  275. package/src/commands/sleep.ts +2 -1
  276. package/src/commands/sort.ts +10 -6
  277. package/src/commands/source.ts +47 -0
  278. package/src/commands/sudo.ts +1 -1
  279. package/src/commands/tar.ts +28 -7
  280. package/src/commands/tee.ts +7 -1
  281. package/src/commands/test.ts +135 -0
  282. package/src/commands/tr.ts +3 -1
  283. package/src/commands/true.ts +17 -0
  284. package/src/commands/type.ts +43 -0
  285. package/src/commands/uname.ts +5 -1
  286. package/src/commands/uniq.ts +8 -2
  287. package/src/commands/uptime.ts +49 -0
  288. package/src/commands/wget.ts +105 -119
  289. package/src/commands/which.ts +37 -0
  290. package/src/commands/xargs.ts +11 -2
  291. package/src/index.ts +27 -18
  292. package/src/modules/linuxRootfs.ts +642 -0
  293. package/src/modules/neofetch.ts +1 -0
  294. package/src/standalone-wo-sftp.ts +38 -0
  295. package/src/utils/expand.ts +238 -0
  296. package/src/utils/vfsDiff.ts +275 -0
  297. package/standalone-wo-sftp.js +507 -0
  298. package/standalone-wo-sftp.js.map +7 -0
  299. package/standalone.js +486 -109
  300. package/standalone.js.map +4 -4
  301. package/tests/bun-test-shim.ts +9 -1
  302. package/tests/command-helpers.test.ts +1 -5
  303. package/tests/new-features.test.ts +1036 -0
  304. package/tests/parser-executor.test.ts +27 -27
  305. package/tests/sftp.test.ts +122 -42
  306. package/tests/users.test.ts +23 -5
  307. package/CHANGELOG.md +0 -150
@@ -11,9 +11,13 @@ const _globalEnv: Record<string, string> = {
11
11
  };
12
12
 
13
13
  /** @deprecated use env.vars from CommandContext */
14
- export function getEnvVar(name: string): string | undefined { return _globalEnv[name]; }
14
+ export function getEnvVar(name: string): string | undefined {
15
+ return _globalEnv[name];
16
+ }
15
17
  /** @deprecated use env.vars from CommandContext */
16
- export function setEnvVar(name: string, value: string): void { _globalEnv[name] = value; }
18
+ export function setEnvVar(name: string, value: string): void {
19
+ _globalEnv[name] = value;
20
+ }
17
21
  /** @deprecated use env.vars from CommandContext */
18
22
  export function getAllEnvVars(authUser: string): Record<string, string> {
19
23
  _globalEnv.USER = authUser;
@@ -28,7 +32,9 @@ export const setCommand: ShellModule = {
28
32
  params: ["[VAR=value]"],
29
33
  run: ({ args, env }) => {
30
34
  if (args.length === 0) {
31
- const out = Object.entries(env.vars).map(([k, v]) => `${k}=${v}`).join("\n");
35
+ const out = Object.entries(env.vars)
36
+ .map(([k, v]) => `${k}=${v}`)
37
+ .join("\n");
32
38
  return { stdout: out, exitCode: 0 };
33
39
  }
34
40
  for (const arg of args) {
@@ -1,20 +1,48 @@
1
- import type { CommandContext, CommandResult, ShellModule } from "../types/commands";
2
- import { getArg, ifFlag } from "./command-helpers";
1
+ import type {
2
+ CommandContext,
3
+ CommandResult,
4
+ ShellModule,
5
+ } from "../types/commands";
6
+ import { expandAsync } from "../utils/expand";
7
+ import { ifFlag } from "./command-helpers";
3
8
  import { resolvePath } from "./helpers";
4
- import { runCommand } from "./index";
9
+ import { runCommand } from "./runtime";
5
10
 
6
- /** Expand $VAR and ${VAR:-default} in a line using the current env */
7
- function expandVars(line: string, env: Record<string, string>, lastExit: number): string {
8
- return line
9
- .replace(/\$\?/g, String(lastExit))
10
- .replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, n, d) => env[n] ?? d)
11
- .replace(/\$\{([^}]+)\}/g, (_, n) => env[n] ?? "")
12
- .replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, n) => env[n] ?? "")
13
- .replace(/^~(\/|$)/, `${env.HOME ?? "/home/user"}$1`);
11
+ /** Alias for clarity inside sh.ts */
12
+ type ShellContext = CommandContext;
13
+
14
+ /**
15
+ * Expand all shell forms including $(cmd) substitution.
16
+ * Delegates to centralised expandAsync (single-quote-aware, depth-tracked).
17
+ */
18
+ async function expandVars(
19
+ line: string,
20
+ env: Record<string, string>,
21
+ lastExit: number,
22
+ ctx: ShellContext,
23
+ ): Promise<string> {
24
+ return expandAsync(line, env, lastExit, (sub) =>
25
+ runCommand(
26
+ sub,
27
+ ctx.authUser,
28
+ ctx.hostname,
29
+ ctx.mode,
30
+ ctx.cwd,
31
+ ctx.shell,
32
+ undefined,
33
+ ctx.env,
34
+ ).then((r) => r.stdout ?? ""),
35
+ );
14
36
  }
15
37
 
16
38
  type Block =
17
- | { type: "if"; cond: string; then: string[]; elif: Array<{ cond: string; body: string[] }>; else_: string[] }
39
+ | {
40
+ type: "if";
41
+ cond: string;
42
+ then_: string[];
43
+ elif: Array<{ cond: string; body: string[] }>;
44
+ else_: string[];
45
+ }
18
46
  | { type: "for"; var: string; list: string; body: string[] }
19
47
  | { type: "while"; cond: string; body: string[] }
20
48
  | { type: "cmd"; line: string };
@@ -25,10 +53,16 @@ function parseBlocks(lines: string[]): Block[] {
25
53
  let i = 0;
26
54
  while (i < lines.length) {
27
55
  const line = lines[i]!.trim();
28
- if (!line || line.startsWith("#")) { i++; continue; }
56
+ if (!line || line.startsWith("#")) {
57
+ i++;
58
+ continue;
59
+ }
29
60
 
30
61
  if (line.startsWith("if ") || line === "if") {
31
- const cond = line.replace(/^if\s+/, "").replace(/;\s*then\s*$/, "").trim();
62
+ const cond = line
63
+ .replace(/^if\s+/, "")
64
+ .replace(/;\s*then\s*$/, "")
65
+ .trim();
32
66
  const thenLines: string[] = [];
33
67
  const elifBlocks: Array<{ cond: string; body: string[] }> = [];
34
68
  const elseLines: string[] = [];
@@ -37,36 +71,54 @@ function parseBlocks(lines: string[]): Block[] {
37
71
  i++;
38
72
  while (i < lines.length && lines[i]?.trim() !== "fi") {
39
73
  const l = lines[i]!.trim();
40
- if (l.startsWith("elif ")) { section = "elif"; elifCond = l.replace(/^elif\s+/, "").replace(/;\s*then\s*$/, "").trim(); elifBlocks.push({ cond: elifCond, body: [] }); }
41
- else if (l === "else") { section = "else"; }
42
- else if (l !== "then") {
74
+ if (l.startsWith("elif ")) {
75
+ section = "elif";
76
+ elifCond = l
77
+ .replace(/^elif\s+/, "")
78
+ .replace(/;\s*then\s*$/, "")
79
+ .trim();
80
+ elifBlocks.push({ cond: elifCond, body: [] });
81
+ } else if (l === "else") {
82
+ section = "else";
83
+ } else if (l !== "then") {
43
84
  if (section === "then") thenLines.push(l);
44
- else if (section === "elif" && elifBlocks.length > 0) elifBlocks[elifBlocks.length - 1]!.body.push(l);
85
+ else if (section === "elif" && elifBlocks.length > 0)
86
+ elifBlocks[elifBlocks.length - 1]!.body.push(l);
45
87
  else elseLines.push(l);
46
88
  }
47
89
  i++;
48
90
  }
49
- // biome-ignore lint/suspicious/noThenProperty: expected behavior for if/elif parsing
50
- blocks.push({ type: "if", cond, then: thenLines, elif: elifBlocks, else_: elseLines });
91
+ blocks.push({
92
+ type: "if",
93
+ cond,
94
+ then_: thenLines,
95
+ elif: elifBlocks,
96
+ else_: elseLines,
97
+ });
51
98
  } else if (line.startsWith("for ")) {
52
99
  const m = line.match(/^for\s+(\w+)\s+in\s+(.+?)(?:\s*;\s*do)?$/);
53
100
  if (m) {
54
101
  const body: string[] = [];
55
102
  i++;
56
103
  while (i < lines.length && lines[i]?.trim() !== "done") {
57
- const l = lines[i]!.trim();
58
- if (l !== "do") body.push(l);
104
+ const l = lines[i]!.trim().replace(/^do\s+/, "");
105
+ if (l && l !== "do") body.push(l);
59
106
  i++;
60
107
  }
61
108
  blocks.push({ type: "for", var: m[1]!, list: m[2]!, body });
62
- } else { blocks.push({ type: "cmd", line }); }
109
+ } else {
110
+ blocks.push({ type: "cmd", line });
111
+ }
63
112
  } else if (line.startsWith("while ")) {
64
- const cond = line.replace(/^while\s+/, "").replace(/;\s*do\s*$/, "").trim();
113
+ const cond = line
114
+ .replace(/^while\s+/, "")
115
+ .replace(/;\s*do\s*$/, "")
116
+ .trim();
65
117
  const body: string[] = [];
66
118
  i++;
67
119
  while (i < lines.length && lines[i]?.trim() !== "done") {
68
- const l = lines[i]!.trim();
69
- if (l !== "do") body.push(l);
120
+ const l = lines[i]!.trim().replace(/^do\s+/, "");
121
+ if (l && l !== "do") body.push(l);
70
122
  i++;
71
123
  }
72
124
  blocks.push({ type: "while", cond, body });
@@ -78,8 +130,16 @@ function parseBlocks(lines: string[]): Block[] {
78
130
  return blocks;
79
131
  }
80
132
 
81
- async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean> {
82
- const expanded = expandVars(cond, ctx.env.vars, ctx.env.lastExitCode);
133
+ async function evalCondition(
134
+ cond: string,
135
+ ctx: CommandContext,
136
+ ): Promise<boolean> {
137
+ const expanded = await expandVars(
138
+ cond,
139
+ ctx.env.vars,
140
+ ctx.env.lastExitCode,
141
+ ctx,
142
+ );
83
143
  // test -f / test -d / [ ... ]
84
144
  const testMatch = expanded.match(/^\[?\s*(.+?)\s*\]?$/);
85
145
  if (testMatch) {
@@ -89,8 +149,12 @@ async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean
89
149
  if (fTest) {
90
150
  const [, flag, arg] = fTest;
91
151
  const p = resolvePath(ctx.cwd, arg!);
92
- if (flag === "f") return ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "file";
93
- if (flag === "d") return ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "directory";
152
+ if (flag === "f")
153
+ return ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "file";
154
+ if (flag === "d")
155
+ return (
156
+ ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "directory"
157
+ );
94
158
  if (flag === "e") return ctx.shell.vfs.exists(p);
95
159
  if (flag === "z") return (arg ?? "").length === 0;
96
160
  if (flag === "n") return (arg ?? "").length > 0;
@@ -106,7 +170,8 @@ async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean
106
170
  const numMatch = expr.match(/^(\S+)\s+(-eq|-ne|-lt|-le|-gt|-ge)\s+(\S+)$/);
107
171
  if (numMatch) {
108
172
  const [, a, op, b] = numMatch;
109
- const na = Number(a), nb = Number(b);
173
+ const na = Number(a),
174
+ nb = Number(b);
110
175
  if (op === "-eq") return na === nb;
111
176
  if (op === "-ne") return na !== nb;
112
177
  if (op === "-lt") return na < nb;
@@ -116,18 +181,60 @@ async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean
116
181
  }
117
182
  }
118
183
  // fallback: run command and check exit code
119
- const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
184
+ const r = await runCommand(
185
+ expanded,
186
+ ctx.authUser,
187
+ ctx.hostname,
188
+ ctx.mode,
189
+ ctx.cwd,
190
+ ctx.shell,
191
+ undefined,
192
+ ctx.env,
193
+ );
120
194
  return (r.exitCode ?? 0) === 0;
121
195
  }
122
196
 
123
- async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandResult> {
197
+ async function runBlocks(
198
+ blocks: Block[],
199
+ ctx: CommandContext,
200
+ ): Promise<CommandResult> {
124
201
  let lastResult: CommandResult = { exitCode: 0 };
125
202
  let output = "";
126
203
 
127
204
  for (const block of blocks) {
128
205
  if (block.type === "cmd") {
129
- const expanded = expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode);
130
- const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
206
+ const expanded = await expandVars(
207
+ block.line,
208
+ ctx.env.vars,
209
+ ctx.env.lastExitCode,
210
+ ctx,
211
+ );
212
+
213
+ // Bare VAR=val assignment(s) — handle before dispatching to runCommand
214
+ const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)/;
215
+ const tokens = expanded.trim().split(/\s+/);
216
+ if (tokens.length > 0 && assignRe.test(tokens[0]!)) {
217
+ const allAssign = tokens.every((t) => assignRe.test(t));
218
+ if (allAssign) {
219
+ for (const tok of tokens) {
220
+ const m = tok.match(assignRe)!;
221
+ ctx.env.vars[m[1]!] = m[2]!;
222
+ }
223
+ ctx.env.lastExitCode = 0;
224
+ continue;
225
+ }
226
+ }
227
+
228
+ const r = await runCommand(
229
+ expanded,
230
+ ctx.authUser,
231
+ ctx.hostname,
232
+ ctx.mode,
233
+ ctx.cwd,
234
+ ctx.shell,
235
+ undefined,
236
+ ctx.env,
237
+ );
131
238
  ctx.env.lastExitCode = r.exitCode ?? 0;
132
239
  if (r.stdout) output += `${r.stdout}\n`;
133
240
  if (r.stderr) return { ...r, stdout: output.trim() };
@@ -135,7 +242,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
135
242
  } else if (block.type === "if") {
136
243
  let ran = false;
137
244
  if (await evalCondition(block.cond, ctx)) {
138
- const sub = await runBlocks(parseBlocks(block.then), ctx);
245
+ const sub = await runBlocks(parseBlocks(block.then_), ctx);
139
246
  if (sub.stdout) output += `${sub.stdout}\n`;
140
247
  ran = true;
141
248
  } else {
@@ -143,7 +250,8 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
143
250
  if (await evalCondition(elif.cond, ctx)) {
144
251
  const sub = await runBlocks(parseBlocks(elif.body), ctx);
145
252
  if (sub.stdout) output += `${sub.stdout}\n`;
146
- ran = true; break;
253
+ ran = true;
254
+ break;
147
255
  }
148
256
  }
149
257
  if (!ran && block.else_.length > 0) {
@@ -152,7 +260,12 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
152
260
  }
153
261
  }
154
262
  } else if (block.type === "for") {
155
- const listExpanded = expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode);
263
+ const listExpanded = await expandVars(
264
+ block.list,
265
+ ctx.env.vars,
266
+ ctx.env.lastExitCode,
267
+ ctx,
268
+ );
156
269
  const items = listExpanded.trim().split(/\s+/);
157
270
  for (const item of items) {
158
271
  ctx.env.vars[block.var] = item;
@@ -162,7 +275,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
162
275
  }
163
276
  } else if (block.type === "while") {
164
277
  let iterations = 0;
165
- while (iterations < 1000 && await evalCondition(block.cond, ctx)) {
278
+ while (iterations < 1000 && (await evalCondition(block.cond, ctx))) {
166
279
  const sub = await runBlocks(parseBlocks(block.body), ctx);
167
280
  if (sub.stdout) output += `${sub.stdout}\n`;
168
281
  if (sub.closeSession) return sub;
@@ -184,9 +297,12 @@ export const shCommand: ShellModule = {
184
297
 
185
298
  // sh -c "inline script"
186
299
  if (ifFlag(args, "-c")) {
187
- const script = getArg(args, 1) ?? "";
300
+ const script = args[args.indexOf("-c") + 1] ?? "";
188
301
  if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
189
- const lines = script.split(/[;\n]/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
302
+ const lines = script
303
+ .split(/[;\n]/)
304
+ .map((l) => l.trim())
305
+ .filter((l) => l && !l.startsWith("#"));
190
306
  const blocks = parseBlocks(lines);
191
307
  return runBlocks(blocks, ctx);
192
308
  }
@@ -195,13 +311,23 @@ export const shCommand: ShellModule = {
195
311
  const fileArg = args[0];
196
312
  if (fileArg) {
197
313
  const p = resolvePath(cwd, fileArg);
198
- if (!shell.vfs.exists(p)) return { stderr: `sh: ${fileArg}: No such file or directory`, exitCode: 1 };
314
+ if (!shell.vfs.exists(p))
315
+ return {
316
+ stderr: `sh: ${fileArg}: No such file or directory`,
317
+ exitCode: 1,
318
+ };
199
319
  const content = shell.vfs.readFile(p);
200
- const lines = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
320
+ const lines = content
321
+ .split("\n")
322
+ .map((l) => l.trim())
323
+ .filter((l) => l && !l.startsWith("#"));
201
324
  const blocks = parseBlocks(lines);
202
325
  return runBlocks(blocks, ctx);
203
326
  }
204
327
 
205
- return { stderr: "sh: invalid usage. Use: sh -c 'cmd' or sh <file>", exitCode: 1 };
328
+ return {
329
+ stderr: "sh: invalid usage. Use: sh -c 'cmd' or sh <file>",
330
+ exitCode: 1,
331
+ };
206
332
  },
207
333
  };
@@ -0,0 +1,53 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ export const shiftCommand: ShellModule = {
4
+ name: "shift",
5
+ description: "Shift positional parameters",
6
+ category: "shell",
7
+ params: ["[n]"],
8
+ // shift is meaningful only inside sh scripts where positional params exist.
9
+ // In the current impl, positional params ($1 $2 …) aren't tracked in env by default.
10
+ // We store them under env.vars.__argv and shift there if present.
11
+ run: ({ args, env }) => {
12
+ if (!env) return { exitCode: 0 };
13
+ const n = parseInt(args[0] ?? "1", 10) || 1;
14
+ const argv = env.vars.__argv?.split("\x00").filter(Boolean) ?? [];
15
+ env.vars.__argv = argv.slice(n).join("\x00");
16
+ // Update $1 $2 … in env
17
+ const shifted = argv.slice(n);
18
+ for (let i = 1; i <= 9; i++) {
19
+ env.vars[String(i)] = shifted[i - 1] ?? "";
20
+ }
21
+ return { exitCode: 0 };
22
+ },
23
+ };
24
+
25
+ export const trapCommand: ShellModule = {
26
+ name: "trap",
27
+ description: "Trap signals and events",
28
+ category: "shell",
29
+ params: ["[action] [signal...]"],
30
+ // Store trap handlers in env for EXIT signal support
31
+ run: ({ args, env }) => {
32
+ if (!env || args.length === 0) return { exitCode: 0 };
33
+ const action = args[0] ?? "";
34
+ const signals = args.slice(1);
35
+ for (const sig of signals) {
36
+ env.vars[`__trap_${sig.toUpperCase()}`] = action;
37
+ }
38
+ return { exitCode: 0 };
39
+ },
40
+ };
41
+
42
+ export const returnCommand: ShellModule = {
43
+ name: "return",
44
+ description: "Return from a shell function",
45
+ category: "shell",
46
+ params: ["[n]"],
47
+ run: ({ args, env }) => {
48
+ const code = parseInt(args[0] ?? "0", 10);
49
+ if (env) env.lastExitCode = code;
50
+ // Signal the caller via exitCode; function return is handled by runBlocks
51
+ return { exitCode: code };
52
+ },
53
+ };
@@ -7,7 +7,8 @@ export const sleepCommand: ShellModule = {
7
7
  params: ["<seconds>"],
8
8
  run: async ({ args }) => {
9
9
  const secs = parseFloat(args[0] ?? "1");
10
- if (Number.isNaN(secs) || secs < 0) return { stderr: "sleep: invalid time", exitCode: 1 };
10
+ if (Number.isNaN(secs) || secs < 0)
11
+ return { stderr: "sleep: invalid time", exitCode: 1 };
11
12
  await new Promise((r) => setTimeout(r, secs * 1000));
12
13
  return { exitCode: 0 };
13
14
  },
@@ -15,12 +15,16 @@ export const sortCommand: ShellModule = {
15
15
 
16
16
  const getContent = (): string => {
17
17
  if (files.length > 0) {
18
- return files.map((f) => {
19
- try {
20
- assertPathAccess(authUser, resolvePath(cwd, f), "sort");
21
- return shell.vfs.readFile(resolvePath(cwd, f));
22
- } catch { return ""; }
23
- }).join("\n");
18
+ return files
19
+ .map((f) => {
20
+ try {
21
+ assertPathAccess(authUser, resolvePath(cwd, f), "sort");
22
+ return shell.vfs.readFile(resolvePath(cwd, f));
23
+ } catch {
24
+ return "";
25
+ }
26
+ })
27
+ .join("\n");
24
28
  }
25
29
  return stdin ?? "";
26
30
  };
@@ -0,0 +1,47 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { resolvePath } from "./helpers";
3
+ import { runCommand } from "./runtime";
4
+
5
+ export const sourceCommand: ShellModule = {
6
+ name: "source",
7
+ aliases: ["."],
8
+ description: "Execute commands from a file in the current shell environment",
9
+ category: "shell",
10
+ params: ["<file> [args...]"],
11
+ run: async ({ args, authUser, hostname, cwd, shell, env }) => {
12
+ const fileArg = args[0];
13
+ if (!fileArg) {
14
+ return { stderr: "source: missing filename", exitCode: 1 };
15
+ }
16
+
17
+ const filePath = resolvePath(cwd, fileArg);
18
+ if (!shell.vfs.exists(filePath)) {
19
+ return {
20
+ stderr: `source: ${fileArg}: No such file or directory`,
21
+ exitCode: 1,
22
+ };
23
+ }
24
+
25
+ const content = shell.vfs.readFile(filePath);
26
+ let lastExitCode = 0;
27
+
28
+ for (const line of content.split("\n")) {
29
+ const l = line.trim();
30
+ if (!l || l.startsWith("#")) continue;
31
+ const result = await runCommand(
32
+ l,
33
+ authUser,
34
+ hostname,
35
+ "shell",
36
+ cwd,
37
+ shell,
38
+ undefined,
39
+ env,
40
+ );
41
+ lastExitCode = result.exitCode ?? 0;
42
+ if (result.closeSession || result.switchUser) return result;
43
+ }
44
+
45
+ return { exitCode: lastExitCode };
46
+ },
47
+ };
@@ -1,6 +1,6 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { parseArgs } from "./command-helpers";
3
- import { runCommand } from "./index";
3
+ import { runCommand } from "./runtime";
4
4
 
5
5
  function parseSudoArgs(args: string[]): {
6
6
  targetUser: string;
@@ -12,13 +12,22 @@ export const tarCommand: ShellModule = {
12
12
  const extract = ifFlag(args, ["-x"]);
13
13
  const list = ifFlag(args, ["-t"]);
14
14
  const fFlag = args.findIndex((a) => a.includes("f"));
15
- const archiveName = fFlag !== -1 ? args[fFlag + 1] : args.find((a) => a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz"));
15
+ const archiveName =
16
+ fFlag !== -1
17
+ ? args[fFlag + 1]
18
+ : args.find(
19
+ (a) =>
20
+ a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz"),
21
+ );
16
22
 
17
- if (!archiveName) return { stderr: "tar: no archive specified", exitCode: 1 };
23
+ if (!archiveName)
24
+ return { stderr: "tar: no archive specified", exitCode: 1 };
18
25
  const archivePath = resolvePath(cwd, archiveName);
19
26
 
20
27
  if (create) {
21
- const fileArgs = args.filter((a) => !a.startsWith("-") && a !== archiveName);
28
+ const fileArgs = args.filter(
29
+ (a) => !a.startsWith("-") && a !== archiveName,
30
+ );
22
31
  const entries: Record<string, string> = {};
23
32
  for (const f of fileArgs) {
24
33
  const p = resolvePath(cwd, f);
@@ -28,7 +37,8 @@ export const tarCommand: ShellModule = {
28
37
  else {
29
38
  const walk = (dir: string, prefix: string) => {
30
39
  for (const e of shell.vfs.list(dir)) {
31
- const full = `${dir}/${e}`, rel = `${prefix}/${e}`;
40
+ const full = `${dir}/${e}`,
41
+ rel = `${prefix}/${e}`;
32
42
  const s = shell.vfs.stat(full);
33
43
  if (s.type === "file") entries[rel] = shell.vfs.readFile(full);
34
44
  else walk(full, rel);
@@ -36,7 +46,12 @@ export const tarCommand: ShellModule = {
36
46
  };
37
47
  walk(p, f);
38
48
  }
39
- } catch { return { stderr: `tar: ${f}: No such file or directory`, exitCode: 1 }; }
49
+ } catch {
50
+ return {
51
+ stderr: `tar: ${f}: No such file or directory`,
52
+ exitCode: 1,
53
+ };
54
+ }
40
55
  }
41
56
  shell.writeFileAsUser(authUser, archivePath, JSON.stringify(entries));
42
57
  return { exitCode: 0 };
@@ -44,8 +59,14 @@ export const tarCommand: ShellModule = {
44
59
 
45
60
  if (list || extract) {
46
61
  let entries: Record<string, string>;
47
- try { entries = JSON.parse(shell.vfs.readFile(archivePath)); }
48
- catch { return { stderr: `tar: ${archiveName}: cannot open archive`, exitCode: 1 }; }
62
+ try {
63
+ entries = JSON.parse(shell.vfs.readFile(archivePath));
64
+ } catch {
65
+ return {
66
+ stderr: `tar: ${archiveName}: cannot open archive`,
67
+ exitCode: 1,
68
+ };
69
+ }
49
70
  if (list) return { stdout: Object.keys(entries).join("\n"), exitCode: 0 };
50
71
  for (const [name, content] of Object.entries(entries)) {
51
72
  shell.writeFileAsUser(authUser, resolvePath(cwd, name), content);
@@ -14,7 +14,13 @@ export const teeCommand: ShellModule = {
14
14
  for (const f of files) {
15
15
  const p = resolvePath(cwd, f);
16
16
  if (append) {
17
- const existing = (() => { try { return shell.vfs.readFile(p); } catch { return ""; } })();
17
+ const existing = (() => {
18
+ try {
19
+ return shell.vfs.readFile(p);
20
+ } catch {
21
+ return "";
22
+ }
23
+ })();
18
24
  shell.writeFileAsUser(authUser, p, existing + input);
19
25
  } else {
20
26
  shell.writeFileAsUser(authUser, p, input);