@shrkcrft/generator 0.1.0-alpha.3 → 0.1.0-alpha.31

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.
@@ -1 +1 @@
1
- {"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAW5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,gEAAgE;IAChE,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAmGf"}
1
+ {"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAY5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,gEAAgE;IAChE,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAqHf"}
package/dist/dry-run.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { safeResolveTargetPath } from '@shrkcrft/core';
3
- import { renderTemplate, validateTemplateVariables, } from '@shrkcrft/templates';
3
+ import { renderTemplate, templateRemainderLines, validateTemplateVariables, } from '@shrkcrft/templates';
4
4
  import { OverwriteStrategy } from "./overwrite-strategy.js";
5
5
  import { FileChangeType } from "./file-change.js";
6
6
  import { decideForExisting, summarizeConflicts } from "./conflict-handler.js";
7
7
  import { buildNameVariables } from "./naming-strategy.js";
8
8
  import { evaluatePlannedChange, } from "./planned-change.js";
9
+ import { describeInvalidPlannedOperation, validatePlannedOperation } from "./planned-operation-fields.js";
9
10
  export function planGeneration(template, request) {
10
11
  const warnings = [];
11
12
  const overwriteStrategy = request.overwriteStrategy ?? OverwriteStrategy.Never;
@@ -24,12 +25,19 @@ export function planGeneration(template, request) {
24
25
  hasConflicts: false,
25
26
  warnings,
26
27
  postGenerationNotes: template.postGenerationNotes ?? [],
28
+ remainderLines: templateRemainderLines(template),
27
29
  },
28
30
  safe: false,
29
31
  };
30
32
  }
31
33
  const rendered = renderTemplate(template, validation.resolved);
32
34
  const changes = [];
35
+ // Per-file content overlay so that MULTIPLE changes (or a files() create
36
+ // followed by a changes() op) targeting the SAME file COMPOSE. Without
37
+ // this every change is evaluated against the original on-disk bytes and
38
+ // the writer's last same-file write clobbers the earlier ones. Keyed by
39
+ // absolute path → the cumulative content after the prior change(s).
40
+ const overlay = new Map();
33
41
  // 1) Legacy CREATE-only files() output — unchanged behaviour.
34
42
  for (const file of rendered.files) {
35
43
  let safe;
@@ -59,14 +67,16 @@ export function planGeneration(template, request) {
59
67
  // unreadable existing file — treat as conflict
60
68
  }
61
69
  const decision = decideForExisting(file.overwrite ? OverwriteStrategy.Overwrite : overwriteStrategy, existing, file.content);
70
+ const contents = decision.type === FileChangeType.Skip ? existing : file.content;
62
71
  changes.push({
63
72
  type: decision.type,
64
73
  absolutePath,
65
74
  relativePath: relPath,
66
- contents: decision.type === FileChangeType.Skip ? existing : file.content,
75
+ contents,
67
76
  reason: decision.reason,
68
77
  sizeBytes: Buffer.byteLength(file.content, 'utf8'),
69
78
  });
79
+ overlay.set(absolutePath, contents);
70
80
  }
71
81
  else {
72
82
  changes.push({
@@ -77,13 +87,20 @@ export function planGeneration(template, request) {
77
87
  reason: 'New file (does not exist)',
78
88
  sizeBytes: Buffer.byteLength(file.content, 'utf8'),
79
89
  });
90
+ overlay.set(absolutePath, file.content);
80
91
  }
81
92
  }
82
- // 2) v2 planned changes — evaluate against live filesystem.
83
- for (const tplChange of rendered.changes) {
84
- const evaluated = planOne(tplChange, request.projectRoot);
93
+ // 2) v2 planned changes — evaluate against the live filesystem, threaded
94
+ // through the overlay so successive changes to one file compose.
95
+ // Each op is shape-checked against PLANNED_OPERATION_FIELDS first, so a
96
+ // misspelled op becomes a located Conflict instead of a TypeError.
97
+ rendered.changes.forEach((tplChange, index) => {
98
+ const evaluated = planOne(tplChange, request.projectRoot, overlay, {
99
+ where: `template '${template.id}' change[${index}]`,
100
+ warnings,
101
+ });
85
102
  changes.push(evaluated);
86
- }
103
+ });
87
104
  const { hasConflicts } = summarizeConflicts(changes);
88
105
  return {
89
106
  plan: {
@@ -94,12 +111,32 @@ export function planGeneration(template, request) {
94
111
  hasConflicts,
95
112
  warnings,
96
113
  postGenerationNotes: rendered.postGenerationNotes,
114
+ remainderLines: templateRemainderLines(template),
97
115
  },
98
116
  safe: !hasConflicts && changes.length > 0,
99
117
  };
100
118
  }
101
- function planOne(tplChange, projectRoot) {
119
+ function planOne(tplChange, projectRoot, overlay, ctx) {
102
120
  const op = tplChange.operation;
121
+ const where = ctx?.where ?? 'change';
122
+ const shape = validatePlannedOperation(op);
123
+ const rawTarget = typeof tplChange.targetPath === 'string' ? tplChange.targetPath : '';
124
+ const invalid = describeInvalidPlannedOperation(shape, where) ??
125
+ (rawTarget.length === 0 ? `${where} (${shape.kind}): missing targetPath` : undefined);
126
+ if (invalid) {
127
+ return {
128
+ type: FileChangeType.Conflict,
129
+ absolutePath: rawTarget,
130
+ relativePath: rawTarget,
131
+ contents: '',
132
+ reason: invalid,
133
+ sizeBytes: 0,
134
+ operation: op,
135
+ };
136
+ }
137
+ if (shape.unknown.length > 0) {
138
+ ctx?.warnings.push(`${where} (${shape.kind}): unknown key(s) ${shape.unknown.join(', ')} are ignored by the engine`);
139
+ }
103
140
  let safe;
104
141
  try {
105
142
  safe = safeResolveTargetPath(tplChange.targetPath, projectRoot);
@@ -121,15 +158,39 @@ function planOne(tplChange, projectRoot) {
121
158
  targetPath: tplChange.targetPath,
122
159
  operation: op,
123
160
  };
124
- const existing = existsSync(safe.absolutePath)
125
- ? readFileSafe(safe.absolutePath)
126
- : null;
127
- return evaluatePlannedChange({
128
- change,
129
- absolutePath: safe.absolutePath,
130
- relativePath: safe.relativePath,
131
- existing,
132
- });
161
+ // Prefer the overlay (cumulative result of prior same-file changes) over
162
+ // the on-disk bytes so successive ops on one file compose deterministically.
163
+ const existing = overlay?.has(safe.absolutePath)
164
+ ? (overlay.get(safe.absolutePath) ?? null)
165
+ : existsSync(safe.absolutePath)
166
+ ? readFileSafe(safe.absolutePath)
167
+ : null;
168
+ let result;
169
+ try {
170
+ result = evaluatePlannedChange({
171
+ change,
172
+ absolutePath: safe.absolutePath,
173
+ relativePath: safe.relativePath,
174
+ existing,
175
+ });
176
+ }
177
+ catch (e) {
178
+ // A well-shaped op can still carry a wrong-typed value; the plan must name
179
+ // the change, never surface a bare TypeError.
180
+ return {
181
+ type: FileChangeType.Conflict,
182
+ absolutePath: safe.absolutePath,
183
+ relativePath: safe.relativePath,
184
+ contents: existing ?? '',
185
+ reason: `${where} (${shape.kind}): could not be evaluated — ${e.message}`,
186
+ sizeBytes: 0,
187
+ operation: op,
188
+ };
189
+ }
190
+ // Record the cumulative content (Skip/Conflict carry the unchanged bytes,
191
+ // which is exactly what a later op on the same file should see).
192
+ overlay?.set(safe.absolutePath, result.contents);
193
+ return result;
133
194
  }
134
195
  function readFileSafe(absolutePath) {
135
196
  try {
@@ -170,6 +231,8 @@ function previewContentForOperation(op) {
170
231
  return `${op.enumName}.${op.entryName} = '${op.entryValue}'`;
171
232
  case 'insert-object-entry':
172
233
  return `${op.objectName}.${op.entryKey}: ${op.entryValue}`;
234
+ case 'insert-array-entry':
235
+ return `${op.arrayName} ⟵ ${op.entryValue}`;
173
236
  case 'insert-before-closing-brace':
174
237
  return op.snippet;
175
238
  case 'insert-between-anchors':
@@ -1,4 +1,4 @@
1
- import type { IPlannedOperation } from './planned-change.js';
1
+ import type { IPlannedOperation } from './operations.js';
2
2
  export declare enum FileChangeType {
3
3
  Create = "create",
4
4
  /** Legacy v1 full-file overwrite when the same template is regenerated. */
@@ -1 +1 @@
1
- {"version":3,"file":"file-change.d.ts","sourceRoot":"","sources":["../src/file-change.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,oBAAY,cAAc;IACxB,MAAM,WAAW;IACjB,2EAA2E;IAC3E,MAAM,WAAW;IACjB,yDAAyD;IACzD,MAAM,WAAW;IACjB,0EAA0E;IAC1E,WAAW,iBAAiB;IAC5B,2EAA2E;IAC3E,YAAY,kBAAkB;IAC9B,oDAAoD;IACpD,OAAO,YAAY;IACnB,+DAA+D;IAC/D,MAAM,WAAW;IACjB,iEAAiE;IACjE,YAAY,kBAAkB;IAC9B,4DAA4D;IAC5D,YAAY,kBAAkB;IAC9B,IAAI,SAAS;IACb,QAAQ,aAAa;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B"}
1
+ {"version":3,"file":"file-change.d.ts","sourceRoot":"","sources":["../src/file-change.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,oBAAY,cAAc;IACxB,MAAM,WAAW;IACjB,2EAA2E;IAC3E,MAAM,WAAW;IACjB,yDAAyD;IACzD,MAAM,WAAW;IACjB,0EAA0E;IAC1E,WAAW,iBAAiB;IAC5B,2EAA2E;IAC3E,YAAY,kBAAkB;IAC9B,oDAAoD;IACpD,OAAO,YAAY;IACnB,+DAA+D;IAC/D,MAAM,WAAW;IACjB,iEAAiE;IACjE,YAAY,kBAAkB;IAC9B,4DAA4D;IAC5D,YAAY,kBAAkB;IAC9B,IAAI,SAAS;IACb,QAAQ,aAAa;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"folder-apply.d.ts","sourceRoot":"","sources":["../src/folder-apply.ts"],"names":[],"mappings":"AAcA,OAAO,EAAuB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,cAAc,EAAE,EAC9B,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAsGtB"}
1
+ {"version":3,"file":"folder-apply.d.ts","sourceRoot":"","sources":["../src/folder-apply.ts"],"names":[],"mappings":"AAeA,OAAO,EAAuB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,cAAc,EAAE,EAC9B,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAgJtB"}
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { existsSync, renameSync, rmSync } from 'node:fs';
14
14
  import * as nodePath from 'node:path';
15
+ import { pathEscapesRootViaSymlink, safeResolveTargetPath } from '@shrkcrft/core';
15
16
  import { checkFolderOpSafety, FolderOpSafety } from "./folder-safety.js";
16
17
  function resolveAbs(projectRoot, p) {
17
18
  return nodePath.isAbsolute(p) ? p : nodePath.resolve(projectRoot, p);
@@ -60,7 +61,26 @@ export function applyFolderOps(ops, options) {
60
61
  });
61
62
  continue;
62
63
  }
63
- if (existsSync(absNewPath)) {
64
+ // The rename DESTINATION is never validated by checkFolderOpSafety (which
65
+ // only checks targetPath), and resolveAbs even allows absolute newPaths —
66
+ // so `newPath: '../sibling'` or '/etc/x' would move the folder OUTSIDE the
67
+ // project root. Enforce containment through the same chokepoint as writes.
68
+ let safeNew;
69
+ try {
70
+ safeNew = safeResolveTargetPath(op.newPath, options.projectRoot);
71
+ }
72
+ catch (e) {
73
+ const pathErr = e;
74
+ rejected.push({
75
+ ...baseResult,
76
+ applied: false,
77
+ safety: FolderOpSafety.Unsafe,
78
+ reason: `rename-folder destination "${op.newPath}" is outside the project root (${pathErr.code}).`,
79
+ });
80
+ continue;
81
+ }
82
+ const safeAbsNewPath = safeNew.absolutePath;
83
+ if (existsSync(safeAbsNewPath)) {
64
84
  rejected.push({
65
85
  ...baseResult,
66
86
  applied: false,
@@ -73,8 +93,19 @@ export function applyFolderOps(ops, options) {
73
93
  applied.push({ ...baseResult, applied: false });
74
94
  continue;
75
95
  }
96
+ // Defense in depth: re-verify the SOURCE has not become a symlink that
97
+ // escapes the sandbox between safety-check and mutation (TOCTOU window).
98
+ if (pathEscapesRootViaSymlink(options.projectRoot, absTarget)) {
99
+ rejected.push({
100
+ ...baseResult,
101
+ applied: false,
102
+ safety: FolderOpSafety.Unsafe,
103
+ reason: `rename-folder source "${op.targetPath}" resolves outside the project root (symlink escape).`,
104
+ });
105
+ continue;
106
+ }
76
107
  try {
77
- renameSync(absTarget, absNewPath);
108
+ renameSync(absTarget, safeAbsNewPath);
78
109
  applied.push({ ...baseResult, applied: true });
79
110
  }
80
111
  catch (e) {
@@ -93,6 +124,19 @@ export function applyFolderOps(ops, options) {
93
124
  applied.push({ ...baseResult, applied: false });
94
125
  continue;
95
126
  }
127
+ // Defense in depth: re-verify the target has not become a symlink that
128
+ // escapes the sandbox before a recursive delete. checkFolderOpSafety
129
+ // already gates this; a second realpath probe closes the TOCTOU window
130
+ // so we never rmSync EXTERNAL data.
131
+ if (pathEscapesRootViaSymlink(options.projectRoot, absTarget)) {
132
+ rejected.push({
133
+ ...baseResult,
134
+ applied: false,
135
+ safety: FolderOpSafety.Unsafe,
136
+ reason: `delete-folder target "${op.targetPath}" resolves outside the project root (symlink escape).`,
137
+ });
138
+ continue;
139
+ }
96
140
  try {
97
141
  rmSync(absTarget, { recursive: true, force: false });
98
142
  applied.push({ ...baseResult, applied: true });
@@ -1 +1 @@
1
- {"version":3,"file":"folder-safety.d.ts","sourceRoot":"","sources":["../src/folder-safety.ts"],"names":[],"mappings":"AAeA,oBAAY,cAAc;IACxB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,GAAG,eAAe,EACvC,OAAO,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACxC,qBAAqB,CAqDvB"}
1
+ {"version":3,"file":"folder-safety.d.ts","sourceRoot":"","sources":["../src/folder-safety.ts"],"names":[],"mappings":"AAgBA,oBAAY,cAAc;IACxB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,GAAG,eAAe,EACvC,OAAO,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACxC,qBAAqB,CA+DvB"}
@@ -12,6 +12,7 @@
12
12
  import { existsSync, statSync } from 'node:fs';
13
13
  import * as nodePath from 'node:path';
14
14
  import * as nodeOs from 'node:os';
15
+ import { pathEscapesRootViaSymlink } from '@shrkcrft/core';
15
16
  export var FolderOpSafety;
16
17
  (function (FolderOpSafety) {
17
18
  FolderOpSafety["Safe"] = "safe";
@@ -33,6 +34,16 @@ export function checkFolderOpSafety(projectRoot, target, kind, options) {
33
34
  reason: `Target path "${target}" resolves to "${abs}" which is outside the project root "${normalisedRoot}".`,
34
35
  };
35
36
  }
37
+ // Realpath-aware containment — a lexically-clean path can still traverse an
38
+ // in-root symlink (e.g. `linkdir -> ../outside`) so that `linkdir/secret`
39
+ // physically lives outside the sandbox. A destructive rmSync/renameSync on
40
+ // such a path would corrupt EXTERNAL data while reporting success.
41
+ if (pathEscapesRootViaSymlink(normalisedRoot, abs)) {
42
+ return {
43
+ safety: FolderOpSafety.Unsafe,
44
+ reason: `Target path "${target}" resolves to "${abs}" which is outside the project root (symlink escape).`,
45
+ };
46
+ }
36
47
  if (abs === normalisedRoot) {
37
48
  return { safety: FolderOpSafety.Unsafe, reason: 'Target path resolves to the project root.' };
38
49
  }
@@ -8,6 +8,13 @@ export interface IGenerationPlan {
8
8
  hasConflicts: boolean;
9
9
  warnings: readonly string[];
10
10
  postGenerationNotes: readonly string[];
11
+ /**
12
+ * The template's declared remainder — what it deliberately does NOT
13
+ * scaffold, and the manual steps that cover it — as the printable lines
14
+ * `templateRemainderLines` renders (`@shrkcrft/templates`). Absent/empty
15
+ * when the template declares neither `notScaffolded` nor `manualSteps`.
16
+ */
17
+ remainderLines?: readonly string[];
11
18
  /**
12
19
  * Optional folder operations attached to the plan. Carried verbatim
13
20
  * into saved plans and executed via `applyFolderOps()` during `shrk apply`
@@ -1 +1 @@
1
- {"version":3,"file":"generation-plan.d.ts","sourceRoot":"","sources":["../src/generation-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"generation-plan.d.ts","sourceRoot":"","sources":["../src/generation-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"generator-engine.d.ts","sourceRoot":"","sources":["../src/generator-engine.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAED,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,MAAM,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CA2DrC"}
1
+ {"version":3,"file":"generator-engine.d.ts","sourceRoot":"","sources":["../src/generator-engine.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAED,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,MAAM,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CA2ErC"}
@@ -21,8 +21,15 @@ export function generate(template, request) {
21
21
  if (plan.hasConflicts) {
22
22
  return err(new AppErrorImpl(ERROR_CODES.TARGET_FILE_EXISTS, 'Generation refused: plan has conflicts (use --force / different overwrite strategy)', { details: { conflicts: plan.changes.filter((c) => c.type === FileChangeType.Conflict) } }));
23
23
  }
24
- const written = [];
25
- let totalBytes = 0;
24
+ // Multiple changes can target the SAME path (e.g. create-then-insert). The
25
+ // overlay in planGeneration makes each change's `contents` cumulative, so the
26
+ // LAST writeable change per path already carries the composed result. Write
27
+ // each distinct path once and count written/totalBytes by distinct path —
28
+ // matching the synthetic-plan writer. Writing every change would double-count
29
+ // and re-write the same file. (Single-op-per-path plans, the common case, are
30
+ // unchanged.)
31
+ const lastWriteableByPath = new Map();
32
+ const writeOrder = [];
26
33
  let skipped = 0;
27
34
  for (const change of plan.changes) {
28
35
  if (change.type === FileChangeType.Skip) {
@@ -30,15 +37,23 @@ export function generate(template, request) {
30
37
  continue;
31
38
  }
32
39
  if (isWriteableChange(change.type)) {
33
- try {
34
- mkdirSync(dirname(change.absolutePath), { recursive: true });
35
- writeFileSync(change.absolutePath, change.contents, 'utf8');
36
- written.push(change);
37
- totalBytes += change.sizeBytes;
38
- }
39
- catch (e) {
40
- return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to write ${change.absolutePath}`, { details: { path: change.absolutePath }, cause: e }));
41
- }
40
+ if (!lastWriteableByPath.has(change.absolutePath))
41
+ writeOrder.push(change.absolutePath);
42
+ lastWriteableByPath.set(change.absolutePath, change);
43
+ }
44
+ }
45
+ const written = [];
46
+ let totalBytes = 0;
47
+ for (const path of writeOrder) {
48
+ const change = lastWriteableByPath.get(path);
49
+ try {
50
+ mkdirSync(dirname(change.absolutePath), { recursive: true });
51
+ writeFileSync(change.absolutePath, change.contents, 'utf8');
52
+ written.push(change);
53
+ totalBytes += change.sizeBytes;
54
+ }
55
+ catch (e) {
56
+ return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to write ${change.absolutePath}`, { details: { path: change.absolutePath }, cause: e }));
42
57
  }
43
58
  }
44
59
  return ok({
package/dist/index.d.ts CHANGED
@@ -9,9 +9,11 @@ export * from './naming-strategy.js';
9
9
  export * from './saved-plan.js';
10
10
  export * from './plan-signing.js';
11
11
  export * from './planned-change.js';
12
+ export * from './planned-operation-fields.js';
12
13
  export * from './folder-safety.js';
13
14
  export * from './folder-apply.js';
14
15
  export * from './synthetic-plan.js';
16
+ export * from './package-delegate-plan.js';
15
17
  export * from './spec/index.js';
16
18
  export * from './grounding/index.js';
17
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -9,8 +9,10 @@ export * from "./naming-strategy.js";
9
9
  export * from "./saved-plan.js";
10
10
  export * from "./plan-signing.js";
11
11
  export * from "./planned-change.js";
12
+ export * from "./planned-operation-fields.js";
12
13
  export * from "./folder-safety.js";
13
14
  export * from "./folder-apply.js";
14
15
  export * from "./synthetic-plan.js";
16
+ export * from "./package-delegate-plan.js";
15
17
  export * from "./spec/index.js";
16
18
  export * from "./grounding/index.js";
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Operation union — declared by templates, persisted in saved plans.
3
+ * Part of the v2 generation model.
4
+ */
5
+ export type PlannedOperationKind = 'create' | 'append' | 'insert-after' | 'insert-before' | 'replace' | 'export' | 'ensure-import' | 'insert-enum-entry' | 'insert-object-entry' | 'insert-array-entry' | 'insert-before-closing-brace' | 'insert-between-anchors';
6
+ export interface ICreateOperation {
7
+ kind: 'create';
8
+ content: string;
9
+ description?: string;
10
+ }
11
+ export interface IAppendOperation {
12
+ kind: 'append';
13
+ /**
14
+ * The snippet to append at the end of the file. The engine adds a single
15
+ * `\n` separator between the existing trailing content and the snippet if
16
+ * the existing file does not already end with a newline.
17
+ */
18
+ snippet: string;
19
+ /**
20
+ * Optional idempotency marker. If the existing file already contains this
21
+ * string anywhere, the operation is skipped (already applied).
22
+ */
23
+ ifMissing?: string;
24
+ description?: string;
25
+ }
26
+ export interface IInsertAfterOperation {
27
+ kind: 'insert-after';
28
+ /** Literal substring that must appear exactly once in the file. */
29
+ anchor: string;
30
+ /** The snippet to insert immediately after `anchor`. */
31
+ snippet: string;
32
+ /** Idempotency check; default = `snippet`. */
33
+ ifMissing?: string;
34
+ description?: string;
35
+ }
36
+ export interface IInsertBeforeOperation {
37
+ kind: 'insert-before';
38
+ anchor: string;
39
+ snippet: string;
40
+ ifMissing?: string;
41
+ description?: string;
42
+ }
43
+ export interface IReplaceOperation {
44
+ kind: 'replace';
45
+ /** Literal substring to find. */
46
+ find: string;
47
+ /** Replacement text. */
48
+ replaceWith: string;
49
+ /**
50
+ * If provided, the engine requires exactly this many matches; otherwise the
51
+ * default is exactly 1. Multiple matches without an explicit `expectMatches`
52
+ * is a conflict (ambiguous replace).
53
+ */
54
+ expectMatches?: number;
55
+ description?: string;
56
+ }
57
+ export interface IExportOperation {
58
+ kind: 'export';
59
+ /** The symbol/path to re-export. */
60
+ from: string;
61
+ /** Optional named symbols. When omitted, emits `export * from`. */
62
+ symbols?: readonly string[];
63
+ /** Idempotency check; default = computed export line. */
64
+ ifMissing?: string;
65
+ description?: string;
66
+ }
67
+ export interface IEnsureImportOperation {
68
+ kind: 'ensure-import';
69
+ /** Module specifier, e.g. `'./events'` or `'@app/plugin-core'`. */
70
+ from: string;
71
+ /**
72
+ * Named symbols to ensure. The op is a NO-OP for symbols already imported
73
+ * from `from`. Default import (`type: 'default'`) and namespace import
74
+ * (`type: 'namespace'`) are also supported via dedicated fields below.
75
+ */
76
+ symbols?: readonly string[];
77
+ /** Treat the import as `import type { ... }` instead of value import. */
78
+ typeOnly?: boolean;
79
+ /** Default import binding (e.g. `import Foo from 'foo'`). */
80
+ defaultBinding?: string;
81
+ /** Namespace import binding (e.g. `import * as foo from 'foo'`). */
82
+ namespaceBinding?: string;
83
+ description?: string;
84
+ }
85
+ export interface IInsertEnumEntryOperation {
86
+ kind: 'insert-enum-entry';
87
+ /** Enum identifier, e.g. `PaginationEventType`. */
88
+ enumName: string;
89
+ /** Identifier of the new enum member, e.g. `ITEM_SELECTED`. */
90
+ entryName: string;
91
+ /** Literal string value to assign, e.g. `'pagination.itemSelected'`. */
92
+ entryValue: string;
93
+ description?: string;
94
+ }
95
+ export interface IInsertObjectEntryOperation {
96
+ kind: 'insert-object-entry';
97
+ /** Object identifier, e.g. `ROUTE_KEYS`. */
98
+ objectName: string;
99
+ /** Key to add. */
100
+ entryKey: string;
101
+ /** Value literal (already source-formatted). */
102
+ entryValue: string;
103
+ /** When `true`, allow shorthand entries; default `false`. */
104
+ shorthand?: boolean;
105
+ description?: string;
106
+ }
107
+ export interface IInsertArrayEntryOperation {
108
+ kind: 'insert-array-entry';
109
+ /**
110
+ * Array identifier — a `const`/`let`/`var` bound to an array literal,
111
+ * e.g. `editorScopeEntries` or `DEFAULT_PANELS`. The element is inserted
112
+ * before the array's matching closing bracket.
113
+ */
114
+ arrayName: string;
115
+ /**
116
+ * Fallback array identifiers tried, in order, when `arrayName` does not
117
+ * resolve in the target project (registration arrays are often named
118
+ * differently across codebases). The first cleanly-resolved candidate
119
+ * wins. When none resolve, the op surfaces an actionable manual-wiring
120
+ * conflict instead of an opaque "array not found".
121
+ */
122
+ arrayNameAlternatives?: readonly string[];
123
+ /**
124
+ * Precise manual-wiring instruction shown when no candidate array resolves.
125
+ * When set, it replaces the synthesized "wire <entry> into <array> manually"
126
+ * message — letting a template author mark this step as a MANUAL follow-up
127
+ * with exact guidance instead of an opaque conflict.
128
+ */
129
+ manualStepInstruction?: string;
130
+ /** Element source text to add (already source-formatted, no trailing comma). */
131
+ entryValue: string;
132
+ /** Optional idempotency marker (default = `entryValue`). */
133
+ ifMissing?: string;
134
+ description?: string;
135
+ }
136
+ export interface IInsertBeforeClosingBraceOperation {
137
+ kind: 'insert-before-closing-brace';
138
+ /** Container identifier, e.g. an interface/class/enum name. */
139
+ containerName: string;
140
+ /** Snippet inserted immediately before the matching closing brace. */
141
+ snippet: string;
142
+ /** Optional idempotency marker (default = `snippet`). */
143
+ ifMissing?: string;
144
+ description?: string;
145
+ }
146
+ export interface IInsertBetweenAnchorsOperation {
147
+ kind: 'insert-between-anchors';
148
+ beginAnchor: string;
149
+ endAnchor: string;
150
+ snippet: string;
151
+ /** Optional idempotency marker (default = `snippet`). */
152
+ ifMissing?: string;
153
+ description?: string;
154
+ }
155
+ export type IPlannedOperation = ICreateOperation | IAppendOperation | IInsertAfterOperation | IInsertBeforeOperation | IReplaceOperation | IExportOperation | IEnsureImportOperation | IInsertEnumEntryOperation | IInsertObjectEntryOperation | IInsertArrayEntryOperation | IInsertBeforeClosingBraceOperation | IInsertBetweenAnchorsOperation;
156
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Operation union — declared by templates, persisted in saved plans.
3
+ * Part of the v2 generation model.
4
+ */
5
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Package a delegate worker's raw edit into a SIGNED-ready synthetic plan.
3
+ *
4
+ * This is the deterministic chokepoint between a stochastic worker and the
5
+ * write path. It:
6
+ * 1. drops ops whose `kind` is not in the recipe's `allowedOps` (reported,
7
+ * never applied);
8
+ * 2. validates every remaining op's fields against the real operation union —
9
+ * a malformed op refuses the whole package (the worker must retry);
10
+ * 3. evaluates the ops against the live file system via the SAME
11
+ * `evaluateSavedPlanInPlace` apply uses, so anchor-not-found / ambiguous /
12
+ * file-missing all surface as conflicts BEFORE anything is signed;
13
+ * 4. builds an `ISavedPlan` (templateId `__delegate/<recipe>`) the caller
14
+ * signs + applies through the unmodified apply pipeline.
15
+ *
16
+ * No model, no network. The raw-op input type is declared locally so the
17
+ * generator layer carries no dependency on `@shrkcrft/ai`.
18
+ */
19
+ import { type AppError, type Result } from '@shrkcrft/core';
20
+ import type { IGenerationPlan } from './generation-plan.js';
21
+ import type { ISavedPlan } from './saved-plan.js';
22
+ export declare const DELEGATE_TEMPLATE_PREFIX = "__delegate/";
23
+ /** A raw operation as parsed from a worker (structurally `IDelegateRawOp`). */
24
+ export interface IDelegateOpInput {
25
+ targetPath: string;
26
+ operation: {
27
+ kind: string;
28
+ } & Record<string, unknown>;
29
+ }
30
+ export interface IPackageDelegateInput {
31
+ ops: readonly IDelegateOpInput[];
32
+ /** `IPlannedOperation` kinds the recipe permits; others are dropped. */
33
+ allowedOps: readonly string[];
34
+ /** Recipe id — becomes the synthetic templateId `__delegate/<id>`. */
35
+ recipeId: string;
36
+ projectRoot: string;
37
+ }
38
+ export interface IDroppedOp {
39
+ kind: string;
40
+ targetPath: string;
41
+ reason: string;
42
+ }
43
+ export interface IPackageDelegateResult {
44
+ /** Conflict-free, ready to sign. Present only when `ready`. */
45
+ plan?: ISavedPlan;
46
+ /** The evaluated generation plan (changes + conflicts) — always present. */
47
+ generation: IGenerationPlan;
48
+ /** Ops whose kind was not in `allowedOps`. */
49
+ droppedOps: readonly IDroppedOp[];
50
+ /** True when no conflicts — the plan is built and ready to sign + apply. */
51
+ ready: boolean;
52
+ }
53
+ export declare function packageDelegatePlan(input: IPackageDelegateInput): Result<IPackageDelegateResult, AppError>;
54
+ //# sourceMappingURL=package-delegate-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-delegate-plan.d.ts","sourceRoot":"","sources":["../src/package-delegate-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAmBlD,eAAO,MAAM,wBAAwB,gBAAgB,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjC,wEAAwE;IACxE,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,4EAA4E;IAC5E,UAAU,EAAE,eAAe,CAAC;IAC5B,8CAA8C;IAC9C,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC;IAClC,4EAA4E;IAC5E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,qBAAqB,GAC3B,MAAM,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAoE1C"}