genesis-compiler 1.2.2 → 1.2.3
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/README.md +10 -4
- package/docs/assurance-model.md +15 -0
- package/docs/stack-components.md +44 -0
- package/package.json +2 -2
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/src/cli.js +24 -1
- package/src/index/agent-skills.js +25 -4
- package/src/index/city-presentation.js +83 -0
- package/src/index/code-index.js +24 -3
- package/src/index/init.js +1 -1
- package/src/index/stack-catalog.js +2 -0
- package/src/index/stack-city-presentation.js +178 -0
- package/src/index/stack-piece.js +7 -0
- package/src/index/stack.js +8 -0
- package/src/index/utils.js +1 -1
- package/src/index/workspace-setup.js +76 -0
- package/src/index.js +8 -1
- package/stacks/pieces/jskit.md +11 -0
package/README.md
CHANGED
|
@@ -116,8 +116,13 @@ Genesis never overwrites an unmanaged skill and preserves locally modified
|
|
|
116
116
|
managed skills. Run `genesis init` after manually editing `genesis/stack.md`,
|
|
117
117
|
or use `genesis stack add`, to synchronize selected skills.
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
Run `genesis prepare` to execute the selected Stack's exact workspace recipe
|
|
120
|
+
directly with the caller's environment and installed toolchain. It executes
|
|
121
|
+
argument arrays without a shell, honors declared project-relative working
|
|
122
|
+
directories and conditional paths, stops on the first failure, and never
|
|
123
|
+
returns environment values. Hosts that need their own runtime, permission, or
|
|
124
|
+
process policy can instead call the public `inspectWorkspaceSetup()` API and
|
|
125
|
+
execute the same normalized recipe themselves. A concrete Stack piece may declare one exact,
|
|
121
126
|
ordered `## Workspace setup` recipe with labels, argument arrays expressed as
|
|
122
127
|
backticked tokens, abstract runtimes, and optional project-relative working
|
|
123
128
|
directories and readiness paths. A recipe whose declared project marker does
|
|
@@ -127,8 +132,9 @@ piece waits for `package.json` and then declares `npm install`; generic
|
|
|
127
132
|
`genesis/stack.md` replaces all component defaults, including with
|
|
128
133
|
`- Nothing.`. Without that override, exactly one component recipe is used;
|
|
129
134
|
multiple component recipes are reported as ambiguous and never merged.
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
The standalone command does not provision declared resources: callers provide
|
|
136
|
+
their own database, credentials, compilers, and other infrastructure. Stack
|
|
137
|
+
resources such as database variables do not gate dependency installation.
|
|
132
138
|
|
|
133
139
|
Hosts that manage project environments can call `inspectEnvironment()`.
|
|
134
140
|
Concrete Stack integrations may declare public, non-secret Environment
|
package/docs/assurance-model.md
CHANGED
|
@@ -24,3 +24,18 @@ semantically current, or that passing checks prove the whole product correct.
|
|
|
24
24
|
|
|
25
25
|
Git remains the review and recovery boundary. The host running the generated
|
|
26
26
|
prompt owns the agent and its interaction.
|
|
27
|
+
|
|
28
|
+
## Filesystem ownership
|
|
29
|
+
|
|
30
|
+
Genesis does not copy package-file ownership or broad source modes into a
|
|
31
|
+
project. It creates ordinary project files with mode `0666`, directories and
|
|
32
|
+
executable skill files with mode `0777`, and lets the invoking process umask
|
|
33
|
+
and the destination directory's inherited ACL establish the effective mode and
|
|
34
|
+
group. Atomic replacements are created beside their destination so they inherit
|
|
35
|
+
the same filesystem policy before becoming visible. Private hook continuation
|
|
36
|
+
state remains explicitly `0600`.
|
|
37
|
+
|
|
38
|
+
This keeps ordinary single-user projects conventional while allowing a managed
|
|
39
|
+
host to require shared-group writes and no access for other users. The host is
|
|
40
|
+
responsible for establishing and verifying that umask, setgid directory, ACL,
|
|
41
|
+
and cross-identity contract before Genesis or an agent starts.
|
package/docs/stack-components.md
CHANGED
|
@@ -24,6 +24,17 @@ What this component contributes.
|
|
|
24
24
|
|
|
25
25
|
- `javascript`
|
|
26
26
|
|
|
27
|
+
## City regions
|
|
28
|
+
|
|
29
|
+
- Ignore `**/test/**`
|
|
30
|
+
- Ignore `**/tests/**`
|
|
31
|
+
- Ignore `**/__tests__/**`
|
|
32
|
+
- Ignore `**/*.test.*`
|
|
33
|
+
- Ignore `**/*.spec.*`
|
|
34
|
+
- Match `packages` as `Packages`: `packages/**`
|
|
35
|
+
- Match `source` as `Source`: `src/**`
|
|
36
|
+
- Fallback `everything-else` as `Everything else`
|
|
37
|
+
|
|
27
38
|
## Resources
|
|
28
39
|
|
|
29
40
|
```json genesis-resource
|
|
@@ -239,6 +250,25 @@ may contribute indexers, and duplicates are collapsed. Index output is
|
|
|
239
250
|
navigation data, not proof that every dynamic callable or framework edge was
|
|
240
251
|
discovered.
|
|
241
252
|
|
|
253
|
+
`City regions` is an optional complete presentation contract for Machine City.
|
|
254
|
+
Leading `Ignore` entries are canonical project-relative glob patterns using
|
|
255
|
+
only `*` within a path segment and `**` across segments. Genesis excludes their
|
|
256
|
+
matching files before it emits Machine City buildings or functions. This keeps
|
|
257
|
+
test/spec source out of the product City without requiring a host-side filter.
|
|
258
|
+
Each subsequent ordered `Match` entry declares one canonical project-relative
|
|
259
|
+
`path/**` prefix, stable id, and human title. Exactly one final `Fallback` entry
|
|
260
|
+
owns every remaining indexed path. Ignore patterns, ids, and prefixes must be
|
|
261
|
+
unique, prefixes must not overlap, and no more than one selected component may
|
|
262
|
+
declare the contract. Genesis resolves every indexed file and structural
|
|
263
|
+
district to a region and a campus while generating
|
|
264
|
+
`.genesis/machine-city.json`; a City renderer consumes those emitted facts and
|
|
265
|
+
must not reclassify or filter paths. Under a matched prefix, the first directory
|
|
266
|
+
is a campus, while files directly below the prefix belong to the prefix campus.
|
|
267
|
+
Under the fallback, each top-level directory is a campus and project-root files
|
|
268
|
+
belong to the project-root campus. The built-in JSKIT piece excludes exact
|
|
269
|
+
`test`, `tests`, and `__tests__` path segments plus `*.test.*` and `*.spec.*`
|
|
270
|
+
files, then declares `Packages`, `Source`, and `Everything else`.
|
|
271
|
+
|
|
242
272
|
Do not create a Genesis-owned generic skill merely because a component is
|
|
243
273
|
named `nodejs`, `php`, or another common technology. Official, user, and host
|
|
244
274
|
skills with those names must coexist normally. Use `## Skill` only for an
|
|
@@ -337,6 +367,20 @@ blocked result.
|
|
|
337
367
|
The optional environment object is accepted for a consistent host inspection
|
|
338
368
|
boundary, but Workspace setup v1 neither reads nor returns its values.
|
|
339
369
|
|
|
370
|
+
Standalone users may execute the same normalized recipe with their own
|
|
371
|
+
environment and installed toolchain:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
genesis prepare
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`genesis prepare` runs each declared argv directly without a shell, in order,
|
|
378
|
+
and stops at the first failure. It honors project-relative workdirs and
|
|
379
|
+
conditional paths. It does not provision databases, compilers, credentials,
|
|
380
|
+
runtimes, or other resources. Managed hosts remain free to consume
|
|
381
|
+
`inspectWorkspaceSetup()` and enforce their own execution, identity, runtime,
|
|
382
|
+
and permission policies.
|
|
383
|
+
|
|
340
384
|
Preview hosts read the normalized Launch contract independently:
|
|
341
385
|
|
|
342
386
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genesis-compiler",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with optional Codex hooks.",
|
|
6
6
|
"repository": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@ast-grep/lang-ruby": "^0.0.7",
|
|
57
57
|
"@ast-grep/lang-rust": "^0.0.7",
|
|
58
58
|
"@ast-grep/napi": "^0.45.1",
|
|
59
|
-
"@jskit-ai/agent-docs": "0.1.
|
|
59
|
+
"@jskit-ai/agent-docs": "0.1.132",
|
|
60
60
|
"yaml": "^2.9.0"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
initialize,
|
|
12
12
|
installCodex,
|
|
13
13
|
listStackPieces,
|
|
14
|
+
prepareWorkspace,
|
|
14
15
|
verify,
|
|
15
16
|
} from './index.js';
|
|
16
17
|
import {
|
|
@@ -30,6 +31,7 @@ const USAGE = `Usage:
|
|
|
30
31
|
genesis stack add <piece...>
|
|
31
32
|
genesis context <path...>
|
|
32
33
|
genesis index [function-or-path...]
|
|
34
|
+
genesis prepare
|
|
33
35
|
genesis prompt [request...]
|
|
34
36
|
genesis prompt --task <start|work|deslop|program|blueprint|describe|review> [request...]
|
|
35
37
|
genesis verify
|
|
@@ -46,7 +48,7 @@ prompt to the agent you already use. Review all edits through the ordinary Git
|
|
|
46
48
|
diff, then run genesis verify for the Stack's concrete checks.
|
|
47
49
|
`;
|
|
48
50
|
|
|
49
|
-
const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'hook', 'index', 'init', 'prompt', 'stack', 'verify']);
|
|
51
|
+
const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'hook', 'index', 'init', 'prepare', 'prompt', 'stack', 'verify']);
|
|
50
52
|
|
|
51
53
|
function parseCommand(argv) {
|
|
52
54
|
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
|
|
@@ -165,6 +167,19 @@ function writeResult(command, result) {
|
|
|
165
167
|
line(process.stdout, `index: ${result.status}`);
|
|
166
168
|
return;
|
|
167
169
|
}
|
|
170
|
+
if (command === 'prepare') {
|
|
171
|
+
if (result.summary) line(process.stdout, result.summary);
|
|
172
|
+
namedItems('Commands', result.commands?.map(({ label, argv }) => `${label}: ${argv.join(' ')}`));
|
|
173
|
+
for (const item of result.diagnostics || []) {
|
|
174
|
+
line(process.stdout, `${item.code}: ${item.message}`);
|
|
175
|
+
for (const field of ['stdout', 'stderr']) {
|
|
176
|
+
const output = item.details?.[field]?.trim();
|
|
177
|
+
if (output) line(process.stdout, output);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
line(process.stdout, `prepare: ${result.status}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
168
183
|
if (command === 'hook') {
|
|
169
184
|
if (['discover', 'session'].includes(result.kind) && result.output) line(process.stdout, result.output);
|
|
170
185
|
else if (result.kind === 'stop') line(process.stdout, JSON.stringify(result.output));
|
|
@@ -226,6 +241,14 @@ async function execute({ command, operands, options }) {
|
|
|
226
241
|
}
|
|
227
242
|
if (command === 'context') return getContext({ paths: operands, projectRoot });
|
|
228
243
|
if (command === 'index') return indexCodebase({ projectRoot, queries: operands });
|
|
244
|
+
if (command === 'prepare') {
|
|
245
|
+
return prepareWorkspace({
|
|
246
|
+
projectRoot,
|
|
247
|
+
onEvent: (event) => {
|
|
248
|
+
if (!options.json) line(process.stderr, event.message);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
229
252
|
if (command === 'hook') {
|
|
230
253
|
if (operands[0] === 'discover') {
|
|
231
254
|
return { kind: 'discover', ...await codexAdoptionRecommendation({ projectRoot }) };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import {
|
|
4
|
-
cp,
|
|
5
4
|
lstat,
|
|
6
5
|
mkdir,
|
|
7
6
|
readFile,
|
|
8
7
|
readdir,
|
|
9
8
|
rename,
|
|
10
9
|
rm,
|
|
10
|
+
writeFile,
|
|
11
11
|
} from 'node:fs/promises';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -92,7 +92,7 @@ async function skillTree(directory, relative = '') {
|
|
|
92
92
|
files.push({
|
|
93
93
|
path: child,
|
|
94
94
|
hash: sha256(content),
|
|
95
|
-
mode: (info.mode &
|
|
95
|
+
mode: (info.mode & 0o100) === 0 ? 0o100644 : 0o100755,
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
return files;
|
|
@@ -251,7 +251,7 @@ async function replaceSkill(source, target) {
|
|
|
251
251
|
await mkdir(path.dirname(target), { recursive: true });
|
|
252
252
|
let replaced = false;
|
|
253
253
|
try {
|
|
254
|
-
await
|
|
254
|
+
await copySkillTree(source, temporary);
|
|
255
255
|
try {
|
|
256
256
|
await rename(target, backup);
|
|
257
257
|
replaced = true;
|
|
@@ -271,6 +271,23 @@ async function replaceSkill(source, target) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
async function copySkillTree(source, target) {
|
|
275
|
+
const info = await lstat(source);
|
|
276
|
+
if (info.isSymbolicLink() || (!info.isDirectory() && !info.isFile())) {
|
|
277
|
+
invalidSkill(`Agent Skill contains a non-ordinary entry: ${source}.`);
|
|
278
|
+
}
|
|
279
|
+
if (info.isDirectory()) {
|
|
280
|
+
await mkdir(target, { mode: 0o777 });
|
|
281
|
+
const entries = await readdir(source, { withFileTypes: true });
|
|
282
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
283
|
+
await copySkillTree(path.join(source, entry.name), path.join(target, entry.name));
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const mode = (info.mode & 0o100) === 0 ? 0o666 : 0o777;
|
|
288
|
+
await writeFile(target, await readFile(source), { flag: 'wx', mode });
|
|
289
|
+
}
|
|
290
|
+
|
|
274
291
|
function targetFiles(skill) {
|
|
275
292
|
return skill.files.map(({ path: file }) => `${SKILLS_ROOT}/${skill.name}/${file}`);
|
|
276
293
|
}
|
|
@@ -304,6 +321,10 @@ export async function syncProjectSkills({ projectRoot, stack } = {}) {
|
|
|
304
321
|
continue;
|
|
305
322
|
}
|
|
306
323
|
if (installed.hash !== managed.hash) {
|
|
324
|
+
if (installed.hash === skill.hash) {
|
|
325
|
+
manifest.value.skills[skill.name] = { hash: skill.hash, source: skill.source };
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
307
328
|
diagnostics.push({
|
|
308
329
|
code: 'AGENT_SKILL_CUSTOMIZED',
|
|
309
330
|
message: `Preserved locally modified Agent Skill ${skill.name}.`,
|
|
@@ -388,7 +409,7 @@ export async function inspectProjectSkills({ projectRoot, stack } = {}) {
|
|
|
388
409
|
message: `Project Agent Skill ${expected.name} is externally managed; Genesis preserved it.`,
|
|
389
410
|
details: { name: expected.name, path: `${SKILLS_ROOT}/${expected.name}/SKILL.md` },
|
|
390
411
|
});
|
|
391
|
-
} else if (installed.hash !== managed.hash) {
|
|
412
|
+
} else if (installed.hash !== managed.hash && installed.hash !== expected.hash) {
|
|
392
413
|
customized = true;
|
|
393
414
|
diagnostics.push({
|
|
394
415
|
code: 'AGENT_SKILL_CUSTOMIZED',
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
function presentationRegionForPath(filePath, regions) {
|
|
4
|
+
return regions.find((region) => (
|
|
5
|
+
!region.fallback
|
|
6
|
+
&& (filePath === region.pathPrefix || filePath.startsWith(`${region.pathPrefix}/`))
|
|
7
|
+
)) || regions.find(({ fallback }) => fallback) || null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parentDirectory(filePath) {
|
|
11
|
+
const parent = path.posix.dirname(filePath);
|
|
12
|
+
return parent === '.' ? '' : parent;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function presentationCampusPath(filePath, region, { file = false } = {}) {
|
|
16
|
+
const ownershipPath = file ? parentDirectory(filePath) : filePath;
|
|
17
|
+
if (!region || region.fallback) {
|
|
18
|
+
return ownershipPath.split('/').filter(Boolean)[0] || '';
|
|
19
|
+
}
|
|
20
|
+
if (ownershipPath === region.pathPrefix) return region.pathPrefix;
|
|
21
|
+
const relative = ownershipPath.slice(region.pathPrefix.length + 1);
|
|
22
|
+
const child = relative.split('/').filter(Boolean)[0];
|
|
23
|
+
return child ? `${region.pathPrefix}/${child}` : region.pathPrefix;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function campusId(regionId, campusPath) {
|
|
27
|
+
return `presentation-campus:${regionId}:${campusPath || '.'}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolves Stack-declared City regions and campus roots into Machine City facts. */
|
|
31
|
+
export function buildCityPresentation({ districts = [], files = [], regions = [] } = {}) {
|
|
32
|
+
if (regions.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
campuses: [],
|
|
35
|
+
placementForPath: () => ({ presentationCampusId: null, presentationRegionId: null }),
|
|
36
|
+
regions: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const districtsByPath = new Map(districts.map((district) => [district.path, district]));
|
|
40
|
+
const placements = new Map();
|
|
41
|
+
const mutableCampuses = new Map();
|
|
42
|
+
const mutableRegions = new Map(regions.map((region) => [region.id, {
|
|
43
|
+
...region,
|
|
44
|
+
buildingCount: 0,
|
|
45
|
+
}]));
|
|
46
|
+
|
|
47
|
+
function resolvePlacement(filePath = '', { file = false } = {}) {
|
|
48
|
+
if (placements.has(filePath)) return placements.get(filePath);
|
|
49
|
+
const region = presentationRegionForPath(filePath, regions);
|
|
50
|
+
const campusPath = presentationCampusPath(filePath, region, { file });
|
|
51
|
+
const placement = {
|
|
52
|
+
presentationCampusId: campusId(region.id, campusPath),
|
|
53
|
+
presentationRegionId: region.id,
|
|
54
|
+
};
|
|
55
|
+
placements.set(filePath, placement);
|
|
56
|
+
if (!mutableCampuses.has(placement.presentationCampusId)) {
|
|
57
|
+
mutableCampuses.set(placement.presentationCampusId, {
|
|
58
|
+
id: placement.presentationCampusId,
|
|
59
|
+
regionId: region.id,
|
|
60
|
+
path: campusPath,
|
|
61
|
+
title: campusPath ? path.posix.basename(campusPath) : 'Project root',
|
|
62
|
+
districtId: districtsByPath.get(campusPath)?.id || null,
|
|
63
|
+
buildingCount: 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return placement;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const placement = resolvePlacement(file.path, { file: true });
|
|
71
|
+
mutableRegions.get(placement.presentationRegionId).buildingCount += 1;
|
|
72
|
+
mutableCampuses.get(placement.presentationCampusId).buildingCount += 1;
|
|
73
|
+
}
|
|
74
|
+
for (const district of districts) resolvePlacement(district.path);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
campuses: [...mutableCampuses.values()]
|
|
78
|
+
.filter(({ buildingCount }) => buildingCount > 0)
|
|
79
|
+
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
80
|
+
placementForPath: (filePath = '') => placements.get(filePath) || resolvePlacement(filePath),
|
|
81
|
+
regions: [...mutableRegions.values()],
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/index/code-index.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { astGrepCodeIndexers } from './code-indexers/ast-grep.js';
|
|
5
5
|
import { asDiagnostic } from './errors.js';
|
|
6
|
+
import { buildCityPresentation } from './city-presentation.js';
|
|
7
|
+
import { cityPathExcluded } from './stack-city-presentation.js';
|
|
6
8
|
import { gitContext } from './git.js';
|
|
7
9
|
import { isProjectContentPath } from './paths.js';
|
|
8
10
|
import { inspectProgram } from './program.js';
|
|
@@ -128,8 +130,14 @@ function mergeIndexedFiles(contributions) {
|
|
|
128
130
|
};
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
function machineCity({ components, contributions, diagnostics, indexers }) {
|
|
133
|
+
function machineCity({ cityRegions, components, contributions, diagnostics, indexers }) {
|
|
132
134
|
const merged = mergeIndexedFiles(contributions);
|
|
135
|
+
const structuralDistricts = directoryRecords(merged.files.map(({ path: filePath }) => filePath));
|
|
136
|
+
const presentation = buildCityPresentation({
|
|
137
|
+
districts: structuralDistricts,
|
|
138
|
+
files: merged.files,
|
|
139
|
+
regions: cityRegions,
|
|
140
|
+
});
|
|
133
141
|
const visibility = new Map(merged.functions.map((entry) => [entry.id, entry.visibility]));
|
|
134
142
|
const codeHash = sha256(stableJson(merged.files.map(({ path: filePath, hash, mode }) => ({
|
|
135
143
|
path: filePath,
|
|
@@ -141,13 +149,19 @@ function machineCity({ components, contributions, diagnostics, indexers }) {
|
|
|
141
149
|
schemaVersion: 1,
|
|
142
150
|
status: indexers.length === 0 ? 'unconfigured' : diagnostics.length > 0 ? 'completed-with-warning' : 'current',
|
|
143
151
|
codeHash,
|
|
152
|
+
presentationRegions: presentation.regions,
|
|
153
|
+
presentationCampuses: presentation.campuses,
|
|
144
154
|
stackComponents: components,
|
|
145
155
|
indexers,
|
|
146
156
|
diagnostics,
|
|
147
157
|
functions: merged.functions,
|
|
148
|
-
districts:
|
|
158
|
+
districts: structuralDistricts.map((district) => ({
|
|
159
|
+
...district,
|
|
160
|
+
...presentation.placementForPath(district.path),
|
|
161
|
+
})),
|
|
149
162
|
buildings: merged.files.map((file) => ({
|
|
150
163
|
...file,
|
|
164
|
+
...presentation.placementForPath(file.path),
|
|
151
165
|
districtId: directoryId(parentDirectory(file.path)),
|
|
152
166
|
title: path.posix.basename(file.path),
|
|
153
167
|
publicFunctionCount: file.functionIds.filter((id) => visibility.get(id) === 'public').length,
|
|
@@ -229,7 +243,13 @@ export async function buildProjectIndex({ projectRoot, queries = [], write = tru
|
|
|
229
243
|
const stack = await readStack(root);
|
|
230
244
|
const states = await gitVisibleFileStates(root, { includePath: isProjectContentPath });
|
|
231
245
|
const files = [...states]
|
|
232
|
-
.filter(([, state]) =>
|
|
246
|
+
.filter(([filePath, state]) => (
|
|
247
|
+
state.exists
|
|
248
|
+
&& !state.symlink
|
|
249
|
+
&& !state.special
|
|
250
|
+
&& state.hash
|
|
251
|
+
&& !cityPathExcluded(filePath, stack.cityExclusions)
|
|
252
|
+
))
|
|
233
253
|
.map(([filePath, state]) => ({ path: filePath, hash: state.hash, mode: state.mode }));
|
|
234
254
|
const indexers = [...new Set(stack.components.flatMap((component) => component.indexers || []))].sort();
|
|
235
255
|
const contributions = [];
|
|
@@ -249,6 +269,7 @@ export async function buildProjectIndex({ projectRoot, queries = [], write = tru
|
|
|
249
269
|
}
|
|
250
270
|
}
|
|
251
271
|
const machine = machineCity({
|
|
272
|
+
cityRegions: stack.cityRegions,
|
|
252
273
|
components: stack.components.map(({ id }) => id),
|
|
253
274
|
contributions,
|
|
254
275
|
diagnostics,
|
package/src/index/init.js
CHANGED
|
@@ -12,7 +12,7 @@ async function createIfMissing(projectRoot, relativePath, source) {
|
|
|
12
12
|
const location = path.join(projectRoot, relativePath);
|
|
13
13
|
await mkdir(path.dirname(location), { recursive: true });
|
|
14
14
|
try {
|
|
15
|
-
await writeFile(location, source, { flag: 'wx', mode:
|
|
15
|
+
await writeFile(location, source, { flag: 'wx', mode: 0o666 });
|
|
16
16
|
return relativePath;
|
|
17
17
|
} catch (error) {
|
|
18
18
|
if (error?.code === 'EEXIST') return null;
|
|
@@ -63,6 +63,8 @@ export async function listBuiltinStackPieces() {
|
|
|
63
63
|
guidance: piece.guidance,
|
|
64
64
|
requires: piece.requires,
|
|
65
65
|
conflicts: piece.conflicts,
|
|
66
|
+
cityExclusions: piece.cityExclusions,
|
|
67
|
+
cityRegions: piece.cityRegions,
|
|
66
68
|
indexers: piece.indexers,
|
|
67
69
|
environmentDefaults: piece.environmentDefaults,
|
|
68
70
|
launchTargets: piece.launchTargets.map(({ id }) => id),
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
4
|
+
const IGNORE_LINE = /^- Ignore `([^`]+)`[ \t]*$/u;
|
|
5
|
+
const MATCH_LINE = /^- Match `([^`]+)` as `([^`]+)`: `([^`]+)\/\*\*`[ \t]*$/u;
|
|
6
|
+
const FALLBACK_LINE = /^- Fallback `([^`]+)` as `([^`]+)`[ \t]*$/u;
|
|
7
|
+
const REGEXP_SPECIAL = /[.+^${}()|[\]\\]/u;
|
|
8
|
+
const compiledGlobPatterns = new Map();
|
|
9
|
+
|
|
10
|
+
function invalid(piecePath, message) {
|
|
11
|
+
throw new GenesisError('STACK_PIECE_INVALID', message, { path: piecePath });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizedPrefix(value, piecePath) {
|
|
15
|
+
const prefix = String(value || '').trim();
|
|
16
|
+
if (
|
|
17
|
+
!prefix
|
|
18
|
+
|| prefix.startsWith('/')
|
|
19
|
+
|| prefix.includes('\\')
|
|
20
|
+
|| /[*?\[\]{}!]/u.test(prefix)
|
|
21
|
+
|| prefix.split('/').some((segment) => !segment || ['.', '..'].includes(segment))
|
|
22
|
+
) {
|
|
23
|
+
invalid(piecePath, '## City regions match paths must be canonical project-relative prefixes.');
|
|
24
|
+
}
|
|
25
|
+
return prefix;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizedGlob(value, piecePath) {
|
|
29
|
+
const pattern = String(value || '').trim();
|
|
30
|
+
if (
|
|
31
|
+
!pattern
|
|
32
|
+
|| pattern.startsWith('/')
|
|
33
|
+
|| pattern.includes('\\')
|
|
34
|
+
|| pattern.includes('***')
|
|
35
|
+
|| /[?\[\]{}!]/u.test(pattern)
|
|
36
|
+
|| pattern.split('/').some((segment) => !segment || ['.', '..'].includes(segment))
|
|
37
|
+
) {
|
|
38
|
+
invalid(
|
|
39
|
+
piecePath,
|
|
40
|
+
'## City regions ignore patterns must be canonical project-relative globs using only * and ** wildcards.',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return pattern;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function regionIdentity(id, title, piecePath) {
|
|
47
|
+
if (!ID_PATTERN.test(id)) {
|
|
48
|
+
invalid(piecePath, '## City regions ids must use lowercase letters, digits, and single hyphens.');
|
|
49
|
+
}
|
|
50
|
+
if (!title.trim()) invalid(piecePath, '## City regions titles must not be empty.');
|
|
51
|
+
return { id, title: title.trim() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function globExpression(pattern) {
|
|
55
|
+
if (compiledGlobPatterns.has(pattern)) return compiledGlobPatterns.get(pattern);
|
|
56
|
+
let expression = '^';
|
|
57
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
58
|
+
const character = pattern[index];
|
|
59
|
+
if (character === '*' && pattern[index + 1] === '*') {
|
|
60
|
+
if (pattern[index + 2] === '/') {
|
|
61
|
+
expression += '(?:[^/]+/)*';
|
|
62
|
+
index += 2;
|
|
63
|
+
} else {
|
|
64
|
+
expression += '.*';
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
} else if (character === '*') {
|
|
68
|
+
expression += '[^/]*';
|
|
69
|
+
} else {
|
|
70
|
+
expression += REGEXP_SPECIAL.test(character) ? `\\${character}` : character;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const compiled = new RegExp(`${expression}$`, 'u');
|
|
74
|
+
compiledGlobPatterns.set(pattern, compiled);
|
|
75
|
+
return compiled;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cityPathExcluded(filePath, exclusions = []) {
|
|
79
|
+
return exclusions.some((pattern) => globExpression(pattern).test(filePath));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A City presentation declaration is one complete, ordered contract. Ignore
|
|
84
|
+
* globs remove non-product files before Machine City is emitted. Matching
|
|
85
|
+
* prefixes are explicit Stack facts; one final fallback owns every remaining
|
|
86
|
+
* path. Genesis resolves both region and campus placement, so renderers never
|
|
87
|
+
* classify or filter repository paths.
|
|
88
|
+
*/
|
|
89
|
+
export function parseStackCityPresentationLines(lines, { path = 'stack piece' } = {}) {
|
|
90
|
+
if (lines === undefined) return { exclusions: [], regions: [] };
|
|
91
|
+
const content = lines.filter((line) => line.trim());
|
|
92
|
+
if (content.length === 0 || (content.length === 1 && content[0].trim() === '- Nothing.')) {
|
|
93
|
+
return { exclusions: [], regions: [] };
|
|
94
|
+
}
|
|
95
|
+
const exclusions = [];
|
|
96
|
+
const regions = [];
|
|
97
|
+
let regionDeclarationStarted = false;
|
|
98
|
+
content.forEach((line, index) => {
|
|
99
|
+
const ignore = line.match(IGNORE_LINE);
|
|
100
|
+
if (ignore) {
|
|
101
|
+
if (regionDeclarationStarted) {
|
|
102
|
+
invalid(path, '## City regions Ignore entries must precede Match and Fallback entries.');
|
|
103
|
+
}
|
|
104
|
+
exclusions.push(normalizedGlob(ignore[1], path));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
regionDeclarationStarted = true;
|
|
108
|
+
const match = line.match(MATCH_LINE);
|
|
109
|
+
if (match) {
|
|
110
|
+
const identity = regionIdentity(match[1], match[2], path);
|
|
111
|
+
regions.push({
|
|
112
|
+
...identity,
|
|
113
|
+
fallback: false,
|
|
114
|
+
pathPrefix: normalizedPrefix(match[3], path),
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const fallback = line.match(FALLBACK_LINE);
|
|
119
|
+
if (fallback) {
|
|
120
|
+
if (index !== content.length - 1) {
|
|
121
|
+
invalid(path, '## City regions fallback must be the final declaration.');
|
|
122
|
+
}
|
|
123
|
+
regions.push({
|
|
124
|
+
...regionIdentity(fallback[1], fallback[2], path),
|
|
125
|
+
fallback: true,
|
|
126
|
+
pathPrefix: null,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
invalid(
|
|
131
|
+
path,
|
|
132
|
+
'## City regions entries must use Ignore with a glob, Match with an id, title, and path/**, or one final Fallback.',
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
if (new Set(exclusions).size !== exclusions.length) {
|
|
136
|
+
invalid(path, '## City regions contains a duplicate ignore pattern.');
|
|
137
|
+
}
|
|
138
|
+
if (new Set(regions.map(({ id }) => id)).size !== regions.length) {
|
|
139
|
+
invalid(path, '## City regions contains a duplicate region id.');
|
|
140
|
+
}
|
|
141
|
+
const matches = regions.filter(({ fallback }) => !fallback);
|
|
142
|
+
if (new Set(matches.map(({ pathPrefix }) => pathPrefix)).size !== matches.length) {
|
|
143
|
+
invalid(path, '## City regions contains a duplicate path prefix.');
|
|
144
|
+
}
|
|
145
|
+
if (regions.filter(({ fallback }) => fallback).length !== 1) {
|
|
146
|
+
invalid(path, '## City regions requires exactly one final fallback.');
|
|
147
|
+
}
|
|
148
|
+
for (const [index, left] of matches.entries()) {
|
|
149
|
+
if (matches.slice(index + 1).some((right) => (
|
|
150
|
+
left.pathPrefix.startsWith(`${right.pathPrefix}/`)
|
|
151
|
+
|| right.pathPrefix.startsWith(`${left.pathPrefix}/`)
|
|
152
|
+
))) {
|
|
153
|
+
invalid(path, '## City regions match prefixes must not overlap.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { exclusions, regions };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function composeStackCityPresentation(components = []) {
|
|
160
|
+
const owners = components.filter((component) => (
|
|
161
|
+
component.cityRegions.length > 0 || component.cityExclusions.length > 0
|
|
162
|
+
));
|
|
163
|
+
if (owners.length === 0) return { exclusions: [], regions: [] };
|
|
164
|
+
if (owners.length > 1) {
|
|
165
|
+
throw new GenesisError(
|
|
166
|
+
'STACK_CITY_PRESENTATION_AMBIGUOUS',
|
|
167
|
+
`Stack components ${owners.map(({ id }) => id).join(', ')} declare competing City presentation contracts.`,
|
|
168
|
+
{ components: owners.map(({ id }) => id) },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
exclusions: [...owners[0].cityExclusions],
|
|
173
|
+
regions: owners[0].cityRegions.map((region) => ({
|
|
174
|
+
...region,
|
|
175
|
+
component: owners[0].id,
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
}
|
package/src/index/stack-piece.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GenesisError } from './errors.js';
|
|
2
2
|
import { parseStackCommandLines } from './stack-command.js';
|
|
3
|
+
import { parseStackCityPresentationLines } from './stack-city-presentation.js';
|
|
3
4
|
import { parseStackDeploymentLines } from './stack-deployment.js';
|
|
4
5
|
import { parseStackEnvironmentDefaultLines } from './stack-environment-defaults.js';
|
|
5
6
|
import { parseStackEnvironmentFileLines } from './stack-environment-files.js';
|
|
@@ -220,6 +221,10 @@ export function parseStackPieceSource(value, {
|
|
|
220
221
|
const deslop = textSection(all, 'Deslop');
|
|
221
222
|
if (!description) invalid(piecePath, 'Stack piece needs Description.');
|
|
222
223
|
const commands = parseStackCommandLines(all.get('Commands') || [], { path: piecePath });
|
|
224
|
+
const cityPresentation = parseStackCityPresentationLines(
|
|
225
|
+
all.has('City regions') ? all.get('City regions') : undefined,
|
|
226
|
+
{ path: piecePath },
|
|
227
|
+
);
|
|
223
228
|
const environmentDefaults = parseStackEnvironmentDefaultLines(
|
|
224
229
|
all.has('Environment defaults') ? all.get('Environment defaults') : undefined,
|
|
225
230
|
{ path: piecePath },
|
|
@@ -249,6 +254,8 @@ export function parseStackPieceSource(value, {
|
|
|
249
254
|
skill: skill(all, piecePath),
|
|
250
255
|
resources: resources(all, piecePath),
|
|
251
256
|
commands: commands.map(({ line: _line, ...command }) => command),
|
|
257
|
+
cityExclusions: cityPresentation.exclusions,
|
|
258
|
+
cityRegions: cityPresentation.regions,
|
|
252
259
|
environmentDefaults,
|
|
253
260
|
environmentFiles: environmentFiles || [],
|
|
254
261
|
launchTargets: launch?.targets || [],
|
package/src/index/stack.js
CHANGED
|
@@ -5,6 +5,7 @@ import { syncProjectSkills } from './agent-skills.js';
|
|
|
5
5
|
import { GenesisError } from './errors.js';
|
|
6
6
|
import { readBuiltinStackCatalog } from './stack-catalog.js';
|
|
7
7
|
import { parseStackCommandLines } from './stack-command.js';
|
|
8
|
+
import { composeStackCityPresentation } from './stack-city-presentation.js';
|
|
8
9
|
import { composeStackDeployment, parseStackDeploymentLines } from './stack-deployment.js';
|
|
9
10
|
import { resolveStackPieces } from './stack-composition.js';
|
|
10
11
|
import {
|
|
@@ -264,6 +265,7 @@ export async function readStack(projectRoot) {
|
|
|
264
265
|
).map(({ line: _line, ...command }) => command);
|
|
265
266
|
const componentCommands = components.flatMap((piece) => piece.commands);
|
|
266
267
|
const commands = distinctCommands(projectCommands.length > 0 ? projectCommands : componentCommands);
|
|
268
|
+
const cityPresentation = composeStackCityPresentation(components);
|
|
267
269
|
const resources = resourceDeclarations(components);
|
|
268
270
|
const environmentDefaults = composeStackEnvironmentDefaults(components);
|
|
269
271
|
const projectEnvironmentFiles = parseStackEnvironmentFileLines(
|
|
@@ -290,6 +292,8 @@ export async function readStack(projectRoot) {
|
|
|
290
292
|
path: STACK_PATH,
|
|
291
293
|
identityHash: sha256(stableJson({
|
|
292
294
|
components: components.map(({ id }) => id),
|
|
295
|
+
cityExclusions: cityPresentation.exclusions,
|
|
296
|
+
cityRegions: cityPresentation.regions,
|
|
293
297
|
commands: commands.map(({ label, argv }) => ({ label, argv })),
|
|
294
298
|
environmentDefaults,
|
|
295
299
|
environmentFiles,
|
|
@@ -299,6 +303,8 @@ export async function readStack(projectRoot) {
|
|
|
299
303
|
workspaceSetup,
|
|
300
304
|
})),
|
|
301
305
|
components,
|
|
306
|
+
cityExclusions: cityPresentation.exclusions,
|
|
307
|
+
cityRegions: cityPresentation.regions,
|
|
302
308
|
commands,
|
|
303
309
|
environmentDefaults,
|
|
304
310
|
environmentFiles,
|
|
@@ -319,6 +325,8 @@ export function stackPromptContext(stack) {
|
|
|
319
325
|
...(piece.guidance ? { guidance: piece.guidance } : {}),
|
|
320
326
|
requires: piece.requires,
|
|
321
327
|
})),
|
|
328
|
+
cityExclusions: stack.cityExclusions,
|
|
329
|
+
cityRegions: stack.cityRegions,
|
|
322
330
|
verifyCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
|
|
323
331
|
environmentDefaults: stack.environmentDefaults,
|
|
324
332
|
environmentFiles: stack.environmentFiles,
|
package/src/index/utils.js
CHANGED
|
@@ -29,7 +29,7 @@ export function uniqueSorted(values = []) {
|
|
|
29
29
|
return [...new Set(values)].sort();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export async function writeFileAtomic(filePath, source, { mode =
|
|
32
|
+
export async function writeFileAtomic(filePath, source, { mode = 0o666 } = {}) {
|
|
33
33
|
const temporary = `${filePath}.${randomUUID()}.tmp`;
|
|
34
34
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
35
35
|
try {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { asDiagnostic } from './errors.js';
|
|
4
5
|
import { gitContext } from './git.js';
|
|
6
|
+
import { runProcess } from './process.js';
|
|
5
7
|
import { readStack } from './stack.js';
|
|
8
|
+
import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
|
|
6
9
|
import { sha256, stableJson, uniqueSorted } from './utils.js';
|
|
7
10
|
|
|
11
|
+
async function emit(onEvent, event) {
|
|
12
|
+
try { await onEvent?.(event); } catch { /* Progress observers do not control preparation. */ }
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export async function inspectWorkspaceSetupForStack({ projectRoot: root, stack }) {
|
|
9
16
|
const diagnostics = [...stack.workspaceSetup.diagnostics];
|
|
10
17
|
const applicableSteps = [];
|
|
@@ -68,3 +75,72 @@ export async function inspectProjectWorkspaceSetup({
|
|
|
68
75
|
const stack = await readStack(root);
|
|
69
76
|
return inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
70
77
|
}
|
|
78
|
+
|
|
79
|
+
/** Execute the selected Stack's exact workspace recipe with the caller's environment. */
|
|
80
|
+
export async function prepareProjectWorkspace({
|
|
81
|
+
environment = process.env,
|
|
82
|
+
onEvent,
|
|
83
|
+
processRunner = runProcess,
|
|
84
|
+
projectRoot,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
87
|
+
const stack = await readStack(root);
|
|
88
|
+
const setup = await inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
89
|
+
if (setup.status !== 'ready') {
|
|
90
|
+
const summary = setup.diagnostics.map(({ message }) => message).join(' ')
|
|
91
|
+
|| 'The selected Stack declares no applicable workspace preparation commands.';
|
|
92
|
+
return {
|
|
93
|
+
...setup,
|
|
94
|
+
summary,
|
|
95
|
+
commands: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resolvedEnvironment = withStackEnvironmentDefaults(
|
|
100
|
+
environment,
|
|
101
|
+
stack.environmentDefaults,
|
|
102
|
+
);
|
|
103
|
+
const commands = [];
|
|
104
|
+
try {
|
|
105
|
+
for (const step of setup.steps) {
|
|
106
|
+
await emit(onEvent, {
|
|
107
|
+
type: 'genesis.workspace-preparation',
|
|
108
|
+
code: 'WORKSPACE_PREPARATION_STARTED',
|
|
109
|
+
message: `Preparing: ${step.label}.`,
|
|
110
|
+
details: { label: step.label, argv: step.argv, workdir: step.workdir },
|
|
111
|
+
});
|
|
112
|
+
await processRunner(step.argv[0], step.argv.slice(1), {
|
|
113
|
+
cwd: path.resolve(root, step.workdir),
|
|
114
|
+
env: resolvedEnvironment,
|
|
115
|
+
maxBytes: 32 * 1024 * 1024,
|
|
116
|
+
code: 'WORKSPACE_PREPARATION_FAILED',
|
|
117
|
+
});
|
|
118
|
+
commands.push({
|
|
119
|
+
label: step.label,
|
|
120
|
+
argv: step.argv,
|
|
121
|
+
workdir: step.workdir,
|
|
122
|
+
});
|
|
123
|
+
await emit(onEvent, {
|
|
124
|
+
type: 'genesis.workspace-preparation',
|
|
125
|
+
code: 'WORKSPACE_PREPARATION_COMPLETED',
|
|
126
|
+
message: `Prepared: ${step.label}.`,
|
|
127
|
+
details: { label: step.label, argv: step.argv, workdir: step.workdir },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...setup,
|
|
132
|
+
status: 'passed',
|
|
133
|
+
summary: `Prepared the workspace with ${commands.length} command${commands.length === 1 ? '' : 's'}.`,
|
|
134
|
+
commands,
|
|
135
|
+
diagnostics: [],
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
...setup,
|
|
140
|
+
status: 'failed',
|
|
141
|
+
summary: error.message,
|
|
142
|
+
commands,
|
|
143
|
+
diagnostics: [asDiagnostic(error)],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,10 @@ import { inspectProjectLaunch } from './index/launch.js';
|
|
|
12
12
|
import { listBuiltinStackPieces } from './index/stack-catalog.js';
|
|
13
13
|
import { addStackPieces } from './index/stack.js';
|
|
14
14
|
import { verifyProject } from './index/verification.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
inspectProjectWorkspaceSetup,
|
|
17
|
+
prepareProjectWorkspace,
|
|
18
|
+
} from './index/workspace-setup.js';
|
|
16
19
|
|
|
17
20
|
function withIndexResult(result, index) {
|
|
18
21
|
const changedFiles = [...new Set([...result.changedFiles, ...index.changedFiles])].sort();
|
|
@@ -86,6 +89,10 @@ export function inspectWorkspaceSetup(options) {
|
|
|
86
89
|
return inspectProjectWorkspaceSetup(options);
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
export function prepareWorkspace(options) {
|
|
93
|
+
return prepareProjectWorkspace(options);
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
export function generatePrompt(options) {
|
|
90
97
|
return generateProjectPrompt(options);
|
|
91
98
|
}
|
package/stacks/pieces/jskit.md
CHANGED
|
@@ -36,6 +36,17 @@ application-owned composition.
|
|
|
36
36
|
|
|
37
37
|
- Dotenv `.env`
|
|
38
38
|
|
|
39
|
+
## City regions
|
|
40
|
+
|
|
41
|
+
- Ignore `**/test/**`
|
|
42
|
+
- Ignore `**/tests/**`
|
|
43
|
+
- Ignore `**/__tests__/**`
|
|
44
|
+
- Ignore `**/*.test.*`
|
|
45
|
+
- Ignore `**/*.spec.*`
|
|
46
|
+
- Match `packages` as `Packages`: `packages/**`
|
|
47
|
+
- Match `source` as `Source`: `src/**`
|
|
48
|
+
- Fallback `everything-else` as `Everything else`
|
|
49
|
+
|
|
39
50
|
## Launch
|
|
40
51
|
|
|
41
52
|
### Target `app`: Run app
|