@clawplays/ospec-cli 0.3.4 → 0.3.5
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 +7 -0
- package/dist/cli.js +1 -1
- package/dist/commands/ArchiveCommand.js +36 -7
- package/dist/commands/UpdateCommand.js +109 -3
- package/dist/core/types.d.ts +4 -0
- package/dist/services/ConfigManager.js +7 -0
- package/dist/services/ProjectService.js +250 -60
- package/dist/services/RunService.js +52 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,12 @@ OSpec is a document-driven workflow for AI-assisted development, helping you def
|
|
|
37
37
|
npm install -g @clawplays/ospec-cli
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Update inside your project directory:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ospec update
|
|
44
|
+
```
|
|
45
|
+
|
|
40
46
|
## Recommended Prompts
|
|
41
47
|
|
|
42
48
|
Most teams only need 3 steps to use OSpec:
|
|
@@ -141,6 +147,7 @@ Archive notes:
|
|
|
141
147
|
- run your project-specific deploy, test, and QA flow first
|
|
142
148
|
- use `ospec verify` to confirm the active change is ready
|
|
143
149
|
- use `ospec finalize` to rebuild indexes and archive the accepted change
|
|
150
|
+
- new projects archive under `changes/archived/YYYY-MM/YYYY-MM-DD/<change-name>`; existing flat archives are reorganized by `ospec update`
|
|
144
151
|
|
|
145
152
|
</details>
|
|
146
153
|
|
package/dist/cli.js
CHANGED
|
@@ -224,7 +224,7 @@ const services_1 = require("./services");
|
|
|
224
224
|
|
|
225
225
|
|
|
226
226
|
|
|
227
|
-
const CLI_VERSION = '0.3.
|
|
227
|
+
const CLI_VERSION = '0.3.5';
|
|
228
228
|
|
|
229
229
|
function showInitUsage() {
|
|
230
230
|
console.log('Usage: ospec init [root-dir] [--summary "..."] [--tech-stack node,react] [--architecture "..."] [--document-language en-US|zh-CN|ja-JP|ar]');
|
|
@@ -180,7 +180,7 @@ class ArchiveCommand extends BaseCommand_1.BaseCommand {
|
|
|
180
180
|
this.success('Change is ready to archive');
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
|
-
const archivePath = await this.performArchive(targetPath, projectRoot, featureState);
|
|
183
|
+
const archivePath = await this.performArchive(targetPath, projectRoot, featureState, config);
|
|
184
184
|
this.success(`Change archived to ${archivePath}`);
|
|
185
185
|
return archivePath;
|
|
186
186
|
}
|
|
@@ -194,11 +194,10 @@ class ArchiveCommand extends BaseCommand_1.BaseCommand {
|
|
|
194
194
|
throw error;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
async performArchive(targetPath, projectRoot, featureState) {
|
|
197
|
+
async performArchive(targetPath, projectRoot, featureState, config) {
|
|
198
198
|
const archivedRoot = path.join(projectRoot, constants_1.DIR_NAMES.CHANGES, constants_1.DIR_NAMES.ARCHIVED);
|
|
199
199
|
await services_1.services.fileService.ensureDir(archivedRoot);
|
|
200
|
-
const
|
|
201
|
-
const archivePath = path.join(archivedRoot, archiveDirName);
|
|
200
|
+
const archivePath = await this.resolveArchivePath(archivedRoot, featureState.feature, config);
|
|
202
201
|
const nextState = {
|
|
203
202
|
...featureState,
|
|
204
203
|
status: 'archived',
|
|
@@ -222,9 +221,29 @@ class ArchiveCommand extends BaseCommand_1.BaseCommand {
|
|
|
222
221
|
proposal.data.status = status;
|
|
223
222
|
await services_1.services.fileService.writeFile(proposalPath, gray_matter_1.default.stringify(proposal.content, proposal.data));
|
|
224
223
|
}
|
|
225
|
-
async
|
|
226
|
-
const
|
|
227
|
-
const
|
|
224
|
+
async resolveArchivePath(archivedRoot, featureName, config) {
|
|
225
|
+
const archiveLayout = config?.archive?.layout === 'month-day' ? 'month-day' : 'flat';
|
|
226
|
+
const archiveDate = this.getLocalArchiveDateParts();
|
|
227
|
+
if (archiveLayout === 'month-day') {
|
|
228
|
+
const archiveDayRoot = path.join(archivedRoot, archiveDate.month, archiveDate.day);
|
|
229
|
+
await services_1.services.fileService.ensureDir(archiveDayRoot);
|
|
230
|
+
const archiveLeafName = await this.resolveArchiveLeafName(archiveDayRoot, featureName);
|
|
231
|
+
return path.join(archiveDayRoot, archiveLeafName);
|
|
232
|
+
}
|
|
233
|
+
const archiveDirName = await this.resolveLegacyArchiveDirName(archivedRoot, archiveDate.day, featureName);
|
|
234
|
+
return path.join(archivedRoot, archiveDirName);
|
|
235
|
+
}
|
|
236
|
+
async resolveArchiveLeafName(archiveDayRoot, featureName) {
|
|
237
|
+
let candidate = featureName;
|
|
238
|
+
let index = 2;
|
|
239
|
+
while (await services_1.services.fileService.exists(path.join(archiveDayRoot, candidate))) {
|
|
240
|
+
candidate = `${featureName}-${index}`;
|
|
241
|
+
index += 1;
|
|
242
|
+
}
|
|
243
|
+
return candidate;
|
|
244
|
+
}
|
|
245
|
+
async resolveLegacyArchiveDirName(archivedRoot, archiveDay, featureName) {
|
|
246
|
+
const baseName = `${archiveDay}-${featureName}`;
|
|
228
247
|
let candidate = baseName;
|
|
229
248
|
let index = 2;
|
|
230
249
|
while (await services_1.services.fileService.exists(path.join(archivedRoot, candidate))) {
|
|
@@ -233,6 +252,16 @@ class ArchiveCommand extends BaseCommand_1.BaseCommand {
|
|
|
233
252
|
}
|
|
234
253
|
return candidate;
|
|
235
254
|
}
|
|
255
|
+
getLocalArchiveDateParts() {
|
|
256
|
+
const now = new Date();
|
|
257
|
+
const year = String(now.getFullYear());
|
|
258
|
+
const monthNumber = String(now.getMonth() + 1).padStart(2, '0');
|
|
259
|
+
const dayNumber = String(now.getDate()).padStart(2, '0');
|
|
260
|
+
return {
|
|
261
|
+
month: `${year}-${monthNumber}`,
|
|
262
|
+
day: `${year}-${monthNumber}-${dayNumber}`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
236
265
|
toRelativePath(rootDir, targetPath) {
|
|
237
266
|
return path.relative(rootDir, targetPath).replace(/\\/g, '/');
|
|
238
267
|
}
|
|
@@ -18,8 +18,14 @@ class UpdateCommand extends BaseCommand_1.BaseCommand {
|
|
|
18
18
|
const protocolResult = await services_1.services.projectService.syncProtocolGuidance(targetPath);
|
|
19
19
|
const toolingResult = await this.syncProjectTooling(targetPath, protocolResult.documentLanguage);
|
|
20
20
|
const pluginResult = await this.syncEnabledPluginAssets(targetPath);
|
|
21
|
+
const archiveResult = await this.syncArchiveLayout(targetPath);
|
|
21
22
|
const skillResult = await this.syncInstalledSkills();
|
|
22
|
-
const refreshedFiles =
|
|
23
|
+
const refreshedFiles = Array.from(new Set([
|
|
24
|
+
...protocolResult.refreshedFiles,
|
|
25
|
+
...toolingResult.refreshedFiles,
|
|
26
|
+
...pluginResult.refreshedFiles,
|
|
27
|
+
...(archiveResult.configSaved ? ['.skillrc'] : []),
|
|
28
|
+
]));
|
|
23
29
|
const createdFiles = [...protocolResult.createdFiles, ...toolingResult.createdFiles, ...pluginResult.createdFiles];
|
|
24
30
|
const skippedFiles = [...protocolResult.skippedFiles, ...toolingResult.skippedFiles, ...pluginResult.skippedFiles];
|
|
25
31
|
this.success(`Updated OSpec assets for ${protocolResult.projectName}`);
|
|
@@ -40,8 +46,14 @@ class UpdateCommand extends BaseCommand_1.BaseCommand {
|
|
|
40
46
|
if (pluginResult.configSaved) {
|
|
41
47
|
this.info(' plugin config normalized: .skillrc');
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
if (archiveResult.configSaved) {
|
|
50
|
+
this.info(' archive layout normalized: .skillrc');
|
|
51
|
+
}
|
|
52
|
+
if (archiveResult.migratedChanges.length > 0) {
|
|
53
|
+
this.info(` archived changes migrated: ${archiveResult.migratedChanges.length}`);
|
|
54
|
+
}
|
|
55
|
+
this.info(' note: update refreshes protocol docs, tooling, hooks, managed skills, managed assets for already-enabled plugins, and the archive layout when needed');
|
|
56
|
+
this.info(' note: it does not enable, disable, or migrate active or queued changes automatically');
|
|
45
57
|
}
|
|
46
58
|
async syncProjectTooling(rootDir, documentLanguage) {
|
|
47
59
|
const toolingPaths = [
|
|
@@ -222,6 +234,100 @@ class UpdateCommand extends BaseCommand_1.BaseCommand {
|
|
|
222
234
|
}
|
|
223
235
|
return { createdFiles, skippedFiles };
|
|
224
236
|
}
|
|
237
|
+
async syncArchiveLayout(rootDir) {
|
|
238
|
+
const rawConfig = await services_1.services.fileService.readJSON((0, path_1.join)(rootDir, '.skillrc'));
|
|
239
|
+
const config = await services_1.services.configManager.loadConfig(rootDir);
|
|
240
|
+
const nextConfig = JSON.parse(JSON.stringify(config));
|
|
241
|
+
const archivedRoot = (0, path_1.join)(rootDir, 'changes', 'archived');
|
|
242
|
+
const migratedChanges = [];
|
|
243
|
+
if (await services_1.services.fileService.exists(archivedRoot)) {
|
|
244
|
+
const entryNames = (await services_1.services.fileService.readDir(archivedRoot)).sort((left, right) => left.localeCompare(right));
|
|
245
|
+
for (const entryName of entryNames) {
|
|
246
|
+
const entryPath = (0, path_1.join)(archivedRoot, entryName);
|
|
247
|
+
const stat = await services_1.services.fileService.stat(entryPath);
|
|
248
|
+
if (!stat.isDirectory()) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const parsed = this.parseLegacyArchiveDirName(entryName);
|
|
252
|
+
if (!parsed) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const archivedState = await this.readArchivedChangeState(entryPath);
|
|
256
|
+
if (!archivedState) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const archiveDayRoot = (0, path_1.join)(archivedRoot, parsed.month, parsed.day);
|
|
260
|
+
await services_1.services.fileService.ensureDir(archiveDayRoot);
|
|
261
|
+
const targetPath = await this.resolveArchiveMigrationTarget(archiveDayRoot, archivedState.feature);
|
|
262
|
+
await services_1.services.fileService.move(entryPath, targetPath);
|
|
263
|
+
migratedChanges.push({
|
|
264
|
+
from: `changes/archived/${entryName}`,
|
|
265
|
+
to: this.toRelativePath(rootDir, targetPath),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let configSaved = false;
|
|
270
|
+
if (nextConfig.archive?.layout !== 'month-day') {
|
|
271
|
+
nextConfig.archive = {
|
|
272
|
+
...(nextConfig.archive || {}),
|
|
273
|
+
layout: 'month-day',
|
|
274
|
+
};
|
|
275
|
+
await services_1.services.configManager.saveConfig(rootDir, nextConfig);
|
|
276
|
+
configSaved = true;
|
|
277
|
+
}
|
|
278
|
+
else if (!rawConfig?.archive || rawConfig.archive.layout !== 'month-day') {
|
|
279
|
+
await services_1.services.configManager.saveConfig(rootDir, nextConfig);
|
|
280
|
+
configSaved = true;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
configSaved,
|
|
284
|
+
migratedChanges,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
parseLegacyArchiveDirName(entryName) {
|
|
288
|
+
const match = /^(\d{4}-\d{2}-\d{2})-(.+)$/.exec(entryName);
|
|
289
|
+
if (!match) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
month: match[1].slice(0, 7),
|
|
294
|
+
day: match[1],
|
|
295
|
+
leafName: match[2],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async resolveArchiveMigrationTarget(archiveDayRoot, leafName) {
|
|
299
|
+
let candidate = leafName;
|
|
300
|
+
let index = 2;
|
|
301
|
+
while (await services_1.services.fileService.exists((0, path_1.join)(archiveDayRoot, candidate))) {
|
|
302
|
+
candidate = `${leafName}-${index}`;
|
|
303
|
+
index += 1;
|
|
304
|
+
}
|
|
305
|
+
return (0, path_1.join)(archiveDayRoot, candidate);
|
|
306
|
+
}
|
|
307
|
+
async readArchivedChangeState(entryPath) {
|
|
308
|
+
const statePath = (0, path_1.join)(entryPath, 'state.json');
|
|
309
|
+
if (!(await services_1.services.fileService.exists(statePath))) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const state = await services_1.services.fileService.readJSON(statePath);
|
|
314
|
+
if (typeof state?.feature !== 'string' || state.feature.trim().length === 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (state.status !== 'archived') {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
feature: state.feature.trim(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
toRelativePath(rootDir, targetPath) {
|
|
329
|
+
return (0, path_1.relative)(rootDir, targetPath).replace(/\\/g, '/');
|
|
330
|
+
}
|
|
225
331
|
getManagedSkillNames() {
|
|
226
332
|
return ['ospec', 'ospec-change'];
|
|
227
333
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -89,6 +89,9 @@ export interface CheckpointPluginConfig {
|
|
|
89
89
|
auto_pass_stitch_review: boolean;
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
export interface ArchiveConfig {
|
|
93
|
+
layout: 'flat' | 'month-day';
|
|
94
|
+
}
|
|
92
95
|
export interface SkillrcConfig {
|
|
93
96
|
version: string;
|
|
94
97
|
mode: ProjectMode;
|
|
@@ -104,6 +107,7 @@ export interface SkillrcConfig {
|
|
|
104
107
|
include?: string[];
|
|
105
108
|
exclude?: string[];
|
|
106
109
|
};
|
|
110
|
+
archive?: ArchiveConfig;
|
|
107
111
|
plugins?: {
|
|
108
112
|
stitch?: StitchPluginConfig;
|
|
109
113
|
checkpoint?: CheckpointPluginConfig;
|
|
@@ -81,6 +81,9 @@ class ConfigManager {
|
|
|
81
81
|
index: {
|
|
82
82
|
exclude: ['node_modules/**', 'dist/**', '*.test.*'],
|
|
83
83
|
},
|
|
84
|
+
archive: {
|
|
85
|
+
layout: 'month-day',
|
|
86
|
+
},
|
|
84
87
|
plugins: this.createDefaultPluginsConfig(),
|
|
85
88
|
workflow,
|
|
86
89
|
};
|
|
@@ -345,6 +348,7 @@ class ConfigManager {
|
|
|
345
348
|
}
|
|
346
349
|
normalizeConfig(config) {
|
|
347
350
|
const mode = ['lite', 'standard', 'full'].includes(config.mode) ? config.mode : 'full';
|
|
351
|
+
const archive = config.archive && typeof config.archive === 'object' ? config.archive : {};
|
|
348
352
|
const hooks = config.hooks || {
|
|
349
353
|
'pre-commit': true,
|
|
350
354
|
'post-merge': true,
|
|
@@ -370,6 +374,9 @@ class ConfigManager {
|
|
|
370
374
|
version: config.version === '3.0' ? '4.0' : config.version,
|
|
371
375
|
mode,
|
|
372
376
|
documentLanguage: this.normalizeDocumentLanguage(config.documentLanguage),
|
|
377
|
+
archive: {
|
|
378
|
+
layout: archive.layout === 'month-day' ? 'month-day' : 'flat',
|
|
379
|
+
},
|
|
373
380
|
hooks: {
|
|
374
381
|
...normalizedHooks,
|
|
375
382
|
...(legacyWarnDefaults
|
|
@@ -4385,71 +4385,15 @@ class ProjectService {
|
|
|
4385
4385
|
|
|
4386
4386
|
|
|
4387
4387
|
|
|
4388
|
-
const
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
await this.fileService.ensureDir(archivedRoot);
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
const datePrefix = new Date().toISOString().slice(0, 10);
|
|
4405
|
-
|
|
4406
|
-
|
|
4388
|
+
const config = await this.configManager.loadConfig(projectRoot);
|
|
4407
4389
|
|
|
4408
4390
|
|
|
4409
4391
|
|
|
4410
4392
|
|
|
4411
4393
|
|
|
4412
|
-
const baseName = `${datePrefix}-${featureState.feature}`;
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
let archiveDirName = baseName;
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
let archiveIndex = 2;
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
4394
|
|
|
4432
4395
|
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
while (await this.fileService.exists(path_1.default.join(archivedRoot, archiveDirName))) {
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
archiveDirName = `${baseName}-${archiveIndex}`;
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
archiveIndex += 1;
|
|
4396
|
+
const archivedRoot = path_1.default.join(projectRoot, constants_1.DIR_NAMES.CHANGES, constants_1.DIR_NAMES.ARCHIVED);
|
|
4453
4397
|
|
|
4454
4398
|
|
|
4455
4399
|
|
|
@@ -4457,7 +4401,7 @@ class ProjectService {
|
|
|
4457
4401
|
|
|
4458
4402
|
|
|
4459
4403
|
|
|
4460
|
-
|
|
4404
|
+
await this.fileService.ensureDir(archivedRoot);
|
|
4461
4405
|
|
|
4462
4406
|
|
|
4463
4407
|
|
|
@@ -4465,7 +4409,7 @@ class ProjectService {
|
|
|
4465
4409
|
|
|
4466
4410
|
|
|
4467
4411
|
|
|
4468
|
-
const archivePath =
|
|
4412
|
+
const archivePath = await this.resolveArchivePath(archivedRoot, featureState.feature, config);
|
|
4469
4413
|
|
|
4470
4414
|
|
|
4471
4415
|
|
|
@@ -12449,6 +12393,252 @@ ${formatSuggestion()}
|
|
|
12449
12393
|
|
|
12450
12394
|
|
|
12451
12395
|
|
|
12396
|
+
async resolveArchivePath(archivedRoot, featureName, config) {
|
|
12397
|
+
|
|
12398
|
+
|
|
12399
|
+
|
|
12400
|
+
|
|
12401
|
+
|
|
12402
|
+
const archiveLayout = config?.archive?.layout === 'month-day' ? 'month-day' : 'flat';
|
|
12403
|
+
|
|
12404
|
+
|
|
12405
|
+
|
|
12406
|
+
|
|
12407
|
+
|
|
12408
|
+
const archiveDate = this.getLocalArchiveDateParts();
|
|
12409
|
+
|
|
12410
|
+
|
|
12411
|
+
|
|
12412
|
+
|
|
12413
|
+
|
|
12414
|
+
if (archiveLayout === 'month-day') {
|
|
12415
|
+
|
|
12416
|
+
|
|
12417
|
+
|
|
12418
|
+
|
|
12419
|
+
|
|
12420
|
+
const archiveDayRoot = path_1.default.join(archivedRoot, archiveDate.month, archiveDate.day);
|
|
12421
|
+
|
|
12422
|
+
|
|
12423
|
+
|
|
12424
|
+
|
|
12425
|
+
|
|
12426
|
+
await this.fileService.ensureDir(archiveDayRoot);
|
|
12427
|
+
|
|
12428
|
+
|
|
12429
|
+
|
|
12430
|
+
|
|
12431
|
+
|
|
12432
|
+
const archiveLeafName = await this.resolveArchiveLeafName(archiveDayRoot, featureName);
|
|
12433
|
+
|
|
12434
|
+
|
|
12435
|
+
|
|
12436
|
+
|
|
12437
|
+
|
|
12438
|
+
return path_1.default.join(archiveDayRoot, archiveLeafName);
|
|
12439
|
+
|
|
12440
|
+
|
|
12441
|
+
|
|
12442
|
+
|
|
12443
|
+
|
|
12444
|
+
}
|
|
12445
|
+
|
|
12446
|
+
|
|
12447
|
+
|
|
12448
|
+
|
|
12449
|
+
|
|
12450
|
+
const archiveDirName = await this.resolveLegacyArchiveDirName(archivedRoot, archiveDate.day, featureName);
|
|
12451
|
+
|
|
12452
|
+
|
|
12453
|
+
|
|
12454
|
+
|
|
12455
|
+
|
|
12456
|
+
return path_1.default.join(archivedRoot, archiveDirName);
|
|
12457
|
+
|
|
12458
|
+
|
|
12459
|
+
|
|
12460
|
+
|
|
12461
|
+
|
|
12462
|
+
}
|
|
12463
|
+
|
|
12464
|
+
|
|
12465
|
+
|
|
12466
|
+
|
|
12467
|
+
|
|
12468
|
+
async resolveArchiveLeafName(archiveDayRoot, featureName) {
|
|
12469
|
+
|
|
12470
|
+
|
|
12471
|
+
|
|
12472
|
+
|
|
12473
|
+
|
|
12474
|
+
let candidate = featureName;
|
|
12475
|
+
|
|
12476
|
+
|
|
12477
|
+
|
|
12478
|
+
|
|
12479
|
+
|
|
12480
|
+
let archiveIndex = 2;
|
|
12481
|
+
|
|
12482
|
+
|
|
12483
|
+
|
|
12484
|
+
|
|
12485
|
+
|
|
12486
|
+
while (await this.fileService.exists(path_1.default.join(archiveDayRoot, candidate))) {
|
|
12487
|
+
|
|
12488
|
+
|
|
12489
|
+
|
|
12490
|
+
|
|
12491
|
+
|
|
12492
|
+
candidate = `${featureName}-${archiveIndex}`;
|
|
12493
|
+
|
|
12494
|
+
|
|
12495
|
+
|
|
12496
|
+
|
|
12497
|
+
|
|
12498
|
+
archiveIndex += 1;
|
|
12499
|
+
|
|
12500
|
+
|
|
12501
|
+
|
|
12502
|
+
|
|
12503
|
+
|
|
12504
|
+
}
|
|
12505
|
+
|
|
12506
|
+
|
|
12507
|
+
|
|
12508
|
+
|
|
12509
|
+
|
|
12510
|
+
return candidate;
|
|
12511
|
+
|
|
12512
|
+
|
|
12513
|
+
|
|
12514
|
+
|
|
12515
|
+
|
|
12516
|
+
}
|
|
12517
|
+
|
|
12518
|
+
|
|
12519
|
+
|
|
12520
|
+
|
|
12521
|
+
|
|
12522
|
+
async resolveLegacyArchiveDirName(archivedRoot, archiveDay, featureName) {
|
|
12523
|
+
|
|
12524
|
+
|
|
12525
|
+
|
|
12526
|
+
|
|
12527
|
+
|
|
12528
|
+
const baseName = `${archiveDay}-${featureName}`;
|
|
12529
|
+
|
|
12530
|
+
|
|
12531
|
+
|
|
12532
|
+
|
|
12533
|
+
|
|
12534
|
+
let candidate = baseName;
|
|
12535
|
+
|
|
12536
|
+
|
|
12537
|
+
|
|
12538
|
+
|
|
12539
|
+
|
|
12540
|
+
let archiveIndex = 2;
|
|
12541
|
+
|
|
12542
|
+
|
|
12543
|
+
|
|
12544
|
+
|
|
12545
|
+
|
|
12546
|
+
while (await this.fileService.exists(path_1.default.join(archivedRoot, candidate))) {
|
|
12547
|
+
|
|
12548
|
+
|
|
12549
|
+
|
|
12550
|
+
|
|
12551
|
+
|
|
12552
|
+
candidate = `${baseName}-${archiveIndex}`;
|
|
12553
|
+
|
|
12554
|
+
|
|
12555
|
+
|
|
12556
|
+
|
|
12557
|
+
|
|
12558
|
+
archiveIndex += 1;
|
|
12559
|
+
|
|
12560
|
+
|
|
12561
|
+
|
|
12562
|
+
|
|
12563
|
+
|
|
12564
|
+
}
|
|
12565
|
+
|
|
12566
|
+
|
|
12567
|
+
|
|
12568
|
+
|
|
12569
|
+
|
|
12570
|
+
return candidate;
|
|
12571
|
+
|
|
12572
|
+
|
|
12573
|
+
|
|
12574
|
+
|
|
12575
|
+
|
|
12576
|
+
}
|
|
12577
|
+
|
|
12578
|
+
|
|
12579
|
+
|
|
12580
|
+
|
|
12581
|
+
|
|
12582
|
+
getLocalArchiveDateParts() {
|
|
12583
|
+
|
|
12584
|
+
|
|
12585
|
+
|
|
12586
|
+
|
|
12587
|
+
|
|
12588
|
+
const now = new Date();
|
|
12589
|
+
|
|
12590
|
+
|
|
12591
|
+
|
|
12592
|
+
|
|
12593
|
+
|
|
12594
|
+
const year = String(now.getFullYear());
|
|
12595
|
+
|
|
12596
|
+
|
|
12597
|
+
|
|
12598
|
+
|
|
12599
|
+
|
|
12600
|
+
const monthNumber = String(now.getMonth() + 1).padStart(2, '0');
|
|
12601
|
+
|
|
12602
|
+
|
|
12603
|
+
|
|
12604
|
+
|
|
12605
|
+
|
|
12606
|
+
const dayNumber = String(now.getDate()).padStart(2, '0');
|
|
12607
|
+
|
|
12608
|
+
|
|
12609
|
+
|
|
12610
|
+
|
|
12611
|
+
|
|
12612
|
+
return {
|
|
12613
|
+
|
|
12614
|
+
|
|
12615
|
+
|
|
12616
|
+
|
|
12617
|
+
|
|
12618
|
+
month: `${year}-${monthNumber}`,
|
|
12619
|
+
|
|
12620
|
+
|
|
12621
|
+
|
|
12622
|
+
|
|
12623
|
+
|
|
12624
|
+
day: `${year}-${monthNumber}-${dayNumber}`,
|
|
12625
|
+
|
|
12626
|
+
|
|
12627
|
+
|
|
12628
|
+
|
|
12629
|
+
|
|
12630
|
+
};
|
|
12631
|
+
|
|
12632
|
+
|
|
12633
|
+
|
|
12634
|
+
|
|
12635
|
+
|
|
12636
|
+
}
|
|
12637
|
+
|
|
12638
|
+
|
|
12639
|
+
|
|
12640
|
+
|
|
12641
|
+
|
|
12452
12642
|
toRelativePath(rootDir, filePath) {
|
|
12453
12643
|
|
|
12454
12644
|
|
|
@@ -350,13 +350,59 @@ class RunService {
|
|
|
350
350
|
if (!(await this.fileService.exists(archivedDir))) {
|
|
351
351
|
return null;
|
|
352
352
|
}
|
|
353
|
+
const candidatePaths = await this.listArchivedChangeDirectories(archivedDir);
|
|
354
|
+
const matches = [];
|
|
355
|
+
for (const candidatePath of candidatePaths) {
|
|
356
|
+
const statePath = path_1.default.join(candidatePath, constants_1.FILE_NAMES.STATE);
|
|
357
|
+
if (!(await this.fileService.exists(statePath))) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const state = await this.fileService.readJSON(statePath);
|
|
362
|
+
if (state?.feature === changeName && state?.status === 'archived') {
|
|
363
|
+
matches.push(this.toArchivedRelativePath(archivedDir, candidatePath));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return matches.sort().at(-1) || null;
|
|
371
|
+
}
|
|
372
|
+
async listArchivedChangeDirectories(archivedDir) {
|
|
353
373
|
const entries = await fs_extra_1.default.readdir(archivedDir, { withFileTypes: true });
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
374
|
+
const candidates = [];
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
if (!entry.isDirectory()) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const entryPath = path_1.default.join(archivedDir, entry.name);
|
|
380
|
+
if (/^\d{4}-\d{2}-\d{2}-.+/.test(entry.name)) {
|
|
381
|
+
candidates.push(entryPath);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (!/^\d{4}-\d{2}$/.test(entry.name)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const dayEntries = await fs_extra_1.default.readdir(entryPath, { withFileTypes: true });
|
|
388
|
+
for (const dayEntry of dayEntries) {
|
|
389
|
+
if (!dayEntry.isDirectory() || !/^\d{4}-\d{2}-\d{2}$/.test(dayEntry.name)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const dayPath = path_1.default.join(entryPath, dayEntry.name);
|
|
393
|
+
const changeEntries = await fs_extra_1.default.readdir(dayPath, { withFileTypes: true });
|
|
394
|
+
for (const changeEntry of changeEntries) {
|
|
395
|
+
if (changeEntry.isDirectory()) {
|
|
396
|
+
candidates.push(path_1.default.join(dayPath, changeEntry.name));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return candidates;
|
|
402
|
+
}
|
|
403
|
+
toArchivedRelativePath(archivedDir, candidatePath) {
|
|
404
|
+
const relativePath = path_1.default.relative(archivedDir, candidatePath).replace(/\\/g, '/');
|
|
405
|
+
return `changes/archived/${relativePath}`;
|
|
360
406
|
}
|
|
361
407
|
async ensureRunDirectories(rootDir) {
|
|
362
408
|
await Promise.all([
|