@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.10
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/{CodeAgent-kyprmy06.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-21mtze3j.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-5s780vnz.js → application.command-m1kcd7jf.js} +54 -42
- package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
- package/buildBatch.proc.js +24 -15
- package/{cloud.command-yj6xb2a7.js → cloud.command-9cp1t0f5.js} +60 -67
- package/cloud.script-dgstqt1m.js +24 -0
- package/{code.command-3wkn4ja5.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-9jjn3gcw.js → context.command-0qx447mb.js} +17 -13
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +3 -3
- package/guidelines/conventions/conventions.instruction.md +457 -1071
- package/guidelines/cssRule/cssRule.instruction.md +28 -7
- package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +43 -20
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +44 -11
- package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +135 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
- package/incrementalBuilder.proc.js +10 -7
- package/index-192ha4xh.js +1173 -0
- package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
- package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
- package/{index-rtct7d43.js → index-20bxxyvn.js} +80 -80
- package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
- package/{index-8r4d7cmt.js → index-4mhy6qme.js} +4 -4
- package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
- package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/index-665zp9rs.js +271 -0
- package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
- package/{index-krq7btbt.js → index-8fp3b47h.js} +419 -418
- package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
- package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-rc5rwyps.js → index-bspm21y1.js} +71 -60
- package/index-cpb3nvma.js +667 -0
- package/{index-jx4bzvwg.js → index-d47ypebv.js} +97 -55
- package/{index-ae5pwhgs.js → index-d72hfmdm.js} +15 -13
- package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
- package/{index-8ggztp8k.js → index-fyq1z6kt.js} +68 -201
- package/{index-1w2qaee2.js → index-g1mbppa9.js} +331 -259
- package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
- package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
- package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/index-mdjc93e1.js +33 -0
- package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
- package/{index-56d2g8ms.js → index-qwa5k1j8.js} +1 -1
- package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
- package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
- package/index-vk0pa0mn.js +240 -0
- package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
- package/index.js +22 -22
- package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-vsreyy42.js → localRegistry.command-y176p558.js} +15 -11
- package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
- package/package.json +21 -21
- package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
- package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-md2teygq.js → subspace.command-5c7yasd7.js} +424 -364
- package/templates/appSample/lib/task/Task.Util.tsx +7 -7
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
- package/templates/appSample/ui/QuantityControl.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +23 -52
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/{tunnel.command-jx3tdm2d.js → tunnel.command-2kw8vt9g.js} +8 -4
- package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-yxd7rr7g.js → workspace.command-6qfgq4b4.js} +38 -34
- package/TunnelShare-q4jq08qg.js +0 -9
- package/cloud.script-h6pvcpa2.js +0 -20
- package/context.runner-atcrzz5b.js +0 -21
- package/index-6w0rbr9d.js +0 -68
- package/index-n68jx79w.js +0 -120
- package/index-pgj2rt78.js +0 -404
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
runner,
|
|
6
6
|
script,
|
|
7
7
|
command
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-jw1zm273.js";
|
|
9
9
|
import {
|
|
10
10
|
FileSys2
|
|
11
11
|
} from "./index-52w0fvh8.js";
|
|
@@ -14,21 +14,31 @@ import {
|
|
|
14
14
|
Executor,
|
|
15
15
|
WorkspaceExecutor,
|
|
16
16
|
AppExecutor
|
|
17
|
-
} from "./index-
|
|
17
|
+
} from "./index-va06nr10.js";
|
|
18
18
|
import {
|
|
19
19
|
GlobalConfig
|
|
20
|
-
} from "./index-
|
|
20
|
+
} from "./index-cpb3nvma.js";
|
|
21
|
+
import"./index-m9jj544v.js";
|
|
22
|
+
import"./index-g1mbppa9.js";
|
|
23
|
+
import"./index-192ha4xh.js";
|
|
21
24
|
import {
|
|
22
25
|
SubspaceConfig
|
|
23
|
-
} from "./index-
|
|
26
|
+
} from "./index-4pf8g7ff.js";
|
|
24
27
|
import {
|
|
25
28
|
SlicePlanner
|
|
26
|
-
} from "./index-
|
|
29
|
+
} from "./index-35hc95h5.js";
|
|
30
|
+
import"./index-665zp9rs.js";
|
|
27
31
|
|
|
28
32
|
// pkgs/@akanjs/devkit/subspace.ts
|
|
33
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
34
|
+
import path2 from "path";
|
|
35
|
+
import { toError as toError2 } from "akanjs/common";
|
|
36
|
+
|
|
37
|
+
// pkgs/@akanjs/devkit/subspaceClone.ts
|
|
29
38
|
import { mkdir, readdir, rename, rm } from "fs/promises";
|
|
30
39
|
import path from "path";
|
|
31
|
-
|
|
40
|
+
import { toError } from "akanjs/common";
|
|
41
|
+
class SubspaceClone {
|
|
32
42
|
static anchorFile = "akan.subspace.json";
|
|
33
43
|
static subspaceOwnedRootEntries = ["akan.config.ts", "Jenkinsfile"];
|
|
34
44
|
static syncSubjectPrefix = "chore(subspace): sync from ";
|
|
@@ -36,175 +46,107 @@ class Subspace {
|
|
|
36
46
|
static subspaceOwnedDirs = ["env"];
|
|
37
47
|
static secretsMarkers = ["# akan:secrets (managed by akan.config.ts \u2014 do not edit)", "# akan:secrets:end"];
|
|
38
48
|
static holdDir = ".akan-subspace-hold";
|
|
39
|
-
static
|
|
49
|
+
static isSubspaceOwned(file) {
|
|
50
|
+
const segments = file.split("/");
|
|
51
|
+
if (!SubspaceClone.memberDirs.includes(segments[0] ?? ""))
|
|
52
|
+
return false;
|
|
53
|
+
return SubspaceClone.subspaceOwnedDirs.includes(segments[2] ?? "");
|
|
54
|
+
}
|
|
55
|
+
static async open(workspace, declaration, branch) {
|
|
56
|
+
const clone = new SubspaceClone(workspace, declaration);
|
|
57
|
+
await clone.#ensure(branch);
|
|
58
|
+
return clone;
|
|
59
|
+
}
|
|
40
60
|
#workspace;
|
|
41
|
-
#config;
|
|
42
61
|
#declaration;
|
|
43
|
-
|
|
62
|
+
#clonePath;
|
|
63
|
+
#clone;
|
|
64
|
+
constructor(workspace, declaration) {
|
|
44
65
|
this.#workspace = workspace;
|
|
45
|
-
this.#config = config;
|
|
46
66
|
this.#declaration = declaration;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return this.#declaration.name;
|
|
50
|
-
}
|
|
51
|
-
get apps() {
|
|
52
|
-
return this.#declaration.apps;
|
|
53
|
-
}
|
|
54
|
-
get workspaceId() {
|
|
55
|
-
return this.#declaration.workspaceId;
|
|
56
|
-
}
|
|
57
|
-
get #remote() {
|
|
58
|
-
return `subspace-${this.#declaration.name}`;
|
|
59
|
-
}
|
|
60
|
-
get #clonePath() {
|
|
61
|
-
return path.join(this.#workspace.workspaceRoot, ".akan/subspace", this.#declaration.name);
|
|
62
|
-
}
|
|
63
|
-
async#git(args) {
|
|
64
|
-
return await this.#workspace.spawn("git", args);
|
|
67
|
+
this.#clonePath = path.join(workspace.workspaceRoot, ".akan/subspace", declaration.name);
|
|
68
|
+
this.#clone = new Executor(`subspace-${declaration.name}`, this.#clonePath);
|
|
65
69
|
}
|
|
66
70
|
async#cloneGit(args) {
|
|
67
71
|
return await this.#workspace.spawn("git", ["-C", this.#clonePath, ...args]);
|
|
68
72
|
}
|
|
69
|
-
async
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
async fetch(branch) {
|
|
76
|
-
const remotes = (await this.#git(["remote"])).split(`
|
|
77
|
-
`).map((line) => line.trim());
|
|
78
|
-
if (remotes.includes(this.#remote))
|
|
79
|
-
await this.#git(["remote", "set-url", this.#remote, this.#declaration.repo]);
|
|
80
|
-
else
|
|
81
|
-
await this.#git(["remote", "add", this.#remote, this.#declaration.repo]);
|
|
82
|
-
try {
|
|
83
|
-
await this.#git(["fetch", "--quiet", this.#remote, branch]);
|
|
84
|
-
return true;
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
73
|
+
async#ensure(branch) {
|
|
74
|
+
if (!await FileSys2.dirExists(path.join(this.#clonePath, ".git"))) {
|
|
75
|
+
await rm(this.#clonePath, { recursive: true, force: true });
|
|
76
|
+
await mkdir(path.dirname(this.#clonePath), { recursive: true });
|
|
77
|
+
await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
|
|
87
78
|
}
|
|
79
|
+
await this.#clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
|
|
80
|
+
await this.#clone.spawn("git", ["checkout", "--quiet", "-f", "-B", branch, `origin/${branch}`]);
|
|
81
|
+
await this.#clone.spawn("git", ["clean", "-qfd"]);
|
|
88
82
|
}
|
|
89
|
-
async
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
83
|
+
async applySlice({ libs, paths, plans }, { branch, sha, remove }) {
|
|
84
|
+
const held = await this.#holdSubspaceOwned();
|
|
85
|
+
const heldRootEntries = await this.#holdRootEntries();
|
|
86
|
+
for (const member of SubspaceClone.memberDirs)
|
|
87
|
+
await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
|
|
88
|
+
await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
|
|
89
|
+
await this.#extract(paths);
|
|
90
|
+
await this.#restoreRootEntries(heldRootEntries);
|
|
91
|
+
await this.#restoreSubspaceOwned(held);
|
|
92
|
+
for (const entry of remove)
|
|
93
|
+
if (!SubspaceClone.subspaceOwnedRootEntries.includes(entry))
|
|
94
|
+
await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
|
|
95
|
+
await this.#rewriteGitignore();
|
|
96
|
+
const pruned = await this.#rewriteManifest(plans);
|
|
97
|
+
await this.#rewriteWorkspaceSection(libs);
|
|
98
|
+
await this.#writeLocalEnv();
|
|
99
|
+
for (const lib of libs)
|
|
100
|
+
await this.#stampLib(lib, branch, sha);
|
|
101
|
+
return { libs, pruned };
|
|
101
102
|
}
|
|
102
|
-
async
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { plans, libs, paths };
|
|
103
|
+
async changedFiles() {
|
|
104
|
+
const dirty = (await this.#clone.spawn("git", ["status", "--porcelain"])).trim();
|
|
105
|
+
return dirty ? dirty.split(`
|
|
106
|
+
`).length : 0;
|
|
107
107
|
}
|
|
108
|
-
async
|
|
109
|
-
const {
|
|
110
|
-
const rootFiles = plans[0]?.rootFiles ?? [];
|
|
111
|
-
const present = new Set(rootFiles.map((file) => file.split("/")[0] ?? file));
|
|
112
|
-
const owned = Subspace.subspaceOwnedRootEntries;
|
|
113
|
-
const excluded = this.#config.exclude.filter((entry) => !owned.includes(entry));
|
|
114
|
-
const excludedTops = new Set(excluded.map((entry) => entry.replace(/\/+$/, "")));
|
|
115
|
-
const libFiles = new Map(plans.flatMap((plan) => Object.entries(plan.libFiles)));
|
|
116
|
-
const { pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, await this.#workspace.getPackageJson(), plans.flatMap((plan) => plan.requiredDependencies));
|
|
117
|
-
let branchRefusal = null;
|
|
108
|
+
async verify() {
|
|
109
|
+
const env = { ...process.env, ...this.#localEnv() };
|
|
118
110
|
try {
|
|
119
|
-
this.#
|
|
111
|
+
await this.#clone.spawn("bun", ["install"], { env });
|
|
112
|
+
for (const app of this.#declaration.apps)
|
|
113
|
+
await this.#clone.spawn("bunx", ["akan", "sync", app], { env });
|
|
114
|
+
return null;
|
|
120
115
|
} catch (error) {
|
|
121
|
-
|
|
116
|
+
return toError(error).message;
|
|
122
117
|
}
|
|
123
|
-
return {
|
|
124
|
-
name: this.name,
|
|
125
|
-
repo: this.#declaration.repo,
|
|
126
|
-
branch,
|
|
127
|
-
branchRefusal,
|
|
128
|
-
apps: [...this.#declaration.apps],
|
|
129
|
-
libs: libs.map((lib) => ({
|
|
130
|
-
name: lib,
|
|
131
|
-
apps: plans.filter((plan) => plan.libs.includes(lib)).map((plan) => plan.app)
|
|
132
|
-
})),
|
|
133
|
-
files: plans.reduce((sum, plan) => sum + plan.appFiles.length, 0) + [...libFiles.values()].reduce((sum, files) => sum + files.length, 0),
|
|
134
|
-
rootEntries: [...present].filter((entry) => !excludedTops.has(entry) && !owned.includes(entry)).sort((a, b) => a.localeCompare(b)),
|
|
135
|
-
excludedRootEntries: excluded,
|
|
136
|
-
subspaceOwnedRootEntries: owned.filter((entry) => present.has(entry)),
|
|
137
|
-
prunedDependencies: pruned,
|
|
138
|
-
lockProblem: rootFiles.includes("bun.lock") ? await this.#loopbackRegistryLock(this.#workspace.workspaceRoot) : null,
|
|
139
|
-
warnings: [...new Set([...plans.flatMap((plan) => plan.warnings), ...warnings])]
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
#isSubspaceOwned(file) {
|
|
143
|
-
const segments = file.split("/");
|
|
144
|
-
if (!Subspace.memberDirs.includes(segments[0] ?? ""))
|
|
145
|
-
return false;
|
|
146
|
-
return Subspace.subspaceOwnedDirs.includes(segments[2] ?? "");
|
|
147
118
|
}
|
|
148
|
-
|
|
149
|
-
|
|
119
|
+
async lock() {
|
|
120
|
+
const lockPath = path.join(this.#clonePath, "bun.lock");
|
|
121
|
+
return await FileSys2.fileExists(lockPath) ? await FileSys2.readText(lockPath) : null;
|
|
150
122
|
}
|
|
151
|
-
async
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const normalize = (content) => {
|
|
159
|
-
try {
|
|
160
|
-
return LibSource.unstamped(content);
|
|
161
|
-
} catch {
|
|
162
|
-
return content;
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
return normalize(theirs) !== normalize(ours);
|
|
123
|
+
async commit(branch, sha) {
|
|
124
|
+
await this.#writeAnchorFile(branch, sha);
|
|
125
|
+
await this.#clone.spawn("git", ["add", "-A"]);
|
|
126
|
+
const message = `${SubspaceClone.syncSubjectPrefix}${this.#workspace.repoName}@${sha}`;
|
|
127
|
+
await this.#clone.spawn("git", ["commit", "--quiet", "-m", message]);
|
|
128
|
+
await this.#clone.spawn("git", ["push", "--quiet", "origin", branch]);
|
|
129
|
+
return (await this.#clone.spawn("git", ["rev-parse", "--short", "HEAD"])).trim();
|
|
166
130
|
}
|
|
167
|
-
async#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const [{ paths, libs }, anchor] = await Promise.all([
|
|
176
|
-
this.slice(),
|
|
177
|
-
hasBranch ? this.anchor(branch) : Promise.resolve({ commit: null, incoming: [] })
|
|
178
|
-
]);
|
|
179
|
-
if (!hasBranch)
|
|
180
|
-
return { name: this.name, branch, hasBranch, anchor, behindPaths: [], driftedLibs: [] };
|
|
181
|
-
const ref = `${this.#remote}/${branch}`;
|
|
182
|
-
const behindPaths = await this.#changedSliceFiles(ref, ["HEAD", ref, "--", ...paths]);
|
|
183
|
-
const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
|
|
184
|
-
return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
|
|
185
|
-
}
|
|
186
|
-
async#ensureClone(branch) {
|
|
187
|
-
if (!await FileSys2.dirExists(path.join(this.#clonePath, ".git"))) {
|
|
188
|
-
await rm(this.#clonePath, { recursive: true, force: true });
|
|
189
|
-
await mkdir(path.dirname(this.#clonePath), { recursive: true });
|
|
190
|
-
await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
|
|
191
|
-
}
|
|
192
|
-
const clone = new Executor(`subspace-${this.name}`, this.#clonePath);
|
|
193
|
-
await clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
|
|
194
|
-
await clone.spawn("git", ["checkout", "--quiet", "-f", "-B", branch, `origin/${branch}`]);
|
|
195
|
-
await clone.spawn("git", ["clean", "-qfd"]);
|
|
196
|
-
return clone;
|
|
131
|
+
async#writeAnchorFile(branch, sha) {
|
|
132
|
+
await FileSys2.writeJson(path.join(this.#clonePath, SubspaceClone.anchorFile), {
|
|
133
|
+
workspace: this.#workspace.repoName,
|
|
134
|
+
hubSha: sha,
|
|
135
|
+
branch,
|
|
136
|
+
apps: this.#declaration.apps,
|
|
137
|
+
syncedAt: new Date().toISOString()
|
|
138
|
+
});
|
|
197
139
|
}
|
|
198
140
|
async#holdSubspaceOwned() {
|
|
199
141
|
const held = [];
|
|
200
|
-
const holdRoot = path.join(this.#clonePath,
|
|
142
|
+
const holdRoot = path.join(this.#clonePath, SubspaceClone.holdDir);
|
|
201
143
|
await rm(holdRoot, { recursive: true, force: true });
|
|
202
|
-
for (const member of
|
|
144
|
+
for (const member of SubspaceClone.memberDirs) {
|
|
203
145
|
const memberRoot = path.join(this.#clonePath, member);
|
|
204
146
|
if (!await FileSys2.dirExists(memberRoot))
|
|
205
147
|
continue;
|
|
206
148
|
for (const name of await readdir(memberRoot)) {
|
|
207
|
-
for (const owned of
|
|
149
|
+
for (const owned of SubspaceClone.subspaceOwnedDirs) {
|
|
208
150
|
const from = path.join(memberRoot, name, owned);
|
|
209
151
|
if (!await FileSys2.dirExists(from))
|
|
210
152
|
continue;
|
|
@@ -219,13 +161,13 @@ class Subspace {
|
|
|
219
161
|
}
|
|
220
162
|
async#restoreSubspaceOwned(held) {
|
|
221
163
|
for (const { from, to } of held)
|
|
222
|
-
await
|
|
223
|
-
await rm(path.join(this.#clonePath,
|
|
164
|
+
await SubspaceClone.#copyMissing(to, from);
|
|
165
|
+
await rm(path.join(this.#clonePath, SubspaceClone.holdDir), { recursive: true, force: true });
|
|
224
166
|
}
|
|
225
167
|
async#holdRootEntries() {
|
|
226
|
-
const holdRoot = path.join(this.#clonePath,
|
|
168
|
+
const holdRoot = path.join(this.#clonePath, SubspaceClone.holdDir, "root");
|
|
227
169
|
const held = [];
|
|
228
|
-
for (const entry of
|
|
170
|
+
for (const entry of SubspaceClone.subspaceOwnedRootEntries) {
|
|
229
171
|
const from = path.join(this.#clonePath, entry);
|
|
230
172
|
if (!await FileSys2.entryExists(from))
|
|
231
173
|
continue;
|
|
@@ -238,8 +180,8 @@ class Subspace {
|
|
|
238
180
|
return held;
|
|
239
181
|
}
|
|
240
182
|
async#restoreRootEntries(held) {
|
|
241
|
-
const holdRoot = path.join(this.#clonePath,
|
|
242
|
-
for (const entry of
|
|
183
|
+
const holdRoot = path.join(this.#clonePath, SubspaceClone.holdDir, "root");
|
|
184
|
+
for (const entry of SubspaceClone.subspaceOwnedRootEntries) {
|
|
243
185
|
const target = path.join(this.#clonePath, entry);
|
|
244
186
|
await rm(target, { recursive: true, force: true });
|
|
245
187
|
if (held.includes(entry))
|
|
@@ -248,7 +190,7 @@ class Subspace {
|
|
|
248
190
|
}
|
|
249
191
|
async#isPushedCopy(entry) {
|
|
250
192
|
const subject = await this.#cloneGit(["log", "-1", "--format=%s", "--", entry]).catch(() => "");
|
|
251
|
-
return subject.trim().startsWith(
|
|
193
|
+
return subject.trim().startsWith(SubspaceClone.syncSubjectPrefix);
|
|
252
194
|
}
|
|
253
195
|
static async#copyMissing(from, to) {
|
|
254
196
|
await mkdir(to, { recursive: true });
|
|
@@ -256,7 +198,7 @@ class Subspace {
|
|
|
256
198
|
const source = path.join(from, entry.name);
|
|
257
199
|
const target = path.join(to, entry.name);
|
|
258
200
|
if (entry.isDirectory())
|
|
259
|
-
await
|
|
201
|
+
await SubspaceClone.#copyMissing(source, target);
|
|
260
202
|
else if (!await FileSys2.entryExists(target))
|
|
261
203
|
await rename(source, target);
|
|
262
204
|
}
|
|
@@ -278,11 +220,11 @@ class Subspace {
|
|
|
278
220
|
}
|
|
279
221
|
}
|
|
280
222
|
#localEnv() {
|
|
281
|
-
const { serveDomain } = WorkspaceExecutor.getBaseDevEnv();
|
|
223
|
+
const { serveDomain, defaulted = [] } = WorkspaceExecutor.getBaseDevEnv();
|
|
282
224
|
return {
|
|
283
225
|
AKAN_WORKSPACE_ID: "local",
|
|
284
|
-
AKAN_PUBLIC_REPO_NAME: this.name,
|
|
285
|
-
AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
|
|
226
|
+
AKAN_PUBLIC_REPO_NAME: this.#declaration.name,
|
|
227
|
+
...defaulted.includes("AKAN_PUBLIC_SERVE_DOMAIN") ? {} : { AKAN_PUBLIC_SERVE_DOMAIN: serveDomain },
|
|
286
228
|
AKAN_PUBLIC_ENV: "local",
|
|
287
229
|
AKAN_PUBLIC_OPERATION_MODE: "local",
|
|
288
230
|
AKAN_PUBLIC_LOG_LEVEL: "warn"
|
|
@@ -317,37 +259,16 @@ class Subspace {
|
|
|
317
259
|
this.#workspace.logger.warn(warning);
|
|
318
260
|
await FileSys2.writeJson(manifestPath, {
|
|
319
261
|
...packageJson,
|
|
320
|
-
name: this.name,
|
|
321
|
-
description: `${this.name} workspace`
|
|
262
|
+
name: this.#declaration.name,
|
|
263
|
+
description: `${this.#declaration.name} workspace`
|
|
322
264
|
});
|
|
323
265
|
return pruned;
|
|
324
266
|
}
|
|
325
|
-
async#applySlice(clone, branch) {
|
|
326
|
-
const { libs, paths, plans } = await this.slice();
|
|
327
|
-
const held = await this.#holdSubspaceOwned();
|
|
328
|
-
const heldRootEntries = await this.#holdRootEntries();
|
|
329
|
-
for (const member of Subspace.memberDirs)
|
|
330
|
-
await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
|
|
331
|
-
await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
|
|
332
|
-
await this.#extract(paths);
|
|
333
|
-
await this.#restoreRootEntries(heldRootEntries);
|
|
334
|
-
await this.#restoreSubspaceOwned(held);
|
|
335
|
-
for (const entry of this.#config.exclude)
|
|
336
|
-
if (!Subspace.subspaceOwnedRootEntries.includes(entry))
|
|
337
|
-
await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
|
|
338
|
-
await this.#rewriteGitignore();
|
|
339
|
-
const pruned = await this.#rewriteManifest(plans);
|
|
340
|
-
await this.#rewriteWorkspaceSection(libs);
|
|
341
|
-
await this.#writeLocalEnv();
|
|
342
|
-
for (const lib of libs)
|
|
343
|
-
await this.#stampLib(clone, lib, branch);
|
|
344
|
-
return { libs, pruned };
|
|
345
|
-
}
|
|
346
267
|
async#rewriteGitignore() {
|
|
347
268
|
const gitignorePath = path.join(this.#clonePath, ".gitignore");
|
|
348
269
|
if (!await FileSys2.fileExists(gitignorePath))
|
|
349
270
|
return;
|
|
350
|
-
const [begin = "", end = ""] =
|
|
271
|
+
const [begin = "", end = ""] = SubspaceClone.secretsMarkers;
|
|
351
272
|
const lines = (await FileSys2.readText(gitignorePath)).split(/\r?\n/);
|
|
352
273
|
const beginIdx = lines.indexOf(begin);
|
|
353
274
|
const endIdx = lines.indexOf(end);
|
|
@@ -361,7 +282,7 @@ class Subspace {
|
|
|
361
282
|
}
|
|
362
283
|
async#rewriteWorkspaceSection(libs) {
|
|
363
284
|
const replacements = [
|
|
364
|
-
[/^- Repo: .*$/m, `- Repo: ${this.name}`],
|
|
285
|
+
[/^- Repo: .*$/m, `- Repo: ${this.#declaration.name}`],
|
|
365
286
|
[/^- Apps: .*$/m, `- Apps: ${this.#declaration.apps.join(", ")}`],
|
|
366
287
|
[/^- Libraries: .*$/m, `- Libraries: ${libs.length ? libs.join(", ") : "(none)"}`]
|
|
367
288
|
];
|
|
@@ -375,13 +296,12 @@ class Subspace {
|
|
|
375
296
|
await FileSys2.writeText(filePath, rewritten);
|
|
376
297
|
}
|
|
377
298
|
}
|
|
378
|
-
async#stampLib(
|
|
299
|
+
async#stampLib(lib, branch, sha) {
|
|
379
300
|
const manifestPath = path.join(this.#clonePath, "libs", lib, "package.json");
|
|
380
301
|
if (!await FileSys2.fileExists(manifestPath))
|
|
381
302
|
return;
|
|
382
303
|
const origin = `${this.#workspace.repoName}#${branch}`;
|
|
383
|
-
const
|
|
384
|
-
const [hash, previous] = await Promise.all([this.#hashLib(clone, lib), this.#committedStamp(clone, lib)]);
|
|
304
|
+
const [hash, previous] = await Promise.all([this.#hashLib(lib), this.#committedStamp(lib)]);
|
|
385
305
|
const manifest = await FileSys2.readJson(manifestPath);
|
|
386
306
|
const akan = manifest[LibSource.manifestKey] ?? {};
|
|
387
307
|
const unchanged = previous?.origin === origin && previous.hash === hash;
|
|
@@ -389,9 +309,9 @@ class Subspace {
|
|
|
389
309
|
manifest[LibSource.manifestKey] = { ...akan, source };
|
|
390
310
|
await FileSys2.writeJson(manifestPath, manifest);
|
|
391
311
|
}
|
|
392
|
-
async#committedStamp(
|
|
312
|
+
async#committedStamp(lib) {
|
|
393
313
|
try {
|
|
394
|
-
const committed = await clone.spawn("git", ["show", `HEAD:libs/${lib}/package.json`]);
|
|
314
|
+
const committed = await this.#clone.spawn("git", ["show", `HEAD:libs/${lib}/package.json`]);
|
|
395
315
|
const manifest = JSON.parse(committed);
|
|
396
316
|
const akan = manifest[LibSource.manifestKey] ?? {};
|
|
397
317
|
return akan.source;
|
|
@@ -399,8 +319,8 @@ class Subspace {
|
|
|
399
319
|
return;
|
|
400
320
|
}
|
|
401
321
|
}
|
|
402
|
-
async#hashLib(
|
|
403
|
-
const listed = await clone.spawn("git", [
|
|
322
|
+
async#hashLib(lib) {
|
|
323
|
+
const listed = await this.#clone.spawn("git", [
|
|
404
324
|
"ls-files",
|
|
405
325
|
"-z",
|
|
406
326
|
"--cached",
|
|
@@ -409,11 +329,165 @@ class Subspace {
|
|
|
409
329
|
"--",
|
|
410
330
|
`libs/${lib}`
|
|
411
331
|
]);
|
|
412
|
-
const candidates = listed.split("\x00").filter((file) => !!file && !
|
|
332
|
+
const candidates = listed.split("\x00").filter((file) => !!file && !SubspaceClone.isSubspaceOwned(file)).sort();
|
|
413
333
|
const present = await Promise.all(candidates.map(async (file) => await FileSys2.entryExists(path.join(this.#clonePath, file))));
|
|
414
334
|
const files = candidates.filter((_, index) => present[index]);
|
|
415
335
|
return await LibSource.hashFiles(files, `libs/${lib}/package.json`, (file) => FileSys2.readText(path.join(this.#clonePath, file)));
|
|
416
336
|
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// pkgs/@akanjs/devkit/subspace.ts
|
|
340
|
+
class Subspace {
|
|
341
|
+
static retiredConfigFile = "akan.subspace.ts";
|
|
342
|
+
static #retiredConfigRefusal = `${Subspace.retiredConfigFile} is no longer read: move it under \`subspace\` in akan.config.ts and delete it`;
|
|
343
|
+
static #loopbackRegistry = /https?:\/\/(?:127\.0\.0\.1|localhost|0\.0\.0\.0|\[::1\])(?::\d+)?/;
|
|
344
|
+
#workspace;
|
|
345
|
+
#config;
|
|
346
|
+
#declaration;
|
|
347
|
+
constructor(workspace, config, declaration) {
|
|
348
|
+
this.#workspace = workspace;
|
|
349
|
+
this.#config = config;
|
|
350
|
+
this.#declaration = declaration;
|
|
351
|
+
}
|
|
352
|
+
get name() {
|
|
353
|
+
return this.#declaration.name;
|
|
354
|
+
}
|
|
355
|
+
get apps() {
|
|
356
|
+
return this.#declaration.apps;
|
|
357
|
+
}
|
|
358
|
+
get workspaceId() {
|
|
359
|
+
return this.#declaration.workspaceId;
|
|
360
|
+
}
|
|
361
|
+
get #remote() {
|
|
362
|
+
return `subspace-${this.#declaration.name}`;
|
|
363
|
+
}
|
|
364
|
+
async#git(args) {
|
|
365
|
+
return await this.#workspace.spawn("git", args);
|
|
366
|
+
}
|
|
367
|
+
async branch() {
|
|
368
|
+
return (await this.#git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
369
|
+
}
|
|
370
|
+
async headSha() {
|
|
371
|
+
return (await this.#git(["rev-parse", "--short", "HEAD"])).trim();
|
|
372
|
+
}
|
|
373
|
+
async fetch(branch) {
|
|
374
|
+
const remotes = (await this.#git(["remote"])).split(`
|
|
375
|
+
`).map((line) => line.trim());
|
|
376
|
+
if (remotes.includes(this.#remote))
|
|
377
|
+
await this.#git(["remote", "set-url", this.#remote, this.#declaration.repo]);
|
|
378
|
+
else
|
|
379
|
+
await this.#git(["remote", "add", this.#remote, this.#declaration.repo]);
|
|
380
|
+
try {
|
|
381
|
+
await this.#git(["fetch", "--quiet", this.#remote, branch]);
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async anchor(branch) {
|
|
388
|
+
const ref = `${this.#remote}/${branch}`;
|
|
389
|
+
const commit = (await this.#git(["log", "-1", "--format=%H", ref, "--", SubspaceClone.anchorFile])).trim();
|
|
390
|
+
if (!commit)
|
|
391
|
+
return { commit: null, incoming: [] };
|
|
392
|
+
const log = await this.#git(["log", "--format=%H%x09%an%x09%s", `${commit}..${ref}`]);
|
|
393
|
+
const incoming = log.split(`
|
|
394
|
+
`).filter((line) => !!line.trim()).map((line) => {
|
|
395
|
+
const [sha = "", author = "", ...subject] = line.split("\t");
|
|
396
|
+
return { sha: sha.slice(0, 12), author, subject: subject.join("\t") };
|
|
397
|
+
});
|
|
398
|
+
return { commit, incoming };
|
|
399
|
+
}
|
|
400
|
+
async slice() {
|
|
401
|
+
const plans = await Promise.all(this.#declaration.apps.map(async (appName) => await new SlicePlanner(AppExecutor.from(this.#workspace, appName)).plan()));
|
|
402
|
+
const libs = [...new Set(plans.flatMap((plan) => plan.libs))].sort();
|
|
403
|
+
const paths = [...this.#declaration.apps.map((app) => `apps/${app}`), ...libs.map((lib) => `libs/${lib}`)];
|
|
404
|
+
return { plans, libs, paths };
|
|
405
|
+
}
|
|
406
|
+
async plan(branch) {
|
|
407
|
+
const workspaceRefusal = await this.#workspaceRefusal();
|
|
408
|
+
const { plans, libs } = await this.slice();
|
|
409
|
+
const rootFiles = plans[0]?.rootFiles ?? [];
|
|
410
|
+
const present = new Set(SlicePlanner.rootEntriesOf(rootFiles));
|
|
411
|
+
const owned = SubspaceClone.subspaceOwnedRootEntries;
|
|
412
|
+
const excluded = this.#config.exclude.filter((entry) => !owned.includes(entry));
|
|
413
|
+
const excludedTops = new Set(excluded.map((entry) => entry.replace(/\/+$/, "")));
|
|
414
|
+
const libFiles = new Map(plans.flatMap((plan) => Object.entries(plan.libFiles)));
|
|
415
|
+
const { pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, await this.#workspace.getPackageJson(), plans.flatMap((plan) => plan.requiredDependencies));
|
|
416
|
+
let branchRefusal = null;
|
|
417
|
+
try {
|
|
418
|
+
this.#config.assertPushable(branch);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
branchRefusal = toError2(error).message;
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
name: this.name,
|
|
424
|
+
repo: this.#declaration.repo,
|
|
425
|
+
branch,
|
|
426
|
+
branchRefusal,
|
|
427
|
+
workspaceRefusal,
|
|
428
|
+
apps: [...this.#declaration.apps],
|
|
429
|
+
libs: libs.map((lib) => ({
|
|
430
|
+
name: lib,
|
|
431
|
+
apps: plans.filter((plan) => plan.libs.includes(lib)).map((plan) => plan.app)
|
|
432
|
+
})),
|
|
433
|
+
files: plans.reduce((sum, plan) => sum + plan.appFiles.length, 0) + [...libFiles.values()].reduce((sum, files) => sum + files.length, 0),
|
|
434
|
+
rootEntries: [...present].filter((entry) => !excludedTops.has(entry) && !owned.includes(entry) && entry !== Subspace.retiredConfigFile).sort((a, b) => a.localeCompare(b)),
|
|
435
|
+
excludedRootEntries: excluded,
|
|
436
|
+
subspaceOwnedRootEntries: owned.filter((entry) => present.has(entry)),
|
|
437
|
+
prunedDependencies: pruned,
|
|
438
|
+
lockProblem: await this.#headLockProblem(),
|
|
439
|
+
warnings: [...new Set([...plans.flatMap((plan) => plan.warnings), ...warnings])]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
static planBlockers(plan) {
|
|
443
|
+
return [plan.branchRefusal, plan.workspaceRefusal, plan.lockProblem].filter((reason) => !!reason);
|
|
444
|
+
}
|
|
445
|
+
async#retiredConfigLeft() {
|
|
446
|
+
return await FileSys2.fileExists(path2.join(this.#workspace.workspaceRoot, Subspace.retiredConfigFile));
|
|
447
|
+
}
|
|
448
|
+
async#workspaceRefusal() {
|
|
449
|
+
if (await this.#retiredConfigLeft())
|
|
450
|
+
return Subspace.#retiredConfigRefusal;
|
|
451
|
+
return await this.#workspace.hasChanges() ? "workspace working tree is dirty" : null;
|
|
452
|
+
}
|
|
453
|
+
#isStamped(file) {
|
|
454
|
+
return /^libs\/[^/]+\/package\.json$/.test(file);
|
|
455
|
+
}
|
|
456
|
+
async#differsBeyondStamp(ref, file) {
|
|
457
|
+
const [theirs, ours] = await Promise.all([
|
|
458
|
+
this.#git(["show", `${ref}:${file}`]).catch(() => ""),
|
|
459
|
+
this.#git(["show", `HEAD:${file}`]).catch(() => "")
|
|
460
|
+
]);
|
|
461
|
+
if (!theirs || !ours)
|
|
462
|
+
return true;
|
|
463
|
+
const normalize = (content) => {
|
|
464
|
+
try {
|
|
465
|
+
return LibSource.unstamped(content);
|
|
466
|
+
} catch {
|
|
467
|
+
return content;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
return normalize(theirs) !== normalize(ours);
|
|
471
|
+
}
|
|
472
|
+
async#changedSliceFiles(ref, diffArgs) {
|
|
473
|
+
const files = (await this.#git(["diff", "--name-only", ...diffArgs])).split(`
|
|
474
|
+
`).filter((file) => !!file.trim() && !SubspaceClone.isSubspaceOwned(file));
|
|
475
|
+
const kept = await Promise.all(files.map(async (file) => this.#isStamped(file) ? await this.#differsBeyondStamp(ref, file) : true));
|
|
476
|
+
return files.filter((_, index) => kept[index]);
|
|
477
|
+
}
|
|
478
|
+
async status(branch) {
|
|
479
|
+
const hasBranch = await this.fetch(branch);
|
|
480
|
+
const [{ paths, libs }, anchor] = await Promise.all([
|
|
481
|
+
this.slice(),
|
|
482
|
+
hasBranch ? this.anchor(branch) : Promise.resolve({ commit: null, incoming: [] })
|
|
483
|
+
]);
|
|
484
|
+
if (!hasBranch)
|
|
485
|
+
return { name: this.name, branch, hasBranch, anchor, behindPaths: [], driftedLibs: [] };
|
|
486
|
+
const ref = `${this.#remote}/${branch}`;
|
|
487
|
+
const behindPaths = await this.#changedSliceFiles(ref, ["HEAD", ref, "--", ...paths]);
|
|
488
|
+
const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
|
|
489
|
+
return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
|
|
490
|
+
}
|
|
417
491
|
async diff(branch, filter) {
|
|
418
492
|
if (!await this.fetch(branch))
|
|
419
493
|
throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
|
|
@@ -436,34 +510,28 @@ class Subspace {
|
|
|
436
510
|
return { files: [], patch: "" };
|
|
437
511
|
return { files, patch: await this.#git(["diff", from, "HEAD", "--", ...files]) };
|
|
438
512
|
}
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
if (!await FileSys2.fileExists(lockPath))
|
|
442
|
-
return null;
|
|
443
|
-
const registry = (await FileSys2.readText(lockPath)).match(Subspace.#loopbackRegistry)?.[0];
|
|
513
|
+
static #loopbackRegistryProblem(lock) {
|
|
514
|
+
const registry = lock?.match(Subspace.#loopbackRegistry)?.[0];
|
|
444
515
|
return registry ? `bun.lock resolves packages from ${registry}, a registry only this machine reaches; reinstall from a public registry and commit that lock first` : null;
|
|
445
516
|
}
|
|
446
|
-
async#
|
|
447
|
-
|
|
448
|
-
try {
|
|
449
|
-
await clone.spawn("bun", ["install"], { env });
|
|
450
|
-
for (const app of this.#declaration.apps)
|
|
451
|
-
await clone.spawn("bunx", ["akan", "sync", app], { env });
|
|
452
|
-
return null;
|
|
453
|
-
} catch (error) {
|
|
454
|
-
return error instanceof Error ? error.message : String(error);
|
|
455
|
-
}
|
|
517
|
+
async#headLockProblem() {
|
|
518
|
+
return Subspace.#loopbackRegistryProblem(await this.#git(["show", "HEAD:bun.lock"]).catch(() => null));
|
|
456
519
|
}
|
|
457
520
|
async push(branch, { verify = true } = {}) {
|
|
458
521
|
this.#config.assertPushable(branch);
|
|
459
|
-
|
|
460
|
-
|
|
522
|
+
const refusal = await this.#workspaceRefusal() ?? await this.#headLockProblem();
|
|
523
|
+
if (refusal)
|
|
524
|
+
return { name: this.name, outcome: "refused", reason: refusal, changedFiles: 0 };
|
|
461
525
|
if (!await this.fetch(branch))
|
|
462
526
|
return { name: this.name, outcome: "refused", reason: `subspace has no branch "${branch}"`, changedFiles: 0 };
|
|
463
|
-
const clone = await this.#
|
|
464
|
-
const { pruned } = await
|
|
465
|
-
|
|
466
|
-
|
|
527
|
+
const clone = await SubspaceClone.open(this.#workspace, this.#declaration, branch);
|
|
528
|
+
const { pruned } = await clone.applySlice(await this.slice(), {
|
|
529
|
+
branch,
|
|
530
|
+
sha: await this.headSha(),
|
|
531
|
+
remove: [Subspace.retiredConfigFile, ...this.#config.exclude]
|
|
532
|
+
});
|
|
533
|
+
const changedFiles = await clone.changedFiles();
|
|
534
|
+
if (!changedFiles)
|
|
467
535
|
return {
|
|
468
536
|
name: this.name,
|
|
469
537
|
outcome: "skipped",
|
|
@@ -471,52 +539,38 @@ class Subspace {
|
|
|
471
539
|
changedFiles: 0,
|
|
472
540
|
prunedDependencies: pruned
|
|
473
541
|
};
|
|
474
|
-
const changedFiles = dirty.split(`
|
|
475
|
-
`).length;
|
|
476
542
|
if (verify) {
|
|
477
|
-
const failure = await
|
|
543
|
+
const failure = await clone.verify();
|
|
478
544
|
if (failure)
|
|
479
545
|
return { name: this.name, outcome: "refused", reason: failure, changedFiles };
|
|
480
546
|
}
|
|
481
|
-
const lockProblem = await
|
|
547
|
+
const lockProblem = Subspace.#loopbackRegistryProblem(await clone.lock());
|
|
482
548
|
if (lockProblem)
|
|
483
549
|
return { name: this.name, outcome: "refused", reason: lockProblem, changedFiles };
|
|
484
|
-
await this.#writeAnchorFile(branch);
|
|
485
|
-
await clone.spawn("git", ["add", "-A"]);
|
|
486
|
-
const message = `${Subspace.syncSubjectPrefix}${this.#workspace.repoName}@${await this.headSha()}`;
|
|
487
|
-
await clone.spawn("git", ["commit", "--quiet", "-m", message]);
|
|
488
|
-
await clone.spawn("git", ["push", "--quiet", "origin", branch]);
|
|
489
550
|
return {
|
|
490
551
|
name: this.name,
|
|
491
552
|
outcome: "pushed",
|
|
492
|
-
commit:
|
|
553
|
+
commit: await clone.commit(branch, await this.headSha()),
|
|
493
554
|
changedFiles,
|
|
494
555
|
prunedDependencies: pruned
|
|
495
556
|
};
|
|
496
557
|
}
|
|
497
|
-
async#writeAnchorFile(branch) {
|
|
498
|
-
await FileSys2.writeJson(path.join(this.#clonePath, Subspace.anchorFile), {
|
|
499
|
-
workspace: this.#workspace.repoName,
|
|
500
|
-
hubSha: await this.headSha(),
|
|
501
|
-
branch,
|
|
502
|
-
apps: this.#declaration.apps,
|
|
503
|
-
syncedAt: new Date().toISOString()
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
558
|
async pull(branch, { adoptLibs = false } = {}) {
|
|
559
|
+
if (await this.#retiredConfigLeft())
|
|
560
|
+
throw new Error(Subspace.#retiredConfigRefusal);
|
|
507
561
|
if (!await this.fetch(branch))
|
|
508
562
|
throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
|
|
509
563
|
const anchor = await this.anchor(branch);
|
|
510
564
|
if (!anchor.commit)
|
|
511
|
-
throw new Error(`Subspace "${this.name}" carries no ${
|
|
565
|
+
throw new Error(`Subspace "${this.name}" carries no ${SubspaceClone.anchorFile} \u2014 it has never received a push`);
|
|
512
566
|
if (!anchor.incoming.length)
|
|
513
567
|
return { name: this.name, applied: [], libPatch: null, ignored: [], incoming: [] };
|
|
514
568
|
const range = `${anchor.commit}..${this.#remote}/${branch}`;
|
|
515
569
|
const changed = (await this.#git(["diff", "--name-only", range])).split(`
|
|
516
570
|
`).filter((file) => !!file.trim());
|
|
517
571
|
const appPrefixes = this.#declaration.apps.map((app) => `apps/${app}/`);
|
|
518
|
-
const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !
|
|
519
|
-
const libFiles = changed.filter((file) => file.startsWith("libs/") && !
|
|
572
|
+
const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !SubspaceClone.isSubspaceOwned(file));
|
|
573
|
+
const libFiles = changed.filter((file) => file.startsWith("libs/") && !SubspaceClone.isSubspaceOwned(file));
|
|
520
574
|
const ignored = changed.filter((file) => !appFiles.includes(file) && !libFiles.includes(file));
|
|
521
575
|
const applied = adoptLibs ? [...appFiles, ...libFiles] : appFiles;
|
|
522
576
|
if (applied.length)
|
|
@@ -530,145 +584,149 @@ class Subspace {
|
|
|
530
584
|
await this.#git(["apply", "--3way", absolute]);
|
|
531
585
|
}
|
|
532
586
|
async#savePatch(range, files, label) {
|
|
533
|
-
const patchPath =
|
|
534
|
-
await
|
|
587
|
+
const patchPath = path2.join(this.#workspace.workspaceRoot, ".akan/subspace", `${this.name}-${label}.patch`);
|
|
588
|
+
await mkdir2(path2.dirname(patchPath), { recursive: true });
|
|
535
589
|
await FileSys2.writeText(patchPath, await this.#git(["diff", range, "--", ...files]));
|
|
536
|
-
return { path:
|
|
590
|
+
return { path: path2.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
|
|
537
591
|
}
|
|
538
592
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
|
|
593
|
+
|
|
594
|
+
// pkgs/@akanjs/devkit/subspaceReport.ts
|
|
595
|
+
class SubspaceReport {
|
|
596
|
+
static statuses(statuses) {
|
|
597
|
+
const sections = [
|
|
598
|
+
"Akan Subspace Status",
|
|
599
|
+
`branch: ${statuses[0]?.branch ?? "(none)"}`,
|
|
600
|
+
"",
|
|
601
|
+
...statuses.flatMap((status) => {
|
|
602
|
+
if (!status.hasBranch)
|
|
603
|
+
return [` ${status.name}: no such branch in the subspace \u2014 push is refused`];
|
|
604
|
+
const behind = status.behindPaths.length ? `${status.behindPaths.length} file(s) behind` : "up to date";
|
|
605
|
+
const incoming = status.anchor.incoming.length ? `${status.anchor.incoming.length} customer commit(s) to pull` : "no customer commits";
|
|
606
|
+
const drift = status.driftedLibs.length ? ` DRIFTED LIBS: ${status.driftedLibs.join(", ")}` : null;
|
|
607
|
+
return [
|
|
608
|
+
` ${status.name}: ${behind}, ${incoming}`,
|
|
609
|
+
...status.anchor.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`),
|
|
610
|
+
...drift ? [drift] : []
|
|
611
|
+
];
|
|
612
|
+
})
|
|
613
|
+
];
|
|
614
|
+
return sections.join(`
|
|
558
615
|
`);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
616
|
+
}
|
|
617
|
+
static pushResults(results) {
|
|
618
|
+
const sections = [
|
|
619
|
+
"Akan Subspace Push",
|
|
620
|
+
"",
|
|
621
|
+
...results.flatMap((result) => {
|
|
622
|
+
const detail = result.outcome === "pushed" ? `${result.commit} (${result.changedFiles} files)` : result.reason ?? "";
|
|
623
|
+
const [first = "", ...rest] = detail.split(`
|
|
567
624
|
`);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
625
|
+
const pruned = result.prunedDependencies?.length ? [` ${result.prunedDependencies.length} unused root dependenc(ies) left out of package.json`] : [];
|
|
626
|
+
return [
|
|
627
|
+
` ${result.outcome.padEnd(8)} ${result.name} ${first}`,
|
|
628
|
+
...rest.map((line) => ` ${line}`),
|
|
629
|
+
...pruned
|
|
630
|
+
];
|
|
631
|
+
})
|
|
632
|
+
];
|
|
633
|
+
return sections.join(`
|
|
577
634
|
`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
"",
|
|
585
|
-
...result.incoming.length ? result.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`) : [" (none \u2014 nothing to pull)"],
|
|
586
|
-
"",
|
|
587
|
-
`Applied to the working tree, uncommitted (${result.applied.length}):`,
|
|
588
|
-
"",
|
|
589
|
-
...result.applied.length ? result.applied.map((file) => ` ${file}`) : [" (none)"],
|
|
590
|
-
...result.libPatch ? [
|
|
635
|
+
}
|
|
636
|
+
static pullResult(result) {
|
|
637
|
+
const sections = [
|
|
638
|
+
`Akan Subspace Pull \u2014 ${result.name}`,
|
|
639
|
+
"",
|
|
640
|
+
`Customer commits since the last push (${result.incoming.length}):`,
|
|
591
641
|
"",
|
|
592
|
-
|
|
642
|
+
...result.incoming.length ? result.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`) : [" (none \u2014 nothing to pull)"],
|
|
593
643
|
"",
|
|
594
|
-
|
|
644
|
+
`Applied to the working tree, uncommitted (${result.applied.length}):`,
|
|
595
645
|
"",
|
|
596
|
-
`
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
646
|
+
...result.applied.length ? result.applied.map((file) => ` ${file}`) : [" (none)"],
|
|
647
|
+
...result.libPatch ? [
|
|
648
|
+
"",
|
|
649
|
+
`Library edits NOT applied (${result.libPatch.files.length}) \u2014 review before adopting:`,
|
|
650
|
+
"",
|
|
651
|
+
...result.libPatch.files.map((file) => ` ${file}`),
|
|
652
|
+
"",
|
|
653
|
+
` patch: ${result.libPatch.path}`,
|
|
654
|
+
" adopt with: akan subspace pull <name> --adopt-libs"
|
|
655
|
+
] : [],
|
|
656
|
+
...result.ignored.length ? ["", `Ignored (workspace-owned or subspace-owned): ${result.ignored.length} file(s)`] : []
|
|
657
|
+
];
|
|
658
|
+
return sections.join(`
|
|
602
659
|
`);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
"",
|
|
609
|
-
...result.outcome === "cancelled" ? [" cancelled \u2014 the subspace's env archive is untouched."] : [
|
|
610
|
-
`Uploaded (${result.files.length}), replacing that workspace's whole archive:`,
|
|
660
|
+
}
|
|
661
|
+
static envUpload(result) {
|
|
662
|
+
const sections = [
|
|
663
|
+
`Akan Subspace Env Upload \u2014 ${result.name}`,
|
|
664
|
+
`cloud workspace: ${result.workspaceId} \xB7 host: ${result.host}`,
|
|
611
665
|
"",
|
|
612
|
-
...result.
|
|
666
|
+
...result.outcome === "cancelled" ? [" cancelled \u2014 the subspace's env archive is untouched."] : [
|
|
667
|
+
`Uploaded (${result.files.length}), replacing that workspace's whole archive:`,
|
|
668
|
+
"",
|
|
669
|
+
...result.files.map((file) => ` ${file}`),
|
|
670
|
+
"",
|
|
671
|
+
` apps: ${result.apps.join(", ")}`,
|
|
672
|
+
` libs: ${result.libs.join(", ") || "(none)"}`
|
|
673
|
+
]
|
|
674
|
+
];
|
|
675
|
+
return sections.join(`
|
|
676
|
+
`);
|
|
677
|
+
}
|
|
678
|
+
static diff(result) {
|
|
679
|
+
const total = result.app.files.length + result.libs.files.length;
|
|
680
|
+
const sections = [
|
|
681
|
+
`Akan Subspace Diff \u2014 ${result.name} (${result.branch})`,
|
|
682
|
+
"what `akan subspace push` would change in the subspace",
|
|
613
683
|
"",
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
684
|
+
...total ? [] : [" subspace is identical to the workspace for this slice."],
|
|
685
|
+
...result.libs.files.length ? [
|
|
686
|
+
`LIBRARY changes (${result.libs.files.length}) \u2014 the subspace edited shared code:`,
|
|
687
|
+
"",
|
|
688
|
+
...result.libs.files.map((file) => ` ${file}`),
|
|
689
|
+
"",
|
|
690
|
+
result.libs.patch
|
|
691
|
+
] : [],
|
|
692
|
+
...result.app.files.length ? [
|
|
693
|
+
`App changes (${result.app.files.length}):`,
|
|
694
|
+
"",
|
|
695
|
+
...result.app.files.map((file) => ` ${file}`),
|
|
696
|
+
"",
|
|
697
|
+
result.app.patch
|
|
698
|
+
] : []
|
|
699
|
+
];
|
|
700
|
+
return sections.join(`
|
|
619
701
|
`);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
...result.libs.files.length ? [
|
|
629
|
-
`LIBRARY changes (${result.libs.files.length}) \u2014 the subspace edited shared code:`,
|
|
702
|
+
}
|
|
703
|
+
static plan(plan) {
|
|
704
|
+
const list = (entries) => entries.length ? entries.join(", ") : "(none)";
|
|
705
|
+
const blockers = Subspace.planBlockers(plan);
|
|
706
|
+
const sections = [
|
|
707
|
+
`Akan Subspace Plan \u2014 ${plan.name} (${plan.branch})`,
|
|
708
|
+
`repo: ${plan.repo}`,
|
|
709
|
+
"what `akan subspace push` would ship, worked out from this checkout alone",
|
|
630
710
|
"",
|
|
631
|
-
|
|
711
|
+
`apps: ${list(plan.apps)}`,
|
|
712
|
+
`libs: ${plan.libs.length ? "" : "(none)"}`.trimEnd(),
|
|
713
|
+
...plan.libs.map((lib) => ` - ${lib.name} (from ${lib.apps.join(", ")})`),
|
|
714
|
+
`files under apps/ and libs/: ${plan.files}`,
|
|
632
715
|
"",
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
`App changes (${result.app.files.length}):`,
|
|
716
|
+
`root entries pushed: ${list(plan.rootEntries)}`,
|
|
717
|
+
`root entries excluded: ${list(plan.excludedRootEntries)}`,
|
|
718
|
+
`root entries the subspace keeps its own: ${list(plan.subspaceOwnedRootEntries)}`,
|
|
637
719
|
"",
|
|
638
|
-
|
|
720
|
+
`Root dependencies left out of package.json (${plan.prunedDependencies.length}):`,
|
|
721
|
+
...plan.prunedDependencies.length ? plan.prunedDependencies.map((dep) => ` - ${dep}`) : [" (none)"],
|
|
639
722
|
"",
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
]
|
|
643
|
-
|
|
644
|
-
`
|
|
645
|
-
}
|
|
646
|
-
function formatSubspacePlan(plan) {
|
|
647
|
-
const list = (entries) => entries.length ? entries.join(", ") : "(none)";
|
|
648
|
-
const blockers = [plan.branchRefusal, plan.lockProblem].filter((reason) => !!reason);
|
|
649
|
-
const sections = [
|
|
650
|
-
`Akan Subspace Plan \u2014 ${plan.name} (${plan.branch})`,
|
|
651
|
-
`repo: ${plan.repo}`,
|
|
652
|
-
"what `akan subspace push` would ship, worked out from this checkout alone",
|
|
653
|
-
"",
|
|
654
|
-
`apps: ${list(plan.apps)}`,
|
|
655
|
-
`libs: ${plan.libs.length ? "" : "(none)"}`.trimEnd(),
|
|
656
|
-
...plan.libs.map((lib) => ` - ${lib.name} (from ${lib.apps.join(", ")})`),
|
|
657
|
-
`files under apps/ and libs/: ${plan.files}`,
|
|
658
|
-
"",
|
|
659
|
-
`root entries pushed: ${list(plan.rootEntries)}`,
|
|
660
|
-
`root entries excluded: ${list(plan.excludedRootEntries)}`,
|
|
661
|
-
`root entries the subspace keeps its own: ${list(plan.subspaceOwnedRootEntries)}`,
|
|
662
|
-
"",
|
|
663
|
-
`Root dependencies left out of package.json (${plan.prunedDependencies.length}):`,
|
|
664
|
-
...plan.prunedDependencies.length ? plan.prunedDependencies.map((dep) => ` - ${dep}`) : [" (none)"],
|
|
665
|
-
"",
|
|
666
|
-
blockers.length ? "A push would be refused:" : "Nothing here would make a push refuse.",
|
|
667
|
-
...blockers.map((reason) => ` - ${reason}`),
|
|
668
|
-
...plan.warnings.length ? ["", `Warnings (${plan.warnings.length}):`, ...plan.warnings.map((w) => ` - ${w}`)] : []
|
|
669
|
-
];
|
|
670
|
-
return sections.join(`
|
|
723
|
+
blockers.length ? "A push would be refused:" : "Nothing here would make a push refuse.",
|
|
724
|
+
...blockers.map((reason) => ` - ${reason}`),
|
|
725
|
+
...plan.warnings.length ? ["", `Warnings (${plan.warnings.length}):`, ...plan.warnings.map((w) => ` - ${w}`)] : []
|
|
726
|
+
];
|
|
727
|
+
return sections.join(`
|
|
671
728
|
`);
|
|
729
|
+
}
|
|
672
730
|
}
|
|
673
731
|
|
|
674
732
|
// pkgs/@akanjs/cli/subspace/subspace.script.ts
|
|
@@ -767,7 +825,7 @@ class SubspaceRunner extends runner("subspace") {
|
|
|
767
825
|
if (!asked)
|
|
768
826
|
return { name: subspace.name, workspaceId, host, apps, libs: [], outcome: "cancelled", files: [] };
|
|
769
827
|
const { libs } = await subspace.slice();
|
|
770
|
-
const { CloudScript } = await import("./cloud.script-
|
|
828
|
+
const { CloudScript } = await import("./cloud.script-dgstqt1m.js");
|
|
771
829
|
const { files } = await CommandContainer.get(CloudScript).uploadEnv(workspace, {
|
|
772
830
|
host,
|
|
773
831
|
workspaceId,
|
|
@@ -800,14 +858,14 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
|
|
|
800
858
|
const statuses = await this.subspaceRunner.status(workspace, names);
|
|
801
859
|
const drifted = statuses.filter((status) => status.driftedLibs.length).length;
|
|
802
860
|
spinner.succeed(`Checked ${statuses.length} subspace(s) (${drifted} with library drift)`);
|
|
803
|
-
Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) :
|
|
861
|
+
Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : SubspaceReport.statuses(statuses));
|
|
804
862
|
}
|
|
805
863
|
async push(workspace, names, { verify, format }) {
|
|
806
864
|
const spinner = workspace.spinning("Pushing to subspaces...");
|
|
807
865
|
const results = await this.subspaceRunner.push(workspace, names, { verify });
|
|
808
866
|
const pushed = results.filter((result) => result.outcome === "pushed").length;
|
|
809
867
|
spinner.succeed(`Pushed ${pushed} of ${results.length} subspace(s)`);
|
|
810
|
-
Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) :
|
|
868
|
+
Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : SubspaceReport.pushResults(results));
|
|
811
869
|
if (results.some((result) => result.outcome === "refused"))
|
|
812
870
|
process.exitCode = 1;
|
|
813
871
|
}
|
|
@@ -815,27 +873,29 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
|
|
|
815
873
|
const spinner = workspace.spinning(`Planning ${name}...`);
|
|
816
874
|
const plan = await this.subspaceRunner.plan(workspace, name);
|
|
817
875
|
spinner.succeed(`${plan.apps.length} app(s), ${plan.libs.length} librar(ies), ${plan.files} file(s)`);
|
|
818
|
-
Logger.rawLog(format === "json" ? JSON.stringify(plan, null, 2) :
|
|
876
|
+
Logger.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : SubspaceReport.plan(plan));
|
|
877
|
+
if (Subspace.planBlockers(plan).length)
|
|
878
|
+
process.exitCode = 1;
|
|
819
879
|
}
|
|
820
880
|
async diff(workspace, name, filter, format = "text") {
|
|
821
881
|
const spinner = workspace.spinning(`Diffing ${name}...`);
|
|
822
882
|
const result = await this.subspaceRunner.diff(workspace, name, filter);
|
|
823
883
|
const total = result.app.files.length + result.libs.files.length;
|
|
824
884
|
spinner.succeed(`${total} file(s) differ (${result.libs.files.length} in shared libraries)`);
|
|
825
|
-
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) :
|
|
885
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : SubspaceReport.diff(result));
|
|
826
886
|
}
|
|
827
887
|
async uploadEnv(workspace, name, { host, force, format }) {
|
|
828
888
|
const result = await this.subspaceRunner.uploadEnv(workspace, name, {
|
|
829
889
|
host: await GlobalConfig.hostFor(workspace, host),
|
|
830
890
|
force
|
|
831
891
|
});
|
|
832
|
-
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) :
|
|
892
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : SubspaceReport.envUpload(result));
|
|
833
893
|
}
|
|
834
894
|
async pull(workspace, name, { adoptLibs, format }) {
|
|
835
895
|
const spinner = workspace.spinning(`Pulling customer commits from ${name}...`);
|
|
836
896
|
const result = await this.subspaceRunner.pull(workspace, name, { adoptLibs });
|
|
837
897
|
spinner.succeed(`${result.incoming.length} customer commit(s), ${result.applied.length} file(s) applied`);
|
|
838
|
-
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) :
|
|
898
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : SubspaceReport.pullResult(result));
|
|
839
899
|
}
|
|
840
900
|
}
|
|
841
901
|
|