@bevel-software/platform-shared 0.15.1 → 0.20.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/auth/types.d.ts +12 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/git/pr.types.d.ts +75 -11
- package/dist/git/pr.types.d.ts.map +1 -1
- package/dist/git/types.d.ts +28 -1
- package/dist/git/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/workflow/events.d.ts +32 -4
- package/dist/workflow/events.d.ts.map +1 -1
- package/dist/workflow/events.js.map +1 -1
- package/dist/workflow/interface.d.ts +103 -4
- package/dist/workflow/interface.d.ts.map +1 -1
- package/dist/workflow/types.d.ts +49 -0
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workspace/access-verbs.d.ts +83 -0
- package/dist/workspace/access-verbs.d.ts.map +1 -0
- package/dist/workspace/access-verbs.js +110 -0
- package/dist/workspace/access-verbs.js.map +1 -0
- package/dist/workspace/agent-preamble.d.ts +33 -0
- package/dist/workspace/agent-preamble.d.ts.map +1 -0
- package/dist/workspace/agent-preamble.js +45 -0
- package/dist/workspace/agent-preamble.js.map +1 -0
- package/dist/workspace/entry-exists.d.ts +27 -0
- package/dist/workspace/entry-exists.d.ts.map +1 -0
- package/dist/workspace/entry-exists.js +33 -0
- package/dist/workspace/entry-exists.js.map +1 -0
- package/dist/workspace/filename.d.ts +15 -0
- package/dist/workspace/filename.d.ts.map +1 -1
- package/dist/workspace/filename.js +30 -0
- package/dist/workspace/filename.js.map +1 -1
- package/dist/workspace/frontmatter-carriers.d.ts +44 -0
- package/dist/workspace/frontmatter-carriers.d.ts.map +1 -0
- package/dist/workspace/frontmatter-carriers.js +52 -0
- package/dist/workspace/frontmatter-carriers.js.map +1 -0
- package/dist/workspace/frontmatter.d.ts +23 -4
- package/dist/workspace/frontmatter.d.ts.map +1 -1
- package/dist/workspace/frontmatter.js +59 -9
- package/dist/workspace/frontmatter.js.map +1 -1
- package/dist/workspace/kb-layout.d.ts +226 -19
- package/dist/workspace/kb-layout.d.ts.map +1 -1
- package/dist/workspace/kb-layout.js +353 -23
- package/dist/workspace/kb-layout.js.map +1 -1
- package/dist/workspace/placeholder.d.ts +21 -0
- package/dist/workspace/placeholder.d.ts.map +1 -0
- package/dist/workspace/placeholder.js +28 -0
- package/dist/workspace/placeholder.js.map +1 -0
- package/dist/workspace/platform-files.d.ts +105 -0
- package/dist/workspace/platform-files.d.ts.map +1 -0
- package/dist/workspace/platform-files.js +147 -0
- package/dist/workspace/platform-files.js.map +1 -0
- package/dist/workspace/types.d.ts +8 -0
- package/dist/workspace/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/auth/types.ts +12 -0
- package/src/git/pr.types.ts +77 -11
- package/src/git/types.ts +35 -1
- package/src/index.ts +6 -0
- package/src/workflow/events.ts +33 -3
- package/src/workflow/interface.ts +127 -4
- package/src/workflow/types.ts +43 -0
- package/src/workspace/access-verbs.ts +124 -0
- package/src/workspace/agent-preamble.ts +47 -0
- package/src/workspace/entry-exists.ts +36 -0
- package/src/workspace/filename.ts +31 -0
- package/src/workspace/frontmatter-carriers.ts +57 -0
- package/src/workspace/frontmatter.ts +57 -8
- package/src/workspace/kb-layout.ts +388 -25
- package/src/workspace/placeholder.ts +29 -0
- package/src/workspace/platform-files.ts +188 -0
- package/src/workspace/types.ts +8 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_KB_LAYOUT,
|
|
3
|
+
FIXED_PLATFORM_FILE_NAMES,
|
|
4
|
+
agentsFileOf,
|
|
5
|
+
currentKbLayout,
|
|
6
|
+
reservedRootDirNames,
|
|
7
|
+
type KbLayout,
|
|
8
|
+
} from './kb-layout.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The files the platform reads as configuration, not content. `access.md`
|
|
12
|
+
* governs the folder it sits in and `.bevelignore` layers like `.gitignore`,
|
|
13
|
+
* so both count at any depth; `roles.yaml` and the agent guide are read from
|
|
14
|
+
* the repository root only, so a nested file of either name is ordinary
|
|
15
|
+
* content. Moving one changes what the platform enforces, so moves refuse them.
|
|
16
|
+
*
|
|
17
|
+
* A FUNCTION, not a constant, and that is the whole of the configurable-guide
|
|
18
|
+
* change on this side: the guide's name is a deployment setting, so the fourth
|
|
19
|
+
* platform file is `HEXIS.md` on one deployment and `AGENTS.md` on the next —
|
|
20
|
+
* and on the first, a root `AGENTS.md` is the CUSTOMER'S own conventions file,
|
|
21
|
+
* which has to move and delete like any page. Every gate asks this rather than
|
|
22
|
+
* reading a list captured at module load.
|
|
23
|
+
*/
|
|
24
|
+
export function platformFileNames(layout: KbLayout = currentKbLayout()): readonly string[] {
|
|
25
|
+
return [...FIXED_PLATFORM_FILE_NAMES, agentsFileOf(layout)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The platform file names under the DEFAULT layout — what they were before the
|
|
30
|
+
* guide could be renamed. Kept for callers that want the default answer rather
|
|
31
|
+
* than this deployment's; anything judging a real path asks
|
|
32
|
+
* {@link platformFileNames}, which knows what this deployment called its guide.
|
|
33
|
+
*/
|
|
34
|
+
export const PLATFORM_FILE_NAMES: readonly string[] = Object.freeze(
|
|
35
|
+
platformFileNames(DEFAULT_KB_LAYOUT),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/** The platform files that are read wherever they sit, not only at the root. */
|
|
39
|
+
const PLATFORM_FILES_AT_ANY_DEPTH = new Set(['access.md', '.bevelignore']);
|
|
40
|
+
|
|
41
|
+
/** The names in effect, as a set — rebuilt per call, because the guide's is configurable. */
|
|
42
|
+
const platformFiles = (): ReadonlySet<string> => new Set(platformFileNames());
|
|
43
|
+
|
|
44
|
+
const normalize = (path: string): string => path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
|
|
45
|
+
|
|
46
|
+
const baseName = (path: string): string => {
|
|
47
|
+
const trimmed = normalize(path);
|
|
48
|
+
return trimmed.slice(trimmed.lastIndexOf('/') + 1);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A path that walks out of the repository (`..`) or stands still (`.`). The
|
|
53
|
+
* restore exception is the one write allowed past a destination that denies
|
|
54
|
+
* it, so it answers on the spelling it was handed and refuses anything whose
|
|
55
|
+
* meaning depends on resolving it: `KnowledgeBase/../access.md` names the
|
|
56
|
+
* root's file to a resolver and a nested one to a reader of segments. The
|
|
57
|
+
* move's own path-safety check refuses these too — this gate does not lean on
|
|
58
|
+
* that one.
|
|
59
|
+
*/
|
|
60
|
+
const hasTraversal = (path: string): boolean =>
|
|
61
|
+
normalize(path).split('/').some((segment) => segment === '..' || segment === '.');
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the repository-relative `repoRelativePath` is a platform file.
|
|
65
|
+
* Exact spelling, as the platform reads it: `Access.md` is content. A caller
|
|
66
|
+
* on a case-insensitive disk passes the path's on-disk spelling.
|
|
67
|
+
*/
|
|
68
|
+
export function isPlatformFile(repoRelativePath: string): boolean {
|
|
69
|
+
const norm = normalize(repoRelativePath);
|
|
70
|
+
const name = baseName(norm);
|
|
71
|
+
if (!platformFiles().has(name)) return false;
|
|
72
|
+
return PLATFORM_FILES_AT_ANY_DEPTH.has(name) || norm === name;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The one sentence every surface refuses a platform-file move with. */
|
|
76
|
+
export function platformFileRefusal(pathOrName: string): string {
|
|
77
|
+
return `${baseName(pathOrName)} is a platform file and stays in its folder.`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The sentence a move is refused with when the DESTINATION would be a platform
|
|
82
|
+
* file — a note renamed to `access.md`, or dragged onto the one that is there.
|
|
83
|
+
* The other refusal keeps a platform file where the platform reads it; this one
|
|
84
|
+
* keeps everything else from becoming one, which a rename on disk would
|
|
85
|
+
* otherwise do silently: the folder would come back governed by rules nobody
|
|
86
|
+
* wrote as rules.
|
|
87
|
+
*/
|
|
88
|
+
export function platformFileCreationRefusal(pathOrName: string): string {
|
|
89
|
+
return `${baseName(pathOrName)} is a platform file name; a move cannot create a platform file.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether `repoRelativeDir` is a folder the platform owns rather than content:
|
|
94
|
+
* the repository root itself, or one of its reserved top-level folders
|
|
95
|
+
* (`KnowledgeBase/`, `Skills/`, `Plugins/`, `Data/`, …). Deleting or moving
|
|
96
|
+
* one takes a whole section of the knowledge base with it. Exact spelling; a
|
|
97
|
+
* caller on a case-insensitive disk passes the path's on-disk spelling.
|
|
98
|
+
*/
|
|
99
|
+
export function isPlatformFolder(repoRelativeDir: string): boolean {
|
|
100
|
+
const norm = normalize(repoRelativeDir);
|
|
101
|
+
if (norm === '') return true;
|
|
102
|
+
return !norm.includes('/') && reservedRootDirNames().has(norm);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The sentence a delete or move of a platform folder is refused with. */
|
|
106
|
+
export function platformFolderRefusal(repoRelativeDir: string): string {
|
|
107
|
+
const norm = normalize(repoRelativeDir);
|
|
108
|
+
return norm === ''
|
|
109
|
+
? 'The repository root is a platform folder and cannot be moved or deleted.'
|
|
110
|
+
: `${norm}/ is a platform folder and cannot be moved or deleted.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether the platform file at `repoRelativePath` sits directly in the
|
|
115
|
+
* repository root — the copy every one of the four is read from there, and so
|
|
116
|
+
* never the misplaced one: it is the copy a restore puts back. A nested
|
|
117
|
+
* `access.md` or `.bevelignore` is a platform file too, but it layers on top
|
|
118
|
+
* of the root's rather than standing in for it, which is why moving the
|
|
119
|
+
* ROOT's copy into a folder is a move out and not a restore.
|
|
120
|
+
*/
|
|
121
|
+
export function isRootPlatformFile(repoRelativePath: string): boolean {
|
|
122
|
+
return platformFiles().has(normalize(repoRelativePath));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The place a misplaced platform file is allowed to be put back, when
|
|
127
|
+
* `repoRelativeDestination` names one, and null when it does not.
|
|
128
|
+
*
|
|
129
|
+
* `roles.yaml` and the agent guide are read from the repository root and
|
|
130
|
+
* nowhere else, so their one required location is the root. A nested `.bevelignore` is
|
|
131
|
+
* read too (it layers, see `BevelIgnoreStack`), yet a restore of one lands at
|
|
132
|
+
* the root only — a deliberate narrowing of the exception, not a claim about
|
|
133
|
+
* where the file is read: the root's copy is the one whose absence breaks the
|
|
134
|
+
* workspace, and a folder that never had a `.bevelignore` is not missing one.
|
|
135
|
+
* `access.md` governs whatever folder it sits in, so a folder that has none
|
|
136
|
+
* is a place one is missing from — WHETHER the folder has one is a fact about
|
|
137
|
+
* the disk, which this pure predicate does not know and the caller checks
|
|
138
|
+
* (see `AccessControlService.canRestorePlatformFile`).
|
|
139
|
+
*/
|
|
140
|
+
export type PlatformRestoreDestination =
|
|
141
|
+
| { name: string; kind: 'root' }
|
|
142
|
+
| { name: string; kind: 'folder-without-access-md'; dir: string };
|
|
143
|
+
|
|
144
|
+
export function platformRestoreDestination(
|
|
145
|
+
repoRelativeDestination: string,
|
|
146
|
+
): PlatformRestoreDestination | null {
|
|
147
|
+
const norm = normalize(repoRelativeDestination);
|
|
148
|
+
if (hasTraversal(norm)) return null;
|
|
149
|
+
const name = baseName(norm);
|
|
150
|
+
if (!platformFiles().has(name)) return null;
|
|
151
|
+
if (name === 'access.md') {
|
|
152
|
+
const slash = norm.lastIndexOf('/');
|
|
153
|
+
return { name, kind: 'folder-without-access-md', dir: slash === -1 ? '' : norm.slice(0, slash) };
|
|
154
|
+
}
|
|
155
|
+
return norm === name ? { name, kind: 'root' } : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether moving `repoRelativeSource` to `repoRelativeDestination` has the
|
|
160
|
+
* SHAPE of a platform-file restore — an admin putting a misplaced copy back.
|
|
161
|
+
* Who is asking is not part of the shape; `canRestorePlatformFile` answers
|
|
162
|
+
* that, and the state of the disk with it.
|
|
163
|
+
*
|
|
164
|
+
* Three things make the shape, and all three are about the move rather than
|
|
165
|
+
* about the source's current standing:
|
|
166
|
+
*
|
|
167
|
+
* - the source is NAMED like a platform file. A nested `roles.yaml`, or a
|
|
168
|
+
* nested copy of the agent guide, is ordinary content where it sits
|
|
169
|
+
* (`isPlatformFile` says so, and moving it needs no exception), but it is
|
|
170
|
+
* still the copy a restore carries back to the root — judging the shape on
|
|
171
|
+
* `isPlatformFile` would skip the exception for exactly the two files the
|
|
172
|
+
* root can lose;
|
|
173
|
+
* - the source is not the root's own copy, which is the copy a restore puts
|
|
174
|
+
* back, never the one it takes out;
|
|
175
|
+
* - the destination is a required location for that same name, so the file
|
|
176
|
+
* lands under the name the platform reads rather than beside it.
|
|
177
|
+
*/
|
|
178
|
+
export function isPlatformRestoreShape(
|
|
179
|
+
repoRelativeSource: string,
|
|
180
|
+
repoRelativeDestination: string,
|
|
181
|
+
): boolean {
|
|
182
|
+
if (hasTraversal(repoRelativeSource)) return false;
|
|
183
|
+
const name = baseName(repoRelativeSource);
|
|
184
|
+
if (!platformFiles().has(name)) return false;
|
|
185
|
+
if (isRootPlatformFile(repoRelativeSource)) return false;
|
|
186
|
+
const target = platformRestoreDestination(repoRelativeDestination);
|
|
187
|
+
return target !== null && target.name === name;
|
|
188
|
+
}
|
package/src/workspace/types.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface FileTreeEntry {
|
|
|
26
26
|
relativePath: string;
|
|
27
27
|
type: 'file' | 'directory';
|
|
28
28
|
children?: FileTreeEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* Set on a DIRECTORY of a read-filtered listing, and only when non-zero:
|
|
31
|
+
* how many entries the caller's read rules kept out of that directory's
|
|
32
|
+
* subtree — on the root, out of the whole listing. A number, never names —
|
|
33
|
+
* it lets the explorer tell "nothing is shared with you" apart from "this
|
|
34
|
+
* folder is empty" without saying what exists.
|
|
35
|
+
*/
|
|
36
|
+
withheld?: number;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
import type { AuthUser } from '../auth/types.js';
|