@akanjs/cli 3.0.0-beta.19 → 3.0.0-beta.20

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 (95) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-6cm97kay.js → CodeAgent-awv258wb.js} +2 -2
  3. package/{CodeTui-za4jsm41.js → CodeTui-1nqh35ez.js} +20 -22
  4. package/TunnelShare-scf4e4pd.js +9 -0
  5. package/{agent.command-e0wpbbyg.js → agent.command-1s9ag7sb.js} +8 -9
  6. package/akanCode.js +4 -4
  7. package/{application.command-8wyxqc8q.js → application.command-49hwy6va.js} +25 -56
  8. package/{applicationBuildRunner-n9gzaxd0.js → applicationBuildRunner-ybyp97pv.js} +33 -59
  9. package/{applicationReleasePackager-vpggfqgq.js → applicationReleasePackager-esnrphw8.js} +40 -60
  10. package/buildBatch.proc.js +19 -24
  11. package/{capacitorApp-q3z0qht0.js → capacitorApp-1synz4en.js} +3 -4
  12. package/{cloud.command-4zwa5dsx.js → cloud.command-d7rqtzt9.js} +16 -26
  13. package/cloud.script-bkbpcr96.js +19 -0
  14. package/{code.command-468c9d7s.js → code.command-nn48xzej.js} +22 -37
  15. package/context.command-s3mcmvpa.js +48 -0
  16. package/context.runner-k99nb5pk.js +21 -0
  17. package/{dependencyScanner-0z2h2tks.js → dependencyScanner-ye2e6pdz.js} +2 -2
  18. package/{devTui-ffjsarqa.js → devTui-q7k7bakd.js} +5 -9
  19. package/fileSys-5ngz93ga.js +9 -0
  20. package/{guideline.command-vhw13pqr.js → guideline.command-sct4qnt5.js} +4 -5
  21. package/guidelines/conventions/conventions.instruction.md +5 -1
  22. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  23. package/incrementalBuilder.proc.js +30 -48
  24. package/{index-s5rhm04y.js → index-0bq14vr1.js} +1 -1
  25. package/{index-10dkrrww.js → index-13ffe9wp.js} +4 -4
  26. package/{index-srmay5jw.js → index-22d57v4c.js} +84 -67
  27. package/{index-53176s7j.js → index-31yr441d.js} +10 -17
  28. package/index-3hx1z96c.js +87 -0
  29. package/{index-4m929vvn.js → index-3sq321ak.js} +10 -18
  30. package/{index-bhtmj233.js → index-3t7g9e4r.js} +41 -78
  31. package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
  32. package/{index-47pj3aee.js → index-5e0n4hqx.js} +11 -12
  33. package/{index-7ep8qtvp.js → index-5pc1qrbv.js} +5 -8
  34. package/{index-esk82rvk.js → index-8mna0947.js} +34 -71
  35. package/{index-sq2dzkzm.js → index-8sf7wmdr.js} +6 -6
  36. package/{index-crmeqnmf.js → index-9d9bgcx4.js} +283 -722
  37. package/{index-bwf965h6.js → index-ahemdr5n.js} +6 -6
  38. package/{index-ab0c38ea.js → index-as7xmyz8.js} +20 -20
  39. package/{index-bc88j1ry.js → index-axzjaqve.js} +94 -142
  40. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  41. package/{index-667w0yah.js → index-c6eggbee.js} +2 -5
  42. package/{index-a1w6934z.js → index-ddxvdd1p.js} +169 -305
  43. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  44. package/{index-22xtgmhn.js → index-dzf3r49a.js} +1 -1
  45. package/{index-89hy31kf.js → index-fgq75haa.js} +172 -287
  46. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  47. package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
  48. package/{index-vqx8bdxz.js → index-mxav6j3c.js} +78 -231
  49. package/{index-2446k7b4.js → index-myhc3c1j.js} +512 -588
  50. package/{index-3jrp517k.js → index-nv5976x3.js} +45 -128
  51. package/{index-zqgj2rt2.js → index-przybvy5.js} +69 -147
  52. package/{index-n3cv0tm4.js → index-smme4r47.js} +1 -1
  53. package/{index-qdgzsnhg.js → index-w2zdcw3k.js} +110 -231
  54. package/{index-14zrtgfa.js → index-wa5jbxj1.js} +6 -39
  55. package/{index-bftkz6cp.js → index-ww4z397j.js} +198 -555
  56. package/{index-kwaq3n9n.js → index-xqjwd384.js} +4 -7
  57. package/{index-txr7dj2g.js → index-xqpzbab5.js} +106 -240
  58. package/{index-e4v6vhxq.js → index-xsjh6nzh.js} +178 -165
  59. package/{index-18zybgkc.js → index-ygn77a5w.js} +47 -99
  60. package/{index-9d6nvkz2.js → index-ztp3e73t.js} +79 -35
  61. package/index.js +23 -24
  62. package/{library.command-cqr175dy.js → library.command-1gx04jgn.js} +4 -5
  63. package/{localRegistry.command-49z55qqz.js → localRegistry.command-zndxj1q6.js} +21 -42
  64. package/{module.command-9p8mm3b0.js → module.command-3waddk95.js} +6 -7
  65. package/{package.command-wxyz909t.js → package.command-zrc7br1y.js} +5 -6
  66. package/package.json +2 -2
  67. package/{page.command-gsrsfqqs.js → page.command-vpjgardh.js} +4 -5
  68. package/{primitive.command-ns1mptje.js → primitive.command-sz84tk4t.js} +9 -20
  69. package/{prompter-1f8d9pcc.js → prompter-ptaa3nk0.js} +2 -2
  70. package/{quality.command-e76k2q9k.js → quality.command-g869hafs.js} +129 -363
  71. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  72. package/{repair.command-pkrsm3cw.js → repair.command-chsnjvvv.js} +11 -35
  73. package/{scalar.command-0cpt3fc8.js → scalar.command-2v21newx.js} +5 -6
  74. package/skills/akan-code/SKILL.md +12 -4
  75. package/{subspace.command-fktpwp0g.js → subspace.command-ckcbwfp1.js} +39 -58
  76. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +3 -3
  77. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +3 -3
  78. package/templates/client.ts +7 -30
  79. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  80. package/templates/lib/__lib/lib.document.ts +0 -2
  81. package/templates/lib/sig.ts +8 -30
  82. package/templates/server.ts +0 -2
  83. package/{tunnel.command-d5jy9gk8.js → tunnel.command-kd2yr4fx.js} +5 -6
  84. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  85. package/typecheck.proc.js +1 -1
  86. package/workflow.command-44252cqv.js +79 -0
  87. package/{workspace.command-gv84eh1p.js → workspace.command-fqcztt5p.js} +72 -107
  88. package/TunnelShare-2j6s511d.js +0 -9
  89. package/cloud.script-fwx3vb34.js +0 -20
  90. package/context.command-dsnw0jby.js +0 -80
  91. package/context.runner-rs4rj15d.js +0 -22
  92. package/devTuiView-1q1mdgrv.js +0 -9
  93. package/getDirname-dr3jz1xg.js +0 -7
  94. package/index-6wwzs1bc.js +0 -7
  95. package/workflow.command-6tkcysmx.js +0 -105
@@ -216,6 +216,8 @@ Do not narrate code. Do document the thing the code cannot say. Both halves are
216
216
  ## Client / Server Boundaries (`apps/**`, `libs/**`, `pkgs/akanjs/**`)
217
217
 
218
218
  - Use `"use client";` at the top of client component files.
219
+ - A `"use client"` file re-exports by name (`export { A, B } from "./x"`), never with `export *`: the server knows a
220
+ client module only by the export names it spells out, so `akan start` and `akan build` stop on a star re-export.
219
221
  - Be careful when importing client-only code from page or layout modules.
220
222
  - Keep page props serializable unless the existing route pattern clearly allows otherwise.
221
223
  - In domain UI the boundary is mechanical, not a judgment call: `Template`, `Zone`, and `Util` are always client components with `"use client"` on line 1; `Unit` and `View` are always server components and never carry the directive.
@@ -679,7 +681,9 @@ it.
679
681
  (RFC 7591; Cursor's `cursor://` redirect admitted) plus the consent page `libs/shared/page/oauth/consent` — and
680
682
  names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
681
683
  `aud`. A token is the app's own access JWT plus `aud`/`iss`/`client_id`, so `AccountMiddleware` and the guards
682
- judge it unchanged; there is no scope. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
684
+ judge it unchanged. The tokens this built-in server mints carry no scope; `auth.scopes` / `AKAN_MCP_SCOPES` are for
685
+ tokens an external issuer mints, and declared beside this server they refuse every one of its tokens with 403
686
+ `insufficient_scope`. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
683
687
  `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `accessTokenSeconds`,
684
688
  `clientIdMetadata`, `enabled`, plus `issuer` / `resource` — set those when a tunnel or an edge makes the derived
685
689
  origin wrong, because an MCP client compares the issuer byte for byte). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` (or
@@ -189,11 +189,13 @@ export default page()
189
189
  - OAuth resource metadata is published at `/.well-known/oauth-protected-resource` (and at that path plus the mount
190
190
  path, the spelling most clients try first). `auth.authorizationServers` / `AKAN_MCP_AUTH_SERVERS`,
191
191
  `auth.scopes` / `AKAN_MCP_SCOPES`, `auth.resource` / `AKAN_MCP_RESOURCE` and `auth.verify` configure it;
192
- `insufficient_scope` is enforced only once scopes are declared. Naming an issuer changes three things at once: a
193
- request with no credential is answered 401 from `initialize` on (an MCP client starts its OAuth flow on nothing
194
- else), a token carrying no `aud` is refused (that issuer mints for its other resources too), and the challenge
195
- carries no `error` code (RFC 6750 §3.1 reserves it for a credential that was presented). A server naming no
196
- issuer keeps anonymous access and accepts a first-party token, which is bound by app and environment instead.
192
+ `insufficient_scope` is enforced only once scopes are declared. Declare them only for tokens an external issuer
193
+ mints: the built-in authorization server's tokens carry no scope, so beside it every one of them would be refused
194
+ with 403. Naming an issuer changes three things at once: a request with no credential is answered 401 from
195
+ `initialize` on (an MCP client starts its OAuth flow on nothing else), a token carrying no `aud` is refused (that
196
+ issuer mints for its other resources too), and the challenge carries no `error` code (RFC 6750 §3.1 reserves it
197
+ for a credential that was presented). A server naming no issuer keeps anonymous access and accepts a first-party
198
+ token, which is bound by app and environment instead.
197
199
  - **The boot log names every published entry with no dictionary `.desc()`.** An agent picks a tool by its
198
200
  description, so a missing one is a broken tool. What the framework generates has no text of its own and borrows
199
201
  the model's: the generated list reads the `.of()` label, and the base CRUD tools append the model's `.desc()` to
@@ -1,22 +1,22 @@
1
1
  // @bun
2
+ import {
3
+ ApplicationBuildReporter,
4
+ HmrWatcher,
5
+ WatchRootResolver
6
+ } from "./index-khpfpcsd.js";
2
7
  import {
3
8
  CodegenLock,
4
9
  WorkspaceExecutor,
5
10
  AppExecutor
6
- } from "./index-crmeqnmf.js";
7
- import"./index-q0y2jq9j.js";
8
- import"./index-6wwzs1bc.js";
11
+ } from "./index-9d9bgcx4.js";
12
+ import"./index-5b6tavv3.js";
9
13
  import {
10
14
  GraphClientEntryDiscovery,
11
15
  RouteClientBuilder,
12
16
  AutoImportSync,
13
17
  DevChangePlanner,
14
18
  DevGeneratedIndexSync
15
- } from "./index-89hy31kf.js";
16
- import {
17
- HmrWatcher,
18
- WatchRootResolver
19
- } from "./index-hwkygn9s.js";
19
+ } from "./index-fgq75haa.js";
20
20
  import {
21
21
  prepareDevWatchBatch
22
22
  } from "./index-a5vmm6x4.js";
@@ -30,12 +30,6 @@ import path from "path";
30
30
  import { Logger } from "akanjs/common";
31
31
 
32
32
  class BuildBatchRunner {
33
- static #entryCandidates = (workspaceRoot) => [
34
- path.join(workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
35
- path.join(workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
36
- path.join(import.meta.dir, "buildBatch.proc.js"),
37
- path.join(import.meta.dir, "buildBatch.proc.ts")
38
- ];
39
33
  #logger = new Logger("BuildBatchRunner");
40
34
  #entry = null;
41
35
  #workspaceRoot;
@@ -47,7 +41,12 @@ class BuildBatchRunner {
47
41
  async#resolveEntry() {
48
42
  if (this.#entry)
49
43
  return this.#entry;
50
- const candidates = BuildBatchRunner.#entryCandidates(this.#workspaceRoot);
44
+ const candidates = [
45
+ path.join(this.#workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
46
+ path.join(this.#workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
47
+ path.join(import.meta.dir, "buildBatch.proc.js"),
48
+ path.join(import.meta.dir, "buildBatch.proc.ts")
49
+ ];
51
50
  for (const candidate of candidates) {
52
51
  if (!await Bun.file(candidate).exists())
53
52
  continue;
@@ -196,7 +195,7 @@ class IncrementalBuilder {
196
195
  }
197
196
  };
198
197
  } catch (err) {
199
- const errMsg = err instanceof Error ? err.message : String(err);
198
+ const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
200
199
  this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
201
200
  this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
202
201
  return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
@@ -336,20 +335,19 @@ class IncrementalBuilder {
336
335
  }, 150);
337
336
  }
338
337
  async installWatcher() {
339
- const artifactDir = this.#artifactDir;
340
338
  const roots = await new WatchRootResolver(this.#app).resolve();
341
339
  const watcher = new HmrWatcher({
342
340
  roots,
343
341
  logger: this.#logger,
344
342
  onBatch: async (batch) => {
345
- await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(artifactDir, batch));
343
+ await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(batch));
346
344
  }
347
345
  });
348
346
  await watcher.start();
349
347
  this.#watcher = watcher;
350
348
  this.#logger.verbose(`watching ${roots.length} roots`);
351
349
  }
352
- async#handleWatchBatch(artifactDir, batch) {
350
+ async#handleWatchBatch(batch) {
353
351
  const rawKinds = new Set(batch.kinds);
354
352
  if (rawKinds.size === 0)
355
353
  return;
@@ -402,7 +400,7 @@ class IncrementalBuilder {
402
400
  }
403
401
  const needs = [];
404
402
  if (kinds.includes("code") && rebuildClient) {
405
- if (this.#shouldRebuildCsr())
403
+ if (this.#csrActive)
406
404
  needs.push("csr");
407
405
  else
408
406
  this.#logger.verbose(`csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`);
@@ -417,13 +415,10 @@ class IncrementalBuilder {
417
415
  this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
418
416
  }
419
417
  }
420
- async#runBatch({
421
- generation,
422
- needs,
423
- changedFiles
424
- }) {
418
+ async#runBatch(work) {
419
+ const { generation, needs, changedFiles } = work;
425
420
  const started = Date.now();
426
- const result = await this.#batchRunner.run(await this.#batchRequest({ generation, needs, changedFiles }), (msg) => BuilderChannel.emit(msg));
421
+ const result = await this.#batchRunner.run(await this.#batchRequest(work), (msg) => BuilderChannel.emit(msg));
427
422
  if (result.optimizedFonts)
428
423
  this.#optimizedFonts = result.optimizedFonts;
429
424
  if (result.cssAssets)
@@ -437,11 +432,7 @@ class IncrementalBuilder {
437
432
  this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
438
433
  return result;
439
434
  }
440
- async#batchRequest({
441
- generation,
442
- needs,
443
- changedFiles
444
- }) {
435
+ async#batchRequest({ generation, needs, changedFiles }) {
445
436
  return {
446
437
  appName: this.#app.name,
447
438
  workspaceRoot: this.#app.workspace.workspaceRoot,
@@ -506,9 +497,6 @@ class IncrementalBuilder {
506
497
  await BuilderChannel.send({ type: "build-csr-res", id: msg.id, ok: true });
507
498
  });
508
499
  }
509
- #shouldRebuildCsr() {
510
- return this.#csrActive;
511
- }
512
500
  static #csrArmedByEnv() {
513
501
  return process.env.AKAN_DEV_CSR_REBUILD === "1";
514
502
  }
@@ -607,23 +595,17 @@ class IncrementalBuilder {
607
595
  builder.shutdown(msg.reason);
608
596
  return;
609
597
  }
610
- if (msg.type === "build-route") {
598
+ if (msg.type !== "build-route" && msg.type !== "build-csr")
599
+ return;
600
+ if (!builder || builder.shuttingDown) {
611
601
  const error = builder?.shuttingDown ? recyclingError : bootingError;
612
- if (!builder || builder.shuttingDown) {
613
- BuilderChannel.emit({ type: "build-route-res", id: msg.id, ok: false, error });
614
- return;
615
- }
616
- builder.handleBuildRoute(msg);
602
+ BuilderChannel.emit({ type: `${msg.type}-res`, id: msg.id, ok: false, error });
617
603
  return;
618
604
  }
619
- if (msg.type === "build-csr") {
620
- const error = builder?.shuttingDown ? recyclingError : bootingError;
621
- if (!builder || builder.shuttingDown) {
622
- BuilderChannel.emit({ type: "build-csr-res", id: msg.id, ok: false, error });
623
- return;
624
- }
605
+ if (msg.type === "build-route")
606
+ builder.handleBuildRoute(msg);
607
+ else
625
608
  builder.handleBuildCsr(msg);
626
- }
627
609
  });
628
610
  process.on("disconnect", () => {
629
611
  logger.warn("host IPC channel closed; exiting builder");
@@ -648,7 +630,7 @@ class IncrementalBuilder {
648
630
  await builder.rearmCsrFromEnv();
649
631
  }
650
632
  }
651
- IncrementalBuilder.main().catch((err) => {
633
+ await IncrementalBuilder.main().catch((err) => {
652
634
  console.error(err);
653
635
  process.exit(1);
654
636
  });
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  getDirname2
4
- } from "./index-6wwzs1bc.js";
4
+ } from "./index-5b6tavv3.js";
5
5
 
6
6
  // pkgs/@akanjs/devkit/prompter.ts
7
7
  import fsPromise from "fs/promises";
@@ -1,16 +1,16 @@
1
1
  // @bun
2
2
  import {
3
3
  jsonText
4
- } from "./index-2446k7b4.js";
4
+ } from "./index-myhc3c1j.js";
5
5
  import {
6
6
  akanMcpInstallTargets
7
- } from "./index-18zybgkc.js";
7
+ } from "./index-ygn77a5w.js";
8
8
  import {
9
9
  script
10
- } from "./index-qdgzsnhg.js";
10
+ } from "./index-w2zdcw3k.js";
11
11
  import {
12
12
  ContextRunner2
13
- } from "./index-3jrp517k.js";
13
+ } from "./index-nv5976x3.js";
14
14
 
15
15
  // pkgs/@akanjs/cli/context/context.script.ts
16
16
  import { Logger } from "akanjs/common";
@@ -1,15 +1,19 @@
1
1
  // @bun
2
2
  import {
3
- FileSys
4
- } from "./index-q0y2jq9j.js";
3
+ FileSys2
4
+ } from "./index-5b6tavv3.js";
5
+ import {
6
+ ConfigLock
7
+ } from "./index-3hx1z96c.js";
5
8
 
6
9
  // pkgs/@akanjs/devkit/cloud/globalConfig.ts
7
- import { chmod, mkdir } from "fs/promises";
10
+ import { mkdir, rename, rm, writeFile } from "fs/promises";
8
11
  import dayjs from "dayjs";
9
12
 
10
13
  // pkgs/@akanjs/devkit/cloud/constants.ts
11
- var basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
12
- var configPath = `${basePath}/config.json`;
14
+ import path from "path";
15
+ var basePath = path.join(Bun.env.HOME ?? Bun.env.USERPROFILE ?? "", ".akan");
16
+ var configPath = path.join(basePath, "config.json");
13
17
  var getDefaultHostConfig = (host = GlobalConfig.akanCloudHost) => ({ host });
14
18
  var defaultAkanGlobalConfig = {
15
19
  cloudHost: {},
@@ -21,8 +25,8 @@ var defaultAkanGlobalConfig = {
21
25
  class GlobalConfig {
22
26
  static akanCloudHost = process.env.USE_AKANJS_PKGS === "true" ? `http://localhost:${process.env.CLOUD_HOST_PORT ?? 8283}` : "https://cloud.akanjs.com";
23
27
  static async#getAkanGlobalConfig() {
24
- const exists = await FileSys.fileExists(configPath);
25
- const akanConfig = exists ? await FileSys.readJson(configPath) : {};
28
+ const exists = await FileSys2.fileExists(configPath);
29
+ const akanConfig = exists ? await FileSys2.readJson(configPath) : {};
26
30
  return {
27
31
  ...defaultAkanGlobalConfig,
28
32
  ...akanConfig,
@@ -33,50 +37,82 @@ class GlobalConfig {
33
37
  }
34
38
  static async#setAkanGlobalConfig(akanConfig) {
35
39
  await mkdir(basePath, { recursive: true, mode: 448 });
36
- await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
37
- await chmod(configPath, 384);
40
+ const temp = `${configPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
41
+ try {
42
+ await writeFile(temp, JSON.stringify(akanConfig, null, 2), { mode: 384 });
43
+ await rename(temp, configPath);
44
+ } catch (error) {
45
+ await rm(temp, { force: true });
46
+ throw error;
47
+ }
48
+ }
49
+ static #lockPath = `${configPath}.lock`;
50
+ static async#update(mutate) {
51
+ await ConfigLock.run(GlobalConfig.#lockPath, async () => {
52
+ await GlobalConfig.#setAkanGlobalConfig(mutate(await GlobalConfig.#getAkanGlobalConfig()));
53
+ });
38
54
  }
39
55
  static async getHostConfig(host = GlobalConfig.akanCloudHost) {
40
56
  const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
41
57
  return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
42
58
  }
43
59
  static async setHostConfig(config = getDefaultHostConfig()) {
44
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
45
- akanConfig.cloudHost[config.host] = GlobalConfig.toHostConfigDto(config);
46
- await GlobalConfig.#setAkanGlobalConfig(akanConfig);
60
+ await GlobalConfig.#update((akanConfig) => GlobalConfig.#withHost(akanConfig, config));
61
+ }
62
+ static #withHost(akanConfig, config) {
63
+ return {
64
+ ...akanConfig,
65
+ cloudHost: { ...akanConfig.cloudHost, [config.host]: GlobalConfig.toHostConfigDto(config) }
66
+ };
67
+ }
68
+ static async refreshHostAuth(host, refresh) {
69
+ try {
70
+ return await ConfigLock.run(GlobalConfig.#lockPath, async () => {
71
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
72
+ const current = GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
73
+ const accessToken = current.auth?.accessToken;
74
+ const refreshToken = accessToken?.refreshToken;
75
+ if (!accessToken || !refreshToken || !GlobalConfig.needRefreshToken(accessToken))
76
+ return current;
77
+ const claimed = {
78
+ ...current,
79
+ auth: { ...current.auth, accessToken: { ...accessToken, refreshToken: null } }
80
+ };
81
+ await GlobalConfig.#setAkanGlobalConfig(GlobalConfig.#withHost(akanConfig, claimed));
82
+ const next = await refresh(refreshToken).catch(() => null);
83
+ if (!next?.jwt)
84
+ return claimed;
85
+ const refreshed = { ...current, auth: { ...current.auth, accessToken: next } };
86
+ await GlobalConfig.#setAkanGlobalConfig(GlobalConfig.#withHost(await GlobalConfig.#getAkanGlobalConfig(), refreshed));
87
+ return refreshed;
88
+ });
89
+ } catch {
90
+ return await GlobalConfig.getHostConfig(host);
91
+ }
47
92
  }
48
93
  static async getRemoteEnvServers() {
49
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
50
- return akanConfig.remoteEnvServers;
94
+ return (await GlobalConfig.#getAkanGlobalConfig()).remoteEnvServers;
51
95
  }
52
96
  static async setRemoteEnvServer(name, config) {
53
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
54
- await GlobalConfig.#setAkanGlobalConfig({
97
+ await GlobalConfig.#update((akanConfig) => ({
55
98
  ...akanConfig,
56
- remoteEnvServers: {
57
- ...akanConfig.remoteEnvServers,
58
- [name]: config
59
- }
60
- });
99
+ remoteEnvServers: { ...akanConfig.remoteEnvServers, [name]: config }
100
+ }));
61
101
  }
62
102
  static async removeRemoteEnvServer(name) {
63
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
64
- const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
65
- await GlobalConfig.#setAkanGlobalConfig({
66
- ...akanConfig,
67
- remoteEnvServers
103
+ await GlobalConfig.#update((akanConfig) => {
104
+ const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
105
+ return { ...akanConfig, remoteEnvServers };
68
106
  });
69
107
  }
70
108
  static async getTestTargets() {
71
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
72
- return akanConfig.testTargets;
109
+ return (await GlobalConfig.#getAkanGlobalConfig()).testTargets;
73
110
  }
74
111
  static async setTestTargets(testTargets) {
75
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
76
- await GlobalConfig.#setAkanGlobalConfig({
112
+ await GlobalConfig.#update((akanConfig) => ({
77
113
  ...akanConfig,
78
114
  testTargets: { ...akanConfig.testTargets, ...testTargets }
79
- });
115
+ }));
80
116
  }
81
117
  static needRefreshToken(accessToken) {
82
118
  return !!accessToken?.expiresAt?.isBefore(dayjs().add(1, "hour"));
@@ -125,11 +161,7 @@ class HttpClient {
125
161
  }
126
162
  async get(url, { headers, signal } = {}) {
127
163
  const response = await fetch(`${this.baseUrl}${url}`, {
128
- headers: {
129
- "Content-Type": "application/json",
130
- ...this.headers,
131
- ...headers
132
- },
164
+ headers: { "Content-Type": "application/json", ...this.headers, ...headers },
133
165
  ...signal ? { signal } : {}
134
166
  });
135
167
  return await HttpClient.#body(response, url);
@@ -172,7 +204,12 @@ class CloudApi {
172
204
  url;
173
205
  static async fromHost(workspace, host) {
174
206
  const hostConfig = await GlobalConfig.getHostConfig(host);
175
- return new CloudApi(workspace, hostConfig);
207
+ const accessToken = hostConfig.auth?.accessToken;
208
+ if (!accessToken || !GlobalConfig.needRefreshToken(accessToken))
209
+ return new CloudApi(workspace, hostConfig);
210
+ const tokenless = new CloudApi(workspace, hostConfig);
211
+ const refreshed = await GlobalConfig.refreshHostAuth(hostConfig.host, (refreshToken) => tokenless.refreshAuthToken(refreshToken));
212
+ return new CloudApi(workspace, refreshed);
176
213
  }
177
214
  constructor(workspace, hostConfig) {
178
215
  this.#workspace = workspace;
@@ -181,16 +218,18 @@ class CloudApi {
181
218
  this.url = `${this.host}/api`;
182
219
  this.#api = new HttpClient(this.url);
183
220
  if (this.#accessToken && !GlobalConfig.needRefreshToken(this.#accessToken))
184
- this.#api.setHeaders({
185
- Authorization: `Bearer ${this.#accessToken.jwt}`
186
- });
221
+ this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
222
+ }
223
+ #authorize(accessTokenDto) {
224
+ this.#accessToken = GlobalConfig.toAccessToken(accessTokenDto);
225
+ this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
226
+ return this.#accessToken;
187
227
  }
188
228
  async uploadEnv(devProjectId, file) {
189
229
  const formData = new FormData;
190
230
  formData.append("devProjectId", devProjectId);
191
231
  formData.append("file", file);
192
- const data = await this.#api.post(`/uploadEnv/${devProjectId}`, formData);
193
- return data;
232
+ return await this.#api.post(`/uploadEnv/${devProjectId}`, formData);
194
233
  }
195
234
  async downloadEnv(devProjectId) {
196
235
  const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
@@ -199,34 +238,13 @@ class CloudApi {
199
238
  }
200
239
  async getRemoteAuthToken(remoteId) {
201
240
  try {
202
- const accessToken = await this.#api.get(`/getRemoteAuthToken/${remoteId}`);
203
- this.#accessToken = GlobalConfig.toAccessToken(accessToken);
204
- this.#api.setHeaders({
205
- Authorization: `Bearer ${this.#accessToken.jwt}`
206
- });
207
- return this.#accessToken;
241
+ return this.#authorize(await this.#api.get(`/getRemoteAuthToken/${remoteId}`));
208
242
  } catch (_) {
209
243
  return null;
210
244
  }
211
245
  }
212
- async#ensureAccessTokenLive({
213
- allowUnauthorized = false
214
- } = {}) {
215
- if (!this.#accessToken)
216
- throw new Error("No access token");
217
- const needRefresh = GlobalConfig.needRefreshToken(this.#accessToken);
218
- if (!needRefresh)
219
- return this.#accessToken;
220
- const refreshToken = this.#accessToken?.refreshToken;
221
- if (!refreshToken)
222
- throw new Error("No refresh token");
223
- return await this.refreshAuthToken(refreshToken);
224
- }
225
246
  async refreshAuthToken(refreshToken) {
226
- const response = await this.#api.post(`/refreshAuthToken`, { refreshToken });
227
- this.#accessToken = GlobalConfig.toAccessToken(response);
228
- this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
229
- return this.#accessToken;
247
+ return this.#authorize(await this.#api.post(`/refreshAuthToken`, { refreshToken }, { signal: AbortSignal.timeout(20000) }));
230
248
  }
231
249
  async requestTunnel(input) {
232
250
  return await this.#api.post(`/tunnel/requestTunnel`, input);
@@ -239,8 +257,7 @@ class CloudApi {
239
257
  }
240
258
  async getRemoteSelf() {
241
259
  try {
242
- const data = await this.#api.get(`/getRemoteSelf`);
243
- return data;
260
+ return await this.#api.get(`/getRemoteSelf`);
244
261
  } catch {
245
262
  return null;
246
263
  }
@@ -5,33 +5,26 @@ import {
5
5
  writeGeneratedSyncState,
6
6
  createRepairReport,
7
7
  renderRepairReport
8
- } from "./index-2446k7b4.js";
8
+ } from "./index-myhc3c1j.js";
9
9
  import {
10
10
  AkanContextAnalyzer
11
- } from "./index-18zybgkc.js";
11
+ } from "./index-ygn77a5w.js";
12
12
  import {
13
13
  runner
14
- } from "./index-qdgzsnhg.js";
14
+ } from "./index-w2zdcw3k.js";
15
15
 
16
16
  // pkgs/@akanjs/cli/repair/repair.runner.ts
17
- var commandForShell = (command) => command.startsWith("akan ") ? `bun run ${command}` : command;
17
+ var spawnShell = async (workspace, command) => await workspace.spawn("bash", ["-lc", command.startsWith("akan ") ? `bun run ${command}` : command], {
18
+ cwd: workspace.workspaceRoot
19
+ });
18
20
  var defaultExecutor = (workspace) => async (command) => {
21
+ const base = { command: command.command, reason: command.reason };
19
22
  try {
20
- const stdout = await workspace.spawn("bash", ["-lc", commandForShell(command.command)], {
21
- cwd: workspace.workspaceRoot
22
- });
23
- return {
24
- command: command.command,
25
- reason: command.reason,
26
- status: "passed",
27
- exitCode: 0,
28
- stdout
29
- };
23
+ return { ...base, status: "passed", exitCode: 0, stdout: await spawnShell(workspace, command.command) };
30
24
  } catch (error) {
31
25
  const commandError = error;
32
26
  return {
33
- command: command.command,
34
- reason: command.reason,
27
+ ...base,
35
28
  status: "failed",
36
29
  exitCode: commandError.code ?? 1,
37
30
  stdout: commandError.stdout,
@@ -158,4 +151,4 @@ class RepairRunner extends runner("repair") {
158
151
  }
159
152
  }
160
153
 
161
- export { RepairRunner };
154
+ export { spawnShell, RepairRunner };
@@ -0,0 +1,87 @@
1
+ // @bun
2
+ // pkgs/@akanjs/devkit/cloud/configLock.ts
3
+ import { mkdir, open, readFile, rm, stat } from "fs/promises";
4
+ import path from "path";
5
+
6
+ class ConfigLock {
7
+ static waitTimeoutMs = 30000;
8
+ static #unknownHolderStaleMs = 1e4;
9
+ static #pollMs = 50;
10
+ static async run(lockPath, fn) {
11
+ await mkdir(path.dirname(lockPath), { recursive: true, mode: 448 });
12
+ const deadline = Date.now() + ConfigLock.waitTimeoutMs;
13
+ while (!await ConfigLock.#take(lockPath)) {
14
+ if (Date.now() >= deadline) {
15
+ const holder = (await ConfigLock.#holder(lockPath))?.pid ?? "unknown";
16
+ throw new Error(`${lockPath} is held by process ${holder}; if no akan command is running, delete it and retry.`);
17
+ }
18
+ await ConfigLock.#reclaimIfAbandoned(lockPath);
19
+ await Bun.sleep(ConfigLock.#pollMs);
20
+ }
21
+ try {
22
+ return await fn();
23
+ } finally {
24
+ await rm(lockPath, { force: true });
25
+ }
26
+ }
27
+ static async#take(lockPath) {
28
+ const handle = await open(lockPath, "wx", 384).catch((error) => {
29
+ if (error.code === "EEXIST")
30
+ return null;
31
+ throw error;
32
+ });
33
+ if (!handle)
34
+ return false;
35
+ try {
36
+ await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
37
+ } finally {
38
+ await handle.close();
39
+ }
40
+ return true;
41
+ }
42
+ static async#reclaimIfAbandoned(lockPath) {
43
+ if (!await ConfigLock.#abandoned(lockPath))
44
+ return;
45
+ const guard = `${lockPath}.reclaim`;
46
+ if (!await ConfigLock.#take(guard)) {
47
+ if (await ConfigLock.#abandoned(guard))
48
+ await rm(guard, { force: true });
49
+ return;
50
+ }
51
+ try {
52
+ if (await ConfigLock.#abandoned(lockPath))
53
+ await rm(lockPath, { force: true });
54
+ } finally {
55
+ await rm(guard, { force: true });
56
+ }
57
+ }
58
+ static async#abandoned(lockPath) {
59
+ const info = await stat(lockPath).catch(() => null);
60
+ if (!info)
61
+ return false;
62
+ const holder = await ConfigLock.#holder(lockPath);
63
+ if (!holder)
64
+ return Date.now() - info.mtimeMs > ConfigLock.#unknownHolderStaleMs;
65
+ return !ConfigLock.#isAlive(holder.pid);
66
+ }
67
+ static async#holder(lockPath) {
68
+ try {
69
+ const parsed = JSON.parse(await readFile(lockPath, "utf8"));
70
+ return typeof parsed.pid === "number" && typeof parsed.at === "number" ? { pid: parsed.pid, at: parsed.at } : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ static #isAlive(pid) {
76
+ if (!Number.isInteger(pid) || pid <= 0)
77
+ return false;
78
+ try {
79
+ process.kill(pid, 0);
80
+ return true;
81
+ } catch (error) {
82
+ return error.code === "EPERM";
83
+ }
84
+ }
85
+ }
86
+
87
+ export { ConfigLock };