@shrkcrft/generator 0.1.0-alpha.22 → 0.1.0-alpha.24
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/folder-apply.d.ts.map +1 -1
- package/dist/folder-apply.js +25 -1
- package/dist/folder-safety.d.ts.map +1 -1
- package/dist/folder-safety.js +11 -0
- package/dist/generator-engine.d.ts.map +1 -1
- package/dist/generator-engine.js +26 -11
- package/dist/operations.d.ts +15 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/planned-change.js +42 -5
- package/dist/synthetic-plan.d.ts.map +1 -1
- package/dist/synthetic-plan.js +50 -15
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync, renameSync, rmSync } from 'node:fs';
|
|
14
14
|
import * as nodePath from 'node:path';
|
|
15
|
-
import { safeResolveTargetPath } from '@shrkcrft/core';
|
|
15
|
+
import { pathEscapesRootViaSymlink, safeResolveTargetPath } from '@shrkcrft/core';
|
|
16
16
|
import { checkFolderOpSafety, FolderOpSafety } from "./folder-safety.js";
|
|
17
17
|
function resolveAbs(projectRoot, p) {
|
|
18
18
|
return nodePath.isAbsolute(p) ? p : nodePath.resolve(projectRoot, p);
|
|
@@ -93,6 +93,17 @@ export function applyFolderOps(ops, options) {
|
|
|
93
93
|
applied.push({ ...baseResult, applied: false });
|
|
94
94
|
continue;
|
|
95
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
|
+
}
|
|
96
107
|
try {
|
|
97
108
|
renameSync(absTarget, safeAbsNewPath);
|
|
98
109
|
applied.push({ ...baseResult, applied: true });
|
|
@@ -113,6 +124,19 @@ export function applyFolderOps(ops, options) {
|
|
|
113
124
|
applied.push({ ...baseResult, applied: false });
|
|
114
125
|
continue;
|
|
115
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
|
+
}
|
|
116
140
|
try {
|
|
117
141
|
rmSync(absTarget, { recursive: true, force: false });
|
|
118
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
|
}
|
|
@@ -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/operations.d.ts
CHANGED
|
@@ -112,6 +112,21 @@ export interface IInsertArrayEntryOperation {
|
|
|
112
112
|
* before the array's matching closing bracket.
|
|
113
113
|
*/
|
|
114
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;
|
|
115
130
|
/** Element source text to add (already source-formatted, no trailing comma). */
|
|
116
131
|
entryValue: string;
|
|
117
132
|
/** Optional idempotency marker (default = `entryValue`). */
|
package/dist/operations.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,QAAQ,GACR,QAAQ,GACR,cAAc,GACd,eAAe,GACf,SAAS,GACT,QAAQ,GACR,eAAe,GACf,mBAAmB,GACnB,qBAAqB,GACrB,oBAAoB,GACpB,6BAA6B,GAC7B,wBAAwB,CAAC;AAE7B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,qBAAqB,CAAC;IAC5B,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,oBAAoB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kCAAkC;IACjD,IAAI,EAAE,6BAA6B,CAAC;IACpC,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,wBAAwB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,sBAAsB,GACtB,iBAAiB,GACjB,gBAAgB,GAChB,sBAAsB,GACtB,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,kCAAkC,GAClC,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,QAAQ,GACR,QAAQ,GACR,cAAc,GACd,eAAe,GACf,SAAS,GACT,QAAQ,GACR,eAAe,GACf,mBAAmB,GACnB,qBAAqB,GACrB,oBAAoB,GACpB,6BAA6B,GAC7B,wBAAwB,CAAC;AAE7B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,qBAAqB,CAAC;IAC5B,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,oBAAoB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kCAAkC;IACjD,IAAI,EAAE,6BAA6B,CAAC;IACpC,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,wBAAwB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,sBAAsB,GACtB,iBAAiB,GACjB,gBAAgB,GAChB,sBAAsB,GACtB,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,kCAAkC,GAClC,8BAA8B,CAAC"}
|
package/dist/planned-change.js
CHANGED
|
@@ -309,12 +309,26 @@ function evaluateInsertArrayEntry(op, absolutePath, relativePath, existing) {
|
|
|
309
309
|
if (existing === null) {
|
|
310
310
|
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-array-entry: target file does not exist', op);
|
|
311
311
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
// Try the primary array name, then each declared alternative in order. The
|
|
313
|
+
// first cleanly-resolved (present + unambiguous) array wins. This lets a
|
|
314
|
+
// template target a project whose registration array is named differently
|
|
315
|
+
// without silently dead-ending.
|
|
316
|
+
const candidates = [op.arrayName, ...(op.arrayNameAlternatives ?? [])].filter((n) => n.length > 0);
|
|
317
|
+
let arr = null;
|
|
318
|
+
let sawAmbiguous = false;
|
|
319
|
+
for (const name of candidates) {
|
|
320
|
+
const found = findArrayLiteralBlock(existing, name);
|
|
321
|
+
if (!found)
|
|
322
|
+
continue;
|
|
323
|
+
if (found.duplicate) {
|
|
324
|
+
sawAmbiguous = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
arr = found;
|
|
328
|
+
break;
|
|
315
329
|
}
|
|
316
|
-
if (arr
|
|
317
|
-
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing,
|
|
330
|
+
if (!arr) {
|
|
331
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, unresolvedArrayReason(op, candidates, sawAmbiguous), op);
|
|
318
332
|
}
|
|
319
333
|
// Idempotency: skip when the element (or its caller-supplied marker) is
|
|
320
334
|
// already present anywhere inside the array body.
|
|
@@ -572,3 +586,26 @@ function detectIndent(body) {
|
|
|
572
586
|
return null;
|
|
573
587
|
return match[1] ?? null;
|
|
574
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Build the actionable reason for an `insert-array-entry` op whose target
|
|
591
|
+
* array (and all declared alternatives) could not be resolved. Instead of an
|
|
592
|
+
* opaque "array not found", the message tells the human exactly what to wire
|
|
593
|
+
* by hand — preserving the template's "zero manual wiring" promise as an
|
|
594
|
+
* explicit, honest fallback. A template author may override the message with
|
|
595
|
+
* `op.manualStepInstruction`.
|
|
596
|
+
*/
|
|
597
|
+
function unresolvedArrayReason(op, candidates, sawAmbiguous) {
|
|
598
|
+
if (op.manualStepInstruction && op.manualStepInstruction.trim().length > 0) {
|
|
599
|
+
return `insert-array-entry: MANUAL — ${op.manualStepInstruction.trim()}`;
|
|
600
|
+
}
|
|
601
|
+
const entry = oneLineEntryLabel(op.ifMissing ?? op.entryValue);
|
|
602
|
+
const tried = candidates.map((c) => `"${c}"`).join(', ');
|
|
603
|
+
const why = sawAmbiguous
|
|
604
|
+
? `registration array ${tried} appears multiple times (ambiguous)`
|
|
605
|
+
: `no registration array found (tried ${tried})`;
|
|
606
|
+
return `insert-array-entry: ${why} — wire ${entry} into ${op.arrayName} manually`;
|
|
607
|
+
}
|
|
608
|
+
function oneLineEntryLabel(raw) {
|
|
609
|
+
const collapsed = raw.replace(/\s+/g, ' ').trim();
|
|
610
|
+
return collapsed.length > 60 ? collapsed.slice(0, 57) + '…' : collapsed;
|
|
611
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synthetic-plan.d.ts","sourceRoot":"","sources":["../src/synthetic-plan.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GAClB,eAAe,
|
|
1
|
+
{"version":3,"file":"synthetic-plan.d.ts","sourceRoot":"","sources":["../src/synthetic-plan.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GAClB,eAAe,CA8CjB;AAoDD,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAWD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAmEzC"}
|
package/dist/synthetic-plan.js
CHANGED
|
@@ -27,6 +27,14 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
|
|
|
27
27
|
const warnings = [];
|
|
28
28
|
let hasConflicts = false;
|
|
29
29
|
const expected = plan.expectedChanges ?? [];
|
|
30
|
+
// Per-file content overlay so MULTIPLE ops on the SAME file COMPOSE: each op
|
|
31
|
+
// sees the cumulative result of the prior ops on that path rather than the
|
|
32
|
+
// original on-disk bytes. Mirrors the template path in dry-run.ts. Without
|
|
33
|
+
// it, [append A, append B] loses A (the writer's last same-path write
|
|
34
|
+
// clobbers earlier ones) and create-then-modify on one NEW path
|
|
35
|
+
// false-conflicts (op2 reads stale disk). Keyed by absolute path → the
|
|
36
|
+
// cumulative content after the prior op(s).
|
|
37
|
+
const overlay = new Map();
|
|
30
38
|
for (const e of expected) {
|
|
31
39
|
if (!e.operation) {
|
|
32
40
|
// Without an operation we cannot reconstruct contents — surface as
|
|
@@ -43,7 +51,7 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
|
|
|
43
51
|
hasConflicts = true;
|
|
44
52
|
continue;
|
|
45
53
|
}
|
|
46
|
-
const change = applyOperation(projectRoot, e.relativePath, e.operation);
|
|
54
|
+
const change = applyOperation(projectRoot, e.relativePath, e.operation, overlay);
|
|
47
55
|
if (change.type === FileChangeType.Conflict)
|
|
48
56
|
hasConflicts = true;
|
|
49
57
|
changes.push(change);
|
|
@@ -62,7 +70,7 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
|
|
|
62
70
|
}
|
|
63
71
|
return out;
|
|
64
72
|
}
|
|
65
|
-
function applyOperation(projectRoot, relativePath, operation) {
|
|
73
|
+
function applyOperation(projectRoot, relativePath, operation, overlay) {
|
|
66
74
|
// Route through the single generator chokepoint instead of a bare resolve, so
|
|
67
75
|
// a traversal / absolute `relativePath` in a hand-crafted or tampered plan
|
|
68
76
|
// can't write OUTSIDE the project root. An unsafe path becomes a Conflict,
|
|
@@ -82,13 +90,24 @@ function applyOperation(projectRoot, relativePath, operation) {
|
|
|
82
90
|
sizeBytes: 0,
|
|
83
91
|
};
|
|
84
92
|
}
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
// Prefer the overlay (the cumulative result of prior same-file ops) over the
|
|
94
|
+
// on-disk bytes so op N sees the result of ops 1..N-1. Falls back to the live
|
|
95
|
+
// file, then to absent (`null`) for a brand-new path.
|
|
96
|
+
const existing = overlay.has(safe.absolutePath)
|
|
97
|
+
? (overlay.get(safe.absolutePath) ?? null)
|
|
98
|
+
: existsSync(safe.absolutePath)
|
|
99
|
+
? readFileSync(safe.absolutePath, 'utf8')
|
|
100
|
+
: null;
|
|
101
|
+
const result = evaluatePlannedChange({
|
|
87
102
|
change: { targetPath: safe.relativePath, operation },
|
|
88
103
|
absolutePath: safe.absolutePath,
|
|
89
104
|
relativePath: safe.relativePath,
|
|
90
105
|
existing,
|
|
91
106
|
});
|
|
107
|
+
// Record the cumulative content (Skip/Conflict carry the unchanged bytes,
|
|
108
|
+
// which is exactly what a later op on the same file should see).
|
|
109
|
+
overlay.set(safe.absolutePath, result.contents);
|
|
110
|
+
return result;
|
|
92
111
|
}
|
|
93
112
|
/**
|
|
94
113
|
* Write evaluated changes from a synthetic plan directly. The caller
|
|
@@ -118,26 +137,42 @@ export function writeSyntheticPlan(plan) {
|
|
|
118
137
|
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Refused to write ${change.relativePath}: contents carry a <<ccr:…>> marker (a compressed/lossy blob is not apply-grade source).`, { details: { path: change.relativePath } }));
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
// Compose multiple ops on the SAME path into ONE write. `evaluateSavedPlanInPlace`
|
|
141
|
+
// threads an overlay, so the LAST writeable change for a path already carries
|
|
142
|
+
// the cumulative result of every prior same-path op (any trailing Skip leaves
|
|
143
|
+
// those bytes unchanged). Writing that once — instead of every op's contents
|
|
144
|
+
// in order, last-write-wins — avoids redundant writes and keeps the success
|
|
145
|
+
// count honest: `written` and `summary.written` count DISTINCT paths.
|
|
146
|
+
const lastWriteableByPath = new Map();
|
|
147
|
+
const skipPaths = new Set();
|
|
124
148
|
for (const change of plan.changes) {
|
|
125
|
-
if (change.type
|
|
126
|
-
|
|
127
|
-
continue;
|
|
149
|
+
if (isWriteableSyntheticChange(change.type)) {
|
|
150
|
+
lastWriteableByPath.set(change.absolutePath, change);
|
|
128
151
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
152
|
+
else if (change.type === FileChangeType.Skip) {
|
|
153
|
+
skipPaths.add(change.absolutePath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const written = [];
|
|
157
|
+
let totalBytes = 0;
|
|
158
|
+
for (const [absolutePath, change] of lastWriteableByPath) {
|
|
131
159
|
try {
|
|
132
|
-
mkdirSync(nodePath.dirname(
|
|
133
|
-
writeFileSync(
|
|
160
|
+
mkdirSync(nodePath.dirname(absolutePath), { recursive: true });
|
|
161
|
+
writeFileSync(absolutePath, change.contents, 'utf8');
|
|
134
162
|
written.push(change);
|
|
135
163
|
totalBytes += change.sizeBytes;
|
|
136
164
|
}
|
|
137
165
|
catch (e) {
|
|
138
|
-
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to write ${
|
|
166
|
+
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to write ${absolutePath}`, { details: { path: absolutePath }, cause: e }));
|
|
139
167
|
}
|
|
140
168
|
}
|
|
169
|
+
// A path counts as skipped only when EVERY op on it resolved to Skip (a path
|
|
170
|
+
// that was also written is not a skip).
|
|
171
|
+
let skipped = 0;
|
|
172
|
+
for (const p of skipPaths) {
|
|
173
|
+
if (!lastWriteableByPath.has(p))
|
|
174
|
+
skipped += 1;
|
|
175
|
+
}
|
|
141
176
|
return ok({
|
|
142
177
|
summary: { written: written.length, skipped, conflicts: 0, totalBytes },
|
|
143
178
|
written,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/generator",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.24",
|
|
4
4
|
"description": "SharkCraft plan-first generator: GenerationPlan, FileChange, dry-run, safe writes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
47
|
-
"@shrkcrft/templates": "^0.1.0-alpha.
|
|
48
|
-
"@shrkcrft/rules": "^0.1.0-alpha.
|
|
49
|
-
"@shrkcrft/paths": "^0.1.0-alpha.
|
|
46
|
+
"@shrkcrft/core": "^0.1.0-alpha.24",
|
|
47
|
+
"@shrkcrft/templates": "^0.1.0-alpha.24",
|
|
48
|
+
"@shrkcrft/rules": "^0.1.0-alpha.24",
|
|
49
|
+
"@shrkcrft/paths": "^0.1.0-alpha.24"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|