@akanjs/devkit 3.0.0-alpha.87 → 3.0.0-alpha.89

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,8 +1,14 @@
1
1
  import path from "node:path";
2
- import type { confirm as inquirerConfirm, input as inquirerInput, select as inquirerSelect } from "@inquirer/prompts";
2
+ import type {
3
+ checkbox as inquirerCheckbox,
4
+ confirm as inquirerConfirm,
5
+ input as inquirerInput,
6
+ select as inquirerSelect,
7
+ } from "@inquirer/prompts";
3
8
  import { Logger } from "akanjs/common";
4
9
  import chalk from "chalk";
5
10
  import { type Command, program } from "commander";
11
+ import { AppSelectionMemory } from "../appSelectionMemory";
6
12
  import { AppExecutor, Executor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
7
13
  // Import the owning modules directly, never the root barrel: `..` re-exports all 41 devkit modules,
8
14
  // so a barrel import here drags ink, @trapezedev/project, ssh2, @langchain/* and the cloud stack into
@@ -103,6 +109,7 @@ const prompts = async () => await import("@inquirer/prompts");
103
109
  const select = ((config, context) => prompts().then((m) => m.select(config, context))) as typeof inquirerSelect;
104
110
  const confirm = ((config, context) => prompts().then((m) => m.confirm(config, context))) as typeof inquirerConfirm;
105
111
  const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
112
+ const checkbox = ((config, context) => prompts().then((m) => m.checkbox(config, context))) as typeof inquirerCheckbox;
106
113
 
107
114
  /**
108
115
  * Rejects a value that is not one of the declared choices, in commander's own wording.
@@ -208,6 +215,46 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
208
215
  );
209
216
  };
210
217
 
218
+ /** A variadic positional arrives as an array, and each entry may itself be a comma-separated list. */
219
+ const parseAppNameList = (value: string | string[] | undefined): string[] =>
220
+ (Array.isArray(value) ? value : value === undefined ? [] : [value])
221
+ .flatMap((entry) => entry.split(","))
222
+ .map((entry) => entry.trim())
223
+ .filter(Boolean);
224
+
225
+ /**
226
+ * The `Apps` token: named apps, `all`, or a checkbox when the command line names none. Kept out of
227
+ * `getInternalArgumentValue` because it is the only internal arg whose positional is variadic, so it is
228
+ * the only one whose raw value is an array.
229
+ */
230
+ export const getAppsArgumentValue = async (
231
+ value: string | string[] | undefined,
232
+ workspace: WorkspaceExecutor,
233
+ ): Promise<AppExecutor[]> => {
234
+ const appNames = await workspace.getApps();
235
+ if (appNames.length === 0) throw new Error("No apps found in this workspace (apps/<appName>/akan.config.ts)");
236
+ const requested = parseAppNameList(value);
237
+ if (requested.includes("all")) return appNames.map((name) => AppExecutor.from(workspace, name));
238
+ if (requested.length) {
239
+ const unknown = requested.filter((name) => !appNames.includes(name));
240
+ if (unknown.length)
241
+ throw new Error(
242
+ `Unknown app${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Available: ${appNames.join(", ")}, all`,
243
+ );
244
+ return [...new Set(requested)].map((name) => AppExecutor.from(workspace, name));
245
+ }
246
+ if (appNames.length === 1 && appNames[0]) return [AppExecutor.from(workspace, appNames[0])];
247
+ const remembered = new Set(await AppSelectionMemory.read(workspace.workspaceRoot));
248
+ const picked = await checkbox<string>({
249
+ message: "Select the apps to run (space to toggle, enter to confirm)",
250
+ choices: appNames.map((name) => ({ name, value: name, checked: remembered.has(name) })),
251
+ // Enter with nothing ticked is a mis-keypress far more often than an intent to run nothing.
252
+ required: true,
253
+ });
254
+ await AppSelectionMemory.write(workspace.workspaceRoot, picked);
255
+ return picked.map((name) => AppExecutor.from(workspace, name));
256
+ };
257
+
211
258
  export const getInternalArgumentValue = async (
212
259
  argMeta: InternalArgMeta,
213
260
  value: string | undefined,
@@ -353,6 +400,11 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
353
400
  `[sys-name:module-name]`,
354
401
  `${argMeta.type} in this workspace (apps|libs)/<sys-name>/lib/<module-name>`,
355
402
  );
403
+ } else if (argMeta.type === "Apps") {
404
+ programCommand = programCommand.argument(
405
+ `[apps...]`,
406
+ `apps in this workspace apps/<appName>, or all (space- or comma-separated; omit to pick interactively)`,
407
+ );
356
408
  } else {
357
409
  const sysType = argMeta.type.toLowerCase();
358
410
  programCommand = programCommand.argument(
@@ -381,6 +433,11 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
381
433
  commandArgs[argMeta.idx] = await getOptionValue(argMeta, opt, commandContext);
382
434
  else if (argMeta.type === "Argument")
383
435
  commandArgs[argMeta.idx] = await getArgumentValue(argMeta, cmdArgs[argMeta.idx] as string);
436
+ else if (argMeta.type === "Apps")
437
+ commandArgs[argMeta.idx] = await getAppsArgumentValue(
438
+ cmdArgs[argMeta.idx] as string | string[] | undefined,
439
+ workspace,
440
+ );
384
441
  else
385
442
  commandArgs[argMeta.idx] = await getInternalArgumentValue(
386
443
  argMeta as InternalArgMeta,
@@ -390,6 +447,12 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
390
447
  // set app name to env
391
448
  if (commandArgs[argMeta.idx] instanceof AppExecutor)
392
449
  process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
450
+ //? Only when exactly one app resolved. Publishing a name while several are in play would make
451
+ //? every env-derived answer in this process belong to whichever app happened to be last.
452
+ else if (Array.isArray(commandArgs[argMeta.idx])) {
453
+ const apps = commandArgs[argMeta.idx] as AppExecutor[];
454
+ if (apps.length === 1 && apps[0]) process.env.AKAN_PUBLIC_APP_NAME = apps[0].name;
455
+ }
393
456
  assignCommandContext(commandContext, argMeta, commandArgs[argMeta.idx]);
394
457
  if ((opt as { verbose?: boolean }).verbose) Executor.setVerbose(true);
395
458
  }
@@ -159,6 +159,7 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
159
159
  .map((arg) => {
160
160
  if (arg.type === "Workspace") return "";
161
161
  if (arg.type === "Module") return "[sys:module]";
162
+ if (arg.type === "Apps") return "[apps...]";
162
163
  if (arg.type === "Argument") {
163
164
  return `[${camelToKebabCase(arg.name)}]`;
164
165
  }
@@ -192,6 +193,9 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
192
193
  } else if (arg.type === "Module") {
193
194
  argName = "sys:module";
194
195
  argDesc = "Module in format: app-name:module-name or lib-name:module-name";
196
+ } else if (arg.type === "Apps") {
197
+ argName = "apps...";
198
+ argDesc = "App names, space- or comma-separated, or all. Omit to pick them interactively";
195
199
  } else {
196
200
  argName = arg.type.toLowerCase();
197
201
  argDesc = `${arg.type} name in this workspace`;
package/executors.ts CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  } from "./agentsIndex";
42
42
  import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
43
43
  import { getRootBoundarySegments, isRootBoundarySegments } from "./artifact/implicitRootLayout";
44
+ import { CodegenLock } from "./codegenLock";
44
45
  import { FileSys } from "./fileSys";
45
46
  import { getDirname } from "./getDirname";
46
47
  import { Linter } from "./linter";
@@ -1136,18 +1137,22 @@ export class SysExecutor extends Executor {
1136
1137
  : await LibInfo.fromExecutor(this as unknown as LibExecutor, {
1137
1138
  refresh,
1138
1139
  });
1139
- if (write) {
1140
- await Promise.all(this.#getScanTemplateTasks(scanInfo));
1141
- await this.writeJson(`akan.${this.type}.json`, scanInfo.getScanResult());
1142
- if (this.type === "lib") this.#updateDependencies(scanInfo);
1140
+ //* `writeLib` regenerates every dependency lib's barrels, and each mounting app's `akan start`
1141
+ //* regenerates the same ones — so this region races the other dev servers in the workspace and the
1142
+ //* builders that watch what it writes.
1143
+ if (write)
1144
+ await CodegenLock.run(this.workspace.workspaceRoot, `scan:${this.name}`, async () => {
1145
+ await Promise.all(this.#getScanTemplateTasks(scanInfo));
1146
+ await this.writeJson(`akan.${this.type}.json`, scanInfo.getScanResult());
1147
+ if (this.type === "lib") this.#updateDependencies(scanInfo);
1143
1148
 
1144
- if (writeLib) {
1145
- const libInfos = [...scanInfo.getLibInfos().values()];
1146
- await this.#updateDependencies(scanInfo);
1147
- await Promise.all(libInfos.flatMap((libInfo) => libInfo.exec.#getScanTemplateTasks(libInfo)));
1148
- }
1149
- await this.syncAgentsIndex(scanInfo);
1150
- }
1149
+ if (writeLib) {
1150
+ const libInfos = [...scanInfo.getLibInfos().values()];
1151
+ await this.#updateDependencies(scanInfo);
1152
+ await Promise.all(libInfos.flatMap((libInfo) => libInfo.exec.#getScanTemplateTasks(libInfo)));
1153
+ }
1154
+ await this.syncAgentsIndex(scanInfo);
1155
+ });
1151
1156
  this.#scanInfo = scanInfo;
1152
1157
  return scanInfo;
1153
1158
  }
@@ -1405,6 +1410,8 @@ export class AppExecutor extends SysExecutor {
1405
1410
  const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
1406
1411
  const routeEnv = {
1407
1412
  AKAN_PUBLIC_BASE_PATHS: [...akanConfig.basePaths].join(","),
1413
+ AKAN_PUBLIC_API_PREFIX: akanConfig.api.prefix,
1414
+ AKAN_PUBLIC_WS_PREFIX: akanConfig.api.websocketPrefix,
1408
1415
  AKAN_DATABASE_MODE: databaseMode,
1409
1416
  };
1410
1417
  Object.assign(process.env, routeEnv);
package/fileSys.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { lstat, stat } from "node:fs/promises";
1
+ import { lstat, rename, rm, stat } from "node:fs/promises";
2
2
  import { Logger } from "akanjs/common";
3
3
 
4
4
  export class FileSys {
@@ -39,6 +39,24 @@ export class FileSys {
39
39
  static async writeText(path: string, content: string) {
40
40
  return await Bun.file(path).write(content);
41
41
  }
42
+ /**
43
+ * Replaces a file in one `rename`, so a concurrent reader sees either the old bytes or the new ones.
44
+ * Generated barrels are written by every dev server watching the same `libs/` tree while those same
45
+ * servers' watchers are reading them, and a half-written barrel reads back as a user edit.
46
+ *
47
+ * The temp file is a sibling because `rename` is only atomic within a filesystem, and it lands in a
48
+ * watched directory — the `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
49
+ */
50
+ static async writeTextAtomic(filePath: string, content: string) {
51
+ const temp = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
52
+ try {
53
+ await Bun.write(temp, content);
54
+ await rename(temp, filePath);
55
+ } catch (error) {
56
+ await rm(temp, { force: true }).catch(() => undefined);
57
+ throw error;
58
+ }
59
+ }
42
60
  static async writeJson(path: string, content: object) {
43
61
  return await Bun.file(path).write(`${JSON.stringify(content, null, 2)}\n`);
44
62
  }
@@ -1,6 +1,7 @@
1
- import { readdir, readFile, stat, writeFile } from "node:fs/promises";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import ts from "typescript";
4
+ import { FileSys } from "../fileSys";
4
5
 
5
6
  //* Auto-import daemon: a source-editing sibling of DevGeneratedIndexSync. When a domain file changes,
6
7
  //* framework symbols that are used but not imported (e.g. `Int` in a *.constant.ts, `fetch` in a *.store.ts)
@@ -267,7 +268,7 @@ export class AutoImportSync {
267
268
  const resolveExtra = await this.#extraResolverFor(abs, ctx);
268
269
  const next = transformSource(source, abs, ctx, resolveExtra);
269
270
  if (next === null || next === source) return false;
270
- await writeFile(abs, next);
271
+ await FileSys.writeTextAtomic(abs, next);
271
272
  return true;
272
273
  }
273
274
 
@@ -1,5 +1,6 @@
1
- import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { FileSys } from "../fileSys";
3
4
 
4
5
  const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
5
6
  const FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
@@ -101,7 +102,7 @@ export class DevGeneratedIndexSync {
101
102
  const current = await readFile(indexPath, "utf8").catch(() => null);
102
103
  if (current === content) return false;
103
104
  await mkdir(dir, { recursive: true });
104
- await writeFile(indexPath, content);
105
+ await FileSys.writeTextAtomic(indexPath, content);
105
106
  return true;
106
107
  }
107
108
 
@@ -14,11 +14,25 @@ const builderMsgTypeSet = new Set<BuilderMessage["type"]>([
14
14
  "build-status",
15
15
  "builder-metrics",
16
16
  ]);
17
+ /**
18
+ * Where a dev child process writes. `"pipe"` is not only about keeping a TUI's frame clean: a Bun child
19
+ * that inherits the controlling terminal snapshots its termios at spawn and writes that snapshot back
20
+ * when it exits, so a builder spawned while something holds the terminal raw turns it raw again on the
21
+ * first recycle. See the XXX comment on `Spinner.oraOptions`.
22
+ */
23
+ export type DevStdioMode = "inherit" | "pipe";
24
+
17
25
  interface IncrementalBuilderHostOptions {
18
26
  app: App;
19
27
  entry: string;
20
28
  env: Record<string, string>;
29
+ stdio?: DevStdioMode;
21
30
  onMessage: (message: BuilderMessage) => void;
31
+ /**
32
+ * Required when `stdio` is `"pipe"`: an undrained pipe fills and blocks the builder's next write.
33
+ * Text arrives as decoded chunks, not lines — a consumer that needs lines splits them itself.
34
+ */
35
+ onOutput?: (kind: "stdout" | "stderr", text: string) => void;
22
36
  }
23
37
 
24
38
  /**
@@ -63,7 +77,9 @@ export class IncrementalBuilderHost {
63
77
  app: App;
64
78
  ready = false;
65
79
  readonly #onMessage: (message: BuilderMessage) => void;
66
- #proc: Bun.Subprocess<"ignore", "inherit", "inherit"> | null = null;
80
+ readonly #stdio: DevStdioMode;
81
+ readonly #onOutput: ((kind: "stdout" | "stderr", text: string) => void) | null;
82
+ #proc: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe"> | null = null;
67
83
  #status: IncrementalBuilderStatus = "stopped";
68
84
  #restartAttempts = 0;
69
85
  #restartTimer: ReturnType<typeof setTimeout> | null = null;
@@ -83,11 +99,13 @@ export class IncrementalBuilderHost {
83
99
  */
84
100
  readonly #inFlight = new Map<number, "build-route" | "build-csr">();
85
101
  #startOptions: IncrementalBuilderStartOptions = {};
86
- constructor({ app, entry, env, onMessage }: IncrementalBuilderHostOptions) {
102
+ constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }: IncrementalBuilderHostOptions) {
87
103
  this.app = app;
88
104
  this.entry = entry;
89
105
  this.env = env;
106
+ this.#stdio = stdio;
90
107
  this.#onMessage = onMessage;
108
+ this.#onOutput = onOutput ?? null;
91
109
  }
92
110
  get status() {
93
111
  return this.#status;
@@ -115,11 +133,11 @@ export class IncrementalBuilderHost {
115
133
  // the flag is what tells it to re-announce what it booted with.
116
134
  const afterRecycle = this.#spawnAfterRecycle;
117
135
  this.#spawnAfterRecycle = false;
118
- let proc!: Bun.Subprocess<"ignore", "inherit", "inherit">;
136
+ let proc!: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe">;
119
137
  proc = Bun.spawn(["bun", this.entry], {
120
138
  cwd: this.app.cwdPath,
121
139
  env: { ...this.env, AKAN_WATCH: "1", ...(afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {}) },
122
- stdio: ["ignore", "inherit", "inherit"],
140
+ stdio: ["ignore", this.#stdio, this.#stdio],
123
141
  ipc: (msg: BuilderMessage) => {
124
142
  if (this.#proc !== proc) return;
125
143
  if (!msg || typeof msg !== "object") return;
@@ -167,8 +185,25 @@ export class IncrementalBuilderHost {
167
185
  },
168
186
  });
169
187
  this.#proc = proc;
188
+ // Re-attached per spawn, not once per host: a recycle or a crash-restart brings new pipes.
189
+ if (this.#stdio === "pipe" && this.#onOutput) {
190
+ void this.#drain(proc.stdout as unknown as ReadableStream<Uint8Array> | undefined, "stdout");
191
+ void this.#drain(proc.stderr as unknown as ReadableStream<Uint8Array> | undefined, "stderr");
192
+ }
170
193
  this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
171
194
  }
195
+ async #drain(stream: ReadableStream<Uint8Array> | undefined | null, kind: "stdout" | "stderr") {
196
+ if (!stream) return;
197
+ const decoder = new TextDecoder();
198
+ try {
199
+ for await (const chunk of stream) {
200
+ const text = decoder.decode(chunk, { stream: true });
201
+ if (text) this.#onOutput?.(kind, text);
202
+ }
203
+ } catch {
204
+ // The stream closes when the builder exits; nothing further to surface here.
205
+ }
206
+ }
172
207
  #scheduleRestart() {
173
208
  if (this.#manualStop || this.#restartTimer) return;
174
209
  this.#status = "restarting";
@@ -278,7 +313,15 @@ export class IncrementalBuilderHost {
278
313
  this.ready = false;
279
314
  this.#status = "stopped";
280
315
  }
281
- static async create(app: App, env: Record<string, string>, onMessage: (message: BuilderMessage) => void) {
316
+ static async create(
317
+ app: App,
318
+ env: Record<string, string>,
319
+ onMessage: (message: BuilderMessage) => void,
320
+ {
321
+ stdio = "inherit",
322
+ onOutput,
323
+ }: { stdio?: DevStdioMode; onOutput?: (kind: "stdout" | "stderr", text: string) => void } = {},
324
+ ) {
282
325
  const candidates = [
283
326
  path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
284
327
  path.join(
@@ -289,7 +332,8 @@ export class IncrementalBuilderHost {
289
332
  path.join(import.meta.dir, "incrementalBuilder.proc.ts"),
290
333
  ];
291
334
  for (const c of candidates)
292
- if (await Bun.file(c).exists()) return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
335
+ if (await Bun.file(c).exists())
336
+ return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
293
337
  throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
294
338
  }
295
339
  }
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  // barrel reaches `cssCompiler`/`ssrBaseArtifactBuilder`, which pull tailwindcss + @tailwindcss/node
5
5
  // (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
6
6
  // into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
7
+ import { CodegenLock } from "@akanjs/devkit/codegenLock";
7
8
  import type { App } from "@akanjs/devkit/commandDecorators";
8
9
  import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
9
10
  import { AutoImportSync } from "@akanjs/devkit/frontendBuild/autoImportSync";
@@ -299,11 +300,17 @@ class IncrementalBuilder {
299
300
  //* Insert framework imports that are used but omitted (e.g. `Int` in *.constant.ts, `fetch` in
300
301
  //* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
301
302
  //* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
302
- const autoImport = await this.#autoImportSync.syncForBatch(batch.files);
303
+ const [autoImport, indexSync] = await CodegenLock.run(
304
+ this.#app.workspace.workspaceRoot,
305
+ `hmr-batch:${this.#app.name}`,
306
+ async () => {
307
+ const auto = await this.#autoImportSync.syncForBatch(batch.files);
308
+ return [auto, await this.#generatedIndexSync.syncForBatch(batch.files)] as const;
309
+ },
310
+ );
303
311
  for (const error of autoImport.errors) this.#logger.error(error);
304
312
  if (autoImport.changedFiles.length > 0)
305
313
  this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
306
- const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
307
314
  //* Both passes above write source files, and this generation's build consumes what they wrote. Hand
308
315
  //* them to the watcher so its verification scan does not read them back as a user edit and spend a
309
316
  //* second generation rebuilding identical content.
package/index.ts CHANGED
@@ -45,7 +45,5 @@ export type * from "./spinner";
45
45
  export type * from "./transforms";
46
46
  export type * from "./typeChecker";
47
47
  export type * from "./types";
48
- export type * from "./ui";
49
48
  export type * from "./uploadRelease";
50
- export type * from "./useStdoutDimensions";
51
49
  export type * from "./workflow";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-alpha.87",
3
+ "version": "3.0.0-alpha.89",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -33,7 +33,6 @@
33
33
  "./incrementalBuilder": "./incrementalBuilder/index.ts",
34
34
  "./mobile": "./mobile/index.ts",
35
35
  "./transforms": "./transforms/index.ts",
36
- "./ui": "./ui/index.ts",
37
36
  "./workflow": "./workflow/index.ts",
38
37
  "./*.ts": "./*.ts",
39
38
  "./*": "./*.ts"
@@ -45,14 +44,13 @@
45
44
  "@langchain/openai": "^1.4.6",
46
45
  "@tailwindcss/node": "^4.3.0",
47
46
  "@trapezedev/project": "^7.1.4",
48
- "akanjs": "3.0.0-alpha.87",
47
+ "akanjs": "3.0.0-alpha.89",
49
48
  "chalk": "^5.6.2",
50
49
  "commander": "^14.0.3",
51
50
  "dayjs": "^1.11.20",
52
51
  "fontaine": "^0.8.0",
53
52
  "fonteditor-core": "^2.6.3",
54
53
  "ignore": "^7.0.5",
55
- "ink": "^6.8.0",
56
54
  "ora": "^9.4.0",
57
55
  "ssh2": "^1.17.0",
58
56
  "subset-font": "^2.5.0",