assistant-ui 0.0.111 → 0.0.113

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 (90) hide show
  1. package/dist/codemods/v0-12/assistant-api-to-aui.d.ts.map +1 -1
  2. package/dist/codemods/v0-12/assistant-api-to-aui.js +87 -62
  3. package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
  4. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  5. package/dist/codemods/v0-12/primitive-if-to-aui-if.js +31 -45
  6. package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
  7. package/dist/commands/add.d.ts +1 -1
  8. package/dist/commands/add.d.ts.map +1 -1
  9. package/dist/commands/add.js +4 -2
  10. package/dist/commands/add.js.map +1 -1
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/create.js +33 -10
  13. package/dist/commands/create.js.map +1 -1
  14. package/dist/commands/info.d.ts.map +1 -1
  15. package/dist/commands/info.js +2 -1
  16. package/dist/commands/info.js.map +1 -1
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +4 -2
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/mcp.d.ts.map +1 -1
  21. package/dist/commands/mcp.js +25 -28
  22. package/dist/commands/mcp.js.map +1 -1
  23. package/dist/commands/update.js +6 -7
  24. package/dist/commands/update.js.map +1 -1
  25. package/dist/commands/upgrade.d.ts.map +1 -1
  26. package/dist/commands/upgrade.js +5 -2
  27. package/dist/commands/upgrade.js.map +1 -1
  28. package/dist/index.js +2 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/agent-skill.d.ts +1 -1
  31. package/dist/lib/agent-skill.d.ts.map +1 -1
  32. package/dist/lib/agent-skill.js.map +1 -1
  33. package/dist/lib/create-project.d.ts +2 -3
  34. package/dist/lib/create-project.d.ts.map +1 -1
  35. package/dist/lib/create-project.js +17 -42
  36. package/dist/lib/create-project.js.map +1 -1
  37. package/dist/lib/handle-cli-error.d.ts +5 -0
  38. package/dist/lib/handle-cli-error.d.ts.map +1 -0
  39. package/dist/lib/handle-cli-error.js +17 -0
  40. package/dist/lib/handle-cli-error.js.map +1 -0
  41. package/dist/lib/install-ai-sdk-lib.js +8 -1
  42. package/dist/lib/install-ai-sdk-lib.js.map +1 -1
  43. package/dist/lib/install-edge-lib.js +4 -8
  44. package/dist/lib/install-edge-lib.js.map +1 -1
  45. package/dist/lib/run-spawn.d.ts +16 -2
  46. package/dist/lib/run-spawn.d.ts.map +1 -1
  47. package/dist/lib/run-spawn.js +87 -9
  48. package/dist/lib/run-spawn.js.map +1 -1
  49. package/dist/lib/transform.d.ts +1 -1
  50. package/dist/lib/transform.d.ts.map +1 -1
  51. package/dist/lib/transform.js +12 -25
  52. package/dist/lib/transform.js.map +1 -1
  53. package/dist/lib/upgrade.js +20 -16
  54. package/dist/lib/upgrade.js.map +1 -1
  55. package/dist/lib/utils/file-scanner.d.ts +9 -1
  56. package/dist/lib/utils/file-scanner.d.ts.map +1 -1
  57. package/dist/lib/utils/file-scanner.js +22 -20
  58. package/dist/lib/utils/file-scanner.js.map +1 -1
  59. package/dist/lib/utils/package-installer.js +1 -1
  60. package/dist/lib/utils/package-manager.d.ts +3 -1
  61. package/dist/lib/utils/package-manager.d.ts.map +1 -1
  62. package/dist/lib/utils/package-manager.js +22 -10
  63. package/dist/lib/utils/package-manager.js.map +1 -1
  64. package/package.json +6 -6
  65. package/plugin/skills/assistant-ui/SKILL.md +3 -3
  66. package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +370 -0
  67. package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +100 -0
  68. package/src/codemods/v0-12/assistant-api-to-aui.ts +227 -134
  69. package/src/codemods/v0-12/primitive-if-to-aui-if.ts +44 -72
  70. package/src/commands/add.ts +5 -5
  71. package/src/commands/create.ts +41 -7
  72. package/src/commands/info.ts +1 -0
  73. package/src/commands/init.ts +4 -6
  74. package/src/commands/mcp.ts +20 -29
  75. package/src/commands/update.ts +6 -8
  76. package/src/commands/upgrade.ts +5 -2
  77. package/src/index.ts +2 -7
  78. package/src/lib/agent-skill.ts +2 -1
  79. package/src/lib/create-project.ts +24 -58
  80. package/src/lib/handle-cli-error.test.ts +67 -0
  81. package/src/lib/handle-cli-error.ts +17 -0
  82. package/src/lib/install-ai-sdk-lib.ts +13 -1
  83. package/src/lib/install-edge-lib.ts +7 -8
  84. package/src/lib/run-spawn.test.ts +148 -0
  85. package/src/lib/run-spawn.ts +121 -11
  86. package/src/lib/transform.test.ts +62 -0
  87. package/src/lib/transform.ts +21 -37
  88. package/src/lib/upgrade.ts +23 -19
  89. package/src/lib/utils/file-scanner.ts +26 -20
  90. package/src/lib/utils/package-manager.ts +29 -12
@@ -0,0 +1,148 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { PassThrough } from "node:stream";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ spawn: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("cross-spawn", async (importOriginal) => ({
10
+ ...(await importOriginal()),
11
+ spawn: mocks.spawn,
12
+ }));
13
+
14
+ import {
15
+ hasActiveSpawn,
16
+ runSpawn,
17
+ runSpawnCapture,
18
+ SpawnExitError,
19
+ SpawnSignalError,
20
+ } from "./run-spawn";
21
+
22
+ const createChild = () =>
23
+ Object.assign(new EventEmitter(), {
24
+ kill: vi.fn(() => true),
25
+ });
26
+
27
+ const trackSignal = (signal: NodeJS.Signals) => {
28
+ const before = new Set(process.listeners(signal));
29
+ return {
30
+ count: before.size,
31
+ added: () =>
32
+ process.listeners(signal).find((listener) => !before.has(listener)),
33
+ };
34
+ };
35
+
36
+ describe("runSpawn", () => {
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ it("forwards the first termination signal and cleans up listeners", async () => {
43
+ const child = createChild();
44
+ mocks.spawn.mockReturnValue(child);
45
+ const sigterm = trackSignal("SIGTERM");
46
+ const result = runSpawn("assistant-ui", ["create"]);
47
+ const signalHandler = sigterm.added();
48
+
49
+ expect(signalHandler).toBeDefined();
50
+ signalHandler?.("SIGTERM");
51
+ expect(child.kill).toHaveBeenCalledWith("SIGTERM");
52
+
53
+ child.emit("close", null, "SIGTERM");
54
+ await expect(result).rejects.toMatchObject({
55
+ signal: "SIGTERM",
56
+ forwarded: true,
57
+ });
58
+ expect(process.listenerCount("SIGTERM")).toBe(sigterm.count);
59
+ });
60
+
61
+ it("escalates a repeated termination signal", async () => {
62
+ const child = createChild();
63
+ mocks.spawn.mockReturnValue(child);
64
+ const sigterm = trackSignal("SIGTERM");
65
+ const result = runSpawn("assistant-ui", ["create"]);
66
+ const signalHandler = sigterm.added();
67
+
68
+ expect(signalHandler).toBeDefined();
69
+ signalHandler?.("SIGTERM");
70
+ signalHandler?.("SIGTERM");
71
+
72
+ expect(child.kill).toHaveBeenNthCalledWith(1, "SIGTERM");
73
+ expect(child.kill).toHaveBeenNthCalledWith(2, "SIGKILL");
74
+ await expect(result).rejects.toBeInstanceOf(SpawnSignalError);
75
+ expect(process.listenerCount("SIGTERM")).toBe(sigterm.count);
76
+ });
77
+
78
+ it("reports an active spawn only while the child is running", async () => {
79
+ const child = createChild();
80
+ mocks.spawn.mockReturnValue(child);
81
+
82
+ expect(hasActiveSpawn()).toBe(false);
83
+ const result = runSpawn("assistant-ui", ["create"]);
84
+ expect(hasActiveSpawn()).toBe(true);
85
+
86
+ child.emit("close", 0, null);
87
+ await result;
88
+
89
+ expect(hasActiveSpawn()).toBe(false);
90
+ });
91
+
92
+ it("preserves normal child exit handling", async () => {
93
+ const child = createChild();
94
+ mocks.spawn.mockReturnValue(child);
95
+ const result = runSpawn("assistant-ui", ["create"]);
96
+
97
+ child.emit("close", 0, null);
98
+
99
+ await expect(result).resolves.toBeUndefined();
100
+ });
101
+
102
+ it("reports nonzero child exits", async () => {
103
+ const child = createChild();
104
+ mocks.spawn.mockReturnValue(child);
105
+ const result = runSpawn("assistant-ui", ["create"]);
106
+
107
+ child.emit("close", 7, null);
108
+
109
+ await expect(result).rejects.toEqual(expect.any(SpawnExitError));
110
+ await expect(result).rejects.toMatchObject({ code: 7 });
111
+ });
112
+
113
+ it("distinguishes child signals from forwarded signals", async () => {
114
+ const child = createChild();
115
+ mocks.spawn.mockReturnValue(child);
116
+ const result = runSpawn("assistant-ui", ["create"]);
117
+
118
+ child.emit("close", null, "SIGSEGV");
119
+
120
+ await expect(result).rejects.toMatchObject({
121
+ signal: "SIGSEGV",
122
+ forwarded: false,
123
+ });
124
+ });
125
+
126
+ it("captures output without blocking the event loop", async () => {
127
+ const child = Object.assign(new EventEmitter(), {
128
+ kill: vi.fn(() => true),
129
+ stdout: new PassThrough(),
130
+ stderr: new PassThrough(),
131
+ });
132
+ mocks.spawn.mockReturnValue(child);
133
+ const result = runSpawnCapture("jscodeshift", ["--version"]);
134
+
135
+ child.stdout.write("Processing file one\n");
136
+ child.stderr.write("warning\n");
137
+ child.stdout.end();
138
+ child.stderr.end();
139
+ child.emit("close", 0, null);
140
+
141
+ await expect(result).resolves.toMatchObject({
142
+ code: 0,
143
+ signal: null,
144
+ stdout: "Processing file one\n",
145
+ stderr: "warning\n",
146
+ });
147
+ });
148
+ });
@@ -2,31 +2,141 @@ import { spawn } from "cross-spawn";
2
2
 
3
3
  export class SpawnExitError extends Error {
4
4
  code: number;
5
+ stderr: string;
5
6
 
6
- constructor(code: number) {
7
- super(`Process exited with code ${code}`);
7
+ constructor(code: number, stderr = "") {
8
+ super(
9
+ stderr
10
+ ? `Process exited with code ${code}\n${stderr}`
11
+ : `Process exited with code ${code}`,
12
+ );
8
13
  this.code = code;
14
+ this.stderr = stderr;
9
15
  }
10
16
  }
11
17
 
12
- export function runSpawn(
18
+ export class SpawnSignalError extends Error {
19
+ signal: NodeJS.Signals;
20
+ forwarded: boolean;
21
+
22
+ constructor(signal: NodeJS.Signals, forwarded: boolean) {
23
+ super(`Process terminated by ${signal}`);
24
+ this.signal = signal;
25
+ this.forwarded = forwarded;
26
+ }
27
+ }
28
+
29
+ export interface SpawnResult {
30
+ code: number | null;
31
+ signal: NodeJS.Signals | null;
32
+ stdout: string;
33
+ stderr: string;
34
+ }
35
+
36
+ let activeSpawns = 0;
37
+
38
+ // A spawn that is forwarding a signal owns termination for that window, so
39
+ // callers with their own cleanup listeners defer to it instead of racing it.
40
+ export const hasActiveSpawn = (): boolean => activeSpawns > 0;
41
+
42
+ function spawnProcess(
13
43
  command: string,
14
44
  args: string[],
15
- cwd?: string,
16
- ): Promise<void> {
45
+ cwd: string | undefined,
46
+ captureOutput: boolean,
47
+ ): Promise<SpawnResult> {
17
48
  return new Promise((resolve, reject) => {
18
49
  const child = spawn(command, args, {
19
- stdio: "inherit",
50
+ stdio: captureOutput ? ["ignore", "pipe", "pipe"] : "inherit",
20
51
  cwd,
21
52
  });
53
+ let forwardedSignal: NodeJS.Signals | null = null;
54
+ let settled = false;
55
+ let stdout = "";
56
+ let stderr = "";
57
+
58
+ if (captureOutput) {
59
+ child.stdout?.setEncoding("utf8");
60
+ child.stdout?.on("data", (chunk: string) => {
61
+ stdout += chunk;
62
+ });
63
+ child.stderr?.setEncoding("utf8");
64
+ child.stderr?.on("data", (chunk: string) => {
65
+ stderr += chunk;
66
+ });
67
+ }
68
+
69
+ const cleanup = () => {
70
+ activeSpawns -= 1;
71
+ process.off("SIGINT", onSigint);
72
+ process.off("SIGTERM", onSigterm);
73
+ };
74
+
75
+ const rejectWithSignal = (signal: NodeJS.Signals) => {
76
+ settled = true;
77
+ cleanup();
78
+ reject(new SpawnSignalError(signal, forwardedSignal !== null));
79
+ };
80
+
81
+ const forwardSignal = (signal: NodeJS.Signals) => {
82
+ if (forwardedSignal !== null) {
83
+ child.kill("SIGKILL");
84
+ rejectWithSignal(forwardedSignal);
85
+ return;
86
+ }
87
+
88
+ forwardedSignal = signal;
89
+ child.kill(signal);
90
+ };
91
+ const onSigint = () => forwardSignal("SIGINT");
92
+ const onSigterm = () => forwardSignal("SIGTERM");
22
93
 
23
- child.on("error", (error) => reject(error));
24
- child.on("close", (code) => {
25
- if (code !== 0) {
26
- reject(new SpawnExitError(code || 1));
94
+ activeSpawns += 1;
95
+ process.on("SIGINT", onSigint);
96
+ process.on("SIGTERM", onSigterm);
97
+
98
+ child.on("error", (error) => {
99
+ if (settled) return;
100
+ settled = true;
101
+ cleanup();
102
+ if (forwardedSignal !== null) {
103
+ reject(new SpawnSignalError(forwardedSignal, true));
27
104
  } else {
28
- resolve();
105
+ reject(error);
106
+ }
107
+ });
108
+ child.on("close", (code, signal) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ cleanup();
112
+ if (forwardedSignal !== null) {
113
+ reject(new SpawnSignalError(forwardedSignal, true));
114
+ } else {
115
+ resolve({ code, signal: signal ?? null, stdout, stderr });
29
116
  }
30
117
  });
31
118
  });
32
119
  }
120
+
121
+ export function runSpawn(
122
+ command: string,
123
+ args: string[],
124
+ cwd?: string,
125
+ ): Promise<void> {
126
+ return spawnProcess(command, args, cwd, false).then(({ code, signal }) => {
127
+ if (signal != null) {
128
+ throw new SpawnSignalError(signal, false);
129
+ }
130
+ if (code !== 0) {
131
+ throw new SpawnExitError(code || 1);
132
+ }
133
+ });
134
+ }
135
+
136
+ export function runSpawnCapture(
137
+ command: string,
138
+ args: string[],
139
+ cwd?: string,
140
+ ): Promise<SpawnResult> {
141
+ return spawnProcess(command, args, cwd, true);
142
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ runSpawnCapture: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("./run-spawn", async (importOriginal) => ({
8
+ ...(await importOriginal()),
9
+ runSpawnCapture: mocks.runSpawnCapture,
10
+ }));
11
+
12
+ import { transform } from "./transform";
13
+ import { SpawnExitError } from "./run-spawn";
14
+
15
+ describe("transform", () => {
16
+ it("runs codemods asynchronously and reports progress", async () => {
17
+ mocks.runSpawnCapture.mockResolvedValue({
18
+ code: 0,
19
+ signal: null,
20
+ stdout: "Processing file one\nProcessing file two\n",
21
+ stderr: "",
22
+ });
23
+ const onProgress = vi.fn();
24
+
25
+ const errors = await transform(
26
+ "v0-8/ui-package-split",
27
+ "/tmp/project",
28
+ { dry: true },
29
+ {
30
+ logStatus: false,
31
+ onProgress,
32
+ relevantFiles: ["/tmp/project/app.tsx"],
33
+ },
34
+ );
35
+
36
+ expect(errors).toEqual([]);
37
+ expect(onProgress).toHaveBeenCalledWith(2);
38
+ expect(mocks.runSpawnCapture).toHaveBeenCalledWith(
39
+ "npx",
40
+ expect.arrayContaining(["jscodeshift", "--dry"]),
41
+ );
42
+ });
43
+
44
+ it("surfaces the codemod's stderr when it exits nonzero", async () => {
45
+ mocks.runSpawnCapture.mockResolvedValue({
46
+ code: 1,
47
+ signal: null,
48
+ stdout: "",
49
+ stderr: "SyntaxError: Unexpected token\n",
50
+ });
51
+
52
+ const failure = transform(
53
+ "v0-8/ui-package-split",
54
+ "/tmp/project",
55
+ { dry: true },
56
+ { logStatus: false, relevantFiles: ["/tmp/project/app.tsx"] },
57
+ );
58
+
59
+ await expect(failure).rejects.toBeInstanceOf(SpawnExitError);
60
+ await expect(failure).rejects.toThrow("SyntaxError: Unexpected token");
61
+ });
62
+ });
@@ -1,10 +1,10 @@
1
- import { execFileSync, spawnSync } from "node:child_process";
2
1
  import debug from "debug";
3
2
  import path from "node:path";
4
3
  import type { TransformOptions } from "./transform-options";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import * as fs from "node:fs";
7
6
  import { sync as globSync } from "glob";
7
+ import { runSpawnCapture, SpawnExitError, SpawnSignalError } from "./run-spawn";
8
8
 
9
9
  const log = debug("codemod:transform");
10
10
  const error = debug("codemod:transform:error");
@@ -106,7 +106,7 @@ function parseErrors(transform: string, output: string): TransformErrors {
106
106
  return errors;
107
107
  }
108
108
 
109
- export function transform(
109
+ export async function transform(
110
110
  codemod: string,
111
111
  source: string,
112
112
  transformOptions: TransformOptions,
@@ -115,7 +115,7 @@ export function transform(
115
115
  onProgress?: (processedFiles: number) => void;
116
116
  relevantFiles?: string[];
117
117
  } = { logStatus: true },
118
- ): TransformErrors {
118
+ ): Promise<TransformErrors> {
119
119
  if (options.logStatus) {
120
120
  log(`Applying codemod '${codemod}': ${source}`);
121
121
  }
@@ -133,44 +133,28 @@ export function transform(
133
133
 
134
134
  const command = buildCommand(codemodPath, targetFiles, transformOptions);
135
135
 
136
- // Use spawn instead of execFileSync to capture output in real-time
137
- if (options.onProgress) {
138
- const result = spawnSync(command[0]!, command.slice(1), {
139
- encoding: "utf8",
140
- stdio: ["ignore", "pipe", "pipe"],
141
- });
136
+ const result = await runSpawnCapture(command[0]!, command.slice(1));
137
+ if (result.signal !== null) {
138
+ throw new SpawnSignalError(result.signal, false);
139
+ }
140
+ if (!options.onProgress && result.code !== 0) {
141
+ throw new SpawnExitError(result.code || 1, result.stderr);
142
+ }
142
143
 
143
- const stdout = result.stdout || "";
144
+ const { stdout } = result;
144
145
 
145
- // Count the number of processed files from the output
146
+ if (options.onProgress) {
146
147
  const processedFiles = (stdout.match(/Processing file/g) || []).length;
147
- if (options.onProgress) {
148
- options.onProgress(processedFiles);
149
- }
148
+ options.onProgress(processedFiles);
149
+ }
150
150
 
151
- const errors = parseErrors(codemod, stdout);
152
- if (options.logStatus && errors.length > 0) {
153
- errors.forEach(({ transform, filename, summary }) => {
154
- error(
155
- `Error applying codemod [codemod=${transform}, path=${filename}, summary=${summary}]`,
156
- );
157
- });
158
- }
159
- return errors;
160
- } else {
161
- // Use the original synchronous approach if no progress callback
162
- const stdout = execFileSync(command[0]!, command.slice(1), {
163
- encoding: "utf8",
164
- stdio: "pipe",
151
+ const errors = parseErrors(codemod, stdout);
152
+ if (options.logStatus && errors.length > 0) {
153
+ errors.forEach(({ transform, filename, summary }) => {
154
+ error(
155
+ `Error applying codemod [codemod=${transform}, path=${filename}, summary=${summary}]`,
156
+ );
165
157
  });
166
- const errors = parseErrors(codemod, stdout);
167
- if (options.logStatus && errors.length > 0) {
168
- errors.forEach(({ transform, filename, summary }) => {
169
- error(
170
- `Error applying codemod [codemod=${transform}, path=${filename}, summary=${summary}]`,
171
- );
172
- });
173
- }
174
- return errors;
175
158
  }
159
+ return errors;
176
160
  }
@@ -42,6 +42,7 @@ export async function upgrade(options: TransformOptions) {
42
42
  {
43
43
  format: "Progress |{bar}| {percentage}% | ETA: {eta}s || {status}",
44
44
  hideCursor: true,
45
+ gracefulExit: true,
45
46
  },
46
47
  Presets.shades_classic,
47
48
  );
@@ -49,28 +50,31 @@ export async function upgrade(options: TransformOptions) {
49
50
  bar.start(totalWork, 0, { status: "Starting..." });
50
51
  const allErrors: TransformErrors = [];
51
52
 
52
- for (const codemod of bundle) {
53
- bar.update(completedWork, { status: `Running ${codemod}...` });
53
+ try {
54
+ for (const codemod of bundle) {
55
+ bar.update(completedWork, { status: `Running ${codemod}...` });
54
56
 
55
- // Use a custom progress callback to update the progress bar
56
- const errors = transform(codemod, cwd, options, {
57
- logStatus: false,
58
- onProgress: (processedFiles: number) => {
59
- completedWork = bundle.indexOf(codemod) * fileCount + processedFiles;
60
- bar.update(Math.min(completedWork, totalWork), {
61
- status: `Running ${codemod} (${processedFiles}/${fileCount} files)`,
62
- });
63
- },
64
- relevantFiles, // Pass the pre-computed relevant files
65
- });
57
+ // Use a custom progress callback to update the progress bar
58
+ const errors = await transform(codemod, cwd, options, {
59
+ logStatus: false,
60
+ onProgress: (processedFiles: number) => {
61
+ completedWork = bundle.indexOf(codemod) * fileCount + processedFiles;
62
+ bar.update(Math.min(completedWork, totalWork), {
63
+ status: `Running ${codemod} (${processedFiles}/${fileCount} files)`,
64
+ });
65
+ },
66
+ relevantFiles, // Pass the pre-computed relevant files
67
+ });
66
68
 
67
- allErrors.push(...errors);
68
- completedWork = (bundle.indexOf(codemod) + 1) * fileCount;
69
- bar.update(completedWork, { status: `Completed ${codemod}` });
70
- }
69
+ allErrors.push(...errors);
70
+ completedWork = (bundle.indexOf(codemod) + 1) * fileCount;
71
+ bar.update(completedWork, { status: `Completed ${codemod}` });
72
+ }
71
73
 
72
- bar.update(totalWork, { status: "Checking dependencies..." });
73
- bar.stop();
74
+ bar.update(totalWork, { status: "Checking dependencies..." });
75
+ } finally {
76
+ bar.stop();
77
+ }
74
78
 
75
79
  if (allErrors.length > 0) {
76
80
  log("Some codemods did not apply successfully to all files. Details:");
@@ -8,6 +8,23 @@ export interface ScanOptions {
8
8
  ignore?: string[];
9
9
  }
10
10
 
11
+ export function* readProjectFiles(
12
+ pattern: string,
13
+ options: { cwd: string; ignore?: string[] },
14
+ ): Generator<{ file: string; fullPath: string; content: string }> {
15
+ const files = globSync(pattern, options);
16
+
17
+ for (const file of files) {
18
+ const fullPath = path.join(options.cwd, file);
19
+ try {
20
+ const content = fs.readFileSync(fullPath, "utf8");
21
+ yield { file, fullPath, content };
22
+ } catch {
23
+ continue;
24
+ }
25
+ }
26
+ }
27
+
11
28
  export function scanForImport(
12
29
  importPattern: string | string[],
13
30
  options: ScanOptions = {},
@@ -20,20 +37,13 @@ export function scanForImport(
20
37
  "**/build/**",
21
38
  ];
22
39
 
23
- const files = globSync(pattern, { cwd, ignore });
24
40
  const patterns = Array.isArray(importPattern)
25
41
  ? importPattern
26
42
  : [importPattern];
27
43
 
28
- for (const file of files) {
29
- const fullPath = path.join(cwd, file);
30
- try {
31
- const content = fs.readFileSync(fullPath, "utf8");
32
- if (patterns.some((p) => content.includes(p))) {
33
- return true;
34
- }
35
- } catch {
36
- // Ignore files that cannot be read
44
+ for (const { content } of readProjectFiles(pattern, { cwd, ignore })) {
45
+ if (patterns.some((p) => content.includes(p))) {
46
+ return true;
37
47
  }
38
48
  }
39
49
 
@@ -52,18 +62,14 @@ export function getFilesContaining(
52
62
  "**/build/**",
53
63
  ];
54
64
 
55
- const files = globSync(pattern, { cwd, ignore });
56
65
  const result: string[] = [];
57
66
 
58
- for (const file of files) {
59
- const fullPath = path.join(cwd, file);
60
- try {
61
- const content = fs.readFileSync(fullPath, "utf8");
62
- if (content.includes(searchString)) {
63
- result.push(fullPath);
64
- }
65
- } catch {
66
- // Ignore files that cannot be read
67
+ for (const { fullPath, content } of readProjectFiles(pattern, {
68
+ cwd,
69
+ ignore,
70
+ })) {
71
+ if (content.includes(searchString)) {
72
+ result.push(fullPath);
67
73
  }
68
74
  }
69
75
 
@@ -1,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
3
  import { detect } from "detect-package-manager";
5
4
  import * as readline from "node:readline";
6
5
  import { logger } from "./logger";
6
+ import { runSpawn, SpawnSignalError } from "../run-spawn";
7
+
8
+ export type PackageManagerName = "npm" | "pnpm" | "yarn" | "bun";
7
9
 
8
10
  export function askQuestion(query: string): Promise<string> {
9
11
  return new Promise((resolve) => {
@@ -62,25 +64,40 @@ export async function getInstallCommand(
62
64
  }
63
65
  }
64
66
 
67
+ function detectFromUserAgent(): PackageManagerName | undefined {
68
+ const ua = process.env.npm_config_user_agent;
69
+ if (!ua) return undefined;
70
+ if (ua.startsWith("bun/")) return "bun";
71
+ if (ua.startsWith("pnpm/")) return "pnpm";
72
+ if (ua.startsWith("yarn/")) return "yarn";
73
+ if (ua.startsWith("npm/")) return "npm";
74
+ return undefined;
75
+ }
76
+
77
+ export async function resolvePackageManagerForCwd(
78
+ cwd: string,
79
+ packageManager?: PackageManagerName,
80
+ ): Promise<PackageManagerName> {
81
+ if (packageManager) return packageManager;
82
+ const fromAgent = detectFromUserAgent();
83
+ if (fromAgent) return fromAgent;
84
+ try {
85
+ return await detect({ cwd });
86
+ } catch {
87
+ return "npm";
88
+ }
89
+ }
90
+
65
91
  export async function installPackage(
66
92
  packageName: string,
67
93
  cwd?: string,
68
94
  ): Promise<boolean> {
69
95
  try {
70
96
  const { command, args } = await getInstallCommand(packageName, cwd);
71
- const result = spawnSync(command, args, { stdio: "inherit", cwd });
72
-
73
- if (result.error || result.status !== 0) {
74
- logger.error(
75
- `Installation failed${
76
- result.error ? `: ${String(result.error)}` : "."
77
- }`,
78
- );
79
- return false;
80
- }
81
-
97
+ await runSpawn(command, args, cwd);
82
98
  return true;
83
99
  } catch (e) {
100
+ if (e instanceof SpawnSignalError) throw e;
84
101
  logger.error(`Installation failed: ${String(e)}`);
85
102
  return false;
86
103
  }