@akanjs/devkit 2.1.0-rc.1 → 2.1.0-rc.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.
@@ -1,5 +1,5 @@
1
- import { confirm, input, select } from "@inquirer/prompts";
2
1
  import path from "node:path";
2
+ import { confirm, input, select } from "@inquirer/prompts";
3
3
  import { Logger } from "akanjs/common";
4
4
  import chalk from "chalk";
5
5
  import { type Command, program } from "commander";
@@ -19,6 +19,32 @@ import { formatCommandHelp, formatHelp } from "./helpFormatter";
19
19
  import { type CommandCls, getTargetMetas } from "./targetMeta";
20
20
 
21
21
  const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
22
+ const loggedCliErrorObjects = new WeakSet<object>();
23
+ const loggedCliErrorMessages = new Set<string>();
24
+
25
+ const formatCliError = (error: unknown): string => {
26
+ if (error instanceof Error) return error.message || error.name;
27
+ if (typeof error === "string") return error.trim() || "Unknown error";
28
+ if (error === null || error === undefined) return "Unknown error";
29
+ try {
30
+ const json = JSON.stringify(error);
31
+ if (json) return json;
32
+ } catch {
33
+ return String(error);
34
+ }
35
+ return String(error) || "Unknown error";
36
+ };
37
+
38
+ const printCliError = (error: unknown) => {
39
+ if (typeof error === "object" && error !== null) {
40
+ if (loggedCliErrorObjects.has(error)) return;
41
+ loggedCliErrorObjects.add(error);
42
+ }
43
+ const message = formatCliError(error);
44
+ if (loggedCliErrorMessages.has(message)) return;
45
+ loggedCliErrorMessages.add(message);
46
+ Logger.rawLog(`\n${chalk.red(message)}`);
47
+ };
22
48
 
23
49
  const handleOption = (programCommand: Command, argMeta: ArgMeta) => {
24
50
  const {
@@ -219,6 +245,7 @@ const getInternalArgumentValue = async (
219
245
 
220
246
  export const runCommands = async (...commands: CommandCls[]) => {
221
247
  process.on("unhandledRejection", (error) => {
248
+ printCliError(error);
222
249
  process.exit(1);
223
250
  });
224
251
  const __dirname = getDirname(import.meta.url);
@@ -339,8 +366,7 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
339
366
  await targetMeta.handler.call(cmd, ...commandArgs);
340
367
  Logger.rawLog();
341
368
  } catch (e) {
342
- const errMsg = e instanceof Error ? e.message : typeof e === "string" ? e : JSON.stringify(e);
343
- Logger.rawLog(`\n${chalk.red(errMsg)}`);
369
+ printCliError(e);
344
370
  throw e;
345
371
  }
346
372
  });
package/executors.test.ts CHANGED
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { AkanAppConfig } from "./akanConfig";
6
- import { AppExecutor, Executor, PkgExecutor, WorkspaceExecutor } from "./executors";
6
+ import { AppExecutor, CommandExecutionError, Executor, PkgExecutor, WorkspaceExecutor } from "./executors";
7
7
  import { AppInfo } from "./scanInfo";
8
8
  import type { PackageJson } from "./types";
9
9
 
@@ -48,6 +48,41 @@ afterEach(async () => {
48
48
  });
49
49
 
50
50
  describe("Executor filesystem helpers", () => {
51
+ test("reports command failures with command context and captured output", async () => {
52
+ const root = await makeTempRoot();
53
+ const exec = new Executor("fixture", root);
54
+
55
+ let error: unknown;
56
+ try {
57
+ await exec.spawn(process.execPath, ["--eval", "console.error('spawn failed'); process.exit(7)"]);
58
+ } catch (caught) {
59
+ error = caught;
60
+ }
61
+
62
+ expect(error).toBeInstanceOf(CommandExecutionError);
63
+ expect((error as CommandExecutionError).message).toContain(`Command failed: ${process.execPath}`);
64
+ expect((error as CommandExecutionError).message).toContain(`cwd: ${root}`);
65
+ expect((error as CommandExecutionError).message).toContain("exit code: 7");
66
+ expect((error as CommandExecutionError).message).toContain("spawn failed");
67
+ });
68
+
69
+ test("reports inherited stdio command failures with a fallback message", async () => {
70
+ const root = await makeTempRoot();
71
+ const exec = new Executor("fixture", root);
72
+
73
+ let error: unknown;
74
+ try {
75
+ await exec.spawn(process.execPath, ["--eval", "process.exit(3)"], { stdio: "inherit" });
76
+ } catch (caught) {
77
+ error = caught;
78
+ }
79
+
80
+ expect(error).toBeInstanceOf(CommandExecutionError);
81
+ expect((error as CommandExecutionError).message).toContain(`Command failed: ${process.execPath}`);
82
+ expect((error as CommandExecutionError).message).toContain(`cwd: ${root}`);
83
+ expect((error as CommandExecutionError).message).toContain("exit code: 3");
84
+ });
85
+
51
86
  test("resolves paths and reads/writes files relative to cwd", async () => {
52
87
  const root = await makeTempRoot();
53
88
  const exec = new Executor("fixture", root);
package/executors.ts CHANGED
@@ -64,6 +64,58 @@ const staticTemplateFileExtensions = new Set([
64
64
  ".xml",
65
65
  ]);
66
66
 
67
+ const formatCommandArg = (value: string) => (/^[\w@%+=:,./-]+$/.test(value) ? value : JSON.stringify(value));
68
+
69
+ const formatCommandForDisplay = (command: string, args: string[] = []) =>
70
+ [command, ...args].map(formatCommandArg).join(" ");
71
+
72
+ export interface CommandExecutionErrorOptions {
73
+ command: string;
74
+ args?: string[];
75
+ cwd: string;
76
+ code: number | null;
77
+ signal: string | null;
78
+ stdout?: string;
79
+ stderr?: string;
80
+ cause?: unknown;
81
+ }
82
+
83
+ export class CommandExecutionError extends Error {
84
+ command: string;
85
+ args: string[];
86
+ cwd: string;
87
+ code: number | null;
88
+ signal: string | null;
89
+ stdout: string;
90
+ stderr: string;
91
+
92
+ constructor({
93
+ command,
94
+ args = [],
95
+ cwd,
96
+ code,
97
+ signal,
98
+ stdout = "",
99
+ stderr = "",
100
+ cause,
101
+ }: CommandExecutionErrorOptions) {
102
+ const displayCommand = formatCommandForDisplay(command, args);
103
+ const status = signal ? `signal: ${signal}` : `exit code: ${code ?? "unknown"}`;
104
+ const output = (stderr || stdout).trim();
105
+ super([`Command failed: ${displayCommand}`, `cwd: ${cwd}`, status, output ? `\n${output}` : ""].join("\n"), {
106
+ cause,
107
+ });
108
+ this.name = "CommandExecutionError";
109
+ this.command = command;
110
+ this.args = args;
111
+ this.cwd = cwd;
112
+ this.code = code;
113
+ this.signal = signal;
114
+ this.stdout = stdout;
115
+ this.stderr = stderr;
116
+ }
117
+ }
118
+
67
119
  export const execEmoji = {
68
120
  workspace: "🏠",
69
121
  app: "🚀",
@@ -197,22 +249,31 @@ export class Executor {
197
249
  Logger.raw(chalk.red(data.toString()));
198
250
  }
199
251
  exec(command: string, options: ExecOptions = {}) {
252
+ const cwd = options.cwd?.toString() ?? this.cwdPath;
200
253
  const proc = exec(command, { cwd: this.cwdPath, ...options });
254
+ let stdout = "";
255
+ let stderr = "";
201
256
  proc.stdout?.on("data", (data: Buffer) => {
257
+ stdout += data.toString();
202
258
  this.#stdout(data);
203
259
  });
204
260
  proc.stderr?.on("data", (data: Buffer) => {
261
+ stderr += data.toString();
205
262
  this.#stdout(data); // 정상로그도 stderr로 나옴
206
263
  });
207
264
  return new Promise((resolve, reject) => {
265
+ proc.on("error", (error) => {
266
+ reject(new CommandExecutionError({ command, cwd, code: null, signal: null, stdout, stderr, cause: error }));
267
+ });
208
268
  proc.on("exit", (code, signal) => {
209
- if (!!code || signal) reject({ code, signal });
269
+ if (!!code || signal) reject(new CommandExecutionError({ command, cwd, code, signal, stdout, stderr }));
210
270
  else resolve({ code, signal });
211
271
  });
212
272
  });
213
273
  }
214
274
 
215
275
  spawn(command: string, args: string[] = [], options: SpawnOptions = {}): Promise<string> {
276
+ const cwd = options.cwd?.toString() ?? this.cwdPath;
216
277
  const proc = spawn(command, args, {
217
278
  cwd: this.cwdPath,
218
279
  // stdio: "inherit",
@@ -232,8 +293,14 @@ export class Executor {
232
293
  this.#stdout(data); // 정상로그도 stderr로 나옴
233
294
  });
234
295
  return new Promise((resolve, reject) => {
296
+ proc.on("error", (error) => {
297
+ reject(
298
+ new CommandExecutionError({ command, args, cwd, code: null, signal: null, stdout, stderr, cause: error }),
299
+ );
300
+ });
235
301
  proc.on("close", (code, signal) => {
236
- if (code !== 0 || signal) reject(stderr || stdout);
302
+ if (code !== 0 || signal)
303
+ reject(new CommandExecutionError({ command, args, cwd, code, signal, stdout, stderr }));
237
304
  else resolve(stdout);
238
305
  });
239
306
  });
@@ -247,20 +314,40 @@ export class Executor {
247
314
  return proc;
248
315
  }
249
316
  fork(modulePath: string, args: string[] = [], options: ForkOptions = {}) {
317
+ const cwd = options.cwd?.toString() ?? this.cwdPath;
250
318
  const proc = fork(modulePath, args, {
251
319
  cwd: this.cwdPath,
252
320
  // stdio: ["ignore", "inherit", "inherit", "ipc"],
253
321
  ...options,
254
322
  });
323
+ let stdout = "";
324
+ let stderr = "";
255
325
  proc.stdout?.on("data", (data: Buffer) => {
326
+ stdout += data.toString();
256
327
  this.#stdout(data);
257
328
  });
258
329
  proc.stderr?.on("data", (data: Buffer) => {
330
+ stderr += data.toString();
259
331
  this.#stderr(data);
260
332
  });
261
333
  return new Promise((resolve, reject) => {
334
+ proc.on("error", (error) => {
335
+ reject(
336
+ new CommandExecutionError({
337
+ command: modulePath,
338
+ args,
339
+ cwd,
340
+ code: null,
341
+ signal: null,
342
+ stdout,
343
+ stderr,
344
+ cause: error,
345
+ }),
346
+ );
347
+ });
262
348
  proc.on("exit", (code, signal) => {
263
- if (!!code || signal) reject({ code, signal });
349
+ if (!!code || signal)
350
+ reject(new CommandExecutionError({ command: modulePath, args, cwd, code, signal, stdout, stderr }));
264
351
  else resolve({ code, signal });
265
352
  });
266
353
  });
@@ -24,9 +24,9 @@ export class CssImportResolver {
24
24
 
25
25
  async resolve(id: string, fromBase: string): Promise<string | null> {
26
26
  for (const resolve of [
27
+ () => this.#resolveWithTsconfig(id),
27
28
  () => this.#resolveWithBun(id, fromBase),
28
29
  () => this.#resolveWithRequire(id, fromBase),
29
- () => this.#resolveWithTsconfig(id),
30
30
  () => this.#resolvePackageStyle(id, fromBase),
31
31
  ]) {
32
32
  const resolved = await resolve();
@@ -36,21 +36,27 @@ export class CssImportResolver {
36
36
  }
37
37
 
38
38
  #resolveWithBun(id: string, fromBase: string): string | null {
39
- try {
40
- const resolved = Bun.resolveSync(id, fromBase);
41
- return CssImportResolver.isCssFile(resolved) ? resolved : null;
42
- } catch {
43
- return null;
39
+ for (const base of this.#resolutionBases(fromBase)) {
40
+ try {
41
+ const resolved = Bun.resolveSync(id, base);
42
+ if (CssImportResolver.isCssFile(resolved)) return resolved;
43
+ } catch {
44
+ // Try the next known package resolution root.
45
+ }
44
46
  }
47
+ return null;
45
48
  }
46
49
 
47
50
  #resolveWithRequire(id: string, fromBase: string): string | null {
48
- try {
49
- const resolved = require.resolve(id, { paths: [fromBase] });
50
- return CssImportResolver.isCssFile(resolved) ? resolved : null;
51
- } catch {
52
- return null;
51
+ for (const base of this.#resolutionBases(fromBase)) {
52
+ try {
53
+ const resolved = require.resolve(id, { paths: [base] });
54
+ if (CssImportResolver.isCssFile(resolved)) return resolved;
55
+ } catch {
56
+ // Try the next known package resolution root.
57
+ }
53
58
  }
59
+ return null;
54
60
  }
55
61
 
56
62
  async #resolveWithTsconfig(id: string): Promise<string | null> {
@@ -77,8 +83,25 @@ export class CssImportResolver {
77
83
  async #resolvePackageStyle(id: string, fromBase: string): Promise<string | null> {
78
84
  const pkgName = CssImportResolver.getPackageName(id);
79
85
  if (!pkgName) return null;
86
+ for (const base of this.#resolutionBases(fromBase)) {
87
+ try {
88
+ const pkgPath = require.resolve(`${pkgName}/package.json`, { paths: [base] });
89
+ const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
90
+ if (resolved) return resolved;
91
+ } catch {
92
+ // Try the next known package resolution root.
93
+ }
94
+ }
95
+ for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
96
+ const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
97
+ if (resolved) return resolved;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ async #resolvePackageStyleFromPackageJson(id: string, pkgName: string, pkgPath: string): Promise<string | null> {
80
103
  try {
81
- const pkgPath = require.resolve(`${pkgName}/package.json`, { paths: [fromBase] });
104
+ if (!(await Bun.file(pkgPath).exists())) return null;
82
105
  const pkgDir = path.dirname(pkgPath);
83
106
  const pkg = await Bun.file(pkgPath).json();
84
107
  const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
@@ -96,6 +119,24 @@ export class CssImportResolver {
96
119
  }
97
120
  }
98
121
 
122
+ #resolutionBases(fromBase: string): string[] {
123
+ return [
124
+ fromBase,
125
+ this.#workspaceRoot,
126
+ path.dirname(Bun.main),
127
+ path.resolve(path.dirname(Bun.main), "../.."),
128
+ ];
129
+ }
130
+
131
+ #packageJsonCandidates(pkgName: string): string[] {
132
+ return [
133
+ path.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
134
+ path.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
135
+ path.join(path.dirname(Bun.main), "node_modules", pkgName, "package.json"),
136
+ path.join(path.dirname(Bun.main), "../../", pkgName, "package.json"),
137
+ ];
138
+ }
139
+
99
140
  async #firstExisting(basePath: string): Promise<string | null> {
100
141
  for (const suffix of CSS_IMPORT_EXTS) {
101
142
  const candidate = `${basePath}${suffix}`;
@@ -94,13 +94,26 @@ export class SsrBaseArtifactBuilder {
94
94
  async #resolveAkanServerPath() {
95
95
  const candidates = [
96
96
  path.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"),
97
+ path.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"),
98
+ path.join(path.dirname(Bun.main), "node_modules/akanjs/server"),
99
+ path.join(path.dirname(Bun.main), "../../akanjs/server"),
97
100
  path.resolve(import.meta.dir, "../../server"),
98
101
  path.resolve(import.meta.dir, "../server"),
99
102
  ];
103
+ try {
104
+ candidates.unshift(path.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
105
+ } catch {
106
+ // Source workspaces and bundled CLI execution have different resolution roots; try the explicit candidates.
107
+ }
108
+ try {
109
+ candidates.unshift(path.dirname(Bun.resolveSync("akanjs/server", path.dirname(Bun.main))));
110
+ } catch {
111
+ // Published CLI installs may hoist dependencies differently; explicit Bun.main candidates cover that.
112
+ }
100
113
  for (const candidate of candidates) {
101
114
  if (await Bun.file(path.join(candidate, "rscClient.tsx")).exists()) return candidate;
102
115
  }
103
- return candidates[0];
116
+ throw new Error(`[base-artifact] failed to locate akanjs/server; looked in: ${candidates.join(", ")}`);
104
117
  }
105
118
 
106
119
  async #buildStyleAssets(): Promise<{
@@ -143,6 +143,7 @@ export class IncrementalBuilderHost {
143
143
  app.workspace.workspaceRoot,
144
144
  "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts",
145
145
  ),
146
+ path.join(import.meta.dir, "incrementalBuilder.proc.js"),
146
147
  path.join(import.meta.dir, "incrementalBuilder.proc.ts"),
147
148
  ];
148
149
  for (const c of candidates)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.1.0-rc.1",
3
+ "version": "2.1.0-rc.3",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -36,7 +36,7 @@
36
36
  "@langchain/deepseek": "^1.0.26",
37
37
  "@langchain/openai": "^1.4.6",
38
38
  "@trapezedev/project": "^7.1.4",
39
- "akanjs": "2.1.0-rc.0",
39
+ "akanjs": "2.1.0-rc.3",
40
40
  "chalk": "^5.6.2",
41
41
  "commander": "^14.0.3",
42
42
  "fontaine": "^0.8.0",