@clawplays/ospec-cli 0.3.4 → 0.3.7
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 +31 -15
- package/dist/cli.js +1 -1
- package/dist/commands/ArchiveCommand.js +36 -7
- package/dist/commands/ChangesCommand.js +3 -0
- package/dist/commands/InitCommand.js +2 -1
- package/dist/commands/NewCommand.js +20 -0
- package/dist/commands/PluginsCommand.js +4 -3
- package/dist/commands/SkillCommand.js +3 -1
- package/dist/commands/StatusCommand.js +23 -9
- 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/dist/utils/helpers.d.ts +3 -5
- package/dist/utils/helpers.js +21 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -31,15 +31,24 @@ OSpec is a document-driven workflow for AI-assisted development, helping you def
|
|
|
31
31
|
<a href="https://github.com/clawplays/ospec/issues">Issues</a>
|
|
32
32
|
</p>
|
|
33
33
|
|
|
34
|
+
## Why OSpec?
|
|
35
|
+
|
|
36
|
+
AI coding assistants are powerful, but requirements that live only in chat history are hard to inspect, review, and close out cleanly. OSpec adds a lightweight workflow layer so the repository can hold the change context before code is written and after the work ships.
|
|
37
|
+
|
|
38
|
+
- Align before code — keep proposal, tasks, state, verification, and review visible in the repo
|
|
39
|
+
- Keep each requirement explicit — the default path moves one requirement through one active change
|
|
40
|
+
- Stay lightweight — keep the normal flow short with `init -> change -> verify/finalize`
|
|
41
|
+
- Use the assistants you already have — OSpec is built for Codex, Claude Code, and direct CLI workflows
|
|
42
|
+
|
|
34
43
|
## Install With npm
|
|
35
44
|
|
|
36
45
|
```bash
|
|
37
46
|
npm install -g @clawplays/ospec-cli
|
|
38
47
|
```
|
|
39
48
|
|
|
40
|
-
##
|
|
49
|
+
## Quick Start
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
OSpec only takes 3 steps:
|
|
43
52
|
|
|
44
53
|
1. initialize OSpec in your project directory
|
|
45
54
|
2. create and advance one change for a requirement, document update, or bug fix
|
|
@@ -50,13 +59,13 @@ Most teams only need 3 steps to use OSpec:
|
|
|
50
59
|
Recommended prompt:
|
|
51
60
|
|
|
52
61
|
```text
|
|
53
|
-
|
|
62
|
+
OSpec, initialize this project.
|
|
54
63
|
```
|
|
55
64
|
|
|
56
65
|
Claude / Codex skill mode:
|
|
57
66
|
|
|
58
67
|
```text
|
|
59
|
-
|
|
68
|
+
$ospec initialize this project.
|
|
60
69
|
```
|
|
61
70
|
|
|
62
71
|
<details>
|
|
@@ -90,17 +99,15 @@ Use this for requirement delivery, documentation updates, refactors, and bug fix
|
|
|
90
99
|
Recommended prompt:
|
|
91
100
|
|
|
92
101
|
```text
|
|
93
|
-
|
|
102
|
+
OSpec, create and advance a change for this requirement.
|
|
94
103
|
```
|
|
95
104
|
|
|
96
105
|
Claude / Codex skill mode:
|
|
97
106
|
|
|
98
107
|
```text
|
|
99
|
-
|
|
108
|
+
$ospec-change create and advance a change for this requirement.
|
|
100
109
|
```
|
|
101
110
|
|
|
102
|
-

|
|
103
|
-
|
|
104
111
|
<details>
|
|
105
112
|
<summary>Command line</summary>
|
|
106
113
|
|
|
@@ -119,13 +126,13 @@ After the requirement has passed deployment, testing, QA, or other acceptance ch
|
|
|
119
126
|
Recommended prompt:
|
|
120
127
|
|
|
121
128
|
```text
|
|
122
|
-
|
|
129
|
+
OSpec, archive this accepted change.
|
|
123
130
|
```
|
|
124
131
|
|
|
125
132
|
Claude / Codex skill mode:
|
|
126
133
|
|
|
127
134
|
```text
|
|
128
|
-
|
|
135
|
+
$ospec archive this accepted change.
|
|
129
136
|
```
|
|
130
137
|
|
|
131
138
|
<details>
|
|
@@ -141,15 +148,24 @@ Archive notes:
|
|
|
141
148
|
- run your project-specific deploy, test, and QA flow first
|
|
142
149
|
- use `ospec verify` to confirm the active change is ready
|
|
143
150
|
- use `ospec finalize` to rebuild indexes and archive the accepted change
|
|
151
|
+
- new projects archive under `changes/archived/YYYY-MM/YYYY-MM-DD/<change-name>`; existing flat archives are reorganized by `ospec update`
|
|
144
152
|
|
|
145
153
|
</details>
|
|
146
154
|
|
|
155
|
+
## Update With npm
|
|
156
|
+
|
|
157
|
+
For an existing OSpec project, after upgrading the CLI with npm, run this in the project directory to refresh the project's OSpec files:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
ospec update
|
|
161
|
+
```
|
|
162
|
+
|
|
147
163
|
## How The OSpec Workflow Works
|
|
148
164
|
|
|
149
165
|
```text
|
|
150
166
|
┌─────────────────────────────────────────────────────────────────┐
|
|
151
167
|
│ 1. USER REQUEST │
|
|
152
|
-
│ "
|
|
168
|
+
│ "OSpec, create and advance a change for this task." │
|
|
153
169
|
└─────────────────────────────────────────────────────────────────┘
|
|
154
170
|
│
|
|
155
171
|
▼
|
|
@@ -217,13 +233,13 @@ Use Stitch for page design review and preview collaboration, especially for land
|
|
|
217
233
|
AI conversation:
|
|
218
234
|
|
|
219
235
|
```text
|
|
220
|
-
|
|
236
|
+
OSpec, enable the Stitch plugin and connect using Codex/Gemini.
|
|
221
237
|
```
|
|
222
238
|
|
|
223
239
|
Claude / Codex skill mode:
|
|
224
240
|
|
|
225
241
|
```text
|
|
226
|
-
|
|
242
|
+
$ospec enable the Stitch plugin and connect using Codex/Gemini.
|
|
227
243
|
```
|
|
228
244
|
|
|
229
245
|
<details>
|
|
@@ -242,13 +258,13 @@ Use Checkpoint for app flow validation and automated checks, especially for subm
|
|
|
242
258
|
AI conversation:
|
|
243
259
|
|
|
244
260
|
```text
|
|
245
|
-
|
|
261
|
+
OSpec, enable the Checkpoint plugin.
|
|
246
262
|
```
|
|
247
263
|
|
|
248
264
|
Claude / Codex skill mode:
|
|
249
265
|
|
|
250
266
|
```text
|
|
251
|
-
|
|
267
|
+
$ospec enable the Checkpoint plugin.
|
|
252
268
|
```
|
|
253
269
|
|
|
254
270
|
<details>
|
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.7';
|
|
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
|
}
|
|
@@ -21,6 +21,9 @@ class ChangesCommand extends BaseCommand_1.BaseCommand {
|
|
|
21
21
|
console.log(`Total: ${report.totalActiveChanges}`);
|
|
22
22
|
console.log(`Queued: ${queuedChanges.length}`);
|
|
23
23
|
console.log(`PASS ${report.totals.pass} | WARN ${report.totals.warn} | FAIL ${report.totals.fail}`);
|
|
24
|
+
if (report.totalActiveChanges > 1) {
|
|
25
|
+
console.log('WORKFLOW WARN multiple active changes detected. The default workflow expects one active change, and queue runner commands will fail until the repository is back to single-active mode.');
|
|
26
|
+
}
|
|
24
27
|
console.log('');
|
|
25
28
|
if (report.changes.length === 0) {
|
|
26
29
|
console.log('No active changes.');
|
|
@@ -37,6 +37,7 @@ exports.InitCommand = void 0;
|
|
|
37
37
|
const os_1 = require("os");
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const services_1 = require("../services");
|
|
40
|
+
const helpers_1 = require("../utils/helpers");
|
|
40
41
|
const BaseCommand_1 = require("./BaseCommand");
|
|
41
42
|
const SkillCommand_1 = require("./SkillCommand");
|
|
42
43
|
class InitCommand extends BaseCommand_1.BaseCommand {
|
|
@@ -73,7 +74,7 @@ class InitCommand extends BaseCommand_1.BaseCommand {
|
|
|
73
74
|
if (result.firstChangeSuggestion) {
|
|
74
75
|
this.info(` Suggested first change: ${result.firstChangeSuggestion.name}`);
|
|
75
76
|
}
|
|
76
|
-
this.info(` Next: ospec new <change-name>
|
|
77
|
+
this.info(` Next: ${(0, helpers_1.formatCliCommand)('ospec', 'new', '<change-name>', targetDir)}`);
|
|
77
78
|
}
|
|
78
79
|
catch (error) {
|
|
79
80
|
this.error(`Failed to initialize project: ${error}`);
|
|
@@ -37,6 +37,7 @@ exports.NewCommand = void 0;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const constants_1 = require("../core/constants");
|
|
39
39
|
const services_1 = require("../services");
|
|
40
|
+
const helpers_1 = require("../utils/helpers");
|
|
40
41
|
const PathUtils_1 = require("../utils/PathUtils");
|
|
41
42
|
const PluginWorkflowComposer_1 = require("../workflow/PluginWorkflowComposer");
|
|
42
43
|
const BaseCommand_1 = require("./BaseCommand");
|
|
@@ -50,6 +51,7 @@ class NewCommand extends BaseCommand_1.BaseCommand {
|
|
|
50
51
|
const featureDir = PathUtils_1.PathUtils.getChangeDir(targetDir, placement, featureName);
|
|
51
52
|
this.logger.info(`Creating ${placement === constants_1.DIR_NAMES.QUEUED ? 'queued change' : 'change'}: ${featureName}`);
|
|
52
53
|
await this.ensureChangeNameAvailable(targetDir, featureName);
|
|
54
|
+
await this.ensureSingleActiveMode(targetDir, placement, featureName);
|
|
53
55
|
await services_1.services.fileService.ensureDir(path.join(targetDir, constants_1.DIR_NAMES.CHANGES, placement));
|
|
54
56
|
await services_1.services.fileService.ensureDir(featureDir);
|
|
55
57
|
const config = await services_1.services.configManager.loadConfig(targetDir);
|
|
@@ -229,6 +231,24 @@ class NewCommand extends BaseCommand_1.BaseCommand {
|
|
|
229
231
|
}
|
|
230
232
|
throw new Error(`Change ${featureName} already exists in ${conflicts.join(' and ')}. Continue the existing change instead of creating a duplicate.`);
|
|
231
233
|
}
|
|
234
|
+
async ensureSingleActiveMode(targetDir, placement, featureName) {
|
|
235
|
+
if (placement !== constants_1.DIR_NAMES.ACTIVE) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const activeNames = await services_1.services.projectService.listActiveChangeNames(targetDir);
|
|
239
|
+
if (activeNames.length === 0) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (activeNames.length === 1) {
|
|
243
|
+
const activeName = activeNames[0];
|
|
244
|
+
const activeChangePath = path.join(targetDir, constants_1.DIR_NAMES.CHANGES, constants_1.DIR_NAMES.ACTIVE, activeName);
|
|
245
|
+
const progressCommand = (0, helpers_1.formatCliCommand)('ospec', 'progress', activeChangePath);
|
|
246
|
+
const queueCommand = (0, helpers_1.formatCliCommand)('ospec', 'queue', 'add', featureName, targetDir);
|
|
247
|
+
throw new Error(`A single active change is the default workflow, but "${activeName}" is already active. Continue it with "${progressCommand}" or create queued work explicitly with "${queueCommand}".`);
|
|
248
|
+
}
|
|
249
|
+
const queueCommand = (0, helpers_1.formatCliCommand)('ospec', 'queue', 'add', featureName, targetDir);
|
|
250
|
+
throw new Error(`A single active change is the default workflow, but ${activeNames.length} active changes already exist: ${activeNames.join(', ')}. Resolve the repository back to one active change before creating another, or add new work with "${queueCommand}".`);
|
|
251
|
+
}
|
|
232
252
|
async writePluginArtifacts(featureDir, activatedSteps) {
|
|
233
253
|
const checkpointSteps = activatedSteps.filter(step => step === 'checkpoint_ui_review' || step === 'checkpoint_flow_check');
|
|
234
254
|
if (checkpointSteps.length > 0) {
|
|
@@ -5,6 +5,7 @@ const child_process_1 = require("child_process");
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const gray_matter_1 = require("gray-matter");
|
|
7
7
|
const constants_1 = require("../core/constants");
|
|
8
|
+
const helpers_1 = require("../utils/helpers");
|
|
8
9
|
const BaseCommand_1 = require("./BaseCommand");
|
|
9
10
|
const services_1 = require("../services");
|
|
10
11
|
const subcommandHelp_1 = require("../utils/subcommandHelp");
|
|
@@ -162,7 +163,7 @@ class PluginsCommand extends BaseCommand_1.BaseCommand {
|
|
|
162
163
|
if (plugin.runner.extraEnvCount > 0) {
|
|
163
164
|
console.log(` Extra env entries: ${plugin.runner.extraEnvCount}`);
|
|
164
165
|
}
|
|
165
|
-
console.log(` Doctor: ospec plugins doctor
|
|
166
|
+
console.log(` Doctor: ${(0, helpers_1.formatCliCommand)('ospec', 'plugins', 'doctor', plugin.name, projectPath)}`);
|
|
166
167
|
}
|
|
167
168
|
console.log();
|
|
168
169
|
});
|
|
@@ -231,7 +232,7 @@ class PluginsCommand extends BaseCommand_1.BaseCommand {
|
|
|
231
232
|
this.info(` codex model: ${this.getStitchCodexConfig(nextConfig.plugins.stitch).model || '(cli default)'}`);
|
|
232
233
|
if (enabled) {
|
|
233
234
|
this.info(` token env: ${nextConfig.plugins.stitch.runner.token_env || '(not required)'}`);
|
|
234
|
-
this.info(` doctor: ospec plugins doctor stitch
|
|
235
|
+
this.info(` doctor: ${(0, helpers_1.formatCliCommand)('ospec', 'plugins', 'doctor', 'stitch', projectPath)}`);
|
|
235
236
|
}
|
|
236
237
|
this.info(' Affects new changes by default; update existing changes manually if needed');
|
|
237
238
|
return;
|
|
@@ -311,7 +312,7 @@ class PluginsCommand extends BaseCommand_1.BaseCommand {
|
|
|
311
312
|
this.info(` runner.command: ${nextConfig.plugins.checkpoint.runner.command || '(built-in adapter)'}`);
|
|
312
313
|
this.info(` stitch integration: ${nextConfig.plugins.checkpoint.stitch_integration.enabled ? 'enabled' : 'disabled'}`);
|
|
313
314
|
if (enabled) {
|
|
314
|
-
this.info(` doctor: ospec plugins doctor checkpoint
|
|
315
|
+
this.info(` doctor: ${(0, helpers_1.formatCliCommand)('ospec', 'plugins', 'doctor', 'checkpoint', projectPath)}`);
|
|
315
316
|
}
|
|
316
317
|
this.info(' Affects new changes by default; update existing changes manually if needed');
|
|
317
318
|
return;
|
|
@@ -16,6 +16,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
16
16
|
|
|
17
17
|
const services_1 = require("../services");
|
|
18
18
|
|
|
19
|
+
const helpers_1 = require("../utils/helpers");
|
|
20
|
+
|
|
19
21
|
const subcommandHelp_1 = require("../utils/subcommandHelp");
|
|
20
22
|
|
|
21
23
|
const BaseCommand_1 = require("./BaseCommand");
|
|
@@ -554,7 +556,7 @@ class SkillCommand extends BaseCommand_1.BaseCommand {
|
|
|
554
556
|
|
|
555
557
|
if (!result.inSync) {
|
|
556
558
|
|
|
557
|
-
console.log(`\nRecommendation: run "ospec skill
|
|
559
|
+
console.log(`\nRecommendation: run "${(0, helpers_1.formatCliCommand)('ospec', 'skill', this.getInstallAction(provider), result.skillName, selection.targetDir)}" to sync this skill.`);
|
|
558
560
|
|
|
559
561
|
}
|
|
560
562
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.StatusCommand = void 0;
|
|
4
|
+
const path = require("path");
|
|
4
5
|
const services_1 = require("../services");
|
|
6
|
+
const helpers_1 = require("../utils/helpers");
|
|
5
7
|
const BaseCommand_1 = require("./BaseCommand");
|
|
6
8
|
class StatusCommand extends BaseCommand_1.BaseCommand {
|
|
7
9
|
async execute(projectPath) {
|
|
@@ -85,6 +87,9 @@ class StatusCommand extends BaseCommand_1.BaseCommand {
|
|
|
85
87
|
console.log(` ${status}: ${count}`);
|
|
86
88
|
}
|
|
87
89
|
console.log(`Protocol summary: PASS ${changes.totals.pass} | WARN ${changes.totals.warn} | FAIL ${changes.totals.fail}`);
|
|
90
|
+
if (execution.totalActiveChanges > 1) {
|
|
91
|
+
console.log('Workflow warning: multiple active changes detected. The default workflow expects one active change unless you are explicitly managing extra work as queued changes.');
|
|
92
|
+
}
|
|
88
93
|
if (execution.activeChanges.length > 0) {
|
|
89
94
|
console.log('\nCurrent changes:');
|
|
90
95
|
for (const change of execution.activeChanges) {
|
|
@@ -120,37 +125,46 @@ class StatusCommand extends BaseCommand_1.BaseCommand {
|
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
getRecommendedNextSteps(projectPath, structure, docs, execution, queuedChanges, runReport) {
|
|
128
|
+
const formatCommand = (...args) => (0, helpers_1.formatCliCommand)('ospec', ...args);
|
|
123
129
|
if (!structure.initialized) {
|
|
124
130
|
return [
|
|
125
|
-
`Run "
|
|
131
|
+
`Run "${formatCommand('init', projectPath)}" to initialize the repository to a change-ready state.`,
|
|
126
132
|
];
|
|
127
133
|
}
|
|
128
134
|
if (docs.missingRequired.length > 0 || docs.coverage < 100) {
|
|
129
135
|
return [
|
|
130
136
|
'The repository is initialized, but the project knowledge layer is still incomplete.',
|
|
131
|
-
`Run "
|
|
132
|
-
`If you only want to refresh or repair docs without rerunning full init messaging, use "
|
|
137
|
+
`Run "${formatCommand('init', projectPath)}" to reconcile the repository back to change-ready state and regenerate missing project knowledge docs.`,
|
|
138
|
+
`If you only want to refresh or repair docs without rerunning full init messaging, use "${formatCommand('docs', 'generate', projectPath)}".`,
|
|
133
139
|
];
|
|
134
140
|
}
|
|
135
141
|
if (execution.totalActiveChanges === 0 && queuedChanges.length === 0) {
|
|
136
142
|
return [
|
|
137
|
-
`Or run "
|
|
143
|
+
`Or run "${formatCommand('new', '<change-name>', projectPath)}" if you want to create the first change from CLI.`,
|
|
138
144
|
];
|
|
139
145
|
}
|
|
140
146
|
if (execution.totalActiveChanges === 0 && queuedChanges.length > 0) {
|
|
141
147
|
return [
|
|
142
148
|
`There is no active change right now, but ${queuedChanges.length} queued change(s) are waiting.`,
|
|
143
|
-
`Run "
|
|
144
|
-
`Or run "
|
|
149
|
+
`Run "${formatCommand('queue', 'next', projectPath)}" if you want to activate the next queued change manually.`,
|
|
150
|
+
`Or run "${formatCommand('run', 'start', projectPath)}" to begin explicit queue tracking.`,
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
if (execution.totalActiveChanges > 1) {
|
|
154
|
+
return [
|
|
155
|
+
`Multiple active changes are present. The default workflow expects one active change, but ${execution.totalActiveChanges} were found.`,
|
|
156
|
+
`Resolve the repository back to a single active change before using "${formatCommand('run', 'start', projectPath)}".`,
|
|
157
|
+
`For additional work, create queued changes explicitly with "${formatCommand('queue', 'add', '<change-name>', projectPath)}".`,
|
|
145
158
|
];
|
|
146
159
|
}
|
|
147
160
|
const currentChange = execution.activeChanges[0];
|
|
161
|
+
const currentChangePath = path.join(projectPath, 'changes', 'active', currentChange.name);
|
|
148
162
|
const nextSteps = [
|
|
149
|
-
`Continue the active change "${currentChange.name}" with "
|
|
150
|
-
`Run "
|
|
163
|
+
`Continue the active change "${currentChange.name}" with "${formatCommand('progress', currentChangePath)}".`,
|
|
164
|
+
`Run "${formatCommand('verify', currentChangePath)}" before trying to archive it.`,
|
|
151
165
|
];
|
|
152
166
|
if (queuedChanges.length > 0) {
|
|
153
|
-
nextSteps.push(`There are ${queuedChanges.length} queued change(s) waiting behind the active one. Use "
|
|
167
|
+
nextSteps.push(`There are ${queuedChanges.length} queued change(s) waiting behind the active one. Use "${formatCommand('run', runReport.currentRun ? 'step' : 'start', projectPath)}" when you want explicit queue progression.`);
|
|
154
168
|
}
|
|
155
169
|
return nextSteps;
|
|
156
170
|
}
|
|
@@ -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([
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
//# sourceMappingURL=helpers.d.ts.map
|
|
1
|
+
export declare function quoteCliArg(value: string): string;
|
|
2
|
+
export declare function formatCliCommand(...args: Array<string | null | undefined>): string;
|
|
3
|
+
//# sourceMappingURL=helpers.d.ts.map
|
package/dist/utils/helpers.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCliCommand = exports.quoteCliArg = void 0;
|
|
4
|
+
function quoteCliArg(value) {
|
|
5
|
+
const text = String(value ?? '');
|
|
6
|
+
if (text.length === 0) {
|
|
7
|
+
return '""';
|
|
8
|
+
}
|
|
9
|
+
return /\s/.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
|
10
|
+
}
|
|
11
|
+
exports.quoteCliArg = quoteCliArg;
|
|
12
|
+
function formatCliCommand(...args) {
|
|
13
|
+
return args
|
|
14
|
+
.filter((arg) => arg !== undefined && arg !== null && arg !== '')
|
|
15
|
+
.map((arg, index) => {
|
|
16
|
+
const text = String(arg);
|
|
17
|
+
return index === 0 ? text : quoteCliArg(text);
|
|
18
|
+
})
|
|
19
|
+
.join(' ');
|
|
20
|
+
}
|
|
21
|
+
exports.formatCliCommand = formatCliCommand;
|
|
22
|
+
//# sourceMappingURL=helpers.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawplays/ospec-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "CLI tool for enforcing ospec workflow",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"index:rebuild": "node dist/tools/build-index.js",
|
|
22
22
|
"validate": "node dist/cli/commands/validate.js",
|
|
23
23
|
"release:smoke": "node scripts/release-smoke.js",
|
|
24
|
+
"release:notes": "node scripts/release-notes.js",
|
|
24
25
|
"release:sync-version": "node scripts/sync-version.js",
|
|
25
26
|
"release:bump:patch": "npm version patch --no-git-tag-version",
|
|
26
27
|
"release:bump:minor": "npm version minor --no-git-tag-version",
|