@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.
- package/dist/dry-run.d.ts.map +1 -1
- package/dist/dry-run.js +79 -16
- package/dist/file-change.d.ts +1 -1
- package/dist/file-change.d.ts.map +1 -1
- package/dist/folder-apply.d.ts.map +1 -1
- package/dist/folder-apply.js +46 -2
- package/dist/folder-safety.d.ts.map +1 -1
- package/dist/folder-safety.js +11 -0
- package/dist/generation-plan.d.ts +7 -0
- package/dist/generation-plan.d.ts.map +1 -1
- package/dist/generator-engine.d.ts.map +1 -1
- package/dist/generator-engine.js +26 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/operations.d.ts +156 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +5 -0
- package/dist/package-delegate-plan.d.ts +54 -0
- package/dist/package-delegate-plan.d.ts.map +1 -0
- package/dist/package-delegate-plan.js +253 -0
- package/dist/planned-change.d.ts +2 -123
- package/dist/planned-change.d.ts.map +1 -1
- package/dist/planned-change.js +104 -0
- package/dist/planned-operation-fields.d.ts +71 -0
- package/dist/planned-operation-fields.d.ts.map +1 -0
- package/dist/planned-operation-fields.js +100 -0
- package/dist/saved-plan.d.ts +28 -2
- package/dist/saved-plan.d.ts.map +1 -1
- package/dist/saved-plan.js +20 -0
- package/dist/synthetic-plan.d.ts.map +1 -1
- package/dist/synthetic-plan.js +89 -18
- package/package.json +6 -6
package/dist/dry-run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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':
|
package/dist/file-change.d.ts
CHANGED
|
@@ -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,
|
|
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":"
|
|
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"}
|
package/dist/folder-apply.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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":"
|
|
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"}
|
package/dist/folder-safety.js
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/generator-engine.js
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,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"}
|