proteum 2.2.6 → 2.2.8

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.
Files changed (42) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +4 -4
  3. package/agents/project/AGENTS.md +2 -1
  4. package/agents/project/app-root/AGENTS.md +1 -1
  5. package/agents/project/diagnostics.md +1 -1
  6. package/agents/project/root/AGENTS.md +2 -1
  7. package/cli/commands/configure.ts +14 -35
  8. package/cli/commands/dev.ts +105 -52
  9. package/cli/compiler/artifacts/controllers.ts +30 -4
  10. package/cli/compiler/artifacts/manifest.ts +1 -5
  11. package/cli/compiler/artifacts/services.ts +67 -29
  12. package/cli/presentation/commands.ts +9 -9
  13. package/cli/presentation/help.ts +1 -1
  14. package/cli/scaffold/index.ts +2 -5
  15. package/cli/scaffold/templates.ts +3 -10
  16. package/cli/utils/agents.ts +281 -199
  17. package/client/dev/profiler/ApexChart.tsx +4 -3
  18. package/common/dev/serverHotReload.ts +26 -25
  19. package/package.json +1 -1
  20. package/server/app/commands.ts +11 -16
  21. package/server/app/commandsManager.ts +5 -1
  22. package/server/app/controller/index.ts +68 -16
  23. package/server/app/devCommands.ts +3 -3
  24. package/server/app/devDiagnostics.ts +2 -2
  25. package/server/app/index.ts +19 -8
  26. package/server/app/service/container.ts +22 -19
  27. package/server/app/service/index.ts +33 -13
  28. package/server/app.tsconfig.json +0 -1
  29. package/server/services/auth/index.ts +12 -6
  30. package/server/services/auth/router/index.ts +12 -14
  31. package/server/services/auth/router/request.ts +34 -13
  32. package/server/services/disks/driver.ts +1 -1
  33. package/server/services/disks/index.ts +11 -8
  34. package/server/services/email/index.ts +1 -1
  35. package/server/services/prisma/Facet.ts +6 -5
  36. package/server/services/router/index.ts +8 -7
  37. package/server/services/router/request/validation/zod.ts +2 -0
  38. package/server/services/router/response/index.ts +9 -9
  39. package/server/services/router/service.ts +12 -8
  40. package/tests/agents-utils.test.cjs +207 -0
  41. package/tests/dev-transpile-watch.test.cjs +513 -0
  42. package/types/global/vendors.d.ts +70 -0
@@ -11,32 +11,30 @@ import {
11
11
  TAnyRoute,
12
12
  RouterService,
13
13
  TAnyRouter,
14
+ TServerRouter,
14
15
  } from '@server/services/router';
15
16
 
16
17
  import type { Application } from '@server/app/index';
18
+ import AppContainer from '@server/app/container';
17
19
 
18
20
  import type { TRouterServiceArgs } from '@server/services/router/service';
19
21
 
20
22
  // Specific
21
23
  import type { default as UsersService, TAuthCheckConditions, TBasicUser } from '..';
22
- import UsersRequestService from './request';
23
-
24
- /*----------------------------------
25
- - TYPES
26
- ----------------------------------*/
24
+ import { createUsersRequestService, type TUsersRequestContext } from './request';
27
25
 
28
26
  /*----------------------------------
29
27
  - SERVICE
30
28
  ----------------------------------*/
31
29
  export default class AuthenticationRouterService<
32
- TApplication extends Application = Application,
33
- TUser extends TBasicUser = TApplication['app']['userType'],
34
- TRouter extends TAnyRouter = TAnyRouter,
30
+ TApplication extends Application,
31
+ TUser extends TBasicUser,
32
+ TRouter extends TAnyRouter = TServerRouter,
35
33
  TRequest extends ServerRequest<TRouter> = ServerRequest<TRouter>,
36
34
  > extends RouterService<
37
35
  { users: UsersService<TUser, TApplication> },
38
36
  TRouter,
39
- UsersRequestService<TRouter, TUser, TRequest>
37
+ TUsersRequestContext<TUser>
40
38
  > {
41
39
  /*----------------------------------
42
40
  - LIFECYCLE
@@ -45,7 +43,7 @@ export default class AuthenticationRouterService<
45
43
  public users: UsersService<TUser, TApplication>;
46
44
 
47
45
  public constructor(
48
- getConfig: TRouterServiceArgs<{ users: UsersService<TUser, TApplication> }, TRouter>[0],
46
+ getConfig: TRouterServiceArgs<{ users: UsersService<TUser, TApplication> }>[0],
49
47
  app: TApplication,
50
48
  ) {
51
49
  super(getConfig, app);
@@ -59,11 +57,11 @@ export default class AuthenticationRouterService<
59
57
  details: Record<string, any>,
60
58
  minimumCapture: 'summary' | 'resolve' | 'deep' = 'resolve',
61
59
  ) {
62
- this.app.container.Trace.record(
60
+ AppContainer.Trace.record(
63
61
  request.id,
64
62
  'auth.route',
65
63
  {
66
- routePath: route.path || '',
64
+ routePath: 'path' in route ? route.path || '' : '',
67
65
  routeId: route.options.id || '',
68
66
  authInput: route.options.auth ?? null,
69
67
  tracking: route.options.authTracking ?? null,
@@ -219,7 +217,7 @@ export default class AuthenticationRouterService<
219
217
  - ROUTER SERVICE LIFECYCLE
220
218
  ----------------------------------*/
221
219
 
222
- public requestService(request: TRequest): UsersRequestService<TRouter, TUser, TRequest> {
223
- return new UsersRequestService(request, this);
220
+ public requestService(request: TRequest): TUsersRequestContext<TUser> {
221
+ return createUsersRequestService(request, this.users);
224
222
  }
225
223
  }
@@ -2,13 +2,11 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
- // Core
6
- import type { Request as ServerRequest, TAnyRouter } from '@server/services/router';
7
- import RequestService from '@server/services/router/request/service';
5
+ import type { Application } from '@server/app/index';
8
6
 
9
7
  // Specific
10
- import type AuthenticationRouterService from '.';
11
- import type { TAuthCheckConditions, TAuthTrackingContext, TUserRole } from '..';
8
+ import type UsersService from '..';
9
+ import type { TAuthCheckConditions, TAuthRequest, TAuthTrackingContext, TUserRole } from '..';
12
10
 
13
11
  // Types
14
12
  import type { TBasicUser } from '@server/services/auth';
@@ -17,21 +15,36 @@ import type { TBasicUser } from '@server/services/auth';
17
15
  - TYPES
18
16
  ----------------------------------*/
19
17
 
18
+ type TUsersRouterService<TUser extends TBasicUser> = {
19
+ users: UsersService<TUser, Application>;
20
+ };
21
+
22
+ export interface TUsersRequestContext<TUser extends TBasicUser> {
23
+ login(email: string): Promise<unknown>;
24
+ logout(): void;
25
+
26
+ check(): TUser;
27
+ check(conditions: null, tracking?: TAuthTrackingContext): TUser;
28
+ check(conditions: TAuthCheckConditions, tracking?: TAuthTrackingContext): TUser;
29
+ check(conditions: false, tracking?: TAuthTrackingContext): null;
30
+ check(role: TUserRole, feature: null): TUser;
31
+ check(role: false): null;
32
+ check(role: TUserRole | true, feature: FeatureKeys, action?: string): TUser;
33
+ check(role: false, feature: FeatureKeys, action?: string): null;
34
+ }
35
+
20
36
  /*----------------------------------
21
37
  - MODULE
22
38
  ----------------------------------*/
23
39
  export default class UsersRequestService<
24
- TRouter extends TAnyRouter,
25
40
  TUser extends TBasicUser,
26
- TRequest extends ServerRequest<TRouter> = ServerRequest<TRouter>,
27
- > extends RequestService<TRequest> {
41
+ TRequest extends TAuthRequest,
42
+ > implements TUsersRequestContext<TUser> {
28
43
  public constructor(
29
- request: TRequest,
30
- public auth: AuthenticationRouterService<TRouter['app'], TUser, TRouter, TRequest>,
44
+ public request: TRequest,
45
+ public auth: TUsersRouterService<TUser>,
31
46
  public users = auth.users,
32
- ) {
33
- super(request);
34
- }
47
+ ) {}
35
48
 
36
49
  public login(email: string) {
37
50
  if (!this.users.login) throw new Error('The current auth service does not implement login().');
@@ -93,3 +106,11 @@ export default class UsersRequestService<
93
106
  return this.users.check(this.request, roleOrConditions, featureOrTracking, action);
94
107
  }
95
108
  }
109
+
110
+ export const createUsersRequestService = <
111
+ TUser extends TBasicUser,
112
+ TRequest extends TAuthRequest,
113
+ >(
114
+ request: TRequest,
115
+ users: UsersService<TUser, Application>,
116
+ ): TUsersRequestContext<TUser> => new UsersRequestService<TUser, TRequest>(request, { users });
@@ -27,7 +27,7 @@ export type TDrivercnfig = {
27
27
 
28
28
  export type SourceFile = { name: string; path: string; modified: number; parentFolder: string; source: string };
29
29
 
30
- export type TOutputFileOptions = { encoding: string };
30
+ export type TOutputFileOptions = { encoding: BufferEncoding };
31
31
 
32
32
  export type TReadFileOptions = { encoding?: 'string' | 'buffer'; withMetas?: boolean };
33
33
 
@@ -32,8 +32,6 @@ export default class DisksManager<
32
32
  TConfig extends Config & { default: keyof MountpointList & string; drivers: MountpointList },
33
33
  TApplication extends Application,
34
34
  > extends Service<TConfig, Hooks, TApplication, TApplication> {
35
- public default!: MountpointList[keyof MountpointList & string];
36
-
37
35
  /*----------------------------------
38
36
  - LIFECYCLE
39
37
  ----------------------------------*/
@@ -50,10 +48,14 @@ export default class DisksManager<
50
48
  drivers[driverId].parent = this;
51
49
  }*/
52
50
 
53
- const defaultDisk = drivers[this.config.default];
54
- if (defaultDisk === undefined) console.log(`Default disk "${this.config.default as string}" not mounted.`);
51
+ this.default;
52
+ }
55
53
 
56
- this.default = defaultDisk;
54
+ public get default(): Driver {
55
+ const drivers: Services = this.config.drivers;
56
+ const defaultDisk = drivers[this.config.default];
57
+ if (defaultDisk === undefined) throw new Error(`Default disk "${String(this.config.default)}" not mounted.`);
58
+ return defaultDisk;
57
59
  }
58
60
 
59
61
  public async shutdown() {}
@@ -62,13 +64,14 @@ export default class DisksManager<
62
64
  - LIFECYCLE
63
65
  ----------------------------------*/
64
66
 
65
- public get(diskName?: 'default' | keyof MountpointList) {
67
+ public get(diskName?: 'default' | keyof MountpointList): Driver {
68
+ const drivers: Services = this.config.drivers;
66
69
  const disk =
67
70
  diskName === 'default' || diskName === undefined
68
71
  ? this.default
69
- : this.config.drivers[diskName as keyof MountpointList];
72
+ : drivers[String(diskName)];
70
73
 
71
- if (disk === undefined) throw new Error(`Disk "${diskName as string}" not found.`);
74
+ if (disk === undefined) throw new Error(`Disk "${String(diskName)}" not found.`);
72
75
 
73
76
  return disk;
74
77
  }
@@ -57,7 +57,7 @@ type TOptions = { transporter?: string };
57
57
  /*----------------------------------
58
58
  - FONCTIONS
59
59
  ----------------------------------*/
60
- export default abstract class Email<TConfig extends Config, TApplication extends Application = Application> extends Service<
60
+ export default abstract class Email<TConfig extends Config, TApplication extends Application> extends Service<
61
61
  TConfig,
62
62
  Hooks,
63
63
  TApplication,
@@ -1,9 +1,10 @@
1
- import type { PrismaClient } from '@models/types';
2
-
3
1
  export type TDelegate<R = unknown> = {
4
2
  findMany(args?: Record<string, unknown>): Promise<R[]>;
5
3
  findFirst(args?: Record<string, unknown>): Promise<R | null>;
6
4
  };
5
+ type TPrismaRawClient = {
6
+ $queryRawUnsafe(query: string): Promise<Record<string, unknown>[]>;
7
+ };
7
8
 
8
9
  export type TWithStats = { $table: string; $key: string } & Record<string, string>;
9
10
 
@@ -18,7 +19,7 @@ export default class Facet<
18
19
  RT = R,
19
20
  > {
20
21
  constructor(
21
- private readonly prisma: PrismaClient,
22
+ private readonly prisma: TPrismaRawClient,
22
23
  private readonly delegate: D,
23
24
  private readonly subset: S,
24
25
  private readonly transform?: Transform<R, RT>,
@@ -57,13 +58,13 @@ export default class Facet<
57
58
  ), 0)) as ${key}`,
58
59
  );
59
60
 
60
- const statRows = (await this.prisma.$queryRawUnsafe(`
61
+ const statRows = await this.prisma.$queryRawUnsafe(`
61
62
  SELECT ${$key}, ${select.join(', ')}
62
63
  FROM ${$table}
63
64
  WHERE ${$key} IN (
64
65
  ${(results as Array<Record<string, unknown>>).map((row) => "'" + row[$key] + "'").join(',')}
65
66
  )
66
- `)) as Record<string, unknown>[];
67
+ `);
67
68
 
68
69
  for (const stat of statRows) {
69
70
  for (const key in stat) {
@@ -19,7 +19,8 @@ import zod, { ZodError } from 'zod';
19
19
  export { default as schema } from 'zod';
20
20
 
21
21
  // Core
22
- import Service, { AnyService, TServiceArgs } from '@server/app/service';
22
+ import type { Application } from '@server/app/index';
23
+ import Service, { TServiceArgs } from '@server/app/service';
23
24
  import context from '@server/context';
24
25
  import type DisksManager from '@server/services/disks';
25
26
  import { CoreError, InputError, NotFound, toJson as errorToJson } from '@common/errors';
@@ -104,16 +105,16 @@ const staticHtmlCacheControl = 'public, max-age=0, must-revalidate';
104
105
  ----------------------------------*/
105
106
 
106
107
  export type TAnyRouter = ServerRouter<
107
- AnyService['app'],
108
+ Application,
108
109
  TRouterServicesList,
109
- Config<TRouterServicesList, AnyService['app']>
110
+ Config<TRouterServicesList, Application>
110
111
  >;
111
112
 
112
113
  const LogPrefix = '[router]';
113
114
 
114
115
  export type Config<
115
116
  TServices extends TRouterServicesList,
116
- TApplication extends AnyService['app'] = AnyService['app'],
117
+ TApplication extends Application = Application,
117
118
  > = {
118
119
  debug: boolean;
119
120
 
@@ -147,16 +148,16 @@ export type TControllerDefinition = {
147
148
  };
148
149
 
149
150
  export type TServerRouter = ServerRouter<
150
- AnyService['app'],
151
+ Application,
151
152
  TRouterServicesList,
152
- Config<TRouterServicesList, AnyService['app']>
153
+ Config<TRouterServicesList, Application>
153
154
  >;
154
155
 
155
156
  /*----------------------------------
156
157
  - CLASSE
157
158
  ----------------------------------*/
158
159
  export default class ServerRouter<
159
- TApplication extends AnyService['app'] = AnyService['app'],
160
+ TApplication extends Application = Application,
160
161
  TServices extends TRouterServicesList = TRouterServicesList,
161
162
  TConfig extends Config<TServices, TApplication> = Config<TServices, TApplication>,
162
163
  >
@@ -4,6 +4,8 @@ import zod from 'zod';
4
4
  export type TRichTextValidatorOptions = { attachements?: boolean };
5
5
  export type TValidationSchema = zod.ZodTypeAny;
6
6
  export type TValidationShape = zod.ZodRawShape;
7
+ export type TInferValidationSchema<TSchema extends TValidationSchema> = zod.infer<TSchema>;
8
+ export type TTypedValidationSchema<TOutput> = zod.ZodType<TOutput>;
7
9
 
8
10
  type TChoiceOption = { value: PrimitiveValue; label: string };
9
11
 
@@ -12,7 +12,7 @@ import express from 'express';
12
12
 
13
13
  // Core
14
14
  import context from '@server/context';
15
- import type { AnyRouterService, default as ServerRouter, TServerRouter, TAnyRouter } from '@server/services/router';
15
+ import type { default as ServerRouter, TServerRouter, TAnyRouter } from '@server/services/router';
16
16
  import ServerRequest from '@server/services/router/request';
17
17
  import { TMatchedRoute, TRoute, TAnyRoute } from '@common/router';
18
18
  import { NotFound, Forbidden, Anomaly } from '@common/errors';
@@ -40,8 +40,7 @@ export type TBasicSSrData = {
40
40
  currentDomain: string;
41
41
  };
42
42
 
43
- type TServerRouterApplication<TRouter extends TServerRouter> =
44
- TRouter extends ServerRouter<infer TApplication, any, any> ? TApplication : never;
43
+ type TServerRouterApplication<TRouter extends TServerRouter> = TRouter['app'];
45
44
 
46
45
  type TServerRouterPlugins<TRouter extends TServerRouter> =
47
46
  TRouter extends ServerRouter<any, any, infer TConfig>
@@ -80,8 +79,10 @@ export type TRouterContextServices<
80
79
  // Custom context via servuces
81
80
  // For each roiuter service, return the request service (returned by roiuterService.requestService() )
82
81
  {
83
- [serviceName in keyof TPlugins]: TPlugins[serviceName] extends AnyRouterService
84
- ? Exclude<ReturnType<TPlugins[serviceName]['requestService']>, null | undefined>
82
+ [serviceName in keyof TPlugins]: TPlugins[serviceName] extends { requestService: infer TRequestServiceMethod }
83
+ ? TRequestServiceMethod extends (...args: infer TRequestServiceArgs) => infer TRequestService
84
+ ? Exclude<TRequestService, null | undefined>
85
+ : TPlugins[serviceName]
85
86
  : TPlugins[serviceName];
86
87
  };
87
88
 
@@ -97,7 +98,6 @@ const getRouteTraceTarget = (route: TAnyRoute<TRouterContext<TServerRouter>>) =>
97
98
  ----------------------------------*/
98
99
  export default class ServerResponse<
99
100
  TRouter extends TAnyRouter,
100
- TRequestContext extends TRouterContext<TRouter> = TRouterContext<TRouter>,
101
101
  TData extends TResponseData = TResponseData,
102
102
  > extends BaseResponse<TData, ServerRequest<TRouter>> {
103
103
  // Services
@@ -220,13 +220,13 @@ export default class ServerResponse<
220
220
  ----------------------------------*/
221
221
 
222
222
  // Start controller services
223
- private async createContext(route: TAnyRoute<TRouterContext<TRouter>>): Promise<TRequestContext> {
223
+ private async createContext(route: TAnyRoute<TRouterContext<TRouter>>): Promise<TRouterContext<TRouter>> {
224
224
  const contextServices = this.router.createContextServices(this.request);
225
225
  const controllers = createControllers(this.request.api);
226
226
  const customSsrData = this.router.config.context(this.request, this.app) as TRouterRequestContext<TRouter>;
227
227
 
228
228
  // TODO: transmiss safe data (especially for Router), as Router info could be printed on client side
229
- const requestContext = {
229
+ const requestContext: TRouterContext<TRouter> = {
230
230
  // Router context
231
231
  app: this.app,
232
232
  context: undefined!,
@@ -242,7 +242,7 @@ export default class ServerResponse<
242
242
  // Router services
243
243
  ...(contextServices as TRouterContextServices<TRouter>),
244
244
  ...customSsrData,
245
- } as TRequestContext;
245
+ };
246
246
 
247
247
  requestContext.context = requestContext;
248
248
 
@@ -7,15 +7,16 @@ import type { Application } from '@server/app/index';
7
7
  import Service, { TSetupConfig } from '@server/app/service';
8
8
 
9
9
  // Specific
10
- import type { default as Router } from '.';
11
10
  import type ServerRequest from './request';
12
11
  import type { TAnyRouter } from '.';
13
12
 
14
- export type AnyRouterService = RouterService<any, TAnyRouter, object | null>;
13
+ export type AnyRouterService = Service<{}, {}, Application, object> & {
14
+ requestService(request: object): object | null;
15
+ };
15
16
 
16
- export type TRouterServiceArgs<TConfig extends {} = {}, TRouter extends TAnyRouter = TAnyRouter> = [
17
+ export type TRouterServiceArgs<TConfig extends {} = {}> = [
17
18
  getConfig: TSetupConfig<TConfig> | null | undefined,
18
- app: TRouter['app'],
19
+ app: Application,
19
20
  ];
20
21
 
21
22
  /*----------------------------------
@@ -23,11 +24,14 @@ export type TRouterServiceArgs<TConfig extends {} = {}, TRouter extends TAnyRout
23
24
  ----------------------------------*/
24
25
  export default abstract class RouterService<
25
26
  TConfig extends {},
26
- TRouter extends TAnyRouter = TAnyRouter,
27
+ TRouter extends TAnyRouter,
27
28
  TRequestService extends object | null = object | null,
28
- > extends Service<TConfig, {}, TRouter['app'], TRouter> {
29
- public constructor(...[config, app]: TRouterServiceArgs<TConfig, TRouter>) {
30
- super(app as TRouter['app'] & TRouter, config, app);
29
+ > extends Service<TConfig, {}, Application, object> {
30
+ public declare parent: TRouter;
31
+ public declare app: TRouter extends { app: infer TApplication extends Application } ? TApplication : Application;
32
+
33
+ public constructor(...[config, app]: TRouterServiceArgs<TConfig>) {
34
+ super(app, config, app);
31
35
  }
32
36
 
33
37
  public abstract requestService(request: ServerRequest<TRouter>): TRequestService;
@@ -0,0 +1,207 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const test = require('node:test');
6
+
7
+ const coreRoot = path.resolve(__dirname, '..');
8
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
9
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
10
+ require('ts-node/register/transpile-only');
11
+
12
+ const { configureProjectAgentInstructions, resolveProjectAgentMonorepoRoot } = require('../cli/utils/agents.ts');
13
+
14
+ const writeFile = (filepath, content) => {
15
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
16
+ fs.writeFileSync(filepath, content);
17
+ };
18
+
19
+ const makeTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-agents-'));
20
+
21
+ const createCoreFixture = () => {
22
+ const root = makeTempRoot();
23
+ const agentsRoot = path.join(root, 'agents', 'project');
24
+
25
+ writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
26
+ writeFile(path.join(agentsRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- Style rule\n');
27
+ writeFile(path.join(agentsRoot, 'client', 'AGENTS.md'), '# Client Rules\n\n- Client rule\n');
28
+
29
+ return root;
30
+ };
31
+
32
+ const createAppFixture = () => {
33
+ const appRoot = makeTempRoot();
34
+
35
+ for (const dir of ['client/pages', 'server/routes', 'server/services', 'tests/e2e']) {
36
+ fs.mkdirSync(path.join(appRoot, dir), { recursive: true });
37
+ }
38
+
39
+ writeFile(
40
+ path.join(appRoot, '.gitignore'),
41
+ [
42
+ 'node_modules',
43
+ '# Proteum-managed instruction files',
44
+ '/AGENTS.md',
45
+ '/CODING_STYLE.md',
46
+ '# End Proteum-managed instruction files',
47
+ '/.proteum',
48
+ '',
49
+ ].join('\n'),
50
+ );
51
+
52
+ return appRoot;
53
+ };
54
+
55
+ test('standalone configure creates tracked instruction files with embedded corpus', () => {
56
+ const coreRoot = createCoreFixture();
57
+ const appRoot = createAppFixture();
58
+ const result = configureProjectAgentInstructions({ appRoot, coreRoot });
59
+ const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
60
+ const codingStyleContent = fs.readFileSync(path.join(appRoot, 'CODING_STYLE.md'), 'utf8');
61
+ const gitignoreContent = fs.readFileSync(path.join(appRoot, '.gitignore'), 'utf8');
62
+
63
+ assert.equal(result.blocked.length, 0);
64
+ assert.match(agentsContent, /^# Proteum Instructions/m);
65
+ assert.match(agentsContent, /<!-- proteum-instructions:start -->/);
66
+ assert.match(agentsContent, /## Source: AGENTS\.md/);
67
+ assert.match(agentsContent, /## Root Contract/);
68
+ assert.match(agentsContent, /## Source: CODING_STYLE\.md/);
69
+ assert.match(codingStyleContent, /## Source: client\/AGENTS\.md/);
70
+ assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
71
+ assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
72
+ assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
73
+ });
74
+
75
+ test('configure preserves project content outside the managed section', () => {
76
+ const coreRoot = createCoreFixture();
77
+ const appRoot = createAppFixture();
78
+
79
+ writeFile(
80
+ path.join(appRoot, 'AGENTS.md'),
81
+ [
82
+ '# Product Notes',
83
+ '',
84
+ 'Keep this product note.',
85
+ '',
86
+ '# Proteum Instructions',
87
+ '<!-- proteum-instructions:start -->',
88
+ '',
89
+ 'Old managed content.',
90
+ '',
91
+ '<!-- proteum-instructions:end -->',
92
+ '',
93
+ '# Local Footer',
94
+ '',
95
+ 'Keep this footer.',
96
+ '',
97
+ ].join('\n'),
98
+ );
99
+
100
+ configureProjectAgentInstructions({ appRoot, coreRoot });
101
+
102
+ const content = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
103
+ assert.match(content, /# Product Notes/);
104
+ assert.match(content, /Keep this product note\./);
105
+ assert.match(content, /## Source: CODING_STYLE\.md/);
106
+ assert.doesNotMatch(content, /Old managed content/);
107
+ assert.match(content, /# Local Footer/);
108
+ assert.match(content, /Keep this footer\./);
109
+ });
110
+
111
+ test('configure preserves project content around legacy managed stubs', () => {
112
+ const coreRoot = createCoreFixture();
113
+ const appRoot = createAppFixture();
114
+
115
+ writeFile(
116
+ path.join(appRoot, 'AGENTS.md'),
117
+ [
118
+ '## Product Bootstrap',
119
+ '',
120
+ 'Keep these local bootstrap notes.',
121
+ '',
122
+ '# Proteum Managed Instructions',
123
+ '',
124
+ 'This file is managed by `proteum configure agents`.',
125
+ '',
126
+ 'Before reading or applying instructions from this file, read and follow the canonical Proteum instruction file at:',
127
+ '',
128
+ '`node_modules/proteum/agents/project/AGENTS.md`',
129
+ '',
130
+ 'Resolve that path relative to this file. Treat the canonical file as if its full contents were written here.',
131
+ '',
132
+ 'If the canonical file cannot be read, stop and run `npx proteum configure agents` before continuing.',
133
+ '',
134
+ '## Local Footer',
135
+ '',
136
+ 'Keep this footer too.',
137
+ '',
138
+ ].join('\n'),
139
+ );
140
+
141
+ configureProjectAgentInstructions({ appRoot, coreRoot });
142
+
143
+ const content = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
144
+ assert.match(content, /## Product Bootstrap/);
145
+ assert.match(content, /Keep these local bootstrap notes\./);
146
+ assert.match(content, /# Proteum Instructions/);
147
+ assert.match(content, /## Source: CODING_STYLE\.md/);
148
+ assert.doesNotMatch(content, /# Proteum Managed Instructions/);
149
+ assert.doesNotMatch(content, /Before reading or applying instructions from this file/);
150
+ assert.match(content, /## Local Footer/);
151
+ assert.match(content, /Keep this footer too\./);
152
+ });
153
+
154
+ test('monorepo configure writes root and app instruction files', () => {
155
+ const coreRoot = createCoreFixture();
156
+ const monorepoRoot = makeTempRoot();
157
+ const appRoot = path.join(monorepoRoot, 'apps', 'product');
158
+
159
+ fs.mkdirSync(path.join(monorepoRoot, '.git'));
160
+ fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
161
+
162
+ const result = configureProjectAgentInstructions({ appRoot, coreRoot, monorepoRoot });
163
+
164
+ assert.equal(result.mode, 'monorepo');
165
+ assert.equal(resolveProjectAgentMonorepoRoot(appRoot), fs.realpathSync(monorepoRoot));
166
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Source: AGENTS\.md/);
167
+ assert.match(fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
168
+ });
169
+
170
+ test('configure migrates legacy managed symlinks to embedded files', () => {
171
+ const coreRoot = createCoreFixture();
172
+ const appRoot = createAppFixture();
173
+ const installedCoreRoot = createCoreFixture();
174
+ const target = path.join(installedCoreRoot, 'agents', 'project', 'AGENTS.md');
175
+ const linkPath = path.join(appRoot, 'AGENTS.md');
176
+
177
+ fs.symlinkSync(target, linkPath);
178
+
179
+ const result = configureProjectAgentInstructions({ appRoot, coreRoot });
180
+ const stats = fs.lstatSync(linkPath);
181
+ const content = fs.readFileSync(linkPath, 'utf8');
182
+
183
+ assert.equal(result.updated.some((entry) => entry.endsWith('/AGENTS.md')), true);
184
+ assert.equal(stats.isSymbolicLink(), false);
185
+ assert.match(content, /# Proteum Instructions/);
186
+ });
187
+
188
+ test('configure reports blocked paths unless overwrite is allowed', () => {
189
+ const coreRoot = createCoreFixture();
190
+ const appRoot = createAppFixture();
191
+ const blockedPath = path.join(appRoot, 'CODING_STYLE.md');
192
+
193
+ fs.mkdirSync(blockedPath);
194
+
195
+ const preview = configureProjectAgentInstructions({ appRoot, coreRoot, dryRun: true });
196
+ assert.equal(preview.blocked.some((entry) => entry.endsWith('/CODING_STYLE.md')), true);
197
+
198
+ const result = configureProjectAgentInstructions({
199
+ appRoot,
200
+ coreRoot,
201
+ overwriteBlockedPaths: [blockedPath],
202
+ });
203
+
204
+ assert.equal(result.overwritten.some((entry) => entry.endsWith('/CODING_STYLE.md')), true);
205
+ assert.equal(fs.lstatSync(blockedPath).isFile(), true);
206
+ assert.match(fs.readFileSync(blockedPath, 'utf8'), /## Source: AGENTS\.md/);
207
+ });