typescript-virtual-container 1.2.4 → 1.2.5

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 (66) hide show
  1. package/README.md +868 -1245
  2. package/benchmark-results.txt +21 -21
  3. package/dist/SSHMimic/index.d.ts +19 -2
  4. package/dist/SSHMimic/index.d.ts.map +1 -1
  5. package/dist/SSHMimic/index.js +116 -20
  6. package/dist/VirtualFileSystem/index.d.ts +115 -88
  7. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  8. package/dist/VirtualFileSystem/index.js +406 -258
  9. package/dist/VirtualShell/index.d.ts +3 -4
  10. package/dist/VirtualShell/index.d.ts.map +1 -1
  11. package/dist/VirtualShell/index.js +4 -6
  12. package/dist/VirtualUserManager/index.d.ts +25 -0
  13. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  14. package/dist/VirtualUserManager/index.js +33 -0
  15. package/dist/commands/chmod.d.ts +3 -0
  16. package/dist/commands/chmod.d.ts.map +1 -0
  17. package/dist/commands/chmod.js +31 -0
  18. package/dist/commands/cp.d.ts +3 -0
  19. package/dist/commands/cp.d.ts.map +1 -0
  20. package/dist/commands/cp.js +68 -0
  21. package/dist/commands/find.d.ts +3 -0
  22. package/dist/commands/find.d.ts.map +1 -0
  23. package/dist/commands/find.js +48 -0
  24. package/dist/commands/grep.d.ts.map +1 -1
  25. package/dist/commands/grep.js +61 -35
  26. package/dist/commands/head.d.ts +3 -0
  27. package/dist/commands/head.d.ts.map +1 -0
  28. package/dist/commands/head.js +30 -0
  29. package/dist/commands/index.d.ts.map +1 -1
  30. package/dist/commands/index.js +25 -35
  31. package/dist/commands/ln.d.ts +3 -0
  32. package/dist/commands/ln.d.ts.map +1 -0
  33. package/dist/commands/ln.js +42 -0
  34. package/dist/commands/mv.d.ts +3 -0
  35. package/dist/commands/mv.d.ts.map +1 -0
  36. package/dist/commands/mv.js +35 -0
  37. package/dist/commands/tail.d.ts +3 -0
  38. package/dist/commands/tail.d.ts.map +1 -0
  39. package/dist/commands/tail.js +33 -0
  40. package/dist/commands/wc.d.ts +3 -0
  41. package/dist/commands/wc.d.ts.map +1 -0
  42. package/dist/commands/wc.js +48 -0
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/package.json +5 -2
  46. package/scripts/publish-package.sh +70 -0
  47. package/src/SSHMimic/index.ts +143 -28
  48. package/src/VirtualFileSystem/index.ts +500 -280
  49. package/src/VirtualShell/index.ts +4 -6
  50. package/src/VirtualUserManager/index.ts +41 -0
  51. package/src/commands/chmod.ts +33 -0
  52. package/src/commands/cp.ts +76 -0
  53. package/src/commands/find.ts +61 -0
  54. package/src/commands/grep.ts +54 -38
  55. package/src/commands/head.ts +35 -0
  56. package/src/commands/index.ts +25 -43
  57. package/src/commands/ln.ts +47 -0
  58. package/src/commands/mv.ts +43 -0
  59. package/src/commands/tail.ts +37 -0
  60. package/src/commands/wc.ts +48 -0
  61. package/src/index.ts +1 -0
  62. package/standalone.js +62 -52
  63. package/standalone.js.map +4 -4
  64. package/tests/bun-test-shim.ts +1 -0
  65. package/tests/sftp.test.ts +115 -191
  66. package/tests/users.test.ts +66 -83
@@ -1,19 +1,25 @@
1
1
  import { adduserCommand } from "./adduser";
2
2
  import { catCommand } from "./cat";
3
3
  import { cdCommand } from "./cd";
4
+ import { chmodCommand } from "./chmod";
4
5
  import { clearCommand } from "./clear";
6
+ import { cpCommand } from "./cp";
5
7
  import { curlCommand } from "./curl";
6
8
  import { deluserCommand } from "./deluser";
7
9
  import { echoCommand } from "./echo";
8
10
  import { envCommand } from "./env";
9
11
  import { exitCommand } from "./exit";
10
12
  import { exportCommand } from "./export";
13
+ import { findCommand } from "./find";
11
14
  import { grepCommand } from "./grep";
15
+ import { headCommand } from "./head";
12
16
  import { createHelpCommand } from "./help";
13
17
  import { hostnameCommand } from "./hostname";
14
18
  import { htopCommand } from "./htop";
19
+ import { lnCommand } from "./ln";
15
20
  import { lsCommand } from "./ls";
16
21
  import { mkdirCommand } from "./mkdir";
22
+ import { mvCommand } from "./mv";
17
23
  import { nanoCommand } from "./nano";
18
24
  import { neofetchCommand } from "./neofetch";
19
25
  import { passwdCommand } from "./passwd";
@@ -23,9 +29,11 @@ import { setCommand } from "./set";
23
29
  import { shCommand } from "./sh";
24
30
  import { suCommand } from "./su";
25
31
  import { sudoCommand } from "./sudo";
32
+ import { tailCommand } from "./tail";
26
33
  import { touchCommand } from "./touch";
27
34
  import { treeCommand } from "./tree";
28
35
  import { unsetCommand } from "./unset";
36
+ import { wcCommand } from "./wc";
29
37
  import { wgetCommand } from "./wget";
30
38
  import { whoCommand } from "./who";
31
39
  import { whoamiCommand } from "./whoami";
@@ -60,12 +68,21 @@ const BASE_COMMANDS = [
60
68
  shCommand,
61
69
  clearCommand,
62
70
  exitCommand,
71
+ cpCommand,
72
+ mvCommand,
73
+ lnCommand,
74
+ findCommand,
75
+ wcCommand,
76
+ headCommand,
77
+ tailCommand,
78
+ chmodCommand,
63
79
  ];
64
80
  const customCommands = [];
65
81
  const helpCommand = createHelpCommand(() => getCommandModules().map((cmd) => cmd.name));
66
82
  const commandRegistry = new Map();
67
83
  let cachedCommandNames = null;
68
84
  function buildCache() {
85
+ commandRegistry.clear();
69
86
  for (const mod of getCommandModules()) {
70
87
  commandRegistry.set(mod.name, mod);
71
88
  for (const alias of mod.aliases ?? []) {
@@ -75,13 +92,6 @@ function buildCache() {
75
92
  cachedCommandNames = Array.from(commandRegistry.keys()).sort();
76
93
  }
77
94
  function getCommandModules() {
78
- // console.log("Loading command modules...");
79
- // console.log(
80
- // `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
81
- // );
82
- // console.log(
83
- // `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
84
- // );
85
95
  return [...BASE_COMMANDS, ...customCommands, helpCommand];
86
96
  }
87
97
  export function registerCommand(module) {
@@ -94,31 +104,20 @@ export function registerCommand(module) {
94
104
  if (names.some((name) => name.length === 0 || /\s/.test(name))) {
95
105
  throw new Error("Command names and aliases must be non-empty and contain no spaces");
96
106
  }
97
- for (const name of names) {
98
- if (commandRegistry.has(name)) {
99
- throw new Error(`Command '${name}' already exists`);
100
- }
101
- commandRegistry.set(name, normalized);
102
- }
107
+ customCommands.push(normalized);
103
108
  buildCache();
104
109
  }
105
110
  export function createCustomCommand(name, params, run) {
106
- return {
107
- name,
108
- params,
109
- run,
110
- };
111
+ return { name, params, run };
111
112
  }
112
113
  export function getCommandNames() {
113
- if (!cachedCommandNames) {
114
+ if (!cachedCommandNames)
114
115
  buildCache();
115
- }
116
116
  return cachedCommandNames;
117
117
  }
118
118
  export function resolveModule(name) {
119
- if (!cachedCommandNames) {
119
+ if (!cachedCommandNames)
120
120
  buildCache();
121
- }
122
121
  return commandRegistry.get(name.toLowerCase());
123
122
  }
124
123
  function splitArgsRespectingQuotes(input) {
@@ -150,9 +149,8 @@ function splitArgsRespectingQuotes(input) {
150
149
  }
151
150
  current += ch;
152
151
  }
153
- if (current.length > 0) {
152
+ if (current.length > 0)
154
153
  tokens.push(current);
155
- }
156
154
  return tokens;
157
155
  }
158
156
  function parseInput(rawInput) {
@@ -162,21 +160,16 @@ function parseInput(rawInput) {
162
160
  args: parts.slice(1),
163
161
  };
164
162
  }
165
- // Internal async function for pipeline execution
166
163
  export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin) {
167
164
  const trimmed = rawInput.trim();
168
- if (trimmed.length === 0) {
165
+ if (trimmed.length === 0)
169
166
  return { exitCode: 0 };
170
- }
171
167
  if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
172
168
  const { parseShellPipeline } = await import("../VirtualShell/shellParser");
173
169
  const { executePipeline } = await import("../SSHMimic/executor");
174
170
  const pipeline = parseShellPipeline(trimmed);
175
171
  if (!pipeline.isValid) {
176
- return {
177
- stderr: pipeline.error || "Syntax error",
178
- exitCode: 1,
179
- };
172
+ return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
180
173
  }
181
174
  try {
182
175
  return await executePipeline(pipeline, authUser, hostname, mode, cwd, shell);
@@ -189,10 +182,7 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
189
182
  const { commandName, args } = parseInput(trimmed);
190
183
  const mod = resolveModule(commandName);
191
184
  if (!mod) {
192
- return {
193
- stderr: `Command '${trimmed}' not found`,
194
- exitCode: 127,
195
- };
185
+ return { stderr: `Command '${trimmed}' not found`, exitCode: 127 };
196
186
  }
197
187
  try {
198
188
  return await mod.run({
@@ -0,0 +1,3 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const lnCommand: ShellModule;
3
+ //# sourceMappingURL=ln.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ln.d.ts","sourceRoot":"","sources":["../../src/commands/ln.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,eAAO,MAAM,SAAS,EAAE,WA0CvB,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { ifFlag } from "./command-helpers";
2
+ import { assertPathAccess, resolvePath } from "./helpers";
3
+ export const lnCommand = {
4
+ name: "ln",
5
+ params: ["[-s] <target> <link_name>"],
6
+ run: ({ authUser, shell, cwd, args }) => {
7
+ const symbolic = ifFlag(args, ["-s", "--symbolic"]);
8
+ const positionals = args.filter((a) => !a.startsWith("-"));
9
+ const [targetArg, linkArg] = positionals;
10
+ if (!targetArg || !linkArg) {
11
+ return { stderr: "ln: missing operand", exitCode: 1 };
12
+ }
13
+ const linkPath = resolvePath(cwd, linkArg);
14
+ const targetPath = symbolic
15
+ ? targetArg // keep relative for symlinks
16
+ : resolvePath(cwd, targetArg);
17
+ try {
18
+ assertPathAccess(authUser, linkPath, "ln");
19
+ if (!symbolic) {
20
+ // Hard link — copy file contents
21
+ const srcPath = resolvePath(cwd, targetArg);
22
+ assertPathAccess(authUser, srcPath, "ln");
23
+ if (!shell.vfs.exists(srcPath)) {
24
+ return {
25
+ stderr: `ln: ${targetArg}: No such file or directory`,
26
+ exitCode: 1,
27
+ };
28
+ }
29
+ const content = shell.vfs.readFile(srcPath);
30
+ shell.writeFileAsUser(authUser, linkPath, content);
31
+ }
32
+ else {
33
+ shell.vfs.symlink(targetPath, linkPath);
34
+ }
35
+ return { exitCode: 0 };
36
+ }
37
+ catch (err) {
38
+ const msg = err instanceof Error ? err.message : String(err);
39
+ return { stderr: `ln: ${msg}`, exitCode: 1 };
40
+ }
41
+ },
42
+ };
@@ -0,0 +1,3 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const mvCommand: ShellModule;
3
+ //# sourceMappingURL=mv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mv.d.ts","sourceRoot":"","sources":["../../src/commands/mv.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,eAAO,MAAM,SAAS,EAAE,WAuCvB,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { assertPathAccess, resolvePath } from "./helpers";
2
+ export const mvCommand = {
3
+ name: "mv",
4
+ params: ["<source> <dest>"],
5
+ run: ({ authUser, shell, cwd, args }) => {
6
+ const positionals = args.filter((a) => !a.startsWith("-"));
7
+ const [srcArg, destArg] = positionals;
8
+ if (!srcArg || !destArg) {
9
+ return { stderr: "mv: missing operand", exitCode: 1 };
10
+ }
11
+ const srcPath = resolvePath(cwd, srcArg);
12
+ const destPath = resolvePath(cwd, destArg);
13
+ try {
14
+ assertPathAccess(authUser, srcPath, "mv");
15
+ assertPathAccess(authUser, destPath, "mv");
16
+ if (!shell.vfs.exists(srcPath)) {
17
+ return {
18
+ stderr: `mv: ${srcArg}: No such file or directory`,
19
+ exitCode: 1,
20
+ };
21
+ }
22
+ // If dest is a directory, move into it
23
+ const finalDest = shell.vfs.exists(destPath) &&
24
+ shell.vfs.stat(destPath).type === "directory"
25
+ ? `${destPath}/${srcArg.split("/").pop()}`
26
+ : destPath;
27
+ shell.vfs.move(srcPath, finalDest);
28
+ return { exitCode: 0 };
29
+ }
30
+ catch (err) {
31
+ const msg = err instanceof Error ? err.message : String(err);
32
+ return { stderr: `mv: ${msg}`, exitCode: 1 };
33
+ }
34
+ },
35
+ };
@@ -0,0 +1,3 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const tailCommand: ShellModule;
3
+ //# sourceMappingURL=tail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tail.d.ts","sourceRoot":"","sources":["../../src/commands/tail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,eAAO,MAAM,WAAW,EAAE,WAgCzB,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { getFlag } from "./command-helpers";
2
+ import { assertPathAccess, resolvePath } from "./helpers";
3
+ export const tailCommand = {
4
+ name: "tail",
5
+ params: ["[-n <lines>] [file...]"],
6
+ run: ({ authUser, shell, cwd, args, stdin }) => {
7
+ const nArg = getFlag(args, ["-n"]);
8
+ const n = typeof nArg === "string" ? parseInt(nArg, 10) : 10;
9
+ const positionals = args.filter((a) => !a.startsWith("-") && a !== nArg);
10
+ const take = (content) => {
11
+ const lines = content.split("\n");
12
+ return lines.slice(Math.max(0, lines.length - n)).join("\n");
13
+ };
14
+ if (positionals.length === 0) {
15
+ return { stdout: take(stdin ?? ""), exitCode: 0 };
16
+ }
17
+ const results = [];
18
+ for (const file of positionals) {
19
+ const filePath = resolvePath(cwd, file);
20
+ try {
21
+ assertPathAccess(authUser, filePath, "tail");
22
+ results.push(take(shell.vfs.readFile(filePath)));
23
+ }
24
+ catch {
25
+ return {
26
+ stderr: `tail: ${file}: No such file or directory`,
27
+ exitCode: 1,
28
+ };
29
+ }
30
+ }
31
+ return { stdout: results.join("\n"), exitCode: 0 };
32
+ },
33
+ };
@@ -0,0 +1,3 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const wcCommand: ShellModule;
3
+ //# sourceMappingURL=wc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wc.d.ts","sourceRoot":"","sources":["../../src/commands/wc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,eAAO,MAAM,SAAS,EAAE,WA2CvB,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { ifFlag } from "./command-helpers";
2
+ import { assertPathAccess, resolvePath } from "./helpers";
3
+ export const wcCommand = {
4
+ name: "wc",
5
+ params: ["[-l] [-w] [-c] [file...]"],
6
+ run: ({ authUser, shell, cwd, args, stdin }) => {
7
+ const lines = ifFlag(args, ["-l"]);
8
+ const words = ifFlag(args, ["-w"]);
9
+ const bytes = ifFlag(args, ["-c"]);
10
+ const showAll = !lines && !words && !bytes;
11
+ const positionals = args.filter((a) => !a.startsWith("-"));
12
+ const count = (content, label) => {
13
+ const l = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
14
+ const w = content.trim().split(/\s+/).filter(Boolean).length;
15
+ const c = Buffer.byteLength(content, "utf8");
16
+ const parts = [];
17
+ if (showAll || lines)
18
+ parts.push(String(l).padStart(7));
19
+ if (showAll || words)
20
+ parts.push(String(w).padStart(7));
21
+ if (showAll || bytes)
22
+ parts.push(String(c).padStart(7));
23
+ if (label)
24
+ parts.push(` ${label}`);
25
+ return parts.join("");
26
+ };
27
+ if (positionals.length === 0) {
28
+ const content = stdin ?? "";
29
+ return { stdout: count(content, ""), exitCode: 0 };
30
+ }
31
+ const results = [];
32
+ for (const file of positionals) {
33
+ const filePath = resolvePath(cwd, file);
34
+ try {
35
+ assertPathAccess(authUser, filePath, "wc");
36
+ const content = shell.vfs.readFile(filePath);
37
+ results.push(count(content, file));
38
+ }
39
+ catch {
40
+ return {
41
+ stderr: `wc: ${file}: No such file or directory`,
42
+ exitCode: 1,
43
+ };
44
+ }
45
+ }
46
+ return { stdout: results.join("\n"), exitCode: 0 };
47
+ },
48
+ };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export type { AuditLogEntry, HoneyPotStats, } from "./Honeypot/index";
8
8
  export type { CommandContext, CommandMode, CommandOutcome, CommandResult, NanoEditorSession, ShellModule, SudoChallenge, } from "./types/commands";
9
9
  export type { ExecStream, ShellStream } from "./types/streams";
10
10
  export type { RemoveOptions, VfsBaseNode, VfsDirectoryNode, VfsFileNode, VfsNodeStats, VfsNodeType, VfsSnapshot, VfsSnapshotBaseNode, VfsSnapshotDirectoryNode, VfsSnapshotFileNode, VfsSnapshotNode, WriteFileOptions, } from "./types/vfs";
11
+ export type { VfsOptions, VfsPersistenceMode } from "./VirtualFileSystem/index";
11
12
  export { HoneyPot, SshClient, VirtualFileSystem, SftpMimic as VirtualSftpServer, VirtualShell, SshMimic as VirtualSshServer, VirtualUserManager, };
12
13
  export { getArg, getFlag, ifFlag, } from "./commands/command-helpers";
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,iBAAiB,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,YAAY,EACX,aAAa,EACb,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACX,cAAc,EACd,WAAW,EACX,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/D,YAAY,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,SAAS,IAAI,iBAAiB,EAC9B,YAAY,EACZ,QAAQ,IAAI,gBAAgB,EAC5B,kBAAkB,GAClB,CAAC;AAEF,OAAO,EACN,MAAM,EACN,OAAO,EACP,MAAM,GACN,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,iBAAiB,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,YAAY,EACX,aAAa,EACb,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACX,cAAc,EACd,WAAW,EACX,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/D,YAAY,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEhF,OAAO,EACN,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,SAAS,IAAI,iBAAiB,EAC9B,YAAY,EACZ,QAAQ,IAAI,gBAAgB,EAC5B,kBAAkB,GAClB,CAAC;AAEF,OAAO,EACN,MAAM,EACN,OAAO,EACP,MAAM,GACN,MAAM,4BAA4B,CAAC"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "1.2.4",
7
+ "version": "1.2.5",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",
@@ -27,13 +27,16 @@
27
27
  "test": "bunx --bun @biomejs/biome test ./src",
28
28
  "build": "tsc --project tsconfig.json",
29
29
  "deploy:npm": "npm publish --access public",
30
+ "bench": "rm -rf .benchmark-shells/ && bun benchmark-virtualshell.ts",
31
+ "publish-package": "bash ./scripts/publish-package.sh",
30
32
  "standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=standalone.js --tree-shaking=true --minify --sourcemap"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@biomejs/biome": "^2.4.11",
34
36
  "@types/bun": "latest",
35
37
  "@types/node": "^25.6.0",
36
- "@types/ssh2": "^1.15.5"
38
+ "@types/ssh2": "^1.15.5",
39
+ "typescript": "^6.0.3"
37
40
  },
38
41
  "peerDependencies": {
39
42
  "typescript": "^5"
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
5
+ echo "Error: this command must run inside a git repository."
6
+ exit 1
7
+ fi
8
+
9
+ if [[ -n "$(git status --porcelain)" ]]; then
10
+ echo "Error: working tree is not clean. Commit or stash your changes first."
11
+ exit 1
12
+ fi
13
+
14
+ VERSION="$(node -p "require('./package.json').version")"
15
+ TAG="v${VERSION}"
16
+
17
+ if [[ -z "${VERSION}" ]]; then
18
+ echo "Error: package.json version is empty."
19
+ exit 1
20
+ fi
21
+
22
+ git fetch --tags --quiet
23
+
24
+ TAG_EXISTS_LOCAL="false"
25
+ TAG_EXISTS_REMOTE="false"
26
+
27
+ if git rev-parse --verify --quiet "refs/tags/${TAG}" >/dev/null; then
28
+ TAG_EXISTS_LOCAL="true"
29
+ fi
30
+
31
+ if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
32
+ TAG_EXISTS_REMOTE="true"
33
+ fi
34
+
35
+ if [[ "${TAG_EXISTS_LOCAL}" == "true" || "${TAG_EXISTS_REMOTE}" == "true" ]]; then
36
+ echo "Tag ${TAG} already exists. Deleting existing release/tag before republishing."
37
+
38
+ if command -v gh >/dev/null 2>&1; then
39
+ if gh release view "${TAG}" >/dev/null 2>&1; then
40
+ gh release delete "${TAG}" --yes
41
+ echo "GitHub release ${TAG} deleted."
42
+ fi
43
+ else
44
+ echo "gh CLI not found; skipping GitHub release deletion."
45
+ fi
46
+
47
+ if [[ "${TAG_EXISTS_LOCAL}" == "true" ]]; then
48
+ git tag -d "${TAG}" >/dev/null
49
+ echo "Local tag ${TAG} deleted."
50
+ fi
51
+
52
+ if [[ "${TAG_EXISTS_REMOTE}" == "true" ]]; then
53
+ git push --delete origin "${TAG}" >/dev/null
54
+ echo "Remote tag ${TAG} deleted."
55
+ fi
56
+ fi
57
+
58
+ git tag -a "${TAG}" -m "Release ${TAG}"
59
+ git push origin "${TAG}"
60
+
61
+ echo "Tag ${TAG} pushed."
62
+
63
+ if command -v gh >/dev/null 2>&1; then
64
+ gh release create "${TAG}" --title "${TAG}" --generate-notes
65
+ echo "GitHub release ${TAG} created."
66
+ else
67
+ echo "gh CLI not found; skipping GitHub release creation."
68
+ fi
69
+
70
+ echo "Done. The publish workflow should run from tag ${TAG}."