create-principles-disciple 1.130.3 → 1.131.0

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.
@@ -10,6 +10,7 @@ import { migrateLegacyExtensionBackups, reservePdBackupDestination, resolvePdBac
10
10
  import { resolveExtensionsDir, resolveUpdateLayout, resolvePluginDir, readCurrentVersion, } from '../utils/installed-layout.js';
11
11
  import { ActivationCompatibilityReadModel } from '@principles/core/runtime-v2';
12
12
  import { collectFileDepLinkSpecs } from '../utils/update-links.js';
13
+ import { updateMutationController, LEGACY_MUTATION_AUTHORITY, } from '../update/mutation-controller.js';
13
14
  /**
14
15
  * Legacy rule contract preflight (2026-08-19): refuse to swap the runtime
15
16
  * while an ACTIVE owner-approved rule still depends on a RuleHost contract
@@ -1081,10 +1082,19 @@ async function doInlineFullUpdate(workspaceDir) {
1081
1082
  // ---------------------------------------------------------------------------
1082
1083
  // Route handler
1083
1084
  // ---------------------------------------------------------------------------
1084
- export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1085
- const pluginDir = resolvePluginDir(workspaceDir);
1086
- // GET /check
1087
- if (subPath === '/check') {
1085
+ /**
1086
+ * PRI-659: the four mutation kinds below are the legacy console updater's
1087
+ * implementations, registered into the MutationController (ADR-0023/0024
1088
+ * migration boundary). handleUpdateRoute is now a thin dispatcher: it maps
1089
+ * the URL subPath to a mutation kind and lets the controller resolve the
1090
+ * authority. The implementation stays here verbatim (replace-then-delete —
1091
+ * no third updater, no logic duplication); when ReleaseManager matures it
1092
+ * registers under `release-manager` for the same kinds and this route layer
1093
+ * needs no further change.
1094
+ */
1095
+ function legacyCheckMutation(req, res, ctx) {
1096
+ return (async () => {
1097
+ const pluginDir = resolvePluginDir(ctx.workspaceDir);
1088
1098
  if (req.method !== 'GET') {
1089
1099
  sendMethodNotAllowed(res);
1090
1100
  return;
@@ -1111,10 +1121,11 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1111
1121
  catch (err) {
1112
1122
  sendError(res, 500, 'update_check_error', err instanceof Error ? err.message : 'Unknown error');
1113
1123
  }
1114
- return;
1115
- }
1116
- // POST /apply
1117
- if (subPath === '/apply') {
1124
+ })();
1125
+ }
1126
+ function legacyApplyMutation(req, res, ctx) {
1127
+ return (async () => {
1128
+ const pluginDir = resolvePluginDir(ctx.workspaceDir);
1118
1129
  if (req.method !== 'POST') {
1119
1130
  sendMethodNotAllowed(res);
1120
1131
  return;
@@ -1146,7 +1157,7 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1146
1157
  return;
1147
1158
  }
1148
1159
  // Path traversal validation
1149
- if (!validatePathInWorkspace(targetDir, workspaceDir)) {
1160
+ if (!validatePathInWorkspace(targetDir, ctx.workspaceDir)) {
1150
1161
  sendBadRequest(res, 'targetDir must be within workspace or extensions directory');
1151
1162
  return;
1152
1163
  }
@@ -1154,7 +1165,7 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1154
1165
  targetDir,
1155
1166
  mergeStrategy,
1156
1167
  createBackup,
1157
- }, workspaceDir);
1168
+ }, ctx.workspaceDir);
1158
1169
  sendSuccess(res, result);
1159
1170
  }
1160
1171
  catch (err) {
@@ -1164,10 +1175,11 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1164
1175
  }
1165
1176
  sendError(res, 500, 'update_apply_error', err instanceof Error ? err.message : 'Unknown error');
1166
1177
  }
1167
- return;
1168
- }
1169
- // POST /rollback
1170
- if (subPath === '/rollback') {
1178
+ })();
1179
+ }
1180
+ function legacyRollbackMutation(req, res, ctx) {
1181
+ return (async () => {
1182
+ const pluginDir = resolvePluginDir(ctx.workspaceDir);
1171
1183
  if (req.method !== 'POST') {
1172
1184
  sendMethodNotAllowed(res);
1173
1185
  return;
@@ -1189,15 +1201,15 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1189
1201
  return;
1190
1202
  }
1191
1203
  // Path traversal validation
1192
- if (!validatePathInWorkspace(targetDir, workspaceDir)) {
1204
+ if (!validatePathInWorkspace(targetDir, ctx.workspaceDir)) {
1193
1205
  sendBadRequest(res, 'targetDir must be within workspace or extensions directory');
1194
1206
  return;
1195
1207
  }
1196
- if (!validatePathInWorkspace(backupDir, workspaceDir)) {
1208
+ if (!validatePathInWorkspace(backupDir, ctx.workspaceDir)) {
1197
1209
  sendBadRequest(res, 'backupDir must be within the workspace, extensions directory, or PD backups directory');
1198
1210
  return;
1199
1211
  }
1200
- const result = await doRollbackUpdate({ targetDir, backupDir }, workspaceDir);
1212
+ const result = await doRollbackUpdate({ targetDir, backupDir }, ctx.workspaceDir);
1201
1213
  sendSuccess(res, result);
1202
1214
  }
1203
1215
  catch (err) {
@@ -1207,22 +1219,42 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1207
1219
  }
1208
1220
  sendError(res, 500, 'update_rollback_error', err instanceof Error ? err.message : 'Unknown error');
1209
1221
  }
1210
- return;
1211
- }
1212
- // POST /apply-full — inline tarball download + file copy (no external installer)
1213
- if (subPath === '/apply-full') {
1222
+ })();
1223
+ }
1224
+ function legacyApplyFullMutation(req, res, ctx) {
1225
+ return (async () => {
1214
1226
  if (req.method !== 'POST') {
1215
1227
  sendMethodNotAllowed(res);
1216
1228
  return;
1217
1229
  }
1218
1230
  try {
1219
- const result = await doInlineFullUpdate(workspaceDir);
1231
+ const result = await doInlineFullUpdate(ctx.workspaceDir);
1220
1232
  sendSuccess(res, result);
1221
1233
  }
1222
1234
  catch (err) {
1223
1235
  sendError(res, 500, 'update_apply_full_error', err instanceof Error ? err.message : 'Unknown error');
1224
1236
  }
1237
+ })();
1238
+ }
1239
+ // PRI-659: register the legacy authority (ADR-0024 D-1 fallback authority)
1240
+ // for every mutation kind. Registration is the ONLY coupling between this
1241
+ // implementation and the controller — no other module needs to know where
1242
+ // the legacy implementation lives.
1243
+ updateMutationController.register('check', { name: LEGACY_MUTATION_AUTHORITY, handler: legacyCheckMutation });
1244
+ updateMutationController.register('apply', { name: LEGACY_MUTATION_AUTHORITY, handler: legacyApplyMutation });
1245
+ updateMutationController.register('apply-full', { name: LEGACY_MUTATION_AUTHORITY, handler: legacyApplyFullMutation });
1246
+ updateMutationController.register('rollback', { name: LEGACY_MUTATION_AUTHORITY, handler: legacyRollbackMutation });
1247
+ const UPDATE_MUTATION_KINDS = new Map([
1248
+ ['/check', 'check'],
1249
+ ['/apply', 'apply'],
1250
+ ['/apply-full', 'apply-full'],
1251
+ ['/rollback', 'rollback'],
1252
+ ]);
1253
+ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
1254
+ const kind = UPDATE_MUTATION_KINDS.get(subPath);
1255
+ if (kind === undefined) {
1256
+ sendNotFound(res, `Update route not found: ${subPath}`);
1225
1257
  return;
1226
1258
  }
1227
- sendNotFound(res, `Update route not found: ${subPath}`);
1259
+ await updateMutationController.dispatch(req, res, { workspaceDir }, kind);
1228
1260
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Mutation Controller — PRI-659 migration boundary.
3
+ *
4
+ * Governed by:
5
+ * - ADR-0023 (PD Installation Architecture Decisions) — runtime is written
6
+ * only by sanctioned authorities.
7
+ * - ADR-0024 (PD Runtime Mutation Governance, D-1) — the Console Web updater
8
+ * must converge into a trigger/presentation layer over ReleaseManager; it
9
+ * must not remain an independent mutation authority long-term.
10
+ *
11
+ * Role:
12
+ * The controller is the SINGLE routing point between the HTTP surface
13
+ * (`/api/update/*`) and whichever authority actually performs the mutation.
14
+ * It owns:
15
+ * - the mutation-kind registry (check / apply / apply-full / rollback),
16
+ * - authority resolution with an explicit preferred authority
17
+ * (`release-manager`, ADR-0024 D-1) and a safe fallback
18
+ * (`legacy-console-updater`) until ReleaseManager leaves shadow mode,
19
+ * - observability: every dispatched response carries the
20
+ * `X-PD-Mutation-Authority` header so operators can see which
21
+ * authority served a mutation.
22
+ *
23
+ * Non-goals (deliberate):
24
+ * - The controller performs NO file mutation itself. It only routes. It is
25
+ * therefore not a new mutation authority (asserted by migration tests).
26
+ * - It does not wrap, copy, or replace the legacy updater implementation.
27
+ * The legacy implementation stays in `routes/update.ts` and registers
28
+ * itself here. When ReleaseManager matures, it registers under
29
+ * `RELEASE_MANAGER_AUTHORITY` for the same kinds and the route layer
30
+ * needs no further change.
31
+ */
32
+ import type { IncomingMessage, ServerResponse } from 'node:http';
33
+ /** Authority that exists today: the inline console updater (routes/update.ts). */
34
+ export declare const LEGACY_MUTATION_AUTHORITY: 'legacy-console-updater';
35
+ /** Authority designated by ADR-0024 D-1 as the long-term mutation authority. */
36
+ export declare const RELEASE_MANAGER_AUTHORITY: 'release-manager';
37
+ /**
38
+ * Preferred authority for every mutation kind. Until ReleaseManager exits
39
+ * shadow mode (its apply()/rollback() still refuse with
40
+ * `shadow_mode_read_only`) nothing registers under this name and resolution
41
+ * falls back to the legacy authority — preserving current capability
42
+ * (replace-then-delete, not delete-then-rebuild).
43
+ */
44
+ export declare const PREFERRED_MUTATION_AUTHORITY: "release-manager";
45
+ export type MutationKind = 'check' | 'apply' | 'apply-full' | 'rollback';
46
+ export declare const MUTATION_KINDS: readonly MutationKind[];
47
+ export interface MutationContext {
48
+ readonly workspaceDir: string;
49
+ }
50
+ export type MutationHandler = (req: IncomingMessage, res: ServerResponse, ctx: MutationContext) => Promise<void>;
51
+ export interface MutationAuthority {
52
+ readonly name: string;
53
+ readonly handler: MutationHandler;
54
+ }
55
+ export interface MutationGovernanceInfo {
56
+ readonly active: string;
57
+ readonly preferred: string;
58
+ readonly fallback: boolean;
59
+ readonly available: readonly string[];
60
+ }
61
+ export interface ResolvedAuthority {
62
+ readonly authority: MutationAuthority;
63
+ readonly fallback: boolean;
64
+ }
65
+ export declare class MutationController {
66
+ private readonly authorities;
67
+ register(kind: MutationKind, authority: MutationAuthority): void;
68
+ unregister(kind: MutationKind, name: string): boolean;
69
+ hasAuthority(kind: MutationKind, name: string): boolean;
70
+ /**
71
+ * Resolve which authority serves `kind`: the preferred authority when
72
+ * registered, otherwise the legacy authority as an explicit fallback.
73
+ * Throws when nothing is registered — an unregistered kind must fail loud,
74
+ * not silently mutate.
75
+ */
76
+ resolveAuthority(kind: MutationKind): ResolvedAuthority;
77
+ /**
78
+ * Route one mutation request. Response body contract is owned by the
79
+ * authority handler; the controller only annotates the response with the
80
+ * resolved authority so governance state is observable per request.
81
+ */
82
+ dispatch(req: IncomingMessage, res: ServerResponse, ctx: MutationContext, kind: MutationKind): Promise<void>;
83
+ /** Governance snapshot for observability and migration tests. */
84
+ describeGovernance(): Record<MutationKind, MutationGovernanceInfo>;
85
+ }
86
+ /**
87
+ * Process-wide controller for the `/api/update/*` surface. The legacy
88
+ * updater registers its handlers at module load of `routes/update.ts`.
89
+ */
90
+ export declare const updateMutationController: MutationController;
@@ -0,0 +1,89 @@
1
+ /** Authority that exists today: the inline console updater (routes/update.ts). */
2
+ export const LEGACY_MUTATION_AUTHORITY = 'legacy-console-updater';
3
+ /** Authority designated by ADR-0024 D-1 as the long-term mutation authority. */
4
+ export const RELEASE_MANAGER_AUTHORITY = 'release-manager';
5
+ /**
6
+ * Preferred authority for every mutation kind. Until ReleaseManager exits
7
+ * shadow mode (its apply()/rollback() still refuse with
8
+ * `shadow_mode_read_only`) nothing registers under this name and resolution
9
+ * falls back to the legacy authority — preserving current capability
10
+ * (replace-then-delete, not delete-then-rebuild).
11
+ */
12
+ export const PREFERRED_MUTATION_AUTHORITY = RELEASE_MANAGER_AUTHORITY;
13
+ export const MUTATION_KINDS = ['check', 'apply', 'apply-full', 'rollback'];
14
+ export class MutationController {
15
+ authorities = new Map();
16
+ register(kind, authority) {
17
+ let table = this.authorities.get(kind);
18
+ if (table === undefined) {
19
+ table = new Map();
20
+ this.authorities.set(kind, table);
21
+ }
22
+ table.set(authority.name, authority);
23
+ }
24
+ unregister(kind, name) {
25
+ return this.authorities.get(kind)?.delete(name) ?? false;
26
+ }
27
+ hasAuthority(kind, name) {
28
+ return this.authorities.get(kind)?.has(name) ?? false;
29
+ }
30
+ /**
31
+ * Resolve which authority serves `kind`: the preferred authority when
32
+ * registered, otherwise the legacy authority as an explicit fallback.
33
+ * Throws when nothing is registered — an unregistered kind must fail loud,
34
+ * not silently mutate.
35
+ */
36
+ resolveAuthority(kind) {
37
+ const table = this.authorities.get(kind);
38
+ if (table === undefined || table.size === 0) {
39
+ throw new Error(`No mutation authority registered for kind: ${kind}`);
40
+ }
41
+ const preferred = table.get(PREFERRED_MUTATION_AUTHORITY);
42
+ if (preferred !== undefined) {
43
+ return { authority: preferred, fallback: false };
44
+ }
45
+ const legacy = table.get(LEGACY_MUTATION_AUTHORITY);
46
+ if (legacy !== undefined) {
47
+ return { authority: legacy, fallback: true };
48
+ }
49
+ // Multiple non-preferred, non-legacy authorities: refuse to guess.
50
+ throw new Error(`No preferred (${PREFERRED_MUTATION_AUTHORITY}) or legacy (${LEGACY_MUTATION_AUTHORITY}) authority registered for kind: ${kind}`);
51
+ }
52
+ /**
53
+ * Route one mutation request. Response body contract is owned by the
54
+ * authority handler; the controller only annotates the response with the
55
+ * resolved authority so governance state is observable per request.
56
+ */
57
+ // eslint-disable-next-line @typescript-eslint/max-params -- (req, res) mirrors the Node handler shape used across this server
58
+ async dispatch(req, res, ctx, kind) {
59
+ const { authority, fallback } = this.resolveAuthority(kind);
60
+ const headerValue = fallback
61
+ ? `${authority.name} (preferred: ${PREFERRED_MUTATION_AUTHORITY} not yet available)`
62
+ : authority.name;
63
+ res.setHeader('X-PD-Mutation-Authority', headerValue);
64
+ await authority.handler(req, res, ctx);
65
+ }
66
+ /** Governance snapshot for observability and migration tests. */
67
+ describeGovernance() {
68
+ const snapshot = {};
69
+ for (const kind of MUTATION_KINDS) {
70
+ const table = this.authorities.get(kind);
71
+ const available = table !== undefined ? [...table.keys()] : [];
72
+ let info;
73
+ if (table === undefined || table.size === 0) {
74
+ info = { active: 'none', preferred: PREFERRED_MUTATION_AUTHORITY, fallback: false, available };
75
+ }
76
+ else {
77
+ const { authority, fallback } = this.resolveAuthority(kind);
78
+ info = { active: authority.name, preferred: PREFERRED_MUTATION_AUTHORITY, fallback, available };
79
+ }
80
+ snapshot[kind] = info;
81
+ }
82
+ return snapshot;
83
+ }
84
+ }
85
+ /**
86
+ * Process-wide controller for the `/api/update/*` surface. The legacy
87
+ * updater registers its handlers at module load of `routes/update.ts`.
88
+ */
89
+ export const updateMutationController = new MutationController();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-principles-disciple",
3
- "version": "1.130.3",
3
+ "version": "1.131.0",
4
4
  "description": "Interactive CLI installer for Principles Disciple OpenClaw plugin",
5
5
  "type": "module",
6
6
  "bin": {