@akanjs/cli 3.0.0-alpha.80 → 3.0.0-alpha.81
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 +1 -1
- package/{application.command-gmcgen9y.js → application.command-4fq5pmdd.js} +2 -2
- package/{cloud.command-gpq7qxrc.js → cloud.command-wm3mtfve.js} +4 -4
- package/commandManifest.json +1 -1
- package/{index-3c9n3nt8.js → index-1tgp56ke.js} +1 -1
- package/{index-f7wewj2v.js → index-m7e0f1sy.js} +1 -1
- package/{index-xnhf8tr9.js → index-m9c7k0gm.js} +57 -7
- package/{index-ee8z8adx.js → index-ypvdw8nr.js} +2 -2
- package/index.js +5 -5
- package/{localRegistry.command-pjdxa01g.js → localRegistry.command-gk773cte.js} +3 -3
- package/package.json +2 -2
- package/{fleet.command-3bkyyd3a.js → subspace.command-3w3s1zas.js} +191 -313
- package/{workspace.command-mbb5s3wx.js → workspace.command-4ztb4fdh.js} +4 -4
package/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1429e054540fb7cf0961c7a6a76cd1b54564627f1534e7f1e38cd437ba24748b
|
|
@@ -11,13 +11,13 @@ import"./index-ayy5ecax.js";
|
|
|
11
11
|
import"./index-6wwzs1bc.js";
|
|
12
12
|
import {
|
|
13
13
|
ApplicationScript
|
|
14
|
-
} from "./index-
|
|
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-
|
|
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-
|
|
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-
|
|
17
|
+
import"./index-1tgp56ke.js";
|
|
18
18
|
import {
|
|
19
19
|
CloudScript
|
|
20
|
-
} from "./index-
|
|
21
|
-
import"./index-
|
|
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";
|
package/commandManifest.json
CHANGED
|
@@ -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","
|
|
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"}}
|
|
@@ -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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
82
|
-
|
|
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
|
-
`
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
27
|
-
|
|
26
|
+
cloud: async () => (await import("./cloud.command-wm3mtfve.js")).CloudCommand,
|
|
27
|
+
subspace: async () => (await import("./subspace.command-3w3s1zas.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-
|
|
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-
|
|
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-
|
|
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.
|
|
3
|
+
"version": "3.0.0-alpha.81",
|
|
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.
|
|
36
|
+
"akanjs": "3.0.0-alpha.81",
|
|
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-
|
|
19
|
+
} from "./index-m9c7k0gm.js";
|
|
21
20
|
import {
|
|
22
21
|
LibSource
|
|
23
22
|
} from "./index-3d9y066c.js";
|
|
24
23
|
|
|
25
|
-
// pkgs/@akanjs/devkit/
|
|
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
|
|
124
|
-
class
|
|
125
|
-
static anchorFile = "akan.
|
|
126
|
-
static
|
|
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
|
|
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-
|
|
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 `
|
|
49
|
+
return `subspace-${this.#declaration.name}`;
|
|
164
50
|
}
|
|
165
51
|
get #clonePath() {
|
|
166
|
-
return
|
|
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, "--",
|
|
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
|
-
#
|
|
96
|
+
#isSubspaceOwned(file) {
|
|
211
97
|
const segments = file.split("/");
|
|
212
|
-
if (!
|
|
98
|
+
if (!Subspace.memberDirs.includes(segments[0] ?? ""))
|
|
213
99
|
return false;
|
|
214
|
-
return
|
|
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.#
|
|
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(
|
|
143
|
+
if (!await FileSys.dirExists(path.join(this.#clonePath, ".git"))) {
|
|
258
144
|
await rm(this.#clonePath, { recursive: true, force: true });
|
|
259
|
-
await mkdir(
|
|
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(`
|
|
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#
|
|
154
|
+
async#holdSubspaceOwned() {
|
|
269
155
|
const held = [];
|
|
270
|
-
const holdRoot =
|
|
156
|
+
const holdRoot = path.join(this.#clonePath, Subspace.holdDir);
|
|
271
157
|
await rm(holdRoot, { recursive: true, force: true });
|
|
272
|
-
for (const member of
|
|
273
|
-
const memberRoot =
|
|
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
|
|
278
|
-
const from =
|
|
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 =
|
|
282
|
-
await mkdir(
|
|
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#
|
|
176
|
+
async#restoreSubspaceOwned(held) {
|
|
291
177
|
for (const { from, to } of held)
|
|
292
|
-
await
|
|
293
|
-
await rm(
|
|
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 =
|
|
299
|
-
const target =
|
|
184
|
+
const source = path.join(from, entry.name);
|
|
185
|
+
const target = path.join(to, entry.name);
|
|
300
186
|
if (entry.isDirectory())
|
|
301
|
-
await
|
|
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 =
|
|
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(
|
|
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 =
|
|
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.#
|
|
344
|
-
for (const member of
|
|
345
|
-
await rm(
|
|
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.#
|
|
349
|
-
for (const entry of [...
|
|
350
|
-
await rm(
|
|
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 =
|
|
261
|
+
const gitignorePath = path.join(this.#clonePath, ".gitignore");
|
|
361
262
|
if (!await FileSys.fileExists(gitignorePath))
|
|
362
263
|
return;
|
|
363
|
-
const [begin = "", end = ""] =
|
|
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 =
|
|
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 =
|
|
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.#
|
|
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(
|
|
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(`
|
|
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.#
|
|
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: "
|
|
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: `
|
|
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 {
|
|
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(
|
|
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(
|
|
530
|
-
|
|
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(`
|
|
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(`
|
|
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.#
|
|
550
|
-
const libFiles = changed.filter((file) => file.startsWith("libs/") && !this.#
|
|
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 =
|
|
565
|
-
await mkdir(
|
|
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:
|
|
455
|
+
return { path: path.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
|
|
568
456
|
}
|
|
569
457
|
}
|
|
570
|
-
function
|
|
458
|
+
function formatSubspaceStatuses(statuses) {
|
|
571
459
|
const sections = [
|
|
572
|
-
"Akan
|
|
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
|
|
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
|
|
479
|
+
function formatSubspacePushResults(results) {
|
|
592
480
|
const sections = [
|
|
593
|
-
"Akan
|
|
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
|
-
|
|
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
|
|
498
|
+
function formatSubspacePullResult(result) {
|
|
606
499
|
const sections = [
|
|
607
|
-
`Akan
|
|
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
|
|
516
|
+
" adopt with: akan subspace pull <name> --adopt-libs"
|
|
624
517
|
] : [],
|
|
625
|
-
...result.ignored.length ? ["", `Ignored (
|
|
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
|
|
523
|
+
function formatSubspaceDiff(result) {
|
|
631
524
|
const total = result.app.files.length + result.libs.files.length;
|
|
632
525
|
const sections = [
|
|
633
|
-
`Akan
|
|
634
|
-
"what `akan
|
|
526
|
+
`Akan Subspace Diff \u2014 ${result.name} (${result.branch})`,
|
|
527
|
+
"what `akan subspace push` would change in the subspace",
|
|
635
528
|
"",
|
|
636
|
-
...total ? [] : ["
|
|
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
|
|
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,169 @@ function formatFleetDiff(result) {
|
|
|
653
546
|
`);
|
|
654
547
|
}
|
|
655
548
|
|
|
656
|
-
// pkgs/@akanjs/cli/
|
|
549
|
+
// pkgs/@akanjs/cli/subspace/subspace.script.ts
|
|
657
550
|
import { Logger } from "akanjs/common";
|
|
658
551
|
|
|
659
|
-
// pkgs/@akanjs/devkit/
|
|
660
|
-
import
|
|
661
|
-
class
|
|
662
|
-
static fileName = "akan.
|
|
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 =
|
|
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
|
|
562
|
+
return new SubspaceConfig(input);
|
|
671
563
|
}
|
|
672
564
|
pushableBranches;
|
|
673
565
|
exclude;
|
|
674
|
-
|
|
675
|
-
spokes;
|
|
566
|
+
subspaces;
|
|
676
567
|
constructor(input) {
|
|
677
|
-
this.pushableBranches = input.pushableBranches ??
|
|
568
|
+
this.pushableBranches = input.pushableBranches ?? SubspaceConfig.defaultPushableBranches;
|
|
678
569
|
this.exclude = input.exclude ?? [];
|
|
679
|
-
this.
|
|
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
|
|
687
|
-
if (!
|
|
688
|
-
throw new Error(`${
|
|
689
|
-
if (seenNames.has(
|
|
690
|
-
throw new Error(`${
|
|
691
|
-
seenNames.add(
|
|
692
|
-
if (!
|
|
693
|
-
throw new Error(`${
|
|
694
|
-
for (const app of
|
|
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(`${
|
|
698
|
-
appOwners.set(app,
|
|
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.
|
|
594
|
+
return this.subspaces;
|
|
705
595
|
return names.map((name) => {
|
|
706
|
-
const
|
|
707
|
-
if (!
|
|
708
|
-
throw new Error(`${
|
|
709
|
-
return
|
|
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. ${
|
|
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/
|
|
720
|
-
class
|
|
721
|
-
async
|
|
722
|
-
const config = await
|
|
609
|
+
// pkgs/@akanjs/cli/subspace/subspace.runner.ts
|
|
610
|
+
class SubspaceRunner extends runner("subspace") {
|
|
611
|
+
async subspaces(workspace, names) {
|
|
612
|
+
const config = await SubspaceConfig.from(workspace);
|
|
723
613
|
if (!config)
|
|
724
|
-
throw new Error(`No ${
|
|
725
|
-
const
|
|
614
|
+
throw new Error(`No ${SubspaceConfig.fileName} at the workspace root. Declare the customer repos there first.`);
|
|
615
|
+
const subspaces = config.select(names).map((declaration) => new Subspace(workspace, config, declaration));
|
|
726
616
|
const branch = (await workspace.spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
727
|
-
return { config,
|
|
617
|
+
return { config, subspaces, branch };
|
|
728
618
|
}
|
|
729
619
|
async status(workspace, names) {
|
|
730
|
-
const {
|
|
620
|
+
const { subspaces, branch } = await this.subspaces(workspace, names);
|
|
731
621
|
const statuses = [];
|
|
732
|
-
for (const
|
|
733
|
-
statuses.push(await
|
|
622
|
+
for (const subspace of subspaces)
|
|
623
|
+
statuses.push(await subspace.status(branch));
|
|
734
624
|
return statuses;
|
|
735
625
|
}
|
|
736
626
|
async push(workspace, names, { verify } = {}) {
|
|
737
|
-
const { config,
|
|
627
|
+
const { config, subspaces, branch } = await this.subspaces(workspace, names);
|
|
738
628
|
config.assertPushable(branch);
|
|
739
629
|
const results = [];
|
|
740
|
-
for (const
|
|
741
|
-
results.push(await
|
|
630
|
+
for (const subspace of subspaces)
|
|
631
|
+
results.push(await subspace.push(branch, { verify }));
|
|
742
632
|
return results;
|
|
743
633
|
}
|
|
744
634
|
async diff(workspace, name, filter) {
|
|
745
|
-
const {
|
|
746
|
-
const [
|
|
747
|
-
if (!
|
|
748
|
-
throw new Error(`Unknown
|
|
749
|
-
return await
|
|
750
|
-
}
|
|
751
|
-
async check(workspace, { staged } = {}) {
|
|
752
|
-
return await new FleetGuard(workspace).check({ staged });
|
|
635
|
+
const { subspaces, branch } = await this.subspaces(workspace, [name]);
|
|
636
|
+
const [subspace] = subspaces;
|
|
637
|
+
if (!subspace)
|
|
638
|
+
throw new Error(`Unknown subspace "${name}"`);
|
|
639
|
+
return await subspace.diff(branch, filter);
|
|
753
640
|
}
|
|
754
641
|
async pull(workspace, name, { adoptLibs } = {}) {
|
|
755
|
-
const {
|
|
756
|
-
const [
|
|
757
|
-
if (!
|
|
758
|
-
throw new Error(`Unknown
|
|
759
|
-
return await
|
|
642
|
+
const { subspaces, branch } = await this.subspaces(workspace, [name]);
|
|
643
|
+
const [subspace] = subspaces;
|
|
644
|
+
if (!subspace)
|
|
645
|
+
throw new Error(`Unknown subspace "${name}"`);
|
|
646
|
+
return await subspace.pull(branch, { adoptLibs });
|
|
760
647
|
}
|
|
761
648
|
}
|
|
762
649
|
|
|
763
|
-
// pkgs/@akanjs/cli/
|
|
764
|
-
class
|
|
650
|
+
// pkgs/@akanjs/cli/subspace/subspace.script.ts
|
|
651
|
+
class SubspaceScript extends script("subspace", [SubspaceRunner]) {
|
|
765
652
|
async status(workspace, names, format = "text") {
|
|
766
|
-
const spinner = workspace.spinning("Checking
|
|
767
|
-
const statuses = await this.
|
|
653
|
+
const spinner = workspace.spinning("Checking subspaces...");
|
|
654
|
+
const statuses = await this.subspaceRunner.status(workspace, names);
|
|
768
655
|
const drifted = statuses.filter((status) => status.driftedLibs.length).length;
|
|
769
|
-
spinner.succeed(`Checked ${statuses.length}
|
|
770
|
-
Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) :
|
|
656
|
+
spinner.succeed(`Checked ${statuses.length} subspace(s) (${drifted} with library drift)`);
|
|
657
|
+
Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : formatSubspaceStatuses(statuses));
|
|
771
658
|
}
|
|
772
659
|
async push(workspace, names, { verify, format }) {
|
|
773
|
-
const spinner = workspace.spinning("Pushing to
|
|
774
|
-
const results = await this.
|
|
660
|
+
const spinner = workspace.spinning("Pushing to subspaces...");
|
|
661
|
+
const results = await this.subspaceRunner.push(workspace, names, { verify });
|
|
775
662
|
const pushed = results.filter((result) => result.outcome === "pushed").length;
|
|
776
|
-
spinner.succeed(`Pushed ${pushed} of ${results.length}
|
|
777
|
-
Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) :
|
|
663
|
+
spinner.succeed(`Pushed ${pushed} of ${results.length} subspace(s)`);
|
|
664
|
+
Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : formatSubspacePushResults(results));
|
|
778
665
|
if (results.some((result) => result.outcome === "refused"))
|
|
779
666
|
process.exitCode = 1;
|
|
780
667
|
}
|
|
781
668
|
async diff(workspace, name, filter, format = "text") {
|
|
782
669
|
const spinner = workspace.spinning(`Diffing ${name}...`);
|
|
783
|
-
const result = await this.
|
|
670
|
+
const result = await this.subspaceRunner.diff(workspace, name, filter);
|
|
784
671
|
const total = result.app.files.length + result.libs.files.length;
|
|
785
672
|
spinner.succeed(`${total} file(s) differ (${result.libs.files.length} in shared libraries)`);
|
|
786
|
-
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) :
|
|
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;
|
|
673
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceDiff(result));
|
|
794
674
|
}
|
|
795
675
|
async pull(workspace, name, { adoptLibs, format }) {
|
|
796
676
|
const spinner = workspace.spinning(`Pulling customer commits from ${name}...`);
|
|
797
|
-
const result = await this.
|
|
677
|
+
const result = await this.subspaceRunner.pull(workspace, name, { adoptLibs });
|
|
798
678
|
spinner.succeed(`${result.incoming.length} customer commit(s), ${result.applied.length} file(s) applied`);
|
|
799
|
-
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) :
|
|
679
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspacePullResult(result));
|
|
800
680
|
}
|
|
801
681
|
}
|
|
802
682
|
|
|
803
|
-
// pkgs/@akanjs/cli/
|
|
804
|
-
class
|
|
805
|
-
|
|
806
|
-
desc: "
|
|
683
|
+
// pkgs/@akanjs/cli/subspace/subspace.command.ts
|
|
684
|
+
class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: target }) => ({
|
|
685
|
+
subspace: target({ desc: "Mirror apps and libraries between this workspace and the customer repos it serves" }).arg("action", String, {
|
|
686
|
+
desc: "subspace action",
|
|
807
687
|
default: "status",
|
|
808
|
-
enum: ["status", "diff", "push", "pull"
|
|
809
|
-
}).arg("
|
|
688
|
+
enum: ["status", "diff", "push", "pull"]
|
|
689
|
+
}).arg("subspace", String, { desc: "subspace name; every subspace when omitted (required for pull)", nullable: true }).with(Workspace).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).option("verify", Boolean, {
|
|
810
690
|
flag: "y",
|
|
811
|
-
desc: "run bun install + akan sync in the
|
|
691
|
+
desc: "run bun install + akan sync in the subspace before committing",
|
|
812
692
|
default: true
|
|
813
|
-
}).option("adoptLibs", Boolean, { desc: "pull: also apply the
|
|
814
|
-
const names =
|
|
693
|
+
}).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) {
|
|
694
|
+
const names = subspace ? [subspace] : [];
|
|
815
695
|
const output = format;
|
|
816
696
|
if (action === "status")
|
|
817
|
-
await this.
|
|
818
|
-
else if (action === "check")
|
|
819
|
-
await this.fleetScript.check(workspace, { staged, warn, format: output });
|
|
697
|
+
await this.subspaceScript.status(workspace, names, output);
|
|
820
698
|
else if (action === "push")
|
|
821
|
-
await this.
|
|
699
|
+
await this.subspaceScript.push(workspace, names, { verify, format: output });
|
|
822
700
|
else if (action === "diff" || action === "pull") {
|
|
823
|
-
if (!
|
|
824
|
-
throw new Error(`\`akan
|
|
701
|
+
if (!subspace)
|
|
702
|
+
throw new Error(`\`akan subspace ${action}\` needs a name \u2014 it is reviewed one repo at a time.`);
|
|
825
703
|
if (action === "diff")
|
|
826
|
-
await this.
|
|
704
|
+
await this.subspaceScript.diff(workspace, subspace, filePath, output);
|
|
827
705
|
else
|
|
828
|
-
await this.
|
|
706
|
+
await this.subspaceScript.pull(workspace, subspace, { adoptLibs, format: output });
|
|
829
707
|
} else
|
|
830
|
-
throw new Error(`Unknown
|
|
708
|
+
throw new Error(`Unknown subspace action: ${action}. Use status, diff, push or pull.`);
|
|
831
709
|
})
|
|
832
710
|
})) {
|
|
833
711
|
}
|
|
834
712
|
export {
|
|
835
|
-
|
|
713
|
+
SubspaceCommand
|
|
836
714
|
};
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from "./index-5n0hf8r9.js";
|
|
41
41
|
import {
|
|
42
42
|
ApplicationScript
|
|
43
|
-
} from "./index-
|
|
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-
|
|
55
|
+
} from "./index-1tgp56ke.js";
|
|
56
56
|
import {
|
|
57
57
|
CloudScript
|
|
58
|
-
} from "./index-
|
|
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-
|
|
69
|
+
import"./index-m9c7k0gm.js";
|
|
70
70
|
|
|
71
71
|
// pkgs/@akanjs/cli/workspace/workspace.runner.ts
|
|
72
72
|
import path from "path";
|