@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.
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-ma/tpl",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "description": "Generate, enforce and maintain clean project architectures",
5
5
  "type": "module",
6
6
  "repository": {