@oss-ma/tpl 1.0.37 → 1.0.38
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/commands/update.js +169 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// cli/src/commands/update.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { confirm } from "@inquirer/prompts";
|
|
8
|
+
import { readTemplateLock } from "../engine/readLock.js";
|
|
9
|
+
import { loadTemplate } from "../engine/loadTemplate.js";
|
|
10
|
+
import { writeTemplateLock } from "../engine/lock.js";
|
|
11
|
+
async function hashFile(filePath) {
|
|
12
|
+
const content = await fs.readFile(filePath);
|
|
13
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
async function collectTemplateFiles(filesDir) {
|
|
16
|
+
const files = new Set();
|
|
17
|
+
const entries = await fs.readdir(filesDir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const full = path.join(filesDir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
const subFiles = await collectTemplateFiles(full);
|
|
22
|
+
for (const sub of subFiles) {
|
|
23
|
+
files.add(path.join(entry.name, sub).replace(/\\/g, "/"));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (entry.isFile()) {
|
|
27
|
+
files.add(entry.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
export const updateCommand = new Command("update")
|
|
33
|
+
.option("--path <path>", "Project path", ".")
|
|
34
|
+
.option("--yes", "Auto-accept all updates without prompts")
|
|
35
|
+
.option("--dry-run", "Show what would be updated without making changes")
|
|
36
|
+
.description("Update project to latest template version")
|
|
37
|
+
.action(async (opts) => {
|
|
38
|
+
try {
|
|
39
|
+
const projectPath = path.resolve(opts.path);
|
|
40
|
+
// 1) Read current lock
|
|
41
|
+
const lock = await readTemplateLock(projectPath);
|
|
42
|
+
if (!lock) {
|
|
43
|
+
console.log(pc.red("❌ Error:"), "No template.lock found. Project not generated by tpl.");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(pc.cyan("Current version:"), `${lock.template}@${lock.version}`);
|
|
47
|
+
// 2) Load current and latest template
|
|
48
|
+
const currentTemplate = await loadTemplate(lock.template);
|
|
49
|
+
const latestVersion = currentTemplate.spec.version;
|
|
50
|
+
if (lock.version === latestVersion) {
|
|
51
|
+
console.log(pc.green("✅"), "Already at latest version");
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
console.log(pc.cyan("Latest version:"), `${lock.template}@${latestVersion}`);
|
|
55
|
+
if (!opts.yes && !opts.dryRun) {
|
|
56
|
+
const proceed = await confirm({
|
|
57
|
+
message: `Update from ${lock.version} to ${latestVersion}?`,
|
|
58
|
+
default: true,
|
|
59
|
+
});
|
|
60
|
+
if (!proceed) {
|
|
61
|
+
console.log("Update cancelled");
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 3) Analyze changes
|
|
66
|
+
const latestFiles = await collectTemplateFiles(currentTemplate.filesDir);
|
|
67
|
+
const changes = [];
|
|
68
|
+
for (const relPath of latestFiles) {
|
|
69
|
+
const projectFilePath = path.join(projectPath, relPath);
|
|
70
|
+
const exists = await fs.pathExists(projectFilePath);
|
|
71
|
+
if (!exists) {
|
|
72
|
+
changes.push({ path: relPath, status: "added", userModified: false });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const currentHash = await hashFile(projectFilePath);
|
|
76
|
+
const originalHash = lock.filesIntegrity?.[relPath];
|
|
77
|
+
const userModified = originalHash && currentHash !== originalHash;
|
|
78
|
+
// Compare with new template version
|
|
79
|
+
const templateFilePath = path.join(currentTemplate.filesDir, relPath);
|
|
80
|
+
const templateHash = await hashFile(templateFilePath);
|
|
81
|
+
if (currentHash === templateHash) {
|
|
82
|
+
changes.push({ path: relPath, status: "unchanged", userModified: false });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
changes.push({
|
|
86
|
+
path: relPath,
|
|
87
|
+
status: originalHash ? "modified" : "added",
|
|
88
|
+
userModified: !!userModified,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Check for deleted files
|
|
93
|
+
if (lock.filesIntegrity) {
|
|
94
|
+
for (const relPath of Object.keys(lock.filesIntegrity)) {
|
|
95
|
+
if (!latestFiles.has(relPath)) {
|
|
96
|
+
changes.push({ path: relPath, status: "deleted", userModified: false });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 4) Report changes
|
|
101
|
+
const toUpdate = changes.filter((c) => c.status !== "unchanged");
|
|
102
|
+
if (toUpdate.length === 0) {
|
|
103
|
+
console.log(pc.green("✅"), "No changes to apply");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
console.log("");
|
|
107
|
+
console.log(pc.bold("Changes to apply:"));
|
|
108
|
+
for (const change of toUpdate) {
|
|
109
|
+
const icon = change.status === "added"
|
|
110
|
+
? pc.green("+")
|
|
111
|
+
: change.status === "deleted"
|
|
112
|
+
? pc.red("-")
|
|
113
|
+
: pc.yellow("~");
|
|
114
|
+
const warning = change.userModified ? pc.red(" (user modified)") : "";
|
|
115
|
+
console.log(`${icon} ${change.path}${warning}`);
|
|
116
|
+
}
|
|
117
|
+
if (opts.dryRun) {
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(pc.cyan("Dry run mode — no changes made"));
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
// 5) Apply updates
|
|
123
|
+
console.log("");
|
|
124
|
+
let updated = 0;
|
|
125
|
+
let skipped = 0;
|
|
126
|
+
for (const change of toUpdate) {
|
|
127
|
+
const projectFilePath = path.join(projectPath, change.path);
|
|
128
|
+
const templateFilePath = path.join(currentTemplate.filesDir, change.path);
|
|
129
|
+
if (change.status === "deleted") {
|
|
130
|
+
console.log(pc.gray(`Skipping deletion: ${change.path}`));
|
|
131
|
+
skipped++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (change.userModified && !opts.yes) {
|
|
135
|
+
const overwrite = await confirm({
|
|
136
|
+
message: `Overwrite user-modified file: ${change.path}?`,
|
|
137
|
+
default: false,
|
|
138
|
+
});
|
|
139
|
+
if (!overwrite) {
|
|
140
|
+
console.log(pc.gray(`Skipped: ${change.path}`));
|
|
141
|
+
skipped++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await fs.copy(templateFilePath, projectFilePath, { overwrite: true });
|
|
146
|
+
console.log(pc.green(`Updated: ${change.path}`));
|
|
147
|
+
updated++;
|
|
148
|
+
}
|
|
149
|
+
// 6) Update lock
|
|
150
|
+
await writeTemplateLock({
|
|
151
|
+
destDir: projectPath,
|
|
152
|
+
template: lock.template,
|
|
153
|
+
version: latestVersion,
|
|
154
|
+
options: lock.options ?? {},
|
|
155
|
+
packs: lock.packs,
|
|
156
|
+
generatedAt: new Date().toISOString(),
|
|
157
|
+
});
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log(pc.green("✅ Update complete"));
|
|
160
|
+
console.log(`Updated: ${updated} files`);
|
|
161
|
+
if (skipped > 0)
|
|
162
|
+
console.log(`Skipped: ${skipped} files`);
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error(pc.red("❌ Error:"), err?.message ?? String(err));
|
|
167
|
+
process.exit(2);
|
|
168
|
+
}
|
|
169
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { initCommand } from "./commands/init.js";
|
|
4
4
|
import { checkCommand } from "./commands/check.js";
|
|
5
|
+
import { updateCommand } from "./commands/update.js";
|
|
5
6
|
const program = new Command();
|
|
6
7
|
program
|
|
7
8
|
.name("tpl")
|
|
@@ -9,4 +10,5 @@ program
|
|
|
9
10
|
.version("0.1.0");
|
|
10
11
|
program.addCommand(initCommand);
|
|
11
12
|
program.addCommand(checkCommand);
|
|
13
|
+
program.addCommand(updateCommand);
|
|
12
14
|
program.parse(process.argv);
|