openspec-stat 1.2.0 → 1.3.1
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 +28 -0
- package/README.zh-CN.md +28 -0
- package/dist/cjs/cli.js +12 -127
- package/dist/cjs/commands/init.d.ts +7 -0
- package/dist/cjs/commands/init.js +58 -0
- package/dist/cjs/commands/multi.d.ts +16 -0
- package/dist/cjs/commands/multi.js +172 -0
- package/dist/cjs/commands/single.d.ts +2 -0
- package/dist/cjs/commands/single.js +148 -0
- package/dist/cjs/formatters.d.ts +4 -4
- package/dist/cjs/formatters.js +128 -36
- package/dist/cjs/git-analyzer.d.ts +1 -0
- package/dist/cjs/git-analyzer.js +6 -0
- package/dist/cjs/i18n/locales/en.json +74 -1
- package/dist/cjs/i18n/locales/zh-CN.json +74 -1
- package/dist/cjs/multi/config-validator.d.ts +3 -0
- package/dist/cjs/multi/config-validator.js +130 -0
- package/dist/cjs/multi/config-wizard.d.ts +50 -0
- package/dist/cjs/multi/config-wizard.js +331 -0
- package/dist/cjs/multi/multi-repo-analyzer.d.ts +14 -0
- package/dist/cjs/multi/multi-repo-analyzer.js +210 -0
- package/dist/cjs/types.d.ts +46 -0
- package/dist/esm/cli.js +54 -139
- package/dist/esm/commands/init.d.ts +7 -0
- package/dist/esm/commands/init.js +49 -0
- package/dist/esm/commands/multi.d.ts +16 -0
- package/dist/esm/commands/multi.js +192 -0
- package/dist/esm/commands/single.d.ts +2 -0
- package/dist/esm/commands/single.js +162 -0
- package/dist/esm/formatters.d.ts +4 -4
- package/dist/esm/formatters.js +173 -52
- package/dist/esm/git-analyzer.d.ts +1 -0
- package/dist/esm/git-analyzer.js +104 -77
- package/dist/esm/i18n/locales/en.json +74 -1
- package/dist/esm/i18n/locales/zh-CN.json +74 -1
- package/dist/esm/multi/config-validator.d.ts +3 -0
- package/dist/esm/multi/config-validator.js +109 -0
- package/dist/esm/multi/config-wizard.d.ts +50 -0
- package/dist/esm/multi/config-wizard.js +535 -0
- package/dist/esm/multi/multi-repo-analyzer.d.ts +14 -0
- package/dist/esm/multi/multi-repo-analyzer.js +446 -0
- package/dist/esm/types.d.ts +46 -0
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/multi/config-wizard.ts
|
|
30
|
+
var config_wizard_exports = {};
|
|
31
|
+
__export(config_wizard_exports, {
|
|
32
|
+
MULTI_REPO_TEMPLATE: () => MULTI_REPO_TEMPLATE,
|
|
33
|
+
SINGLE_REPO_TEMPLATE: () => SINGLE_REPO_TEMPLATE,
|
|
34
|
+
runConfigWizard: () => runConfigWizard
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(config_wizard_exports);
|
|
37
|
+
var import_prompts = require("@inquirer/prompts");
|
|
38
|
+
var import_chalk = __toESM(require("chalk"));
|
|
39
|
+
var import_fs = require("fs");
|
|
40
|
+
var import_i18n = require("../i18n/index.js");
|
|
41
|
+
async function runConfigWizard(isMultiRepo = false) {
|
|
42
|
+
console.log(import_chalk.default.blue.bold(isMultiRepo ? (0, import_i18n.t)("init.welcomeMulti") : (0, import_i18n.t)("init.welcome")));
|
|
43
|
+
const configName = await (0, import_prompts.input)({
|
|
44
|
+
message: (0, import_i18n.t)("init.configName"),
|
|
45
|
+
default: isMultiRepo ? ".openspec-stats.multi.json" : ".openspec-stats.json"
|
|
46
|
+
});
|
|
47
|
+
if (!isMultiRepo) {
|
|
48
|
+
const singleRepoConfig = await createSingleRepoConfig();
|
|
49
|
+
saveConfig(configName, singleRepoConfig);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const repositories = [];
|
|
53
|
+
let addMore = true;
|
|
54
|
+
while (addMore) {
|
|
55
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("init.addRepository", { number: String(repositories.length + 1) })));
|
|
56
|
+
const repo = await addRepository();
|
|
57
|
+
repositories.push(repo);
|
|
58
|
+
addMore = await (0, import_prompts.confirm)({
|
|
59
|
+
message: (0, import_i18n.t)("init.addMore"),
|
|
60
|
+
default: false
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("init.timeConfig")));
|
|
64
|
+
const timeConfig = await configureTimeRange();
|
|
65
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("init.advanced")));
|
|
66
|
+
const advancedConfig = await configureAdvanced();
|
|
67
|
+
const config = {
|
|
68
|
+
mode: "multi-repo",
|
|
69
|
+
repositories,
|
|
70
|
+
...timeConfig,
|
|
71
|
+
...advancedConfig
|
|
72
|
+
};
|
|
73
|
+
console.log(import_chalk.default.green((0, import_i18n.t)("init.preview")));
|
|
74
|
+
console.log(JSON.stringify(config, null, 2));
|
|
75
|
+
const confirmed = await (0, import_prompts.confirm)({
|
|
76
|
+
message: (0, import_i18n.t)("init.save"),
|
|
77
|
+
default: true
|
|
78
|
+
});
|
|
79
|
+
if (confirmed) {
|
|
80
|
+
saveConfig(configName, config);
|
|
81
|
+
console.log(import_chalk.default.green((0, import_i18n.t)("init.saved", { path: configName })));
|
|
82
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("init.runCommand", { path: configName })));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function createSingleRepoConfig() {
|
|
86
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("init.timeConfig")));
|
|
87
|
+
const timeConfig = await configureTimeRange();
|
|
88
|
+
const branches = await (0, import_prompts.input)({
|
|
89
|
+
message: (0, import_i18n.t)("init.branches"),
|
|
90
|
+
default: "origin/master"
|
|
91
|
+
});
|
|
92
|
+
const advancedConfig = await configureAdvanced();
|
|
93
|
+
return {
|
|
94
|
+
defaultBranches: branches.split(",").map((b) => b.trim()).filter(Boolean),
|
|
95
|
+
...timeConfig,
|
|
96
|
+
...advancedConfig
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function addRepository() {
|
|
100
|
+
const type = await (0, import_prompts.select)({
|
|
101
|
+
message: (0, import_i18n.t)("init.repoType"),
|
|
102
|
+
choices: [
|
|
103
|
+
{
|
|
104
|
+
name: (0, import_i18n.t)("init.repoType.local"),
|
|
105
|
+
value: "local"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: (0, import_i18n.t)("init.repoType.remote"),
|
|
109
|
+
value: "remote"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
const name = await (0, import_prompts.input)({
|
|
114
|
+
message: (0, import_i18n.t)("init.repoName"),
|
|
115
|
+
validate: (value) => value.length > 0 || "Name is required"
|
|
116
|
+
});
|
|
117
|
+
const repo = {
|
|
118
|
+
name,
|
|
119
|
+
type,
|
|
120
|
+
branches: []
|
|
121
|
+
};
|
|
122
|
+
if (type === "local") {
|
|
123
|
+
repo.path = await (0, import_prompts.input)({
|
|
124
|
+
message: (0, import_i18n.t)("init.repoPath"),
|
|
125
|
+
default: ".",
|
|
126
|
+
validate: (value) => value.length > 0 || "Path is required"
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
repo.url = await (0, import_prompts.input)({
|
|
130
|
+
message: (0, import_i18n.t)("init.repoUrl"),
|
|
131
|
+
validate: (value) => {
|
|
132
|
+
if (!value.length)
|
|
133
|
+
return "URL is required";
|
|
134
|
+
if (!value.match(/^(git@|https:\/\/)/)) {
|
|
135
|
+
return (0, import_i18n.t)("init.repoUrlInvalid");
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const useFullClone = await (0, import_prompts.confirm)({
|
|
141
|
+
message: (0, import_i18n.t)("init.useFullClone"),
|
|
142
|
+
default: true
|
|
143
|
+
});
|
|
144
|
+
if (!useFullClone) {
|
|
145
|
+
const depth = await (0, import_prompts.input)({
|
|
146
|
+
message: (0, import_i18n.t)("init.cloneDepth"),
|
|
147
|
+
default: "100",
|
|
148
|
+
validate: (value) => !isNaN(Number(value)) || "Must be a number"
|
|
149
|
+
});
|
|
150
|
+
repo.cloneOptions = { depth: Number(depth) };
|
|
151
|
+
} else {
|
|
152
|
+
repo.cloneOptions = { depth: null };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const branchInput = await (0, import_prompts.input)({
|
|
156
|
+
message: (0, import_i18n.t)("init.branches"),
|
|
157
|
+
default: "origin/master"
|
|
158
|
+
});
|
|
159
|
+
repo.branches = branchInput.split(",").map((b) => b.trim()).filter(Boolean);
|
|
160
|
+
return repo;
|
|
161
|
+
}
|
|
162
|
+
async function configureTimeRange() {
|
|
163
|
+
const useDefault = await (0, import_prompts.confirm)({
|
|
164
|
+
message: (0, import_i18n.t)("init.useDefaultTime"),
|
|
165
|
+
default: true
|
|
166
|
+
});
|
|
167
|
+
if (useDefault) {
|
|
168
|
+
return {
|
|
169
|
+
defaultSinceHours: -30,
|
|
170
|
+
defaultUntilHours: 20
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const sinceHours = await (0, import_prompts.input)({
|
|
174
|
+
message: (0, import_i18n.t)("init.sinceHours"),
|
|
175
|
+
default: "-30",
|
|
176
|
+
validate: (value) => !isNaN(Number(value)) || "Must be a number"
|
|
177
|
+
});
|
|
178
|
+
const untilHours = await (0, import_prompts.input)({
|
|
179
|
+
message: (0, import_i18n.t)("init.untilHours"),
|
|
180
|
+
default: "20",
|
|
181
|
+
validate: (value) => {
|
|
182
|
+
const num = Number(value);
|
|
183
|
+
return !isNaN(num) && num >= 0 && num <= 23 || "Must be 0-23";
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
defaultSinceHours: Number(sinceHours),
|
|
188
|
+
defaultUntilHours: Number(untilHours)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function configureAdvanced() {
|
|
192
|
+
const configureAdvanced2 = await (0, import_prompts.confirm)({
|
|
193
|
+
message: (0, import_i18n.t)("init.configureAdvanced"),
|
|
194
|
+
default: false
|
|
195
|
+
});
|
|
196
|
+
if (!configureAdvanced2) {
|
|
197
|
+
return getDefaultAdvancedConfig();
|
|
198
|
+
}
|
|
199
|
+
const config = {};
|
|
200
|
+
config.openspecDir = await (0, import_prompts.input)({
|
|
201
|
+
message: (0, import_i18n.t)("init.openspecDir"),
|
|
202
|
+
default: "openspec/"
|
|
203
|
+
});
|
|
204
|
+
const maxConcurrent = await (0, import_prompts.input)({
|
|
205
|
+
message: (0, import_i18n.t)("init.maxConcurrent"),
|
|
206
|
+
default: "3",
|
|
207
|
+
validate: (value) => {
|
|
208
|
+
const num = Number(value);
|
|
209
|
+
return !isNaN(num) && num > 0 || "Must be positive number";
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
config.parallelism = {
|
|
213
|
+
maxConcurrent: Number(maxConcurrent),
|
|
214
|
+
timeout: 6e5
|
|
215
|
+
};
|
|
216
|
+
config.remoteCache = {
|
|
217
|
+
dir: "/tmp/openspec-stat-cache",
|
|
218
|
+
autoCleanup: true,
|
|
219
|
+
cleanupOnComplete: true,
|
|
220
|
+
cleanupOnError: true
|
|
221
|
+
};
|
|
222
|
+
config.excludeExtensions = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp"];
|
|
223
|
+
config.activeUserWeeks = 2;
|
|
224
|
+
const addAuthorMapping = await (0, import_prompts.confirm)({
|
|
225
|
+
message: (0, import_i18n.t)("init.authorMapping"),
|
|
226
|
+
default: false
|
|
227
|
+
});
|
|
228
|
+
if (addAuthorMapping) {
|
|
229
|
+
config.authorMapping = await configureAuthorMapping();
|
|
230
|
+
} else {
|
|
231
|
+
config.authorMapping = {};
|
|
232
|
+
}
|
|
233
|
+
return config;
|
|
234
|
+
}
|
|
235
|
+
async function configureAuthorMapping() {
|
|
236
|
+
console.log(import_chalk.default.gray((0, import_i18n.t)("init.authorMappingInfo")));
|
|
237
|
+
const mapping = {};
|
|
238
|
+
let addMore = true;
|
|
239
|
+
while (addMore) {
|
|
240
|
+
const gitIdentity = await (0, import_prompts.input)({
|
|
241
|
+
message: (0, import_i18n.t)("init.gitIdentity")
|
|
242
|
+
});
|
|
243
|
+
if (!gitIdentity)
|
|
244
|
+
break;
|
|
245
|
+
const unifiedName = await (0, import_prompts.input)({
|
|
246
|
+
message: (0, import_i18n.t)("init.unifiedName", { identity: gitIdentity })
|
|
247
|
+
});
|
|
248
|
+
mapping[gitIdentity] = unifiedName;
|
|
249
|
+
addMore = await (0, import_prompts.confirm)({
|
|
250
|
+
message: (0, import_i18n.t)("init.addMoreMapping"),
|
|
251
|
+
default: false
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return mapping;
|
|
255
|
+
}
|
|
256
|
+
function getDefaultAdvancedConfig() {
|
|
257
|
+
return {
|
|
258
|
+
openspecDir: "openspec/",
|
|
259
|
+
excludeExtensions: [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp"],
|
|
260
|
+
activeUserWeeks: 2,
|
|
261
|
+
authorMapping: {},
|
|
262
|
+
parallelism: {
|
|
263
|
+
maxConcurrent: 3,
|
|
264
|
+
timeout: 6e5
|
|
265
|
+
},
|
|
266
|
+
remoteCache: {
|
|
267
|
+
dir: "/tmp/openspec-stat-cache",
|
|
268
|
+
autoCleanup: true,
|
|
269
|
+
cleanupOnComplete: true,
|
|
270
|
+
cleanupOnError: true
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function saveConfig(path, config) {
|
|
275
|
+
(0, import_fs.writeFileSync)(path, JSON.stringify(config, null, 2));
|
|
276
|
+
}
|
|
277
|
+
var SINGLE_REPO_TEMPLATE = {
|
|
278
|
+
defaultBranches: ["origin/master"],
|
|
279
|
+
defaultSinceHours: -30,
|
|
280
|
+
defaultUntilHours: 20,
|
|
281
|
+
authorMapping: {
|
|
282
|
+
"user@email1.com": "User Name",
|
|
283
|
+
"user@email2.com": "User Name"
|
|
284
|
+
},
|
|
285
|
+
openspecDir: "openspec/",
|
|
286
|
+
excludeExtensions: [".md", ".txt", ".png", ".jpg"],
|
|
287
|
+
activeUserWeeks: 2
|
|
288
|
+
};
|
|
289
|
+
var MULTI_REPO_TEMPLATE = {
|
|
290
|
+
mode: "multi-repo",
|
|
291
|
+
repositories: [
|
|
292
|
+
{
|
|
293
|
+
name: "example-local-repo",
|
|
294
|
+
type: "local",
|
|
295
|
+
path: "./path/to/repo",
|
|
296
|
+
branches: ["origin/master"]
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "example-remote-repo",
|
|
300
|
+
type: "remote",
|
|
301
|
+
url: "git@github.com:org/repo.git",
|
|
302
|
+
branches: ["origin/main"],
|
|
303
|
+
cloneOptions: {
|
|
304
|
+
depth: null,
|
|
305
|
+
singleBranch: false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
defaultSinceHours: -30,
|
|
310
|
+
defaultUntilHours: 20,
|
|
311
|
+
authorMapping: {},
|
|
312
|
+
openspecDir: "openspec/",
|
|
313
|
+
excludeExtensions: [".md", ".txt", ".png", ".jpg"],
|
|
314
|
+
activeUserWeeks: 2,
|
|
315
|
+
parallelism: {
|
|
316
|
+
maxConcurrent: 3,
|
|
317
|
+
timeout: 6e5
|
|
318
|
+
},
|
|
319
|
+
remoteCache: {
|
|
320
|
+
dir: "/tmp/openspec-stat-cache",
|
|
321
|
+
autoCleanup: true,
|
|
322
|
+
cleanupOnComplete: true,
|
|
323
|
+
cleanupOnError: true
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
327
|
+
0 && (module.exports = {
|
|
328
|
+
MULTI_REPO_TEMPLATE,
|
|
329
|
+
SINGLE_REPO_TEMPLATE,
|
|
330
|
+
runConfigWizard
|
|
331
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MultiRepoConfig, RepositoryResult } from '../types.js';
|
|
2
|
+
export declare class MultiRepoAnalyzer {
|
|
3
|
+
private config;
|
|
4
|
+
private tempDirs;
|
|
5
|
+
constructor(config: MultiRepoConfig);
|
|
6
|
+
analyzeAll(since: Date, until: Date): Promise<RepositoryResult[]>;
|
|
7
|
+
private analyzeRepository;
|
|
8
|
+
private cloneRemoteRepository;
|
|
9
|
+
private resolveLocalPath;
|
|
10
|
+
private processInBatches;
|
|
11
|
+
private withTimeout;
|
|
12
|
+
cleanupTempDirs(): Promise<void>;
|
|
13
|
+
registerCleanupHandlers(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/multi/multi-repo-analyzer.ts
|
|
30
|
+
var multi_repo_analyzer_exports = {};
|
|
31
|
+
__export(multi_repo_analyzer_exports, {
|
|
32
|
+
MultiRepoAnalyzer: () => MultiRepoAnalyzer
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(multi_repo_analyzer_exports);
|
|
35
|
+
var import_simple_git = __toESM(require("simple-git"));
|
|
36
|
+
var import_fs = require("fs");
|
|
37
|
+
var import_os = require("os");
|
|
38
|
+
var import_path = require("path");
|
|
39
|
+
var import_chalk = __toESM(require("chalk"));
|
|
40
|
+
var import_git_analyzer = require("../git-analyzer.js");
|
|
41
|
+
var import_i18n = require("../i18n/index.js");
|
|
42
|
+
var MultiRepoAnalyzer = class {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
this.tempDirs = /* @__PURE__ */ new Set();
|
|
45
|
+
this.config = config;
|
|
46
|
+
}
|
|
47
|
+
async analyzeAll(since, until) {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const repos = this.config.repositories || [];
|
|
50
|
+
const enabledRepos = repos.filter((r) => r.enabled !== false);
|
|
51
|
+
try {
|
|
52
|
+
const results = await this.processInBatches(
|
|
53
|
+
enabledRepos,
|
|
54
|
+
(repo) => this.analyzeRepository(repo, since, until),
|
|
55
|
+
((_a = this.config.parallelism) == null ? void 0 : _a.maxConcurrent) || 3
|
|
56
|
+
);
|
|
57
|
+
return results;
|
|
58
|
+
} finally {
|
|
59
|
+
if ((_b = this.config.remoteCache) == null ? void 0 : _b.cleanupOnComplete) {
|
|
60
|
+
await this.cleanupTempDirs();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async analyzeRepository(repo, since, until) {
|
|
65
|
+
let repoPath;
|
|
66
|
+
try {
|
|
67
|
+
if (repo.enabled === false) {
|
|
68
|
+
console.log(import_chalk.default.gray((0, import_i18n.t)("multi.repo.skipped", { repo: repo.name })));
|
|
69
|
+
return {
|
|
70
|
+
repository: repo.name,
|
|
71
|
+
type: repo.type,
|
|
72
|
+
path: "",
|
|
73
|
+
analyses: [],
|
|
74
|
+
success: false,
|
|
75
|
+
error: "disabled"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("multi.repo.analyzing", { repo: repo.name, type: repo.type })));
|
|
79
|
+
if (repo.type === "local") {
|
|
80
|
+
repoPath = this.resolveLocalPath(repo.path);
|
|
81
|
+
if (!(0, import_fs.existsSync)((0, import_path.join)(repoPath, ".git"))) {
|
|
82
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
repoPath = await this.cloneRemoteRepository(repo);
|
|
86
|
+
}
|
|
87
|
+
const analyzer = new import_git_analyzer.GitAnalyzer(repoPath, this.config);
|
|
88
|
+
if (repo.type === "local" && this.config.autoFetch !== false) {
|
|
89
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("multi.repo.fetching", { repo: repo.name })));
|
|
90
|
+
await analyzer.fetchRemote();
|
|
91
|
+
}
|
|
92
|
+
const commits = await analyzer.getCommits(since, until, repo.branches);
|
|
93
|
+
const analyses = [];
|
|
94
|
+
for (const commit of commits) {
|
|
95
|
+
const analysis = await analyzer.analyzeCommit(commit);
|
|
96
|
+
if (analysis) {
|
|
97
|
+
analysis.repository = repo.name;
|
|
98
|
+
analysis.repositoryType = repo.type;
|
|
99
|
+
analyses.push(analysis);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.log(import_chalk.default.green((0, import_i18n.t)("multi.repo.completed", { repo: repo.name, commits: String(analyses.length) })));
|
|
103
|
+
return {
|
|
104
|
+
repository: repo.name,
|
|
105
|
+
type: repo.type,
|
|
106
|
+
path: repoPath,
|
|
107
|
+
analyses,
|
|
108
|
+
success: true
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
112
|
+
console.error(import_chalk.default.red((0, import_i18n.t)("multi.repo.failed", { repo: repo.name, error: errorMessage })));
|
|
113
|
+
return {
|
|
114
|
+
repository: repo.name,
|
|
115
|
+
type: repo.type,
|
|
116
|
+
path: "",
|
|
117
|
+
analyses: [],
|
|
118
|
+
success: false,
|
|
119
|
+
error: errorMessage
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async cloneRemoteRepository(repo) {
|
|
124
|
+
var _a, _b, _c, _d, _e;
|
|
125
|
+
const cacheDir = ((_a = this.config.remoteCache) == null ? void 0 : _a.dir) || (0, import_path.join)((0, import_os.tmpdir)(), "openspec-stat-cache");
|
|
126
|
+
if (!(0, import_fs.existsSync)(cacheDir)) {
|
|
127
|
+
(0, import_fs.mkdirSync)(cacheDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
const tempDir = (0, import_fs.mkdtempSync)((0, import_path.join)(cacheDir, `${repo.name}-`));
|
|
130
|
+
this.tempDirs.add(tempDir);
|
|
131
|
+
console.log(import_chalk.default.cyan((0, import_i18n.t)("multi.repo.cloning", { repo: repo.name })));
|
|
132
|
+
const git = (0, import_simple_git.default)();
|
|
133
|
+
const cloneArgs = [];
|
|
134
|
+
if (((_b = repo.cloneOptions) == null ? void 0 : _b.depth) !== null && ((_c = repo.cloneOptions) == null ? void 0 : _c.depth) !== void 0) {
|
|
135
|
+
cloneArgs.push(`--depth=${repo.cloneOptions.depth}`);
|
|
136
|
+
}
|
|
137
|
+
if ((_d = repo.cloneOptions) == null ? void 0 : _d.singleBranch) {
|
|
138
|
+
cloneArgs.push("--single-branch");
|
|
139
|
+
}
|
|
140
|
+
const timeout = ((_e = this.config.parallelism) == null ? void 0 : _e.timeout) || 6e5;
|
|
141
|
+
await this.withTimeout(git.clone(repo.url, tempDir, cloneArgs), timeout, `Clone timeout for ${repo.name}`);
|
|
142
|
+
console.log(import_chalk.default.green((0, import_i18n.t)("multi.repo.cloned", { repo: repo.name })));
|
|
143
|
+
return tempDir;
|
|
144
|
+
}
|
|
145
|
+
resolveLocalPath(path) {
|
|
146
|
+
if (path.startsWith("/") || path.match(/^[A-Za-z]:\\/)) {
|
|
147
|
+
return path;
|
|
148
|
+
}
|
|
149
|
+
return (0, import_path.resolve)(process.cwd(), path);
|
|
150
|
+
}
|
|
151
|
+
async processInBatches(items, processor, concurrency) {
|
|
152
|
+
const results = [];
|
|
153
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
154
|
+
const batch = items.slice(i, i + concurrency);
|
|
155
|
+
const batchNumber = Math.floor(i / concurrency) + 1;
|
|
156
|
+
const totalBatches = Math.ceil(items.length / concurrency);
|
|
157
|
+
if (totalBatches > 1) {
|
|
158
|
+
console.log(
|
|
159
|
+
import_chalk.default.gray((0, import_i18n.t)("multi.progress.batch", { current: String(batchNumber), total: String(totalBatches) }))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const batchResults = await Promise.all(batch.map(processor));
|
|
163
|
+
results.push(...batchResults);
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
async withTimeout(promise, timeout, errorMessage) {
|
|
168
|
+
return Promise.race([
|
|
169
|
+
promise,
|
|
170
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout))
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
async cleanupTempDirs() {
|
|
174
|
+
if (this.tempDirs.size === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(import_chalk.default.gray((0, import_i18n.t)("multi.cleanup.start")));
|
|
178
|
+
for (const dir of this.tempDirs) {
|
|
179
|
+
try {
|
|
180
|
+
if ((0, import_fs.existsSync)(dir)) {
|
|
181
|
+
(0, import_fs.rmSync)(dir, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.warn(`Failed to cleanup ${dir}:`, error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.tempDirs.clear();
|
|
188
|
+
console.log(import_chalk.default.green((0, import_i18n.t)("multi.cleanup.done")));
|
|
189
|
+
}
|
|
190
|
+
registerCleanupHandlers() {
|
|
191
|
+
const cleanup = async () => {
|
|
192
|
+
var _a;
|
|
193
|
+
if ((_a = this.config.remoteCache) == null ? void 0 : _a.autoCleanup) {
|
|
194
|
+
await this.cleanupTempDirs();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
process.once("SIGINT", async () => {
|
|
198
|
+
await cleanup();
|
|
199
|
+
process.exit(130);
|
|
200
|
+
});
|
|
201
|
+
process.once("SIGTERM", async () => {
|
|
202
|
+
await cleanup();
|
|
203
|
+
process.exit(143);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
208
|
+
0 && (module.exports = {
|
|
209
|
+
MultiRepoAnalyzer
|
|
210
|
+
});
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Config {
|
|
|
7
7
|
codeFileExtensions?: string[];
|
|
8
8
|
excludeExtensions?: string[];
|
|
9
9
|
activeUserWeeks?: number;
|
|
10
|
+
autoFetch?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export interface CliOptions {
|
|
12
13
|
repo: string;
|
|
@@ -21,6 +22,7 @@ export interface CliOptions {
|
|
|
21
22
|
verbose?: boolean;
|
|
22
23
|
interactive?: boolean;
|
|
23
24
|
lang?: string;
|
|
25
|
+
noFetch?: boolean;
|
|
24
26
|
}
|
|
25
27
|
export interface CommitInfo {
|
|
26
28
|
hash: string;
|
|
@@ -86,3 +88,47 @@ export interface StatsResult {
|
|
|
86
88
|
proposals: Map<string, ProposalStats>;
|
|
87
89
|
totalCommits: number;
|
|
88
90
|
}
|
|
91
|
+
export interface RepositoryConfig {
|
|
92
|
+
name: string;
|
|
93
|
+
type: 'local' | 'remote';
|
|
94
|
+
path?: string;
|
|
95
|
+
url?: string;
|
|
96
|
+
cloneOptions?: {
|
|
97
|
+
depth?: number | null;
|
|
98
|
+
singleBranch?: boolean;
|
|
99
|
+
};
|
|
100
|
+
branches: string[];
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface RemoteCacheConfig {
|
|
104
|
+
dir: string;
|
|
105
|
+
autoCleanup: boolean;
|
|
106
|
+
cleanupOnComplete: boolean;
|
|
107
|
+
cleanupOnError: boolean;
|
|
108
|
+
}
|
|
109
|
+
export interface ParallelismConfig {
|
|
110
|
+
maxConcurrent: number;
|
|
111
|
+
timeout: number;
|
|
112
|
+
}
|
|
113
|
+
export interface MultiRepoConfig extends Config {
|
|
114
|
+
mode: 'single-repo' | 'multi-repo';
|
|
115
|
+
repositories?: RepositoryConfig[];
|
|
116
|
+
remoteCache?: RemoteCacheConfig;
|
|
117
|
+
parallelism?: ParallelismConfig;
|
|
118
|
+
}
|
|
119
|
+
export interface RepositoryResult {
|
|
120
|
+
repository: string;
|
|
121
|
+
type: 'local' | 'remote';
|
|
122
|
+
path: string;
|
|
123
|
+
analyses: CommitAnalysis[];
|
|
124
|
+
success: boolean;
|
|
125
|
+
error?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface MultiRepoStatsResult extends StatsResult {
|
|
128
|
+
repositories: string[];
|
|
129
|
+
repositoryDetails: Map<string, {
|
|
130
|
+
type: 'local' | 'remote';
|
|
131
|
+
commits: number;
|
|
132
|
+
error?: string;
|
|
133
|
+
}>;
|
|
134
|
+
}
|