@oss-ma/tpl 1.0.36 → 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
|
@@ -26,12 +26,12 @@ jobs:
|
|
|
26
26
|
persist-credentials: false
|
|
27
27
|
|
|
28
28
|
- name: Initialize CodeQL
|
|
29
|
-
uses: github/codeql-action/init@
|
|
29
|
+
uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f
|
|
30
30
|
with:
|
|
31
31
|
languages: javascript-typescript
|
|
32
32
|
queries: security-extended
|
|
33
33
|
|
|
34
34
|
- name: Perform CodeQL Analysis
|
|
35
|
-
uses: github/codeql-action/analyze@
|
|
35
|
+
uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f
|
|
36
36
|
with:
|
|
37
37
|
category: "/language:javascript-typescript"
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Next.js 15 · App Router · TypeScript
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Modern Next.js application with TypeScript, App Router, and professional tooling.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
npm run dev
|
|
@@ -19,7 +23,7 @@ npm run dev
|
|
|
19
23
|
- `npm run format` — Format code with Prettier
|
|
20
24
|
- `npm run typecheck` — Type check with TypeScript
|
|
21
25
|
|
|
22
|
-
##
|
|
26
|
+
## Architecture
|
|
23
27
|
|
|
24
28
|
```
|
|
25
29
|
src/
|
|
@@ -29,12 +33,22 @@ src/
|
|
|
29
33
|
└── lib/ # Library code (providers, etc.)
|
|
30
34
|
```
|
|
31
35
|
|
|
36
|
+
See [ADR documentation](docs/adr/) for architectural decisions.
|
|
37
|
+
|
|
32
38
|
## Standards
|
|
33
39
|
|
|
34
40
|
This project follows the [@oss-ma/tpl](https://www.npmjs.com/package/@oss-ma/tpl) standard.
|
|
35
41
|
|
|
36
42
|
Run `npx @oss-ma/tpl check` to validate compliance.
|
|
37
43
|
|
|
44
|
+
## Contributing
|
|
45
|
+
|
|
46
|
+
1. Fork the repository
|
|
47
|
+
2. Create a feature branch
|
|
48
|
+
3. Make your changes
|
|
49
|
+
4. Run `npm run lint && npm test`
|
|
50
|
+
5. Submit a pull request
|
|
51
|
+
|
|
38
52
|
## Security
|
|
39
53
|
|
|
40
54
|
See [SECURITY.md](SECURITY.md) for reporting vulnerabilities.
|