sandlot 0.1.1 → 0.1.3

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 (59) hide show
  1. package/README.md +145 -518
  2. package/dist/build-emitter.d.ts +47 -0
  3. package/dist/build-emitter.d.ts.map +1 -0
  4. package/dist/builder.d.ts +370 -0
  5. package/dist/builder.d.ts.map +1 -0
  6. package/dist/bundler.d.ts +3 -3
  7. package/dist/bundler.d.ts.map +1 -1
  8. package/dist/commands/compile.d.ts +13 -0
  9. package/dist/commands/compile.d.ts.map +1 -0
  10. package/dist/commands/index.d.ts +17 -0
  11. package/dist/commands/index.d.ts.map +1 -0
  12. package/dist/commands/packages.d.ts +17 -0
  13. package/dist/commands/packages.d.ts.map +1 -0
  14. package/dist/commands/run.d.ts +40 -0
  15. package/dist/commands/run.d.ts.map +1 -0
  16. package/dist/commands/types.d.ts +141 -0
  17. package/dist/commands/types.d.ts.map +1 -0
  18. package/dist/fs.d.ts +60 -42
  19. package/dist/fs.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +304 -491
  23. package/dist/internal.d.ts +5 -0
  24. package/dist/internal.d.ts.map +1 -1
  25. package/dist/internal.js +174 -95
  26. package/dist/runner.d.ts +314 -0
  27. package/dist/runner.d.ts.map +1 -0
  28. package/dist/sandbox-manager.d.ts +45 -33
  29. package/dist/sandbox-manager.d.ts.map +1 -1
  30. package/dist/sandbox.d.ts +144 -70
  31. package/dist/sandbox.d.ts.map +1 -1
  32. package/dist/shared-modules.d.ts +22 -3
  33. package/dist/shared-modules.d.ts.map +1 -1
  34. package/dist/shared-resources.d.ts +0 -3
  35. package/dist/shared-resources.d.ts.map +1 -1
  36. package/dist/typechecker.d.ts +1 -1
  37. package/package.json +3 -17
  38. package/src/build-emitter.ts +64 -0
  39. package/src/builder.ts +498 -0
  40. package/src/bundler.ts +86 -57
  41. package/src/commands/compile.ts +236 -0
  42. package/src/commands/index.ts +51 -0
  43. package/src/commands/packages.ts +154 -0
  44. package/src/commands/run.ts +245 -0
  45. package/src/commands/types.ts +172 -0
  46. package/src/fs.ts +90 -216
  47. package/src/index.ts +34 -12
  48. package/src/internal.ts +5 -2
  49. package/src/sandbox.ts +214 -220
  50. package/src/shared-modules.ts +74 -4
  51. package/src/shared-resources.ts +0 -3
  52. package/src/ts-libs.ts +1 -1
  53. package/src/typechecker.ts +1 -1
  54. package/dist/react.d.ts +0 -159
  55. package/dist/react.d.ts.map +0 -1
  56. package/dist/react.js +0 -149
  57. package/src/commands.ts +0 -733
  58. package/src/react.tsx +0 -331
  59. package/src/sandbox-manager.ts +0 -490
package/src/bundler.ts CHANGED
@@ -1,7 +1,32 @@
1
1
  import type { IFileSystem } from "just-bash/browser";
2
- import * as esbuild from "esbuild-wasm";
2
+ import type * as EsbuildTypes from "esbuild-wasm";
3
3
  import { getPackageManifest, resolveToEsmUrl } from "./packages";
4
- import { getSharedModuleRuntimeCode } from "./shared-modules";
4
+ import { getSharedModuleRuntimeCode, getSharedModuleExports } from "./shared-modules";
5
+
6
+ // Lazily loaded esbuild module - loaded from CDN to avoid bundler issues
7
+ let esbuild: typeof EsbuildTypes | null = null;
8
+
9
+ async function getEsbuild(): Promise<typeof EsbuildTypes> {
10
+ if (esbuild) return esbuild;
11
+
12
+ // Load esbuild-wasm from esm.sh CDN to avoid bundler transformation issues
13
+ // esm.sh provides proper ESM wrappers for npm packages
14
+ const cdnUrl = `https://esm.sh/esbuild-wasm@${ESBUILD_VERSION}`;
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ const mod: any = await import(/* @vite-ignore */ cdnUrl);
18
+
19
+ // esm.sh typically provides both default and named exports
20
+ esbuild = mod.default ?? mod;
21
+
22
+ // Verify we have the initialize function
23
+ if (typeof esbuild?.initialize !== 'function') {
24
+ console.error('esbuild-wasm module structure:', mod);
25
+ throw new Error('Failed to load esbuild-wasm: initialize function not found');
26
+ }
27
+
28
+ return esbuild;
29
+ }
5
30
 
6
31
  /**
7
32
  * How to handle npm package imports (bare imports like "react").
@@ -96,7 +121,7 @@ export interface BundleResult {
96
121
  /**
97
122
  * Any warnings from esbuild
98
123
  */
99
- warnings: esbuild.Message[];
124
+ warnings: EsbuildTypes.Message[];
100
125
 
101
126
  /**
102
127
  * List of files that were included in the bundle
@@ -104,6 +129,11 @@ export interface BundleResult {
104
129
  includedFiles: string[];
105
130
  }
106
131
 
132
+ /**
133
+ * esbuild-wasm version - MUST match the version in package.json dependencies
134
+ */
135
+ const ESBUILD_VERSION = "0.27.2";
136
+
107
137
  // Track initialization state
108
138
  let initialized = false;
109
139
  let initPromise: Promise<void> | null = null;
@@ -112,9 +142,27 @@ let initPromise: Promise<void> | null = null;
112
142
  * Get the esbuild-wasm binary URL based on the installed version
113
143
  */
114
144
  function getWasmUrl(): string {
115
- // Use unpkg CDN to fetch the WASM binary matching our installed version
116
- const version = "0.27.2"; // Match installed version
117
- return `https://unpkg.com/esbuild-wasm@${version}/esbuild.wasm`;
145
+ return `https://unpkg.com/esbuild-wasm@${ESBUILD_VERSION}/esbuild.wasm`;
146
+ }
147
+
148
+ /**
149
+ * Check if the browser environment supports cross-origin isolation.
150
+ * This is needed for SharedArrayBuffer which esbuild-wasm may use.
151
+ */
152
+ function checkCrossOriginIsolation(): void {
153
+ if (typeof window === "undefined") return; // Not in browser
154
+
155
+ // crossOriginIsolated is true when COOP/COEP headers are set correctly
156
+ if (!window.crossOriginIsolated) {
157
+ console.warn(
158
+ "[sandlot] Cross-origin isolation is not enabled. " +
159
+ "esbuild-wasm may have reduced performance or fail on some browsers.\n" +
160
+ "To enable, add these headers to your dev server:\n" +
161
+ " Cross-Origin-Embedder-Policy: require-corp\n" +
162
+ " Cross-Origin-Opener-Policy: same-origin\n" +
163
+ "In Vite, add a plugin to configureServer. See sandlot README for details."
164
+ );
165
+ }
118
166
  }
119
167
 
120
168
  /**
@@ -129,9 +177,14 @@ export async function initBundler(): Promise<void> {
129
177
  return;
130
178
  }
131
179
 
132
- initPromise = esbuild.initialize({
133
- wasmURL: getWasmUrl(),
134
- });
180
+ checkCrossOriginIsolation();
181
+
182
+ initPromise = (async () => {
183
+ const es = await getEsbuild();
184
+ await es.initialize({
185
+ wasmURL: getWasmUrl(),
186
+ });
187
+ })();
135
188
 
136
189
  await initPromise;
137
190
  initialized = true;
@@ -147,7 +200,7 @@ function isBareImport(path: string): boolean {
147
200
  /**
148
201
  * Get the appropriate loader based on file extension
149
202
  */
150
- function getLoader(path: string): esbuild.Loader {
203
+ function getLoader(path: string): EsbuildTypes.Loader {
151
204
  const ext = path.split(".").pop()?.toLowerCase();
152
205
  switch (ext) {
153
206
  case "ts":
@@ -207,7 +260,7 @@ function matchesSharedModule(importPath: string, sharedModuleIds: Set<string>):
207
260
  /**
208
261
  * Create an esbuild plugin that reads from a virtual filesystem
209
262
  */
210
- function createVfsPlugin(options: VfsPluginOptions): esbuild.Plugin {
263
+ function createVfsPlugin(options: VfsPluginOptions): EsbuildTypes.Plugin {
211
264
  const {
212
265
  fs,
213
266
  entryPoint,
@@ -303,27 +356,14 @@ function createVfsPlugin(options: VfsPluginOptions): esbuild.Plugin {
303
356
 
304
357
  // Load shared modules from the registry
305
358
  build.onLoad({ filter: /.*/, namespace: "sandlot-shared" }, (args) => {
306
- // Generate code that looks up the module from the global registry
307
- const contents = `export default ${getSharedModuleRuntimeCode(args.path)};
308
- export * from ${JSON.stringify(args.path)};
309
- // Re-export all named exports by importing from registry
310
- const __mod__ = ${getSharedModuleRuntimeCode(args.path)};
311
- for (const __k__ in __mod__) {
312
- if (__k__ !== 'default') Object.defineProperty(exports, __k__, {
313
- enumerable: true,
314
- get: function() { return __mod__[__k__]; }
315
- });
316
- }`;
317
-
318
- // For ESM format, we need a different approach
319
- // Generate a simple module that re-exports from registry
320
- const esmContents = `
359
+ // Generate ESM code that re-exports from the shared module registry
360
+ const contents = `
321
361
  const __sandlot_mod__ = ${getSharedModuleRuntimeCode(args.path)};
322
362
  export default __sandlot_mod__.default ?? __sandlot_mod__;
323
363
  ${generateNamedExports(args.path)}
324
364
  `;
325
365
  return {
326
- contents: esmContents.trim(),
366
+ contents: contents.trim(),
327
367
  loader: 'js'
328
368
  };
329
369
  });
@@ -349,39 +389,26 @@ ${generateNamedExports(args.path)}
349
389
  }
350
390
 
351
391
  /**
352
- * Generate named export statements for common React exports.
353
- * This is a pragmatic approach - for full generality, we'd need to
354
- * introspect the actual module, but that's not possible at build time.
392
+ * Generate named export statements for shared modules.
393
+ *
394
+ * Uses dynamically discovered exports from the SharedModuleRegistry,
395
+ * which are populated when registerSharedModules() is called.
396
+ *
397
+ * If the module wasn't registered (or has no enumerable exports),
398
+ * returns a comment - named imports won't work but default import will.
355
399
  */
356
400
  function generateNamedExports(moduleId: string): string {
357
- // Common exports for well-known modules
358
- const knownExports: Record<string, string[]> = {
359
- 'react': [
360
- 'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
361
- 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect',
362
- 'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
363
- 'useSyncExternalStore', 'useInsertionEffect', 'useOptimistic', 'useActionState',
364
- 'createElement', 'cloneElement', 'createContext', 'forwardRef', 'lazy', 'memo',
365
- 'startTransition', 'Children', 'Component', 'PureComponent', 'Fragment',
366
- 'Profiler', 'StrictMode', 'Suspense', 'version', 'isValidElement',
367
- ],
368
- 'react-dom': ['createPortal', 'flushSync', 'version'],
369
- 'react-dom/client': ['createRoot', 'hydrateRoot'],
370
- 'react-dom/server': ['renderToString', 'renderToStaticMarkup', 'renderToPipeableStream'],
371
- };
401
+ const exports = getSharedModuleExports(moduleId);
372
402
 
373
- const exports = knownExports[moduleId];
374
- if (!exports) {
375
- // For unknown modules, generate a proxy that re-exports everything
376
- return `
377
- // Dynamic re-export for unknown module
378
- export const __moduleProxy__ = __sandlot_mod__;
379
- `;
403
+ if (exports.length > 0) {
404
+ return exports
405
+ .map(name => `export const ${name} = __sandlot_mod__.${name};`)
406
+ .join('\n');
380
407
  }
381
408
 
382
- return exports
383
- .map(name => `export const ${name} = __sandlot_mod__.${name};`)
384
- .join('\n');
409
+ // Module not registered or has no enumerable exports
410
+ // Default import will still work: import foo from 'module'
411
+ return `// No exports discovered for "${moduleId}" - use default import or call registerSharedModules() first`;
385
412
  }
386
413
 
387
414
  /**
@@ -389,7 +416,7 @@ export const __moduleProxy__ = __sandlot_mod__;
389
416
  *
390
417
  * @example
391
418
  * ```ts
392
- * const fs = IndexedDbFs.createInMemory({
419
+ * const fs = Filesystem.create({
393
420
  * initialFiles: {
394
421
  * "/src/index.ts": "export const hello = 'world';",
395
422
  * "/src/utils.ts": "export function add(a: number, b: number) { return a + b; }",
@@ -445,7 +472,8 @@ export async function bundle(options: BundleOptions): Promise<BundleResult> {
445
472
  sharedModuleIds,
446
473
  });
447
474
 
448
- const result = await esbuild.build({
475
+ const es = await getEsbuild();
476
+ const result = await es.build({
449
477
  entryPoints: [normalizedEntry],
450
478
  bundle: true,
451
479
  write: false,
@@ -456,6 +484,7 @@ export async function bundle(options: BundleOptions): Promise<BundleResult> {
456
484
  target,
457
485
  external,
458
486
  plugins: [plugin],
487
+ jsx: "automatic",
459
488
  });
460
489
 
461
490
  const code = result.outputFiles?.[0]?.text ?? "";
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Compile-related commands: tsc and build
3
+ */
4
+
5
+ import { defineCommand, type CommandContext } from "just-bash/browser";
6
+ import { typecheck, formatDiagnosticsForAgent, type TypecheckResult } from "../typechecker";
7
+ import { bundle } from "../bundler";
8
+ import { loadModule } from "../loader";
9
+ import { type CommandDeps, formatEsbuildMessages } from "./types";
10
+
11
+ /**
12
+ * Create the `tsc` command for type checking
13
+ */
14
+ export function createTscCommand(deps: CommandDeps) {
15
+ const { fs, libFiles, tsconfigPath } = deps;
16
+
17
+ return defineCommand("tsc", async (args, _ctx: CommandContext) => {
18
+ const entryPoint = args[0];
19
+ if (!entryPoint) {
20
+ return {
21
+ stdout: "",
22
+ stderr: `Usage: tsc <entry-point>\n\nExample: tsc /src/index.ts\n`,
23
+ exitCode: 1,
24
+ };
25
+ }
26
+
27
+ try {
28
+ // Check if entry point exists
29
+ if (!(await fs.exists(entryPoint))) {
30
+ return {
31
+ stdout: "",
32
+ stderr: `Error: Entry point not found: ${entryPoint}\n`,
33
+ exitCode: 1,
34
+ };
35
+ }
36
+
37
+ const result = await typecheck({
38
+ fs,
39
+ entryPoint,
40
+ tsconfigPath,
41
+ libFiles,
42
+ });
43
+
44
+ if (result.hasErrors) {
45
+ const formatted = formatDiagnosticsForAgent(result.diagnostics);
46
+ return {
47
+ stdout: "",
48
+ stderr: formatted + "\n",
49
+ exitCode: 1,
50
+ };
51
+ }
52
+
53
+ const checkedCount = result.checkedFiles.length;
54
+ const warningCount = result.diagnostics.filter((d) => d.category === "warning").length;
55
+
56
+ let output = `Type check passed. Checked ${checkedCount} file(s).\n`;
57
+ if (warningCount > 0) {
58
+ output += `\nWarnings:\n${formatDiagnosticsForAgent(result.diagnostics.filter((d) => d.category === "warning"))}\n`;
59
+ }
60
+
61
+ return {
62
+ stdout: output,
63
+ stderr: "",
64
+ exitCode: 0,
65
+ };
66
+ } catch (err) {
67
+ return {
68
+ stdout: "",
69
+ stderr: `Type check failed: ${err instanceof Error ? err.message : String(err)}\n`,
70
+ exitCode: 1,
71
+ };
72
+ }
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Create the `build` command for bundling (with automatic type checking)
78
+ */
79
+ export function createBuildCommand(deps: CommandDeps) {
80
+ const { fs, libFiles, tsconfigPath, onBuild, getValidation, sharedModules } = deps;
81
+
82
+ return defineCommand("build", async (args, _ctx: CommandContext) => {
83
+ // Parse arguments
84
+ let entryPoint: string | null = null;
85
+ let skipTypecheck = false;
86
+ let minify = false;
87
+ let format: "esm" | "iife" | "cjs" = "esm";
88
+
89
+ for (let i = 0; i < args.length; i++) {
90
+ const arg = args[i];
91
+ if (arg === "--skip-typecheck" || arg === "-s") {
92
+ skipTypecheck = true;
93
+ } else if (arg === "--minify" || arg === "-m") {
94
+ minify = true;
95
+ } else if ((arg === "--format" || arg === "-f") && args[i + 1]) {
96
+ const f = args[++i]!.toLowerCase();
97
+ if (f === "esm" || f === "iife" || f === "cjs") {
98
+ format = f;
99
+ }
100
+ } else if (!arg!.startsWith("-")) {
101
+ entryPoint = arg!;
102
+ }
103
+ }
104
+
105
+ // Entry point is required
106
+ if (!entryPoint) {
107
+ return {
108
+ stdout: "",
109
+ stderr: `Usage: build <entry-point> [options]\n\nOptions:\n --skip-typecheck, -s Skip type checking\n --minify, -m Minify output\n --format, -f <fmt> Output format (esm|iife|cjs)\n\nExample: build /src/index.ts\n`,
110
+ exitCode: 1,
111
+ };
112
+ }
113
+
114
+ try {
115
+ // Check if entry point exists
116
+ if (!(await fs.exists(entryPoint))) {
117
+ return {
118
+ stdout: "",
119
+ stderr: `Error: Entry point not found: ${entryPoint}\n`,
120
+ exitCode: 1,
121
+ };
122
+ }
123
+
124
+ // Step 1: Type check (unless skipped)
125
+ let typecheckResult: TypecheckResult | null = null;
126
+ if (!skipTypecheck) {
127
+ typecheckResult = await typecheck({
128
+ fs,
129
+ entryPoint,
130
+ tsconfigPath,
131
+ libFiles,
132
+ });
133
+
134
+ if (typecheckResult.hasErrors) {
135
+ const formatted = formatDiagnosticsForAgent(typecheckResult.diagnostics);
136
+ return {
137
+ stdout: "",
138
+ stderr: `Build failed: Type errors found.\n\n${formatted}\n`,
139
+ exitCode: 1,
140
+ };
141
+ }
142
+ }
143
+
144
+ // Step 2: Bundle
145
+ const bundleResult = await bundle({
146
+ fs,
147
+ entryPoint,
148
+ format,
149
+ minify,
150
+ sharedModules,
151
+ });
152
+
153
+ // Step 3: Load module
154
+ let loadedModule: Record<string, unknown>;
155
+ try {
156
+ loadedModule = await loadModule<Record<string, unknown>>(bundleResult);
157
+ } catch (err) {
158
+ const errorMessage = err instanceof Error ? err.message : String(err);
159
+ return {
160
+ stdout: "",
161
+ stderr: `Build failed: Module failed to load.\n\n${errorMessage}\n`,
162
+ exitCode: 1,
163
+ };
164
+ }
165
+
166
+ // Step 4: Validate (if validation function is set)
167
+ const validateFn = getValidation?.();
168
+ let validatedModule = loadedModule;
169
+
170
+ if (validateFn) {
171
+ try {
172
+ validatedModule = validateFn(loadedModule);
173
+ } catch (err) {
174
+ const errorMessage = err instanceof Error ? err.message : String(err);
175
+ return {
176
+ stdout: "",
177
+ stderr: `Build failed: Validation error.\n\n${errorMessage}\n`,
178
+ exitCode: 1,
179
+ };
180
+ }
181
+ }
182
+
183
+ // Invoke callback with build output (bundle + validated module)
184
+ if (onBuild) {
185
+ await onBuild({ bundle: bundleResult, module: validatedModule });
186
+ }
187
+
188
+ // Build success message
189
+ let output = `Build successful!\n`;
190
+ output += `Entry: ${entryPoint}\n`;
191
+ output += `Format: ${format}\n`;
192
+ output += `Size: ${(bundleResult.code.length / 1024).toFixed(2)} KB\n`;
193
+
194
+ if (typecheckResult) {
195
+ output += `Type checked: ${typecheckResult.checkedFiles.length} file(s)\n`;
196
+ }
197
+
198
+ output += `Bundled: ${bundleResult.includedFiles.length} file(s)\n`;
199
+
200
+ // Show exports for visibility
201
+ const exportNames = Object.keys(loadedModule).filter((k) => !k.startsWith("__"));
202
+ if (exportNames.length > 0) {
203
+ output += `Exports: ${exportNames.join(", ")}\n`;
204
+ }
205
+
206
+ if (validateFn) {
207
+ output += `Validation: passed\n`;
208
+ }
209
+
210
+ // Include warnings if any
211
+ if (bundleResult.warnings.length > 0) {
212
+ output += `\nBuild warnings:\n${formatEsbuildMessages(bundleResult.warnings)}\n`;
213
+ }
214
+
215
+ if (typecheckResult) {
216
+ const warnings = typecheckResult.diagnostics.filter((d) => d.category === "warning");
217
+ if (warnings.length > 0) {
218
+ output += `\nType warnings:\n${formatDiagnosticsForAgent(warnings)}\n`;
219
+ }
220
+ }
221
+
222
+ return {
223
+ stdout: output,
224
+ stderr: "",
225
+ exitCode: 0,
226
+ };
227
+ } catch (err) {
228
+ const errorMessage = err instanceof Error ? err.message : String(err);
229
+ return {
230
+ stdout: "",
231
+ stderr: `Build failed: ${errorMessage}\n`,
232
+ exitCode: 1,
233
+ };
234
+ }
235
+ });
236
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Command factories for sandbox bash environments.
3
+ *
4
+ * Pure factories that create commands for type checking, bundling,
5
+ * package management, and code execution.
6
+ * No global state - all dependencies are passed in explicitly.
7
+ */
8
+
9
+ // Types and utilities
10
+ export {
11
+ type CommandDeps,
12
+ type BuildOutput,
13
+ type ValidateFn,
14
+ type RunContext,
15
+ type RunOptions,
16
+ type RunResult,
17
+ formatEsbuildMessages,
18
+ } from "./types";
19
+
20
+ // Compile commands (tsc, build)
21
+ export { createTscCommand, createBuildCommand } from "./compile";
22
+
23
+ // Package management commands (install, uninstall, list)
24
+ export {
25
+ createInstallCommand,
26
+ createUninstallCommand,
27
+ createListCommand,
28
+ } from "./packages";
29
+
30
+ // Run command
31
+ export { createRunCommand } from "./run";
32
+
33
+ // Re-import for createDefaultCommands
34
+ import type { CommandDeps } from "./types";
35
+ import { createTscCommand, createBuildCommand } from "./compile";
36
+ import { createInstallCommand, createUninstallCommand, createListCommand } from "./packages";
37
+ import { createRunCommand } from "./run";
38
+
39
+ /**
40
+ * Create all default sandbox commands
41
+ */
42
+ export function createDefaultCommands(deps: CommandDeps) {
43
+ return [
44
+ createTscCommand(deps),
45
+ createBuildCommand(deps),
46
+ createRunCommand(deps),
47
+ createInstallCommand(deps),
48
+ createUninstallCommand(deps),
49
+ createListCommand(deps),
50
+ ];
51
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Package management commands: install, uninstall, list
3
+ */
4
+
5
+ import { defineCommand, type CommandContext } from "just-bash/browser";
6
+ import { installPackage, uninstallPackage, listPackages } from "../packages";
7
+ import type { CommandDeps } from "./types";
8
+
9
+ /**
10
+ * Create the `install` command for adding packages from npm
11
+ */
12
+ export function createInstallCommand(deps: CommandDeps) {
13
+ const { fs, typesCache } = deps;
14
+
15
+ return defineCommand("install", async (args, _ctx: CommandContext) => {
16
+ if (args.length === 0) {
17
+ return {
18
+ stdout: "",
19
+ stderr: "Usage: install <package>[@version] [...packages]\n\nExamples:\n install react\n install lodash@4.17.21\n install @tanstack/react-query@5\n",
20
+ exitCode: 1,
21
+ };
22
+ }
23
+
24
+ const results: string[] = [];
25
+ let hasError = false;
26
+
27
+ for (const packageSpec of args) {
28
+ try {
29
+ const result = await installPackage(fs, packageSpec!, { cache: typesCache });
30
+
31
+ let status = `+ ${result.name}@${result.version}`;
32
+ if (result.typesInstalled) {
33
+ status += ` (${result.typeFilesCount} type file${result.typeFilesCount !== 1 ? "s" : ""})`;
34
+ if (result.fromCache) {
35
+ status += " [cached]";
36
+ }
37
+ } else if (result.typesError) {
38
+ status += ` (no types: ${result.typesError})`;
39
+ }
40
+ results.push(status);
41
+ } catch (err) {
42
+ hasError = true;
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ results.push(`x ${packageSpec}: ${message}`);
45
+ }
46
+ }
47
+
48
+ const output = results.join("\n") + "\n";
49
+
50
+ if (hasError) {
51
+ return {
52
+ stdout: "",
53
+ stderr: output,
54
+ exitCode: 1,
55
+ };
56
+ }
57
+
58
+ return {
59
+ stdout: output,
60
+ stderr: "",
61
+ exitCode: 0,
62
+ };
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Create the `uninstall` command for removing packages
68
+ */
69
+ export function createUninstallCommand(deps: CommandDeps) {
70
+ const { fs } = deps;
71
+
72
+ return defineCommand("uninstall", async (args, _ctx: CommandContext) => {
73
+ if (args.length === 0) {
74
+ return {
75
+ stdout: "",
76
+ stderr: "Usage: uninstall <package> [...packages]\n",
77
+ exitCode: 1,
78
+ };
79
+ }
80
+
81
+ const results: string[] = [];
82
+ let hasError = false;
83
+
84
+ for (const packageName of args) {
85
+ try {
86
+ const removed = await uninstallPackage(fs, packageName!);
87
+ if (removed) {
88
+ results.push(`- ${packageName}`);
89
+ } else {
90
+ results.push(`x ${packageName}: not installed`);
91
+ hasError = true;
92
+ }
93
+ } catch (err) {
94
+ hasError = true;
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ results.push(`x ${packageName}: ${message}`);
97
+ }
98
+ }
99
+
100
+ const output = results.join("\n") + "\n";
101
+
102
+ if (hasError) {
103
+ return {
104
+ stdout: "",
105
+ stderr: output,
106
+ exitCode: 1,
107
+ };
108
+ }
109
+
110
+ return {
111
+ stdout: output,
112
+ stderr: "",
113
+ exitCode: 0,
114
+ };
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Create the `list` command (alias: `ls`) for showing installed packages
120
+ */
121
+ export function createListCommand(deps: CommandDeps) {
122
+ const { fs } = deps;
123
+
124
+ return defineCommand("list", async (_args, _ctx: CommandContext) => {
125
+ try {
126
+ const packages = await listPackages(fs);
127
+
128
+ if (packages.length === 0) {
129
+ return {
130
+ stdout: "No packages installed.\n",
131
+ stderr: "",
132
+ exitCode: 0,
133
+ };
134
+ }
135
+
136
+ const output = packages
137
+ .map((pkg) => `${pkg.name}@${pkg.version}`)
138
+ .join("\n") + "\n";
139
+
140
+ return {
141
+ stdout: output,
142
+ stderr: "",
143
+ exitCode: 0,
144
+ };
145
+ } catch (err) {
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ return {
148
+ stdout: "",
149
+ stderr: `Failed to list packages: ${message}\n`,
150
+ exitCode: 1,
151
+ };
152
+ }
153
+ });
154
+ }