proteum 2.2.2-1 → 2.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/AGENTS.md +2 -2
- package/README.md +4 -1
- package/agents/project/AGENTS.md +3 -3
- package/agents/project/diagnostics.md +3 -3
- package/agents/project/root/AGENTS.md +3 -3
- package/agents/project/tests/AGENTS.md +2 -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/cli/runtime/commands.ts
CHANGED
|
@@ -259,6 +259,63 @@ class CheckCommand extends ProteumCommand {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
class E2eCommand extends ProteumCommand {
|
|
263
|
+
public static paths = [['e2e']];
|
|
264
|
+
|
|
265
|
+
public static usage = buildUsage('e2e');
|
|
266
|
+
|
|
267
|
+
public cwd = Option.String('--cwd', { description: 'Run Playwright against another Proteum app root.' });
|
|
268
|
+
public port = Option.String('--port', { description: 'Set E2E_BASE_URL from a local router port.' });
|
|
269
|
+
public url = Option.String('--url', { description: 'Set E2E_BASE_URL from an explicit base URL.' });
|
|
270
|
+
public sessionEmail = Option.String('--session-email', {
|
|
271
|
+
description: 'Mint a dev session before Playwright starts and pass it as E2E_AUTH_TOKEN.',
|
|
272
|
+
});
|
|
273
|
+
public sessionRole = Option.String('--session-role', { description: 'Require the dev session user to have this role.' });
|
|
274
|
+
public env = Option.Array('--env', [], { description: 'Pass an environment value to Playwright as KEY=value.' });
|
|
275
|
+
public envFile = Option.Array('--env-file', [], { description: 'Load environment values from a dotenv file before Playwright starts.' });
|
|
276
|
+
public config = Option.String('--config', { description: 'Playwright config file.' });
|
|
277
|
+
public debug = Option.Boolean('--debug', false, { description: 'Run Playwright in debug mode.' });
|
|
278
|
+
public grep = Option.String('--grep', { description: 'Playwright grep filter.' });
|
|
279
|
+
public headed = Option.Boolean('--headed', false, { description: 'Run browsers in headed mode.' });
|
|
280
|
+
public list = Option.Boolean('--list', false, { description: 'List Playwright tests without running them.' });
|
|
281
|
+
public project = Option.Array('--project', [], { description: 'Playwright project name. Can be repeated.' });
|
|
282
|
+
public reporter = Option.String('--reporter', { description: 'Playwright reporter.' });
|
|
283
|
+
public retries = Option.String('--retries', { description: 'Playwright retry count.' });
|
|
284
|
+
public timeout = Option.String('--timeout', { description: 'Playwright per-test timeout.' });
|
|
285
|
+
public ui = Option.Boolean('--ui', false, { description: 'Run Playwright in UI mode.' });
|
|
286
|
+
public workers = Option.String('--workers', { description: 'Playwright worker count.' });
|
|
287
|
+
public specs = Option.Rest();
|
|
288
|
+
|
|
289
|
+
public async execute() {
|
|
290
|
+
const playwrightArgs = [
|
|
291
|
+
...(this.config ? ['--config', this.config] : []),
|
|
292
|
+
...(this.debug ? ['--debug'] : []),
|
|
293
|
+
...(this.grep ? ['--grep', this.grep] : []),
|
|
294
|
+
...(this.headed ? ['--headed'] : []),
|
|
295
|
+
...(this.list ? ['--list'] : []),
|
|
296
|
+
...this.project.flatMap((project) => ['--project', project]),
|
|
297
|
+
...(this.reporter ? ['--reporter', this.reporter] : []),
|
|
298
|
+
...(this.retries ? ['--retries', this.retries] : []),
|
|
299
|
+
...(this.timeout ? ['--timeout', this.timeout] : []),
|
|
300
|
+
...(this.ui ? ['--ui'] : []),
|
|
301
|
+
...(this.workers ? ['--workers', this.workers] : []),
|
|
302
|
+
...this.specs,
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
this.setCliArgs({
|
|
306
|
+
env: this.env,
|
|
307
|
+
envFile: this.envFile,
|
|
308
|
+
playwrightArgs,
|
|
309
|
+
port: this.port ?? '',
|
|
310
|
+
sessionEmail: this.sessionEmail ?? '',
|
|
311
|
+
sessionRole: this.sessionRole ?? '',
|
|
312
|
+
url: this.url ?? '',
|
|
313
|
+
workdir: this.cwd ?? '',
|
|
314
|
+
});
|
|
315
|
+
return await runCommandModule(() => import('../commands/e2e'));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
262
319
|
class ConnectCommand extends ProteumCommand {
|
|
263
320
|
public static paths = [['connect']];
|
|
264
321
|
|
|
@@ -608,6 +665,7 @@ export const registeredCommands = {
|
|
|
608
665
|
typecheck: TypecheckCommand,
|
|
609
666
|
lint: LintCommand,
|
|
610
667
|
check: CheckCommand,
|
|
668
|
+
e2e: E2eCommand,
|
|
611
669
|
connect: ConnectCommand,
|
|
612
670
|
doctor: DoctorCommand,
|
|
613
671
|
explain: ExplainCommand,
|
|
@@ -640,6 +698,7 @@ export const createCli = (version: string) => {
|
|
|
640
698
|
clipanion.register(TypecheckCommand);
|
|
641
699
|
clipanion.register(LintCommand);
|
|
642
700
|
clipanion.register(CheckCommand);
|
|
701
|
+
clipanion.register(E2eCommand);
|
|
643
702
|
clipanion.register(ConnectCommand);
|
|
644
703
|
clipanion.register(DoctorCommand);
|
|
645
704
|
clipanion.register(ExplainCommand);
|
package/cli/scaffold/index.ts
CHANGED
|
@@ -727,7 +727,7 @@ export const runInitScaffold = async () => {
|
|
|
727
727
|
? 'Run `npm run dev` in the new app directory.'
|
|
728
728
|
: 'Run `npm install`, then `npm run dev` in the new app directory.',
|
|
729
729
|
);
|
|
730
|
-
result.nextSteps.push('Run `proteum configure agents` when you want Proteum-managed instruction
|
|
730
|
+
result.nextSteps.push('Run `proteum configure agents` when you want Proteum-managed instruction stubs.');
|
|
731
731
|
result.nextSteps.push('Use `proteum create page|controller|command|route|service ...` to add app artifacts.');
|
|
732
732
|
|
|
733
733
|
printResult(result, createInitSummary(result, config));
|
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
|
|