@bleedingdev/modern-js-create 3.2.0-ultramodern.20 → 3.2.0-ultramodern.21
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/package.json +1 -1
- package/template-workspace/.agents/agent-reference-repos.json +1 -0
- package/template-workspace/.gitignore.handlebars +0 -1
- package/template-workspace/AGENTS.md +1 -1
- package/template-workspace/README.md.handlebars +3 -3
- package/template-workspace/scripts/setup-agent-reference-repos.mjs +229 -82
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +8 -0
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ The installer copies only the allowlisted private skills from `.agents/skills-lo
|
|
|
40
40
|
|
|
41
41
|
## Agent Reference Repositories
|
|
42
42
|
|
|
43
|
-
The workspace installs read-only source
|
|
43
|
+
The workspace installs read-only source references under `repos/` by default during `pnpm install` using `git subtree add --squash`. These repositories are reference material for coding agents, not application source:
|
|
44
44
|
|
|
45
45
|
- `repos/effect` from `Effect-TS/effect`.
|
|
46
46
|
- `repos/ultramodern.js` from `BleedingDev/ultramodern.js`.
|
|
@@ -20,9 +20,9 @@ pnpm ultramodern:check
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
By default, `pnpm install` also prepares read-only agent reference repositories
|
|
23
|
-
under `repos/` for Effect and UltraModern.js source lookup
|
|
24
|
-
with `ULTRAMODERN_SKIP_AGENT_REPOS=1 pnpm install`,
|
|
25
|
-
`pnpm agents:refs:install`.
|
|
23
|
+
under `repos/` for Effect and UltraModern.js source lookup using squashed git
|
|
24
|
+
subtrees. Disable this setup with `ULTRAMODERN_SKIP_AGENT_REPOS=1 pnpm install`,
|
|
25
|
+
or rerun it explicitly with `pnpm agents:refs:install`.
|
|
26
26
|
|
|
27
27
|
The topology and ownership metadata are generated under `topology/`. The
|
|
28
28
|
workspace also ships `.github/workflows/ultramodern-workspace-gates.yml` and
|
|
@@ -7,7 +7,6 @@ const args = new Set(process.argv.slice(2));
|
|
|
7
7
|
const checkOnly = args.has('--check');
|
|
8
8
|
const configPath = path.join(root, '.agents', 'agent-reference-repos.json');
|
|
9
9
|
const manifestPath = path.join(root, '.modernjs', 'agent-reference-repos.json');
|
|
10
|
-
const tempRoot = path.join(root, '.modernjs', 'agent-reference-repos-tmp');
|
|
11
10
|
|
|
12
11
|
const truthy = value => /^(1|true|yes|on)$/i.test(String(value ?? ''));
|
|
13
12
|
const falsy = value => /^(0|false|no|off)$/i.test(String(value ?? ''));
|
|
@@ -18,6 +17,17 @@ const skipRequested =
|
|
|
18
17
|
const required = truthy(process.env.ULTRAMODERN_AGENT_REPOS_REQUIRED);
|
|
19
18
|
const refresh = truthy(process.env.ULTRAMODERN_AGENT_REPOS_REFRESH);
|
|
20
19
|
|
|
20
|
+
const gitIdentityEnv = {
|
|
21
|
+
GIT_AUTHOR_NAME:
|
|
22
|
+
process.env.GIT_AUTHOR_NAME || 'UltraModern Agent Reference Setup',
|
|
23
|
+
GIT_AUTHOR_EMAIL:
|
|
24
|
+
process.env.GIT_AUTHOR_EMAIL || 'ultramodern-agent-refs@local',
|
|
25
|
+
GIT_COMMITTER_NAME:
|
|
26
|
+
process.env.GIT_COMMITTER_NAME || 'UltraModern Agent Reference Setup',
|
|
27
|
+
GIT_COMMITTER_EMAIL:
|
|
28
|
+
process.env.GIT_COMMITTER_EMAIL || 'ultramodern-agent-refs@local',
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
const log = message => console.log(`[agent-reference-repos] ${message}`);
|
|
22
32
|
const warn = message => console.warn(`[agent-reference-repos] ${message}`);
|
|
23
33
|
|
|
@@ -36,6 +46,11 @@ function run(command, commandArgs, options = {}) {
|
|
|
36
46
|
const result = spawnSync(command, commandArgs, {
|
|
37
47
|
cwd: options.cwd ?? root,
|
|
38
48
|
encoding: 'utf-8',
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
...gitIdentityEnv,
|
|
52
|
+
...(options.env ?? {}),
|
|
53
|
+
},
|
|
39
54
|
stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
|
|
40
55
|
timeout: options.timeout ?? 120000,
|
|
41
56
|
});
|
|
@@ -74,38 +89,160 @@ function hasGit() {
|
|
|
74
89
|
return result.status === 0;
|
|
75
90
|
}
|
|
76
91
|
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
|
|
92
|
+
function hasGitSubtree() {
|
|
93
|
+
const result = spawnSync('git', ['subtree', '-h'], {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
});
|
|
97
|
+
return (
|
|
98
|
+
(result.status === 0 || result.status === 129) &&
|
|
99
|
+
result.stdout.includes('usage: git subtree')
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isGitWorkTree() {
|
|
104
|
+
const result = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
105
|
+
cwd: root,
|
|
106
|
+
encoding: 'utf-8',
|
|
107
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
108
|
+
});
|
|
109
|
+
return result.status === 0 && result.stdout.trim() === 'true';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasCommits() {
|
|
113
|
+
const result = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
114
|
+
cwd: root,
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
117
|
+
});
|
|
118
|
+
return result.status === 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function porcelainStatus() {
|
|
122
|
+
return run('git', ['status', '--porcelain'], { timeout: 30000 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ensureGitRepository() {
|
|
126
|
+
if (!isGitWorkTree()) {
|
|
127
|
+
if (checkOnly) {
|
|
128
|
+
fail('workspace is not a git repository');
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
log('initializing git repository for agent reference subtrees');
|
|
132
|
+
run('git', ['init'], { timeout: 30000 });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!hasCommits()) {
|
|
136
|
+
if (checkOnly) {
|
|
137
|
+
fail('workspace has no initial git commit');
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
log('creating initial workspace commit before adding reference subtrees');
|
|
141
|
+
run('git', ['add', '-A'], { timeout: 30000 });
|
|
142
|
+
run('git', ['commit', '-m', 'Initialize UltraModern workspace'], {
|
|
143
|
+
timeout: 120000,
|
|
144
|
+
});
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const status = porcelainStatus();
|
|
149
|
+
if (status) {
|
|
150
|
+
fail(
|
|
151
|
+
'workspace has uncommitted changes; commit or stash them before installing reference subtrees',
|
|
152
|
+
);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function remoteCommit(repo) {
|
|
160
|
+
let output = run('git', ['ls-remote', repo.url, `refs/heads/${repo.ref}`], {
|
|
161
|
+
timeout: 120000,
|
|
162
|
+
});
|
|
163
|
+
if (!output) {
|
|
164
|
+
output = run('git', ['ls-remote', repo.url, repo.ref], {
|
|
165
|
+
timeout: 120000,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const [commit] = output.split(/\s+/);
|
|
169
|
+
if (!/^[a-f0-9]{40}$/i.test(commit ?? '')) {
|
|
170
|
+
throw new Error(`Could not resolve ${repo.url}#${repo.ref}`);
|
|
171
|
+
}
|
|
172
|
+
return commit;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function subtreeCommitExists(repo) {
|
|
176
|
+
const result = spawnSync(
|
|
177
|
+
'git',
|
|
178
|
+
[
|
|
179
|
+
'log',
|
|
180
|
+
'--grep',
|
|
181
|
+
`git-subtree-dir: ${repo.path}`,
|
|
182
|
+
'--format=%H',
|
|
183
|
+
'-n',
|
|
184
|
+
'1',
|
|
185
|
+
],
|
|
186
|
+
{
|
|
187
|
+
cwd: root,
|
|
188
|
+
encoding: 'utf-8',
|
|
189
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function installedManifestEntry(repo) {
|
|
196
|
+
if (!fs.existsSync(manifestPath)) {
|
|
80
197
|
return undefined;
|
|
81
198
|
}
|
|
82
199
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return marker;
|
|
86
|
-
}
|
|
200
|
+
const manifest = readJson(manifestPath);
|
|
201
|
+
return manifest.repositories?.find(entry => entry.id === repo.id);
|
|
87
202
|
} catch {
|
|
88
203
|
return undefined;
|
|
89
204
|
}
|
|
90
|
-
return undefined;
|
|
91
205
|
}
|
|
92
206
|
|
|
93
|
-
function
|
|
207
|
+
function assertSubtreePresent(repo) {
|
|
94
208
|
assertSafeRepoPath(repo.path);
|
|
95
209
|
const targetPath = path.join(root, repo.path);
|
|
96
|
-
|
|
210
|
+
if (!fs.existsSync(targetPath)) {
|
|
211
|
+
fail(`${repo.path} is missing`);
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
if (!subtreeCommitExists(repo)) {
|
|
215
|
+
fail(`${repo.path} is present but has no git-subtree commit evidence`);
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
return (
|
|
219
|
+
installedManifestEntry(repo) ?? {
|
|
220
|
+
id: repo.id,
|
|
221
|
+
name: repo.name,
|
|
222
|
+
url: repo.url,
|
|
223
|
+
ref: repo.ref,
|
|
224
|
+
path: repo.path,
|
|
225
|
+
readOnly: repo.readOnly !== false,
|
|
226
|
+
status: 'present',
|
|
227
|
+
strategy: 'git-subtree-squash',
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function addSubtree(repo) {
|
|
233
|
+
assertSafeRepoPath(repo.path);
|
|
234
|
+
const targetPath = path.join(root, repo.path);
|
|
235
|
+
const existing = fs.existsSync(targetPath);
|
|
97
236
|
|
|
98
237
|
if (existing && !refresh) {
|
|
99
|
-
|
|
100
|
-
return { ...existing, status: 'present' };
|
|
238
|
+
return assertSubtreePresent(repo);
|
|
101
239
|
}
|
|
102
240
|
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
241
|
+
if (existing && refresh) {
|
|
242
|
+
fail(
|
|
243
|
+
`${repo.path} already exists; refresh for subtree references is intentionally manual`,
|
|
244
|
+
);
|
|
245
|
+
return undefined;
|
|
109
246
|
}
|
|
110
247
|
|
|
111
248
|
if (checkOnly) {
|
|
@@ -113,53 +250,70 @@ function materializeRepository(repo) {
|
|
|
113
250
|
return undefined;
|
|
114
251
|
}
|
|
115
252
|
|
|
116
|
-
|
|
117
|
-
|
|
253
|
+
const commit = remoteCommit(repo);
|
|
254
|
+
log(`adding ${repo.name} as git subtree at ${repo.path} (${commit})`);
|
|
255
|
+
run('git', ['fetch', '--depth', '1', repo.url, repo.ref], {
|
|
256
|
+
timeout: 300000,
|
|
257
|
+
});
|
|
258
|
+
run(
|
|
259
|
+
'git',
|
|
260
|
+
[
|
|
261
|
+
'subtree',
|
|
262
|
+
'add',
|
|
263
|
+
'--prefix',
|
|
264
|
+
repo.path,
|
|
265
|
+
'FETCH_HEAD',
|
|
266
|
+
'--squash',
|
|
267
|
+
'-m',
|
|
268
|
+
`Add ${repo.name} agent reference repo`,
|
|
269
|
+
],
|
|
270
|
+
{ timeout: 600000 },
|
|
271
|
+
);
|
|
118
272
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
],
|
|
134
|
-
{ timeout: 300000 },
|
|
135
|
-
);
|
|
136
|
-
const commit = run('git', ['-C', tempPath, 'rev-parse', 'HEAD']);
|
|
137
|
-
fs.rmSync(path.join(tempPath, '.git'), { recursive: true, force: true });
|
|
138
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
139
|
-
fs.renameSync(tempPath, targetPath);
|
|
273
|
+
return {
|
|
274
|
+
schemaVersion: 1,
|
|
275
|
+
id: repo.id,
|
|
276
|
+
name: repo.name,
|
|
277
|
+
url: repo.url,
|
|
278
|
+
ref: repo.ref,
|
|
279
|
+
commit,
|
|
280
|
+
path: repo.path,
|
|
281
|
+
readOnly: repo.readOnly !== false,
|
|
282
|
+
strategy: 'git-subtree-squash',
|
|
283
|
+
status: 'installed',
|
|
284
|
+
installedAt: new Date().toISOString(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
140
287
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
288
|
+
function writeManifest(entries) {
|
|
289
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
290
|
+
fs.writeFileSync(
|
|
291
|
+
manifestPath,
|
|
292
|
+
`${JSON.stringify(
|
|
293
|
+
{
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
generatedAt: new Date().toISOString(),
|
|
296
|
+
strategy: 'git-subtree-squash',
|
|
297
|
+
installDir: 'repos',
|
|
298
|
+
repositories: entries,
|
|
299
|
+
},
|
|
300
|
+
null,
|
|
301
|
+
2,
|
|
302
|
+
)}\n`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function commitManifestIfChanged() {
|
|
307
|
+
const status = run('git', ['status', '--porcelain', '--', manifestPath], {
|
|
308
|
+
timeout: 30000,
|
|
309
|
+
});
|
|
310
|
+
if (!status) {
|
|
311
|
+
return;
|
|
162
312
|
}
|
|
313
|
+
run('git', ['add', manifestPath], { timeout: 30000 });
|
|
314
|
+
run('git', ['commit', '-m', 'Record agent reference repo manifest'], {
|
|
315
|
+
timeout: 120000,
|
|
316
|
+
});
|
|
163
317
|
}
|
|
164
318
|
|
|
165
319
|
function main() {
|
|
@@ -180,30 +334,25 @@ function main() {
|
|
|
180
334
|
fail('git is required to install agent reference repositories');
|
|
181
335
|
return;
|
|
182
336
|
}
|
|
337
|
+
if (!hasGitSubtree()) {
|
|
338
|
+
fail('git subtree is required to install agent reference repositories');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!ensureGitRepository()) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
183
344
|
|
|
184
|
-
const
|
|
345
|
+
const entries = [];
|
|
185
346
|
for (const repo of config.repositories ?? []) {
|
|
186
|
-
const result =
|
|
347
|
+
const result = checkOnly ? assertSubtreePresent(repo) : addSubtree(repo);
|
|
187
348
|
if (result) {
|
|
188
|
-
|
|
349
|
+
entries.push(result);
|
|
189
350
|
}
|
|
190
351
|
}
|
|
191
352
|
|
|
192
353
|
if (!checkOnly) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
manifestPath,
|
|
196
|
-
`${JSON.stringify(
|
|
197
|
-
{
|
|
198
|
-
schemaVersion: 1,
|
|
199
|
-
generatedAt: new Date().toISOString(),
|
|
200
|
-
installDir: config.installDir ?? 'repos',
|
|
201
|
-
repositories: installed,
|
|
202
|
-
},
|
|
203
|
-
null,
|
|
204
|
-
2,
|
|
205
|
-
)}\n`,
|
|
206
|
-
);
|
|
354
|
+
writeManifest(entries);
|
|
355
|
+
commitManifestIfChanged();
|
|
207
356
|
}
|
|
208
357
|
}
|
|
209
358
|
|
|
@@ -212,6 +361,4 @@ try {
|
|
|
212
361
|
} catch (error) {
|
|
213
362
|
console.error(`[agent-reference-repos] ${error.message}`);
|
|
214
363
|
process.exitCode = 1;
|
|
215
|
-
} finally {
|
|
216
|
-
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
217
364
|
}
|
|
@@ -285,6 +285,10 @@ assert(
|
|
|
285
285
|
agentReferenceRepos.defaultEnabled === true,
|
|
286
286
|
'Agent reference repositories must be enabled by default',
|
|
287
287
|
);
|
|
288
|
+
assert(
|
|
289
|
+
agentReferenceRepos.strategy === 'git-subtree-squash',
|
|
290
|
+
'Agent reference repositories must use git subtree squash strategy',
|
|
291
|
+
);
|
|
288
292
|
assert(
|
|
289
293
|
agentReferenceRepos.repositories?.some(
|
|
290
294
|
(repo) =>
|
|
@@ -309,6 +313,10 @@ assert(
|
|
|
309
313
|
),
|
|
310
314
|
'Agent reference repository setup must expose an opt-out environment variable',
|
|
311
315
|
);
|
|
316
|
+
assert(
|
|
317
|
+
readText('scripts/setup-agent-reference-repos.mjs').includes('git-subtree-dir'),
|
|
318
|
+
'Agent reference repository setup must validate git subtree evidence',
|
|
319
|
+
);
|
|
312
320
|
for (const skillName of baselineAgentSkills) {
|
|
313
321
|
assert(
|
|
314
322
|
skillsLock.baseline?.some((skill) => skill.name === skillName),
|