omgkit 2.24.3 → 2.25.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 +69 -0
- package/bin/omgkit.js +150 -14
- package/lib/cli.js +953 -4
- package/package.json +1 -1
- package/plugin/registry.yaml +2 -2
- package/templates/settings.json +6 -0
package/README.md
CHANGED
|
@@ -747,6 +747,8 @@ Generated in `.omgkit/stdrules/` when you run `omgkit init`:
|
|
|
747
747
|
|
|
748
748
|
## CLI Commands
|
|
749
749
|
|
|
750
|
+
### Global Commands
|
|
751
|
+
|
|
750
752
|
```bash
|
|
751
753
|
omgkit install # Install plugin to Claude Code
|
|
752
754
|
omgkit init # Initialize .omgkit/ in project
|
|
@@ -757,6 +759,73 @@ omgkit uninstall # Remove plugin
|
|
|
757
759
|
omgkit help # Show help
|
|
758
760
|
```
|
|
759
761
|
|
|
762
|
+
### Project Upgrade Commands (New)
|
|
763
|
+
|
|
764
|
+
Keep your project up-to-date with the latest OMGKIT features:
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
omgkit project:upgrade # Upgrade project to latest OMGKIT version
|
|
768
|
+
omgkit project:upgrade --dry # Preview changes without applying
|
|
769
|
+
omgkit project:rollback # Rollback to previous backup
|
|
770
|
+
omgkit project:backups # List available backups
|
|
771
|
+
omgkit project:version # Show project's OMGKIT version
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### Safe Upgrade System
|
|
775
|
+
|
|
776
|
+
OMGKIT's upgrade system is designed with safety first:
|
|
777
|
+
|
|
778
|
+
| Feature | Description |
|
|
779
|
+
|---------|-------------|
|
|
780
|
+
| **Version Tracking** | Each project tracks its OMGKIT version in settings.json |
|
|
781
|
+
| **Smart Merge** | workflow.yaml uses add-only merge (never overwrites your values) |
|
|
782
|
+
| **Protected Files** | config.yaml, sprints/*, artifacts/*, devlogs/* are NEVER modified |
|
|
783
|
+
| **Auto-Backup** | Creates timestamped backup before any changes |
|
|
784
|
+
| **Dry Run** | Preview all changes with `--dry` flag before applying |
|
|
785
|
+
| **Rollback** | One command to restore previous state if needed |
|
|
786
|
+
|
|
787
|
+
#### What Gets Upgraded
|
|
788
|
+
|
|
789
|
+
| File Type | Upgrade Behavior |
|
|
790
|
+
|-----------|-----------------|
|
|
791
|
+
| **stdrules/** | New standards are added, modified ones offer 3-way merge |
|
|
792
|
+
| **workflow.yaml** | Smart merge adds new sections, preserves your customizations |
|
|
793
|
+
| **CLAUDE.md** | Updated with new instructions |
|
|
794
|
+
| **settings.json** | Version updated, structure preserved |
|
|
795
|
+
| **Your files** | NEVER touched (config.yaml, sprints, artifacts, devlogs) |
|
|
796
|
+
|
|
797
|
+
### Config Commands (New)
|
|
798
|
+
|
|
799
|
+
Configure workflow settings via CLI:
|
|
800
|
+
|
|
801
|
+
```bash
|
|
802
|
+
# Get a config value
|
|
803
|
+
omgkit config get testing.enforcement.level
|
|
804
|
+
omgkit config get testing.coverage_gates.unit
|
|
805
|
+
|
|
806
|
+
# Set a config value
|
|
807
|
+
omgkit config set testing.enforcement.level strict
|
|
808
|
+
omgkit config set testing.auto_generate_tasks true
|
|
809
|
+
omgkit config set testing.coverage_gates.unit.minimum 90
|
|
810
|
+
|
|
811
|
+
# List all config or specific section
|
|
812
|
+
omgkit config list
|
|
813
|
+
omgkit config list testing
|
|
814
|
+
|
|
815
|
+
# Reset to default value
|
|
816
|
+
omgkit config reset testing.enforcement.level
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
#### Supported Value Types
|
|
820
|
+
|
|
821
|
+
| Type | Example |
|
|
822
|
+
|------|---------|
|
|
823
|
+
| **String** | `omgkit config set git.main_branch develop` |
|
|
824
|
+
| **Boolean** | `omgkit config set testing.auto_generate_tasks true` |
|
|
825
|
+
| **Number** | `omgkit config set testing.coverage_gates.unit.minimum 90` |
|
|
826
|
+
|
|
827
|
+
**Note**: For arrays, edit `.omgkit/workflow.yaml` directly.
|
|
828
|
+
|
|
760
829
|
---
|
|
761
830
|
|
|
762
831
|
## Documentation Sync Automation
|
package/bin/omgkit.js
CHANGED
|
@@ -21,6 +21,14 @@ import {
|
|
|
21
21
|
doctor,
|
|
22
22
|
uninstallPlugin,
|
|
23
23
|
listComponents,
|
|
24
|
+
upgradeProject,
|
|
25
|
+
rollbackProject,
|
|
26
|
+
listProjectBackups,
|
|
27
|
+
getProjectVersion,
|
|
28
|
+
getConfig,
|
|
29
|
+
setConfig,
|
|
30
|
+
listConfig,
|
|
31
|
+
resetConfig,
|
|
24
32
|
COLORS,
|
|
25
33
|
BANNER,
|
|
26
34
|
log
|
|
@@ -37,21 +45,47 @@ function showHelp() {
|
|
|
37
45
|
${COLORS.bright}USAGE${COLORS.reset}
|
|
38
46
|
omgkit <command> [options]
|
|
39
47
|
|
|
40
|
-
${COLORS.bright}COMMANDS${COLORS.reset}
|
|
41
|
-
${COLORS.cyan}install${COLORS.reset}
|
|
42
|
-
${COLORS.cyan}
|
|
43
|
-
${COLORS.cyan}
|
|
44
|
-
${COLORS.cyan}
|
|
45
|
-
${COLORS.cyan}
|
|
46
|
-
${COLORS.cyan}
|
|
47
|
-
${COLORS.cyan}
|
|
48
|
-
|
|
48
|
+
${COLORS.bright}GLOBAL COMMANDS${COLORS.reset}
|
|
49
|
+
${COLORS.cyan}install${COLORS.reset} Install OMGKIT plugin to Claude Code
|
|
50
|
+
${COLORS.cyan}update${COLORS.reset} Update OMGKIT plugin (same as install)
|
|
51
|
+
${COLORS.cyan}uninstall${COLORS.reset} Remove OMGKIT plugin
|
|
52
|
+
${COLORS.cyan}doctor${COLORS.reset} Check installation status
|
|
53
|
+
${COLORS.cyan}list${COLORS.reset} List all commands/agents/skills
|
|
54
|
+
${COLORS.cyan}version${COLORS.reset} Show version
|
|
55
|
+
${COLORS.cyan}help${COLORS.reset} Show this help
|
|
56
|
+
|
|
57
|
+
${COLORS.bright}PROJECT COMMANDS${COLORS.reset}
|
|
58
|
+
${COLORS.cyan}init${COLORS.reset} Initialize .omgkit/ in current project
|
|
59
|
+
${COLORS.cyan}project:upgrade${COLORS.reset} Upgrade project to latest OMGKIT version
|
|
60
|
+
${COLORS.cyan}project:rollback${COLORS.reset} Rollback project to previous backup
|
|
61
|
+
${COLORS.cyan}project:backups${COLORS.reset} List available project backups
|
|
62
|
+
${COLORS.cyan}project:version${COLORS.reset} Show project's OMGKIT version
|
|
63
|
+
|
|
64
|
+
${COLORS.bright}CONFIG COMMANDS${COLORS.reset}
|
|
65
|
+
${COLORS.cyan}config get <key>${COLORS.reset} Get config value (e.g., testing.enforcement.level)
|
|
66
|
+
${COLORS.cyan}config set <key> <val>${COLORS.reset} Set config value
|
|
67
|
+
${COLORS.cyan}config list [section]${COLORS.reset} List all config or specific section
|
|
68
|
+
${COLORS.cyan}config reset <key>${COLORS.reset} Reset config key to default
|
|
69
|
+
|
|
70
|
+
${COLORS.bright}UPGRADE OPTIONS${COLORS.reset}
|
|
71
|
+
--dry Show what would change without applying
|
|
72
|
+
--force Skip confirmation prompts
|
|
49
73
|
|
|
50
74
|
${COLORS.bright}EXAMPLES${COLORS.reset}
|
|
51
|
-
omgkit install
|
|
52
|
-
omgkit init
|
|
53
|
-
omgkit
|
|
54
|
-
omgkit
|
|
75
|
+
omgkit install # Install plugin globally
|
|
76
|
+
omgkit init # Initialize project
|
|
77
|
+
omgkit project:upgrade # Upgrade project config
|
|
78
|
+
omgkit project:upgrade --dry # Preview upgrade changes
|
|
79
|
+
omgkit project:rollback # Rollback to last backup
|
|
80
|
+
omgkit doctor # Check status
|
|
81
|
+
|
|
82
|
+
${COLORS.bright}CONFIG EXAMPLES${COLORS.reset}
|
|
83
|
+
omgkit config set testing.enforcement.level strict
|
|
84
|
+
omgkit config set testing.auto_generate_tasks true
|
|
85
|
+
omgkit config set testing.coverage_gates.unit.minimum 90
|
|
86
|
+
omgkit config get testing
|
|
87
|
+
omgkit config list testing
|
|
88
|
+
omgkit config reset testing.enforcement.level
|
|
55
89
|
|
|
56
90
|
${COLORS.bright}AFTER INSTALLATION${COLORS.reset}
|
|
57
91
|
In Claude Code, type / to see all OMGKIT commands:
|
|
@@ -59,7 +93,7 @@ ${COLORS.bright}AFTER INSTALLATION${COLORS.reset}
|
|
|
59
93
|
Core: /dev:feature, /dev:fix, /planning:plan, /quality:test, /dev:review
|
|
60
94
|
Omega: /omega:10x, /omega:100x, /omega:1000x, /omega:principles
|
|
61
95
|
Sprint: /sprint:vision-set, /sprint:sprint-new, /sprint:team-run
|
|
62
|
-
|
|
96
|
+
Testing: /quality:verify-done, /quality:coverage-check, /quality:test-plan
|
|
63
97
|
|
|
64
98
|
${COLORS.bright}DOCUMENTATION${COLORS.reset}
|
|
65
99
|
https://omg.mintlify.app
|
|
@@ -102,6 +136,108 @@ switch (command) {
|
|
|
102
136
|
if (!result.success) process.exit(1);
|
|
103
137
|
break;
|
|
104
138
|
}
|
|
139
|
+
case 'project:upgrade': {
|
|
140
|
+
const dryRun = args.includes('--dry');
|
|
141
|
+
const force = args.includes('--force');
|
|
142
|
+
const result = upgradeProject({ dryRun, force });
|
|
143
|
+
if (!result.success && !result.upToDate) process.exit(1);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'project:rollback': {
|
|
147
|
+
const result = rollbackProject();
|
|
148
|
+
if (!result.success) process.exit(1);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'project:backups': {
|
|
152
|
+
console.log(BANNER);
|
|
153
|
+
const backups = listProjectBackups();
|
|
154
|
+
if (backups.length === 0) {
|
|
155
|
+
log.info('No backups found');
|
|
156
|
+
} else {
|
|
157
|
+
console.log(`${COLORS.bright}Available Backups${COLORS.reset}\n`);
|
|
158
|
+
backups.forEach((backup, i) => {
|
|
159
|
+
const age = Math.round((Date.now() - backup.created) / 1000 / 60);
|
|
160
|
+
const ageStr = age < 60 ? `${age} minutes ago` : `${Math.round(age / 60)} hours ago`;
|
|
161
|
+
console.log(` ${i + 1}. ${backup.name} (${ageStr})`);
|
|
162
|
+
});
|
|
163
|
+
console.log(`\nTo restore: omgkit project:rollback`);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'project:version': {
|
|
168
|
+
const projectVersion = getProjectVersion();
|
|
169
|
+
const omgkitVersion = getVersion();
|
|
170
|
+
console.log(`Project OMGKIT version: ${projectVersion || 'not tracked (older project)'}`);
|
|
171
|
+
console.log(`Current OMGKIT version: ${omgkitVersion}`);
|
|
172
|
+
if (projectVersion && projectVersion !== omgkitVersion) {
|
|
173
|
+
log.info('Run: omgkit project:upgrade');
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case 'config': {
|
|
178
|
+
const subCommand = args[0];
|
|
179
|
+
switch (subCommand) {
|
|
180
|
+
case 'get': {
|
|
181
|
+
const key = args[1];
|
|
182
|
+
if (!key) {
|
|
183
|
+
log.error('Usage: omgkit config get <key>');
|
|
184
|
+
log.info('Example: omgkit config get testing.enforcement.level');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const result = getConfig(key);
|
|
188
|
+
if (!result.success) process.exit(1);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'set': {
|
|
192
|
+
const key = args[1];
|
|
193
|
+
const value = args[2];
|
|
194
|
+
if (!key || value === undefined) {
|
|
195
|
+
log.error('Usage: omgkit config set <key> <value>');
|
|
196
|
+
log.info('Example: omgkit config set testing.enforcement.level strict');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
const result = setConfig(key, value);
|
|
200
|
+
if (!result.success) process.exit(1);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'list': {
|
|
204
|
+
const section = args[1];
|
|
205
|
+
const result = listConfig({ section });
|
|
206
|
+
if (!result.success) process.exit(1);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'reset': {
|
|
210
|
+
const key = args[1];
|
|
211
|
+
if (!key) {
|
|
212
|
+
log.error('Usage: omgkit config reset <key>');
|
|
213
|
+
log.info('Example: omgkit config reset testing.enforcement.level');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const result = resetConfig(key);
|
|
217
|
+
if (!result.success) process.exit(1);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
default: {
|
|
221
|
+
log.error(`Unknown config subcommand: ${subCommand || '(none)'}`);
|
|
222
|
+
console.log(`
|
|
223
|
+
${COLORS.bright}Config Commands:${COLORS.reset}
|
|
224
|
+
omgkit config get <key> Get config value
|
|
225
|
+
omgkit config set <key> <val> Set config value
|
|
226
|
+
omgkit config list [section] List all config
|
|
227
|
+
omgkit config reset <key> Reset to default
|
|
228
|
+
|
|
229
|
+
${COLORS.bright}Examples:${COLORS.reset}
|
|
230
|
+
omgkit config get testing.enforcement.level
|
|
231
|
+
omgkit config set testing.enforcement.level strict
|
|
232
|
+
omgkit config set testing.auto_generate_tasks true
|
|
233
|
+
omgkit config list testing
|
|
234
|
+
omgkit config reset testing.enforcement.level
|
|
235
|
+
`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
105
241
|
case 'version':
|
|
106
242
|
case '-v':
|
|
107
243
|
case '--version': {
|
package/lib/cli.js
CHANGED
|
@@ -393,21 +393,33 @@ export function initProject(options = {}) {
|
|
|
393
393
|
{ src: 'CLAUDE.md', dest: 'CLAUDE.md' },
|
|
394
394
|
{ src: 'vision.yaml', dest: '.omgkit/sprints/vision.yaml' },
|
|
395
395
|
{ src: 'backlog.yaml', dest: '.omgkit/sprints/backlog.yaml' },
|
|
396
|
-
{ src: 'settings.json', dest: '.omgkit/settings.json' },
|
|
396
|
+
{ src: 'settings.json', dest: '.omgkit/settings.json', process: true },
|
|
397
397
|
{ src: 'devlogs/README.md', dest: '.omgkit/devlogs/README.md' },
|
|
398
398
|
{ src: 'stdrules/README.md', dest: '.omgkit/stdrules/README.md' },
|
|
399
399
|
{ src: 'stdrules/SKILL_STANDARDS.md', dest: '.omgkit/stdrules/SKILL_STANDARDS.md' },
|
|
400
400
|
{ src: 'stdrules/BEFORE_COMMIT.md', dest: '.omgkit/stdrules/BEFORE_COMMIT.md' },
|
|
401
401
|
{ src: 'stdrules/TESTING_STANDARDS.md', dest: '.omgkit/stdrules/TESTING_STANDARDS.md' },
|
|
402
|
-
{ src: 'artifacts/README.md', dest: '.omgkit/artifacts/README.md' }
|
|
402
|
+
{ src: 'artifacts/README.md', dest: '.omgkit/artifacts/README.md' },
|
|
403
|
+
{ src: 'omgkit/workflow.yaml', dest: '.omgkit/workflow.yaml' }
|
|
403
404
|
];
|
|
404
405
|
|
|
405
|
-
|
|
406
|
+
const version = getVersion();
|
|
407
|
+
const initDate = new Date().toISOString().split('T')[0];
|
|
408
|
+
|
|
409
|
+
templates.forEach(({ src, dest, process: shouldProcess }) => {
|
|
406
410
|
const srcPath = join(templatesDir, src);
|
|
407
411
|
const destPath = join(cwd, dest);
|
|
408
412
|
|
|
409
413
|
if (existsSync(srcPath) && !existsSync(destPath)) {
|
|
410
|
-
|
|
414
|
+
if (shouldProcess) {
|
|
415
|
+
// Process template placeholders
|
|
416
|
+
let content = readFileSync(srcPath, 'utf8');
|
|
417
|
+
content = content.replace(/\{\{OMGKIT_VERSION\}\}/g, version);
|
|
418
|
+
content = content.replace(/\{\{INIT_DATE\}\}/g, initDate);
|
|
419
|
+
writeFileSync(destPath, content);
|
|
420
|
+
} else {
|
|
421
|
+
cpSync(srcPath, destPath);
|
|
422
|
+
}
|
|
411
423
|
createdFiles.push(dest);
|
|
412
424
|
if (!silent) log.success(`Created ${dest}`);
|
|
413
425
|
}
|
|
@@ -934,3 +946,940 @@ export function validatePluginFile(filePath, requiredFields = []) {
|
|
|
934
946
|
result.frontmatter = frontmatter;
|
|
935
947
|
return result;
|
|
936
948
|
}
|
|
949
|
+
|
|
950
|
+
// ============================================================================
|
|
951
|
+
// PROJECT UPGRADE SYSTEM
|
|
952
|
+
// ============================================================================
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Create a hash of file content for change detection
|
|
956
|
+
* @param {string} content - File content
|
|
957
|
+
* @returns {string} Hash string
|
|
958
|
+
*/
|
|
959
|
+
function hashContent(content) {
|
|
960
|
+
let hash = 0;
|
|
961
|
+
for (let i = 0; i < content.length; i++) {
|
|
962
|
+
const char = content.charCodeAt(i);
|
|
963
|
+
hash = ((hash << 5) - hash) + char;
|
|
964
|
+
hash = hash & hash;
|
|
965
|
+
}
|
|
966
|
+
return hash.toString(16);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Deep merge two objects (for YAML config merging)
|
|
971
|
+
* Only adds new keys, never overwrites existing values
|
|
972
|
+
* @param {Object} target - Target object (user's config)
|
|
973
|
+
* @param {Object} source - Source object (new template)
|
|
974
|
+
* @returns {Object} Merged object
|
|
975
|
+
*/
|
|
976
|
+
function deepMergeAddOnly(target, source) {
|
|
977
|
+
const result = { ...target };
|
|
978
|
+
|
|
979
|
+
for (const key of Object.keys(source)) {
|
|
980
|
+
if (!(key in result)) {
|
|
981
|
+
// Key doesn't exist in target, add it
|
|
982
|
+
result[key] = source[key];
|
|
983
|
+
} else if (
|
|
984
|
+
typeof result[key] === 'object' &&
|
|
985
|
+
result[key] !== null &&
|
|
986
|
+
!Array.isArray(result[key]) &&
|
|
987
|
+
typeof source[key] === 'object' &&
|
|
988
|
+
source[key] !== null &&
|
|
989
|
+
!Array.isArray(source[key])
|
|
990
|
+
) {
|
|
991
|
+
// Both are objects, recurse
|
|
992
|
+
result[key] = deepMergeAddOnly(result[key], source[key]);
|
|
993
|
+
}
|
|
994
|
+
// If key exists and is not an object, keep user's value
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Simple YAML parser for config files
|
|
1002
|
+
* @param {string} content - YAML content
|
|
1003
|
+
* @returns {Object} Parsed object
|
|
1004
|
+
*/
|
|
1005
|
+
function parseSimpleYaml(content) {
|
|
1006
|
+
const result = {};
|
|
1007
|
+
const lines = content.split('\n');
|
|
1008
|
+
const stack = [{ obj: result, indent: -1 }];
|
|
1009
|
+
|
|
1010
|
+
for (const line of lines) {
|
|
1011
|
+
// Skip comments and empty lines
|
|
1012
|
+
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
1013
|
+
|
|
1014
|
+
const indent = line.search(/\S/);
|
|
1015
|
+
if (indent === -1) continue;
|
|
1016
|
+
|
|
1017
|
+
const trimmed = line.trim();
|
|
1018
|
+
|
|
1019
|
+
// Pop stack to find parent
|
|
1020
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
1021
|
+
stack.pop();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const parent = stack[stack.length - 1].obj;
|
|
1025
|
+
|
|
1026
|
+
if (trimmed.startsWith('- ')) {
|
|
1027
|
+
// Array item
|
|
1028
|
+
const value = trimmed.substring(2).trim();
|
|
1029
|
+
if (!Array.isArray(parent)) {
|
|
1030
|
+
// Find the key that should be an array
|
|
1031
|
+
const keys = Object.keys(parent);
|
|
1032
|
+
const lastKey = keys[keys.length - 1];
|
|
1033
|
+
if (lastKey && parent[lastKey] === null) {
|
|
1034
|
+
parent[lastKey] = [value];
|
|
1035
|
+
}
|
|
1036
|
+
} else {
|
|
1037
|
+
parent.push(value);
|
|
1038
|
+
}
|
|
1039
|
+
} else if (trimmed.includes(':')) {
|
|
1040
|
+
const colonIndex = trimmed.indexOf(':');
|
|
1041
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
1042
|
+
let value = trimmed.substring(colonIndex + 1).trim();
|
|
1043
|
+
|
|
1044
|
+
// Remove quotes if present
|
|
1045
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
1046
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
1047
|
+
value = value.slice(1, -1);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (value === '' || value === null) {
|
|
1051
|
+
// Object or array follows
|
|
1052
|
+
parent[key] = {};
|
|
1053
|
+
stack.push({ obj: parent[key], indent });
|
|
1054
|
+
} else if (value === 'true') {
|
|
1055
|
+
parent[key] = true;
|
|
1056
|
+
} else if (value === 'false') {
|
|
1057
|
+
parent[key] = false;
|
|
1058
|
+
} else if (!isNaN(Number(value)) && value !== '') {
|
|
1059
|
+
parent[key] = Number(value);
|
|
1060
|
+
} else {
|
|
1061
|
+
parent[key] = value;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return result;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Serialize object to simple YAML
|
|
1071
|
+
* @param {Object} obj - Object to serialize
|
|
1072
|
+
* @param {number} indent - Current indentation level
|
|
1073
|
+
* @returns {string} YAML string
|
|
1074
|
+
*/
|
|
1075
|
+
function toSimpleYaml(obj, indent = 0) {
|
|
1076
|
+
const spaces = ' '.repeat(indent);
|
|
1077
|
+
let result = '';
|
|
1078
|
+
|
|
1079
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1080
|
+
if (value === null || value === undefined) {
|
|
1081
|
+
result += `${spaces}${key}:\n`;
|
|
1082
|
+
} else if (Array.isArray(value)) {
|
|
1083
|
+
result += `${spaces}${key}:\n`;
|
|
1084
|
+
for (const item of value) {
|
|
1085
|
+
if (typeof item === 'object') {
|
|
1086
|
+
result += `${spaces} -\n${toSimpleYaml(item, indent + 2)}`;
|
|
1087
|
+
} else {
|
|
1088
|
+
result += `${spaces} - ${item}\n`;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
} else if (typeof value === 'object') {
|
|
1092
|
+
result += `${spaces}${key}:\n${toSimpleYaml(value, indent + 1)}`;
|
|
1093
|
+
} else if (typeof value === 'string' && (value.includes(':') || value.includes('#'))) {
|
|
1094
|
+
result += `${spaces}${key}: "${value}"\n`;
|
|
1095
|
+
} else {
|
|
1096
|
+
result += `${spaces}${key}: ${value}\n`;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Get project settings including version info
|
|
1105
|
+
* @param {string} cwd - Project directory
|
|
1106
|
+
* @returns {Object|null} Settings object or null
|
|
1107
|
+
*/
|
|
1108
|
+
export function getProjectSettings(cwd = process.cwd()) {
|
|
1109
|
+
const settingsPath = join(cwd, '.omgkit', 'settings.json');
|
|
1110
|
+
if (!existsSync(settingsPath)) return null;
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
return JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Get project version
|
|
1121
|
+
* @param {string} cwd - Project directory
|
|
1122
|
+
* @returns {string|null} Version string or null
|
|
1123
|
+
*/
|
|
1124
|
+
export function getProjectVersion(cwd = process.cwd()) {
|
|
1125
|
+
const settings = getProjectSettings(cwd);
|
|
1126
|
+
return settings?.omgkit?.version || null;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Compare semantic versions
|
|
1131
|
+
* @param {string} v1 - First version
|
|
1132
|
+
* @param {string} v2 - Second version
|
|
1133
|
+
* @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
1134
|
+
*/
|
|
1135
|
+
function compareVersions(v1, v2) {
|
|
1136
|
+
const parts1 = v1.split('.').map(Number);
|
|
1137
|
+
const parts2 = v2.split('.').map(Number);
|
|
1138
|
+
|
|
1139
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
1140
|
+
const p1 = parts1[i] || 0;
|
|
1141
|
+
const p2 = parts2[i] || 0;
|
|
1142
|
+
if (p1 < p2) return -1;
|
|
1143
|
+
if (p1 > p2) return 1;
|
|
1144
|
+
}
|
|
1145
|
+
return 0;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Create backup of .omgkit directory
|
|
1150
|
+
* @param {string} cwd - Project directory
|
|
1151
|
+
* @returns {Object} Backup result with path
|
|
1152
|
+
*/
|
|
1153
|
+
export function createProjectBackup(cwd = process.cwd()) {
|
|
1154
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1155
|
+
if (!existsSync(omgkitDir)) {
|
|
1156
|
+
return { success: false, error: 'NOT_INITIALIZED' };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const now = new Date();
|
|
1160
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').split('T').join('-').substring(0, 19);
|
|
1161
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
1162
|
+
const backupDir = join(cwd, `.omgkit-backup-${timestamp}-${ms}`);
|
|
1163
|
+
|
|
1164
|
+
try {
|
|
1165
|
+
cpSync(omgkitDir, backupDir, { recursive: true });
|
|
1166
|
+
return { success: true, path: backupDir, timestamp };
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
return { success: false, error: e.message };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* List available backups
|
|
1174
|
+
* @param {string} cwd - Project directory
|
|
1175
|
+
* @returns {Object[]} Array of backup info
|
|
1176
|
+
*/
|
|
1177
|
+
export function listProjectBackups(cwd = process.cwd()) {
|
|
1178
|
+
const backups = [];
|
|
1179
|
+
|
|
1180
|
+
try {
|
|
1181
|
+
const items = readdirSync(cwd);
|
|
1182
|
+
for (const item of items) {
|
|
1183
|
+
if (item.startsWith('.omgkit-backup-')) {
|
|
1184
|
+
const fullPath = join(cwd, item);
|
|
1185
|
+
const stat = statSync(fullPath);
|
|
1186
|
+
if (stat.isDirectory()) {
|
|
1187
|
+
backups.push({
|
|
1188
|
+
name: item,
|
|
1189
|
+
path: fullPath,
|
|
1190
|
+
created: stat.mtime
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
// Directory read error
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return backups.sort((a, b) => b.created - a.created);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Rollback project to a backup
|
|
1204
|
+
* @param {Object} options - Options
|
|
1205
|
+
* @param {string} [options.cwd] - Project directory
|
|
1206
|
+
* @param {string} [options.backupPath] - Specific backup to restore
|
|
1207
|
+
* @param {boolean} [options.silent] - Suppress output
|
|
1208
|
+
* @returns {Object} Result
|
|
1209
|
+
*/
|
|
1210
|
+
export function rollbackProject(options = {}) {
|
|
1211
|
+
const { cwd = process.cwd(), backupPath, silent = false } = options;
|
|
1212
|
+
|
|
1213
|
+
if (!silent) {
|
|
1214
|
+
console.log(BANNER);
|
|
1215
|
+
log.omega('Rolling back project...');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1219
|
+
let targetBackup = backupPath;
|
|
1220
|
+
|
|
1221
|
+
if (!targetBackup) {
|
|
1222
|
+
// Find most recent backup
|
|
1223
|
+
const backups = listProjectBackups(cwd);
|
|
1224
|
+
if (backups.length === 0) {
|
|
1225
|
+
if (!silent) log.error('No backups found');
|
|
1226
|
+
return { success: false, error: 'NO_BACKUPS' };
|
|
1227
|
+
}
|
|
1228
|
+
targetBackup = backups[0].path;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (!existsSync(targetBackup)) {
|
|
1232
|
+
if (!silent) log.error(`Backup not found: ${targetBackup}`);
|
|
1233
|
+
return { success: false, error: 'BACKUP_NOT_FOUND' };
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
try {
|
|
1237
|
+
// Remove current .omgkit
|
|
1238
|
+
if (existsSync(omgkitDir)) {
|
|
1239
|
+
rmSync(omgkitDir, { recursive: true, force: true });
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Restore from backup
|
|
1243
|
+
cpSync(targetBackup, omgkitDir, { recursive: true });
|
|
1244
|
+
|
|
1245
|
+
if (!silent) {
|
|
1246
|
+
log.success(`Rolled back to: ${targetBackup}`);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return { success: true, restoredFrom: targetBackup };
|
|
1250
|
+
} catch (e) {
|
|
1251
|
+
if (!silent) log.error(`Rollback failed: ${e.message}`);
|
|
1252
|
+
return { success: false, error: e.message };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Calculate upgrade changes (dry run)
|
|
1258
|
+
* @param {string} cwd - Project directory
|
|
1259
|
+
* @returns {Object} Changes to be applied
|
|
1260
|
+
*/
|
|
1261
|
+
export function calculateUpgradeChanges(cwd = process.cwd()) {
|
|
1262
|
+
const changes = {
|
|
1263
|
+
settings: [],
|
|
1264
|
+
workflow: [],
|
|
1265
|
+
stdrules: [],
|
|
1266
|
+
newFiles: [],
|
|
1267
|
+
protected: [
|
|
1268
|
+
'config.yaml',
|
|
1269
|
+
'sprints/vision.yaml',
|
|
1270
|
+
'sprints/backlog.yaml',
|
|
1271
|
+
'artifacts/*',
|
|
1272
|
+
'devlogs/*'
|
|
1273
|
+
]
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
const templatesDir = join(getPackageRoot(), 'templates');
|
|
1277
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1278
|
+
const currentVersion = getProjectVersion(cwd);
|
|
1279
|
+
const targetVersion = getVersion();
|
|
1280
|
+
|
|
1281
|
+
// Check workflow.yaml changes
|
|
1282
|
+
const workflowTemplatePath = join(templatesDir, 'omgkit', 'workflow.yaml');
|
|
1283
|
+
const workflowProjectPath = join(omgkitDir, 'workflow.yaml');
|
|
1284
|
+
|
|
1285
|
+
if (existsSync(workflowTemplatePath)) {
|
|
1286
|
+
const templateContent = readFileSync(workflowTemplatePath, 'utf8');
|
|
1287
|
+
const templateConfig = parseSimpleYaml(templateContent);
|
|
1288
|
+
|
|
1289
|
+
if (existsSync(workflowProjectPath)) {
|
|
1290
|
+
const projectContent = readFileSync(workflowProjectPath, 'utf8');
|
|
1291
|
+
const projectConfig = parseSimpleYaml(projectContent);
|
|
1292
|
+
|
|
1293
|
+
// Find new top-level sections
|
|
1294
|
+
for (const key of Object.keys(templateConfig)) {
|
|
1295
|
+
if (!(key in projectConfig)) {
|
|
1296
|
+
changes.workflow.push({
|
|
1297
|
+
type: 'add_section',
|
|
1298
|
+
key,
|
|
1299
|
+
value: templateConfig[key]
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
} else {
|
|
1304
|
+
changes.newFiles.push('workflow.yaml');
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Check stdrules updates
|
|
1309
|
+
const stdrulesTemplate = join(templatesDir, 'stdrules');
|
|
1310
|
+
const stdrulesProject = join(omgkitDir, 'stdrules');
|
|
1311
|
+
|
|
1312
|
+
if (existsSync(stdrulesTemplate)) {
|
|
1313
|
+
const templateFiles = readdirSync(stdrulesTemplate).filter(f => f.endsWith('.md'));
|
|
1314
|
+
const settings = getProjectSettings(cwd) || {};
|
|
1315
|
+
const checksums = settings.file_checksums || {};
|
|
1316
|
+
|
|
1317
|
+
for (const file of templateFiles) {
|
|
1318
|
+
const templatePath = join(stdrulesTemplate, file);
|
|
1319
|
+
const projectPath = join(stdrulesProject, file);
|
|
1320
|
+
const templateContent = readFileSync(templatePath, 'utf8');
|
|
1321
|
+
const templateHash = hashContent(templateContent);
|
|
1322
|
+
|
|
1323
|
+
if (!existsSync(projectPath)) {
|
|
1324
|
+
changes.stdrules.push({
|
|
1325
|
+
type: 'new',
|
|
1326
|
+
file,
|
|
1327
|
+
reason: 'File does not exist'
|
|
1328
|
+
});
|
|
1329
|
+
} else {
|
|
1330
|
+
const projectContent = readFileSync(projectPath, 'utf8');
|
|
1331
|
+
const projectHash = hashContent(projectContent);
|
|
1332
|
+
const originalHash = checksums[`stdrules/${file}`];
|
|
1333
|
+
|
|
1334
|
+
// Update only if user hasn't modified the file
|
|
1335
|
+
if (originalHash && projectHash === originalHash && templateHash !== projectHash) {
|
|
1336
|
+
changes.stdrules.push({
|
|
1337
|
+
type: 'update',
|
|
1338
|
+
file,
|
|
1339
|
+
reason: 'Template updated, user has not modified'
|
|
1340
|
+
});
|
|
1341
|
+
} else if (!originalHash && templateHash !== projectHash) {
|
|
1342
|
+
changes.stdrules.push({
|
|
1343
|
+
type: 'skip',
|
|
1344
|
+
file,
|
|
1345
|
+
reason: 'User may have modified (no checksum recorded)'
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Settings.json updates
|
|
1353
|
+
const settingsPath = join(omgkitDir, 'settings.json');
|
|
1354
|
+
if (existsSync(settingsPath)) {
|
|
1355
|
+
const settings = getProjectSettings(cwd);
|
|
1356
|
+
if (!settings?.omgkit) {
|
|
1357
|
+
changes.settings.push({
|
|
1358
|
+
type: 'add',
|
|
1359
|
+
key: 'omgkit',
|
|
1360
|
+
value: { version: targetVersion, initialized_at: null, last_upgraded: null }
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
if (!settings?.file_checksums) {
|
|
1364
|
+
changes.settings.push({
|
|
1365
|
+
type: 'add',
|
|
1366
|
+
key: 'file_checksums',
|
|
1367
|
+
value: {}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return {
|
|
1373
|
+
currentVersion,
|
|
1374
|
+
targetVersion,
|
|
1375
|
+
needsUpgrade: compareVersions(targetVersion, currentVersion || '0.0.0') > 0,
|
|
1376
|
+
changes
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Upgrade project to latest OMGKIT version
|
|
1382
|
+
* @param {Object} options - Options
|
|
1383
|
+
* @param {string} [options.cwd] - Project directory
|
|
1384
|
+
* @param {boolean} [options.dryRun] - Only show what would change
|
|
1385
|
+
* @param {boolean} [options.force] - Skip confirmation
|
|
1386
|
+
* @param {boolean} [options.silent] - Suppress output
|
|
1387
|
+
* @returns {Object} Upgrade result
|
|
1388
|
+
*/
|
|
1389
|
+
export function upgradeProject(options = {}) {
|
|
1390
|
+
const { cwd = process.cwd(), dryRun = false, force = false, silent = false } = options;
|
|
1391
|
+
|
|
1392
|
+
if (!silent) {
|
|
1393
|
+
console.log(BANNER);
|
|
1394
|
+
log.omega('Checking for project upgrades...');
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1398
|
+
|
|
1399
|
+
// Check if project is initialized
|
|
1400
|
+
if (!existsSync(omgkitDir)) {
|
|
1401
|
+
if (!silent) {
|
|
1402
|
+
log.error('Project not initialized. Run: omgkit init');
|
|
1403
|
+
}
|
|
1404
|
+
return { success: false, error: 'NOT_INITIALIZED' };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Calculate changes
|
|
1408
|
+
const upgradeInfo = calculateUpgradeChanges(cwd);
|
|
1409
|
+
|
|
1410
|
+
if (!upgradeInfo.needsUpgrade &&
|
|
1411
|
+
upgradeInfo.changes.workflow.length === 0 &&
|
|
1412
|
+
upgradeInfo.changes.stdrules.filter(s => s.type !== 'skip').length === 0 &&
|
|
1413
|
+
upgradeInfo.changes.newFiles.length === 0) {
|
|
1414
|
+
if (!silent) {
|
|
1415
|
+
log.success('Project is up to date!');
|
|
1416
|
+
console.log(` Current version: ${upgradeInfo.currentVersion || 'unknown'}`);
|
|
1417
|
+
console.log(` OMGKIT version: ${upgradeInfo.targetVersion}`);
|
|
1418
|
+
}
|
|
1419
|
+
return { success: true, upToDate: true };
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Display changes
|
|
1423
|
+
if (!silent) {
|
|
1424
|
+
console.log(`
|
|
1425
|
+
${COLORS.bright}OMGKIT Project Upgrade${COLORS.reset}
|
|
1426
|
+
${'='.repeat(50)}
|
|
1427
|
+
Current version: ${upgradeInfo.currentVersion || 'unknown'}
|
|
1428
|
+
Target version: ${upgradeInfo.targetVersion}
|
|
1429
|
+
|
|
1430
|
+
${COLORS.bright}Changes to be applied:${COLORS.reset}
|
|
1431
|
+
`);
|
|
1432
|
+
|
|
1433
|
+
if (upgradeInfo.changes.workflow.length > 0) {
|
|
1434
|
+
console.log(`${COLORS.cyan}📁 workflow.yaml${COLORS.reset}`);
|
|
1435
|
+
for (const change of upgradeInfo.changes.workflow) {
|
|
1436
|
+
if (change.type === 'add_section') {
|
|
1437
|
+
console.log(` ${COLORS.green}+ ${change.key}: {...}${COLORS.reset}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (upgradeInfo.changes.stdrules.length > 0) {
|
|
1443
|
+
console.log(`\n${COLORS.cyan}📁 stdrules/${COLORS.reset}`);
|
|
1444
|
+
for (const change of upgradeInfo.changes.stdrules) {
|
|
1445
|
+
if (change.type === 'new') {
|
|
1446
|
+
console.log(` ${COLORS.green}+ ${change.file} (new)${COLORS.reset}`);
|
|
1447
|
+
} else if (change.type === 'update') {
|
|
1448
|
+
console.log(` ${COLORS.yellow}~ ${change.file} (updated)${COLORS.reset}`);
|
|
1449
|
+
} else if (change.type === 'skip') {
|
|
1450
|
+
console.log(` ${COLORS.blue}○ ${change.file} (skipped - may be modified)${COLORS.reset}`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (upgradeInfo.changes.newFiles.length > 0) {
|
|
1456
|
+
console.log(`\n${COLORS.cyan}📁 New files${COLORS.reset}`);
|
|
1457
|
+
for (const file of upgradeInfo.changes.newFiles) {
|
|
1458
|
+
console.log(` ${COLORS.green}+ ${file}${COLORS.reset}`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
console.log(`
|
|
1463
|
+
${COLORS.bright}🔒 Protected (no changes):${COLORS.reset}`);
|
|
1464
|
+
for (const protected_ of upgradeInfo.changes.protected) {
|
|
1465
|
+
console.log(` - ${protected_}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (dryRun) {
|
|
1470
|
+
if (!silent) {
|
|
1471
|
+
console.log(`\n${COLORS.yellow}Dry run - no changes made.${COLORS.reset}`);
|
|
1472
|
+
console.log('Run without --dry to apply changes.');
|
|
1473
|
+
}
|
|
1474
|
+
return { success: true, dryRun: true, changes: upgradeInfo.changes };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Create backup before upgrade
|
|
1478
|
+
if (!silent) log.info('\nCreating backup...');
|
|
1479
|
+
const backup = createProjectBackup(cwd);
|
|
1480
|
+
if (!backup.success) {
|
|
1481
|
+
if (!silent) log.error(`Backup failed: ${backup.error}`);
|
|
1482
|
+
return { success: false, error: 'BACKUP_FAILED' };
|
|
1483
|
+
}
|
|
1484
|
+
if (!silent) log.success(`Backup created: ${backup.path}`);
|
|
1485
|
+
|
|
1486
|
+
const templatesDir = join(getPackageRoot(), 'templates');
|
|
1487
|
+
const applied = { workflow: [], stdrules: [], newFiles: [], settings: [] };
|
|
1488
|
+
|
|
1489
|
+
try {
|
|
1490
|
+
// Apply workflow.yaml changes
|
|
1491
|
+
if (upgradeInfo.changes.workflow.length > 0) {
|
|
1492
|
+
const workflowPath = join(omgkitDir, 'workflow.yaml');
|
|
1493
|
+
const workflowTemplatePath = join(templatesDir, 'omgkit', 'workflow.yaml');
|
|
1494
|
+
|
|
1495
|
+
if (existsSync(workflowPath) && existsSync(workflowTemplatePath)) {
|
|
1496
|
+
const projectContent = readFileSync(workflowPath, 'utf8');
|
|
1497
|
+
const templateContent = readFileSync(workflowTemplatePath, 'utf8');
|
|
1498
|
+
|
|
1499
|
+
const projectConfig = parseSimpleYaml(projectContent);
|
|
1500
|
+
const templateConfig = parseSimpleYaml(templateContent);
|
|
1501
|
+
|
|
1502
|
+
const merged = deepMergeAddOnly(projectConfig, templateConfig);
|
|
1503
|
+
|
|
1504
|
+
// Preserve comments from original file by appending new sections
|
|
1505
|
+
let newContent = projectContent;
|
|
1506
|
+
|
|
1507
|
+
for (const change of upgradeInfo.changes.workflow) {
|
|
1508
|
+
if (change.type === 'add_section') {
|
|
1509
|
+
// Find section in template and append
|
|
1510
|
+
const sectionRegex = new RegExp(`^${change.key}:`, 'm');
|
|
1511
|
+
const templateLines = templateContent.split('\n');
|
|
1512
|
+
let inSection = false;
|
|
1513
|
+
let sectionContent = '';
|
|
1514
|
+
|
|
1515
|
+
for (const line of templateLines) {
|
|
1516
|
+
if (line.match(sectionRegex)) {
|
|
1517
|
+
inSection = true;
|
|
1518
|
+
} else if (inSection && line.match(/^\w+:/) && !line.startsWith(' ')) {
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (inSection) {
|
|
1523
|
+
sectionContent += line + '\n';
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (sectionContent) {
|
|
1528
|
+
newContent = newContent.trimEnd() + '\n\n' + sectionContent;
|
|
1529
|
+
applied.workflow.push(change.key);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
writeFileSync(workflowPath, newContent);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Apply stdrules updates
|
|
1539
|
+
for (const change of upgradeInfo.changes.stdrules) {
|
|
1540
|
+
if (change.type === 'new' || change.type === 'update') {
|
|
1541
|
+
const templatePath = join(templatesDir, 'stdrules', change.file);
|
|
1542
|
+
const projectPath = join(omgkitDir, 'stdrules', change.file);
|
|
1543
|
+
|
|
1544
|
+
if (existsSync(templatePath)) {
|
|
1545
|
+
cpSync(templatePath, projectPath);
|
|
1546
|
+
applied.stdrules.push(change.file);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Apply new files
|
|
1552
|
+
for (const file of upgradeInfo.changes.newFiles) {
|
|
1553
|
+
if (file === 'workflow.yaml') {
|
|
1554
|
+
const templatePath = join(templatesDir, 'omgkit', 'workflow.yaml');
|
|
1555
|
+
const projectPath = join(omgkitDir, 'workflow.yaml');
|
|
1556
|
+
if (existsSync(templatePath)) {
|
|
1557
|
+
cpSync(templatePath, projectPath);
|
|
1558
|
+
applied.newFiles.push(file);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Update settings.json
|
|
1564
|
+
const settingsPath = join(omgkitDir, 'settings.json');
|
|
1565
|
+
let settings = getProjectSettings(cwd) || {};
|
|
1566
|
+
|
|
1567
|
+
// Add omgkit version tracking
|
|
1568
|
+
if (!settings.omgkit) {
|
|
1569
|
+
settings.omgkit = {};
|
|
1570
|
+
}
|
|
1571
|
+
settings.omgkit.version = upgradeInfo.targetVersion;
|
|
1572
|
+
settings.omgkit.last_upgraded = new Date().toISOString().split('T')[0];
|
|
1573
|
+
|
|
1574
|
+
// Initialize checksums if not present
|
|
1575
|
+
if (!settings.file_checksums) {
|
|
1576
|
+
settings.file_checksums = {};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Update checksums for stdrules files
|
|
1580
|
+
const stdrulesDir = join(omgkitDir, 'stdrules');
|
|
1581
|
+
if (existsSync(stdrulesDir)) {
|
|
1582
|
+
const files = readdirSync(stdrulesDir).filter(f => f.endsWith('.md'));
|
|
1583
|
+
for (const file of files) {
|
|
1584
|
+
const content = readFileSync(join(stdrulesDir, file), 'utf8');
|
|
1585
|
+
settings.file_checksums[`stdrules/${file}`] = hashContent(content);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1590
|
+
applied.settings.push('version', 'checksums');
|
|
1591
|
+
|
|
1592
|
+
} catch (e) {
|
|
1593
|
+
// Rollback on error
|
|
1594
|
+
if (!silent) {
|
|
1595
|
+
log.error(`Upgrade failed: ${e.message}`);
|
|
1596
|
+
log.info('Rolling back...');
|
|
1597
|
+
}
|
|
1598
|
+
rollbackProject({ cwd, backupPath: backup.path, silent: true });
|
|
1599
|
+
if (!silent) log.success('Rolled back to previous state');
|
|
1600
|
+
return { success: false, error: e.message, rolledBack: true };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (!silent) {
|
|
1604
|
+
console.log(`
|
|
1605
|
+
${COLORS.green}✓ Project upgraded successfully!${COLORS.reset}
|
|
1606
|
+
|
|
1607
|
+
${COLORS.bright}Applied changes:${COLORS.reset}`);
|
|
1608
|
+
|
|
1609
|
+
if (applied.workflow.length > 0) {
|
|
1610
|
+
console.log(` workflow.yaml: Added ${applied.workflow.join(', ')}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (applied.stdrules.length > 0) {
|
|
1613
|
+
console.log(` stdrules/: Updated ${applied.stdrules.join(', ')}`);
|
|
1614
|
+
}
|
|
1615
|
+
if (applied.newFiles.length > 0) {
|
|
1616
|
+
console.log(` New files: ${applied.newFiles.join(', ')}`);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
console.log(`
|
|
1620
|
+
${COLORS.bright}Backup location:${COLORS.reset} ${backup.path}
|
|
1621
|
+
|
|
1622
|
+
To rollback: omgkit project:rollback
|
|
1623
|
+
|
|
1624
|
+
${COLORS.magenta}🔮 Think Omega. Build Omega. Be Omega.${COLORS.reset}
|
|
1625
|
+
`);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return {
|
|
1629
|
+
success: true,
|
|
1630
|
+
previousVersion: upgradeInfo.currentVersion,
|
|
1631
|
+
newVersion: upgradeInfo.targetVersion,
|
|
1632
|
+
applied,
|
|
1633
|
+
backupPath: backup.path
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// =============================================================================
|
|
1638
|
+
// WORKFLOW CONFIG MANAGEMENT
|
|
1639
|
+
// =============================================================================
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Get nested value from object using dot notation
|
|
1643
|
+
* @param {Object} obj - Object to get value from
|
|
1644
|
+
* @param {string} path - Dot-separated path (e.g., 'testing.enforcement.level')
|
|
1645
|
+
* @returns {*} Value at path or undefined
|
|
1646
|
+
*/
|
|
1647
|
+
function getNestedValue(obj, path) {
|
|
1648
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Set nested value in object using dot notation
|
|
1653
|
+
* @param {Object} obj - Object to set value in
|
|
1654
|
+
* @param {string} path - Dot-separated path
|
|
1655
|
+
* @param {*} value - Value to set
|
|
1656
|
+
*/
|
|
1657
|
+
function setNestedValue(obj, path, value) {
|
|
1658
|
+
const keys = path.split('.');
|
|
1659
|
+
const lastKey = keys.pop();
|
|
1660
|
+
const target = keys.reduce((current, key) => {
|
|
1661
|
+
if (!(key in current)) current[key] = {};
|
|
1662
|
+
return current[key];
|
|
1663
|
+
}, obj);
|
|
1664
|
+
target[lastKey] = value;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Delete nested key from object using dot notation
|
|
1669
|
+
* @param {Object} obj - Object to delete from
|
|
1670
|
+
* @param {string} path - Dot-separated path
|
|
1671
|
+
*/
|
|
1672
|
+
function deleteNestedValue(obj, path) {
|
|
1673
|
+
const keys = path.split('.');
|
|
1674
|
+
const lastKey = keys.pop();
|
|
1675
|
+
const target = keys.reduce((current, key) => current?.[key], obj);
|
|
1676
|
+
if (target && lastKey in target) {
|
|
1677
|
+
delete target[lastKey];
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Parse value string to appropriate type
|
|
1683
|
+
* @param {string} value - Value string
|
|
1684
|
+
* @returns {*} Parsed value
|
|
1685
|
+
*/
|
|
1686
|
+
function parseConfigValue(value) {
|
|
1687
|
+
// Boolean
|
|
1688
|
+
if (value === 'true') return true;
|
|
1689
|
+
if (value === 'false') return false;
|
|
1690
|
+
|
|
1691
|
+
// Number
|
|
1692
|
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
|
1693
|
+
if (/^\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
1694
|
+
|
|
1695
|
+
// String (arrays should be edited directly in workflow.yaml)
|
|
1696
|
+
return value;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Read workflow.yaml config
|
|
1701
|
+
* @param {string} cwd - Project directory
|
|
1702
|
+
* @returns {Object|null} Config object or null
|
|
1703
|
+
*/
|
|
1704
|
+
export function readWorkflowConfig(cwd = process.cwd()) {
|
|
1705
|
+
const workflowPath = join(cwd, '.omgkit', 'workflow.yaml');
|
|
1706
|
+
if (!existsSync(workflowPath)) return null;
|
|
1707
|
+
|
|
1708
|
+
try {
|
|
1709
|
+
const content = readFileSync(workflowPath, 'utf8');
|
|
1710
|
+
return parseSimpleYaml(content);
|
|
1711
|
+
} catch (e) {
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Write workflow.yaml config
|
|
1718
|
+
* @param {Object} config - Config object
|
|
1719
|
+
* @param {string} cwd - Project directory
|
|
1720
|
+
*/
|
|
1721
|
+
function writeWorkflowConfig(config, cwd = process.cwd()) {
|
|
1722
|
+
const workflowPath = join(cwd, '.omgkit', 'workflow.yaml');
|
|
1723
|
+
const yaml = toSimpleYaml(config);
|
|
1724
|
+
writeFileSync(workflowPath, yaml);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Get a config value from workflow.yaml
|
|
1729
|
+
* @param {string} key - Dot-separated key (e.g., 'testing.enforcement.level')
|
|
1730
|
+
* @param {Object} options - Options
|
|
1731
|
+
* @returns {Object} Result with success and value
|
|
1732
|
+
*/
|
|
1733
|
+
export function getConfig(key, options = {}) {
|
|
1734
|
+
const { cwd = process.cwd(), silent = false } = options;
|
|
1735
|
+
|
|
1736
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1737
|
+
if (!existsSync(omgkitDir)) {
|
|
1738
|
+
if (!silent) log.error('Not an OMGKIT project. Run: omgkit init');
|
|
1739
|
+
return { success: false, error: 'NOT_INITIALIZED' };
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const config = readWorkflowConfig(cwd);
|
|
1743
|
+
if (!config) {
|
|
1744
|
+
if (!silent) log.error('workflow.yaml not found');
|
|
1745
|
+
return { success: false, error: 'NO_WORKFLOW' };
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const value = getNestedValue(config, key);
|
|
1749
|
+
|
|
1750
|
+
if (value === undefined) {
|
|
1751
|
+
if (!silent) log.warn(`Key '${key}' not found`);
|
|
1752
|
+
return { success: false, error: 'KEY_NOT_FOUND' };
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (!silent) {
|
|
1756
|
+
if (typeof value === 'object') {
|
|
1757
|
+
console.log(`${COLORS.cyan}${key}:${COLORS.reset}`);
|
|
1758
|
+
console.log(toSimpleYaml(value).split('\n').map(l => ' ' + l).join('\n'));
|
|
1759
|
+
} else {
|
|
1760
|
+
console.log(`${COLORS.cyan}${key}${COLORS.reset} = ${COLORS.green}${value}${COLORS.reset}`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
return { success: true, value };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Set a config value in workflow.yaml
|
|
1769
|
+
* @param {string} key - Dot-separated key
|
|
1770
|
+
* @param {string} value - Value to set
|
|
1771
|
+
* @param {Object} options - Options
|
|
1772
|
+
* @returns {Object} Result
|
|
1773
|
+
*/
|
|
1774
|
+
export function setConfig(key, value, options = {}) {
|
|
1775
|
+
const { cwd = process.cwd(), silent = false } = options;
|
|
1776
|
+
|
|
1777
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1778
|
+
if (!existsSync(omgkitDir)) {
|
|
1779
|
+
if (!silent) log.error('Not an OMGKIT project. Run: omgkit init');
|
|
1780
|
+
return { success: false, error: 'NOT_INITIALIZED' };
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
let config = readWorkflowConfig(cwd);
|
|
1784
|
+
if (!config) {
|
|
1785
|
+
if (!silent) log.error('workflow.yaml not found. Run: omgkit project:upgrade');
|
|
1786
|
+
return { success: false, error: 'NO_WORKFLOW' };
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const parsedValue = parseConfigValue(value);
|
|
1790
|
+
const oldValue = getNestedValue(config, key);
|
|
1791
|
+
|
|
1792
|
+
setNestedValue(config, key, parsedValue);
|
|
1793
|
+
writeWorkflowConfig(config, cwd);
|
|
1794
|
+
|
|
1795
|
+
if (!silent) {
|
|
1796
|
+
if (oldValue !== undefined) {
|
|
1797
|
+
log.success(`Updated: ${key}`);
|
|
1798
|
+
console.log(` ${COLORS.red}${oldValue}${COLORS.reset} → ${COLORS.green}${parsedValue}${COLORS.reset}`);
|
|
1799
|
+
} else {
|
|
1800
|
+
log.success(`Set: ${key} = ${parsedValue}`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
return { success: true, key, value: parsedValue, oldValue };
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* List all config from workflow.yaml
|
|
1809
|
+
* @param {Object} options - Options
|
|
1810
|
+
* @returns {Object} Result with config
|
|
1811
|
+
*/
|
|
1812
|
+
export function listConfig(options = {}) {
|
|
1813
|
+
const { cwd = process.cwd(), silent = false, section = null } = options;
|
|
1814
|
+
|
|
1815
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1816
|
+
if (!existsSync(omgkitDir)) {
|
|
1817
|
+
if (!silent) log.error('Not an OMGKIT project. Run: omgkit init');
|
|
1818
|
+
return { success: false, error: 'NOT_INITIALIZED' };
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const config = readWorkflowConfig(cwd);
|
|
1822
|
+
if (!config) {
|
|
1823
|
+
if (!silent) log.error('workflow.yaml not found');
|
|
1824
|
+
return { success: false, error: 'NO_WORKFLOW' };
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const displayConfig = section ? { [section]: getNestedValue(config, section) } : config;
|
|
1828
|
+
|
|
1829
|
+
if (!silent) {
|
|
1830
|
+
console.log(BANNER);
|
|
1831
|
+
console.log(`${COLORS.bright}Workflow Configuration${COLORS.reset}\n`);
|
|
1832
|
+
console.log(toSimpleYaml(displayConfig));
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
return { success: true, config: displayConfig };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Reset a config key to default value
|
|
1840
|
+
* @param {string} key - Dot-separated key to reset
|
|
1841
|
+
* @param {Object} options - Options
|
|
1842
|
+
* @returns {Object} Result
|
|
1843
|
+
*/
|
|
1844
|
+
export function resetConfig(key, options = {}) {
|
|
1845
|
+
const { cwd = process.cwd(), silent = false } = options;
|
|
1846
|
+
|
|
1847
|
+
const omgkitDir = join(cwd, '.omgkit');
|
|
1848
|
+
if (!existsSync(omgkitDir)) {
|
|
1849
|
+
if (!silent) log.error('Not an OMGKIT project. Run: omgkit init');
|
|
1850
|
+
return { success: false, error: 'NOT_INITIALIZED' };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Load default template
|
|
1854
|
+
const templatePath = join(getPackageRoot(), 'templates', 'omgkit', 'workflow.yaml');
|
|
1855
|
+
if (!existsSync(templatePath)) {
|
|
1856
|
+
if (!silent) log.error('Default template not found');
|
|
1857
|
+
return { success: false, error: 'NO_TEMPLATE' };
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const templateContent = readFileSync(templatePath, 'utf8');
|
|
1861
|
+
const templateConfig = parseSimpleYaml(templateContent);
|
|
1862
|
+
const defaultValue = getNestedValue(templateConfig, key);
|
|
1863
|
+
|
|
1864
|
+
if (defaultValue === undefined) {
|
|
1865
|
+
if (!silent) log.error(`Key '${key}' not found in default config`);
|
|
1866
|
+
return { success: false, error: 'KEY_NOT_IN_DEFAULT' };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
let config = readWorkflowConfig(cwd);
|
|
1870
|
+
if (!config) {
|
|
1871
|
+
if (!silent) log.error('workflow.yaml not found');
|
|
1872
|
+
return { success: false, error: 'NO_WORKFLOW' };
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const oldValue = getNestedValue(config, key);
|
|
1876
|
+
setNestedValue(config, key, defaultValue);
|
|
1877
|
+
writeWorkflowConfig(config, cwd);
|
|
1878
|
+
|
|
1879
|
+
if (!silent) {
|
|
1880
|
+
log.success(`Reset: ${key}`);
|
|
1881
|
+
console.log(` ${COLORS.red}${JSON.stringify(oldValue)}${COLORS.reset} → ${COLORS.green}${JSON.stringify(defaultValue)}${COLORS.reset}`);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
return { success: true, key, value: defaultValue, oldValue };
|
|
1885
|
+
}
|
package/package.json
CHANGED
package/plugin/registry.yaml
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# OMGKIT Component Registry
|
|
2
2
|
# Single Source of Truth for Agents, Skills, Commands, Workflows, and MCPs
|
|
3
|
-
# Version: 2.
|
|
3
|
+
# Version: 2.25.1
|
|
4
4
|
# Updated: 2026-01-06
|
|
5
5
|
|
|
6
|
-
version: "2.
|
|
6
|
+
version: "2.25.1"
|
|
7
7
|
|
|
8
8
|
# =============================================================================
|
|
9
9
|
# OPTIMIZED ALIGNMENT PRINCIPLE (OAP)
|