fastscript 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname, relative, resolve, isAbsolute } from "node:path";
4
+ import { loadConversionManifest } from "./conversion-manifest.mjs";
5
+
6
+ function normalize(path) {
7
+ return String(path || "").replace(/\\/g, "/");
8
+ }
9
+
10
+ function sha256(value) {
11
+ return createHash("sha256").update(String(value || "")).digest("hex");
12
+ }
13
+
14
+ function parseArgs(args = []) {
15
+ const options = {
16
+ manifest: resolve(".fastscript", "conversion", "latest", "conversion-manifest.json"),
17
+ dryRun: false,
18
+ out: resolve(".fastscript", "conversion", "rollback-latest.json"),
19
+ };
20
+
21
+ for (let i = 0; i < args.length; i += 1) {
22
+ const arg = args[i];
23
+ if (arg === "--manifest") {
24
+ options.manifest = resolve(args[i + 1] || options.manifest);
25
+ i += 1;
26
+ continue;
27
+ }
28
+ if (arg === "--dry-run") {
29
+ options.dryRun = true;
30
+ continue;
31
+ }
32
+ if (arg === "--out") {
33
+ options.out = resolve(args[i + 1] || options.out);
34
+ i += 1;
35
+ continue;
36
+ }
37
+ }
38
+
39
+ return options;
40
+ }
41
+
42
+ function pathStartsWith(child, parent) {
43
+ const rel = relative(parent, child);
44
+ if (!rel) return true;
45
+ return !rel.startsWith("..") && !isAbsolute(rel);
46
+ }
47
+
48
+ function safeResolve(root, relPath) {
49
+ const abs = resolve(root, relPath);
50
+ if (!pathStartsWith(abs, root)) {
51
+ throw new Error(`rollback denied path escape: ${relPath}`);
52
+ }
53
+ return abs;
54
+ }
55
+
56
+ export async function runMigrateRollback(args = []) {
57
+ const options = parseArgs(args);
58
+ const { path: manifestPath, manifest } = loadConversionManifest(options.manifest);
59
+
60
+ const targetRoot = resolve(manifest?.projectRoot || ".", manifest?.target || ".");
61
+ const operations = Array.isArray(manifest?.rollback?.operations) ? manifest.rollback.operations : [];
62
+
63
+ const report = {
64
+ generatedAt: new Date().toISOString(),
65
+ manifestPath,
66
+ targetRoot,
67
+ dryRun: options.dryRun,
68
+ attempted: operations.length,
69
+ applied: 0,
70
+ skipped: 0,
71
+ failures: [],
72
+ };
73
+
74
+ if (!operations.length) {
75
+ mkdirSync(dirname(options.out), { recursive: true });
76
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
77
+ console.log("rollback complete: applied=0, skipped=0");
78
+ console.log(`rollback report: ${normalize(relative(resolve("."), options.out))}`);
79
+ return;
80
+ }
81
+
82
+ for (const operation of operations) {
83
+ try {
84
+ if (operation.type === "rename") {
85
+ const fromAbs = safeResolve(targetRoot, operation.from);
86
+ const toAbs = safeResolve(targetRoot, operation.to);
87
+
88
+ if (!existsSync(fromAbs)) {
89
+ report.skipped += 1;
90
+ continue;
91
+ }
92
+
93
+ if (existsSync(toAbs)) {
94
+ throw new Error(`destination exists: ${normalize(operation.to)}`);
95
+ }
96
+
97
+ if (!options.dryRun) {
98
+ mkdirSync(dirname(toAbs), { recursive: true });
99
+ renameSync(fromAbs, toAbs);
100
+ }
101
+
102
+ report.applied += 1;
103
+ continue;
104
+ }
105
+
106
+ if (operation.type === "restore-content") {
107
+ const fileAbs = safeResolve(targetRoot, operation.file);
108
+ if (typeof operation.contents !== "string") {
109
+ throw new Error(`restore-content missing contents payload for ${operation.file}`);
110
+ }
111
+
112
+ if (!existsSync(fileAbs)) {
113
+ throw new Error(`restore-content target missing: ${operation.file}`);
114
+ }
115
+
116
+ if (!options.dryRun) {
117
+ writeFileSync(fileAbs, operation.contents, "utf8");
118
+ }
119
+
120
+ const actualHash = sha256(readFileSync(fileAbs, "utf8"));
121
+ if (operation.toHash && actualHash !== operation.toHash) {
122
+ throw new Error(`restore-content hash mismatch: ${operation.file}`);
123
+ }
124
+
125
+ report.applied += 1;
126
+ continue;
127
+ }
128
+
129
+ report.skipped += 1;
130
+ } catch (error) {
131
+ report.failures.push({ operation, error: String(error?.message || error) });
132
+ }
133
+ }
134
+
135
+ mkdirSync(dirname(options.out), { recursive: true });
136
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
137
+
138
+ if (report.failures.length > 0) {
139
+ throw new Error(`rollback failed: ${report.failures.length} operation(s) failed`);
140
+ }
141
+
142
+ console.log(`rollback complete: applied=${report.applied}, skipped=${report.skipped}`);
143
+ console.log(`rollback report: ${normalize(relative(resolve("."), options.out))}`);
144
+ }