@blkc-code/mapgen 0.3.0 → 0.6.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/index.d.ts +34 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +108 -54
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +2 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +1 -0
- package/dist/plan.js.map +1 -1
- package/dist/project-files.d.ts +13 -0
- package/dist/project-files.d.ts.map +1 -0
- package/dist/project-files.js +33 -0
- package/dist/project-files.js.map +1 -0
- package/dist/roads.d.ts +15 -0
- package/dist/roads.d.ts.map +1 -0
- package/dist/roads.js +328 -0
- package/dist/roads.js.map +1 -0
- package/dist/tree.d.ts +55 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +252 -0
- package/dist/tree.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +132 -53
- package/src/plan.ts +3 -0
- package/src/project-files.ts +39 -0
- package/src/roads.ts +326 -0
- package/src/tree.ts +285 -0
package/dist/tree.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project as a world, before anything is placed. A folder holding an application is a
|
|
3
|
+
* building. A folder holding none is a road, at any depth, named by its literal path. Loose
|
|
4
|
+
* files sit on the road of their own folder, grouped by type. Files that look like secrets
|
|
5
|
+
* are kept apart, for the vault.
|
|
6
|
+
*
|
|
7
|
+
* Only names and counts are read here, and package manifests for links. Never source code.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { readdirSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { DEFAULT_APP_MARKERS, IGNORE, gitignoredNames, isOwnRepo, markerMatcher, readPackage } from "./project-files.js";
|
|
13
|
+
export const SECRETS_GROUP = "Secrets";
|
|
14
|
+
/** Secrets comes first, so a file like .env.json is never left in Config. */
|
|
15
|
+
export const DEFAULT_FILE_GROUPS = {
|
|
16
|
+
[SECRETS_GROUP]: [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa", "id_ed25519", ".npmrc", ".netrc"],
|
|
17
|
+
Docs: ["*.md", "*.mdx", "*.rst", "LICENSE*", "NOTICE*"],
|
|
18
|
+
Config: [".env.*", "*.json", "*.jsonc", "*.yaml", "*.yml", "*.toml", "*.config.*", ".*ignore", ".*rc", ".*rc.*", ".editorconfig", "Dockerfile*", "Makefile"],
|
|
19
|
+
Pages: ["*.html", "*.htm"],
|
|
20
|
+
Images: ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.webp", "*.ico"],
|
|
21
|
+
Logs: ["*.log", "*.txt", "*history*"],
|
|
22
|
+
Code: ["*.ts", "*.tsx", "*.js", "*.mjs", "*.cjs", "*.py", "*.go", "*.rs", "*.java", "*.rb", "*.php", "*.sh", "*.css", "*.sql"],
|
|
23
|
+
};
|
|
24
|
+
const OTHER_GROUP = "Other";
|
|
25
|
+
/** Templates are meant to be committed, and hold no real values. */
|
|
26
|
+
const SECRET_TEMPLATE = /\.(example|sample|template|dist)$/i;
|
|
27
|
+
/** What a folder inside an application is, by its name. Anything else under src/ is a Desk. */
|
|
28
|
+
export const DEFAULT_PART_TYPES = {
|
|
29
|
+
"Server rack": ["api", "server", "servers", "backend", "supabase", "migrations", "db", "database", "prisma"],
|
|
30
|
+
Workbench: ["lib", "libs", "shared", "utils", "core", "common", "kernel"],
|
|
31
|
+
Whiteboard: ["test", "tests", "__tests__", "e2e", "spec", "specs"],
|
|
32
|
+
Bookshelf: ["docs", "doc", "documentation"],
|
|
33
|
+
Toolbox: ["scripts", "tools", "bin", "skills"],
|
|
34
|
+
Plants: ["public", "assets", "static", "images"],
|
|
35
|
+
};
|
|
36
|
+
const FEATURE_ITEM = "Desk";
|
|
37
|
+
const OTHER_ITEM = "Filing cabinet";
|
|
38
|
+
/** Folders inside an application that hold its parts, rather than being one. */
|
|
39
|
+
const PART_CONTAINERS = new Set(["src", "app", "pages", "features", "modules"]);
|
|
40
|
+
const MAX_ENTRIES = 400000;
|
|
41
|
+
const MAX_DEPTH = 12;
|
|
42
|
+
const clean = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
43
|
+
const join = (base, name) => (base ? `${base}/${name}` : name);
|
|
44
|
+
const within = (rel, parent) => rel === parent || rel.startsWith(`${parent}/`);
|
|
45
|
+
export function scanTree(root, options = {}) {
|
|
46
|
+
const gitignored = gitignoredNames(root);
|
|
47
|
+
const skipped = new Set((options.ignore ?? []).map(clean));
|
|
48
|
+
const isSkipped = (rel) => skipped.has(rel) || skipped.has(path.posix.basename(rel));
|
|
49
|
+
const roads = (options.roads ?? []).map(clean);
|
|
50
|
+
/** An added road, somewhere above or below this folder, opens the way through. */
|
|
51
|
+
const onAddedRoad = (rel) => roads.some((road) => within(rel, road) || within(road, rel));
|
|
52
|
+
const markers = [...(options.replaceDefaults ? [] : DEFAULT_APP_MARKERS), ...(options.markers ?? [])].map(markerMatcher);
|
|
53
|
+
const includes = new Set((options.include ?? []).map(clean));
|
|
54
|
+
const groupMatchers = Object.entries({ ...DEFAULT_FILE_GROUPS, ...options.fileGroups })
|
|
55
|
+
.sort(([a], [b]) => Number(b === SECRETS_GROUP) - Number(a === SECRETS_GROUP))
|
|
56
|
+
.map(([name, patterns]) => ({ name, patterns: patterns.map(markerMatcher) }));
|
|
57
|
+
const groupOf = (file) => groupMatchers.find((group) => !(group.name === SECRETS_GROUP && SECRET_TEMPLATE.test(file)) && group.patterns.some((pattern) => pattern.test(file)))?.name ?? OTHER_GROUP;
|
|
58
|
+
const customParts = Object.entries(options.partTypes ?? {}).map(([item, folders]) => ({ item, folders: folders.map(clean) }));
|
|
59
|
+
const itemFor = (rel, nested) => {
|
|
60
|
+
const name = path.posix.basename(rel);
|
|
61
|
+
const custom = customParts.find(({ folders }) => folders.some((folder) => folder === name || rel === folder || rel.endsWith(`/${folder}`)));
|
|
62
|
+
if (custom)
|
|
63
|
+
return custom.item;
|
|
64
|
+
const known = Object.entries(DEFAULT_PART_TYPES).find(([, folders]) => folders.includes(name));
|
|
65
|
+
if (known)
|
|
66
|
+
return known[0];
|
|
67
|
+
return nested ? FEATURE_ITEM : OTHER_ITEM;
|
|
68
|
+
};
|
|
69
|
+
// What git tracks is the truth, repo by repo, since an added road can hold separate repos.
|
|
70
|
+
const trackedIn = new Map();
|
|
71
|
+
const tracked = (repo) => {
|
|
72
|
+
let files = trackedIn.get(repo);
|
|
73
|
+
if (!files) {
|
|
74
|
+
try {
|
|
75
|
+
files = execFileSync("git", ["ls-files", "-z"], { cwd: path.join(root, repo), encoding: "utf8", maxBuffer: 512 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
files = [];
|
|
79
|
+
}
|
|
80
|
+
trackedIn.set(repo, files);
|
|
81
|
+
}
|
|
82
|
+
return files;
|
|
83
|
+
};
|
|
84
|
+
let budget = MAX_ENTRIES;
|
|
85
|
+
let truncated = false;
|
|
86
|
+
const list = (rel) => {
|
|
87
|
+
try {
|
|
88
|
+
const entries = readdirSync(path.join(root, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
89
|
+
budget -= entries.length;
|
|
90
|
+
if (budget <= 0)
|
|
91
|
+
truncated = true;
|
|
92
|
+
return entries;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const countOnDisk = (rel, depth = 0) => {
|
|
99
|
+
if (budget <= 0 || depth > MAX_DEPTH)
|
|
100
|
+
return 0;
|
|
101
|
+
let total = 0;
|
|
102
|
+
for (const entry of list(rel)) {
|
|
103
|
+
if (entry.isFile())
|
|
104
|
+
total += 1;
|
|
105
|
+
else if (entry.isDirectory() && !entry.name.startsWith(".") && !IGNORE.has(entry.name) && !gitignored.has(entry.name))
|
|
106
|
+
total += countOnDisk(join(rel, entry.name), depth + 1);
|
|
107
|
+
}
|
|
108
|
+
return total;
|
|
109
|
+
};
|
|
110
|
+
const countFiles = (rel, repo) => {
|
|
111
|
+
const inside = repo ? rel.slice(repo.length + 1) : rel;
|
|
112
|
+
const prefix = inside ? `${inside}/` : "";
|
|
113
|
+
const count = tracked(repo).filter((file) => file.startsWith(prefix)).length;
|
|
114
|
+
return count > 0 ? count : countOnDisk(rel);
|
|
115
|
+
};
|
|
116
|
+
const secrets = [];
|
|
117
|
+
const looseFiles = (owner, files) => {
|
|
118
|
+
const groups = new Map();
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const rel = join(owner, file);
|
|
121
|
+
const node = { id: `file:${rel}`, kind: "file", name: file, path: rel, children: [] };
|
|
122
|
+
const group = groupOf(file);
|
|
123
|
+
if (group === SECRETS_GROUP) {
|
|
124
|
+
secrets.push(node);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
groups.set(group, [...(groups.get(group) ?? []), node]);
|
|
128
|
+
}
|
|
129
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, children]) => ({ id: `group:${owner}:${name}`, kind: "group", name, path: owner, children }));
|
|
130
|
+
};
|
|
131
|
+
/** A folder is shown unless it's hidden, junk, ignored, or another repo no road opened. */
|
|
132
|
+
const showFolder = (rel, name) => {
|
|
133
|
+
if (IGNORE.has(name) || isSkipped(rel))
|
|
134
|
+
return false;
|
|
135
|
+
const opened = onAddedRoad(rel);
|
|
136
|
+
if (name.startsWith(".") && name !== ".github" && !opened)
|
|
137
|
+
return false;
|
|
138
|
+
if (gitignored.has(name) && !opened)
|
|
139
|
+
return false;
|
|
140
|
+
return opened || !isOwnRepo(path.join(root, rel));
|
|
141
|
+
};
|
|
142
|
+
const isApp = (rel, files) => includes.has(rel) || files.some((file) => markers.some((marker) => marker.test(file)));
|
|
143
|
+
const partsOf = (rel, repo, nested, depth) => {
|
|
144
|
+
const parts = [];
|
|
145
|
+
for (const entry of list(rel)) {
|
|
146
|
+
if (!entry.isDirectory())
|
|
147
|
+
continue;
|
|
148
|
+
const child = join(rel, entry.name);
|
|
149
|
+
if (entry.name.startsWith(".") || !showFolder(child, entry.name))
|
|
150
|
+
continue;
|
|
151
|
+
if (PART_CONTAINERS.has(entry.name) && depth < 3) {
|
|
152
|
+
const inner = partsOf(child, repo, true, depth + 1);
|
|
153
|
+
if (inner.length > 0) {
|
|
154
|
+
parts.push(...inner);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const files = countFiles(child, repo);
|
|
159
|
+
if (files > 0)
|
|
160
|
+
parts.push({ id: `part:${child}`, kind: "part", name: path.posix.basename(child), path: child, files, item: itemFor(child, nested), children: [] });
|
|
161
|
+
}
|
|
162
|
+
return parts;
|
|
163
|
+
};
|
|
164
|
+
const building = (rel, repo, files) => ({
|
|
165
|
+
id: `building:${rel}`,
|
|
166
|
+
kind: "building",
|
|
167
|
+
name: path.posix.basename(rel) || path.basename(path.resolve(root)),
|
|
168
|
+
path: rel,
|
|
169
|
+
files: countFiles(rel, repo),
|
|
170
|
+
children: [...partsOf(rel, repo, false, 0), ...looseFiles(rel, files)],
|
|
171
|
+
});
|
|
172
|
+
const walk = (rel, repo, depth) => {
|
|
173
|
+
const entries = list(rel);
|
|
174
|
+
const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
175
|
+
const children = [];
|
|
176
|
+
if (depth < MAX_DEPTH && budget > 0) {
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
if (!entry.isDirectory())
|
|
179
|
+
continue;
|
|
180
|
+
const child = join(rel, entry.name);
|
|
181
|
+
if (!showFolder(child, entry.name))
|
|
182
|
+
continue;
|
|
183
|
+
const childRepo = isOwnRepo(path.join(root, child)) ? child : repo;
|
|
184
|
+
const childFiles = list(child).filter((item) => item.isFile()).map((item) => item.name);
|
|
185
|
+
if (isApp(child, childFiles)) {
|
|
186
|
+
children.push(building(child, childRepo, childFiles));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const road = walk(child, childRepo, depth + 1);
|
|
190
|
+
if (road.children.length > 0)
|
|
191
|
+
children.push(road);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (depth >= MAX_DEPTH) {
|
|
196
|
+
truncated = true;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
id: `road:${rel}`,
|
|
200
|
+
kind: "road",
|
|
201
|
+
name: rel ? `${path.posix.basename(rel)}/` : "/",
|
|
202
|
+
path: rel,
|
|
203
|
+
children: [...children, ...looseFiles(rel, files)],
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
const hasBuildings = (node) => node.children.some((child) => child.kind === "building" || (child.kind === "road" && hasBuildings(child)));
|
|
207
|
+
const topFiles = list("").filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
208
|
+
let top = walk("", "", 0);
|
|
209
|
+
// With no application inside it, the whole project is one, marked or not.
|
|
210
|
+
if (!hasBuildings(top)) {
|
|
211
|
+
secrets.length = 0;
|
|
212
|
+
top = building("", "", topFiles);
|
|
213
|
+
}
|
|
214
|
+
const buildings = [];
|
|
215
|
+
const collect = (node) => {
|
|
216
|
+
if (node.kind === "building")
|
|
217
|
+
buildings.push(node);
|
|
218
|
+
for (const child of node.children)
|
|
219
|
+
if (child.kind === "road")
|
|
220
|
+
collect(child);
|
|
221
|
+
if (node.kind === "road")
|
|
222
|
+
for (const child of node.children)
|
|
223
|
+
if (child.kind === "building")
|
|
224
|
+
buildings.push(child);
|
|
225
|
+
};
|
|
226
|
+
collect(top);
|
|
227
|
+
const owners = new Map();
|
|
228
|
+
const manifests = new Map();
|
|
229
|
+
for (const node of buildings) {
|
|
230
|
+
const manifest = readPackage(path.join(root, node.path));
|
|
231
|
+
if (!manifest)
|
|
232
|
+
continue;
|
|
233
|
+
manifests.set(node.id, manifest);
|
|
234
|
+
if (typeof manifest["name"] === "string")
|
|
235
|
+
owners.set(manifest["name"], node.id);
|
|
236
|
+
}
|
|
237
|
+
const links = [];
|
|
238
|
+
for (const [from, manifest] of manifests) {
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
241
|
+
for (const name of Object.keys((manifest[field] ?? {}))) {
|
|
242
|
+
const to = owners.get(name);
|
|
243
|
+
if (to && to !== from && !seen.has(to)) {
|
|
244
|
+
seen.add(to);
|
|
245
|
+
links.push({ from, to });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { root: top, secrets, links, truncated };
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=tree.js.map
|
package/dist/tree.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blkc-code/mapgen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Grows a unique map from a project, and keeps it in step as the project changes",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "^5.9.3",
|
|
29
29
|
"vitest": "^5.1.0",
|
|
30
|
-
"@blkc-code/render-canvas": "^0.
|
|
30
|
+
"@blkc-code/render-canvas": "^0.7.0",
|
|
31
31
|
"@types/node": "^22.9.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* here, never source code.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
11
12
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
14
|
+
import { DEFAULT_APP_MARKERS, IGNORE, gitignoredNames, isOwnRepo, markerMatcher, readPackage } from "./project-files.js";
|
|
13
15
|
import { OFFICE_PLAN, worldPlan, type RoomRole, type Theme, type WorldPlan } from "./plan.js";
|
|
14
16
|
|
|
15
17
|
export * from "./plan.js";
|
|
18
|
+
export * from "./project-files.js";
|
|
19
|
+
export * from "./tree.js";
|
|
20
|
+
export * from "./roads.js";
|
|
16
21
|
|
|
17
22
|
export interface MapRoom {
|
|
18
23
|
id: string;
|
|
@@ -28,15 +33,26 @@ export interface MapRoom {
|
|
|
28
33
|
/** Edited by hand: the generator leaves it alone. */
|
|
29
34
|
locked?: boolean;
|
|
30
35
|
files?: number;
|
|
36
|
+
/** The road this sits on, as a path. */
|
|
37
|
+
road?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface MapItem {
|
|
40
|
+
type: string;
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
room?: string;
|
|
44
|
+
/** The real folder or file group this stands for. */
|
|
45
|
+
part?: string;
|
|
46
|
+
/** Shown beside it on the map, for things like a group of loose files. */
|
|
47
|
+
label?: string;
|
|
31
48
|
}
|
|
32
|
-
export interface MapItem { type: string; x: number; y: number; room?: string }
|
|
33
49
|
export interface MapDoor { x: number; y: number }
|
|
34
50
|
export interface OfficeMap {
|
|
35
51
|
version: 1;
|
|
36
52
|
/** Which world this map is for. The office if left out. */
|
|
37
53
|
world?: string;
|
|
38
|
-
/**
|
|
39
|
-
layout?: "halls";
|
|
54
|
+
/** roads: grows from the project's folders, with no size limit. halls: even rows along corridors. Left out on the oldest maps. */
|
|
55
|
+
layout?: "halls" | "roads";
|
|
40
56
|
/** How tall the rooms are in the first pair of rows. */
|
|
41
57
|
roomHeight?: number;
|
|
42
58
|
seed: string;
|
|
@@ -63,7 +79,22 @@ export interface ProjectArea {
|
|
|
63
79
|
files: number;
|
|
64
80
|
/** How many other areas depend on this one. */
|
|
65
81
|
dependents?: number;
|
|
82
|
+
/** The folder path it sits on, such as "apps/". */
|
|
83
|
+
street?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** How to read a project. This is the part of a Kit's world.json the scan needs. */
|
|
87
|
+
export interface ScanOptions {
|
|
88
|
+
/** File names that mark a folder as an application, such as "go.mod" or "*.xydsf". */
|
|
89
|
+
markers?: string[];
|
|
90
|
+
/** Use only `markers`, not the built-in ones as well. */
|
|
91
|
+
replaceDefaults?: boolean;
|
|
92
|
+
/** Folders that get a building even with no marker. */
|
|
93
|
+
include?: string[];
|
|
94
|
+
/** Folders that never get a building: a name, or a path from the top. */
|
|
95
|
+
ignore?: string[];
|
|
66
96
|
}
|
|
97
|
+
|
|
67
98
|
export interface ProjectScan {
|
|
68
99
|
areas: ProjectArea[];
|
|
69
100
|
rootFiles: number;
|
|
@@ -82,12 +113,14 @@ const TIGHT_ABOVE = 14;
|
|
|
82
113
|
const MAX_ENTRIES = 200000;
|
|
83
114
|
/** One huge folder can't use up the scan and leave the rest uncounted. */
|
|
84
115
|
const MAX_ENTRIES_PER_AREA = 20000;
|
|
116
|
+
/** How deep to look for applications. */
|
|
117
|
+
const MAX_APP_DEPTH = 5;
|
|
118
|
+
|
|
85
119
|
const MAX_DEPTH = 8;
|
|
86
|
-
const IGNORE = new Set(["node_modules", ".git", "dist", "build", "out", "target", "coverage", ".next", ".turbo", ".cache", "vendor", "venv", "__pycache__", ".venv", ".idea", ".vscode", ".claude", ".blkc", ".gemini", ".cursor"]);
|
|
87
120
|
const CONTAINERS = new Set(["src", "packages", "apps", "services", "libs", "lib", "modules", "crates", "cmd", "internal", "app"]);
|
|
88
121
|
|
|
89
|
-
const slug = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
90
|
-
const titled = (value: string): string =>
|
|
122
|
+
export const slug = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
123
|
+
export const titled = (value: string): string =>
|
|
91
124
|
value.replace(/^\./, "").split(/[-_]/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ").slice(0, 18) || value.slice(0, 18);
|
|
92
125
|
|
|
93
126
|
export function hashText(text: string): number {
|
|
@@ -97,7 +130,7 @@ export function hashText(text: string): number {
|
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
/** A small seeded generator, so the same project always grows the same way. */
|
|
100
|
-
function rng(seed: string): () => number {
|
|
133
|
+
export function rng(seed: string): () => number {
|
|
101
134
|
let state = hashText(seed) || 1;
|
|
102
135
|
return () => {
|
|
103
136
|
state ^= state << 13;
|
|
@@ -108,33 +141,8 @@ function rng(seed: string): () => number {
|
|
|
108
141
|
};
|
|
109
142
|
}
|
|
110
143
|
|
|
111
|
-
/** The plain folder names in a .gitignore. Patterns with wildcards aren't followed. */
|
|
112
|
-
export function gitignoredNames(root: string): Set<string> {
|
|
113
|
-
const names = new Set<string>();
|
|
114
|
-
try {
|
|
115
|
-
for (const line of readFileSync(path.join(root, ".gitignore"), "utf8").split("\n")) {
|
|
116
|
-
const name = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
117
|
-
if (name && !/[*!#/\s]/.test(name)) names.add(name);
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
// no .gitignore
|
|
121
|
-
}
|
|
122
|
-
return names;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** A folder with its own .git is another project, even if it sits inside this one. */
|
|
126
|
-
export const isOwnRepo = (dir: string): boolean => existsSync(path.join(dir, ".git"));
|
|
127
|
-
|
|
128
144
|
type Json = Record<string, unknown>;
|
|
129
145
|
|
|
130
|
-
const readPackage = (dir: string): Json | undefined => {
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8")) as Json;
|
|
133
|
-
} catch {
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
146
|
/** Top-level folders a workspace config says hold packages, such as tools from "tools/*". */
|
|
139
147
|
function workspaceFolders(root: string): string[] {
|
|
140
148
|
const globs: string[] = [];
|
|
@@ -179,8 +187,19 @@ function findLinks(root: string, areas: ProjectArea[]): { from: string; to: stri
|
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
/** Folder names, file counts and how the folders depend on each other. */
|
|
182
|
-
export function scanProject(root: string): ProjectScan {
|
|
190
|
+
export function scanProject(root: string, options: ScanOptions = {}): ProjectScan {
|
|
183
191
|
const ignored = new Set([...IGNORE, ...gitignoredNames(root)]);
|
|
192
|
+
const clean = (value: string): string => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
193
|
+
const skipped = new Set((options.ignore ?? []).map(clean));
|
|
194
|
+
const isSkipped = (rel: string): boolean => skipped.has(rel) || skipped.has(path.posix.basename(rel));
|
|
195
|
+
|
|
196
|
+
// What git tracks is the truth. Build output on disk isn't part of the project.
|
|
197
|
+
let tracked: string[] | undefined;
|
|
198
|
+
try {
|
|
199
|
+
tracked = execFileSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
|
|
200
|
+
} catch {
|
|
201
|
+
// not a git repo, so the folders are all there is
|
|
202
|
+
}
|
|
184
203
|
|
|
185
204
|
let budget = MAX_ENTRIES;
|
|
186
205
|
const countFiles = (dir: string, depth: number): number => {
|
|
@@ -196,7 +215,7 @@ export function scanProject(root: string): ProjectScan {
|
|
|
196
215
|
if (--budget <= 0) break;
|
|
197
216
|
if (entry.isDirectory()) {
|
|
198
217
|
const child = path.join(dir, entry.name);
|
|
199
|
-
if (!ignored.has(entry.name) && !isOwnRepo(child)) files += countFiles(child, depth + 1);
|
|
218
|
+
if (!ignored.has(entry.name) && !entry.name.startsWith(".") && !isOwnRepo(child)) files += countFiles(child, depth + 1);
|
|
200
219
|
} else if (entry.isFile()) {
|
|
201
220
|
files += 1;
|
|
202
221
|
}
|
|
@@ -214,22 +233,60 @@ export function scanProject(root: string): ProjectScan {
|
|
|
214
233
|
}
|
|
215
234
|
};
|
|
216
235
|
|
|
236
|
+
const markers = [...(options.replaceDefaults ? [] : DEFAULT_APP_MARKERS), ...(options.markers ?? [])].map(markerMatcher);
|
|
237
|
+
const isApp = (dir: string): boolean => {
|
|
238
|
+
try {
|
|
239
|
+
return readdirSync(dir, { withFileTypes: true }).some((entry) => entry.isFile() && markers.some((marker) => marker.test(entry.name)));
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const below = (rel: string): string[] => subdirs(path.join(root, rel)).map((name) => (rel ? `${rel}/${name}` : name)).filter((child) => !isSkipped(child));
|
|
245
|
+
|
|
246
|
+
// An application is a building. A folder holding applications is a street. Everything else doesn't count.
|
|
247
|
+
const apps: string[] = [];
|
|
248
|
+
const findApps = (rel: string, depth: number): void => {
|
|
249
|
+
for (const child of below(rel)) {
|
|
250
|
+
if (isApp(path.join(root, child))) apps.push(child);
|
|
251
|
+
else if (depth < MAX_APP_DEPTH) findApps(child, depth + 1);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
findApps("", 1);
|
|
255
|
+
const included = (options.include ?? []).map(clean).filter((rel) => !apps.includes(rel) && existsSync(path.join(root, rel)));
|
|
256
|
+
|
|
217
257
|
const containers = new Set([...CONTAINERS, ...workspaceFolders(root)]);
|
|
218
258
|
const areas: ProjectArea[] = [];
|
|
219
259
|
let remaining = MAX_ENTRIES;
|
|
220
|
-
const add = (rel: string): void => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
260
|
+
const add = (rel: string, street?: string): void => {
|
|
261
|
+
let files = tracked ? tracked.filter((file) => file.startsWith(`${rel}/`)).length : 0;
|
|
262
|
+
if (files === 0) {
|
|
263
|
+
// New and not committed yet.
|
|
264
|
+
budget = Math.min(MAX_ENTRIES_PER_AREA, remaining);
|
|
265
|
+
const before = budget;
|
|
266
|
+
files = countFiles(path.join(root, rel), 0);
|
|
267
|
+
remaining -= before - Math.max(budget, 0);
|
|
268
|
+
}
|
|
269
|
+
if (files > 0) areas.push({ id: slug(rel), name: titled(path.basename(rel)), path: `${rel}/`, files, ...(street === undefined ? {} : { street }) });
|
|
226
270
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
271
|
+
const readsAsOneApp = apps.length + included.length <= 1 && (isApp(root) || apps.length === 0);
|
|
272
|
+
const insideOneApp = apps.length === 1 && included.length === 0 && !isApp(root);
|
|
273
|
+
if (readsAsOneApp || insideOneApp) {
|
|
274
|
+
// One application: its own folders are the rooms.
|
|
275
|
+
const base = insideOneApp ? (apps[0] as string) : "";
|
|
276
|
+
for (const rel of below(base)) {
|
|
277
|
+
const name = path.posix.basename(rel);
|
|
278
|
+
const children = containers.has(name) ? below(rel) : [];
|
|
279
|
+
// One package in packages/ is still a package, but one folder in src/ is just a folder.
|
|
280
|
+
const open = children.length >= 2 || children.some((child) => readPackage(path.join(root, child)));
|
|
281
|
+
if (open) for (const child of children) add(child);
|
|
282
|
+
else add(rel);
|
|
283
|
+
}
|
|
284
|
+
for (const rel of included) add(rel);
|
|
285
|
+
} else {
|
|
286
|
+
for (const rel of [...apps, ...included]) {
|
|
287
|
+
const parent = path.posix.dirname(rel);
|
|
288
|
+
add(rel, parent === "." ? "" : `${parent}/`);
|
|
289
|
+
}
|
|
233
290
|
}
|
|
234
291
|
let rootFiles = 0;
|
|
235
292
|
try {
|
|
@@ -261,7 +318,7 @@ export function scanProject(root: string): ProjectScan {
|
|
|
261
318
|
// --- What a folder looks like ---------------------------------------------------------------------------
|
|
262
319
|
|
|
263
320
|
/** The look comes from a folder's own name, so everything in apps/ doesn't look like a front end. */
|
|
264
|
-
function themeFor(folder: string, plan: WorldPlan, random: () => number): Theme {
|
|
321
|
+
export function themeFor(folder: string, plan: WorldPlan, random: () => number): Theme {
|
|
265
322
|
const own = folder.replace(/\/$/, "").split("/").pop() ?? folder;
|
|
266
323
|
const found = plan.themes.find(({ match }) => match.test(own));
|
|
267
324
|
if (found) return found.theme;
|
|
@@ -279,7 +336,7 @@ function targetSize(id: string, files: number, seed: string, compact: false | nu
|
|
|
279
336
|
};
|
|
280
337
|
}
|
|
281
338
|
|
|
282
|
-
const deskCount = (files: number): number => Math.min(9, Math.max(1, Math.round(Math.log2(files + 1) - 1)));
|
|
339
|
+
export const deskCount = (files: number): number => Math.min(9, Math.max(1, Math.round(Math.log2(files + 1) - 1)));
|
|
283
340
|
|
|
284
341
|
// --- Geometry -------------------------------------------------------------------------------------------
|
|
285
342
|
|
|
@@ -401,7 +458,12 @@ function placeInHalls(
|
|
|
401
458
|
related: MapRoom[],
|
|
402
459
|
family: MapRoom[],
|
|
403
460
|
random: () => number,
|
|
461
|
+
street?: string,
|
|
404
462
|
): { box: Box; door?: MapDoor; replaces?: MapRoom } | undefined {
|
|
463
|
+
const streetOf = (room: MapRoom): string => {
|
|
464
|
+
const parent = path.posix.dirname((room.paths[0] ?? "").replace(/\/$/, ""));
|
|
465
|
+
return parent === "." ? "" : `${parent}/`;
|
|
466
|
+
};
|
|
405
467
|
const beside = (x: number, w: number, y: number, room: MapRoom): boolean => room.y === y && (room.x + room.w + 1 === x || x + w + 1 === room.x);
|
|
406
468
|
const empty = map.rooms
|
|
407
469
|
.filter((room) => room.role === "vacant" && !room.locked && room.w >= MIN_ROOM_W)
|
|
@@ -414,7 +476,9 @@ function placeInHalls(
|
|
|
414
476
|
const open = hallRows(map).filter((row) => row.end + w <= row.limit);
|
|
415
477
|
const score = (row: HallRow): number => row.end
|
|
416
478
|
- (related.some((other) => other.y === row.y) ? 20 : 0)
|
|
417
|
-
- (family.some((other) => other.y === row.y) ?
|
|
479
|
+
- (family.some((other) => other.y === row.y) ? 30 : 0)
|
|
480
|
+
// A new street starts its own row, where it can.
|
|
481
|
+
+ (street !== undefined && row.rooms.some((other) => !other.role && streetOf(other) !== street) ? 25 : 0)
|
|
418
482
|
+ random() * 3;
|
|
419
483
|
const row = open.map((candidate) => ({ candidate, value: score(candidate) })).sort((a, b) => a.value - b.value)[0]?.candidate;
|
|
420
484
|
if (row) return { box: { x: row.end, y: row.y, w, h: row.h }, door: doorInto(row.end, w, row.doorY, random) };
|
|
@@ -433,7 +497,7 @@ function placeInHalls(
|
|
|
433
497
|
map.rooms.push({ id: "stairs", name: "", x: landing, y: top.y + HALL_H + 1, w: HALL_H, h: y - 1 - (top.y + HALL_H + 1), floor: "hall", paths: [], role: "hall" });
|
|
434
498
|
map.rooms.push({ id: "hall-1", name: "", x: top.x, y, w: landing + HALL_H - top.x, h: HALL_H, floor: "hall", paths: [], role: "hall" });
|
|
435
499
|
map.doors.push({ x: landing, y: top.y + HALL_H }, { x: landing, y: y - 1 });
|
|
436
|
-
return placeInHalls(map, id, width, related, family, random);
|
|
500
|
+
return placeInHalls(map, id, width, related, family, random, street);
|
|
437
501
|
}
|
|
438
502
|
|
|
439
503
|
/** Corridors reach the end of their rows, and rows fill out to a straight edge with empty rooms. */
|
|
@@ -497,7 +561,7 @@ function reachable(rooms: Box[], doors: MapDoor[], from: Box): Set<Box> {
|
|
|
497
561
|
|
|
498
562
|
// --- Furnishing ------------------------------------------------------------------------------------------
|
|
499
563
|
|
|
500
|
-
function furnish(room: MapRoom, doors: MapDoor[], seed: string, plan: WorldPlan): MapItem[] {
|
|
564
|
+
export function furnish(room: MapRoom, doors: MapDoor[], seed: string, plan: WorldPlan): MapItem[] {
|
|
501
565
|
const random = rng(`${seed}:furnish:${room.id}:${room.w}x${room.h}`);
|
|
502
566
|
const blocked = new Set<string>();
|
|
503
567
|
const reserved = new Set<string>();
|
|
@@ -716,7 +780,7 @@ export function evolveMap(previous: OfficeMap | undefined, scan: ProjectScan, se
|
|
|
716
780
|
const prefer = related.length > 0 ? related : family;
|
|
717
781
|
const pull = related.length > 0 ? 40 : 8;
|
|
718
782
|
if (map.layout === "halls") {
|
|
719
|
-
const spot = placeInHalls(map, area.id, wantedSize.w, related, family, rng(`${map.seed}:place:${area.id}`));
|
|
783
|
+
const spot = placeInHalls(map, area.id, wantedSize.w, related, family, rng(`${map.seed}:place:${area.id}`), area.street);
|
|
720
784
|
if (!spot) continue; // the building is full; this folder's agents use the lobby
|
|
721
785
|
if (spot.replaces) {
|
|
722
786
|
const old = spot.replaces.id;
|
|
@@ -749,7 +813,22 @@ export function evolveMap(previous: OfficeMap | undefined, scan: ProjectScan, se
|
|
|
749
813
|
if (previous) changes.push(`New room: ${area.name}`);
|
|
750
814
|
}
|
|
751
815
|
|
|
752
|
-
if (map.layout === "halls")
|
|
816
|
+
if (map.layout === "halls") {
|
|
817
|
+
squareUp(map, map.seed, touched);
|
|
818
|
+
// A corridor is named for the literal paths along it.
|
|
819
|
+
const streetFor = new Map(scan.areas.map((area) => [area.id, area.street]));
|
|
820
|
+
for (const hall of corridors(map)) {
|
|
821
|
+
const streets = new Set<string>();
|
|
822
|
+
for (const row of hallRows(map).filter((candidate) => candidate.hall === hall)) {
|
|
823
|
+
for (const room of row.rooms) {
|
|
824
|
+
const street = streetFor.get(room.id);
|
|
825
|
+
if (street) streets.add(street);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
hall.paths = [...streets];
|
|
829
|
+
hall.name = hall.paths.join(" + ").slice(0, 24);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
753
832
|
|
|
754
833
|
// A new door or a bigger room can change what fits, so those rooms are furnished again.
|
|
755
834
|
for (const id of touched) {
|
package/src/plan.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface WorldPlan {
|
|
|
24
24
|
fallback: Theme & { floors: string[] };
|
|
25
25
|
/** For the biggest rooms. */
|
|
26
26
|
filler: [string, string];
|
|
27
|
+
/** What draws each kind of part in a building. Anything else uses the workstation. */
|
|
28
|
+
partItems?: Record<string, string>;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/** The office, and the world used when a map doesn't name one. */
|
|
@@ -59,6 +61,7 @@ export const OFFICE_PLAN: WorldPlan = {
|
|
|
59
61
|
],
|
|
60
62
|
fallback: { floor: "", floors: ["wood", "carpet", "tile", "stone", "concrete"], items: ["plant", "shelf", "lamp"], decor: ["poster"] },
|
|
61
63
|
filler: ["plant", "fern"],
|
|
64
|
+
partItems: { Desk: "desk", "Server rack": "server", Workbench: "table", Whiteboard: "whiteboard", Bookshelf: "shelf", Toolbox: "printer", Plants: "plant", "Filing cabinet": "shelf" },
|
|
62
65
|
};
|
|
63
66
|
|
|
64
67
|
const plans = new Map<string, WorldPlan>([[OFFICE_PLAN.id, OFFICE_PLAN]]);
|