@guru-ai-product/ai-product-kit 0.1.251112172507
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/README.md +107 -0
- package/bin/setup.js +89 -0
- package/package.json +24 -0
- package/skills/aipk_design/GURU_AI.md +10 -0
- package/skills/aipk_design/SKILL.md +33 -0
- package/skills/aipk_design/auto_panel_splitter/SKILL.md +360 -0
- package/skills/aipk_design/auto_panel_splitter/scripts/auto_panel_splitter.js +399 -0
- package/skills/aipk_design/auto_panel_splitter/scripts/panel_asset_mapper.js +445 -0
- package/skills/aipk_development/GURU_AI.md +8 -0
- package/skills/aipk_development/SKILL.md +20 -0
- package/skills/aipk_development/templates//345/256/236/346/226/275/350/256/241/345/210/222/346/226/207/346/241/243.md +302 -0
- package/skills/aipk_init_project/SKILL.md +188 -0
- package/skills/aipk_init_project/scripts/check_agents.sh +55 -0
- package/skills/aipk_init_project/template/AGENTS_TEMPLATE.md +138 -0
- package/skills/aipk_operations/GURU_AI.md +10 -0
- package/skills/aipk_operations/SKILL.md +37 -0
- package/skills/aipk_operations/aso_new_release/SKILL.md +84 -0
- package/skills/aipk_operations/aso_new_release/references/aso-update-notes-prompt.md +196 -0
- package/skills/aipk_operations/aso_new_release/references/aso-update-notes-template.md +162 -0
- package/skills/aipk_requirements/GURU_AI.md +39 -0
- package/skills/aipk_requirements/SKILL.md +243 -0
- package/skills/aipk_requirements/changes/SKILL.md +196 -0
- package/skills/aipk_requirements/changes/template/index.md +30 -0
- package/skills/aipk_requirements/changes/template//351/234/200/346/261/202/345/217/230/345/212/250/350/257/264/346/230/216/346/226/207/346/241/243/346/250/241/346/235/277.md +576 -0
- package/skills/aipk_requirements/changes/template//351/234/200/346/261/202/345/217/230/345/212/250/350/257/264/346/230/216/347/224/237/346/210/220Prompt.md +349 -0
- package/skills/aipk_requirements/changes/template//351/234/200/346/261/202/345/217/230/345/212/250/350/257/264/346/230/216/347/224/237/346/210/220/346/214/207/345/215/227.md +292 -0
- package/skills/aipk_requirements/documentation/SKILL.md +930 -0
- package/skills/aipk_requirements/documentation/template/1_/344/272/247/345/223/201/345/256/232/344/275/215/344/270/216/345/210/206/346/236/220.md +86 -0
- package/skills/aipk_requirements/documentation/template/2_/345/212/237/350/203/275/351/234/200/346/261/202.md +67 -0
- package/skills/aipk_requirements/documentation/template/3_/345/225/206/344/270/232/345/214/226/347/255/226/347/225/245.md +131 -0
- package/skills/aipk_requirements/documentation/template/4_/347/224/250/346/210/267/347/225/214/351/235/242/344/270/216/344/275/223/351/252/214.md +665 -0
- package/skills/aipk_requirements/documentation/template/AI /345/272/224/347/224/250/347/261/273APP/351/200/232/347/224/250/345/237/213/347/202/271/346/226/207/346/241/243.xlsx +0 -0
- package/skills/aipk_requirements/documentation/template/Draft_/344/272/247/345/223/201/351/234/200/346/261/202/350/215/211/347/250/277.md +60 -0
- package/skills/aipk_requirements/documentation/template/Draft_/347/224/250/346/210/267/346/227/205/347/250/213/350/215/211/347/250/277.md +84 -0
- package/skills/aipk_requirements/documentation/template/index.md +83 -0
- package/skills/aipk_requirements/documentation/template//345/237/213/347/202/271/350/247/204/350/214/203/346/226/207/346/241/243.md +372 -0
- package/skills/aipk_requirements/documentation/template//351/234/200/346/261/202/350/277/255/344/273/243/346/250/241/346/235/277.md +821 -0
- package/skills/aipk_requirements/documentation/template//351/234/200/346/261/202/350/277/255/344/273/243/347/264/242/345/274/225.md +30 -0
- package/skills/aipk_requirements/initiative_planning/SKILL.md +407 -0
- package/skills/aipk_requirements/initiative_planning/template/SWOT/345/210/206/346/236/220/346/250/241/346/235/277.md +381 -0
- package/skills/aipk_requirements/initiative_planning/template//344/272/247/345/223/201/350/247/204/345/210/222/346/250/241/346/235/277.md +322 -0
- package/skills/aipk_requirements/initiative_planning/template//345/225/206/344/270/232/345/214/226/350/247/204/345/210/222/346/250/241/346/235/277.md +201 -0
- package/skills/aipk_requirements/initiative_planning/template//345/270/202/345/234/272/344/270/216/345/225/206/344/270/232/345/210/206/346/236/220/346/250/241/346/235/277.md +176 -0
- package/skills/aipk_requirements/initiative_planning/template//346/212/200/346/234/257/350/247/204/345/210/222/346/250/241/346/235/277.md +314 -0
- package/skills/aipk_requirements/intake/SKILL.md +260 -0
- package/skills/aipk_requirements/intake/examples/K12/346/225/260/345/255/246/345/255/246/344/271/240/345/272/224/347/224/250/346/241/210/344/276/213.md +371 -0
- package/skills/aipk_requirements/intake/examples//347/224/265/345/225/206/345/271/263/345/217/260/344/274/230/345/214/226/346/241/210/344/276/213.md +426 -0
- package/skills/aipk_requirements/intake/references/Gemini_DeepResearch_/345/267/245/345/205/267/351/223/276/346/225/264/345/220/210/346/236/266/346/236/204/350/256/276/350/256/241.md +1272 -0
- package/skills/aipk_requirements/intake/template//344/272/247/345/223/201/351/234/200/346/261/202/345/217/221/347/216/260/344/270/216/351/252/214/350/257/201/346/250/241/346/235/277.md +53 -0
- package/skills/aipk_requirements/intake/template//345/270/202/345/234/272/346/234/272/344/274/232/350/257/204/344/274/260/344/270/216/347/253/236/344/272/211/345/210/206/346/236/220/346/250/241/346/235/277.md +53 -0
- package/skills/aipk_requirements/intake/template//346/212/200/346/234/257/346/226/271/346/241/210/350/257/204/344/274/260/344/270/216/345/217/257/350/241/214/346/200/247/345/210/206/346/236/220/346/250/241/346/235/277.md +72 -0
- package/skills/aipk_requirements/intake/template//346/267/261/345/272/246/347/224/250/346/210/267/350/241/214/344/270/272/344/270/216/351/234/200/346/261/202/346/264/236/345/257/237/346/250/241/346/235/277.md +59 -0
- package/skills/aipk_requirements/review/SKILL.md +218 -0
- package/skills/aipk_skill_generate/GURU_AI.md +8 -0
- package/skills/aipk_skill_generate/SKILL.md +259 -0
- package/skills/aipk_skill_generate/agent_skills_spec.md +79 -0
- package/skills/aipk_tool_prompts/AI/344/275/223/351/252/214/350/256/276/350/256/241/346/214/207/345/215/227.md +93 -0
- package/skills/aipk_tool_prompts/GURU_AI.md +13 -0
- package/skills/aipk_tool_prompts/Prompt/347/224/237/346/210/220.md +20 -0
- package/skills/aipk_tool_prompts/SKILL.md +55 -0
- package/skills/aipk_tool_prompts/images/20250710200701.png +0 -0
- package/skills/aipk_tool_prompts/images/20250710200802.png +0 -0
- package/skills/aipk_tool_prompts//346/240/207/350/256/260/346/226/207/346/241/243/346/233/264/346/226/260.md +33 -0
- package/skills/aipk_tool_prompts//347/224/237/346/210/220DrawIO/346/226/207/346/241/243.md +45 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const fsp = require("node:fs/promises");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
const { parseArgs } = require("node:util");
|
|
9
|
+
const sharp = require("sharp");
|
|
10
|
+
|
|
11
|
+
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
12
|
+
const FORMAT_MAP = new Map([
|
|
13
|
+
[".png", "png"],
|
|
14
|
+
[".jpg", "jpeg"],
|
|
15
|
+
[".jpeg", "jpeg"],
|
|
16
|
+
[".webp", "webp"],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
class PanelRow {
|
|
20
|
+
constructor({ originalName, originalPath, fileSlug, version, panelTitle, notes }) {
|
|
21
|
+
this.originalName = originalName;
|
|
22
|
+
this.originalPath = originalPath;
|
|
23
|
+
this.fileSlug = fileSlug;
|
|
24
|
+
this.version = version;
|
|
25
|
+
this.panelTitle = panelTitle;
|
|
26
|
+
this.notes = notes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
finalStem() {
|
|
30
|
+
return this.version ? `${this.fileSlug}-${this.version}` : this.fileSlug;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
extension() {
|
|
34
|
+
return path.extname(this.originalName).toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class CopyResult {
|
|
39
|
+
constructor(row, destination, status, newHash, existingHash) {
|
|
40
|
+
this.row = row;
|
|
41
|
+
this.destination = destination;
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.newHash = newHash;
|
|
44
|
+
this.existingHash = existingHash;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseCli() {
|
|
49
|
+
const { values } = parseArgs({
|
|
50
|
+
allowPositionals: false,
|
|
51
|
+
options: {
|
|
52
|
+
source: { type: "string" },
|
|
53
|
+
mapping: { type: "string" },
|
|
54
|
+
"init-mapping": { type: "boolean", default: false },
|
|
55
|
+
force: { type: "boolean", default: false },
|
|
56
|
+
destination: { type: "string" },
|
|
57
|
+
"doc-root": { type: "string", default: "../requirements/documentation" },
|
|
58
|
+
"doc-slug": { type: "string" },
|
|
59
|
+
iteration: { type: "string" },
|
|
60
|
+
"dry-run": { type: "boolean", default: false },
|
|
61
|
+
"diff-report": { type: "string" },
|
|
62
|
+
"update-mapping-notes": { type: "boolean", default: false },
|
|
63
|
+
"trim-top": { type: "string", default: "0" },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!values.source) {
|
|
68
|
+
throw new Error("--source is required.");
|
|
69
|
+
}
|
|
70
|
+
if (!values.mapping) {
|
|
71
|
+
throw new Error("--mapping is required.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const toInt = (name) => {
|
|
75
|
+
const value = Number(values[name]);
|
|
76
|
+
if (!Number.isFinite(value)) {
|
|
77
|
+
throw new Error(`Option --${name} must be a number.`);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
source: path.resolve(process.cwd(), values.source),
|
|
84
|
+
mapping: path.resolve(process.cwd(), values.mapping),
|
|
85
|
+
initMapping: Boolean(values["init-mapping"]),
|
|
86
|
+
force: Boolean(values.force),
|
|
87
|
+
destination: values.destination
|
|
88
|
+
? path.resolve(process.cwd(), values.destination)
|
|
89
|
+
: undefined,
|
|
90
|
+
docRoot: path.resolve(process.cwd(), values["doc-root"]),
|
|
91
|
+
docSlug: values["doc-slug"],
|
|
92
|
+
iteration: values.iteration,
|
|
93
|
+
dryRun: Boolean(values["dry-run"]),
|
|
94
|
+
diffReport: values["diff-report"]
|
|
95
|
+
? path.resolve(process.cwd(), values["diff-report"])
|
|
96
|
+
: undefined,
|
|
97
|
+
updateMappingNotes: Boolean(values["update-mapping-notes"]),
|
|
98
|
+
trimTop: toInt("trim-top"),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function pathExists(target) {
|
|
103
|
+
try {
|
|
104
|
+
await fsp.access(target);
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function ensureDirectory(dirPath) {
|
|
112
|
+
await fsp.mkdir(dirPath, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function discoverImages(sourceDir) {
|
|
116
|
+
const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
|
|
117
|
+
return entries
|
|
118
|
+
.filter((entry) => entry.isFile() && IMAGE_EXTS.has(path.extname(entry.name).toLowerCase()))
|
|
119
|
+
.map((entry) => entry.name)
|
|
120
|
+
.sort();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stringifyCsvRow(values) {
|
|
124
|
+
return values
|
|
125
|
+
.map((value = "") => {
|
|
126
|
+
const str = String(value ?? "");
|
|
127
|
+
if (/[",\n\r]/.test(str)) {
|
|
128
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
129
|
+
}
|
|
130
|
+
return str;
|
|
131
|
+
})
|
|
132
|
+
.join(",");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stringifyCsv(rows, header) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
if (header) {
|
|
138
|
+
lines.push(stringifyCsvRow(header));
|
|
139
|
+
}
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
lines.push(stringifyCsvRow(row));
|
|
142
|
+
}
|
|
143
|
+
return `${lines.join("\n")}\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseCsv(content) {
|
|
147
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
148
|
+
const rows = [];
|
|
149
|
+
let field = "";
|
|
150
|
+
let row = [];
|
|
151
|
+
let inQuotes = false;
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
154
|
+
const char = normalized[i];
|
|
155
|
+
if (inQuotes) {
|
|
156
|
+
if (char === '"') {
|
|
157
|
+
if (normalized[i + 1] === '"') {
|
|
158
|
+
field += '"';
|
|
159
|
+
i += 1;
|
|
160
|
+
} else {
|
|
161
|
+
inQuotes = false;
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
field += char;
|
|
165
|
+
}
|
|
166
|
+
} else if (char === '"') {
|
|
167
|
+
inQuotes = true;
|
|
168
|
+
} else if (char === ',') {
|
|
169
|
+
row.push(field);
|
|
170
|
+
field = "";
|
|
171
|
+
} else if (char === "\n") {
|
|
172
|
+
row.push(field);
|
|
173
|
+
rows.push(row);
|
|
174
|
+
row = [];
|
|
175
|
+
field = "";
|
|
176
|
+
} else {
|
|
177
|
+
field += char;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (inQuotes) {
|
|
182
|
+
throw new Error("CSV parsing error: unterminated quoted field.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (field.length > 0 || row.length > 0) {
|
|
186
|
+
row.push(field);
|
|
187
|
+
rows.push(row);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return rows.filter((cells) => cells.some((cell) => cell.trim().length > 0));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function writeMappingTemplate(images, mappingPath, force) {
|
|
194
|
+
if (await pathExists(mappingPath)) {
|
|
195
|
+
if (!force) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Mapping file ${mappingPath} already exists. Pass --force to overwrite.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
await ensureDirectory(path.dirname(mappingPath));
|
|
202
|
+
const header = ["original_name", "file_slug", "version", "panel_title", "notes"];
|
|
203
|
+
const rows = images.map((name) => [name, path.parse(name).name, "", "", ""]);
|
|
204
|
+
await fsp.writeFile(mappingPath, stringifyCsv(rows, header), "utf8");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function loadMapping(sourceDir, mappingPath) {
|
|
208
|
+
if (!(await pathExists(mappingPath))) {
|
|
209
|
+
throw new Error(`Mapping file ${mappingPath} does not exist. Run with --init-mapping first.`);
|
|
210
|
+
}
|
|
211
|
+
const content = await fsp.readFile(mappingPath, "utf8");
|
|
212
|
+
const rows = parseCsv(content);
|
|
213
|
+
if (!rows.length) {
|
|
214
|
+
throw new Error(`Mapping file ${mappingPath} has no rows.`);
|
|
215
|
+
}
|
|
216
|
+
const header = rows[0].map((col) => col.trim());
|
|
217
|
+
const required = ["original_name", "file_slug", "version", "panel_title", "notes"];
|
|
218
|
+
const missing = required.filter((col) => !header.includes(col));
|
|
219
|
+
if (missing.length) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Mapping file ${mappingPath} is missing columns: ${missing.sort().join(", ")}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const indexMap = new Map(header.map((col, idx) => [col, idx]));
|
|
225
|
+
const records = [];
|
|
226
|
+
for (let i = 1; i < rows.length; i += 1) {
|
|
227
|
+
const row = rows[i];
|
|
228
|
+
const lineNum = i + 1;
|
|
229
|
+
const get = (col) => (row[indexMap.get(col)] ?? "").trim();
|
|
230
|
+
const originalName = get("original_name");
|
|
231
|
+
const fileSlug = get("file_slug");
|
|
232
|
+
if (!originalName) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const originalPath = path.join(sourceDir, originalName);
|
|
236
|
+
if (!(await pathExists(originalPath))) {
|
|
237
|
+
throw new Error(`Row ${lineNum}: source image ${originalPath} does not exist.`);
|
|
238
|
+
}
|
|
239
|
+
if (!fileSlug) {
|
|
240
|
+
throw new Error(`Row ${lineNum}: file_slug is required.`);
|
|
241
|
+
}
|
|
242
|
+
if (/[\\/]/.test(fileSlug)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Row ${lineNum}: file_slug '${fileSlug}' contains path separator characters.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
records.push(
|
|
248
|
+
new PanelRow({
|
|
249
|
+
originalName,
|
|
250
|
+
originalPath,
|
|
251
|
+
fileSlug,
|
|
252
|
+
version: get("version"),
|
|
253
|
+
panelTitle: get("panel_title"),
|
|
254
|
+
notes: get("notes"),
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (!records.length) {
|
|
259
|
+
throw new Error(`No rows found in mapping file ${mappingPath}.`);
|
|
260
|
+
}
|
|
261
|
+
return records;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveDestination(args) {
|
|
265
|
+
if (args.destination) {
|
|
266
|
+
return args.destination;
|
|
267
|
+
}
|
|
268
|
+
if (!args.docSlug || !args.iteration) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
"Either provide --destination or both --doc-slug and --iteration to resolve the final path."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return path.join(args.docRoot, args.docSlug, "assets", args.iteration);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function fileSha256(filePath) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const hash = crypto.createHash("sha256");
|
|
279
|
+
const stream = fs.createReadStream(filePath);
|
|
280
|
+
stream.on("error", reject);
|
|
281
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
282
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function preparePanelBuffer(srcPath, trimTop) {
|
|
287
|
+
if (trimTop <= 0) {
|
|
288
|
+
return fsp.readFile(srcPath);
|
|
289
|
+
}
|
|
290
|
+
const ext = path.extname(srcPath).toLowerCase();
|
|
291
|
+
const pipeline = sharp(srcPath);
|
|
292
|
+
const metadata = await pipeline.metadata();
|
|
293
|
+
if (!metadata.height || !metadata.width) {
|
|
294
|
+
throw new Error(`Unable to read image metadata for ${srcPath}.`);
|
|
295
|
+
}
|
|
296
|
+
const top = Math.min(trimTop, Math.max(metadata.height - 1, 0));
|
|
297
|
+
const height = Math.max(metadata.height - top, 1);
|
|
298
|
+
const formatter = FORMAT_MAP.get(ext) || "png";
|
|
299
|
+
return pipeline
|
|
300
|
+
.extract({ left: 0, top, width: metadata.width, height })
|
|
301
|
+
.toFormat(formatter)
|
|
302
|
+
.toBuffer();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function evaluatePanel(preparedBuffer, destination) {
|
|
306
|
+
const newHash = crypto.createHash("sha256").update(preparedBuffer).digest("hex");
|
|
307
|
+
if (!(await pathExists(destination))) {
|
|
308
|
+
return { status: "missing", newHash, existingHash: null };
|
|
309
|
+
}
|
|
310
|
+
const existingHash = await fileSha256(destination);
|
|
311
|
+
if (existingHash === newHash) {
|
|
312
|
+
return { status: "identical", newHash, existingHash };
|
|
313
|
+
}
|
|
314
|
+
return { status: "different", newHash, existingHash };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function copyPanels(rows, destDir, dryRun, trimTop) {
|
|
318
|
+
const results = [];
|
|
319
|
+
for (const row of rows) {
|
|
320
|
+
const target = path.join(destDir, `${row.finalStem()}${row.extension()}`);
|
|
321
|
+
const prepared = await preparePanelBuffer(row.originalPath, trimTop);
|
|
322
|
+
const evaluation = await evaluatePanel(prepared, target);
|
|
323
|
+
results.push(new CopyResult(row, target, evaluation.status, evaluation.newHash, evaluation.existingHash));
|
|
324
|
+
if (dryRun || evaluation.status === "identical") {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
await ensureDirectory(path.dirname(target));
|
|
328
|
+
await fsp.writeFile(target, prepared);
|
|
329
|
+
}
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function writeDiffReport(results, reportPath) {
|
|
334
|
+
await ensureDirectory(path.dirname(reportPath));
|
|
335
|
+
const header = [
|
|
336
|
+
"original_name",
|
|
337
|
+
"export_name",
|
|
338
|
+
"destination",
|
|
339
|
+
"status",
|
|
340
|
+
"new_sha256",
|
|
341
|
+
"existing_sha256",
|
|
342
|
+
];
|
|
343
|
+
const rows = results.map((res) => [
|
|
344
|
+
res.row.originalName,
|
|
345
|
+
`${res.row.finalStem()}${res.row.extension()}`,
|
|
346
|
+
res.destination,
|
|
347
|
+
res.status,
|
|
348
|
+
res.newHash,
|
|
349
|
+
res.existingHash || "",
|
|
350
|
+
]);
|
|
351
|
+
await fsp.writeFile(reportPath, stringifyCsv(rows, header), "utf8");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function appendDiffNotes(rows, results) {
|
|
355
|
+
const noteMap = {
|
|
356
|
+
missing: "不一致:目标缺失",
|
|
357
|
+
different: "不一致:像素不一致",
|
|
358
|
+
};
|
|
359
|
+
rows.forEach((row, idx) => {
|
|
360
|
+
const result = results[idx];
|
|
361
|
+
if (!result) return;
|
|
362
|
+
const note = noteMap[result.status];
|
|
363
|
+
if (!note) return;
|
|
364
|
+
if (row.notes.includes(note)) return;
|
|
365
|
+
row.notes = row.notes ? `${row.notes};${note}` : note;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function writeMapping(rows, mappingPath) {
|
|
370
|
+
await ensureDirectory(path.dirname(mappingPath));
|
|
371
|
+
const header = ["original_name", "file_slug", "version", "panel_title", "notes"];
|
|
372
|
+
const dataRows = rows.map((row) => [
|
|
373
|
+
row.originalName,
|
|
374
|
+
row.fileSlug,
|
|
375
|
+
row.version,
|
|
376
|
+
row.panelTitle,
|
|
377
|
+
row.notes,
|
|
378
|
+
]);
|
|
379
|
+
await fsp.writeFile(mappingPath, stringifyCsv(dataRows, header), "utf8");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function main() {
|
|
383
|
+
const args = parseCli();
|
|
384
|
+
if (!(await pathExists(args.source))) {
|
|
385
|
+
throw new Error(`Source directory ${args.source} does not exist.`);
|
|
386
|
+
}
|
|
387
|
+
if (args.updateMappingNotes && args.dryRun) {
|
|
388
|
+
throw new Error("--update-mapping-notes cannot be combined with --dry-run.");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (args.initMapping) {
|
|
392
|
+
const images = await discoverImages(args.source);
|
|
393
|
+
if (!images.length) {
|
|
394
|
+
throw new Error(`No image files found under ${args.source}.`);
|
|
395
|
+
}
|
|
396
|
+
await writeMappingTemplate(images, args.mapping, args.force);
|
|
397
|
+
console.log(`Wrote template mapping for ${images.length} panels to ${args.mapping}.`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const rows = await loadMapping(args.source, args.mapping);
|
|
402
|
+
const destDir = resolveDestination(args);
|
|
403
|
+
const results = await copyPanels(rows, destDir, args.dryRun, args.trimTop);
|
|
404
|
+
|
|
405
|
+
const summary = new Map();
|
|
406
|
+
for (const res of results) {
|
|
407
|
+
summary.set(res.status, (summary.get(res.status) || 0) + 1);
|
|
408
|
+
let action;
|
|
409
|
+
if (args.dryRun) {
|
|
410
|
+
action = "DRY-RUN";
|
|
411
|
+
} else if (res.status === "identical") {
|
|
412
|
+
action = "Skipped";
|
|
413
|
+
} else if (res.status === "missing") {
|
|
414
|
+
action = "Copied";
|
|
415
|
+
} else {
|
|
416
|
+
action = "Overwrote";
|
|
417
|
+
}
|
|
418
|
+
console.log(`${action} ${res.row.originalPath} -> ${res.destination} [${res.status}]`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (args.diffReport) {
|
|
422
|
+
await writeDiffReport(results, args.diffReport);
|
|
423
|
+
console.log(`Diff report written to ${args.diffReport}.`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (args.updateMappingNotes) {
|
|
427
|
+
appendDiffNotes(rows, results);
|
|
428
|
+
await writeMapping(rows, args.mapping);
|
|
429
|
+
console.log(`Mapping notes updated with diff status: ${args.mapping}.`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const copies = (summary.get("missing") || 0) + (summary.get("different") || 0);
|
|
433
|
+
console.log(
|
|
434
|
+
`Evaluated ${results.length} panels under ${destDir} (copied ${copies}, identical ${
|
|
435
|
+
summary.get("identical") || 0
|
|
436
|
+
}).`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (require.main === module) {
|
|
441
|
+
main().catch((err) => {
|
|
442
|
+
console.error(err.message || err);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: development
|
|
3
|
+
description: Development phase index pointing to the single authoritative implementation plan template.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 开发阶段模板索引
|
|
7
|
+
|
|
8
|
+
开发阶段目前仅保留一个标准化资产——`实施计划文档` 模板。所有与开发过程相关的说明、Checklist、交付要求都应在该模板中进行维护和引用。
|
|
9
|
+
|
|
10
|
+
## 可用模板
|
|
11
|
+
|
|
12
|
+
- [`实施计划文档`](./templates/实施计划文档.md) — 统一的执行计划模板,覆盖需求对齐、设计联动、开发排期、验收要求与发布交接等内容。
|
|
13
|
+
|
|
14
|
+
## 使用说明
|
|
15
|
+
|
|
16
|
+
1. 在创建或更新开发计划时,始终从上述模板拷贝,并保留原有结构。
|
|
17
|
+
2. 如需新增章节或规范,请在模板中补充,并保持变更记录,确保后续项目能够复用。
|
|
18
|
+
3. 若模板内容与其他文档存在冲突,以此模板为准,并在 `../requirements` 或 `../operations` 中补充引用说明。
|
|
19
|
+
|
|
20
|
+
> 所有与开发执行相关的新材料(脚本、指南、Checklist)都应收敛到此模板,以确保唯一且可复用的来源。
|