proteum 2.2.2 → 2.2.6
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/AGENTS.md +5 -5
- package/README.md +4 -1
- package/agents/project/AGENTS.md +9 -8
- package/agents/project/diagnostics.md +10 -8
- package/agents/project/optimizations.md +2 -2
- package/agents/project/root/AGENTS.md +8 -7
- package/agents/project/tests/AGENTS.md +3 -2
- package/cli/app/index.ts +19 -9
- package/cli/commands/check.ts +7 -3
- package/cli/commands/configure.ts +14 -9
- package/cli/commands/e2e.ts +204 -0
- package/cli/commands/typecheck.ts +7 -3
- package/cli/presentation/commands.ts +37 -7
- package/cli/runtime/command.ts +2 -2
- package/cli/runtime/commands.ts +59 -0
- package/cli/scaffold/index.ts +1 -1
- package/cli/utils/agents.ts +175 -80
- package/cli/utils/check.ts +32 -4
- package/docs/dev-sessions.md +11 -2
- package/docs/diagnostics.md +2 -1
- package/package.json +1 -1
- package/scripts/update-codex-agents.ts +2 -2
- package/server/services/router/request/index.ts +2 -1
- package/server/services/router/request/ip.test.cjs +60 -0
- package/server/services/router/request/ip.ts +71 -0
package/cli/utils/agents.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { logVerbose } from '../runtime/verbose';
|
|
|
12
12
|
----------------------------------*/
|
|
13
13
|
|
|
14
14
|
type TProjectInstructionArgs = { coreRoot: string };
|
|
15
|
-
type
|
|
15
|
+
type TConfigureProjectAgentInstructionsArgs = {
|
|
16
16
|
appRoot: string;
|
|
17
17
|
coreRoot: string;
|
|
18
18
|
dryRun?: boolean;
|
|
@@ -20,9 +20,14 @@ type TConfigureProjectAgentSymlinksArgs = {
|
|
|
20
20
|
overwriteBlockedPaths?: string[];
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
type
|
|
23
|
+
type TAgentInstructionDefinition = {
|
|
24
|
+
includeWorktreeBootstrap?: boolean;
|
|
25
|
+
projectPath: string;
|
|
26
|
+
sourcePath: string;
|
|
27
|
+
ensureParentDir?: boolean;
|
|
28
|
+
};
|
|
24
29
|
|
|
25
|
-
type
|
|
30
|
+
type TEnsureInstructionStubsResult = {
|
|
26
31
|
blocked: string[];
|
|
27
32
|
created: string[];
|
|
28
33
|
overwritten: string[];
|
|
@@ -30,7 +35,7 @@ type TEnsureSymlinksResult = {
|
|
|
30
35
|
updated: string[];
|
|
31
36
|
};
|
|
32
37
|
|
|
33
|
-
export type
|
|
38
|
+
export type TConfigureProjectAgentInstructionsResult = {
|
|
34
39
|
appRoot: string;
|
|
35
40
|
blocked: string[];
|
|
36
41
|
created: string[];
|
|
@@ -51,7 +56,24 @@ export type TProjectAgentFileInspection = {
|
|
|
51
56
|
- CONSTANTS
|
|
52
57
|
----------------------------------*/
|
|
53
58
|
|
|
54
|
-
const
|
|
59
|
+
const managedInstructionStubHeader = '# Proteum Managed Instructions';
|
|
60
|
+
const managedInstructionStubIntro = 'This file is managed by `proteum configure agents`.';
|
|
61
|
+
const newProteumWorktreeBootstrapBlock = [
|
|
62
|
+
'## New Proteum Worktree Bootstrap',
|
|
63
|
+
'',
|
|
64
|
+
'When starting work in a newly created Proteum worktree, immediately copy the `.env` file from the main worktree, change it to use a different free port by updating `PORT`, `URL`, and `URL_INTERNAL`, and run:',
|
|
65
|
+
'',
|
|
66
|
+
'```',
|
|
67
|
+
'npx proteum configure agents',
|
|
68
|
+
'npm i',
|
|
69
|
+
'npx prisma generate',
|
|
70
|
+
'npx proteum refresh',
|
|
71
|
+
'```',
|
|
72
|
+
'',
|
|
73
|
+
'Run it before reading project-specific AGENTS files or making code changes. If the command is interactive, answer according to the current repository layout, then continue by reading the generated AGENTS.md files.',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const sharedAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
55
77
|
{ projectPath: 'CODING_STYLE.md', sourcePath: 'CODING_STYLE.md' },
|
|
56
78
|
{ projectPath: 'diagnostics.md', sourcePath: 'diagnostics.md' },
|
|
57
79
|
{ projectPath: 'optimizations.md', sourcePath: 'optimizations.md' },
|
|
@@ -65,34 +87,36 @@ const sharedAppAgentLinkDefinitions: TAgentLinkDefinition[] = [
|
|
|
65
87
|
{ projectPath: path.join('tests', 'e2e', 'AGENTS.md'), sourcePath: path.join('tests', 'AGENTS.md') },
|
|
66
88
|
];
|
|
67
89
|
|
|
68
|
-
const
|
|
69
|
-
{ projectPath: 'AGENTS.md', sourcePath: 'AGENTS.md' },
|
|
70
|
-
...
|
|
90
|
+
const standaloneAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
91
|
+
{ projectPath: 'AGENTS.md', sourcePath: 'AGENTS.md', includeWorktreeBootstrap: true },
|
|
92
|
+
...sharedAppAgentInstructionDefinitions,
|
|
71
93
|
];
|
|
72
94
|
|
|
73
|
-
const
|
|
95
|
+
const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
74
96
|
{ projectPath: 'AGENTS.md', sourcePath: path.join('app-root', 'AGENTS.md') },
|
|
75
|
-
...
|
|
97
|
+
...sharedAppAgentInstructionDefinitions,
|
|
76
98
|
];
|
|
77
99
|
|
|
78
|
-
const
|
|
79
|
-
{ projectPath: 'AGENTS.md', sourcePath: path.join('root', 'AGENTS.md') },
|
|
100
|
+
const monorepoRootAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
101
|
+
{ projectPath: 'AGENTS.md', sourcePath: path.join('root', 'AGENTS.md'), includeWorktreeBootstrap: true },
|
|
80
102
|
];
|
|
81
103
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
104
|
+
const legacyProjectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
|
|
105
|
+
const legacyProjectInstructionGitignoreBlockEnd = '# End Proteum-managed instruction symlinks';
|
|
106
|
+
const projectInstructionGitignoreBlockStart = '# Proteum-managed instruction files';
|
|
107
|
+
const projectInstructionGitignoreBlockEnd = '# End Proteum-managed instruction files';
|
|
84
108
|
|
|
85
109
|
/*----------------------------------
|
|
86
110
|
- PUBLIC API
|
|
87
111
|
----------------------------------*/
|
|
88
112
|
|
|
89
|
-
export function
|
|
113
|
+
export function configureProjectAgentInstructions({
|
|
90
114
|
appRoot,
|
|
91
115
|
coreRoot,
|
|
92
116
|
dryRun = false,
|
|
93
117
|
monorepoRoot,
|
|
94
118
|
overwriteBlockedPaths = [],
|
|
95
|
-
}:
|
|
119
|
+
}: TConfigureProjectAgentInstructionsArgs): TConfigureProjectAgentInstructionsResult {
|
|
96
120
|
const normalizedAppRoot = path.resolve(appRoot);
|
|
97
121
|
const normalizedMonorepoRoot = monorepoRoot ? path.resolve(monorepoRoot) : undefined;
|
|
98
122
|
const normalizedOverwriteBlockedPaths = new Set(
|
|
@@ -100,7 +124,7 @@ export function configureProjectAgentSymlinks({
|
|
|
100
124
|
);
|
|
101
125
|
const mode =
|
|
102
126
|
normalizedMonorepoRoot && normalizedMonorepoRoot !== normalizedAppRoot ? ('monorepo' as const) : ('standalone' as const);
|
|
103
|
-
const result:
|
|
127
|
+
const result: TConfigureProjectAgentInstructionsResult = {
|
|
104
128
|
appRoot: normalizedAppRoot,
|
|
105
129
|
blocked: [],
|
|
106
130
|
created: [],
|
|
@@ -114,50 +138,66 @@ export function configureProjectAgentSymlinks({
|
|
|
114
138
|
if (mode === 'monorepo' && normalizedMonorepoRoot) {
|
|
115
139
|
result.monorepoRoot = normalizedMonorepoRoot;
|
|
116
140
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
const rootInstructions = getRootAgentInstructionDefinitions({ coreRoot });
|
|
142
|
+
const rootStubs = ensureInstructionStubs(
|
|
143
|
+
normalizedMonorepoRoot,
|
|
144
|
+
rootInstructions,
|
|
145
|
+
'[agents]',
|
|
146
|
+
path.join(coreRoot, 'agents', 'project'),
|
|
147
|
+
{
|
|
148
|
+
dryRun,
|
|
149
|
+
overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
mergeInstructionResults(result, rootStubs, normalizedMonorepoRoot);
|
|
153
|
+
|
|
154
|
+
if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedMonorepoRoot, instructionDefinitions: rootInstructions }))
|
|
125
155
|
result.updatedGitignores.push(path.join(normalizedMonorepoRoot, '.gitignore'));
|
|
126
156
|
}
|
|
127
157
|
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
const appInstructions = getAppAgentInstructionDefinitions({ coreRoot, mode });
|
|
159
|
+
const appStubs = ensureInstructionStubs(
|
|
160
|
+
normalizedAppRoot,
|
|
161
|
+
appInstructions,
|
|
162
|
+
'[agents]',
|
|
163
|
+
path.join(coreRoot, 'agents', 'project'),
|
|
164
|
+
{
|
|
165
|
+
dryRun,
|
|
166
|
+
overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
mergeInstructionResults(result, appStubs, normalizedAppRoot);
|
|
134
170
|
|
|
135
|
-
if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedAppRoot,
|
|
171
|
+
if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedAppRoot, instructionDefinitions: appInstructions }))
|
|
136
172
|
result.updatedGitignores.push(path.join(normalizedAppRoot, '.gitignore'));
|
|
137
173
|
|
|
138
174
|
return result;
|
|
139
175
|
}
|
|
140
176
|
|
|
177
|
+
export const configureProjectAgentSymlinks = configureProjectAgentInstructions;
|
|
178
|
+
|
|
141
179
|
export function getProjectInstructionGitignoreEntries({ coreRoot }: TProjectInstructionArgs) {
|
|
142
180
|
return Array.from(
|
|
143
181
|
new Set(
|
|
144
|
-
|
|
145
|
-
`/${normalizeProjectPathForGitignore(
|
|
182
|
+
getAppAgentInstructionDefinitions({ coreRoot, mode: 'standalone' }).map((instructionDefinition) =>
|
|
183
|
+
`/${normalizeProjectPathForGitignore(instructionDefinition.projectPath)}`,
|
|
146
184
|
),
|
|
147
185
|
),
|
|
148
186
|
);
|
|
149
187
|
}
|
|
150
188
|
|
|
151
189
|
export function renderProjectInstructionGitignoreBlock({ coreRoot }: TProjectInstructionArgs) {
|
|
152
|
-
return renderInstructionGitignoreBlock({
|
|
190
|
+
return renderInstructionGitignoreBlock({
|
|
191
|
+
instructionDefinitions: getAppAgentInstructionDefinitions({ coreRoot, mode: 'standalone' }),
|
|
192
|
+
});
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
export function inspectProjectAgentFiles({ appRoot }: { appRoot: string }): TProjectAgentFileInspection {
|
|
156
196
|
const normalizedAppRoot = path.resolve(appRoot);
|
|
157
197
|
const expectedAgentPaths = Array.from(
|
|
158
198
|
new Set(
|
|
159
|
-
|
|
160
|
-
.map((
|
|
199
|
+
standaloneAppAgentInstructionDefinitions
|
|
200
|
+
.map((instructionDefinition) => instructionDefinition.projectPath)
|
|
161
201
|
.filter((projectPath) => projectPath.endsWith('AGENTS.md')),
|
|
162
202
|
),
|
|
163
203
|
);
|
|
@@ -187,42 +227,47 @@ export function inspectProjectAgentFiles({ appRoot }: { appRoot: string }): TPro
|
|
|
187
227
|
- HELPERS
|
|
188
228
|
----------------------------------*/
|
|
189
229
|
|
|
190
|
-
function
|
|
230
|
+
function getAppAgentInstructionDefinitions({
|
|
191
231
|
coreRoot,
|
|
192
232
|
mode,
|
|
193
233
|
}: TProjectInstructionArgs & { mode: 'monorepo' | 'standalone' }) {
|
|
194
234
|
const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
|
|
195
|
-
const sourceDefinitions =
|
|
235
|
+
const sourceDefinitions =
|
|
236
|
+
mode === 'monorepo' ? monorepoAppAgentInstructionDefinitions : standaloneAppAgentInstructionDefinitions;
|
|
196
237
|
|
|
197
|
-
return
|
|
238
|
+
return resolveAgentInstructionDefinitions({
|
|
198
239
|
agentSourceRoot,
|
|
199
|
-
|
|
240
|
+
instructionDefinitions: sourceDefinitions,
|
|
200
241
|
});
|
|
201
242
|
}
|
|
202
243
|
|
|
203
|
-
function
|
|
204
|
-
return
|
|
244
|
+
function getRootAgentInstructionDefinitions({ coreRoot }: TProjectInstructionArgs) {
|
|
245
|
+
return resolveAgentInstructionDefinitions({
|
|
205
246
|
agentSourceRoot: path.join(coreRoot, 'agents', 'project'),
|
|
206
|
-
|
|
247
|
+
instructionDefinitions: monorepoRootAgentInstructionDefinitions,
|
|
207
248
|
});
|
|
208
249
|
}
|
|
209
250
|
|
|
210
|
-
function
|
|
251
|
+
function resolveAgentInstructionDefinitions({
|
|
211
252
|
agentSourceRoot,
|
|
212
|
-
|
|
253
|
+
instructionDefinitions,
|
|
213
254
|
}: {
|
|
214
255
|
agentSourceRoot: string;
|
|
215
|
-
|
|
256
|
+
instructionDefinitions: TAgentInstructionDefinition[];
|
|
216
257
|
}) {
|
|
217
|
-
return
|
|
218
|
-
...
|
|
219
|
-
sourcePath: path.join(agentSourceRoot,
|
|
258
|
+
return instructionDefinitions.map((instructionDefinition) => ({
|
|
259
|
+
...instructionDefinition,
|
|
260
|
+
sourcePath: path.join(agentSourceRoot, instructionDefinition.sourcePath),
|
|
220
261
|
}));
|
|
221
262
|
}
|
|
222
263
|
|
|
223
|
-
function renderInstructionGitignoreBlock({
|
|
264
|
+
function renderInstructionGitignoreBlock({ instructionDefinitions }: { instructionDefinitions: TAgentInstructionDefinition[] }) {
|
|
224
265
|
const entries = Array.from(
|
|
225
|
-
new Set(
|
|
266
|
+
new Set(
|
|
267
|
+
instructionDefinitions.map(
|
|
268
|
+
(instructionDefinition) => `/${normalizeProjectPathForGitignore(instructionDefinition.projectPath)}`,
|
|
269
|
+
),
|
|
270
|
+
),
|
|
226
271
|
);
|
|
227
272
|
|
|
228
273
|
return [projectInstructionGitignoreBlockStart, ...entries, projectInstructionGitignoreBlockEnd].join('\n');
|
|
@@ -230,15 +275,17 @@ function renderInstructionGitignoreBlock({ linkDefinitions }: { linkDefinitions:
|
|
|
230
275
|
|
|
231
276
|
function ensureInstructionGitignoreEntries({
|
|
232
277
|
rootDir,
|
|
233
|
-
|
|
278
|
+
instructionDefinitions,
|
|
234
279
|
}: {
|
|
235
280
|
rootDir: string;
|
|
236
|
-
|
|
281
|
+
instructionDefinitions: TAgentInstructionDefinition[];
|
|
237
282
|
}) {
|
|
238
283
|
const gitignoreFilepath = path.join(rootDir, '.gitignore');
|
|
239
284
|
if (!pathEntryExists(gitignoreFilepath)) return false;
|
|
240
285
|
|
|
241
|
-
const managedEntries = new Set(
|
|
286
|
+
const managedEntries = new Set(
|
|
287
|
+
instructionDefinitions.map((instructionDefinition) => normalizeGitignoreEntry(instructionDefinition.projectPath)),
|
|
288
|
+
);
|
|
242
289
|
const lines = fs.readFileSync(gitignoreFilepath, 'utf8').split(/\r?\n/);
|
|
243
290
|
const filteredLines: string[] = [];
|
|
244
291
|
let insideManagedBlock = false;
|
|
@@ -246,12 +293,12 @@ function ensureInstructionGitignoreEntries({
|
|
|
246
293
|
for (const line of lines) {
|
|
247
294
|
const trimmedLine = line.trim();
|
|
248
295
|
|
|
249
|
-
if (trimmedLine === projectInstructionGitignoreBlockStart) {
|
|
296
|
+
if (trimmedLine === projectInstructionGitignoreBlockStart || trimmedLine === legacyProjectInstructionGitignoreBlockStart) {
|
|
250
297
|
insideManagedBlock = true;
|
|
251
298
|
continue;
|
|
252
299
|
}
|
|
253
300
|
|
|
254
|
-
if (trimmedLine === projectInstructionGitignoreBlockEnd) {
|
|
301
|
+
if (trimmedLine === projectInstructionGitignoreBlockEnd || trimmedLine === legacyProjectInstructionGitignoreBlockEnd) {
|
|
255
302
|
insideManagedBlock = false;
|
|
256
303
|
continue;
|
|
257
304
|
}
|
|
@@ -263,7 +310,7 @@ function ensureInstructionGitignoreEntries({
|
|
|
263
310
|
}
|
|
264
311
|
|
|
265
312
|
const baseContent = trimTrailingBlankLines(filteredLines).join('\n');
|
|
266
|
-
const managedBlock = renderInstructionGitignoreBlock({
|
|
313
|
+
const managedBlock = renderInstructionGitignoreBlock({ instructionDefinitions });
|
|
267
314
|
const nextContent = baseContent ? `${baseContent}\n\n${managedBlock}\n` : `${managedBlock}\n`;
|
|
268
315
|
|
|
269
316
|
if (nextContent === fs.readFileSync(gitignoreFilepath, 'utf8')) return false;
|
|
@@ -274,9 +321,9 @@ function ensureInstructionGitignoreEntries({
|
|
|
274
321
|
return true;
|
|
275
322
|
}
|
|
276
323
|
|
|
277
|
-
function
|
|
324
|
+
function ensureInstructionStubs(
|
|
278
325
|
rootDir: string,
|
|
279
|
-
|
|
326
|
+
instructionDefinitions: TAgentInstructionDefinition[],
|
|
280
327
|
logPrefix: string,
|
|
281
328
|
managedSourceRoot: string,
|
|
282
329
|
{
|
|
@@ -286,8 +333,8 @@ function ensureSymlinks(
|
|
|
286
333
|
dryRun: boolean;
|
|
287
334
|
overwriteBlockedPaths: Set<string>;
|
|
288
335
|
},
|
|
289
|
-
):
|
|
290
|
-
const result:
|
|
336
|
+
): TEnsureInstructionStubsResult {
|
|
337
|
+
const result: TEnsureInstructionStubsResult = {
|
|
291
338
|
blocked: [],
|
|
292
339
|
created: [],
|
|
293
340
|
overwritten: [],
|
|
@@ -295,24 +342,31 @@ function ensureSymlinks(
|
|
|
295
342
|
updated: [],
|
|
296
343
|
};
|
|
297
344
|
|
|
298
|
-
for (const
|
|
299
|
-
const projectFilepath = path.join(rootDir,
|
|
345
|
+
for (const instructionDefinition of instructionDefinitions) {
|
|
346
|
+
const projectFilepath = path.join(rootDir, instructionDefinition.projectPath);
|
|
300
347
|
const projectParentDir = path.dirname(projectFilepath);
|
|
301
348
|
const relativeProjectPath = path.relative(rootDir, projectFilepath) || '.';
|
|
302
349
|
|
|
303
|
-
if (
|
|
350
|
+
if (instructionDefinition.ensureParentDir) fs.ensureDirSync(projectParentDir);
|
|
304
351
|
else if (!fs.existsSync(projectParentDir)) {
|
|
305
352
|
result.skipped.push(relativeProjectPath);
|
|
306
353
|
continue;
|
|
307
354
|
}
|
|
308
355
|
|
|
309
|
-
const sourceFilepath =
|
|
356
|
+
const sourceFilepath = instructionDefinition.sourcePath;
|
|
310
357
|
if (!fs.existsSync(sourceFilepath)) throw new Error(`Missing project instruction asset: ${sourceFilepath}`);
|
|
311
358
|
|
|
359
|
+
const stubContent = renderInstructionStub({
|
|
360
|
+
includeWorktreeBootstrap: instructionDefinition.includeWorktreeBootstrap === true,
|
|
361
|
+
projectFilepath,
|
|
362
|
+
sourceFilepath,
|
|
363
|
+
});
|
|
364
|
+
|
|
312
365
|
const existingState = inspectExistingPath({
|
|
313
366
|
managedSourceRoot,
|
|
314
367
|
projectFilepath,
|
|
315
368
|
sourceFilepath,
|
|
369
|
+
stubContent,
|
|
316
370
|
});
|
|
317
371
|
|
|
318
372
|
if (existingState.kind === 'match') {
|
|
@@ -326,57 +380,94 @@ function ensureSymlinks(
|
|
|
326
380
|
continue;
|
|
327
381
|
}
|
|
328
382
|
|
|
329
|
-
const symlinkTarget = path.relative(projectParentDir, sourceFilepath);
|
|
330
|
-
|
|
331
383
|
if (existingState.kind === 'managed-different') {
|
|
332
384
|
if (!dryRun) {
|
|
333
|
-
fs.
|
|
334
|
-
fs.
|
|
385
|
+
fs.removeSync(projectFilepath);
|
|
386
|
+
fs.writeFileSync(projectFilepath, stubContent);
|
|
335
387
|
}
|
|
336
388
|
result.updated.push(relativeProjectPath);
|
|
337
|
-
logVerbose(`${logPrefix} Updated ${relativeProjectPath}
|
|
389
|
+
logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
|
|
338
390
|
continue;
|
|
339
391
|
}
|
|
340
392
|
|
|
341
393
|
if (existingState.kind === 'blocked') {
|
|
342
394
|
if (!dryRun) {
|
|
343
395
|
fs.removeSync(projectFilepath);
|
|
344
|
-
fs.
|
|
396
|
+
fs.writeFileSync(projectFilepath, stubContent);
|
|
345
397
|
}
|
|
346
398
|
result.overwritten.push(relativeProjectPath);
|
|
347
|
-
logVerbose(`${logPrefix} Replaced ${relativeProjectPath}
|
|
399
|
+
logVerbose(`${logPrefix} Replaced ${relativeProjectPath}`);
|
|
348
400
|
continue;
|
|
349
401
|
}
|
|
350
402
|
|
|
351
|
-
if (!dryRun) fs.
|
|
403
|
+
if (!dryRun) fs.writeFileSync(projectFilepath, stubContent);
|
|
352
404
|
result.created.push(relativeProjectPath);
|
|
353
|
-
logVerbose(`${logPrefix} Created ${relativeProjectPath}
|
|
405
|
+
logVerbose(`${logPrefix} Created ${relativeProjectPath}`);
|
|
354
406
|
}
|
|
355
407
|
|
|
356
408
|
return result;
|
|
357
409
|
}
|
|
358
410
|
|
|
411
|
+
function renderInstructionStub({
|
|
412
|
+
includeWorktreeBootstrap,
|
|
413
|
+
projectFilepath,
|
|
414
|
+
sourceFilepath,
|
|
415
|
+
}: {
|
|
416
|
+
includeWorktreeBootstrap: boolean;
|
|
417
|
+
projectFilepath: string;
|
|
418
|
+
sourceFilepath: string;
|
|
419
|
+
}) {
|
|
420
|
+
const sourcePath = normalizeProjectPathForGitignore(path.relative(path.dirname(projectFilepath), sourceFilepath));
|
|
421
|
+
const lines = [
|
|
422
|
+
...(includeWorktreeBootstrap ? [...newProteumWorktreeBootstrapBlock, ''] : []),
|
|
423
|
+
managedInstructionStubHeader,
|
|
424
|
+
'',
|
|
425
|
+
managedInstructionStubIntro,
|
|
426
|
+
'',
|
|
427
|
+
'Before reading or applying instructions from this file, read and follow the canonical Proteum instruction file at:',
|
|
428
|
+
'',
|
|
429
|
+
`\`${sourcePath}\``,
|
|
430
|
+
'',
|
|
431
|
+
'Resolve that path relative to this file. Treat the canonical file as if its full contents were written here.',
|
|
432
|
+
'',
|
|
433
|
+
'If the canonical file cannot be read, stop and run `npx proteum configure agents` before continuing.',
|
|
434
|
+
'',
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
return lines.join('\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
359
440
|
function inspectExistingPath({
|
|
360
441
|
managedSourceRoot,
|
|
361
442
|
projectFilepath,
|
|
362
443
|
sourceFilepath,
|
|
444
|
+
stubContent,
|
|
363
445
|
}: {
|
|
364
446
|
managedSourceRoot: string;
|
|
365
447
|
projectFilepath: string;
|
|
366
448
|
sourceFilepath: string;
|
|
449
|
+
stubContent: string;
|
|
367
450
|
}) {
|
|
368
451
|
if (!pathEntryExists(projectFilepath)) return { kind: 'missing' as const };
|
|
369
452
|
|
|
370
453
|
const stats = fs.lstatSync(projectFilepath);
|
|
371
|
-
if (!stats.isSymbolicLink())
|
|
454
|
+
if (!stats.isSymbolicLink()) {
|
|
455
|
+
if (!stats.isFile()) return { kind: 'blocked' as const };
|
|
456
|
+
|
|
457
|
+
const existingContent = fs.readFileSync(projectFilepath, 'utf8');
|
|
458
|
+
if (existingContent === stubContent) return { kind: 'match' as const };
|
|
459
|
+
if (isManagedInstructionStub(existingContent)) return { kind: 'managed-different' as const };
|
|
460
|
+
|
|
461
|
+
return { kind: 'blocked' as const };
|
|
462
|
+
}
|
|
372
463
|
|
|
373
464
|
const existingTarget = resolveSymlinkTarget(projectFilepath);
|
|
374
465
|
const normalizedExistingTarget = normalizeAbsolutePath(existingTarget);
|
|
375
466
|
const normalizedSourceFilepath = normalizeAbsolutePath(sourceFilepath);
|
|
376
467
|
const normalizedManagedSourceRoot = normalizeAbsolutePath(managedSourceRoot);
|
|
377
468
|
|
|
378
|
-
if (normalizedExistingTarget === normalizedSourceFilepath) return { kind: 'match' as const };
|
|
379
469
|
if (
|
|
470
|
+
normalizedExistingTarget === normalizedSourceFilepath ||
|
|
380
471
|
normalizedExistingTarget === normalizedManagedSourceRoot ||
|
|
381
472
|
normalizedExistingTarget.startsWith(`${normalizedManagedSourceRoot}/`)
|
|
382
473
|
)
|
|
@@ -385,15 +476,19 @@ function inspectExistingPath({
|
|
|
385
476
|
return { kind: 'blocked' as const };
|
|
386
477
|
}
|
|
387
478
|
|
|
479
|
+
function isManagedInstructionStub(content: string) {
|
|
480
|
+
return content.includes(`${managedInstructionStubHeader}\n\n${managedInstructionStubIntro}\n`);
|
|
481
|
+
}
|
|
482
|
+
|
|
388
483
|
function resolveSymlinkTarget(projectFilepath: string) {
|
|
389
484
|
const projectParentDir = path.dirname(projectFilepath);
|
|
390
485
|
const rawTarget = fs.readlinkSync(projectFilepath);
|
|
391
486
|
return path.resolve(projectParentDir, rawTarget);
|
|
392
487
|
}
|
|
393
488
|
|
|
394
|
-
function
|
|
395
|
-
result:
|
|
396
|
-
next:
|
|
489
|
+
function mergeInstructionResults(
|
|
490
|
+
result: TConfigureProjectAgentInstructionsResult,
|
|
491
|
+
next: TEnsureInstructionStubsResult,
|
|
397
492
|
rootDir: string,
|
|
398
493
|
) {
|
|
399
494
|
result.created.push(...next.created.map((entry) => formatResultPath(rootDir, entry)));
|
package/cli/utils/check.ts
CHANGED
|
@@ -2,10 +2,11 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
import cli from '..';
|
|
5
|
-
import Compiler from '../compiler';
|
|
6
5
|
import { runProcess } from './runProcess';
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const appConfigPaths = ['identity.config.ts', 'proteum.config.ts'];
|
|
8
|
+
const appTsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json', 'commands/tsconfig.json'];
|
|
9
|
+
const frameworkTsconfigPaths = ['client/app.tsconfig.json', 'server/app.tsconfig.json', 'cli/tsconfig.json'];
|
|
9
10
|
const eslintConfigPaths = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
|
|
10
11
|
|
|
11
12
|
const resolveInstalledBinary = (packageName: string, binName: string) => cli.paths.resolveBinary(packageName, binName);
|
|
@@ -15,6 +16,28 @@ const resolveExistingAppPaths = (paths: string[]) =>
|
|
|
15
16
|
.map((relativePath) => ({ relativePath, absolutePath: path.join(cli.paths.appRoot, relativePath) }))
|
|
16
17
|
.filter(({ absolutePath }) => fs.existsSync(absolutePath));
|
|
17
18
|
|
|
19
|
+
const isFrameworkCheckout = () => {
|
|
20
|
+
let packageJson: { name?: unknown } | undefined;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
packageJson = JSON.parse(fs.readFileSync(path.join(cli.paths.appRoot, 'package.json'), 'utf8'));
|
|
24
|
+
} catch {}
|
|
25
|
+
|
|
26
|
+
return packageJson?.name === 'proteum' && fs.existsSync(path.join(cli.paths.appRoot, 'cli', 'bin.js'));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const resolveTypecheckProjects = () => {
|
|
30
|
+
const appProjects = resolveExistingAppPaths(appTsconfigPaths);
|
|
31
|
+
if (appProjects.length > 0) return appProjects;
|
|
32
|
+
|
|
33
|
+
if (isFrameworkCheckout()) return resolveExistingAppPaths(frameworkTsconfigPaths);
|
|
34
|
+
|
|
35
|
+
return [];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const hasAppConfig = () =>
|
|
39
|
+
appConfigPaths.every((relativePath) => fs.existsSync(path.join(cli.paths.appRoot, relativePath)));
|
|
40
|
+
|
|
18
41
|
const getTypecheckEnv = () => {
|
|
19
42
|
const existingNodeOptions = process.env.NODE_OPTIONS ?? '';
|
|
20
43
|
|
|
@@ -26,16 +49,21 @@ const getTypecheckEnv = () => {
|
|
|
26
49
|
};
|
|
27
50
|
|
|
28
51
|
export const refreshGeneratedTypings = async () => {
|
|
52
|
+
const { default: Compiler } = await import('../compiler');
|
|
29
53
|
const compiler = new Compiler('dev');
|
|
30
54
|
|
|
31
55
|
await compiler.refreshGeneratedTypings();
|
|
32
56
|
};
|
|
33
57
|
|
|
34
58
|
export const runAppTypecheck = async () => {
|
|
35
|
-
const existingProjects =
|
|
59
|
+
const existingProjects = resolveTypecheckProjects();
|
|
36
60
|
|
|
37
61
|
if (existingProjects.length === 0)
|
|
38
|
-
throw new Error(
|
|
62
|
+
throw new Error(
|
|
63
|
+
`No TypeScript projects found. Expected one of: ${[...appTsconfigPaths, ...frameworkTsconfigPaths].join(
|
|
64
|
+
', ',
|
|
65
|
+
)}.`,
|
|
66
|
+
);
|
|
39
67
|
|
|
40
68
|
const tsc = resolveInstalledBinary('typescript', 'tsc');
|
|
41
69
|
|
package/docs/dev-sessions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dev Sessions
|
|
2
2
|
|
|
3
|
-
Proteum ships
|
|
3
|
+
Proteum ships dev-only auth bootstrap commands so `proteum verify browser`, `proteum e2e`, Playwright runs, and local debugging can start from an authenticated state without driving the login UI.
|
|
4
4
|
|
|
5
5
|
## When To Use It
|
|
6
6
|
|
|
@@ -31,10 +31,18 @@ proteum session admin@example.com --role ADMIN --port 3101
|
|
|
31
31
|
proteum session god@example.com --role GOD --url http://localhost:3102 --json
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Playwright wrapper mode:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/admin.spec.ts
|
|
38
|
+
proteum e2e --url http://localhost:3101 --env FEATURE_FLAG=true --grep smoke
|
|
39
|
+
```
|
|
40
|
+
|
|
34
41
|
Behavior:
|
|
35
42
|
|
|
36
43
|
- local mode refreshes generated artifacts, builds the dev output, starts a temporary local dev server, creates the session, prints the payload, and exits
|
|
37
44
|
- remote mode talks to an already running `proteum dev` instance
|
|
45
|
+
- `proteum e2e` talks to an already running `proteum dev` instance when `--session-email` is present, then starts Playwright with `E2E_BASE_URL`, optional `E2E_PORT`, optional `E2E_AUTH_TOKEN`, and any explicit `--env` or `--env-file` values in the child process environment
|
|
38
46
|
- the command requires an explicit email and optionally asserts a role before returning the session
|
|
39
47
|
- the command is available only in dev mode
|
|
40
48
|
- browser verification flows should keep browser state app-local and disposable through `proteum verify browser` or direct Playwright instead of reusing a shared temp profile
|
|
@@ -68,7 +76,7 @@ curl -H "$(jq -r '.curlCookieHeader' session.json)" http://localhost:3101/api/Au
|
|
|
68
76
|
## Agent Guidance
|
|
69
77
|
|
|
70
78
|
- Prefer `proteum session` over UI login automation when the goal is to test or debug protected application behavior.
|
|
71
|
-
- Prefer `proteum verify browser` for focused browser-visible verification. When lower-level control is required, use direct Playwright with a disposable profile.
|
|
79
|
+
- Prefer `proteum verify browser` for focused browser-visible verification, and `proteum e2e --port <port>` for targeted or full Playwright suites. When lower-level control is required, use direct Playwright with a disposable profile.
|
|
72
80
|
- Use UI login automation only when the auth UX itself is the feature under test.
|
|
73
81
|
- Pair it with `proteum diagnose` for a fast protected-route summary, `proteum perf request` for a one-request timing breakdown, then use `proteum trace` when you need lower-level request events.
|
|
74
82
|
- Only the final verifier agent should usually run browser flows. Earlier agents should stay on `orient`, `verify owner`, `verify request`, and request-level diagnostics unless browser execution is required.
|
|
@@ -78,6 +86,7 @@ Typical flow:
|
|
|
78
86
|
```bash
|
|
79
87
|
proteum orient /dashboard
|
|
80
88
|
proteum session admin@example.com --role ADMIN --port 3101 --json > session.json
|
|
89
|
+
proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/dashboard.spec.ts
|
|
81
90
|
proteum diagnose /dashboard --hit /dashboard --port 3101
|
|
82
91
|
proteum perf request /dashboard --port 3101
|
|
83
92
|
proteum trace latest --port 3101
|
package/docs/diagnostics.md
CHANGED
|
@@ -53,6 +53,7 @@ proteum diagnose /api/Auth/CurrentUser --url http://127.0.0.1:3101
|
|
|
53
53
|
proteum verify owner /api/Auth/CurrentUser
|
|
54
54
|
proteum verify request /dashboard --port 3101
|
|
55
55
|
proteum verify browser /dashboard --port 3101 --session-email admin@example.com --session-role ADMIN
|
|
56
|
+
proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/dashboard.spec.ts
|
|
56
57
|
|
|
57
58
|
proteum perf top --since today
|
|
58
59
|
proteum perf request /dashboard --port 3101
|
|
@@ -210,7 +211,7 @@ For AI coding agents or automation:
|
|
|
210
211
|
2. Read `./.proteum/manifest.json` or run `proteum explain --json` only after you know which surface matters.
|
|
211
212
|
3. Run `proteum doctor --json` and `proteum doctor --contracts --json` to inspect framework and generated-artifact diagnostics.
|
|
212
213
|
4. Use `proteum verify owner <query>` or `proteum diagnose <path> --port <port>` for the smallest trustworthy runtime surface before broad checks.
|
|
213
|
-
5. Use `proteum verify browser` for browser-visible verification,
|
|
214
|
+
5. Use `proteum verify browser` for browser-visible verification, or `proteum e2e --port <port>` for targeted/full Playwright suites. Only drop to direct Playwright when the Proteum wrapper cannot express the needed control. Keep auth sourced from Proteum session helpers, and reserve browser flows for the final verifier agent unless they are the only trustworthy surface.
|
|
214
215
|
6. For performance, CPU, SQL, render, cache, or connected-boundary questions, use `proteum perf request <requestId|path>` against the same running dev server.
|
|
215
216
|
7. Use `proteum trace ...` when you need lower-level event detail than `diagnose` or `perf` provides.
|
|
216
217
|
8. Run global checks second, not first. Unrelated diagnostics should remain visible but non-blocking during focused verification unless strict global mode is required.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.6",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
|
|
8
8
|
// Core
|
|
9
|
-
import {
|
|
9
|
+
import { configureProjectAgentInstructions } from '../cli/utils/agents';
|
|
10
10
|
|
|
11
11
|
/*----------------------------------
|
|
12
12
|
- TYPES
|
|
@@ -31,5 +31,5 @@ for (const projectRoot of projectRoots) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
console.log(`[update-codex-agents] Syncing project Codex assets in ${projectRoot}`);
|
|
34
|
-
|
|
34
|
+
configureProjectAgentInstructions({ appRoot: projectRoot, coreRoot: proteumRoot });
|
|
35
35
|
}
|