@akanjs/cli 3.0.0-alpha.80 → 3.0.0-alpha.82

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.
package/.build-stamp CHANGED
@@ -1 +1 @@
1
- 63d29c41a185afe732ccce2296754b62d7c91739ea63a59ec9eb83380af7b4aa
1
+ 133d9fd6deee4acd90c4506d240dc80a0ae269fc5a4e0e37b76f334390c27745
@@ -11,13 +11,13 @@ import"./index-ayy5ecax.js";
11
11
  import"./index-6wwzs1bc.js";
12
12
  import {
13
13
  ApplicationScript
14
- } from "./index-f7wewj2v.js";
14
+ } from "./index-m7e0f1sy.js";
15
15
  import"./index-xxe90kqy.js";
16
16
  import"./index-ctphds8v.js";
17
17
  import {
18
18
  AbstractDoc
19
19
  } from "./index-meh558jy.js";
20
- import"./index-xnhf8tr9.js";
20
+ import"./index-m9c7k0gm.js";
21
21
  import {
22
22
  getMobileTargetChoices
23
23
  } from "./index-47pj3aee.js";
@@ -9,16 +9,16 @@ import {
9
9
  } from "./index-ft1sfjz0.js";
10
10
  import"./index-k1n0h7dk.js";
11
11
  import"./index-6wwzs1bc.js";
12
- import"./index-f7wewj2v.js";
12
+ import"./index-m7e0f1sy.js";
13
13
  import"./index-xxe90kqy.js";
14
14
  import"./index-ctphds8v.js";
15
15
  import"./index-mfm35dhh.js";
16
16
  import"./index-a8h184wr.js";
17
- import"./index-3c9n3nt8.js";
17
+ import"./index-1tgp56ke.js";
18
18
  import {
19
19
  CloudScript
20
- } from "./index-ee8z8adx.js";
21
- import"./index-xnhf8tr9.js";
20
+ } from "./index-ypvdw8nr.js";
21
+ import"./index-m9c7k0gm.js";
22
22
 
23
23
  // pkgs/@akanjs/cli/cloud/cloud.command.ts
24
24
  var localRegistryUrl = () => process.env.AKAN_NPM_REGISTRY ?? "http://127.0.0.1:4873";
@@ -1 +1 @@
1
- {"byCommand":{"create-workspace":"workspace","generate-agent-rules":"workspace","lint":"workspace","lint-all":"workspace","sync-all":"workspace","init":"workspace","agent":"agent","create-application":"application","remove-application":"application","plan-slice":"application","sync":"application","compact":"application","script":"application","console":"application","logs":"application","build":"application","b":"application","typecheck":"application","t":"application","test":"application","build-ios":"application","bi":"application","build-android":"application","ba":"application","start":"application","s":"application","start-ios":"application","si":"application","start-android":"application","sa":"application","release-ios":"application","release-android":"application","release-source":"application","codepush":"application","dbup":"application","dbdown":"application","configure-app":"application","create-library":"library","remove-library":"library","sync-library":"library","library-status":"library","install-library":"library","start-registry":"localRegistry","reset-registry":"localRegistry","smoke-registry":"localRegistry","version":"package","create-package":"package","remove-package":"package","sync-package":"package","build-package":"package","verify-dist-package":"package","verify-akan-publish-packages":"package","create-module":"module","remove-module":"module","create-service":"module","create-view":"module","create-unit":"module","create-template":"module","create-crud-page":"page","context":"context","doctor":"context","mcp-install":"context","mcp":"context","mcp-call":"context","login":"cloud","logout":"cloud","set-llm":"cloud","reset-llm":"cloud","ask":"cloud","deploy-akan":"cloud","update":"cloud","download-env":"cloud","upload-env":"cloud","fleet":"fleet","guideline":"guideline","generate-instruction":"guideline","update-instruction":"guideline","generate-document":"guideline","reapply-instruction":"guideline","create-scalar":"scalar","remove-scalar":"scalar","create-ui":"primitive","add-field":"primitive","add-enum-field":"primitive","quality":"quality","repair":"repair","workflow":"workflow"}}
1
+ {"byCommand":{"create-workspace":"workspace","generate-agent-rules":"workspace","lint":"workspace","lint-all":"workspace","sync-all":"workspace","init":"workspace","agent":"agent","create-application":"application","remove-application":"application","plan-slice":"application","sync":"application","compact":"application","script":"application","console":"application","logs":"application","build":"application","b":"application","typecheck":"application","t":"application","test":"application","build-ios":"application","bi":"application","build-android":"application","ba":"application","start":"application","s":"application","start-ios":"application","si":"application","start-android":"application","sa":"application","release-ios":"application","release-android":"application","release-source":"application","codepush":"application","dbup":"application","dbdown":"application","configure-app":"application","create-library":"library","remove-library":"library","sync-library":"library","library-status":"library","install-library":"library","start-registry":"localRegistry","reset-registry":"localRegistry","smoke-registry":"localRegistry","version":"package","create-package":"package","remove-package":"package","sync-package":"package","build-package":"package","verify-dist-package":"package","verify-akan-publish-packages":"package","create-module":"module","remove-module":"module","create-service":"module","create-view":"module","create-unit":"module","create-template":"module","create-crud-page":"page","context":"context","doctor":"context","mcp-install":"context","mcp":"context","mcp-call":"context","login":"cloud","logout":"cloud","set-llm":"cloud","reset-llm":"cloud","ask":"cloud","deploy-akan":"cloud","update":"cloud","download-env":"cloud","upload-env":"cloud","subspace":"subspace","guideline":"guideline","generate-instruction":"guideline","update-instruction":"guideline","generate-document":"guideline","reapply-instruction":"guideline","create-scalar":"scalar","remove-scalar":"scalar","create-ui":"primitive","add-field":"primitive","add-enum-field":"primitive","quality":"quality","repair":"repair","workflow":"workflow"}}
@@ -14,7 +14,7 @@ import {
14
14
  } from "./index-ft1sfjz0.js";
15
15
  import {
16
16
  openBrowser
17
- } from "./index-f7wewj2v.js";
17
+ } from "./index-m7e0f1sy.js";
18
18
 
19
19
  // pkgs/@akanjs/cli/npmRegistry.ts
20
20
  var defaultNpmRegistry = "https://registry.npmjs.org";
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  SlicePlanner,
21
21
  formatSlicePlan
22
- } from "./index-xnhf8tr9.js";
22
+ } from "./index-m9c7k0gm.js";
23
23
  import {
24
24
  resolveMobileTargets
25
25
  } from "./index-47pj3aee.js";
@@ -12,8 +12,55 @@ class SlicePlanner {
12
12
  "@biomejs/biome",
13
13
  "@types/bun",
14
14
  "akanjs",
15
+ "tailwindcss",
15
16
  "typescript"
16
17
  ];
18
+ static akanPackages = ["akanjs", "@akanjs/devkit", "@akanjs/cli"];
19
+ static async#frameworkPeers(workspace) {
20
+ const peers = new Set;
21
+ let found = false;
22
+ for (const akanPackage of SlicePlanner.akanPackages) {
23
+ for (const prefix of ["node_modules", "pkgs"]) {
24
+ const manifestPath = `${prefix}/${akanPackage}/package.json`;
25
+ if (!await workspace.exists(manifestPath))
26
+ continue;
27
+ const manifest = await workspace.readJson(manifestPath);
28
+ for (const peer of Object.keys(manifest.peerDependencies ?? {}))
29
+ peers.add(peer);
30
+ found = true;
31
+ break;
32
+ }
33
+ }
34
+ return found ? peers : null;
35
+ }
36
+ static typesPackageOf(dependency) {
37
+ return dependency.startsWith("@") ? `@types/${dependency.slice(1).replace("/", "__")}` : `@types/${dependency}`;
38
+ }
39
+ static async pruneDependencies(workspace, rootPackageJson, required) {
40
+ const peers = await SlicePlanner.#frameworkPeers(workspace);
41
+ const { workspaces: _workspaces, ...rest } = rootPackageJson;
42
+ if (!peers)
43
+ return {
44
+ packageJson: rest,
45
+ pruned: [],
46
+ warnings: [`dependencies carried whole \u2014 none of ${SlicePlanner.akanPackages.join(", ")} is installed`]
47
+ };
48
+ const keep = new Set([...SlicePlanner.toolchainDependencies, ...peers, ...required]);
49
+ for (const dependency of [...keep])
50
+ keep.add(SlicePlanner.typesPackageOf(dependency));
51
+ const pick = (entries) => Object.fromEntries(Object.entries(entries ?? {}).filter(([name]) => keep.has(name)));
52
+ const patched = rootPackageJson.patchedDependencies ?? {};
53
+ const packageJson = {
54
+ ...rest,
55
+ dependencies: pick(rootPackageJson.dependencies),
56
+ devDependencies: pick(rootPackageJson.devDependencies),
57
+ ...Object.keys(patched).length ? {
58
+ patchedDependencies: Object.fromEntries(Object.entries(patched).filter(([spec]) => keep.has(spec.slice(0, spec.lastIndexOf("@")))))
59
+ } : {}
60
+ };
61
+ const pruned = Object.keys({ ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies }).filter((name) => !keep.has(name)).sort();
62
+ return { packageJson, pruned, warnings: [] };
63
+ }
17
64
  #app;
18
65
  constructor(app) {
19
66
  this.#app = app;
@@ -38,7 +85,6 @@ class SlicePlanner {
38
85
  ...[...appInfo.getLibInfos().values()].map((lib) => lib.getScanResult())
39
86
  ];
40
87
  return new Set([
41
- ...SlicePlanner.toolchainDependencies,
42
88
  ...scanResults.flatMap((scanResult) => [
43
89
  ...scanResult.dependencies,
44
90
  ...scanResult.devDependencies,
@@ -64,12 +110,15 @@ class SlicePlanner {
64
110
  const libFiles = Object.fromEntries(await Promise.all(libs.map(async (lib) => [lib, await this.#trackedFiles([`libs/${lib}`])])));
65
111
  const rootFiles = allTracked.filter((file) => !this.#isMemberFile(file));
66
112
  const required = SlicePlanner.#requiredDependencies(appInfo);
67
- const rootDependencies = { ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies };
68
- const unusedDependencies = Object.keys(rootDependencies).filter((dep) => !required.has(dep)).sort();
69
- const { workspaces: _workspaces, ...slicePackageJson } = rootPackageJson;
113
+ const {
114
+ packageJson,
115
+ pruned,
116
+ warnings: manifestWarnings
117
+ } = await SlicePlanner.pruneDependencies(this.#app.workspace, rootPackageJson, required);
70
118
  const warnings = [
71
119
  ...untracked.length ? [`${untracked.length} untracked file(s) under the slice paths: ${untracked.join(", ")}`] : [],
72
120
  ...rootPackageJson.workspaces ? ["root `workspaces` dropped \u2014 a slice consumes akanjs from the registry"] : [],
121
+ ...manifestWarnings,
73
122
  ...await this.#patchWarnings(rootPackageJson)
74
123
  ];
75
124
  return {
@@ -78,8 +127,9 @@ class SlicePlanner {
78
127
  appFiles,
79
128
  libFiles,
80
129
  rootFiles,
81
- packageJson: slicePackageJson,
82
- unusedDependencies,
130
+ packageJson,
131
+ requiredDependencies: [...required].sort(),
132
+ unusedDependencies: pruned,
83
133
  warnings
84
134
  };
85
135
  }
@@ -94,7 +144,7 @@ function formatSlicePlan(plan) {
94
144
  ...libCounts,
95
145
  `workspace shell: ${plan.rootFiles.length} files across ${rootEntriesOf(plan.rootFiles).join(", ")}`,
96
146
  "",
97
- `Unused root dependencies (${plan.unusedDependencies.length}) \u2014 prune candidates, not pruned:`,
147
+ `Root dependencies this slice does not use (${plan.unusedDependencies.length}) \u2014 pruned:`,
98
148
  "",
99
149
  ...plan.unusedDependencies.length ? plan.unusedDependencies.map((dep) => ` - ${dep}`) : [" (none)"],
100
150
  "",
@@ -12,13 +12,13 @@ import {
12
12
  } from "./index-ft1sfjz0.js";
13
13
  import {
14
14
  ApplicationScript
15
- } from "./index-f7wewj2v.js";
15
+ } from "./index-m7e0f1sy.js";
16
16
  import {
17
17
  PackageScript
18
18
  } from "./index-a8h184wr.js";
19
19
  import {
20
20
  CloudRunner
21
- } from "./index-3c9n3nt8.js";
21
+ } from "./index-1tgp56ke.js";
22
22
 
23
23
  // pkgs/@akanjs/cli/cloud/cloud.script.ts
24
24
  import { Logger } from "akanjs/common";
package/index.js CHANGED
@@ -14,17 +14,17 @@ import path from "path";
14
14
 
15
15
  // pkgs/@akanjs/cli/commandModules.ts
16
16
  var commandModules = {
17
- workspace: async () => (await import("./workspace.command-mbb5s3wx.js")).WorkspaceCommand,
17
+ workspace: async () => (await import("./workspace.command-4ztb4fdh.js")).WorkspaceCommand,
18
18
  agent: async () => (await import("./agent.command-180hh0ye.js")).AgentCommand,
19
- application: async () => (await import("./application.command-gmcgen9y.js")).ApplicationCommand,
19
+ application: async () => (await import("./application.command-4fq5pmdd.js")).ApplicationCommand,
20
20
  library: async () => (await import("./library.command-wpedct6c.js")).LibraryCommand,
21
- localRegistry: async () => (await import("./localRegistry.command-pjdxa01g.js")).LocalRegistryCommand,
21
+ localRegistry: async () => (await import("./localRegistry.command-gk773cte.js")).LocalRegistryCommand,
22
22
  package: async () => (await import("./package.command-fcbd0b1d.js")).PackageCommand,
23
23
  module: async () => (await import("./module.command-8n4kct3e.js")).ModuleCommand,
24
24
  page: async () => (await import("./page.command-x4379grq.js")).PageCommand,
25
25
  context: async () => (await import("./context.command-dk2m2sjx.js")).ContextCommand,
26
- cloud: async () => (await import("./cloud.command-gpq7qxrc.js")).CloudCommand,
27
- fleet: async () => (await import("./fleet.command-3bkyyd3a.js")).FleetCommand,
26
+ cloud: async () => (await import("./cloud.command-wm3mtfve.js")).CloudCommand,
27
+ subspace: async () => (await import("./subspace.command-nr32nm35.js")).SubspaceCommand,
28
28
  guideline: async () => (await import("./guideline.command-w6ddv13r.js")).GuidelineCommand,
29
29
  scalar: async () => (await import("./scalar.command-2emk89eh.js")).ScalarCommand,
30
30
  primitive: async () => (await import("./primitive.command-8etmnqhq.js")).PrimitiveCommand,
@@ -12,19 +12,19 @@ import"./index-ayy5ecax.js";
12
12
  import"./index-6wwzs1bc.js";
13
13
  import {
14
14
  ApplicationScript
15
- } from "./index-f7wewj2v.js";
15
+ } from "./index-m7e0f1sy.js";
16
16
  import"./index-xxe90kqy.js";
17
17
  import"./index-ctphds8v.js";
18
18
  import"./index-wet2ytnr.js";
19
19
  import {
20
20
  getNpmRegistryUrl,
21
21
  CloudRunner
22
- } from "./index-3c9n3nt8.js";
22
+ } from "./index-1tgp56ke.js";
23
23
  import"./index-mfm35dhh.js";
24
24
  import {
25
25
  PackageScript
26
26
  } from "./index-a8h184wr.js";
27
- import"./index-xnhf8tr9.js";
27
+ import"./index-m9c7k0gm.js";
28
28
 
29
29
  // pkgs/@akanjs/cli/localRegistry/localRegistry.runner.ts
30
30
  import { mkdir, rm } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "3.0.0-alpha.80",
3
+ "version": "3.0.0-alpha.82",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -33,7 +33,7 @@
33
33
  "@langchain/openai": "^1.4.6",
34
34
  "@tailwindcss/node": "^4.3.0",
35
35
  "@trapezedev/project": "^7.1.4",
36
- "akanjs": "3.0.0-alpha.80",
36
+ "akanjs": "3.0.0-alpha.82",
37
37
  "chalk": "^5.6.2",
38
38
  "commander": "^14.0.3",
39
39
  "dayjs": "^1.11.20",
@@ -8,8 +8,7 @@ import {
8
8
  import {
9
9
  Executor,
10
10
  WorkspaceExecutor,
11
- AppExecutor,
12
- LibExecutor
11
+ AppExecutor
13
12
  } from "./index-k1n0h7dk.js";
14
13
  import {
15
14
  FileSys
@@ -17,134 +16,21 @@ import {
17
16
  import"./index-6wwzs1bc.js";
18
17
  import {
19
18
  SlicePlanner
20
- } from "./index-xnhf8tr9.js";
19
+ } from "./index-m9c7k0gm.js";
21
20
  import {
22
21
  LibSource
23
22
  } from "./index-3d9y066c.js";
24
23
 
25
- // pkgs/@akanjs/devkit/fleetGuard.ts
26
- import path from "path";
27
- class FleetGuard {
28
- static anchorFile = "akan.fleet.json";
29
- static hubOwnedRootFiles = ["package.json", "tsconfig.json", "biome.json", "bunfig.toml"];
30
- static spokeOwnedDirs = ["env"];
31
- static contact = "Ask the akansoft team to make this change in the hub, then pull it back down.";
32
- #workspace;
33
- constructor(workspace) {
34
- this.#workspace = workspace;
35
- }
36
- async#readAnchorFile() {
37
- const anchorPath = path.join(this.#workspace.workspaceRoot, FleetGuard.anchorFile);
38
- if (!await FileSys.fileExists(anchorPath))
39
- return null;
40
- return await FileSys.readJson(anchorPath);
41
- }
42
- async#anchorCommit() {
43
- const commit = await this.#workspace.spawn("git", ["log", "-1", "--format=%H", "--", FleetGuard.anchorFile]);
44
- return commit.trim() || null;
45
- }
46
- #isSpokeOwned(file) {
47
- const segments = file.split("/");
48
- if (segments[0] !== "apps" && segments[0] !== "libs")
49
- return false;
50
- return FleetGuard.spokeOwnedDirs.includes(segments[2] ?? "");
51
- }
52
- #violationOf(file) {
53
- if (this.#isSpokeOwned(file))
54
- return null;
55
- if (file === FleetGuard.anchorFile)
56
- return { path: file, reason: "the fleet anchor is written by a push" };
57
- if (FleetGuard.hubOwnedRootFiles.includes(file))
58
- return { path: file, reason: "workspace manifests are owned by the hub" };
59
- if (file.startsWith("libs/"))
60
- return { path: file, reason: "shared libraries are owned by the hub" };
61
- return null;
62
- }
63
- async#changedFiles({ staged }) {
64
- if (staged) {
65
- const cached = await this.#workspace.spawn("git", ["diff", "--cached", "--name-only"]);
66
- return cached.split(`
67
- `).filter((file) => !!file.trim());
68
- }
69
- const anchor = await this.#anchorCommit();
70
- const sinceAnchor = anchor ? (await this.#workspace.spawn("git", ["diff", "--name-only", `${anchor}..HEAD`])).split(`
71
- `).filter((file) => !!file.trim()) : [];
72
- const working = (await this.#workspace.spawn("git", ["status", "--porcelain"])).split(`
73
- `).filter((line) => !!line.trim()).map((line) => line.slice(3).split(" -> ").at(-1) ?? "").filter((file) => !!file);
74
- return [...new Set([...sinceAnchor, ...working])];
75
- }
76
- async#libDrift() {
77
- const libNames = await this.#workspace.getLibs();
78
- const statuses = await Promise.all(libNames.map(async (libName) => await new LibSource(LibExecutor.from(this.#workspace, libName)).status()));
79
- return statuses.filter((status) => status.drift === "drifted").map((status) => ({ lib: status.lib, expected: status.stamp?.hash ?? "", actual: status.hash }));
80
- }
81
- async check({ staged = false } = {}) {
82
- const spoke = await this.#readAnchorFile();
83
- if (!spoke)
84
- return { spoke: null, anchor: null, violations: [], driftedLibs: [] };
85
- const [anchor, changed, driftedLibs] = await Promise.all([
86
- this.#anchorCommit(),
87
- this.#changedFiles({ staged }),
88
- staged ? Promise.resolve([]) : this.#libDrift()
89
- ]);
90
- const violations = changed.map((file) => this.#violationOf(file)).filter((violation) => !!violation);
91
- return { spoke, anchor, violations, driftedLibs };
92
- }
93
- }
94
- function formatFleetGuardReport(report, { warn = false } = {}) {
95
- if (!report.spoke)
96
- return "Akan Fleet Guard: not a fleet spoke (no akan.fleet.json) \u2014 nothing to check.";
97
- const clean = !report.violations.length && !report.driftedLibs.length;
98
- const sections = [
99
- "Akan Fleet Guard",
100
- `hub: ${report.spoke.hub}#${report.spoke.branch} apps: ${report.spoke.apps.join(", ")}`,
101
- `last push: ${report.anchor?.slice(0, 12) ?? "(none)"}`,
102
- "",
103
- ...clean ? [" no hub-owned file was changed here."] : [],
104
- ...report.violations.length ? [
105
- `Hub-owned files changed in this repo (${report.violations.length}):`,
106
- "",
107
- ...report.violations.map((violation) => ` ${violation.path} \u2014 ${violation.reason}`)
108
- ] : [],
109
- ...report.driftedLibs.length ? [
110
- "",
111
- `Libraries edited since the last push (${report.driftedLibs.length}):`,
112
- "",
113
- ...report.driftedLibs.map((drift) => ` libs/${drift.lib} stamped ${drift.expected.slice(0, 12)}, now ${drift.actual.slice(0, 12)}`)
114
- ] : [],
115
- ...clean ? [] : ["", ` ${FleetGuard.contact}`, ...warn ? [" (warning only \u2014 this commit is not blocked)"] : []]
116
- ];
117
- return sections.join(`
118
- `);
119
- }
120
-
121
- // pkgs/@akanjs/devkit/fleetSpoke.ts
24
+ // pkgs/@akanjs/devkit/subspace.ts
122
25
  import { mkdir, readdir, rename, rm } from "fs/promises";
123
- import path2 from "path";
124
- class FleetSpoke {
125
- static anchorFile = "akan.fleet.json";
126
- static hubOnlyEntries = ["akan.fleet.ts"];
26
+ import path from "path";
27
+ class Subspace {
28
+ static anchorFile = "akan.subspace.json";
29
+ static workspaceOnlyEntries = ["akan.subspace.ts"];
127
30
  static memberDirs = ["apps", "libs"];
128
- static spokeOwnedDirs = ["env"];
31
+ static subspaceOwnedDirs = ["env"];
129
32
  static secretsMarkers = ["# akan:secrets (managed by akan.config.ts \u2014 do not edit)", "# akan:secrets:end"];
130
- static holdDir = ".akan-fleet-hold";
131
- static guardHookLine = "bunx akan fleet check --staged --warn";
132
- static guardWorkflow = [
133
- "name: akan fleet guard",
134
- "on: [push, pull_request]",
135
- "jobs:",
136
- " guard:",
137
- " runs-on: ubuntu-latest",
138
- " steps:",
139
- " - uses: actions/checkout@v4",
140
- " with:",
141
- " fetch-depth: 0",
142
- " - uses: oven-sh/setup-bun@v2",
143
- " - run: bun install",
144
- " - run: bunx akan fleet check",
145
- ""
146
- ].join(`
147
- `);
33
+ static holdDir = ".akan-subspace-hold";
148
34
  #workspace;
149
35
  #config;
150
36
  #declaration;
@@ -160,10 +46,10 @@ class FleetSpoke {
160
46
  return this.#declaration.apps;
161
47
  }
162
48
  get #remote() {
163
- return `spoke-${this.#declaration.name}`;
49
+ return `subspace-${this.#declaration.name}`;
164
50
  }
165
51
  get #clonePath() {
166
- return path2.join(this.#workspace.workspaceRoot, ".akan/fleet", this.#declaration.name);
52
+ return path.join(this.#workspace.workspaceRoot, ".akan/subspace", this.#declaration.name);
167
53
  }
168
54
  async#git(args) {
169
55
  return await this.#workspace.spawn("git", args);
@@ -190,7 +76,7 @@ class FleetSpoke {
190
76
  }
191
77
  async anchor(branch) {
192
78
  const ref = `${this.#remote}/${branch}`;
193
- const commit = (await this.#git(["log", "-1", "--format=%H", ref, "--", FleetSpoke.anchorFile])).trim();
79
+ const commit = (await this.#git(["log", "-1", "--format=%H", ref, "--", Subspace.anchorFile])).trim();
194
80
  if (!commit)
195
81
  return { commit: null, incoming: [] };
196
82
  const log = await this.#git(["log", "--format=%H%x09%an%x09%s", `${commit}..${ref}`]);
@@ -207,11 +93,11 @@ class FleetSpoke {
207
93
  const paths = [...this.#declaration.apps.map((app) => `apps/${app}`), ...libs.map((lib) => `libs/${lib}`)];
208
94
  return { plans, libs, paths };
209
95
  }
210
- #isSpokeOwned(file) {
96
+ #isSubspaceOwned(file) {
211
97
  const segments = file.split("/");
212
- if (!FleetSpoke.memberDirs.includes(segments[0] ?? ""))
98
+ if (!Subspace.memberDirs.includes(segments[0] ?? ""))
213
99
  return false;
214
- return FleetSpoke.spokeOwnedDirs.includes(segments[2] ?? "");
100
+ return Subspace.subspaceOwnedDirs.includes(segments[2] ?? "");
215
101
  }
216
102
  #isStamped(file) {
217
103
  return /^libs\/[^/]+\/package\.json$/.test(file);
@@ -249,37 +135,37 @@ class FleetSpoke {
249
135
  const ref = `${this.#remote}/${branch}`;
250
136
  const diff = await this.#git(["diff", "--name-only", "HEAD", ref, "--", ...paths]);
251
137
  const behindPaths = await this.#meaningfulFiles(ref, diff.split(`
252
- `).filter((file) => !!file.trim()).filter((file) => !this.#isSpokeOwned(file)));
138
+ `).filter((file) => !!file.trim()).filter((file) => !this.#isSubspaceOwned(file)));
253
139
  const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
254
140
  return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
255
141
  }
256
142
  async#ensureClone(branch) {
257
- if (!await FileSys.dirExists(path2.join(this.#clonePath, ".git"))) {
143
+ if (!await FileSys.dirExists(path.join(this.#clonePath, ".git"))) {
258
144
  await rm(this.#clonePath, { recursive: true, force: true });
259
- await mkdir(path2.dirname(this.#clonePath), { recursive: true });
145
+ await mkdir(path.dirname(this.#clonePath), { recursive: true });
260
146
  await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
261
147
  }
262
- const clone = new Executor(`spoke-${this.name}`, this.#clonePath);
148
+ const clone = new Executor(`subspace-${this.name}`, this.#clonePath);
263
149
  await clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
264
150
  await clone.spawn("git", ["checkout", "--quiet", "-B", branch, `origin/${branch}`]);
265
151
  await clone.spawn("git", ["clean", "-qfd"]);
266
152
  return clone;
267
153
  }
268
- async#holdSpokeOwned() {
154
+ async#holdSubspaceOwned() {
269
155
  const held = [];
270
- const holdRoot = path2.join(this.#clonePath, FleetSpoke.holdDir);
156
+ const holdRoot = path.join(this.#clonePath, Subspace.holdDir);
271
157
  await rm(holdRoot, { recursive: true, force: true });
272
- for (const member of FleetSpoke.memberDirs) {
273
- const memberRoot = path2.join(this.#clonePath, member);
158
+ for (const member of Subspace.memberDirs) {
159
+ const memberRoot = path.join(this.#clonePath, member);
274
160
  if (!await FileSys.dirExists(memberRoot))
275
161
  continue;
276
162
  for (const name of await readdir(memberRoot)) {
277
- for (const owned of FleetSpoke.spokeOwnedDirs) {
278
- const from = path2.join(memberRoot, name, owned);
163
+ for (const owned of Subspace.subspaceOwnedDirs) {
164
+ const from = path.join(memberRoot, name, owned);
279
165
  if (!await FileSys.dirExists(from))
280
166
  continue;
281
- const to = path2.join(holdRoot, member, name, owned);
282
- await mkdir(path2.dirname(to), { recursive: true });
167
+ const to = path.join(holdRoot, member, name, owned);
168
+ await mkdir(path.dirname(to), { recursive: true });
283
169
  await rename(from, to);
284
170
  held.push({ from, to });
285
171
  }
@@ -287,18 +173,18 @@ class FleetSpoke {
287
173
  }
288
174
  return held;
289
175
  }
290
- async#restoreSpokeOwned(held) {
176
+ async#restoreSubspaceOwned(held) {
291
177
  for (const { from, to } of held)
292
- await FleetSpoke.#copyMissing(to, from);
293
- await rm(path2.join(this.#clonePath, FleetSpoke.holdDir), { recursive: true, force: true });
178
+ await Subspace.#copyMissing(to, from);
179
+ await rm(path.join(this.#clonePath, Subspace.holdDir), { recursive: true, force: true });
294
180
  }
295
181
  static async#copyMissing(from, to) {
296
182
  await mkdir(to, { recursive: true });
297
183
  for (const entry of await readdir(from, { withFileTypes: true })) {
298
- const source = path2.join(from, entry.name);
299
- const target = path2.join(to, entry.name);
184
+ const source = path.join(from, entry.name);
185
+ const target = path.join(to, entry.name);
300
186
  if (entry.isDirectory())
301
- await FleetSpoke.#copyMissing(source, target);
187
+ await Subspace.#copyMissing(source, target);
302
188
  else if (!await FileSys.entryExists(target))
303
189
  await rename(source, target);
304
190
  }
@@ -320,17 +206,17 @@ class FleetSpoke {
320
206
  };
321
207
  }
322
208
  async#writeLocalEnv() {
323
- const excludePath = path2.join(this.#clonePath, ".git/info/exclude");
209
+ const excludePath = path.join(this.#clonePath, ".git/info/exclude");
324
210
  const exclude = await FileSys.fileExists(excludePath) ? await FileSys.readText(excludePath) : "";
325
211
  if (!exclude.split(`
326
212
  `).includes("/.env")) {
327
- await mkdir(path2.dirname(excludePath), { recursive: true });
213
+ await mkdir(path.dirname(excludePath), { recursive: true });
328
214
  await FileSys.writeText(excludePath, exclude.trim() ? `${exclude.replace(/\n*$/, `
329
215
  `)}/.env
330
216
  ` : `/.env
331
217
  `);
332
218
  }
333
- const envPath = path2.join(this.#clonePath, ".env");
219
+ const envPath = path.join(this.#clonePath, ".env");
334
220
  if (await FileSys.fileExists(envPath))
335
221
  return;
336
222
  const lines = Object.entries(this.#localEnv()).map(([key, value]) => `${key}=${value}`);
@@ -338,29 +224,44 @@ class FleetSpoke {
338
224
  `)}
339
225
  `);
340
226
  }
227
+ async#rewriteManifest(plans) {
228
+ const manifestPath = path.join(this.#clonePath, "package.json");
229
+ if (!await FileSys.fileExists(manifestPath))
230
+ return [];
231
+ const rootPackageJson = await FileSys.readJson(manifestPath);
232
+ const { packageJson, pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, rootPackageJson, plans.flatMap((plan) => plan.requiredDependencies));
233
+ for (const warning of warnings)
234
+ this.#workspace.logger.warn(warning);
235
+ await FileSys.writeJson(manifestPath, {
236
+ ...packageJson,
237
+ name: this.name,
238
+ description: `${this.name} workspace`
239
+ });
240
+ return pruned;
241
+ }
341
242
  async#applySlice(clone, branch) {
342
- const { libs, paths } = await this.slice();
343
- const held = await this.#holdSpokeOwned();
344
- for (const member of FleetSpoke.memberDirs)
345
- await rm(path2.join(this.#clonePath, member), { recursive: true, force: true });
243
+ const { libs, paths, plans } = await this.slice();
244
+ const held = await this.#holdSubspaceOwned();
245
+ for (const member of Subspace.memberDirs)
246
+ await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
346
247
  await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
347
248
  await this.#extract(paths);
348
- await this.#restoreSpokeOwned(held);
349
- for (const entry of [...FleetSpoke.hubOnlyEntries, ...this.#config.exclude])
350
- await rm(path2.join(this.#clonePath, entry), { recursive: true, force: true });
249
+ await this.#restoreSubspaceOwned(held);
250
+ for (const entry of [...Subspace.workspaceOnlyEntries, ...this.#config.exclude])
251
+ await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
351
252
  await this.#rewriteGitignore();
253
+ const pruned = await this.#rewriteManifest(plans);
352
254
  await this.#rewriteWorkspaceSection(libs);
353
- await this.#writeGuards();
354
255
  await this.#writeLocalEnv();
355
256
  for (const lib of libs)
356
257
  await this.#stampLib(clone, lib, branch);
357
- return { libs };
258
+ return { libs, pruned };
358
259
  }
359
260
  async#rewriteGitignore() {
360
- const gitignorePath = path2.join(this.#clonePath, ".gitignore");
261
+ const gitignorePath = path.join(this.#clonePath, ".gitignore");
361
262
  if (!await FileSys.fileExists(gitignorePath))
362
263
  return;
363
- const [begin = "", end = ""] = FleetSpoke.secretsMarkers;
264
+ const [begin = "", end = ""] = Subspace.secretsMarkers;
364
265
  const lines = (await FileSys.readText(gitignorePath)).split(`
365
266
  `);
366
267
  const beginIdx = lines.indexOf(begin);
@@ -380,7 +281,7 @@ class FleetSpoke {
380
281
  [/^- Libraries: .*$/m, `- Libraries: ${libs.length ? libs.join(", ") : "(none)"}`]
381
282
  ];
382
283
  for (const file of ["AGENTS.md", "CLAUDE.md"]) {
383
- const filePath = path2.join(this.#clonePath, file);
284
+ const filePath = path.join(this.#clonePath, file);
384
285
  if (!await FileSys.fileExists(filePath))
385
286
  continue;
386
287
  const content = await FileSys.readText(filePath);
@@ -390,7 +291,7 @@ class FleetSpoke {
390
291
  }
391
292
  }
392
293
  async#stampLib(clone, lib, branch) {
393
- const manifestPath = path2.join(this.#clonePath, "libs", lib, "package.json");
294
+ const manifestPath = path.join(this.#clonePath, "libs", lib, "package.json");
394
295
  if (!await FileSys.fileExists(manifestPath))
395
296
  return;
396
297
  const origin = `${this.#workspace.repoName}#${branch}`;
@@ -423,10 +324,10 @@ class FleetSpoke {
423
324
  "--",
424
325
  `libs/${lib}`
425
326
  ]);
426
- const files = listed.split("\x00").filter((file) => !!file && !this.#isSpokeOwned(file)).sort();
327
+ const files = listed.split("\x00").filter((file) => !!file && !this.#isSubspaceOwned(file)).sort();
427
328
  const hasher = new Bun.CryptoHasher("sha256");
428
329
  for (const file of files) {
429
- const content = await FileSys.readText(path2.join(this.#clonePath, file));
330
+ const content = await FileSys.readText(path.join(this.#clonePath, file));
430
331
  hasher.update(file);
431
332
  hasher.update("\x00");
432
333
  if (file === `libs/${lib}/package.json`) {
@@ -441,7 +342,7 @@ class FleetSpoke {
441
342
  }
442
343
  async diff(branch, filter) {
443
344
  if (!await this.fetch(branch))
444
- throw new Error(`Spoke "${this.name}" has no branch "${branch}"`);
345
+ throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
445
346
  const { libs, paths } = await this.slice();
446
347
  const libPaths = libs.map((lib) => `libs/${lib}`);
447
348
  const appPaths = paths.filter((entry) => !libPaths.includes(entry));
@@ -459,31 +360,11 @@ class FleetSpoke {
459
360
  const pathspec = filter ? [filter] : scoped;
460
361
  const names = await this.#git(["diff", "--name-only", from, "HEAD", "--", ...pathspec]);
461
362
  const files = await this.#meaningfulFiles(from, names.split(`
462
- `).filter((file) => !!file.trim()).filter((file) => !this.#isSpokeOwned(file)));
363
+ `).filter((file) => !!file.trim()).filter((file) => !this.#isSubspaceOwned(file)));
463
364
  if (!files.length)
464
365
  return { files: [], patch: "" };
465
366
  return { files, patch: await this.#git(["diff", from, "HEAD", "--", ...files]) };
466
367
  }
467
- async#writeGuards() {
468
- const { ci, preCommit } = this.#config.guard;
469
- if (ci === "github") {
470
- const workflowPath = path2.join(this.#clonePath, ".github/workflows/akan-fleet-guard.yml");
471
- await mkdir(path2.dirname(workflowPath), { recursive: true });
472
- await FileSys.writeText(workflowPath, FleetSpoke.guardWorkflow);
473
- }
474
- if (!preCommit)
475
- return;
476
- const hookPath = path2.join(this.#clonePath, ".husky/pre-commit");
477
- const existing = await FileSys.fileExists(hookPath) ? await FileSys.readText(hookPath) : "";
478
- if (existing.includes(FleetSpoke.guardHookLine))
479
- return;
480
- await mkdir(path2.dirname(hookPath), { recursive: true });
481
- const content = existing.trim() ? `${existing.replace(/\n*$/, `
482
- `)}${FleetSpoke.guardHookLine}
483
- ` : `${FleetSpoke.guardHookLine}
484
- `;
485
- await FileSys.writeText(hookPath, content);
486
- }
487
368
  async#verify(clone) {
488
369
  const env = { ...process.env, ...this.#localEnv() };
489
370
  try {
@@ -498,14 +379,20 @@ class FleetSpoke {
498
379
  async push(branch, { verify = true } = {}) {
499
380
  this.#config.assertPushable(branch);
500
381
  if (await this.#workspace.hasChanges())
501
- return { name: this.name, outcome: "refused", reason: "hub working tree is dirty", changedFiles: 0 };
382
+ return { name: this.name, outcome: "refused", reason: "workspace working tree is dirty", changedFiles: 0 };
502
383
  if (!await this.fetch(branch))
503
- return { name: this.name, outcome: "refused", reason: `spoke has no branch "${branch}"`, changedFiles: 0 };
384
+ return { name: this.name, outcome: "refused", reason: `subspace has no branch "${branch}"`, changedFiles: 0 };
504
385
  const clone = await this.#ensureClone(branch);
505
- await this.#applySlice(clone, branch);
386
+ const { pruned } = await this.#applySlice(clone, branch);
506
387
  const dirty = (await clone.spawn("git", ["status", "--porcelain"])).trim();
507
388
  if (!dirty)
508
- return { name: this.name, outcome: "skipped", reason: "already up to date", changedFiles: 0 };
389
+ return {
390
+ name: this.name,
391
+ outcome: "skipped",
392
+ reason: "already up to date",
393
+ changedFiles: 0,
394
+ prunedDependencies: pruned
395
+ };
509
396
  const changedFiles = dirty.split(`
510
397
  `).length;
511
398
  if (verify) {
@@ -515,19 +402,20 @@ class FleetSpoke {
515
402
  }
516
403
  await this.#writeAnchorFile(branch);
517
404
  await clone.spawn("git", ["add", "-A"]);
518
- const message = `chore(fleet): sync from ${this.#workspace.repoName}@${await this.headSha()}`;
405
+ const message = `chore(subspace): sync from ${this.#workspace.repoName}@${await this.headSha()}`;
519
406
  await clone.spawn("git", ["commit", "--quiet", "-m", message]);
520
407
  await clone.spawn("git", ["push", "--quiet", "origin", branch]);
521
408
  return {
522
409
  name: this.name,
523
410
  outcome: "pushed",
524
411
  commit: (await clone.spawn("git", ["rev-parse", "--short", "HEAD"])).trim(),
525
- changedFiles
412
+ changedFiles,
413
+ prunedDependencies: pruned
526
414
  };
527
415
  }
528
416
  async#writeAnchorFile(branch) {
529
- await FileSys.writeJson(path2.join(this.#clonePath, FleetSpoke.anchorFile), {
530
- hub: this.#workspace.repoName,
417
+ await FileSys.writeJson(path.join(this.#clonePath, Subspace.anchorFile), {
418
+ workspace: this.#workspace.repoName,
531
419
  hubSha: await this.headSha(),
532
420
  branch,
533
421
  apps: this.#declaration.apps,
@@ -536,18 +424,18 @@ class FleetSpoke {
536
424
  }
537
425
  async pull(branch, { adoptLibs = false } = {}) {
538
426
  if (!await this.fetch(branch))
539
- throw new Error(`Spoke "${this.name}" has no branch "${branch}"`);
427
+ throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
540
428
  const anchor = await this.anchor(branch);
541
429
  if (!anchor.commit)
542
- throw new Error(`Spoke "${this.name}" carries no ${FleetSpoke.anchorFile} \u2014 it has never received a push`);
430
+ throw new Error(`Subspace "${this.name}" carries no ${Subspace.anchorFile} \u2014 it has never received a push`);
543
431
  if (!anchor.incoming.length)
544
432
  return { name: this.name, applied: [], libPatch: null, ignored: [], incoming: [] };
545
433
  const range = `${anchor.commit}..${this.#remote}/${branch}`;
546
434
  const changed = (await this.#git(["diff", "--name-only", range])).split(`
547
435
  `).filter((file) => !!file.trim());
548
436
  const appPrefixes = this.#declaration.apps.map((app) => `apps/${app}/`);
549
- const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !this.#isSpokeOwned(file));
550
- const libFiles = changed.filter((file) => file.startsWith("libs/") && !this.#isSpokeOwned(file));
437
+ const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !this.#isSubspaceOwned(file));
438
+ const libFiles = changed.filter((file) => file.startsWith("libs/") && !this.#isSubspaceOwned(file));
551
439
  const ignored = changed.filter((file) => !appFiles.includes(file) && !libFiles.includes(file));
552
440
  const applied = adoptLibs ? [...appFiles, ...libFiles] : appFiles;
553
441
  if (applied.length)
@@ -561,20 +449,20 @@ class FleetSpoke {
561
449
  await this.#git(["apply", "--3way", absolute]);
562
450
  }
563
451
  async#savePatch(range, files, label) {
564
- const patchPath = path2.join(this.#workspace.workspaceRoot, ".akan/fleet", `${this.name}-${label}.patch`);
565
- await mkdir(path2.dirname(patchPath), { recursive: true });
452
+ const patchPath = path.join(this.#workspace.workspaceRoot, ".akan/subspace", `${this.name}-${label}.patch`);
453
+ await mkdir(path.dirname(patchPath), { recursive: true });
566
454
  await FileSys.writeText(patchPath, await this.#git(["diff", range, "--", ...files]));
567
- return { path: path2.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
455
+ return { path: path.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
568
456
  }
569
457
  }
570
- function formatFleetStatuses(statuses) {
458
+ function formatSubspaceStatuses(statuses) {
571
459
  const sections = [
572
- "Akan Fleet Status",
460
+ "Akan Subspace Status",
573
461
  `branch: ${statuses[0]?.branch ?? "(none)"}`,
574
462
  "",
575
463
  ...statuses.flatMap((status) => {
576
464
  if (!status.hasBranch)
577
- return [` ${status.name}: no such branch in the spoke \u2014 push is refused`];
465
+ return [` ${status.name}: no such branch in the subspace \u2014 push is refused`];
578
466
  const behind = status.behindPaths.length ? `${status.behindPaths.length} file(s) behind` : "up to date";
579
467
  const incoming = status.anchor.incoming.length ? `${status.anchor.incoming.length} customer commit(s) to pull` : "no customer commits";
580
468
  const drift = status.driftedLibs.length ? ` DRIFTED LIBS: ${status.driftedLibs.join(", ")}` : null;
@@ -588,23 +476,28 @@ function formatFleetStatuses(statuses) {
588
476
  return sections.join(`
589
477
  `);
590
478
  }
591
- function formatFleetPushResults(results) {
479
+ function formatSubspacePushResults(results) {
592
480
  const sections = [
593
- "Akan Fleet Push",
481
+ "Akan Subspace Push",
594
482
  "",
595
483
  ...results.flatMap((result) => {
596
484
  const detail = result.outcome === "pushed" ? `${result.commit} (${result.changedFiles} files)` : result.reason ?? "";
597
485
  const [first = "", ...rest] = detail.split(`
598
486
  `);
599
- return [` ${result.outcome.padEnd(8)} ${result.name} ${first}`, ...rest.map((line) => ` ${line}`)];
487
+ const pruned = result.prunedDependencies?.length ? [` ${result.prunedDependencies.length} unused root dependenc(ies) left out of package.json`] : [];
488
+ return [
489
+ ` ${result.outcome.padEnd(8)} ${result.name} ${first}`,
490
+ ...rest.map((line) => ` ${line}`),
491
+ ...pruned
492
+ ];
600
493
  })
601
494
  ];
602
495
  return sections.join(`
603
496
  `);
604
497
  }
605
- function formatFleetPullResult(result) {
498
+ function formatSubspacePullResult(result) {
606
499
  const sections = [
607
- `Akan Fleet Pull \u2014 ${result.name}`,
500
+ `Akan Subspace Pull \u2014 ${result.name}`,
608
501
  "",
609
502
  `Customer commits since the last push (${result.incoming.length}):`,
610
503
  "",
@@ -620,22 +513,22 @@ function formatFleetPullResult(result) {
620
513
  ...result.libPatch.files.map((file) => ` ${file}`),
621
514
  "",
622
515
  ` patch: ${result.libPatch.path}`,
623
- " adopt with: akan fleet pull <name> --adopt-libs"
516
+ " adopt with: akan subspace pull <name> --adopt-libs"
624
517
  ] : [],
625
- ...result.ignored.length ? ["", `Ignored (hub-owned or spoke-owned): ${result.ignored.length} file(s)`] : []
518
+ ...result.ignored.length ? ["", `Ignored (workspace-owned or subspace-owned): ${result.ignored.length} file(s)`] : []
626
519
  ];
627
520
  return sections.join(`
628
521
  `);
629
522
  }
630
- function formatFleetDiff(result) {
523
+ function formatSubspaceDiff(result) {
631
524
  const total = result.app.files.length + result.libs.files.length;
632
525
  const sections = [
633
- `Akan Fleet Diff \u2014 ${result.name} (${result.branch})`,
634
- "what `akan fleet push` would change in the spoke",
526
+ `Akan Subspace Diff \u2014 ${result.name} (${result.branch})`,
527
+ "what `akan subspace push` would change in the subspace",
635
528
  "",
636
- ...total ? [] : [" spoke is identical to the hub for this slice."],
529
+ ...total ? [] : [" subspace is identical to the workspace for this slice."],
637
530
  ...result.libs.files.length ? [
638
- `LIBRARY changes (${result.libs.files.length}) \u2014 the spoke edited shared code:`,
531
+ `LIBRARY changes (${result.libs.files.length}) \u2014 the subspace edited shared code:`,
639
532
  "",
640
533
  ...result.libs.files.map((file) => ` ${file}`),
641
534
  "",
@@ -653,184 +546,210 @@ function formatFleetDiff(result) {
653
546
  `);
654
547
  }
655
548
 
656
- // pkgs/@akanjs/cli/fleet/fleet.script.ts
549
+ // pkgs/@akanjs/cli/subspace/subspace.script.ts
657
550
  import { Logger } from "akanjs/common";
658
551
 
659
- // pkgs/@akanjs/devkit/fleetConfig.ts
660
- import path3 from "path";
661
- class FleetConfig {
662
- static fileName = "akan.fleet.ts";
552
+ // pkgs/@akanjs/devkit/subspaceConfig.ts
553
+ import path2 from "path";
554
+ class SubspaceConfig {
555
+ static fileName = "akan.subspace.ts";
663
556
  static defaultPushableBranches = ["main", "develop", "debug"];
664
- static defaultGuard = { ci: "github", preCommit: true };
665
557
  static async from(workspace) {
666
- const configPath = path3.join(workspace.workspaceRoot, FleetConfig.fileName);
558
+ const configPath = path2.join(workspace.workspaceRoot, SubspaceConfig.fileName);
667
559
  if (!await FileSys.fileExists(configPath))
668
560
  return null;
669
561
  const input = await import(configPath).then((mod) => mod.default);
670
- return new FleetConfig(input);
562
+ return new SubspaceConfig(input);
671
563
  }
672
564
  pushableBranches;
673
565
  exclude;
674
- guard;
675
- spokes;
566
+ subspaces;
676
567
  constructor(input) {
677
- this.pushableBranches = input.pushableBranches ?? FleetConfig.defaultPushableBranches;
568
+ this.pushableBranches = input.pushableBranches ?? SubspaceConfig.defaultPushableBranches;
678
569
  this.exclude = input.exclude ?? [];
679
- this.guard = { ...FleetConfig.defaultGuard, ...input.guard };
680
- this.spokes = input.spokes ?? [];
570
+ this.subspaces = input.subspaces ?? [];
681
571
  this.#assertValid();
682
572
  }
683
573
  #assertValid() {
684
574
  const seenNames = new Set;
685
575
  const appOwners = new Map;
686
- for (const spoke of this.spokes) {
687
- if (!spoke.name || !spoke.repo)
688
- throw new Error(`${FleetConfig.fileName}: every spoke needs a name and a repo`);
689
- if (seenNames.has(spoke.name))
690
- throw new Error(`${FleetConfig.fileName}: duplicate spoke "${spoke.name}"`);
691
- seenNames.add(spoke.name);
692
- if (!spoke.apps?.length)
693
- throw new Error(`${FleetConfig.fileName}: spoke "${spoke.name}" declares no apps`);
694
- for (const app of spoke.apps) {
576
+ for (const subspace of this.subspaces) {
577
+ if (!subspace.name || !subspace.repo)
578
+ throw new Error(`${SubspaceConfig.fileName}: every subspace needs a name and a repo`);
579
+ if (seenNames.has(subspace.name))
580
+ throw new Error(`${SubspaceConfig.fileName}: duplicate subspace "${subspace.name}"`);
581
+ seenNames.add(subspace.name);
582
+ if (!subspace.apps?.length)
583
+ throw new Error(`${SubspaceConfig.fileName}: subspace "${subspace.name}" declares no apps`);
584
+ for (const app of subspace.apps) {
695
585
  const owner = appOwners.get(app);
696
586
  if (owner)
697
- throw new Error(`${FleetConfig.fileName}: app "${app}" is claimed by both "${owner}" and "${spoke.name}"`);
698
- appOwners.set(app, spoke.name);
587
+ throw new Error(`${SubspaceConfig.fileName}: app "${app}" is claimed by both "${owner}" and "${subspace.name}"`);
588
+ appOwners.set(app, subspace.name);
699
589
  }
700
590
  }
701
591
  }
702
592
  select(names) {
703
593
  if (!names.length)
704
- return this.spokes;
594
+ return this.subspaces;
705
595
  return names.map((name) => {
706
- const spoke = this.spokes.find((candidate) => candidate.name === name);
707
- if (!spoke)
708
- throw new Error(`${FleetConfig.fileName}: unknown spoke "${name}"`);
709
- return spoke;
596
+ const subspace = this.subspaces.find((candidate) => candidate.name === name);
597
+ if (!subspace)
598
+ throw new Error(`${SubspaceConfig.fileName}: unknown subspace "${name}"`);
599
+ return subspace;
710
600
  });
711
601
  }
712
602
  assertPushable(branch) {
713
603
  if (this.pushableBranches.includes(branch))
714
604
  return;
715
- throw new Error(`Branch "${branch}" is not pushable. ${FleetConfig.fileName} allows: ${this.pushableBranches.join(", ")}`);
605
+ throw new Error(`Branch "${branch}" is not pushable. ${SubspaceConfig.fileName} allows: ${this.pushableBranches.join(", ")}`);
716
606
  }
717
607
  }
718
608
 
719
- // pkgs/@akanjs/cli/fleet/fleet.runner.ts
720
- class FleetRunner extends runner("fleet") {
721
- async spokes(workspace, names) {
722
- const config = await FleetConfig.from(workspace);
609
+ // pkgs/@akanjs/cli/subspace/subspace.runner.ts
610
+ var loadPrompts = async () => await import("@inquirer/prompts");
611
+
612
+ class SubspaceRunner extends runner("subspace") {
613
+ static multiTargetActions = ["status", "push"];
614
+ async config(workspace) {
615
+ const config = await SubspaceConfig.from(workspace);
723
616
  if (!config)
724
- throw new Error(`No ${FleetConfig.fileName} at the workspace root. Declare the customer repos there first.`);
725
- const spokes = config.select(names).map((declaration) => new FleetSpoke(workspace, config, declaration));
617
+ throw new Error(`No ${SubspaceConfig.fileName} at the workspace root. Declare the customer repos there first.`);
618
+ return config;
619
+ }
620
+ async subspaces(workspace, names) {
621
+ const config = await this.config(workspace);
622
+ const subspaces = config.select(names).map((declaration) => new Subspace(workspace, config, declaration));
726
623
  const branch = (await workspace.spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
727
- return { config, spokes, branch };
624
+ return { config, subspaces, branch };
625
+ }
626
+ async selectNames(workspace, action) {
627
+ const config = await this.config(workspace);
628
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
629
+ return await SubspaceRunner.chooseNames(config, action, { interactive });
630
+ }
631
+ static async chooseNames(config, action, { interactive, prompts }) {
632
+ const multiple = SubspaceRunner.multiTargetActions.includes(action);
633
+ if (!config.subspaces.length)
634
+ throw new Error(`${SubspaceConfig.fileName} declares no subspaces.`);
635
+ if (!interactive) {
636
+ throw new Error(multiple ? `\`akan subspace ${action}\` needs a subspace name when it cannot prompt. Pass one, or run \`akan subspace ${action}-all\` for every subspace.` : `\`akan subspace ${action}\` needs a subspace name when it cannot prompt \u2014 it is reviewed one repo at a time.`);
637
+ }
638
+ const { checkbox, select } = prompts ?? await loadPrompts();
639
+ const choices = config.subspaces.map((subspace) => ({
640
+ name: subspace.name,
641
+ value: subspace.name,
642
+ description: `${subspace.repo} \xB7 ${subspace.apps.join(", ")}`
643
+ }));
644
+ if (multiple)
645
+ return await checkbox({ message: `Select subspaces to ${action}`, choices, required: true });
646
+ return [await select({ message: `Select a subspace to ${action}`, choices })];
728
647
  }
729
648
  async status(workspace, names) {
730
- const { spokes, branch } = await this.spokes(workspace, names);
649
+ const { subspaces, branch } = await this.subspaces(workspace, names);
731
650
  const statuses = [];
732
- for (const spoke of spokes)
733
- statuses.push(await spoke.status(branch));
651
+ for (const subspace of subspaces)
652
+ statuses.push(await subspace.status(branch));
734
653
  return statuses;
735
654
  }
736
655
  async push(workspace, names, { verify } = {}) {
737
- const { config, spokes, branch } = await this.spokes(workspace, names);
656
+ const { config, subspaces, branch } = await this.subspaces(workspace, names);
738
657
  config.assertPushable(branch);
739
658
  const results = [];
740
- for (const spoke of spokes)
741
- results.push(await spoke.push(branch, { verify }));
659
+ for (const subspace of subspaces)
660
+ results.push(await subspace.push(branch, { verify }));
742
661
  return results;
743
662
  }
744
663
  async diff(workspace, name, filter) {
745
- const { spokes, branch } = await this.spokes(workspace, [name]);
746
- const [spoke] = spokes;
747
- if (!spoke)
748
- throw new Error(`Unknown spoke "${name}"`);
749
- return await spoke.diff(branch, filter);
750
- }
751
- async check(workspace, { staged } = {}) {
752
- return await new FleetGuard(workspace).check({ staged });
664
+ const { subspaces, branch } = await this.subspaces(workspace, [name]);
665
+ const [subspace] = subspaces;
666
+ if (!subspace)
667
+ throw new Error(`Unknown subspace "${name}"`);
668
+ return await subspace.diff(branch, filter);
753
669
  }
754
670
  async pull(workspace, name, { adoptLibs } = {}) {
755
- const { spokes, branch } = await this.spokes(workspace, [name]);
756
- const [spoke] = spokes;
757
- if (!spoke)
758
- throw new Error(`Unknown spoke "${name}"`);
759
- return await spoke.pull(branch, { adoptLibs });
671
+ const { subspaces, branch } = await this.subspaces(workspace, [name]);
672
+ const [subspace] = subspaces;
673
+ if (!subspace)
674
+ throw new Error(`Unknown subspace "${name}"`);
675
+ return await subspace.pull(branch, { adoptLibs });
760
676
  }
761
677
  }
762
678
 
763
- // pkgs/@akanjs/cli/fleet/fleet.script.ts
764
- class FleetScript extends script("fleet", [FleetRunner]) {
679
+ // pkgs/@akanjs/cli/subspace/subspace.script.ts
680
+ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
681
+ async select(workspace, action) {
682
+ return await this.subspaceRunner.selectNames(workspace, action);
683
+ }
765
684
  async status(workspace, names, format = "text") {
766
- const spinner = workspace.spinning("Checking spokes...");
767
- const statuses = await this.fleetRunner.status(workspace, names);
685
+ const spinner = workspace.spinning("Checking subspaces...");
686
+ const statuses = await this.subspaceRunner.status(workspace, names);
768
687
  const drifted = statuses.filter((status) => status.driftedLibs.length).length;
769
- spinner.succeed(`Checked ${statuses.length} spoke(s) (${drifted} with library drift)`);
770
- Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : formatFleetStatuses(statuses));
688
+ spinner.succeed(`Checked ${statuses.length} subspace(s) (${drifted} with library drift)`);
689
+ Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : formatSubspaceStatuses(statuses));
771
690
  }
772
691
  async push(workspace, names, { verify, format }) {
773
- const spinner = workspace.spinning("Pushing to spokes...");
774
- const results = await this.fleetRunner.push(workspace, names, { verify });
692
+ const spinner = workspace.spinning("Pushing to subspaces...");
693
+ const results = await this.subspaceRunner.push(workspace, names, { verify });
775
694
  const pushed = results.filter((result) => result.outcome === "pushed").length;
776
- spinner.succeed(`Pushed ${pushed} of ${results.length} spoke(s)`);
777
- Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : formatFleetPushResults(results));
695
+ spinner.succeed(`Pushed ${pushed} of ${results.length} subspace(s)`);
696
+ Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : formatSubspacePushResults(results));
778
697
  if (results.some((result) => result.outcome === "refused"))
779
698
  process.exitCode = 1;
780
699
  }
781
700
  async diff(workspace, name, filter, format = "text") {
782
701
  const spinner = workspace.spinning(`Diffing ${name}...`);
783
- const result = await this.fleetRunner.diff(workspace, name, filter);
702
+ const result = await this.subspaceRunner.diff(workspace, name, filter);
784
703
  const total = result.app.files.length + result.libs.files.length;
785
704
  spinner.succeed(`${total} file(s) differ (${result.libs.files.length} in shared libraries)`);
786
- Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatFleetDiff(result));
787
- }
788
- async check(workspace, { staged, warn, format }) {
789
- const report = await this.fleetRunner.check(workspace, { staged });
790
- Logger.rawLog(format === "json" ? JSON.stringify(report, null, 2) : formatFleetGuardReport(report, { warn }));
791
- const failed = !!report.violations.length || !!report.driftedLibs.length;
792
- if (failed && !warn)
793
- process.exitCode = 1;
705
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceDiff(result));
794
706
  }
795
707
  async pull(workspace, name, { adoptLibs, format }) {
796
708
  const spinner = workspace.spinning(`Pulling customer commits from ${name}...`);
797
- const result = await this.fleetRunner.pull(workspace, name, { adoptLibs });
709
+ const result = await this.subspaceRunner.pull(workspace, name, { adoptLibs });
798
710
  spinner.succeed(`${result.incoming.length} customer commit(s), ${result.applied.length} file(s) applied`);
799
- Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatFleetPullResult(result));
711
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspacePullResult(result));
800
712
  }
801
713
  }
802
714
 
803
- // pkgs/@akanjs/cli/fleet/fleet.command.ts
804
- class FleetCommand extends command("fleet", [FleetScript], ({ public: target }) => ({
805
- fleet: target({ desc: "Mirror apps and libraries between this hub workspace and the customer repos it serves" }).arg("action", String, {
806
- desc: "fleet action",
715
+ // pkgs/@akanjs/cli/subspace/subspace.command.ts
716
+ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: target }) => ({
717
+ subspace: target({ desc: "Mirror apps and libraries between this workspace and the customer repos it serves" }).arg("action", String, {
718
+ desc: "subspace action; status-all and push-all take every declared subspace",
807
719
  default: "status",
808
- enum: ["status", "diff", "push", "pull", "check"]
809
- }).arg("spoke", String, { desc: "spoke name; every spoke when omitted (required for pull)", nullable: true }).with(Workspace).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).option("verify", Boolean, {
720
+ enum: ["status", "status-all", "diff", "push", "push-all", "pull"]
721
+ }).arg("subspace", String, {
722
+ desc: "subspace name; asks which one(s) when omitted, and is refused by status-all / push-all",
723
+ nullable: true
724
+ }).with(Workspace).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).option("verify", Boolean, {
810
725
  flag: "y",
811
- desc: "run bun install + akan sync in the spoke before committing",
726
+ desc: "run bun install + akan sync in the subspace before committing",
812
727
  default: true
813
- }).option("adoptLibs", Boolean, { desc: "pull: also apply the spoke's library edits", default: false }).option("path", String, { desc: "diff: limit to one path", nullable: true }).option("staged", Boolean, { desc: "check: only look at what is staged", default: false }).option("warn", Boolean, { desc: "check: report without failing", default: false }).exec(async function(action, spoke, workspace, format, verify, adoptLibs, filePath, staged, warn) {
814
- const names = spoke ? [spoke] : [];
728
+ }).option("adoptLibs", Boolean, { desc: "pull: also apply the subspace's library edits", default: false }).option("path", String, { desc: "diff: limit to one path", nullable: true }).exec(async function(action, subspace, workspace, format, verify, adoptLibs, filePath) {
815
729
  const output = format;
816
- if (action === "status")
817
- await this.fleetScript.status(workspace, names, output);
818
- else if (action === "check")
819
- await this.fleetScript.check(workspace, { staged, warn, format: output });
820
- else if (action === "push")
821
- await this.fleetScript.push(workspace, names, { verify, format: output });
822
- else if (action === "diff" || action === "pull") {
823
- if (!spoke)
824
- throw new Error(`\`akan fleet ${action}\` needs a spoke name \u2014 it is reviewed one repo at a time.`);
825
- if (action === "diff")
826
- await this.fleetScript.diff(workspace, spoke, filePath, output);
730
+ const all = action === "status-all" || action === "push-all";
731
+ const base = all ? action.slice(0, -"-all".length) : action;
732
+ if (base !== "status" && base !== "diff" && base !== "push" && base !== "pull")
733
+ throw new Error(`Unknown subspace action: ${action}. Use status, status-all, diff, push, push-all or pull.`);
734
+ if (all && subspace)
735
+ throw new Error(`\`akan subspace ${action}\` takes no name. Run \`akan subspace ${base} ${subspace}\` instead.`);
736
+ const names = subspace ? [subspace] : all ? [] : await this.subspaceScript.select(workspace, base);
737
+ if (base === "status")
738
+ await this.subspaceScript.status(workspace, names, output);
739
+ else if (base === "push")
740
+ await this.subspaceScript.push(workspace, names, { verify, format: output });
741
+ else {
742
+ const [name] = names;
743
+ if (!name)
744
+ throw new Error(`\`akan subspace ${base}\` needs a name \u2014 it is reviewed one repo at a time.`);
745
+ if (base === "diff")
746
+ await this.subspaceScript.diff(workspace, name, filePath, output);
827
747
  else
828
- await this.fleetScript.pull(workspace, spoke, { adoptLibs, format: output });
829
- } else
830
- throw new Error(`Unknown fleet action: ${action}. Use status, diff, push, pull or check.`);
748
+ await this.subspaceScript.pull(workspace, name, { adoptLibs, format: output });
749
+ }
831
750
  })
832
751
  })) {
833
752
  }
834
753
  export {
835
- FleetCommand
754
+ SubspaceCommand
836
755
  };
@@ -40,7 +40,7 @@ import {
40
40
  } from "./index-5n0hf8r9.js";
41
41
  import {
42
42
  ApplicationScript
43
- } from "./index-f7wewj2v.js";
43
+ } from "./index-m7e0f1sy.js";
44
44
  import {
45
45
  LibraryScript
46
46
  } from "./index-xxe90kqy.js";
@@ -52,10 +52,10 @@ import {
52
52
  import {
53
53
  getNpmRegistryUrl,
54
54
  getLatestPackageVersion
55
- } from "./index-3c9n3nt8.js";
55
+ } from "./index-1tgp56ke.js";
56
56
  import {
57
57
  CloudScript
58
- } from "./index-ee8z8adx.js";
58
+ } from "./index-ypvdw8nr.js";
59
59
  import {
60
60
  ContextScript
61
61
  } from "./index-dq6wq1tv.js";
@@ -66,7 +66,7 @@ import"./index-k86zc21w.js";
66
66
  import"./index-1kzsn94d.js";
67
67
  import"./index-cd5x18qh.js";
68
68
  import"./index-v0s24t8j.js";
69
- import"./index-xnhf8tr9.js";
69
+ import"./index-m9c7k0gm.js";
70
70
 
71
71
  // pkgs/@akanjs/cli/workspace/workspace.runner.ts
72
72
  import path from "path";