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