typescript-virtual-container 1.2.9 → 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 (281) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +141 -50
  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 +32 -16
  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.map +1 -1
  14. package/dist/VirtualPackageManager/index.js +192 -43
  15. package/dist/VirtualShell/index.d.ts +10 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +18 -7
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +3 -1
  20. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  21. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  22. package/dist/commands/adduser.d.ts +6 -0
  23. package/dist/commands/adduser.d.ts.map +1 -1
  24. package/dist/commands/adduser.js +6 -0
  25. package/dist/commands/alias.d.ts +5 -0
  26. package/dist/commands/alias.d.ts.map +1 -1
  27. package/dist/commands/alias.js +5 -0
  28. package/dist/commands/apt.d.ts +5 -0
  29. package/dist/commands/apt.d.ts.map +1 -1
  30. package/dist/commands/apt.js +32 -9
  31. package/dist/commands/awk.d.ts +11 -0
  32. package/dist/commands/awk.d.ts.map +1 -1
  33. package/dist/commands/awk.js +15 -2
  34. package/dist/commands/base64.d.ts +5 -0
  35. package/dist/commands/base64.d.ts.map +1 -1
  36. package/dist/commands/base64.js +9 -1
  37. package/dist/commands/cat.d.ts +5 -0
  38. package/dist/commands/cat.d.ts.map +1 -1
  39. package/dist/commands/cat.js +10 -2
  40. package/dist/commands/cd.d.ts +5 -0
  41. package/dist/commands/cd.d.ts.map +1 -1
  42. package/dist/commands/cd.js +5 -0
  43. package/dist/commands/chmod.d.ts +5 -0
  44. package/dist/commands/chmod.d.ts.map +1 -1
  45. package/dist/commands/chmod.js +5 -0
  46. package/dist/commands/cp.d.ts +5 -0
  47. package/dist/commands/cp.d.ts.map +1 -1
  48. package/dist/commands/cp.js +5 -0
  49. package/dist/commands/curl.d.ts +5 -0
  50. package/dist/commands/curl.d.ts.map +1 -1
  51. package/dist/commands/curl.js +34 -6
  52. package/dist/commands/cut.d.ts +5 -0
  53. package/dist/commands/cut.d.ts.map +1 -1
  54. package/dist/commands/cut.js +8 -1
  55. package/dist/commands/date.d.ts +5 -0
  56. package/dist/commands/date.d.ts.map +1 -1
  57. package/dist/commands/date.js +7 -1
  58. package/dist/commands/declare.d.ts +3 -0
  59. package/dist/commands/declare.d.ts.map +1 -0
  60. package/dist/commands/declare.js +39 -0
  61. package/dist/commands/diff.d.ts +5 -0
  62. package/dist/commands/diff.d.ts.map +1 -1
  63. package/dist/commands/diff.js +5 -0
  64. package/dist/commands/dpkg.d.ts +5 -0
  65. package/dist/commands/dpkg.d.ts.map +1 -1
  66. package/dist/commands/dpkg.js +24 -7
  67. package/dist/commands/du.d.ts.map +1 -1
  68. package/dist/commands/du.js +8 -2
  69. package/dist/commands/echo.d.ts +5 -0
  70. package/dist/commands/echo.d.ts.map +1 -1
  71. package/dist/commands/echo.js +13 -4
  72. package/dist/commands/env.d.ts +5 -0
  73. package/dist/commands/env.d.ts.map +1 -1
  74. package/dist/commands/env.js +11 -1
  75. package/dist/commands/exit.d.ts +5 -0
  76. package/dist/commands/exit.d.ts.map +1 -1
  77. package/dist/commands/exit.js +12 -2
  78. package/dist/commands/export.d.ts.map +1 -1
  79. package/dist/commands/export.js +3 -1
  80. package/dist/commands/find.d.ts +5 -0
  81. package/dist/commands/find.d.ts.map +1 -1
  82. package/dist/commands/find.js +5 -0
  83. package/dist/commands/free.d.ts +5 -0
  84. package/dist/commands/free.d.ts.map +1 -1
  85. package/dist/commands/free.js +5 -0
  86. package/dist/commands/grep.d.ts +5 -0
  87. package/dist/commands/grep.d.ts.map +1 -1
  88. package/dist/commands/grep.js +12 -2
  89. package/dist/commands/gzip.d.ts +5 -0
  90. package/dist/commands/gzip.d.ts.map +1 -1
  91. package/dist/commands/gzip.js +18 -2
  92. package/dist/commands/head.d.ts +5 -0
  93. package/dist/commands/head.d.ts.map +1 -1
  94. package/dist/commands/head.js +5 -0
  95. package/dist/commands/help.d.ts.map +1 -1
  96. package/dist/commands/help.js +98 -45
  97. package/dist/commands/history.d.ts +5 -0
  98. package/dist/commands/history.d.ts.map +1 -1
  99. package/dist/commands/history.js +5 -0
  100. package/dist/commands/hostname.d.ts +5 -0
  101. package/dist/commands/hostname.d.ts.map +1 -1
  102. package/dist/commands/hostname.js +5 -0
  103. package/dist/commands/id.d.ts.map +1 -1
  104. package/dist/commands/id.js +4 -1
  105. package/dist/commands/index.d.ts +2 -17
  106. package/dist/commands/index.d.ts.map +1 -1
  107. package/dist/commands/index.js +2 -340
  108. package/dist/commands/ls.d.ts.map +1 -1
  109. package/dist/commands/ls.js +3 -1
  110. package/dist/commands/lsb-release.d.ts.map +1 -1
  111. package/dist/commands/lsb-release.js +8 -2
  112. package/dist/commands/nano.js +1 -1
  113. package/dist/commands/neofetch.js +1 -1
  114. package/dist/commands/node.d.ts +9 -0
  115. package/dist/commands/node.d.ts.map +1 -0
  116. package/dist/commands/node.js +316 -0
  117. package/dist/commands/npm.d.ts +19 -0
  118. package/dist/commands/npm.d.ts.map +1 -0
  119. package/dist/commands/npm.js +109 -0
  120. package/dist/commands/ping.d.ts.map +1 -1
  121. package/dist/commands/ping.js +3 -1
  122. package/dist/commands/printf.d.ts +3 -0
  123. package/dist/commands/printf.d.ts.map +1 -0
  124. package/dist/commands/printf.js +113 -0
  125. package/dist/commands/ps.d.ts.map +1 -1
  126. package/dist/commands/ps.js +4 -1
  127. package/dist/commands/python.d.ts +30 -0
  128. package/dist/commands/python.d.ts.map +1 -0
  129. package/dist/commands/python.js +2058 -0
  130. package/dist/commands/read.d.ts +3 -0
  131. package/dist/commands/read.d.ts.map +1 -0
  132. package/dist/commands/read.js +34 -0
  133. package/dist/commands/registry.d.ts +8 -0
  134. package/dist/commands/registry.d.ts.map +1 -0
  135. package/dist/commands/registry.js +229 -0
  136. package/dist/commands/runtime.d.ts +6 -0
  137. package/dist/commands/runtime.d.ts.map +1 -0
  138. package/dist/commands/runtime.js +280 -0
  139. package/dist/commands/sed.d.ts.map +1 -1
  140. package/dist/commands/sed.js +11 -3
  141. package/dist/commands/set.d.ts.map +1 -1
  142. package/dist/commands/set.js +9 -3
  143. package/dist/commands/sh.d.ts.map +1 -1
  144. package/dist/commands/sh.js +57 -36
  145. package/dist/commands/shift.d.ts +5 -0
  146. package/dist/commands/shift.d.ts.map +1 -0
  147. package/dist/commands/shift.js +52 -0
  148. package/dist/commands/sleep.d.ts.map +1 -1
  149. package/dist/commands/sort.d.ts.map +1 -1
  150. package/dist/commands/sort.js +4 -2
  151. package/dist/commands/source.d.ts.map +1 -1
  152. package/dist/commands/source.js +5 -2
  153. package/dist/commands/sudo.js +1 -1
  154. package/dist/commands/tar.d.ts.map +1 -1
  155. package/dist/commands/tar.js +11 -3
  156. package/dist/commands/tee.d.ts.map +1 -1
  157. package/dist/commands/tee.js +8 -6
  158. package/dist/commands/test.d.ts.map +1 -1
  159. package/dist/commands/test.js +46 -24
  160. package/dist/commands/tr.d.ts.map +1 -1
  161. package/dist/commands/tr.js +3 -1
  162. package/dist/commands/true.d.ts +4 -0
  163. package/dist/commands/true.d.ts.map +1 -0
  164. package/dist/commands/true.js +14 -0
  165. package/dist/commands/type.d.ts.map +1 -1
  166. package/dist/commands/type.js +1 -1
  167. package/dist/commands/uname.d.ts.map +1 -1
  168. package/dist/commands/uname.js +4 -1
  169. package/dist/commands/uniq.d.ts.map +1 -1
  170. package/dist/commands/uptime.d.ts.map +1 -1
  171. package/dist/commands/uptime.js +4 -1
  172. package/dist/commands/wget.d.ts.map +1 -1
  173. package/dist/commands/wget.js +32 -7
  174. package/dist/commands/which.d.ts.map +1 -1
  175. package/dist/commands/xargs.d.ts.map +1 -1
  176. package/dist/commands/xargs.js +1 -1
  177. package/dist/index.d.ts +15 -14
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +9 -9
  180. package/dist/modules/linuxRootfs.d.ts +18 -1
  181. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  182. package/dist/modules/linuxRootfs.js +160 -17
  183. package/dist/standalone-wo-sftp.d.ts +2 -0
  184. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  185. package/dist/standalone-wo-sftp.js +30 -0
  186. package/dist/utils/expand.d.ts +50 -0
  187. package/dist/utils/expand.d.ts.map +1 -0
  188. package/dist/utils/expand.js +183 -0
  189. package/dist/utils/vfsDiff.d.ts +90 -0
  190. package/dist/utils/vfsDiff.d.ts.map +1 -0
  191. package/dist/utils/vfsDiff.js +177 -0
  192. package/package.json +2 -1
  193. package/src/SSHMimic/exec.ts +10 -1
  194. package/src/SSHMimic/executor.ts +104 -18
  195. package/src/SSHMimic/index.ts +49 -15
  196. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  197. package/src/VirtualFileSystem/index.ts +78 -28
  198. package/src/VirtualPackageManager/index.ts +208 -49
  199. package/src/VirtualShell/index.ts +35 -7
  200. package/src/VirtualShell/shell.ts +23 -3
  201. package/src/VirtualShell/shellParser.ts +134 -36
  202. package/src/VirtualUserManager/index.ts +7 -2
  203. package/src/commands/adduser.ts +6 -0
  204. package/src/commands/alias.ts +5 -1
  205. package/src/commands/apt.ts +47 -17
  206. package/src/commands/awk.ts +20 -6
  207. package/src/commands/base64.ts +13 -2
  208. package/src/commands/cat.ts +13 -5
  209. package/src/commands/cd.ts +5 -0
  210. package/src/commands/chmod.ts +5 -0
  211. package/src/commands/cp.ts +5 -0
  212. package/src/commands/curl.ts +56 -12
  213. package/src/commands/cut.ts +8 -1
  214. package/src/commands/date.ts +7 -1
  215. package/src/commands/declare.ts +44 -0
  216. package/src/commands/diff.ts +17 -3
  217. package/src/commands/dpkg.ts +33 -11
  218. package/src/commands/du.ts +17 -5
  219. package/src/commands/echo.ts +22 -9
  220. package/src/commands/env.ts +11 -1
  221. package/src/commands/exit.ts +12 -2
  222. package/src/commands/export.ts +3 -1
  223. package/src/commands/find.ts +5 -0
  224. package/src/commands/free.ts +9 -2
  225. package/src/commands/grep.ts +12 -2
  226. package/src/commands/gzip.ts +28 -4
  227. package/src/commands/head.ts +5 -0
  228. package/src/commands/help.ts +121 -47
  229. package/src/commands/history.ts +7 -2
  230. package/src/commands/hostname.ts +5 -0
  231. package/src/commands/id.ts +4 -1
  232. package/src/commands/index.ts +9 -360
  233. package/src/commands/ls.ts +5 -3
  234. package/src/commands/lsb-release.ts +8 -2
  235. package/src/commands/nano.ts +1 -1
  236. package/src/commands/neofetch.ts +1 -1
  237. package/src/commands/node.ts +341 -0
  238. package/src/commands/npm.ts +132 -0
  239. package/src/commands/ping.ts +6 -2
  240. package/src/commands/printf.ts +112 -0
  241. package/src/commands/ps.ts +21 -9
  242. package/src/commands/python.ts +2229 -0
  243. package/src/commands/read.ts +41 -0
  244. package/src/commands/registry.ts +244 -0
  245. package/src/commands/runtime.ts +353 -0
  246. package/src/commands/sed.ts +27 -9
  247. package/src/commands/set.ts +9 -3
  248. package/src/commands/sh.ts +159 -55
  249. package/src/commands/shift.ts +53 -0
  250. package/src/commands/sleep.ts +2 -1
  251. package/src/commands/sort.ts +10 -6
  252. package/src/commands/source.ts +15 -3
  253. package/src/commands/sudo.ts +1 -1
  254. package/src/commands/tar.ts +28 -7
  255. package/src/commands/tee.ts +7 -1
  256. package/src/commands/test.ts +61 -26
  257. package/src/commands/tr.ts +3 -1
  258. package/src/commands/true.ts +17 -0
  259. package/src/commands/type.ts +6 -3
  260. package/src/commands/uname.ts +5 -1
  261. package/src/commands/uniq.ts +8 -2
  262. package/src/commands/uptime.ts +4 -1
  263. package/src/commands/wget.ts +51 -12
  264. package/src/commands/which.ts +5 -2
  265. package/src/commands/xargs.ts +11 -2
  266. package/src/index.ts +23 -24
  267. package/src/modules/linuxRootfs.ts +233 -30
  268. package/src/standalone-wo-sftp.ts +38 -0
  269. package/src/utils/expand.ts +238 -0
  270. package/src/utils/vfsDiff.ts +275 -0
  271. package/standalone-wo-sftp.js +507 -0
  272. package/standalone-wo-sftp.js.map +7 -0
  273. package/standalone.js +253 -191
  274. package/standalone.js.map +4 -4
  275. package/tests/bun-test-shim.ts +9 -1
  276. package/tests/command-helpers.test.ts +1 -5
  277. package/tests/new-features.test.ts +415 -5
  278. package/tests/parser-executor.test.ts +27 -27
  279. package/tests/sftp.test.ts +122 -42
  280. package/tests/users.test.ts +23 -5
  281. 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,42 +1,48 @@
1
- import type { CommandContext, CommandResult, ShellModule } from "../types/commands";
1
+ import type {
2
+ CommandContext,
3
+ CommandResult,
4
+ ShellModule,
5
+ } from "../types/commands";
6
+ import { expandAsync } from "../utils/expand";
2
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
11
  /** Alias for clarity inside sh.ts */
7
12
  type ShellContext = CommandContext;
8
13
 
9
- /** Expand $VAR and ${VAR:-default} in a line using the current env (sync, no $(cmd)) */
10
- function expandVarsSync(line: string, env: Record<string, string>, lastExit: number): string {
11
- return line
12
- .replace(/\$\?/g, String(lastExit))
13
- .replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, n, d) => env[n] ?? d)
14
- .replace(/\$\{([^}]+)\}/g, (_, n) => env[n] ?? "")
15
- .replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, n) => env[n] ?? "")
16
- .replace(/^~(\/|$)/, `${env.HOME ?? "/home/user"}$1`);
17
- }
18
-
19
14
  /**
20
- * Expand $VAR, ${VAR:-default}, and $(cmd) substitution asynchronously.
21
- * Used before executing each line in sh script context.
15
+ * Expand all shell forms including $(cmd) substitution.
16
+ * Delegates to centralised expandAsync (single-quote-aware, depth-tracked).
22
17
  */
23
- async function expandVars(line: string, env: Record<string, string>, lastExit: number, ctx: ShellContext): Promise<string> {
24
- // $(cmd) substitution first
25
- if (line.includes("$(")) {
26
- const subRe = /\$\(([^)]+)\)/g;
27
- const matches = [...line.matchAll(subRe)];
28
- for (const m of matches) {
29
- const sub = m[1]?.trim() ?? "";
30
- const subResult = await runCommand(sub, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
31
- const subOut = (subResult.stdout ?? "").replace(/\n$/, "");
32
- line = line.replace(m[0], subOut);
33
- }
34
- }
35
- return expandVarsSync(line, env, lastExit);
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
+ );
36
36
  }
37
37
 
38
38
  type Block =
39
- | { 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
+ }
40
46
  | { type: "for"; var: string; list: string; body: string[] }
41
47
  | { type: "while"; cond: string; body: string[] }
42
48
  | { type: "cmd"; line: string };
@@ -47,10 +53,16 @@ function parseBlocks(lines: string[]): Block[] {
47
53
  let i = 0;
48
54
  while (i < lines.length) {
49
55
  const line = lines[i]!.trim();
50
- if (!line || line.startsWith("#")) { i++; continue; }
56
+ if (!line || line.startsWith("#")) {
57
+ i++;
58
+ continue;
59
+ }
51
60
 
52
61
  if (line.startsWith("if ") || line === "if") {
53
- 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();
54
66
  const thenLines: string[] = [];
55
67
  const elifBlocks: Array<{ cond: string; body: string[] }> = [];
56
68
  const elseLines: string[] = [];
@@ -59,17 +71,30 @@ function parseBlocks(lines: string[]): Block[] {
59
71
  i++;
60
72
  while (i < lines.length && lines[i]?.trim() !== "fi") {
61
73
  const l = lines[i]!.trim();
62
- if (l.startsWith("elif ")) { section = "elif"; elifCond = l.replace(/^elif\s+/, "").replace(/;\s*then\s*$/, "").trim(); elifBlocks.push({ cond: elifCond, body: [] }); }
63
- else if (l === "else") { section = "else"; }
64
- 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") {
65
84
  if (section === "then") thenLines.push(l);
66
- 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);
67
87
  else elseLines.push(l);
68
88
  }
69
89
  i++;
70
90
  }
71
- // biome-ignore lint/suspicious/noThenProperty: expected behavior for if/elif parsing
72
- 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
+ });
73
98
  } else if (line.startsWith("for ")) {
74
99
  const m = line.match(/^for\s+(\w+)\s+in\s+(.+?)(?:\s*;\s*do)?$/);
75
100
  if (m) {
@@ -81,9 +106,14 @@ function parseBlocks(lines: string[]): Block[] {
81
106
  i++;
82
107
  }
83
108
  blocks.push({ type: "for", var: m[1]!, list: m[2]!, body });
84
- } else { blocks.push({ type: "cmd", line }); }
109
+ } else {
110
+ blocks.push({ type: "cmd", line });
111
+ }
85
112
  } else if (line.startsWith("while ")) {
86
- 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();
87
117
  const body: string[] = [];
88
118
  i++;
89
119
  while (i < lines.length && lines[i]?.trim() !== "done") {
@@ -100,8 +130,16 @@ function parseBlocks(lines: string[]): Block[] {
100
130
  return blocks;
101
131
  }
102
132
 
103
- async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean> {
104
- const expanded = await expandVars(cond, ctx.env.vars, ctx.env.lastExitCode, ctx);
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
+ );
105
143
  // test -f / test -d / [ ... ]
106
144
  const testMatch = expanded.match(/^\[?\s*(.+?)\s*\]?$/);
107
145
  if (testMatch) {
@@ -111,8 +149,12 @@ async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean
111
149
  if (fTest) {
112
150
  const [, flag, arg] = fTest;
113
151
  const p = resolvePath(ctx.cwd, arg!);
114
- if (flag === "f") return ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "file";
115
- 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
+ );
116
158
  if (flag === "e") return ctx.shell.vfs.exists(p);
117
159
  if (flag === "z") return (arg ?? "").length === 0;
118
160
  if (flag === "n") return (arg ?? "").length > 0;
@@ -128,7 +170,8 @@ async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean
128
170
  const numMatch = expr.match(/^(\S+)\s+(-eq|-ne|-lt|-le|-gt|-ge)\s+(\S+)$/);
129
171
  if (numMatch) {
130
172
  const [, a, op, b] = numMatch;
131
- const na = Number(a), nb = Number(b);
173
+ const na = Number(a),
174
+ nb = Number(b);
132
175
  if (op === "-eq") return na === nb;
133
176
  if (op === "-ne") return na !== nb;
134
177
  if (op === "-lt") return na < nb;
@@ -138,18 +181,60 @@ async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean
138
181
  }
139
182
  }
140
183
  // fallback: run command and check exit code
141
- 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
+ );
142
194
  return (r.exitCode ?? 0) === 0;
143
195
  }
144
196
 
145
- async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandResult> {
197
+ async function runBlocks(
198
+ blocks: Block[],
199
+ ctx: CommandContext,
200
+ ): Promise<CommandResult> {
146
201
  let lastResult: CommandResult = { exitCode: 0 };
147
202
  let output = "";
148
203
 
149
204
  for (const block of blocks) {
150
205
  if (block.type === "cmd") {
151
- const expanded = await expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode, ctx);
152
- 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
+ );
153
238
  ctx.env.lastExitCode = r.exitCode ?? 0;
154
239
  if (r.stdout) output += `${r.stdout}\n`;
155
240
  if (r.stderr) return { ...r, stdout: output.trim() };
@@ -157,7 +242,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
157
242
  } else if (block.type === "if") {
158
243
  let ran = false;
159
244
  if (await evalCondition(block.cond, ctx)) {
160
- const sub = await runBlocks(parseBlocks(block.then), ctx);
245
+ const sub = await runBlocks(parseBlocks(block.then_), ctx);
161
246
  if (sub.stdout) output += `${sub.stdout}\n`;
162
247
  ran = true;
163
248
  } else {
@@ -165,7 +250,8 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
165
250
  if (await evalCondition(elif.cond, ctx)) {
166
251
  const sub = await runBlocks(parseBlocks(elif.body), ctx);
167
252
  if (sub.stdout) output += `${sub.stdout}\n`;
168
- ran = true; break;
253
+ ran = true;
254
+ break;
169
255
  }
170
256
  }
171
257
  if (!ran && block.else_.length > 0) {
@@ -174,7 +260,12 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
174
260
  }
175
261
  }
176
262
  } else if (block.type === "for") {
177
- const listExpanded = await expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode, ctx);
263
+ const listExpanded = await expandVars(
264
+ block.list,
265
+ ctx.env.vars,
266
+ ctx.env.lastExitCode,
267
+ ctx,
268
+ );
178
269
  const items = listExpanded.trim().split(/\s+/);
179
270
  for (const item of items) {
180
271
  ctx.env.vars[block.var] = item;
@@ -184,7 +275,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
184
275
  }
185
276
  } else if (block.type === "while") {
186
277
  let iterations = 0;
187
- while (iterations < 1000 && await evalCondition(block.cond, ctx)) {
278
+ while (iterations < 1000 && (await evalCondition(block.cond, ctx))) {
188
279
  const sub = await runBlocks(parseBlocks(block.body), ctx);
189
280
  if (sub.stdout) output += `${sub.stdout}\n`;
190
281
  if (sub.closeSession) return sub;
@@ -208,7 +299,10 @@ export const shCommand: ShellModule = {
208
299
  if (ifFlag(args, "-c")) {
209
300
  const script = args[args.indexOf("-c") + 1] ?? "";
210
301
  if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
211
- 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("#"));
212
306
  const blocks = parseBlocks(lines);
213
307
  return runBlocks(blocks, ctx);
214
308
  }
@@ -217,13 +311,23 @@ export const shCommand: ShellModule = {
217
311
  const fileArg = args[0];
218
312
  if (fileArg) {
219
313
  const p = resolvePath(cwd, fileArg);
220
- 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
+ };
221
319
  const content = shell.vfs.readFile(p);
222
- 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("#"));
223
324
  const blocks = parseBlocks(lines);
224
325
  return runBlocks(blocks, ctx);
225
326
  }
226
327
 
227
- 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
+ };
228
332
  },
229
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
  };
@@ -1,6 +1,6 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { resolvePath } from "./helpers";
3
- import { runCommand } from ".";
3
+ import { runCommand } from "./runtime";
4
4
 
5
5
  export const sourceCommand: ShellModule = {
6
6
  name: "source",
@@ -16,7 +16,10 @@ export const sourceCommand: ShellModule = {
16
16
 
17
17
  const filePath = resolvePath(cwd, fileArg);
18
18
  if (!shell.vfs.exists(filePath)) {
19
- return { stderr: `source: ${fileArg}: No such file or directory`, exitCode: 1 };
19
+ return {
20
+ stderr: `source: ${fileArg}: No such file or directory`,
21
+ exitCode: 1,
22
+ };
20
23
  }
21
24
 
22
25
  const content = shell.vfs.readFile(filePath);
@@ -25,7 +28,16 @@ export const sourceCommand: ShellModule = {
25
28
  for (const line of content.split("\n")) {
26
29
  const l = line.trim();
27
30
  if (!l || l.startsWith("#")) continue;
28
- const result = await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, env);
31
+ const result = await runCommand(
32
+ l,
33
+ authUser,
34
+ hostname,
35
+ "shell",
36
+ cwd,
37
+ shell,
38
+ undefined,
39
+ env,
40
+ );
29
41
  lastExitCode = result.exitCode ?? 0;
30
42
  if (result.closeSession || result.switchUser) return result;
31
43
  }
@@ -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);