@bevel-software/platform-core-backend 0.8.0 → 0.9.0
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/dist/core/core-ports.d.ts +7 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +24 -11
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +7 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +39 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts +3 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +4 -2
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/render-roles-yaml.d.ts +22 -0
- package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
- package/dist/modules/access/render-roles-yaml.js +56 -0
- package/dist/modules/access/render-roles-yaml.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +15 -1
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +30 -15
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +10 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +111 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-git.js +86 -0
- package/dist/modules/workspace/startup/kb-git.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.js +21 -0
- package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
- package/dist/modules/workspace/workspace.service.d.ts +0 -35
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +2 -101
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/kb-template/gitignore.template +16 -0
- package/package.json +3 -3
- package/src/core/core-ports.ts +7 -0
- package/src/core/create-core-server.ts +30 -11
- package/src/core/create-core-services.ts +43 -22
- package/src/modules/access/__tests__/roles-admin.service.test.ts +24 -2
- package/src/modules/access/access.routes.ts +3 -0
- package/src/modules/access/render-roles-yaml.ts +65 -0
- package/src/modules/access/roles-admin.service.ts +32 -14
- package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
- package/src/modules/settings/setup.routes.ts +112 -5
- package/src/modules/workspace/__tests__/workspace.service.test.ts +5 -94
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
- package/src/modules/workspace/startup/kb-git.ts +94 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
- package/src/modules/workspace/startup/on-server-start.ts +97 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
- package/src/modules/workspace/{plugins-migration.ts → startup/steps/groups-to-plugins.step.ts} +342 -260
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
- package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
- package/src/modules/workspace/workspace.service.ts +2 -106
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -512
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +0 -427
- package/src/modules/workspace/kb-seed.interface.ts +0 -36
- package/src/modules/workspace/kb-seed.service.ts +0 -584
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { renderRolesYaml } from '../../../access/render-roles-yaml.js';
|
|
4
|
+
import { TEMPLATE_SOURCE_FALLBACKS, reservedRootDirs, templateSource } from './template-files.step.js';
|
|
5
|
+
/**
|
|
6
|
+
* The empty-remote seed builder the runner takes as `buildSeedTree`: the full
|
|
7
|
+
* template tree, every reserved root, and a generated roles.yaml. DIRECT fs
|
|
8
|
+
* writes are correct here — the target is a temp directory the runner inits,
|
|
9
|
+
* commits and pushes itself, not a branch handle with buffered ops.
|
|
10
|
+
*
|
|
11
|
+
* Resolves to the repo-relative paths it GENERATED (roles.yaml plus each
|
|
12
|
+
* reserved root's .gitkeep): a template `.gitignore` rule could match any of
|
|
13
|
+
* them, and the runner force-adds them after `git add -A` so a required seed
|
|
14
|
+
* file can never be silently dropped from the seed commit.
|
|
15
|
+
*
|
|
16
|
+
* `extraRootDirs` is validated eagerly, at composition time: a bad value
|
|
17
|
+
* should fail at boot beside the rest of the wiring, not mid-seed of
|
|
18
|
+
* somebody's knowledge base.
|
|
19
|
+
*/
|
|
20
|
+
export function buildSeedTree(templateDir, extraRootDirs, seedAdminEmails) {
|
|
21
|
+
const requiredDirs = reservedRootDirs(extraRootDirs);
|
|
22
|
+
return async (dir) => {
|
|
23
|
+
const generated = [];
|
|
24
|
+
await copyTemplateTree(templateDir, dir);
|
|
25
|
+
// Reserved roots the template does not carry. Without this the seed commit
|
|
26
|
+
// would hold only what the template has, and a distribution's own roots
|
|
27
|
+
// would appear a step later, when the first startup phase tops them up —
|
|
28
|
+
// the same folders, arriving in a second commit for no reason. Keyed on
|
|
29
|
+
// the DIRECTORY's existence: a template already carrying content under a
|
|
30
|
+
// root never gets a pointless placeholder beside it.
|
|
31
|
+
for (const rootDir of requiredDirs) {
|
|
32
|
+
const abs = path.join(dir, rootDir);
|
|
33
|
+
const found = await lstatOrNull(abs);
|
|
34
|
+
if (found) {
|
|
35
|
+
if (found.isDirectory())
|
|
36
|
+
continue;
|
|
37
|
+
// Only a template shipping a FILE under a reserved name reaches this —
|
|
38
|
+
// a broken build, not a broken knowledge base.
|
|
39
|
+
throw new Error(`KB root "${rootDir}" exists in the template but is not a directory.`);
|
|
40
|
+
}
|
|
41
|
+
await fs.mkdir(abs, { recursive: true });
|
|
42
|
+
await fs.writeFile(path.join(abs, '.gitkeep'), '', 'utf8');
|
|
43
|
+
generated.push(`${rootDir}/.gitkeep`);
|
|
44
|
+
}
|
|
45
|
+
// Generated, never templated — see roles-yaml.step.ts. The runner refuses
|
|
46
|
+
// to seed an empty remote with no admins, so the list is non-empty here.
|
|
47
|
+
await fs.writeFile(path.join(dir, 'roles.yaml'), renderRolesYaml(seedAdminEmails), 'utf8');
|
|
48
|
+
generated.push('roles.yaml');
|
|
49
|
+
return generated;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Copy the entire template tree into `dest` (roles.yaml isn't in it — it's generated). */
|
|
53
|
+
async function copyTemplateTree(templateDir, dest) {
|
|
54
|
+
const packableToReal = new Map(Object.entries(TEMPLATE_SOURCE_FALLBACKS).map(([real, packable]) => [packable, real]));
|
|
55
|
+
const walk = async (relDir) => {
|
|
56
|
+
const abs = path.join(templateDir, relDir);
|
|
57
|
+
const entries = await fs.readdir(abs, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
// Never copy a git dir: a KB_TEMPLATE_DIR that is itself a working tree
|
|
60
|
+
// (this repo in a Docker build) must not seed its history into the KB.
|
|
61
|
+
if (entry.name === '.git')
|
|
62
|
+
continue;
|
|
63
|
+
const rel = relDir ? path.join(relDir, entry.name) : entry.name;
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
await walk(rel);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// A packable spelling at the template root seeds under its REAL name
|
|
69
|
+
// — unless the template also carries the literal file (a
|
|
70
|
+
// distribution's own template), which wins and is copied by its own
|
|
71
|
+
// walk entry; copying the packable twin too would clobber it.
|
|
72
|
+
const realName = relDir === '' ? packableToReal.get(entry.name) : undefined;
|
|
73
|
+
if (realName !== undefined) {
|
|
74
|
+
if (!(await exists(path.join(templateDir, realName)))) {
|
|
75
|
+
await copyTemplateFile(templateDir, realName, dest);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
await copyTemplateFile(templateDir, rel, dest);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
await walk('');
|
|
83
|
+
}
|
|
84
|
+
/** Copy one template file (by repo-relative path) into `dest`, creating parents. */
|
|
85
|
+
async function copyTemplateFile(templateDir, relPath, dest) {
|
|
86
|
+
const from = await templateSource(templateDir, relPath);
|
|
87
|
+
const to = path.join(dest, relPath);
|
|
88
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
89
|
+
await fs.copyFile(from, to);
|
|
90
|
+
}
|
|
91
|
+
async function exists(p) {
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(p);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** `lstat` without the throw — null when nothing is at `p`. */
|
|
101
|
+
async function lstatOrNull(p) {
|
|
102
|
+
try {
|
|
103
|
+
return await fs.lstat(p);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=seed-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-tree.js","sourceRoot":"","sources":["../../../../../src/modules/workspace/startup/steps/seed-tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAEvG;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAC3B,WAAmB,EACnB,aAAgC,EAChC,eAAkC;IAElC,MAAM,YAAY,GAAG,gBAAgB,CAAC,aAAa,CAAC,CAAC;IACrD,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;QACnB,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,MAAM,gBAAgB,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACzC,2EAA2E;QAC3E,wEAAwE;QACxE,yEAAyE;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,qDAAqD;QACrD,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACpC,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;YACrC,IAAI,KAAK,EAAE,CAAC;gBACV,IAAI,KAAK,CAAC,WAAW,EAAE;oBAAE,SAAS;gBAClC,uEAAuE;gBACvE,+CAA+C;gBAC/C,MAAM,IAAI,KAAK,CAAC,YAAY,OAAO,kDAAkD,CAAC,CAAC;YACzF,CAAC;YACD,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;YAC3D,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,WAAW,CAAC,CAAC;QACxC,CAAC;QACD,0EAA0E;QAC1E,yEAAyE;QACzE,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,eAAe,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3F,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7B,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,KAAK,UAAU,gBAAgB,CAAC,WAAmB,EAAE,IAAY;IAC/D,MAAM,cAAc,GAAG,IAAI,GAAG,CAC5B,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CACtF,CAAC;IACF,MAAM,IAAI,GAAG,KAAK,EAAE,MAAc,EAAiB,EAAE;QACnD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC3C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,wEAAwE;YACxE,uEAAuE;YACvE,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;gBAAE,SAAS;YACpC,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YAChE,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;gBAChB,SAAS;YACX,CAAC;YACD,qEAAqE;YACrE,yDAAyD;YACzD,oEAAoE;YACpE,8DAA8D;YAC9D,MAAM,QAAQ,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC5E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;oBACtD,MAAM,gBAAgB,CAAC,WAAW,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;gBACtD,CAAC;gBACD,SAAS;YACX,CAAC;YACD,MAAM,gBAAgB,CAAC,WAAW,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;AACjB,CAAC;AAED,oFAAoF;AACpF,KAAK,UAAU,gBAAgB,CAAC,WAAmB,EAAE,OAAe,EAAE,IAAY;IAChF,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACxD,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,CAAS;IAC7B,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,KAAK,UAAU,WAAW,CAAC,CAAS;IAClC,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { OnServerStart, ServerStartContext, StepResult } from '../on-server-start.js';
|
|
2
|
+
/**
|
|
3
|
+
* The **required scaffolding** — the minimum an operational KB needs. Any of
|
|
4
|
+
* these missing from a protected branch are added at the startup phase; the
|
|
5
|
+
* sample ontology is NOT (it only seeds a fully-empty repo, see seed-tree.ts).
|
|
6
|
+
*
|
|
7
|
+
* Two kinds:
|
|
8
|
+
* - {@link REQUIRED_FILES}: repo-root files added when the file is missing.
|
|
9
|
+
* - Reserved root dirs (core's two plus a distribution's `extraRootDirs`):
|
|
10
|
+
* when a dir is entirely absent it's created by adding its `<dir>/.gitkeep`.
|
|
11
|
+
* Keyed on the *directory's* existence, not the `.gitkeep` file — so a
|
|
12
|
+
* branch that already has content under `KnowledgeBase/` never gets a
|
|
13
|
+
* pointless placeholder.
|
|
14
|
+
*
|
|
15
|
+
* `roles.yaml` is in neither, and is not part of the template at all: it is
|
|
16
|
+
* generated from `ADMIN_EMAIL` (see roles-yaml.step.ts), so a repo can't be
|
|
17
|
+
* seeded with a stale hard-coded Admin list.
|
|
18
|
+
*/
|
|
19
|
+
export declare const REQUIRED_FILES: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Repo-root files the startup phase GENERATES rather than copies — today just
|
|
22
|
+
* `roles.yaml`, rendered from `ADMIN_EMAIL` (see roles-yaml.step.ts and
|
|
23
|
+
* seed-tree.ts). Reserved-root validation must treat these exactly like
|
|
24
|
+
* {@link REQUIRED_FILES}: a root claiming a generated name is the same silent
|
|
25
|
+
* typo with the same silent outcome.
|
|
26
|
+
*/
|
|
27
|
+
export declare const GENERATED_FILES: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Destination name → the packable spelling the template may carry instead.
|
|
30
|
+
* npm strips every file named `.gitignore` from a published tarball, so the
|
|
31
|
+
* packaged template cannot ship one under its real name (see
|
|
32
|
+
* {@link templateSource}).
|
|
33
|
+
*/
|
|
34
|
+
export declare const TEMPLATE_SOURCE_FALLBACKS: Readonly<Record<string, string>>;
|
|
35
|
+
/**
|
|
36
|
+
* Core's guaranteed roots plus a distribution's extras, validated once at
|
|
37
|
+
* composition time: every entry is joined onto the repo root and onto
|
|
38
|
+
* `<dir>/.gitkeep`, so a separator or a `..` would write outside the repo
|
|
39
|
+
* being maintained, and a bad value should fail at boot beside the rest of
|
|
40
|
+
* the wiring rather than part-way through maintaining somebody's knowledge
|
|
41
|
+
* base. Shared with the empty-remote seed builder (seed-tree.ts) so the two
|
|
42
|
+
* paths can never disagree about what a deployment guarantees.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reservedRootDirs(extraRootDirs: readonly string[]): readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* Where `relPath`'s template content actually lives. npm refuses to pack
|
|
47
|
+
* files named `.gitignore` — every such file is silently stripped from the
|
|
48
|
+
* published tarball — so the packaged template ships the KB's gitignore
|
|
49
|
+
* under a packable name and the seeder writes it to its real one. A
|
|
50
|
+
* template carrying the literal file (a distribution's own
|
|
51
|
+
* KB_TEMPLATE_DIR, or this repo's tree in a Docker build) wins outright:
|
|
52
|
+
* the mapping is a fallback, never a rename.
|
|
53
|
+
*/
|
|
54
|
+
export declare function templateSource(templateDir: string, relPath: string): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* The template top-up as an {@link OnServerStart} step: add any missing base
|
|
57
|
+
* scaffolding to every PROTECTED branch, and keep the managed AGENTS.md
|
|
58
|
+
* current. Drafts are deliberately out of scope — whatever the protected
|
|
59
|
+
* branches gain, drafts fork from; a scaffolding addition on a draft would
|
|
60
|
+
* surface as noise in its change request's diff. (Unlike the Groups→Plugins
|
|
61
|
+
* rename, a missing file diffs as one file, not the whole tree — so the
|
|
62
|
+
* uniform-application argument does not bite here.)
|
|
63
|
+
*
|
|
64
|
+
* Everything is DECLARED on the branch handle; reads go against the pre-step
|
|
65
|
+
* tree via `repoDir()`. Fail-open behavior from the lazy top-up (best-effort,
|
|
66
|
+
* never throws) is deliberately gone: an unexpected state — a file squatting
|
|
67
|
+
* a reserved root name — now throws and stops the boot, which is the phase's
|
|
68
|
+
* contract for states a human must look at.
|
|
69
|
+
*/
|
|
70
|
+
export declare class TemplateFilesStep implements OnServerStart {
|
|
71
|
+
readonly name = "template-files";
|
|
72
|
+
private readonly requiredDirs;
|
|
73
|
+
/**
|
|
74
|
+
* @param extraRootDirs Additional root folders this distribution reserves,
|
|
75
|
+
* on top of core's two. Their `.gitkeep` is written
|
|
76
|
+
* directly rather than copied, so a distribution can
|
|
77
|
+
* claim a root without also shipping a template entry
|
|
78
|
+
* for it.
|
|
79
|
+
*/
|
|
80
|
+
constructor(extraRootDirs?: readonly string[]);
|
|
81
|
+
run(ctx: ServerStartContext): Promise<StepResult>;
|
|
82
|
+
private topUp;
|
|
83
|
+
/**
|
|
84
|
+
* Which reserved roots are absent — and which are SQUATTED. `lstat`, not
|
|
85
|
+
* `exists`: `fs.access` answers "is there something here?", which is true of
|
|
86
|
+
* a FILE named `Plugins` — and a skip-if-present check would then do nothing
|
|
87
|
+
* and report success, leaving a knowledge base permanently missing a root it
|
|
88
|
+
* claims to guarantee. `lstat` rather than `stat` so a SYMLINK is rejected
|
|
89
|
+
* too: a link named `Plugins` is not a KB layout, and one pointing outside
|
|
90
|
+
* the repo would make every later write into it land somewhere nobody asked
|
|
91
|
+
* for. A squatter THROWS — under this phase's fail-closed contract that
|
|
92
|
+
* stops the boot, which such a state deserves.
|
|
93
|
+
*/
|
|
94
|
+
private missingDirs;
|
|
95
|
+
/**
|
|
96
|
+
* Declare each missing reserved root as an empty `<dir>/.gitkeep`.
|
|
97
|
+
* WRITTEN, not copied from the template: a `.gitkeep` is empty by
|
|
98
|
+
* definition, and requiring a template entry per root would mean a
|
|
99
|
+
* distribution could not reserve one without forking the packaged template.
|
|
100
|
+
*/
|
|
101
|
+
private ensureRequiredDirs;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=template-files.step.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-files.step.d.ts","sourceRoot":"","sources":["../../../../../src/modules/workspace/startup/steps/template-files.step.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAY,aAAa,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAErG;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA6D,CAAC;AAE1G;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAAmB,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEtE,CAAC;AAwCF;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAcpF;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS1F;AAoBD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IACrD,QAAQ,CAAC,IAAI,oBAAoB;IAEjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IAEjD;;;;;;OAMG;gBACS,aAAa,GAAE,SAAS,MAAM,EAAO;IAI3C,GAAG,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC;YAOzC,KAAK;IAoEnB;;;;;;;;;;OAUG;YACW,WAAW;IAiBzB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;CAQ3B"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { KNOWLEDGE_BASE_DIR, PLUGINS_DIR } from '@bevel-software/platform-shared';
|
|
4
|
+
import { IGNORE_FILENAME } from '../../bevel-ignore.js';
|
|
5
|
+
/**
|
|
6
|
+
* The **required scaffolding** — the minimum an operational KB needs. Any of
|
|
7
|
+
* these missing from a protected branch are added at the startup phase; the
|
|
8
|
+
* sample ontology is NOT (it only seeds a fully-empty repo, see seed-tree.ts).
|
|
9
|
+
*
|
|
10
|
+
* Two kinds:
|
|
11
|
+
* - {@link REQUIRED_FILES}: repo-root files added when the file is missing.
|
|
12
|
+
* - Reserved root dirs (core's two plus a distribution's `extraRootDirs`):
|
|
13
|
+
* when a dir is entirely absent it's created by adding its `<dir>/.gitkeep`.
|
|
14
|
+
* Keyed on the *directory's* existence, not the `.gitkeep` file — so a
|
|
15
|
+
* branch that already has content under `KnowledgeBase/` never gets a
|
|
16
|
+
* pointless placeholder.
|
|
17
|
+
*
|
|
18
|
+
* `roles.yaml` is in neither, and is not part of the template at all: it is
|
|
19
|
+
* generated from `ADMIN_EMAIL` (see roles-yaml.step.ts), so a repo can't be
|
|
20
|
+
* seeded with a stale hard-coded Admin list.
|
|
21
|
+
*/
|
|
22
|
+
export const REQUIRED_FILES = ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore'];
|
|
23
|
+
/**
|
|
24
|
+
* Repo-root files the startup phase GENERATES rather than copies — today just
|
|
25
|
+
* `roles.yaml`, rendered from `ADMIN_EMAIL` (see roles-yaml.step.ts and
|
|
26
|
+
* seed-tree.ts). Reserved-root validation must treat these exactly like
|
|
27
|
+
* {@link REQUIRED_FILES}: a root claiming a generated name is the same silent
|
|
28
|
+
* typo with the same silent outcome.
|
|
29
|
+
*/
|
|
30
|
+
export const GENERATED_FILES = ['roles.yaml'];
|
|
31
|
+
/**
|
|
32
|
+
* Destination name → the packable spelling the template may carry instead.
|
|
33
|
+
* npm strips every file named `.gitignore` from a published tarball, so the
|
|
34
|
+
* packaged template cannot ship one under its real name (see
|
|
35
|
+
* {@link templateSource}).
|
|
36
|
+
*/
|
|
37
|
+
export const TEMPLATE_SOURCE_FALLBACKS = {
|
|
38
|
+
'.gitignore': 'gitignore.template',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The two roots CORE gives a knowledge base: the ontologies, and the plugins
|
|
42
|
+
* that hold skills and tools.
|
|
43
|
+
*
|
|
44
|
+
* `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
|
|
45
|
+
* the agentic execution layer, which is not part of this platform — a core
|
|
46
|
+
* deployment that created them would be handing every operator three empty
|
|
47
|
+
* folders it has no feature to fill. A distribution that DOES own that layer
|
|
48
|
+
* passes them as `extraRootDirs` (and ships a template carrying their
|
|
49
|
+
* READMEs); the names stay reserved in `kb-layout.ts` either way, so a KB
|
|
50
|
+
* that has them still renders them as roots rather than folding them into
|
|
51
|
+
* Knowledge.
|
|
52
|
+
*/
|
|
53
|
+
const CORE_REQUIRED_DIRS = [KNOWLEDGE_BASE_DIR, PLUGINS_DIR];
|
|
54
|
+
/**
|
|
55
|
+
* A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
|
|
56
|
+
* `/x`. The name is joined onto the repo root, so anything else writes outside
|
|
57
|
+
* the repo being maintained.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately NOT a check against the reserved-root set in `kb-layout.ts`:
|
|
60
|
+
* `Data`, `Agents` and `Pipelines` are all in that set, and they are precisely
|
|
61
|
+
* what a distribution passes here. Being reserved is what makes a name worth
|
|
62
|
+
* claiming — the file tree renders it as its own root instead of folding it
|
|
63
|
+
* into Knowledge — so rejecting reserved names would reject the only real use.
|
|
64
|
+
*/
|
|
65
|
+
function assertRootSegment(dir) {
|
|
66
|
+
if (!dir || dir === '.' || dir === '..' || dir.includes('/') || dir.includes('\\') || path.isAbsolute(dir)) {
|
|
67
|
+
throw new Error(`Reserved KB root must be a single path segment (no separators, no ".."); got "${dir}"`);
|
|
68
|
+
}
|
|
69
|
+
// `.git` can never be a KB root: writing `<dir>/.gitkeep` under it would
|
|
70
|
+
// corrupt the clone's own metadata. Any case — Windows filesystems treat
|
|
71
|
+
// `.GIT` as the same directory.
|
|
72
|
+
if (dir.toLowerCase() === '.git') {
|
|
73
|
+
throw new Error(`Reserved KB root must not be ".git" (any case); got "${dir}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Core's guaranteed roots plus a distribution's extras, validated once at
|
|
78
|
+
* composition time: every entry is joined onto the repo root and onto
|
|
79
|
+
* `<dir>/.gitkeep`, so a separator or a `..` would write outside the repo
|
|
80
|
+
* being maintained, and a bad value should fail at boot beside the rest of
|
|
81
|
+
* the wiring rather than part-way through maintaining somebody's knowledge
|
|
82
|
+
* base. Shared with the empty-remote seed builder (seed-tree.ts) so the two
|
|
83
|
+
* paths can never disagree about what a deployment guarantees.
|
|
84
|
+
*/
|
|
85
|
+
export function reservedRootDirs(extraRootDirs) {
|
|
86
|
+
for (const dir of extraRootDirs) {
|
|
87
|
+
assertRootSegment(dir);
|
|
88
|
+
// A root named after a required OR generated FILE is a typo with a silent
|
|
89
|
+
// outcome: the file is laid down first, so the dir check finds the path
|
|
90
|
+
// taken and skips it, and the directory the caller asked for never appears
|
|
91
|
+
// with nothing said about why.
|
|
92
|
+
if (REQUIRED_FILES.includes(dir) || GENERATED_FILES.includes(dir)) {
|
|
93
|
+
throw new Error(`Reserved KB root "${dir}" collides with a required or generated file of the same name`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...CORE_REQUIRED_DIRS, ...extraRootDirs];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Where `relPath`'s template content actually lives. npm refuses to pack
|
|
100
|
+
* files named `.gitignore` — every such file is silently stripped from the
|
|
101
|
+
* published tarball — so the packaged template ships the KB's gitignore
|
|
102
|
+
* under a packable name and the seeder writes it to its real one. A
|
|
103
|
+
* template carrying the literal file (a distribution's own
|
|
104
|
+
* KB_TEMPLATE_DIR, or this repo's tree in a Docker build) wins outright:
|
|
105
|
+
* the mapping is a fallback, never a rename.
|
|
106
|
+
*/
|
|
107
|
+
export async function templateSource(templateDir, relPath) {
|
|
108
|
+
const direct = path.join(templateDir, relPath);
|
|
109
|
+
if (await exists(direct))
|
|
110
|
+
return direct;
|
|
111
|
+
const packable = TEMPLATE_SOURCE_FALLBACKS[relPath];
|
|
112
|
+
if (packable !== undefined) {
|
|
113
|
+
const fallback = path.join(templateDir, packable);
|
|
114
|
+
if (await exists(fallback))
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
return direct; // let the ENOENT surface under the name the caller asked for
|
|
118
|
+
}
|
|
119
|
+
async function exists(p) {
|
|
120
|
+
try {
|
|
121
|
+
await fs.access(p);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** `lstat` without the throw — null when nothing is at `p`. */
|
|
129
|
+
async function lstatOrNull(p) {
|
|
130
|
+
try {
|
|
131
|
+
return await fs.lstat(p);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The template top-up as an {@link OnServerStart} step: add any missing base
|
|
139
|
+
* scaffolding to every PROTECTED branch, and keep the managed AGENTS.md
|
|
140
|
+
* current. Drafts are deliberately out of scope — whatever the protected
|
|
141
|
+
* branches gain, drafts fork from; a scaffolding addition on a draft would
|
|
142
|
+
* surface as noise in its change request's diff. (Unlike the Groups→Plugins
|
|
143
|
+
* rename, a missing file diffs as one file, not the whole tree — so the
|
|
144
|
+
* uniform-application argument does not bite here.)
|
|
145
|
+
*
|
|
146
|
+
* Everything is DECLARED on the branch handle; reads go against the pre-step
|
|
147
|
+
* tree via `repoDir()`. Fail-open behavior from the lazy top-up (best-effort,
|
|
148
|
+
* never throws) is deliberately gone: an unexpected state — a file squatting
|
|
149
|
+
* a reserved root name — now throws and stops the boot, which is the phase's
|
|
150
|
+
* contract for states a human must look at.
|
|
151
|
+
*/
|
|
152
|
+
export class TemplateFilesStep {
|
|
153
|
+
name = 'template-files';
|
|
154
|
+
requiredDirs;
|
|
155
|
+
/**
|
|
156
|
+
* @param extraRootDirs Additional root folders this distribution reserves,
|
|
157
|
+
* on top of core's two. Their `.gitkeep` is written
|
|
158
|
+
* directly rather than copied, so a distribution can
|
|
159
|
+
* claim a root without also shipping a template entry
|
|
160
|
+
* for it.
|
|
161
|
+
*/
|
|
162
|
+
constructor(extraRootDirs = []) {
|
|
163
|
+
this.requiredDirs = reservedRootDirs(extraRootDirs);
|
|
164
|
+
}
|
|
165
|
+
async run(ctx) {
|
|
166
|
+
for (const branch of await ctx.protectedBranches()) {
|
|
167
|
+
await this.topUp(ctx.templateDir, branch);
|
|
168
|
+
}
|
|
169
|
+
return { outcome: 'ok' };
|
|
170
|
+
}
|
|
171
|
+
async topUp(templateDir, branch) {
|
|
172
|
+
const repoDir = await branch.repoDir();
|
|
173
|
+
const added = [];
|
|
174
|
+
for (const rel of REQUIRED_FILES) {
|
|
175
|
+
// `lstat`, not `exists`: a DIRECTORY or SYMLINK squatting a required
|
|
176
|
+
// file's name would read as "present", and a skip-if-present check
|
|
177
|
+
// would then report success over a knowledge base whose root access
|
|
178
|
+
// policy (say) cannot be read. Fail-closed, same as the reserved-root
|
|
179
|
+
// squatting check below: this is a state a human must fix.
|
|
180
|
+
const found = await lstatOrNull(path.join(repoDir, rel));
|
|
181
|
+
if (found) {
|
|
182
|
+
if (found.isFile())
|
|
183
|
+
continue;
|
|
184
|
+
throw new Error(`Required KB file "${rel}" on branch "${branch.name}" exists but is not a regular file ` +
|
|
185
|
+
`(${found.isSymbolicLink() ? 'symlink' : found.isDirectory() ? 'directory' : 'special file'}). ` +
|
|
186
|
+
'Remove or rename it — the platform requires this name to be a readable file.');
|
|
187
|
+
}
|
|
188
|
+
let content = await readTemplate(templateDir, rel);
|
|
189
|
+
// The on-disk merge below only runs against an EXISTING ignore file; a
|
|
190
|
+
// freshly-declared one was merely assumed to carry the AGENTS.md rule —
|
|
191
|
+
// true of the packaged template, not necessarily of a distribution's
|
|
192
|
+
// custom one. Make it true here, so the managed conventions doc is
|
|
193
|
+
// hidden from the file tree from the first boot either way.
|
|
194
|
+
if (rel === IGNORE_FILENAME) {
|
|
195
|
+
content = withIgnorePattern(new TextDecoder().decode(content), 'AGENTS.md');
|
|
196
|
+
}
|
|
197
|
+
branch.write(rel, content);
|
|
198
|
+
added.push(rel);
|
|
199
|
+
}
|
|
200
|
+
// AGENTS.md left VISIBLE by a stale `.bevelignore` is closed here — and
|
|
201
|
+
// UNCONDITIONALLY, not only when the file was just added: a KB whose
|
|
202
|
+
// AGENTS.md predates the CLAUDE.md→AGENTS.md rename has an ignore file
|
|
203
|
+
// that lists the old name and knows nothing of the new one, so the
|
|
204
|
+
// conventions doc shows up in the file tree and the agent view.
|
|
205
|
+
// Idempotent: an ignore file already carrying the rule — or absent, in
|
|
206
|
+
// which case the template's copy declared above arrives with the rule in
|
|
207
|
+
// it — changes nothing and produces no note. Deliberately checked by
|
|
208
|
+
// LINE PRESENCE, not effective outcome: a later `!AGENTS.md` negation is
|
|
209
|
+
// the operator explicitly choosing to SHOW the file, and hiding it is a
|
|
210
|
+
// default this step provides, not a mandate it re-imposes every boot.
|
|
211
|
+
added.push(...(await mergeIgnorePattern(repoDir, branch, 'AGENTS.md')));
|
|
212
|
+
// AGENTS.md is MANAGED, not merely seeded: the platform owns its content,
|
|
213
|
+
// and a stale copy is replaced with the packaged template's every startup
|
|
214
|
+
// phase. The file's own header says so, which is what makes overwriting
|
|
215
|
+
// edits a stated contract instead of a surprise.
|
|
216
|
+
let agentsRefreshed = false;
|
|
217
|
+
if (!added.includes('AGENTS.md') && (await templateDiffers(templateDir, repoDir, 'AGENTS.md'))) {
|
|
218
|
+
branch.write('AGENTS.md', await readTemplate(templateDir, 'AGENTS.md'));
|
|
219
|
+
added.push('AGENTS.md');
|
|
220
|
+
agentsRefreshed = true;
|
|
221
|
+
}
|
|
222
|
+
added.push(...this.ensureRequiredDirs(repoDir, branch, await this.missingDirs(repoDir)));
|
|
223
|
+
if (added.length === 0)
|
|
224
|
+
return;
|
|
225
|
+
// One honest line; it becomes the commit subject when this step is the
|
|
226
|
+
// first to dirty the branch.
|
|
227
|
+
branch.note(agentsRefreshed && added.length === 1
|
|
228
|
+
? 'Update AGENTS.md to the current platform template'
|
|
229
|
+
: `Add missing KB scaffolding: ${added.join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Which reserved roots are absent — and which are SQUATTED. `lstat`, not
|
|
233
|
+
* `exists`: `fs.access` answers "is there something here?", which is true of
|
|
234
|
+
* a FILE named `Plugins` — and a skip-if-present check would then do nothing
|
|
235
|
+
* and report success, leaving a knowledge base permanently missing a root it
|
|
236
|
+
* claims to guarantee. `lstat` rather than `stat` so a SYMLINK is rejected
|
|
237
|
+
* too: a link named `Plugins` is not a KB layout, and one pointing outside
|
|
238
|
+
* the repo would make every later write into it land somewhere nobody asked
|
|
239
|
+
* for. A squatter THROWS — under this phase's fail-closed contract that
|
|
240
|
+
* stops the boot, which such a state deserves.
|
|
241
|
+
*/
|
|
242
|
+
async missingDirs(repoDir) {
|
|
243
|
+
const missing = [];
|
|
244
|
+
for (const rootDir of this.requiredDirs) {
|
|
245
|
+
const found = await lstatOrNull(path.join(repoDir, rootDir));
|
|
246
|
+
if (found) {
|
|
247
|
+
if (found.isDirectory())
|
|
248
|
+
continue;
|
|
249
|
+
throw new Error(`KB root "${rootDir}" exists but is not a directory ` +
|
|
250
|
+
`(${found.isSymbolicLink() ? 'symlink' : 'file'}). Remove or rename it — ` +
|
|
251
|
+
'the platform requires this name to be a folder.');
|
|
252
|
+
}
|
|
253
|
+
missing.push(rootDir);
|
|
254
|
+
}
|
|
255
|
+
return missing;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Declare each missing reserved root as an empty `<dir>/.gitkeep`.
|
|
259
|
+
* WRITTEN, not copied from the template: a `.gitkeep` is empty by
|
|
260
|
+
* definition, and requiring a template entry per root would mean a
|
|
261
|
+
* distribution could not reserve one without forking the packaged template.
|
|
262
|
+
*/
|
|
263
|
+
ensureRequiredDirs(repoDir, branch, missing) {
|
|
264
|
+
const added = [];
|
|
265
|
+
for (const rootDir of missing) {
|
|
266
|
+
branch.write(`${rootDir}/.gitkeep`, '');
|
|
267
|
+
added.push(`${rootDir}/.gitkeep`);
|
|
268
|
+
}
|
|
269
|
+
return added;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** The template's content for `relPath`, bytes as shipped. */
|
|
273
|
+
async function readTemplate(templateDir, relPath) {
|
|
274
|
+
return fs.readFile(await templateSource(templateDir, relPath));
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Whether the repo's copy of `relPath` differs from the template's, modulo
|
|
278
|
+
* line endings — a CRLF checkout of identical content must read as "same",
|
|
279
|
+
* or the managed-file refresh would commit churn on every boot forever.
|
|
280
|
+
*/
|
|
281
|
+
async function templateDiffers(templateDir, repoDir, relPath) {
|
|
282
|
+
const norm = (text) => text.replace(/\r\n?/g, '\n');
|
|
283
|
+
const [current, template] = await Promise.all([
|
|
284
|
+
fs.readFile(path.join(repoDir, relPath), 'utf8'),
|
|
285
|
+
templateSource(templateDir, relPath).then((from) => fs.readFile(from, 'utf8')),
|
|
286
|
+
]);
|
|
287
|
+
return norm(current) !== norm(template);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Ensure `.bevelignore` carries `pattern`, declaring the appended content when
|
|
291
|
+
* absent. Returns the paths changed, for the note.
|
|
292
|
+
*
|
|
293
|
+
* APPENDS — never rewrites. The file is the operator's, and every rule
|
|
294
|
+
* already in it is theirs to keep; this adds one line under a comment saying
|
|
295
|
+
* where it came from. Absent file, or a file that already lists the pattern,
|
|
296
|
+
* is a no-op — an absent file means the template's copy (declared in the same
|
|
297
|
+
* step) arrives with the pattern in it.
|
|
298
|
+
*
|
|
299
|
+
* Matched line-wise rather than by substring: a rule for `Plugins/AGENTS.md`
|
|
300
|
+
* is not a rule for the root `AGENTS.md`, and treating it as one would leave
|
|
301
|
+
* the mismatch this exists to close.
|
|
302
|
+
*/
|
|
303
|
+
async function mergeIgnorePattern(repoDir, branch, pattern) {
|
|
304
|
+
let current;
|
|
305
|
+
try {
|
|
306
|
+
current = await fs.readFile(path.join(repoDir, IGNORE_FILENAME), 'utf8');
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// No ignore file — the copy declared from the template arrives with the
|
|
310
|
+
// pattern in it (guaranteed at declaration time, see the required-files
|
|
311
|
+
// loop above).
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
const merged = withIgnorePattern(current, pattern);
|
|
315
|
+
if (merged === current)
|
|
316
|
+
return [];
|
|
317
|
+
branch.write(IGNORE_FILENAME, merged);
|
|
318
|
+
return [IGNORE_FILENAME];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* `text` with `pattern` guaranteed present as a LINE — appended under a
|
|
322
|
+
* comment naming its origin when absent, returned unchanged when present.
|
|
323
|
+
* Line-wise match, same rationale as {@link mergeIgnorePattern}.
|
|
324
|
+
*
|
|
325
|
+
* An explicit `!pattern` line also returns the text unchanged: that is the
|
|
326
|
+
* operator choosing to SHOW the file, and ordered matching means a positive
|
|
327
|
+
* line appended after it would win and silently defeat the choice. Hiding
|
|
328
|
+
* the conventions doc is a default this provides, never a mandate.
|
|
329
|
+
*/
|
|
330
|
+
function withIgnorePattern(text, pattern) {
|
|
331
|
+
const lines = text.split('\n').map((l) => l.trim());
|
|
332
|
+
if (lines.includes(pattern) || lines.includes(`!${pattern}`))
|
|
333
|
+
return text;
|
|
334
|
+
const separator = text.endsWith('\n') ? '' : '\n';
|
|
335
|
+
return `${text}${separator}\n# Added by the platform: the conventions doc is not node content.\n${pattern}\n`;
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=template-files.step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-files.step.js","sourceRoot":"","sources":["../../../../../src/modules/workspace/startup/steps/template-files.step.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '@bevel-software/platform-shared';
|
|
2
2
|
import type { IDiffService } from '../diff/diff.interface.js';
|
|
3
|
-
import type { IKbSeedService } from './kb-seed.interface.js';
|
|
4
3
|
/**
|
|
5
4
|
* Workspaces are per-branch, not per-user (PLAN §3). One on-disk clone per
|
|
6
5
|
* branch name, shared by every user editing that branch — coordination of
|
|
@@ -67,21 +66,6 @@ export declare class WorkspaceService implements IWorkspaceService {
|
|
|
67
66
|
* each other's working tree.
|
|
68
67
|
*/
|
|
69
68
|
private readonly inFlightBootstraps;
|
|
70
|
-
/**
|
|
71
|
-
* Branches whose clone already got this process's scaffolding top-up.
|
|
72
|
-
* Per-process on purpose: each boot of a new build re-offers the top-up
|
|
73
|
-
* once, which is exactly when an upgrade's migrations must run. Cleared
|
|
74
|
-
* wherever `branchDirs` forgets a branch (delete, failed bootstrap), so a
|
|
75
|
-
* re-created clone is offered the top-up again rather than judged by the
|
|
76
|
-
* fate of one that no longer exists.
|
|
77
|
-
*
|
|
78
|
-
* `inFlightTopUps` is the concurrency half: membership in the Set is
|
|
79
|
-
* claimed BEFORE the (slow, file-moving) top-up finishes, so a concurrent
|
|
80
|
-
* caller that finds the branch claimed must still await the in-flight run
|
|
81
|
-
* — returning early would hand out a workspace mid-migration.
|
|
82
|
-
*/
|
|
83
|
-
private readonly toppedUpBranches;
|
|
84
|
-
private readonly inFlightTopUps;
|
|
85
69
|
/** Per-workspace-dir `git fetch origin` timestamp + in-flight tracker. */
|
|
86
70
|
private readonly lastFetchAt;
|
|
87
71
|
private readonly inFlightFetches;
|
|
@@ -90,13 +74,6 @@ export declare class WorkspaceService implements IWorkspaceService {
|
|
|
90
74
|
* WorkspaceService for path resolution.
|
|
91
75
|
*/
|
|
92
76
|
private diffService;
|
|
93
|
-
/**
|
|
94
|
-
* Late-bound by the composition root. Awaited once before the first per-branch
|
|
95
|
-
* clone so the KB remote is guaranteed to carry the protected branches + base
|
|
96
|
-
* scaffolding — a fresh (or partially-populated) remote is seeded rather than
|
|
97
|
-
* assumed. Null in tests / setups that pre-provision the remote themselves.
|
|
98
|
-
*/
|
|
99
|
-
private seedService;
|
|
100
77
|
/**
|
|
101
78
|
* Late-bound by the composition root: notified with the workspace id
|
|
102
79
|
* right after a branch's clone is created. The git layer uses this to
|
|
@@ -118,7 +95,6 @@ export declare class WorkspaceService implements IWorkspaceService {
|
|
|
118
95
|
private readonly kbRepoUrl;
|
|
119
96
|
private readonly gitUsername;
|
|
120
97
|
setDiffService(diffService: IDiffService): void;
|
|
121
|
-
setSeedService(seedService: IKbSeedService): void;
|
|
122
98
|
setWorkspaceClonedListener(listener: (workspaceId: string) => void): void;
|
|
123
99
|
/** Build git clone args that inject credentials via -c headers so the token never appears in the repo URL. */
|
|
124
100
|
private gitCloneArgs;
|
|
@@ -182,17 +158,6 @@ export declare class WorkspaceService implements IWorkspaceService {
|
|
|
182
158
|
* same keys before it pulls.
|
|
183
159
|
*/
|
|
184
160
|
private normalizeCloneTracking;
|
|
185
|
-
/**
|
|
186
|
-
* The ONE spelling of the best-effort scaffolding top-up (three call sites:
|
|
187
|
-
* the restart-survivor path in `getOrCreateForBranch`, and the
|
|
188
|
-
* already-cloned / fresh-clone paths in `cloneProcessMapForBranch`). Once
|
|
189
|
-
* per branch per process, and SINGLE-FLIGHT: the branch is claimed in
|
|
190
|
-
* `toppedUpBranches` before the run starts, so a concurrent caller that
|
|
191
|
-
* finds it claimed awaits the in-flight run instead of returning a
|
|
192
|
-
* workspace the migration is still moving files under. Never throws — a
|
|
193
|
-
* failed top-up costs the upgrade, not the clone.
|
|
194
|
-
*/
|
|
195
|
-
private topUpScaffolding;
|
|
196
161
|
private cloneProcessMapForBranch;
|
|
197
162
|
getMetadata(workspaceId: string): Promise<WorkspaceInfo | null>;
|
|
198
163
|
/**
|